diff --git a/.coin-or/projDesc.xml b/.coin-or/projDesc.xml index c08006d08e8..073efd968a7 100644 --- a/.coin-or/projDesc.xml +++ b/.coin-or/projDesc.xml @@ -227,8 +227,8 @@ Carl D. Laird, Chair, Pyomo Management Committee, claird at andrew dot cmu dot e Use explicit overrides to disable use of automated version reporting. --> - 6.6.2 - 6.6.2 + 6.7.2 + 6.7.2 @@ -287,7 +287,7 @@ Carl D. Laird, Chair, Pyomo Management Committee, claird at andrew dot cmu dot e Any - Python 3.8, 3.9, 3.10, 3.11 + Python 3.8, 3.9, 3.10, 3.11, 3.12 diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 6d3e6401c5b..36b9898397f 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -43,4 +43,6 @@ ed13c8c65d6c3f56973887744be1c62a5d4756de 0d93f98aa608f892df404ad8015885d26e09bb55 63a3c602a00a2b747fc308c0571bbe33e55a3731 363a16a609f519b3edfdfcf40c66d6de7ac135af +d024718991455519e09149ede53a38df1c067abe +017e21ee50d98d8b2f2083e6880f030025ed5378 diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index da183eebfe2..932b0d8eea6 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -15,52 +15,94 @@ concurrency: cancel-in-progress: true env: - PYOMO_SETUP_ARGS: --with-distributable-extensions + PYOMO_SETUP_ARGS: "--with-cython --with-distributable-extensions" jobs: - manylinux: - name: ${{ matrix.TARGET }}/${{ matrix.wheel-version }}_wheel_creation + native_wheels: + name: Build wheels (${{ matrix.wheel-version }}) on ${{ matrix.os }} for native and cross-compiled architecture runs-on: ${{ matrix.os }} strategy: - fail-fast: false + fail-fast: true matrix: - wheel-version: ['cp38-cp38', 'cp39-cp39', 'cp310-cp310', 'cp311-cp311'] - os: [ubuntu-latest] + os: [ubuntu-22.04, windows-latest, macos-latest] + arch: [all] + wheel-version: ['cp38*', 'cp39*', 'cp310*', 'cp311*', 'cp312*'] + include: - - os: ubuntu-latest - TARGET: manylinux - python-version: [3.8] + - wheel-version: 'cp38*' + TARGET: 'py38' + - wheel-version: 'cp39*' + TARGET: 'py39' + - wheel-version: 'cp310*' + TARGET: 'py310' + - wheel-version: 'cp311*' + TARGET: 'py311' + - wheel-version: 'cp312*' + TARGET: 'py312' steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install twine wheel setuptools pybind11 - # TODO: Update the manylinux builder to next tagged release - - name: Build manylinux Python wheels - uses: RalfG/python-wheels-manylinux-build@a1e012c58ed3960f81b7ed2759a037fb0ad28e2d - with: - python-versions: ${{ matrix.wheel-version }} - build-requirements: 'cython pybind11' - package-path: '' - pip-wheel-args: '' - # When locally testing, --no-deps flag is necessary (PyUtilib dependency will trigger an error otherwise) - - name: Consolidate wheels - run: | - sudo test -d dist || mkdir -v dist - sudo find . -name \*.whl | grep -v /dist/ | xargs -n1 -i mv -v "{}" dist/ - - name: Delete linux wheels - run: | - sudo rm -rfv dist/*-linux_x86_64.whl - - name: Upload artifact - uses: actions/upload-artifact@v3 - with: - name: manylinux-wheels - path: dist + - uses: actions/checkout@v4 + - name: Build wheels + uses: pypa/cibuildwheel@v2.16.5 + with: + output-dir: dist + env: + CIBW_ARCHS_LINUX: "native" + CIBW_ARCHS_MACOS: "native arm64" + CIBW_ARCHS_WINDOWS: "native ARM64" + CIBW_SKIP: "*-musllinux*" + CIBW_BUILD: ${{ matrix.wheel-version }} + CIBW_BUILD_VERBOSITY: 1 + CIBW_BEFORE_BUILD: pip install cython pybind11 + CIBW_CONFIG_SETTINGS: '--global-option="--with-cython --with-distributable-extensions"' + - uses: actions/upload-artifact@v4 + with: + name: native_wheels-${{ matrix.os }}-${{ matrix.TARGET }} + path: dist/*.whl + overwrite: true + + alternative_wheels: + name: Build wheels (${{ matrix.wheel-version }}) on ${{ matrix.os }} for aarch64 + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-22.04] + arch: [all] + wheel-version: ['cp38*', 'cp39*', 'cp310*', 'cp311*', 'cp312*'] + + include: + - wheel-version: 'cp38*' + TARGET: 'py38' + - wheel-version: 'cp39*' + TARGET: 'py39' + - wheel-version: 'cp310*' + TARGET: 'py310' + - wheel-version: 'cp311*' + TARGET: 'py311' + - wheel-version: 'cp312*' + TARGET: 'py312' + steps: + - uses: actions/checkout@v4 + - name: Set up QEMU + if: runner.os == 'Linux' + uses: docker/setup-qemu-action@v3 + with: + platforms: all + - name: Build wheels + uses: pypa/cibuildwheel@v2.16.5 + with: + output-dir: dist + env: + CIBW_ARCHS_LINUX: "aarch64" + CIBW_SKIP: "*-musllinux*" + CIBW_BUILD: ${{ matrix.wheel-version }} + CIBW_BUILD_VERBOSITY: 1 + CIBW_BEFORE_BUILD: pip install cython pybind11 + CIBW_CONFIG_SETTINGS: '--global-option="--with-cython --with-distributable-extensions"' + - uses: actions/upload-artifact@v4 + with: + name: alt_wheels-${{ matrix.os }}-${{ matrix.TARGET }} + path: dist/*.whl + overwrite: true generictarball: name: ${{ matrix.TARGET }} @@ -74,9 +116,9 @@ jobs: TARGET: generic_tarball python-version: [3.8] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -87,72 +129,9 @@ jobs: run: | python setup.py --without-cython sdist --format=gztar - name: Upload artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: generictarball path: dist + overwrite: true - osx: - name: ${{ matrix.TARGET }}py${{ matrix.python-version }}/wheel_creation - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [macos-latest] - include: - - os: macos-latest - TARGET: osx - python-version: [ 3.8, 3.9, '3.10', '3.11' ] - steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install twine wheel setuptools cython pybind11 - - name: Build OSX Python wheels - run: | - python setup.py --with-cython --with-distributable-extensions sdist --format=gztar bdist_wheel - - - name: Upload artifact - uses: actions/upload-artifact@v3 - with: - name: osx-wheels - path: dist - - windows: - name: ${{ matrix.TARGET }}py${{ matrix.python-version }}/wheel_creation - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [windows-latest] - include: - - os: windows-latest - TARGET: win - python-version: [ 3.8, 3.9, '3.10', '3.11' ] - steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - shell: pwsh - run: | - $env:PYTHONWARNINGS="ignore::UserWarning" - Invoke-Expression "python -m pip install --upgrade pip" - Invoke-Expression "pip install setuptools twine wheel cython pybind11" - - name: Build Windows Python wheels - shell: pwsh - run: | - $env:PYTHONWARNINGS="ignore::UserWarning" - Invoke-Expression "python setup.py --with-cython --with-distributable-extensions sdist --format=gztar bdist_wheel" - - name: Upload artifact - uses: actions/upload-artifact@v3 - with: - name: win-wheels - path: dist diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index e773587ec85..5063571c65f 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -33,14 +33,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Pyomo source - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.10' - name: Black Formatting Check run: | - pip install black + # Note v24.4.1 fails due to a bug in the parser + pip install 'black!=24.4.1' black . -S -C --check --diff --exclude examples/pyomobook/python-ch/BadIndent.py - name: Spell Check uses: crate-ci/typos@master @@ -56,17 +57,17 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - python: ['3.11'] + python: ['3.12'] other: [""] category: [""] include: - os: ubuntu-latest - python: '3.11' + python: '3.12' TARGET: linux PYENV: pip - - os: macos-latest + - os: macos-13 python: '3.10' TARGET: osx PYENV: pip @@ -75,24 +76,24 @@ jobs: python: 3.9 TARGET: win PYENV: conda - PACKAGES: glpk + PACKAGES: glpk pytest-qt filelock - os: ubuntu-latest - python: 3.9 + python: '3.11' other: /conda skip_doctest: 1 TARGET: linux PYENV: conda - PACKAGES: + PACKAGES: pytest-qt - os: ubuntu-latest - python: 3.8 + python: '3.10' other: /mpi mpi: 3 skip_doctest: 1 TARGET: linux PYENV: conda - PACKAGES: mpi4py + PACKAGES: openmpi mpi4py - os: ubuntu-latest python: '3.10' @@ -112,7 +113,7 @@ jobs: steps: - name: Checkout Pyomo source - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Configure job parameters run: | @@ -134,7 +135,7 @@ jobs: | tr '\n' ' ' | sed 's/ \+/ /g' >> $GITHUB_ENV #- name: Pip package cache - # uses: actions/cache@v3 + # uses: actions/cache@v4 # if: matrix.PYENV == 'pip' # id: pip-cache # with: @@ -142,7 +143,7 @@ jobs: # key: pip-${{env.CACHE_VER}}.0-${{runner.os}}-${{matrix.python}} #- name: OS package cache - # uses: actions/cache@v3 + # uses: actions/cache@v4 # if: matrix.TARGET != 'osx' # id: os-cache # with: @@ -150,7 +151,7 @@ jobs: # key: pkg-${{env.CACHE_VER}}.0-${{runner.os}} - name: TPL package download cache - uses: actions/cache@v3 + uses: actions/cache@v4 if: ${{ ! matrix.slim }} id: download-cache with: @@ -180,7 +181,7 @@ jobs: # Notes: # - install glpk # - pyodbc needs: gcc pkg-config unixodbc freetds - for pkg in bash pkg-config unixodbc freetds glpk; do + for pkg in bash pkg-config unixodbc freetds glpk ginac; do brew list $pkg || brew install $pkg done @@ -192,7 +193,8 @@ jobs: # - install glpk # - ipopt needs: libopenblas-dev gfortran liblapack-dev sudo apt-get -o Dir::Cache=${GITHUB_WORKSPACE}/cache/os \ - install libopenblas-dev gfortran liblapack-dev glpk-utils + install libopenblas-dev gfortran liblapack-dev glpk-utils \ + libginac-dev sudo chmod -R 777 ${GITHUB_WORKSPACE}/cache/os - name: Update Windows @@ -202,17 +204,26 @@ jobs: - name: Set up Python ${{ matrix.python }} if: matrix.PYENV == 'pip' - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - name: Set up Miniconda Python ${{ matrix.python }} if: matrix.PYENV == 'conda' - uses: conda-incubator/setup-miniconda@v2 + uses: conda-incubator/setup-miniconda@v3 with: auto-update-conda: false python-version: ${{ matrix.python }} + # This is necessary for qt (UI) tests; the package utilized here does not + # have support for OSX. + - name: Set up UI testing infrastructure + if: ${{ matrix.TARGET != 'osx' }} + uses: pyvista/setup-headless-display-action@v2 + with: + qt: true + pyvista: false + # GitHub actions is very fragile when it comes to setting up various # Python interpreters, expecially the setup-miniconda interface. # Per the setup-miniconda documentation, it is important to always @@ -235,6 +246,7 @@ jobs: run: | python -c 'import sys;print(sys.executable)' python -m pip install --cache-dir cache/pip --upgrade pip + python -m pip install --cache-dir cache/pip setuptools PYOMO_DEPENDENCIES=`python setup.py dependencies \ --extras "$EXTRAS" | tail -1` PACKAGES="${PYTHON_CORE_PKGS} ${PYTHON_PACKAGES} ${PYOMO_DEPENDENCIES} " @@ -253,11 +265,18 @@ jobs: if test -z "${{matrix.slim}}"; then python -m pip install --cache-dir cache/pip cplex docplex \ || echo "WARNING: CPLEX Community Edition is not available" - python -m pip install --cache-dir cache/pip \ - -i https://pypi.gurobi.com gurobipy \ + python -m pip install --cache-dir cache/pip gurobipy==10.0.3 \ || echo "WARNING: Gurobi is not available" python -m pip install --cache-dir cache/pip xpress \ || echo "WARNING: Xpress Community Edition is not available" + python -m pip install --cache-dir cache/pip maingopy \ + || echo "WARNING: MAiNGO is not available" + if [[ ${{matrix.python}} == pypy* ]]; then + echo "skipping wntr for pypy" + else + python -m pip install wntr \ + || echo "WARNING: WNTR is not available" + fi fi python -c 'import sys; print("PYTHON_EXE=%s" \ % (sys.executable,))' >> $GITHUB_ENV @@ -302,7 +321,7 @@ jobs: fi # HACK: Remove problem packages on conda+Linux if test "${{matrix.TARGET}}" == linux; then - EXCLUDE="casadi numdifftools pint $EXCLUDE" + EXCLUDE="casadi numdifftools $EXCLUDE" fi EXCLUDE=`echo "$EXCLUDE" | xargs` if test -n "$EXCLUDE"; then @@ -317,6 +336,7 @@ jobs: CONDA_DEPENDENCIES="$CONDA_DEPENDENCIES $PKG" fi done + echo "" echo "*** Install Pyomo dependencies ***" # Note: this will fail the build if any installation fails (or # possibly if it outputs messages to stderr) @@ -324,7 +344,7 @@ jobs: if test -z "${{matrix.slim}}"; then PYVER=$(echo "py${{matrix.python}}" | sed 's/\.//g') echo "Installing for $PYVER" - for PKG in 'cplex>=12.10' docplex gurobi xpress cyipopt pymumps scip; do + for PKG in 'cplex>=12.10' docplex 'gurobi=10.0.3' xpress cyipopt pymumps scip; do echo "" echo "*** Install $PKG ***" # conda can literally take an hour to determine that a @@ -339,10 +359,11 @@ jobs: | sed -r 's/\s+/ /g' | cut -d\ -f3) || echo "" if test -n "$_BUILDS"; then _ISPY=$(echo "$_BUILDS" | grep "^py") \ - || echo "No python build detected" - _PYOK=$(echo "$_BUILDS" | grep "^$PYVER") \ - || echo "No python build matching $PYVER detected" + || echo "INFO: No python build detected." + _PYOK=$(echo "$_BUILDS" | grep -E "^($PYVER|pyh)") \ + || echo "INFO: No python build matching $PYVER detected." if test -z "$_ISPY" -o -n "$_PYOK"; then + echo "" echo "... INSTALLING $PKG" conda install -y "$PKG" || _BUILDS="" fi @@ -351,18 +372,6 @@ jobs: echo "WARNING: $PKG is not available" fi done - # TODO: This is a hack to stop test_qt.py from running until we - # can better troubleshoot why it fails on GHA - for QTPACKAGE in qt pyqt; do - # Because conda is insane, removing packages can cause - # unrelated packages to be updated (breaking version - # specifications specified previously, e.g., in - # setup.py). There doesn't appear to be a good - # workaround, so we will just force-remove (recognizing - # that it may break other conda cruft). - conda remove --force-remove $QTPACKAGE \ - || echo "$QTPACKAGE not in this environment" - done fi # Re-try Pyomo (optional) dependencies with pip if test -n "$PYPI_DEPENDENCIES"; then @@ -585,8 +594,8 @@ jobs: echo "COVERAGE_PROCESS_START=$COVERAGE_RC" >> $GITHUB_ENV cp ${GITHUB_WORKSPACE}/.coveragerc ${COVERAGE_RC} echo "data_file=${COVERAGE_BASE}age" >> ${COVERAGE_RC} - SITE_PACKAGES=$($PYTHON_EXE -c "from distutils.sysconfig import \ - get_python_lib; print(get_python_lib())") + SITE_PACKAGES=$($PYTHON_EXE -c \ + "import sysconfig; print(sysconfig.get_path('purelib'))") echo "Python site-packages: $SITE_PACKAGES" echo 'import coverage; coverage.process_startup()' \ > ${SITE_PACKAGES}/run_coverage_at_startup.pth @@ -616,7 +625,7 @@ jobs: $PYTHON_EXE -m pytest -v \ -W ignore::Warning ${{matrix.category}} \ pyomo `pwd`/pyomo-model-libraries \ - `pwd`/examples/pyomobook --junitxml="TEST-pyomo.xml" + `pwd`/examples `pwd`/doc --junitxml="TEST-pyomo.xml" - name: Run Pyomo MPI tests if: matrix.mpi != 0 @@ -626,7 +635,7 @@ jobs: $PYTHON_EXE -c "from pyomo.dataportal.parse_datacmds import \ parse_data_commands; parse_data_commands(data='')" # Note: if we are testing with openmpi, add '--oversubscribe' - mpirun -np ${{matrix.mpi}} pytest -v \ + mpirun -np ${{matrix.mpi}} -oversubscribe pytest -v \ --junit-xml=TEST-pyomo-mpi.xml \ -m "mpi" -W ignore::Warning \ pyomo `pwd`/pyomo-model-libraries @@ -643,7 +652,7 @@ jobs: coverage xml -i - name: Record build artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{github.job}}_${{env.GHA_JOBGROUP}}-${{env.GHA_JOBNAME}} path: | @@ -660,10 +669,10 @@ jobs: timeout-minutes: 10 steps: - name: Checkout Pyomo source - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python 3.8 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 @@ -703,35 +712,35 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest, macos-13, windows-latest] include: - os: ubuntu-latest TARGET: linux - - os: macos-latest + - os: macos-13 TARGET: osx - os: windows-latest TARGET: win steps: - name: Checkout Pyomo source - uses: actions/checkout@v3 + uses: actions/checkout@v4 # We need the source for .codecov.yml and running "coverage xml" #- name: Pip package cache - # uses: actions/cache@v3 + # uses: actions/cache@v4 # id: pip-cache # with: # path: cache/pip # key: pip-${{env.CACHE_VER}}.0-${{runner.os}}-3.8 - name: Download build artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: path: artifacts - name: Set up Python 3.8 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 @@ -826,7 +835,7 @@ jobs: - name: Upload codecov reports if: github.repository_owner == 'Pyomo' || github.ref != 'refs/heads/main' - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: files: coverage.xml token: ${{ secrets.PYOMO_CODECOV_TOKEN }} @@ -838,7 +847,7 @@ jobs: if: | hashFiles('coverage-other.xml') != '' && (github.repository_owner == 'Pyomo' || github.ref != 'refs/heads/main') - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: files: coverage-other.xml token: ${{ secrets.PYOMO_CODECOV_TOKEN }} diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 2885fd107a8..a45fdd54f03 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -7,6 +7,11 @@ on: pull_request: branches: - main + types: + - opened + - reopened + - synchronize + - ready_for_review workflow_dispatch: inputs: git-ref: @@ -34,16 +39,19 @@ jobs: lint: name: lint/style-and-typos runs-on: ubuntu-latest + if: | + contains(github.event.pull_request.title, '[WIP]') != true && !github.event.pull_request.draft steps: - name: Checkout Pyomo source - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.10' - name: Black Formatting Check run: | - pip install black + # Note v24.4.1 fails due to a bug in the parser + pip install 'black!=24.4.1' black . -S -C --check --diff --exclude examples/pyomobook/python-ch/BadIndent.py - name: Spell Check uses: crate-ci/typos@master @@ -59,8 +67,8 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - python: [ 3.8, 3.9, '3.10', '3.11' ] + os: [ubuntu-latest, macos-13, windows-latest] + python: [ 3.8, 3.9, '3.10', '3.11', '3.12' ] other: [""] category: [""] @@ -69,34 +77,34 @@ jobs: TARGET: linux PYENV: pip - - os: macos-latest + - os: macos-13 TARGET: osx PYENV: pip - os: windows-latest TARGET: win PYENV: conda - PACKAGES: glpk + PACKAGES: glpk pytest-qt filelock - os: ubuntu-latest - python: 3.9 + python: '3.11' other: /conda skip_doctest: 1 TARGET: linux PYENV: conda - PACKAGES: + PACKAGES: pytest-qt - os: ubuntu-latest - python: 3.9 + python: '3.10' other: /mpi mpi: 3 skip_doctest: 1 TARGET: linux PYENV: conda - PACKAGES: mpi4py + PACKAGES: openmpi mpi4py - os: ubuntu-latest - python: 3.11 + python: '3.11' other: /singletest category: "-m 'neos or importtest'" skip_doctest: 1 @@ -142,7 +150,7 @@ jobs: steps: - name: Checkout Pyomo source - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Configure job parameters run: | @@ -164,7 +172,7 @@ jobs: | tr '\n' ' ' | sed 's/ \+/ /g' >> $GITHUB_ENV #- name: Pip package cache - # uses: actions/cache@v3 + # uses: actions/cache@v4 # if: matrix.PYENV == 'pip' # id: pip-cache # with: @@ -172,7 +180,7 @@ jobs: # key: pip-${{env.CACHE_VER}}.0-${{runner.os}}-${{matrix.python}} #- name: OS package cache - # uses: actions/cache@v3 + # uses: actions/cache@v4 # if: matrix.TARGET != 'osx' # id: os-cache # with: @@ -180,7 +188,7 @@ jobs: # key: pkg-${{env.CACHE_VER}}.0-${{runner.os}} - name: TPL package download cache - uses: actions/cache@v3 + uses: actions/cache@v4 if: ${{ ! matrix.slim }} id: download-cache with: @@ -210,7 +218,7 @@ jobs: # Notes: # - install glpk # - pyodbc needs: gcc pkg-config unixodbc freetds - for pkg in bash pkg-config unixodbc freetds glpk; do + for pkg in bash pkg-config unixodbc freetds glpk ginac; do brew list $pkg || brew install $pkg done @@ -222,7 +230,8 @@ jobs: # - install glpk # - ipopt needs: libopenblas-dev gfortran liblapack-dev sudo apt-get -o Dir::Cache=${GITHUB_WORKSPACE}/cache/os \ - install libopenblas-dev gfortran liblapack-dev glpk-utils + install libopenblas-dev gfortran liblapack-dev glpk-utils \ + libginac-dev sudo chmod -R 777 ${GITHUB_WORKSPACE}/cache/os - name: Update Windows @@ -232,17 +241,26 @@ jobs: - name: Set up Python ${{ matrix.python }} if: matrix.PYENV == 'pip' - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - name: Set up Miniconda Python ${{ matrix.python }} if: matrix.PYENV == 'conda' - uses: conda-incubator/setup-miniconda@v2 + uses: conda-incubator/setup-miniconda@v3 with: auto-update-conda: false python-version: ${{ matrix.python }} + # This is necessary for qt (UI) tests; the package utilized here does not + # have support for OSX. + - name: Set up UI testing infrastructure + if: ${{ matrix.TARGET != 'osx' }} + uses: pyvista/setup-headless-display-action@v2 + with: + qt: true + pyvista: false + # GitHub actions is very fragile when it comes to setting up various # Python interpreters, expecially the setup-miniconda interface. # Per the setup-miniconda documentation, it is important to always @@ -265,6 +283,7 @@ jobs: run: | python -c 'import sys;print(sys.executable)' python -m pip install --cache-dir cache/pip --upgrade pip + python -m pip install --cache-dir cache/pip setuptools PYOMO_DEPENDENCIES=`python setup.py dependencies \ --extras "$EXTRAS" | tail -1` PACKAGES="${PYTHON_CORE_PKGS} ${PYTHON_PACKAGES} ${PYOMO_DEPENDENCIES} " @@ -283,11 +302,18 @@ jobs: if test -z "${{matrix.slim}}"; then python -m pip install --cache-dir cache/pip cplex docplex \ || echo "WARNING: CPLEX Community Edition is not available" - python -m pip install --cache-dir cache/pip \ - -i https://pypi.gurobi.com gurobipy \ + python -m pip install --cache-dir cache/pip gurobipy==10.0.3 \ || echo "WARNING: Gurobi is not available" python -m pip install --cache-dir cache/pip xpress \ || echo "WARNING: Xpress Community Edition is not available" + python -m pip install --cache-dir cache/pip maingopy \ + || echo "WARNING: MAiNGO is not available" + if [[ ${{matrix.python}} == pypy* ]]; then + echo "skipping wntr for pypy" + else + python -m pip install wntr \ + || echo "WARNING: WNTR is not available" + fi fi python -c 'import sys; print("PYTHON_EXE=%s" \ % (sys.executable,))' >> $GITHUB_ENV @@ -332,7 +358,7 @@ jobs: fi # HACK: Remove problem packages on conda+Linux if test "${{matrix.TARGET}}" == linux; then - EXCLUDE="casadi numdifftools pint $EXCLUDE" + EXCLUDE="casadi numdifftools $EXCLUDE" fi EXCLUDE=`echo "$EXCLUDE" | xargs` if test -n "$EXCLUDE"; then @@ -347,6 +373,7 @@ jobs: CONDA_DEPENDENCIES="$CONDA_DEPENDENCIES $PKG" fi done + echo "" echo "*** Install Pyomo dependencies ***" # Note: this will fail the build if any installation fails (or # possibly if it outputs messages to stderr) @@ -354,7 +381,7 @@ jobs: if test -z "${{matrix.slim}}"; then PYVER=$(echo "py${{matrix.python}}" | sed 's/\.//g') echo "Installing for $PYVER" - for PKG in 'cplex>=12.10' docplex gurobi xpress cyipopt pymumps scip; do + for PKG in 'cplex>=12.10' docplex 'gurobi=10.0.3' xpress cyipopt pymumps scip; do echo "" echo "*** Install $PKG ***" # conda can literally take an hour to determine that a @@ -369,10 +396,11 @@ jobs: | sed -r 's/\s+/ /g' | cut -d\ -f3) || echo "" if test -n "$_BUILDS"; then _ISPY=$(echo "$_BUILDS" | grep "^py") \ - || echo "No python build detected" - _PYOK=$(echo "$_BUILDS" | grep "^$PYVER") \ - || echo "No python build matching $PYVER detected" + || echo "INFO: No python build detected." + _PYOK=$(echo "$_BUILDS" | grep -E "^($PYVER|pyh)") \ + || echo "INFO: No python build matching $PYVER detected." if test -z "$_ISPY" -o -n "$_PYOK"; then + echo "" echo "... INSTALLING $PKG" conda install -y "$PKG" || _BUILDS="" fi @@ -381,18 +409,6 @@ jobs: echo "WARNING: $PKG is not available" fi done - # TODO: This is a hack to stop test_qt.py from running until we - # can better troubleshoot why it fails on GHA - for QTPACKAGE in qt pyqt; do - # Because conda is insane, removing packages can cause - # unrelated packages to be updated (breaking version - # specifications specified previously, e.g., in - # setup.py). There doesn't appear to be a good - # workaround, so we will just force-remove (recognizing - # that it may break other conda cruft). - conda remove --force-remove $QTPACKAGE \ - || echo "$QTPACKAGE not in this environment" - done fi # Re-try Pyomo (optional) dependencies with pip if test -n "$PYPI_DEPENDENCIES"; then @@ -600,7 +616,8 @@ jobs: if: ${{ ! matrix.slim }} shell: bash run: | - $PYTHON_EXE -m pip install --cache-dir cache/pip highspy \ + echo "NOTE: temporarily pinning to highspy pre-release for testing" + $PYTHON_EXE -m pip install --cache-dir cache/pip "highspy>=1.7.1.dev1" \ || echo "WARNING: highspy is not available" - name: Set up coverage tracking @@ -615,8 +632,8 @@ jobs: echo "COVERAGE_PROCESS_START=$COVERAGE_RC" >> $GITHUB_ENV cp ${GITHUB_WORKSPACE}/.coveragerc ${COVERAGE_RC} echo "data_file=${COVERAGE_BASE}age" >> ${COVERAGE_RC} - SITE_PACKAGES=$($PYTHON_EXE -c "from distutils.sysconfig import \ - get_python_lib; print(get_python_lib())") + SITE_PACKAGES=$($PYTHON_EXE -c \ + "import sysconfig; print(sysconfig.get_path('purelib'))") echo "Python site-packages: $SITE_PACKAGES" echo 'import coverage; coverage.process_startup()' \ > ${SITE_PACKAGES}/run_coverage_at_startup.pth @@ -646,7 +663,7 @@ jobs: $PYTHON_EXE -m pytest -v \ -W ignore::Warning ${{matrix.category}} \ pyomo `pwd`/pyomo-model-libraries \ - `pwd`/examples/pyomobook --junitxml="TEST-pyomo.xml" + `pwd`/examples `pwd`/doc --junitxml="TEST-pyomo.xml" - name: Run Pyomo MPI tests if: matrix.mpi != 0 @@ -656,7 +673,7 @@ jobs: $PYTHON_EXE -c "from pyomo.dataportal.parse_datacmds import \ parse_data_commands; parse_data_commands(data='')" # Note: if we are testing with openmpi, add '--oversubscribe' - mpirun -np ${{matrix.mpi}} pytest -v \ + mpirun -np ${{matrix.mpi}} -oversubscribe pytest -v \ --junit-xml=TEST-pyomo-mpi.xml \ -m "mpi" -W ignore::Warning \ pyomo `pwd`/pyomo-model-libraries @@ -673,7 +690,7 @@ jobs: coverage xml -i - name: Record build artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{github.job}}_${{env.GHA_JOBGROUP}}-${{env.GHA_JOBNAME}} path: | @@ -691,10 +708,10 @@ jobs: timeout-minutes: 10 steps: - name: Checkout Pyomo source - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python 3.8 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 @@ -728,41 +745,41 @@ jobs: cover: name: process-coverage-${{ matrix.TARGET }} needs: build - if: always() # run even if a build job fails + if: success() || failure() # run even if a build job fails, but not if cancelled runs-on: ${{ matrix.os }} timeout-minutes: 10 strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest, macos-13, windows-latest] include: - os: ubuntu-latest TARGET: linux - - os: macos-latest + - os: macos-13 TARGET: osx - os: windows-latest TARGET: win steps: - name: Checkout Pyomo source - uses: actions/checkout@v3 + uses: actions/checkout@v4 # We need the source for .codecov.yml and running "coverage xml" #- name: Pip package cache - # uses: actions/cache@v3 + # uses: actions/cache@v4 # id: pip-cache # with: # path: cache/pip # key: pip-${{env.CACHE_VER}}.0-${{runner.os}}-3.8 - name: Download build artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: path: artifacts - name: Set up Python 3.8 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 @@ -857,7 +874,7 @@ jobs: - name: Upload codecov reports if: github.repository_owner == 'Pyomo' || github.ref != 'refs/heads/main' - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: files: coverage.xml token: ${{ secrets.PYOMO_CODECOV_TOKEN }} @@ -869,7 +886,7 @@ jobs: if: | hashFiles('coverage-other.xml') != '' && (github.repository_owner == 'Pyomo' || github.ref != 'refs/heads/main') - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: files: coverage-other.xml token: ${{ secrets.PYOMO_CODECOV_TOKEN }} diff --git a/.github/workflows/typos.toml b/.github/workflows/typos.toml index 23f94fc8afd..7a38164898b 100644 --- a/.github/workflows/typos.toml +++ b/.github/workflows/typos.toml @@ -40,4 +40,31 @@ WRONLY = "WRONLY" Hax = "Hax" # Big Sur Sur = "Sur" +# contrib package named mis and the acronym whence the name comes +mis = "mis" +MIS = "MIS" +# Ignore the shorthand ans for answer +ans = "ans" +# Ignore the keyword arange +arange = "arange" +# Ignore IIS +IIS = "IIS" +iis = "iis" +# Ignore PN +PN = "PN" +# Ignore hd +hd = "hd" +# Ignore opf +opf = "opf" +# Ignore FRE +FRE = "FRE" +# Ignore MCH +MCH = "MCH" +# Ignore RO +ro = "ro" +RO = "RO" +# Ignore EOF - end of file +EOF = "EOF" +# Ignore lst as shorthand for list +lst = "lst" # AS NEEDED: Add More Words Below diff --git a/.gitignore b/.gitignore index 09069552990..638dc70d13e 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,7 @@ gurobi.log # Jupyterhub/Jupyterlab checkpoints .ipynb_checkpoints -cplex.log \ No newline at end of file +cplex.log + +# Mac tracking files +*.DS_Store* diff --git a/.jenkins.sh b/.jenkins.sh index f31fef99377..696847fd92c 100644 --- a/.jenkins.sh +++ b/.jenkins.sh @@ -38,14 +38,11 @@ if test -z "$WORKSPACE"; then export WORKSPACE=`pwd` fi if test -z "$TEST_SUITES"; then - export TEST_SUITES="${WORKSPACE}/pyomo/pyomo ${WORKSPACE}/pyomo-model-libraries ${WORKSPACE}/pyomo/examples/pyomobook" + export TEST_SUITES="${WORKSPACE}/pyomo/pyomo ${WORKSPACE}/pyomo-model-libraries ${WORKSPACE}/pyomo/examples ${WORKSPACE}/pyomo/doc" fi if test -z "$SLIM"; then export VENV_SYSTEM_PACKAGES='--system-site-packages' fi -if test ! -z "$CATEGORY"; then - export PY_CAT="-m $CATEGORY" -fi if test "$WORKSPACE" != "`pwd`"; then echo "ERROR: pwd is not WORKSPACE" @@ -77,7 +74,7 @@ if test -z "$MODE" -o "$MODE" == setup; then source python/bin/activate # Because modules set the PYTHONPATH, we need to make sure that the # virtualenv appears first - LOCAL_SITE_PACKAGES=`python -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())"` + LOCAL_SITE_PACKAGES=`python -c "import sysconfig; print(sysconfig.get_path('purelib'))"` export PYTHONPATH="$LOCAL_SITE_PACKAGES:$PYTHONPATH" # Set up Pyomo checkouts @@ -122,10 +119,23 @@ if test -z "$MODE" -o "$MODE" == setup; then echo "PYOMO_CONFIG_DIR=$PYOMO_CONFIG_DIR" echo "" + # Call Pyomo build scripts to build TPLs that would normally be + # skipped by the pyomo download-extensions / build-extensions + # actions below + if [[ " $CATEGORY " == *" builders "* ]]; then + echo "" + echo "Running local build scripts..." + echo "" + set -x + python pyomo/contrib/simplification/build.py --build-deps || exit 1 + set +x + fi + # Use Pyomo to download & compile binary extensions i=0 while /bin/true; do i=$[$i+1] + echo "" echo "Downloading pyomo extensions (attempt $i)" pyomo download-extensions $PYOMO_DOWNLOAD_ARGS if test $? == 0; then @@ -178,7 +188,7 @@ if test -z "$MODE" -o "$MODE" == test; then python -m pytest -v \ -W ignore::Warning \ --junitxml="TEST-pyomo.xml" \ - $PY_CAT $TEST_SUITES $PYTEST_EXTRA_ARGS + -m "$CATEGORY" $TEST_SUITES $PYTEST_EXTRA_ARGS # Combine the coverage results and upload if test -z "$DISABLE_COVERAGE"; then diff --git a/CHANGELOG.md b/CHANGELOG.md index e0ba1f885cb..11b4ecbf785 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,218 @@ Pyomo CHANGELOG =============== +------------------------------------------------------------------------------- +Pyomo 6.7.2 (9 May 2024) +------------------------------------------------------------------------------- + +- General + - Support config domains with either method or attribute domain_name (#3159) + - Automate TPL callback registrations (#3167) + - Fix type registrations for ExternalFunction arguments (#3168) + - Only modify module path and spec for deferred import modules (#3176) + - Add "mixed" standard form representation (#3201) + - Support "default" dispatchers in `ExitNodeDispatcher` (#3194) + - Redefine objective sense as a proper `IntEnum` (#3224) + - Fix division-by-0 bug in linear walker (#3246) +- Core + - Allow `Var` objects in `LinearExpression.args` (#3189) + - Add type hints to components (#3173) + - Simplify expressions generated by `TemplateSumExpression` (#3196) + - Make component data public classes (#3221, #3253) + - Exploit repeated named expressions in `identify_variables` (#3190) +- Documentation + - NFC: Add link to the HOMOWP companion notebooks (#3195) + - Update installation documentation to include Cython instructions (#3208) + - Add links to the Pyomo Book Springer page (#3211) +- Solver Interfaces + - Fix division by zero error in linear presolve (#3161) + - Subprocess timeout update (#3183) + - Solver Refactor - Bug fixes for various components (#3181, #3214, #3228) + - NLv2: handle presolved independent linear subsystems (#3193) + - Update `LegacySolverWrapper` compatibility with the `pyomo` script (#3202) + - Fix mosek_direct to use putqconk instead of putqcon (#3199) + - Check _skip_trivial_constraints before the constraint body (#3226) + - Fix AMPL solver duplicate funcadd (#3206) + - Disable the use of universal newlines in the ipopt_v2 NL file (#3231) + - NLv2: fix reporting numbers of nonlinear discrete variables (#3238) + - Fix: Get SCIP solving time considering float number with some text (#3234) + - Solver Refactor - Add `gurobi_direct` implementation (#3225) +- Testing + - Update TPL package list due to `contrib.solver` (#3164) + - Set maxDiff=None on the base TestCase class (#3171) + - Testing infrastructure updates (#3175) + - Typos update for March 2024 (#3219) + - Add openmpi to testing environment to resolve issue in mpi4py (#3236, #3239) + - Skip black 24.4.1 due to a bug in the parser (#3247) + - Skip tests on draft and WIP pull requests (#3223) + - Update GHA to grab gurobipy from PyPI (#3254) +- GDP + - Use private_data for all original / transformed component mappings (#3166) + - Fix a bug in gdp.bigm transformation for nested GDPs (#3213) +- Contributed Packages + - APPSI: cmodel: handle non-mutable params in var / constraint bounds (#3182) + - APPSI: Allow APPSI FBBT to handle nested named Expressions (#3185) + - APPSI: Add MAiNGO solver interface (#3165) + - CP: Add SequenceVar and other logical expressions for scheduling (#3227) + - DoE: Bug fixes (#3245) + - iis: Add minimal intractable system infeasibility diagnostics (#3172) + - incidence_analysis: Improve `solve_strongly_connected_components` + performance for models with named expressions (#3186) + - incidence_analysis: Add function to plot incidence graph in + Dulmage-Mendelsohn order (#3207) + - incidence_analysis: Require variables and constraints to be specified + separately in `IncidenceGraphInterface.remove_nodes` (#3212) + - latex_printer: bugfix for set operations / multidimensional sets (#3177) + - MindtPy: Add HiGHS support (#2971) + - MindtPy: Add call_before_subproblem_solve callback (#3251) + - Parmest: New UI using experiment lists (#3160) + - piecewise: Add piecewise linear transformations (#3036) + - preprocessing: bugfix: intersect domains in variable aggregator (#3241) + - PyNumero: Allow CyIpopt to solve problems without objectives (#3163) + - PyNumero: Work around bug in CyIpopt 1.4.0 (#3222) + - PyNumero: Include "inventory" in readme (#3248) + - PyROS: Simplify custom domain validators (#3169) + - PyROS: Fix iteration logging for edge case involving discrete sets (#3170) + - PyROS: Update solver timing system (#3198) + - simplification: expression simplification using GiNaC or SymPy (#3088) + +------------------------------------------------------------------------------- +Pyomo 6.7.1 (21 Feb 2024) +------------------------------------------------------------------------------- + +- General + - Add support for tuples in `ComponentMap`; add `DefaultComponentMap` (#3150) + - Update `Path`, `PathList`, and `IsInstance` Domain Validators (#3144) + - Remove usage of `__all__` (#3142) + - Extend Path and Type Checking Validators of `common.config` (#3140) + - Update Copyright Statements (#3139) + - Update `ExitNodeDispatcher` to better support extensibility (#3125) + - Create contributors data gathering script (#3117) + - Prevent duplicate entries in ConfigDict declaration order (#3116) + - Remove unnecessary `__future__` imports (#3109) + - Import pandas through pyomo.common.dependencies (#3102) + - Update links to workshop slides (#3079) + - Remove incorrect use of identity (is) comparisons (#3061) +- Core + - Add `Block.register_private_data_initializer()` (#3153) + - Generalize the simple_constraint_rule decorator (#3152) + - Fix edge case assigning new numeric types to Var/Param with units (#3151) + - Add private_data to `_BlockData` (#3138) + - IndexComponent create implicit sets as "anonymous" sets (#3075) + - Add `all_different` and `count_if` to the logical expression system (#3058) + - Fix RangeSet.__len__ when defined by floats (#3119) + - Overhaul the `Suffix` component (#3072) + - Enforce expression immutability in `expr.args` (#3099) + - Improve NumPy registration when assigning numpy to Param (#3093) + - Track changes in PyPy behavior introduced in 7.3.14 (#3087) + - Remove automatic numpy import (#3077) + - Fix `range_difference` for Sets with nonzero anchor points (#3063) + - Clarify errors raised by accessing Sets by positional index (#3062) +- Documentation + - Update intersphinx links, remove docs for nonfunctional code (#3155) + - Update MPC documentation and citation (#3148) + - Fix an error in the documentation for LinearExpression (#3090) + - Fix Pyomo.DoE documentation (#3070) + - Fix latex_printer documentation (#3066) +- Solver Interfaces + - Preview release of new solver interfaces as pyomo.contrib.solver + (#3137, #3156) + - Make error msg more explicit wrt different interfaces (#3141) + - NLv2: only raise exception for empty models in the legacy API (#3135) + - Add `to_expr()` to AMPLRepn, fix NLWriterInfo return type (#3095) +- Testing + - Update Release Wheel Builder Action (#3149) + - Actions Version Update: Address node.js deprecations (#3118) + - New Black Major Release (24.1.0) (#3108) + - Use scip for PyROS tests (#3104) + - Add missing solver dependency flags for OnlineDocs tests (#3094) + - Re-enable `contrib.viewer.tests.test_qt.py` (#3085) + - Add automated testing of OnlineDocs examples (#3080) + - Silence deprecation warnings emitted by Pyomo tests (#3076) + - Fix Python 3.12 tests (manage `pyutilib`, `distutils` dependencies) (#3065) +- DAE + - Replace deprecated `numpy.math` alias with standard `math` module (#3074) +- GDP + - Handle nested GDPs correctly in all the transformations (#3145) + - Fix bugs in nested models in gdp.hull transformation (#3143) + - Various bug fixes in gdp.mbigm transformation (#3073) + - Add GDP => MINLP Transformation (#3082) +- Contributed Packages + - GDPopt: Fix lbb solve_data bug (#3133) + - GDPopt: Adding missing import for gdpopt.enumerate (#3105) + - FBBT: Extend `fbbt.ExpressionBoundsVisitor` to handle relational + expressions and Expr_if (#3129) + - incidence_analysis: Method to add an edge in IncidenceGraphInterface (#3120) + - incidence_analysis: Add subgraph method to IncidencegraphInterface (#3122) + - incidence_analysis: Add `ampl_repn` option (#3069) + - incidence_analysis: Update documentation (#3067) + - interior_point: Resolve test failure due to Mumps update (#3114) + - MindtPy: Various bug fixes (#3034) + - PyROS: Update Solver Argument Resolution and Validation Routines (#3126) + - PyROS: Update Subproblem Initialization Routines (#3071) + - PyROS: Fix DR polishing under nominal objective focus (#3060) + +------------------------------------------------------------------------------- +Pyomo 6.7.0 (29 Nov 2023) +------------------------------------------------------------------------------- + +- General + - Remove Python 3.7, add Python 3.12 Support (#3050, #2956) + - Update report_timing() to support context manager API (#3039) + - Add `Preformatted` class for logging preformatted messages (#2998) + - QuadraticRepnVisitor: Improve nonlinear expression expansion (#2997) + - Add `CITATION` file to main repository (#2992) + - Minor typo / formatting fixes (#3010, #2975) +- Core + - Fix exception from interaction of Gurobi, Pint, Dask, and Threading (#3026) + - Fix differentiation of `Expressions` with `native_numeric_types` (#3017) + - Warn for explicit declaration of immutable params with units (#3004) + - Use `SetInitializer` for initializing `Param` domains; reinitializing + `IndexedVar` domains (#3001) + - Ensure templatize_constraint returns an expression (#2983) + - Prevent multiple applications of the scaling transform (#2979) +- Solver Interfaces + - Remove presolve-eliminated variables from named expressions (#3056) + - Improve LP/NL writer determinism (#3054) + - Add "writer" for converting linear models to standard matrix form (#3046) + - NLv2/LPv2: Log which suffix values were skipped at the DEBUG level (#3043) + - NLv2: add linear presolve and general problem scaling support (#3037) + - Adjust mps writer format for integer variable declaration (#2946) + - Fix scip results processing (#3023) + - Fix quadratic objective off-diagonal-terms in cplex_direct interface (#3025) + - Consolidate walker logic in LP/NL representations (#3015) + - LP writer: warn user for ignored suffixes (#2982) + - Update handling of `0*` in linear, quadratic walkers (#2981) +- Testing + - Pin `gurobipy` version for testing to 10.0.3 (#3053) + - Update Performance Plot URL (#3033) + - Track change in Black rules (#3021) + - Resolve build infrastructure errors (with mpi4py, gams, networkx) (#3018) + - Improve GHA conda env package setup (#3013, #2967) + - Update Gurobi license checks in tests (#3011) + - Skip `fileutils` test failure that persists in OSX 12.7 (#3008) + - LINTING: New Version of `crate-ci/typos` (#2987) +- GDP + - Improve Disjunction construction error for invalid types (#3042) + - Adding new walker for compute_bounds_on_expr (#3027) + - Fix bugs in gdp.bound_pretransformation (#2973) + - Fix various bugs in GDP transformations (#3009) + - Add a few more GDP examples (#2932) +- Contributed Packages + - APPSI: Add interface to WNTR (#2902) + - APPSI: Capture HiGHS output when initializing model (#3005) + - APPSI: Fix auto-update when unfixing variable and changing bounds (#2996) + - APPSI: Fix reference bug in HiGHS interface (#2995) + - FBBT: Add new walker for compute_bounds_on_expr (#3027) + - incidence_analysis: Fix bugs with subset ordering and 0 coefficients (#3041) + - incidence_analysis: Update paper reference (#2969) + - latex_printer: Add contrib.latex_printer package (#2984) + - MindtPy: Add support for GreyBox models (#2988) + - parmest: Cleanup examples and tests (#3028) + - PyNumero: Handle evaluation errors in CyIpopt solver (#2994) + - PyROS: Report relative variable shifts in solver logs (#3035) + - PyROS: Update logging system (#2990) + ------------------------------------------------------------------------------- Pyomo 6.6.2 (23 Aug 2023) ------------------------------------------------------------------------------- diff --git a/LICENSE.md b/LICENSE.md index 192d315e4b5..9fd5d9b810c 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,7 +1,7 @@ LICENSE ======= -Copyright (c) 2008-2022 National Technology and Engineering Solutions of +Copyright (c) 2008-2024 National Technology and Engineering Solutions of Sandia, LLC . Under the terms of Contract DE-NA0003525 with National Technology and Engineering Solutions of Sandia, LLC , the U.S. Government retains certain rights in this software. diff --git a/README.md b/README.md index e544d854c71..707f1a06c5a 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ subproblems using Python parallel communication libraries. * [About Pyomo](http://www.pyomo.org/about) * [Download](http://www.pyomo.org/installation/) * [Documentation](http://www.pyomo.org/documentation/) -* [Performance Plots](https://software.sandia.gov/downloads/pub/pyomo/performance/index.html) +* [Performance Plots](https://pyomo.github.io/performance/) Pyomo was formerly released as the Coopr software library. @@ -51,7 +51,7 @@ Pyomo is available under the BSD License - see the Pyomo is currently tested with the following Python implementations: -* CPython: 3.8, 3.9, 3.10, 3.11 +* CPython: 3.8, 3.9, 3.10, 3.11, 3.12 * PyPy: 3.9 _Testing and support policy_: @@ -71,8 +71,11 @@ version, we will remove testing for that Python version. ### Tutorials and Examples -* [Pyomo Workshop Slides](https://software.sandia.gov/downloads/pub/pyomo/Pyomo-Workshop-Summer-2018.pdf) +* [Pyomo — Optimization Modeling in Python](https://link.springer.com/book/10.1007/978-3-030-68928-5) +* [Pyomo Workshop Slides](https://github.com/Pyomo/pyomo-tutorials/blob/main/Pyomo-Workshop-December-2023.pdf) * [Prof. Jeffrey Kantor's Pyomo Cookbook](https://jckantor.github.io/ND-Pyomo-Cookbook/) +* The [companion notebooks](https://mobook.github.io/MO-book/intro.html) + for *Hands-On Mathematical Optimization with Python* * [Pyomo Gallery](https://github.com/Pyomo/PyomoGallery) ### Getting Help @@ -83,7 +86,7 @@ To get help from the Pyomo community ask a question on one of the following: ### Developers -Pyomo development moved to this repository in June, 2016 from +Pyomo development moved to this repository in June 2016 from Sandia National Laboratories. Developer discussions are hosted by [Google Groups](https://groups.google.com/forum/#!forum/pyomo-developers). diff --git a/RELEASE.md b/RELEASE.md index da97ba78701..b0228e53944 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,34 +1,24 @@ -We are pleased to announce the release of Pyomo 6.6.2. +We are pleased to announce the release of Pyomo 6.7.2. Pyomo is a collection of Python software packages that supports a diverse set of optimization capabilities for formulating and analyzing optimization models. -The following are highlights of the 6.0 release series: - - - Improved stability and robustness of core Pyomo code and solver interfaces - - Integration of Boolean variables into GDP - - Integration of NumPy support into the Pyomo expression system - - Implemented a more performant and robust expression generation system - - Implemented a more performant NL file writer (NLv2) - - Implemented a more performant LP file writer (LPv2) - - Applied [PEP8 standards](https://peps.python.org/pep-0008/) throughout the - codebase - - Added support for Python 3.10, 3.11 - - Removed support for Python 3.6 - - Removed the `pyomo check` command +The following are highlights of the 6.7 release series: + + - Added support for Python 3.12 + - Removed support for Python 3.7 + - New writer for converting linear models to matrix form + - Improved handling of nested GDPs + - Redesigned user API for parameter estimation - New packages: - - APPSI (Auto-Persistent Pyomo Solver Interfaces) - - CP (Constraint programming models and solver interfaces) - - DoE (Model based design of experiments) - - External grey box models - - IIS (Standard interface to solver IIS capabilities) - - MPC (Data structures/utils for rolling horizon dynamic optimization) - - piecewise (Modeling with and reformulating multivariate piecewise linear - functions) - - PyROS (Pyomo Robust Optimization Solver) - - Structural model analysis - - Rewrite of the TrustRegion Solver + - iis: new capability for identifying minimal intractable systems + - latex_printer: print Pyomo models to a LaTeX compatible format + - contrib.solver: preview of redesigned solver interfaces + - simplification: simplify Pyomo expressions + - New solver interfaces + - MAiNGO: Mixed-integer nonlinear global optimization + - ...and of course numerous minor bug fixes and performance enhancements A full list of updates and changes is available in the [`CHANGELOG.md`](https://github.com/Pyomo/pyomo/blob/main/CHANGELOG.md). diff --git a/conftest.py b/conftest.py index df5b0f31e59..34b366f9fd6 100644 --- a/conftest.py +++ b/conftest.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -11,6 +11,22 @@ import pytest +_implicit_markers = {'default'} +_extended_implicit_markers = _implicit_markers.union({'solver'}) + + +def pytest_collection_modifyitems(items): + """ + This method will mark any unmarked tests with the implicit marker ('default') + + """ + for item in items: + try: + next(item.iter_markers()) + except StopIteration: + for marker in _implicit_markers: + item.add_marker(getattr(pytest.mark, marker)) + def pytest_runtest_setup(item): """ @@ -32,13 +48,10 @@ def pytest_runtest_setup(item): the default mode; but if solver tests are also marked with an explicit category (e.g., "expensive"), we will skip them. """ - marker = item.iter_markers() solvernames = [mark.args[0] for mark in item.iter_markers(name="solver")] solveroption = item.config.getoption("--solver") markeroption = item.config.getoption("-m") - implicit_markers = ['default'] - extended_implicit_markers = implicit_markers + ['solver'] - item_markers = set(mark.name for mark in marker) + item_markers = set(mark.name for mark in item.iter_markers()) if solveroption: if solveroption not in solvernames: pytest.skip("SKIPPED: Test not marked {!r}".format(solveroption)) @@ -46,9 +59,9 @@ def pytest_runtest_setup(item): elif markeroption: return elif item_markers: - if not set(implicit_markers).issubset( - item_markers - ) and not item_markers.issubset(set(extended_implicit_markers)): + if not _implicit_markers.issubset(item_markers) and not item_markers.issubset( + _extended_implicit_markers + ): pytest.skip('SKIPPED: Only running default, solver, and unmarked tests.') diff --git a/doc/OnlineDocs/Makefile b/doc/OnlineDocs/Makefile index 604903631b2..3625325ef73 100644 --- a/doc/OnlineDocs/Makefile +++ b/doc/OnlineDocs/Makefile @@ -23,4 +23,4 @@ clean clean_tests: @$(SPHINXBUILD) -M clean "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) @echo "Removing *.spy, *.out" @find . -name \*.spy -delete - @find tests -name \*.out -delete + @find src -name \*.out -delete diff --git a/doc/OnlineDocs/advanced_topics/flattener/index.rst b/doc/OnlineDocs/advanced_topics/flattener/index.rst index 377de5233ec..f9dd8ea6abb 100644 --- a/doc/OnlineDocs/advanced_topics/flattener/index.rst +++ b/doc/OnlineDocs/advanced_topics/flattener/index.rst @@ -30,8 +30,9 @@ The ``pyomo.dae.flatten`` module aims to address this use case by providing utilities to generate all components indexed, explicitly or implicitly, by user-provided sets. -**When we say "flatten a model," we mean "generate all components in the model, -preserving all user-specified indexing sets."** +**When we say "flatten a model," we mean "recursively generate all components in +the model," where a component can be indexed only by user-specified indexing +sets (or is not indexed at all)**. Data structures --------------- @@ -42,3 +43,23 @@ Slices are necessary as they can encode "implicit indexing" -- where a component is contained in an indexed block. It is natural to return references to these slices, so they may be accessed and manipulated like any other component. + +Citation +-------- +If you use the ``pyomo.dae.flatten`` module in your research, we would appreciate +you citing the following paper, which gives more detail about the motivation for +and examples of using this functinoality. + +.. code-block:: bibtex + + @article{parker2023mpc, + title = {Model predictive control simulations with block-hierarchical differential-algebraic process models}, + journal = {Journal of Process Control}, + volume = {132}, + pages = {103113}, + year = {2023}, + issn = {0959-1524}, + doi = {https://doi.org/10.1016/j.jprocont.2023.103113}, + url = {https://www.sciencedirect.com/science/article/pii/S0959152423002007}, + author = {Robert B. Parker and Bethany L. Nicholson and John D. Siirola and Lorenz T. Biegler}, + } diff --git a/doc/OnlineDocs/advanced_topics/linearexpression.rst b/doc/OnlineDocs/advanced_topics/linearexpression.rst index a320b66590f..8b43c3fa03a 100644 --- a/doc/OnlineDocs/advanced_topics/linearexpression.rst +++ b/doc/OnlineDocs/advanced_topics/linearexpression.rst @@ -2,7 +2,7 @@ LinearExpression ================ Significant speed -improvements can be obtained using the ``LinearExpression`` object +improvements can sometimes be obtained using the ``LinearExpression`` object when there are long, dense, linear expressions. The arguments are :: @@ -11,7 +11,9 @@ when there are long, dense, linear expressions. The arguments are where the second and third arguments are lists that must be of the same length. Here is a simple example that illustrates the -syntax. This example creates two constraints that are the same: +syntax. This example creates two constraints that are the same; in this +particular case the LinearExpression component would offer very little improvement +because Pyomo would be able to detect that `campe2` is a linear expression: .. doctest:: @@ -38,5 +40,5 @@ syntax. This example creates two constraints that are the same: .. warning:: - The lists that are passed to ``LinearModel`` are not copied, so caution must + The lists that are passed to ``LinearExpression`` are not copied, so caution must be exercised if they are modified after the component is constructed. diff --git a/doc/OnlineDocs/bibliography.rst b/doc/OnlineDocs/bibliography.rst index 6cbb96d3bfb..c12d3f81d8c 100644 --- a/doc/OnlineDocs/bibliography.rst +++ b/doc/OnlineDocs/bibliography.rst @@ -39,6 +39,8 @@ Bibliography John D. Siirola, Jean-Paul Watson, and David L. Woodruff. Pyomo - Optimization Modeling in Python, 3rd Edition. Vol. 67. Springer, 2021. + doi: `10.1007/978-3-030-68928-5 + `_ .. [PyomoJournal] William E. Hart, Jean-Paul Watson, David L. Woodruff. "Pyomo: modeling and solving mathematical programs in diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py index d8939cf61dd..a06ccfbc9bd 100644 --- a/doc/OnlineDocs/conf.py +++ b/doc/OnlineDocs/conf.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + #!/usr/bin/env python3 # -*- coding: utf-8 -*- # @@ -26,12 +37,12 @@ sys.path.insert(0, os.path.abspath('../..')) # -- Rebuild SPY files ---------------------------------------------------- -sys.path.insert(0, os.path.abspath('tests')) +sys.path.insert(0, os.path.abspath('src')) try: print("Regenerating SPY files...") from strip_examples import generate_spy_files - generate_spy_files(os.path.abspath('tests')) + generate_spy_files(os.path.abspath('src')) generate_spy_files( os.path.abspath(os.path.join('library_reference', 'kernel', 'examples')) ) @@ -46,8 +57,8 @@ 'numpy': ('https://numpy.org/doc/stable/', None), 'pandas': ('https://pandas.pydata.org/docs/', None), 'scikit-learn': ('https://scikit-learn.org/stable/', None), - 'scipy': ('https://docs.scipy.org/doc/scipy/reference/', None), - 'Sphinx': ('https://www.sphinx-doc.org/en/stable/', None), + 'scipy': ('https://docs.scipy.org/doc/scipy/', None), + 'Sphinx': ('https://www.sphinx-doc.org/en/master/', None), } # -- General configuration ------------------------------------------------ @@ -72,6 +83,8 @@ 'sphinx.ext.doctest', 'sphinx.ext.todo', 'sphinx_copybutton', + 'enum_tools.autoenum', + 'sphinx.ext.autosectionlabel', #'sphinx.ext.githubpages', ] @@ -259,7 +272,7 @@ def check_output(self, want, got, optionflags): yaml_available, networkx_available, matplotlib_available, pympler_available, dill_available, ) -pint_available = attempt_import('pint', defer_check=False)[1] +pint_available = attempt_import('pint', defer_import=False)[1] from pyomo.contrib.parmest.parmest import parmest_available import pyomo.environ as _pe # (trigger all plugin registrations) diff --git a/doc/OnlineDocs/contributed_packages/doe/doe.rst b/doc/OnlineDocs/contributed_packages/doe/doe.rst index 354a9916e9b..8c22ff7370d 100644 --- a/doc/OnlineDocs/contributed_packages/doe/doe.rst +++ b/doc/OnlineDocs/contributed_packages/doe/doe.rst @@ -266,7 +266,7 @@ It allows users to define any number of design decisions. Heatmaps can be drawn The function ``run_grid_search`` enumerates over the design space, each MBDoE problem accomplished by ``compute_FIM`` method. Therefore, ``run_grid_search`` supports only two modes: ``sequential_finite`` and ``direct_kaug``. -.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_compute_FIM.py +.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_grid_search.py :language: python :pyobject: main @@ -284,7 +284,7 @@ Pyomo.DoE accomplishes gradient-based optimization with the ``stochastic_program This function solves twice: It solves the square version of the MBDoE problem first, and then unfixes the design variables as degree of freedoms and solves again. In this way the optimization problem can be well initialized. -.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_compute_FIM.py +.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_optimize_doe.py :language: python :pyobject: main diff --git a/doc/OnlineDocs/contributed_packages/gdpopt.rst b/doc/OnlineDocs/contributed_packages/gdpopt.rst index 5e5b8ccce5d..d550b0ced76 100644 --- a/doc/OnlineDocs/contributed_packages/gdpopt.rst +++ b/doc/OnlineDocs/contributed_packages/gdpopt.rst @@ -175,7 +175,10 @@ To use the GDPopt-LBB solver, define your Pyomo GDP model as usual: >>> m.djn = Disjunction(expr=[m.y1, m.y2]) Invoke the GDPopt-LBB solver + >>> results = SolverFactory('gdpopt.lbb').solve(m) + WARNING: 09/06/22: The GDPopt LBB algorithm currently has known issues. Please + use the results with caution and report any bugs! >>> print(results) # doctest: +SKIP >>> print(results.solver.status) diff --git a/doc/OnlineDocs/contributed_packages/iis.rst b/doc/OnlineDocs/contributed_packages/iis.rst index 98cb9e30771..fa97c2f8c61 100644 --- a/doc/OnlineDocs/contributed_packages/iis.rst +++ b/doc/OnlineDocs/contributed_packages/iis.rst @@ -1,6 +1,135 @@ +Infeasibility Diagnostics +!!!!!!!!!!!!!!!!!!!!!!!!! + +There are two closely related tools for infeasibility diagnosis: + + - :ref:`Infeasible Irreducible System (IIS) Tool` + - :ref:`Minimal Intractable System finder (MIS) Tool` + +The first simply provides a conduit for solvers that compute an +infeasible irreducible system (e.g., Cplex, Gurobi, or Xpress). The +second provides similar functionality, but uses the ``mis`` package +contributed to Pyomo. + + Infeasible Irreducible System (IIS) Tool ======================================== .. automodule:: pyomo.contrib.iis.iis .. autofunction:: pyomo.contrib.iis.write_iis + +Minimal Intractable System finder (MIS) Tool +============================================ + +The file ``mis.py`` finds sets of actions that each, independently, +would result in feasibility. The zero-tolerance is whatever the +solver uses, so users may want to post-process output if it is going +to be used for analysis. It also computes a minimal intractable system +(which is not guaranteed to be unique). It was written by Ben Knueven +as part of the watertap project (https://github.com/watertap-org/watertap) +and is therefore governed by a license shown +at the top of ``mis.py``. + +The algorithms come from John Chinneck's slides, see: https://www.sce.carleton.ca/faculty/chinneck/docs/CPAIOR07InfeasibilityTutorial.pdf + +Solver +------ + +At the time of this writing, you need to use IPopt even for LPs. + +Quick Start +----------- + +The file ``trivial_mis.py`` is a tiny example listed at the bottom of +this help file, which references a Pyomo model with the Python variable +`m` and has these lines: + +.. code-block:: python + + from pyomo.contrib.mis import compute_infeasibility_explanation + ipopt = pyo.SolverFactory("ipopt") + compute_infeasibility_explanation(m, solver=ipopt) + +.. Note:: + This is done instead of solving the problem. + +.. Note:: + IDAES users can pass ``get_solver()`` imported from ``ideas.core.solvers`` + as the solver. + +Interpreting the Output +----------------------- + +Assuming the dependencies are installed, running ``trivial_mis.py`` +(shown below) will +produce a lot of warnings from IPopt and then meaningful output (using a logger). + +Repair Options +^^^^^^^^^^^^^^ + +This output for the trivial example shows three independent ways that the model could be rendered feasible: + + +.. code-block:: text + + Model Trivial Quad may be infeasible. A feasible solution was found with only the following variable bounds relaxed: + ub of var x[1] by 4.464126126706818e-05 + lb of var x[2] by 0.9999553410114216 + Another feasible solution was found with only the following variable bounds relaxed: + lb of var x[1] by 0.7071067726864677 + ub of var x[2] by 0.41421355687130673 + ub of var y by 0.7071067651855212 + Another feasible solution was found with only the following inequality constraints, equality constraints, and/or variable bounds relaxed: + constraint: c by 0.9999999861866736 + + +Minimal Intractable System (MIS) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This output shows a minimal intractable system: + + +.. code-block:: text + + Computed Minimal Intractable System (MIS)! + Constraints / bounds in MIS: + lb of var x[2] + lb of var x[1] + constraint: c + +Constraints / bounds in guards for stability +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This part of the report is for nonlinear programs (NLPs). + +When we’re trying to reduce the constraint set, for an NLP there may be constraints that when missing cause the solver +to fail in some catastrophic fashion. In this implementation this is interpreted as failing to get a `results` +object back from the call to `solve`. In these cases we keep the constraint in the problem but it’s in the +set of “guard” constraints – we can’t really be sure they’re a source of infeasibility or not, +just that “bad things” happen when they’re not included. + +Perhaps ideally we would put a constraint in the “guard” set if IPopt failed to converge, and only put it in the +MIS if IPopt converged to a point of local infeasibility. However, right now the code generally makes the +assumption that if IPopt fails to converge the subproblem is infeasible, though obviously that is far from the truth. +Hence for difficult NLPs even the “Phase 1” may “fail” – in that when finished the subproblem containing just the +constraints in the elastic filter may be feasible -- because IPopt failed to converge and we assumed that meant the +subproblem was not feasible. + +Dealing with NLPs is far from clean, but that doesn’t mean the tool can’t return useful results even when its assumptions are not satisfied. + +trivial_mis.py +-------------- + +.. code-block:: python + + import pyomo.environ as pyo + m = pyo.ConcreteModel("Trivial Quad") + m.x = pyo.Var([1,2], bounds=(0,1)) + m.y = pyo.Var(bounds=(0, 1)) + m.c = pyo.Constraint(expr=m.x[1] * m.x[2] == -1) + m.d = pyo.Constraint(expr=m.x[1] + m.y >= 1) + + from pyomo.contrib.mis import compute_infeasibility_explanation + ipopt = pyo.SolverFactory("ipopt") + compute_infeasibility_explanation(m, solver=ipopt) diff --git a/doc/OnlineDocs/contributed_packages/index.rst b/doc/OnlineDocs/contributed_packages/index.rst index f893753780e..b1d9cbbad3b 100644 --- a/doc/OnlineDocs/contributed_packages/index.rst +++ b/doc/OnlineDocs/contributed_packages/index.rst @@ -20,6 +20,7 @@ Contributed packages distributed with Pyomo: gdpopt.rst iis.rst incidence/index.rst + latex_printer.rst mindtpy.rst mpc/index.rst multistart.rst diff --git a/doc/OnlineDocs/contributed_packages/latex_printer.rst b/doc/OnlineDocs/contributed_packages/latex_printer.rst new file mode 100644 index 00000000000..ff3f628c0c8 --- /dev/null +++ b/doc/OnlineDocs/contributed_packages/latex_printer.rst @@ -0,0 +1,127 @@ +Latex Printing +============== + +Pyomo models can be printed to a LaTeX compatible format using the ``pyomo.contrib.latex_printer.latex_printer`` function: + +.. autofunction:: pyomo.contrib.latex_printer.latex_printer.latex_printer + +.. note:: + + If operating in a Jupyter Notebook, it may be helpful to use: + + ``from IPython.display import display, Math`` + + ``display(Math(latex_printer(m))`` + +Examples +-------- + +A Model ++++++++ + +.. doctest:: + + >>> import pyomo.environ as pyo + >>> from pyomo.contrib.latex_printer import latex_printer + + >>> m = pyo.ConcreteModel(name = 'basicFormulation') + >>> m.x = pyo.Var() + >>> m.y = pyo.Var() + >>> m.z = pyo.Var() + >>> m.c = pyo.Param(initialize=1.0, mutable=True) + >>> m.objective = pyo.Objective( expr = m.x + m.y + m.z ) + >>> m.constraint_1 = pyo.Constraint(expr = m.x**2 + m.y**2.0 - m.z**2.0 <= m.c ) + + >>> pstr = latex_printer(m) + + +A Constraint +++++++++++++ + +.. doctest:: + + >>> import pyomo.environ as pyo + >>> from pyomo.contrib.latex_printer import latex_printer + + >>> m = pyo.ConcreteModel(name = 'basicFormulation') + >>> m.x = pyo.Var() + >>> m.y = pyo.Var() + + >>> m.constraint_1 = pyo.Constraint(expr = m.x**2 + m.y**2 <= 1.0) + + >>> pstr = latex_printer(m.constraint_1) + +A Constraint with Set Summation ++++++++++++++++++++++++++++++++ + +.. doctest:: + + >>> import pyomo.environ as pyo + >>> from pyomo.contrib.latex_printer import latex_printer + >>> m = pyo.ConcreteModel(name='basicFormulation') + >>> m.I = pyo.Set(initialize=[1, 2, 3, 4, 5]) + >>> m.v = pyo.Var(m.I) + + >>> def ruleMaker(m): return sum(m.v[i] for i in m.I) <= 0 + + >>> m.constraint = pyo.Constraint(rule=ruleMaker) + + >>> pstr = latex_printer(m.constraint) + +Using a ComponentMap to Specify Names ++++++++++++++++++++++++++++++++++++++ + +.. doctest:: + + >>> import pyomo.environ as pyo + >>> from pyomo.contrib.latex_printer import latex_printer + >>> from pyomo.common.collections.component_map import ComponentMap + + >>> m = pyo.ConcreteModel(name='basicFormulation') + >>> m.I = pyo.Set(initialize=[1, 2, 3, 4, 5]) + >>> m.v = pyo.Var(m.I) + + >>> def ruleMaker(m): return sum(m.v[i] for i in m.I) <= 0 + + >>> m.constraint = pyo.Constraint(rule=ruleMaker) + + >>> lcm = ComponentMap() + >>> lcm[m.v] = 'x' + >>> lcm[m.I] = ['\\mathcal{A}',['j','k']] + + >>> pstr = latex_printer(m.constraint, latex_component_map=lcm) + + +An Expression ++++++++++++++ + +.. doctest:: + + >>> import pyomo.environ as pyo + >>> from pyomo.contrib.latex_printer import latex_printer + + >>> m = pyo.ConcreteModel(name = 'basicFormulation') + >>> m.x = pyo.Var() + >>> m.y = pyo.Var() + + >>> m.expression_1 = pyo.Expression(expr = m.x**2 + m.y**2) + + >>> pstr = latex_printer(m.expression_1) + + +A Simple Expression ++++++++++++++++++++ + +.. doctest:: + + >>> import pyomo.environ as pyo + >>> from pyomo.contrib.latex_printer import latex_printer + + >>> m = pyo.ConcreteModel(name = 'basicFormulation') + >>> m.x = pyo.Var() + >>> m.y = pyo.Var() + + >>> pstr = latex_printer(m.x + m.y) + + + diff --git a/doc/OnlineDocs/contributed_packages/mpc/api.rst b/doc/OnlineDocs/contributed_packages/mpc/api.rst new file mode 100644 index 00000000000..2752fea8af6 --- /dev/null +++ b/doc/OnlineDocs/contributed_packages/mpc/api.rst @@ -0,0 +1,10 @@ +.. _mpc_api: + +API Reference +============= + +.. toctree:: + data.rst + conversion.rst + interface.rst + modeling.rst diff --git a/doc/OnlineDocs/contributed_packages/mpc/conversion.rst b/doc/OnlineDocs/contributed_packages/mpc/conversion.rst new file mode 100644 index 00000000000..9d9406edb75 --- /dev/null +++ b/doc/OnlineDocs/contributed_packages/mpc/conversion.rst @@ -0,0 +1,5 @@ +Data Conversion +=============== + +.. automodule:: pyomo.contrib.mpc.data.convert + :members: diff --git a/doc/OnlineDocs/contributed_packages/mpc/data.rst b/doc/OnlineDocs/contributed_packages/mpc/data.rst new file mode 100644 index 00000000000..73cb6543b1e --- /dev/null +++ b/doc/OnlineDocs/contributed_packages/mpc/data.rst @@ -0,0 +1,17 @@ +Data Structures +=============== + +.. automodule:: pyomo.contrib.mpc.data.get_cuid + :members: + +.. automodule:: pyomo.contrib.mpc.data.dynamic_data_base + :members: + +.. automodule:: pyomo.contrib.mpc.data.scalar_data + :members: + +.. automodule:: pyomo.contrib.mpc.data.series_data + :members: + +.. automodule:: pyomo.contrib.mpc.data.interval_data + :members: diff --git a/doc/OnlineDocs/contributed_packages/mpc/index.rst b/doc/OnlineDocs/contributed_packages/mpc/index.rst index b93abf223e2..e512d1a6ef5 100644 --- a/doc/OnlineDocs/contributed_packages/mpc/index.rst +++ b/doc/OnlineDocs/contributed_packages/mpc/index.rst @@ -1,7 +1,7 @@ MPC === -This package contains data structures and utilities for dynamic optimization +Pyomo MPC contains data structures and utilities for dynamic optimization and rolling horizon applications, e.g. model predictive control. .. toctree:: @@ -10,3 +10,23 @@ and rolling horizon applications, e.g. model predictive control. overview.rst examples.rst faq.rst + api.rst + +Citation +-------- + +If you use Pyomo MPC in your research, please cite the following paper: + +.. code-block:: bibtex + + @article{parker2023mpc, + title = {Model predictive control simulations with block-hierarchical differential-algebraic process models}, + journal = {Journal of Process Control}, + volume = {132}, + pages = {103113}, + year = {2023}, + issn = {0959-1524}, + doi = {https://doi.org/10.1016/j.jprocont.2023.103113}, + url = {https://www.sciencedirect.com/science/article/pii/S0959152423002007}, + author = {Robert B. Parker and Bethany L. Nicholson and John D. Siirola and Lorenz T. Biegler}, + } diff --git a/doc/OnlineDocs/contributed_packages/mpc/interface.rst b/doc/OnlineDocs/contributed_packages/mpc/interface.rst new file mode 100644 index 00000000000..eb5bac548fd --- /dev/null +++ b/doc/OnlineDocs/contributed_packages/mpc/interface.rst @@ -0,0 +1,8 @@ +Interfaces +========== + +.. automodule:: pyomo.contrib.mpc.interfaces.model_interface + :members: + +.. automodule:: pyomo.contrib.mpc.interfaces.var_linker + :members: diff --git a/doc/OnlineDocs/contributed_packages/mpc/modeling.rst b/doc/OnlineDocs/contributed_packages/mpc/modeling.rst new file mode 100644 index 00000000000..cbae03161b1 --- /dev/null +++ b/doc/OnlineDocs/contributed_packages/mpc/modeling.rst @@ -0,0 +1,11 @@ +Modeling Components +=================== + +.. automodule:: pyomo.contrib.mpc.modeling.constraints + :members: + +.. automodule:: pyomo.contrib.mpc.modeling.cost_expressions + :members: + +.. automodule:: pyomo.contrib.mpc.modeling.terminal + :members: diff --git a/doc/OnlineDocs/contributed_packages/mpc/overview.rst b/doc/OnlineDocs/contributed_packages/mpc/overview.rst index f5dbe85e523..f3bc7504b59 100644 --- a/doc/OnlineDocs/contributed_packages/mpc/overview.rst +++ b/doc/OnlineDocs/contributed_packages/mpc/overview.rst @@ -189,7 +189,7 @@ a tracking cost expression. >>> m.setpoint_idx = var_set >>> m.tracking_cost = tr_cost >>> m.tracking_cost.pprint() - tracking_cost : Size=6, Index=tracking_cost_index + tracking_cost : Size=6, Index=setpoint_idx*time Key : Expression (0, 0) : (var[0,A] - 0.5)**2 (0, 1) : (var[1,A] - 0.5)**2 diff --git a/doc/OnlineDocs/contributed_packages/parmest/datarec.rst b/doc/OnlineDocs/contributed_packages/parmest/datarec.rst index 6b721377e46..2260450192c 100644 --- a/doc/OnlineDocs/contributed_packages/parmest/datarec.rst +++ b/doc/OnlineDocs/contributed_packages/parmest/datarec.rst @@ -3,56 +3,52 @@ Data Reconciliation ==================== -The method :class:`~pyomo.contrib.parmest.parmest.Estimator.theta_est` -can optionally return model values. This feature can be used to return -reconciled data using a user specified objective. In this case, the list -of variable names the user wants to estimate (theta_names) is set to an -empty list and the objective function is defined to minimize +The optional argument ``return_values`` in :class:`~pyomo.contrib.parmest.parmest.Estimator.theta_est` +can be used for data reconciliation or to return model values based on the specified objective. + +For data reconciliation, the ``m.unknown_parameters`` is empty +and the objective function is defined to minimize measurement to model error. Note that the model used for data reconciliation may differ from the model used for parameter estimation. -The following example illustrates the use of parmest for data -reconciliation. The functions +The functions :class:`~pyomo.contrib.parmest.graphics.grouped_boxplot` or :class:`~pyomo.contrib.parmest.graphics.grouped_violinplot` can be used to visually compare the original and reconciled data. -Here's a stylized code snippet showing how box plots might be created: - -.. doctest:: - :skipif: True - - >>> import pyomo.contrib.parmest.parmest as parmest - >>> pest = parmest.Estimator(model_function, data, [], objective_function) - >>> obj, theta, data_rec = pest.theta_est(return_values=['A', 'B']) - >>> parmest.graphics.grouped_boxplot(data, data_rec) - -Returned Values -^^^^^^^^^^^^^^^ +The following example from the reactor design subdirectory returns reconciled values for experiment outputs +(`ca`, `cb`, `cc`, and `cd`) and then uses those values in +parameter estimation (`k1`, `k2`, and `k3`). -Here's a full program that can be run to see returned values (in this case it -is the response function that is defined in the model file): +.. literalinclude:: ../../../../pyomo/contrib/parmest/examples/reactor_design/datarec_example.py + :language: python + +The following example returns model values from a Pyomo Expression. .. doctest:: :skipif: not ipopt_available or not parmest_available >>> import pandas as pd >>> import pyomo.contrib.parmest.parmest as parmest - >>> from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import rooney_biegler_model - - >>> theta_names = ['asymptote', 'rate_constant'] + >>> from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import RooneyBieglerExperiment + >>> # Generate data >>> data = pd.DataFrame(data=[[1,8.3],[2,10.3],[3,19.0], ... [4,16.0],[5,15.6],[7,19.8]], ... columns=['hour', 'y']) - >>> def SSE(model, data): - ... expr = sum((data.y[i]\ - ... - model.response_function[data.hour[i]])**2 for i in data.index) + >>> # Create an experiment list + >>> exp_list = [] + >>> for i in range(data.shape[0]): + ... exp_list.append(RooneyBieglerExperiment(data.loc[i, :])) + + >>> # Define objective + >>> def SSE(model): + ... expr = (model.experiment_outputs[model.y] + ... - model.response_function[model.experiment_outputs[model.hour]] + ... ) ** 2 ... return expr - >>> pest = parmest.Estimator(rooney_biegler_model, data, theta_names, SSE, - ... solver_options=None) + >>> pest = parmest.Estimator(exp_list, obj_function=SSE, solver_options=None) >>> obj, theta, var_values = pest.theta_est(return_values=['response_function']) >>> #print(var_values) - diff --git a/doc/OnlineDocs/contributed_packages/parmest/driver.rst b/doc/OnlineDocs/contributed_packages/parmest/driver.rst index 28238928b83..5881d2748f9 100644 --- a/doc/OnlineDocs/contributed_packages/parmest/driver.rst +++ b/doc/OnlineDocs/contributed_packages/parmest/driver.rst @@ -4,7 +4,7 @@ Parameter Estimation ================================== Parameter Estimation using parmest requires a Pyomo model, experimental -data which defines multiple scenarios, and a list of parameter names +data which defines multiple scenarios, and parameters (thetas) to estimate. parmest uses Pyomo [PyomoBookII]_ and (optionally) mpi-sppy [mpisppy]_ to solve a two-stage stochastic programming problem, where the experimental data is @@ -36,13 +36,12 @@ which includes the following methods: ~pyomo.contrib.parmest.parmest.Estimator.likelihood_ratio_test ~pyomo.contrib.parmest.parmest.Estimator.leaveNout_bootstrap_test -Additional functions are available in parmest to group data, plot -results, and fit distributions to theta values. +Additional functions are available in parmest to plot +results and fit distributions to theta values. .. autosummary:: :nosignatures: - ~pyomo.contrib.parmest.parmest.group_data ~pyomo.contrib.parmest.graphics.pairwise_plot ~pyomo.contrib.parmest.graphics.grouped_boxplot ~pyomo.contrib.parmest.graphics.grouped_violinplot @@ -58,21 +57,33 @@ Section. .. testsetup:: * :skipif: not __import__('pyomo.contrib.parmest.parmest').contrib.parmest.parmest.parmest_available + # Data import pandas as pd - from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import rooney_biegler_model as model_function - data = pd.DataFrame(data=[[1,8.3],[2,10.3],[3,19.0], - [4,16.0],[5,15.6],[6,19.8]], - columns=['hour', 'y']) - theta_names = ['asymptote', 'rate_constant'] - def objective_function(model, data): - expr = sum((data.y[i] - model.response_function[data.hour[i]])**2 for i in data.index) + data = pd.DataFrame( + data=[[1, 8.3], [2, 10.3], [3, 19.0], + [4, 16.0], [5, 15.6], [7, 19.8]], + columns=['hour', 'y'], + ) + + # Sum of squared error function + def SSE(model): + expr = ( + model.experiment_outputs[model.y] + - model.response_function[model.experiment_outputs[model.hour]] + ) ** 2 return expr + # Create an experiment list + from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import RooneyBieglerExperiment + exp_list = [] + for i in range(data.shape[0]): + exp_list.append(RooneyBieglerExperiment(data.loc[i, :])) + .. doctest:: :skipif: not __import__('pyomo.contrib.parmest.parmest').contrib.parmest.parmest.parmest_available >>> import pyomo.contrib.parmest.parmest as parmest - >>> pest = parmest.Estimator(model_function, data, theta_names, objective_function) + >>> pest = parmest.Estimator(exp_list, obj_function=SSE) Optionally, solver options can be supplied, e.g., @@ -80,66 +91,44 @@ Optionally, solver options can be supplied, e.g., :skipif: not __import__('pyomo.contrib.parmest.parmest').contrib.parmest.parmest.parmest_available >>> solver_options = {"max_iter": 6000} - >>> pest = parmest.Estimator(model_function, data, theta_names, objective_function, solver_options) - - - -Model function --------------- - -The first argument is a function which uses data for a single scenario -to return a populated and initialized Pyomo model for that scenario. - -Parameters that the user would like to estimate can be defined as -**mutable parameters (Pyomo `Param`) or variables (Pyomo `Var`)**. -Within parmest, any parameters that are to be estimated are converted to unfixed variables. -Variables that are to be estimated are also unfixed. - -The model does not have to be specifically written as a -two-stage stochastic programming problem for parmest. -That is, parmest can modify the -objective, see :ref:`ObjFunction` below. - -Data ----- - -The second argument is the data which will be used to populate the Pyomo -model. Supported data formats include: - -* **Pandas Dataframe** where each row is a separate scenario and column - names refer to observed quantities. Pandas DataFrames are easily - stored and read in from csv, excel, or databases, or created directly - in Python. -* **List of Pandas Dataframe** where each entry in the list is a separate scenario. - Dataframes store observed quantities, referenced by index and column. -* **List of dictionaries** where each entry in the list is a separate - scenario and the keys (or nested keys) refer to observed quantities. - Dictionaries are often preferred over DataFrames when using static and - time series data. Dictionaries are easily stored and read in from - json or yaml files, or created directly in Python. -* **List of json file names** where each entry in the list contains a - json file name for a separate scenario. This format is recommended - when using large datasets in parallel computing. - -The data must be compatible with the model function that returns a -populated and initialized Pyomo model for a single scenario. Data can -include multiple entries per variable (time series and/or duplicate -sensors). This information can be included in custom objective -functions, see :ref:`ObjFunction` below. - -Theta names ------------ - -The third argument is a list of parameters or variable names that the user wants to -estimate. The list contains strings with `Param` and/or `Var` names from the Pyomo -model. + >>> pest = parmest.Estimator(exp_list, obj_function=SSE, solver_options=solver_options) + + +List of experiment objects +-------------------------- + +The first argument is a list of experiment objects which is used to +create one labeled model for each expeirment. +The template :class:`~pyomo.contrib.parmest.experiment.Experiment` +can be used to generate a list of experiment objects. + +A labeled Pyomo model ``m`` has the following additional suffixes (Pyomo `Suffix`): + +* ``m.experiment_outputs`` which defines experiment output (Pyomo `Param`, `Var`, or `Expression`) + and their associated data values (float, int). +* ``m.unknown_parameters`` which defines the mutable parameters or variables (Pyomo `Param` or `Var`) + to estimate along with their component unique identifier (Pyomo `ComponentUID`). + Within parmest, any parameters that are to be estimated are converted to unfixed variables. + Variables that are to be estimated are also unfixed. + +The experiment class has one required method: + +* :class:`~pyomo.contrib.parmest.experiment.Experiment.get_labeled_model` which returns the labeled Pyomo model. + Note that the model does not have to be specifically written as a + two-stage stochastic programming problem for parmest. + That is, parmest can modify the + objective, see :ref:`ObjFunction` below. + +Parmest comes with several :ref:`examplesection` that illustrates how to set up the list of experiment objects. +The examples commonly include additional :class:`~pyomo.contrib.parmest.experiment.Experiment` class methods to +create the model, finalize the model, and label the model. The user can customize methods to suit their needs. .. _ObjFunction: Objective function ------------------ -The fourth argument is an optional argument which defines the +The second argument is an optional argument which defines the optimization objective function to use in parameter estimation. If no objective function is specified, the Pyomo model is used "as is" and @@ -150,20 +139,27 @@ stochastic programming problem. If the Pyomo model is not written as a two-stage stochastic programming problem in this format, and/or if the user wants to use an objective that is different than the original model, a custom objective function can be -defined for parameter estimation. The objective function arguments -include `model` and `data` and the objective function returns a Pyomo +defined for parameter estimation. The objective function has a single argument, +which is the model from a single experiment. +The objective function returns a Pyomo expression which is used to define "SecondStageCost". The objective function can be used to customize data points and weights that are used in parameter estimation. +Parmest includes one built in objective function to compute the sum of squared errors ("SSE") between the +``m.experiment_outputs`` model values and data values. + Suggested initialization procedure for parameter estimation problems -------------------------------------------------------------------- To check the quality of initial guess values provided for the fitted parameters, we suggest solving a square instance of the problem prior to solving the parameter estimation problem using the following steps: -1. Create :class:`~pyomo.contrib.parmest.parmest.Estimator` object. To initialize the parameter estimation solve from the square problem solution, set optional argument ``solver_options = {bound_push: 1e-8}``. +1. Create :class:`~pyomo.contrib.parmest.parmest.Estimator` object. To initialize the parameter +estimation solve from the square problem solution, set optional argument ``solver_options = {bound_push: 1e-8}``. -2. Call :class:`~pyomo.contrib.parmest.parmest.Estimator.objective_at_theta` with optional argument ``(initialize_parmest_model=True)``. Different initial guess values for the fitted parameters can be provided using optional argument `theta_values` (**Pandas Dataframe**) +2. Call :class:`~pyomo.contrib.parmest.parmest.Estimator.objective_at_theta` with optional +argument ``(initialize_parmest_model=True)``. Different initial guess values for the fitted +parameters can be provided using optional argument `theta_values` (**Pandas Dataframe**) 3. Solve parameter estimation problem by calling :class:`~pyomo.contrib.parmest.parmest.Estimator.theta_est` diff --git a/doc/OnlineDocs/contributed_packages/parmest/examples.rst b/doc/OnlineDocs/contributed_packages/parmest/examples.rst index 793ff3d0c8d..a59d79dfa2b 100644 --- a/doc/OnlineDocs/contributed_packages/parmest/examples.rst +++ b/doc/OnlineDocs/contributed_packages/parmest/examples.rst @@ -20,7 +20,7 @@ Additional use cases include: * Parameter estimation using mpi4py, the example saves results to a file for later analysis/graphics (semibatch example) -The description below uses the reactor design example. The file +The example below uses the reactor design example. The file **reactor_design.py** includes a function which returns an populated instance of the Pyomo model. Note that the model is defined to maximize `cb` and that `k1`, `k2`, and `k3` are fixed. The _main_ program is diff --git a/doc/OnlineDocs/contributed_packages/parmest/scencreate.rst b/doc/OnlineDocs/contributed_packages/parmest/scencreate.rst index 66d41d4c606..b63ac5893c2 100644 --- a/doc/OnlineDocs/contributed_packages/parmest/scencreate.rst +++ b/doc/OnlineDocs/contributed_packages/parmest/scencreate.rst @@ -18,5 +18,5 @@ scenarios to the screen, accessing them via the ``ScensItator`` a ``print`` :language: python .. note:: - This example may produce an error message your version of Ipopt is not based + This example may produce an error message if your version of Ipopt is not based on a good linear solver. diff --git a/doc/OnlineDocs/contributed_packages/pynumero/backward_compatibility.rst b/doc/OnlineDocs/contributed_packages/pynumero/backward_compatibility.rst new file mode 100644 index 00000000000..036a00bee62 --- /dev/null +++ b/doc/OnlineDocs/contributed_packages/pynumero/backward_compatibility.rst @@ -0,0 +1,14 @@ +Backward Compatibility +====================== + +While PyNumero is a third-party contribution to Pyomo, we intend to maintain +the stability of its core functionality. The core functionality of PyNumero +consists of: + +1. The ``NLP`` API and ``PyomoNLP`` implementation of this API +2. HSL and MUMPS linear solver interfaces +3. ``BlockVector`` and ``BlockMatrix`` classes +4. CyIpopt and SciPy solver interfaces + +Other parts of PyNumero, such as ``ExternalGreyBoxBlock`` and +``ImplicitFunctionSolver``, are experimental and subject to change without notice. diff --git a/doc/OnlineDocs/contributed_packages/pynumero/index.rst b/doc/OnlineDocs/contributed_packages/pynumero/index.rst index 6ff8b29f812..711bb83eb3b 100644 --- a/doc/OnlineDocs/contributed_packages/pynumero/index.rst +++ b/doc/OnlineDocs/contributed_packages/pynumero/index.rst @@ -13,6 +13,7 @@ PyNumero. For more details, see the API documentation (:ref:`pynumero_api`). installation.rst tutorial.rst api.rst + backward_compatibility.rst Developers diff --git a/doc/OnlineDocs/contributed_packages/pyros.rst b/doc/OnlineDocs/contributed_packages/pyros.rst index 4ef57fbf26c..95049eded8a 100644 --- a/doc/OnlineDocs/contributed_packages/pyros.rst +++ b/doc/OnlineDocs/contributed_packages/pyros.rst @@ -142,6 +142,7 @@ PyROS Solver Interface Otherwise, the solution returned is certified to only be robust feasible. + PyROS Uncertainty Sets ----------------------------- Uncertainty sets are represented by subclasses of @@ -518,7 +519,7 @@ correspond to first-stage degrees of freedom. >>> # === Designate which variables correspond to first-stage >>> # and second-stage degrees of freedom === - >>> first_stage_variables =[ + >>> first_stage_variables = [ ... m.x1, m.x2, m.x3, m.x4, m.x5, m.x6, ... m.x19, m.x20, m.x21, m.x22, m.x23, m.x24, m.x31, ... ] @@ -538,12 +539,16 @@ correspond to first-stage degrees of freedom. ... solve_master_globally=True, ... load_solution=False, ... ) - =========================================================================================== - PyROS: Pyomo Robust Optimization Solver ... - =========================================================================================== + ============================================================================== + PyROS: The Pyomo Robust Optimization Solver... ... - INFO: Robust optimal solution identified. Exiting PyROS. - + ------------------------------------------------------------------------------ + Robust optimal solution identified. + ------------------------------------------------------------------------------ + ... + ------------------------------------------------------------------------------ + All done. Exiting PyROS. + ============================================================================== >>> # === Query results === >>> time = results_1.time >>> iterations = results_1.iterations @@ -604,6 +609,8 @@ optional keyword argument ``decision_rule_order`` to the PyROS In this example, we select affine decision rules by setting ``decision_rule_order=1``: +.. _example-two-stg: + .. doctest:: :skipif: not (baron.available() and baron.license_is_valid()) @@ -625,11 +632,16 @@ In this example, we select affine decision rules by setting ... solve_master_globally=True, ... decision_rule_order=1, ... ) - =========================================================================================== - PyROS: Pyomo Robust Optimization Solver ... + ============================================================================== + PyROS: The Pyomo Robust Optimization Solver... ... - INFO: Robust optimal solution identified. Exiting PyROS. - + ------------------------------------------------------------------------------ + Robust optimal solution identified. + ------------------------------------------------------------------------------ + ... + ------------------------------------------------------------------------------ + All done. Exiting PyROS. + ============================================================================== >>> # === Compare final objective to the single-stage solution >>> two_stage_final_objective = round( ... pyo.value(results_2.final_objective_value), @@ -646,6 +658,54 @@ For this example, we notice a ~25% decrease in the final objective value when switching from a static decision rule (no second-stage recourse) to an affine decision rule. + +Specifying Arguments Indirectly Through ``options`` +""""""""""""""""""""""""""""""""""""""""""""""""""" +Like other Pyomo solver interface methods, +:meth:`~pyomo.contrib.pyros.PyROS.solve` +provides support for specifying options indirectly by passing +a keyword argument ``options``, whose value must be a :class:`dict` +mapping names of arguments to :meth:`~pyomo.contrib.pyros.PyROS.solve` +to their desired values. +For example, the ``solve()`` statement in the +:ref:`two-stage problem snippet ` +could have been equivalently written as: + +.. doctest:: + :skipif: not (baron.available() and baron.license_is_valid()) + + >>> results_2 = pyros_solver.solve( + ... model=m, + ... first_stage_variables=first_stage_variables, + ... second_stage_variables=second_stage_variables, + ... uncertain_params=uncertain_parameters, + ... uncertainty_set=box_uncertainty_set, + ... local_solver=local_solver, + ... global_solver=global_solver, + ... options={ + ... "objective_focus": pyros.ObjectiveType.worst_case, + ... "solve_master_globally": True, + ... "decision_rule_order": 1, + ... }, + ... ) + ============================================================================== + PyROS: The Pyomo Robust Optimization Solver... + ... + ------------------------------------------------------------------------------ + Robust optimal solution identified. + ------------------------------------------------------------------------------ + ... + ------------------------------------------------------------------------------ + All done. Exiting PyROS. + ============================================================================== + +In the event an argument is passed directly +by position or keyword, *and* indirectly through ``options``, +an appropriate warning is issued, +and the value passed directly takes precedence over the value +passed through ``options``. + + The Price of Robustness """""""""""""""""""""""" In conjunction with standard Python control flow tools, @@ -712,11 +772,11 @@ For this example, we obtain the following price of robustness results: +==========================================+==============================+=============================+ | 0.00 | 35,837,659.18 | 0.00 % | +------------------------------------------+------------------------------+-----------------------------+ - | 0.10 | 36,135,191.59 | 0.82 % | + | 0.10 | 36,135,182.66 | 0.83 % | +------------------------------------------+------------------------------+-----------------------------+ - | 0.20 | 36,437,979.81 | 1.64 % | + | 0.20 | 36,437,979.81 | 1.68 % | +------------------------------------------+------------------------------+-----------------------------+ - | 0.30 | 43,478,190.92 | 17.57 % | + | 0.30 | 43,478,190.91 | 21.32 % | +------------------------------------------+------------------------------+-----------------------------+ | 0.40 | ``robust_infeasible`` | :math:`\text{-----}` | +------------------------------------------+------------------------------+-----------------------------+ @@ -733,7 +793,286 @@ set size on the robust optimal objective function value and demonstrates the ease of implementing a price of robustness study for a given optimization problem under uncertainty. -.. note:: +PyROS Solver Log Output +------------------------------- + +The PyROS solver log output is controlled through the optional +``progress_logger`` argument, itself cast to +a standard Python logger (:py:class:`logging.Logger`) object +at the outset of a :meth:`~pyomo.contrib.pyros.PyROS.solve` call. +The level of detail of the solver log output +can be adjusted by adjusting the level of the +logger object; see :ref:`the following table `. +Note that by default, ``progress_logger`` is cast to a logger of level +:py:obj:`logging.INFO`. + +We refer the reader to the +:doc:`official Python logging library documentation ` +for customization of Python logger objects; +for a basic tutorial, see the :doc:`logging HOWTO `. + +.. _table-logging-levels: + +.. list-table:: PyROS solver log output at the various standard Python :py:mod:`logging` levels. + :widths: 10 50 + :header-rows: 1 + + * - Logging Level + - Output Messages + * - :py:obj:`logging.ERROR` + - * Information on the subproblem for which an exception was raised + by a subordinate solver + * Details about failure of the PyROS coefficient matching routine + * - :py:obj:`logging.WARNING` + - * Information about a subproblem not solved to an acceptable status + by the user-provided subordinate optimizers + * Invocation of a backup solver for a particular subproblem + * Caution about solution robustness guarantees in event that + user passes ``bypass_global_separation=True`` + * - :py:obj:`logging.INFO` + - * PyROS version, author, and disclaimer information + * Summary of user options + * Breakdown of model component statistics + * Iteration log table + * Termination details: message, timing breakdown, summary of statistics + * - :py:obj:`logging.DEBUG` + - * Termination outcomes and summary of statistics for + every master feasility, master, and DR polishing problem + * Progress updates for the separation procedure + * Separation subproblem initial point infeasibilities + * Summary of separation loop outcomes: performance constraints + violated, uncertain parameter scenario added to the + master problem + * Uncertain parameter scenarios added to the master problem + thus far + +An example of an output log produced through the default PyROS +progress logger is shown in +:ref:`the snippet that follows `. +Observe that the log contains the following information: + + +* **Introductory information** (lines 1--18). + Includes the version number, author + information, (UTC) time at which the solver was invoked, + and, if available, information on the local Git branch and + commit hash. +* **Summary of solver options** (lines 19--38). +* **Preprocessing information** (lines 39--41). + Wall time required for preprocessing + the deterministic model and associated components, + i.e. standardizing model components and adding the decision rule + variables and equations. +* **Model component statistics** (lines 42--58). + Breakdown of model component statistics. + Includes components added by PyROS, such as the decision rule variables + and equations. +* **Iteration log table** (lines 59--69). + Summary information on the problem iterates and subproblem outcomes. + The constituent columns are defined in detail in + :ref:`the table following the snippet `. +* **Termination message** (lines 70--71). Very brief summary of the termination outcome. +* **Timing statistics** (lines 72--88). + Tabulated breakdown of the solver timing statistics, based on a + :class:`pyomo.common.timing.HierarchicalTimer` printout. + The identifiers are as follows: + + * ``main``: Total time elapsed by the solver. + * ``main.dr_polishing``: Total time elapsed by the subordinate solvers + on polishing of the decision rules. + * ``main.global_separation``: Total time elapsed by the subordinate solvers + on global separation subproblems. + * ``main.local_separation``: Total time elapsed by the subordinate solvers + on local separation subproblems. + * ``main.master``: Total time elapsed by the subordinate solvers on + the master problems. + * ``main.master_feasibility``: Total time elapsed by the subordinate solvers + on the master feasibility problems. + * ``main.preprocessing``: Total preprocessing time. + * ``main.other``: Total overhead time. + +* **Termination statistics** (lines 89--94). Summary of statistics related to the + iterate at which PyROS terminates. +* **Exit message** (lines 95--96). + + +.. _solver-log-snippet: + +.. code-block:: text + :caption: PyROS solver output log for the :ref:`two-stage problem example `. + :linenos: + + ============================================================================== + PyROS: The Pyomo Robust Optimization Solver, v1.2.11. + Pyomo version: 6.7.2 + Commit hash: unknown + Invoked at UTC 2024-03-28T00:00:00.000000 + + Developed by: Natalie M. Isenberg (1), Jason A. F. Sherman (1), + John D. Siirola (2), Chrysanthos E. Gounaris (1) + (1) Carnegie Mellon University, Department of Chemical Engineering + (2) Sandia National Laboratories, Center for Computing Research + + The developers gratefully acknowledge support from the U.S. Department + of Energy's Institute for the Design of Advanced Energy Systems (IDAES). + ============================================================================== + ================================= DISCLAIMER ================================= + PyROS is still under development. + Please provide feedback and/or report any issues by creating a ticket at + https://github.com/Pyomo/pyomo/issues/new/choose + ============================================================================== + Solver options: + time_limit=None + keepfiles=False + tee=False + load_solution=True + symbolic_solver_labels=False + objective_focus= + nominal_uncertain_param_vals=[0.13248000000000001, 4.97, 4.97, 1800] + decision_rule_order=1 + solve_master_globally=True + max_iter=-1 + robust_feasibility_tolerance=0.0001 + separation_priority_order={} + progress_logger= + backup_local_solvers=[] + backup_global_solvers=[] + subproblem_file_directory=None + bypass_local_separation=False + bypass_global_separation=False + p_robustness={} + ------------------------------------------------------------------------------ + Preprocessing... + Done preprocessing; required wall time of 0.175s. + ------------------------------------------------------------------------------ + Model statistics: + Number of variables : 62 + Epigraph variable : 1 + First-stage variables : 7 + Second-stage variables : 6 + State variables : 18 + Decision rule variables : 30 + Number of uncertain parameters : 4 + Number of constraints : 81 + Equality constraints : 24 + Coefficient matching constraints : 0 + Decision rule equations : 6 + All other equality constraints : 18 + Inequality constraints : 57 + First-stage inequalities (incl. certain var bounds) : 10 + Performance constraints (incl. var bounds) : 47 + ------------------------------------------------------------------------------ + Itn Objective 1-Stg Shift 2-Stg Shift #CViol Max Viol Wall Time (s) + ------------------------------------------------------------------------------ + 0 3.5838e+07 - - 5 1.8832e+04 1.741 + 1 3.5838e+07 3.5184e-15 3.9404e-15 10 4.2516e+06 3.766 + 2 3.5993e+07 1.8105e-01 7.1406e-01 13 5.2004e+06 6.288 + 3 3.6285e+07 5.1968e-01 7.7753e-01 4 1.7892e+04 8.247 + 4 3.6285e+07 9.1166e-13 1.9702e-15 0 7.1157e-10g 11.456 + ------------------------------------------------------------------------------ + Robust optimal solution identified. + ------------------------------------------------------------------------------ + Timing breakdown: + + Identifier ncalls cumtime percall % + ----------------------------------------------------------- + main 1 11.457 11.457 100.0 + ------------------------------------------------------ + dr_polishing 4 0.682 0.171 6.0 + global_separation 47 1.109 0.024 9.7 + local_separation 235 5.810 0.025 50.7 + master 5 1.353 0.271 11.8 + master_feasibility 4 0.247 0.062 2.2 + preprocessing 1 0.429 0.429 3.7 + other n/a 1.828 n/a 16.0 + ====================================================== + =========================================================== + + ------------------------------------------------------------------------------ + Termination stats: + Iterations : 5 + Solve time (wall s) : 11.457 + Final objective value : 3.6285e+07 + Termination condition : pyrosTerminationCondition.robust_optimal + ------------------------------------------------------------------------------ + All done. Exiting PyROS. + ============================================================================== + + +The iteration log table is designed to provide, in a concise manner, +important information about the progress of the iterative algorithm for +the problem of interest. +The constituent columns are defined in the +:ref:`table that follows `. + +.. _table-iteration-log-columns: + +.. list-table:: PyROS iteration log table columns. + :widths: 10 50 + :header-rows: 1 - Please provide feedback and/or report any problems by opening an issue on - the `Pyomo GitHub page `_. + * - Column Name + - Definition + * - Itn + - Iteration number. + * - Objective + - Master solution objective function value. + If the objective of the deterministic model provided + has a maximization sense, + then the negative of the objective function value is displayed. + Expect this value to trend upward as the iteration number + increases. + If the master problems are solved globally + (by passing ``solve_master_globally=True``), + then after the iteration number exceeds the number of uncertain parameters, + this value should be monotonically nondecreasing + as the iteration number is increased. + A dash ("-") is produced in lieu of a value if the master + problem of the current iteration is not solved successfully. + * - 1-Stg Shift + - Infinity norm of the relative difference between the first-stage + variable vectors of the master solutions of the current + and previous iterations. Expect this value to trend + downward as the iteration number increases. + A dash ("-") is produced in lieu of a value + if the current iteration number is 0, + there are no first-stage variables, + or the master problem of the current iteration is not solved successfully. + * - 2-Stg Shift + - Infinity norm of the relative difference between the second-stage + variable vectors (evaluated subject to the nominal uncertain + parameter realization) of the master solutions of the current + and previous iterations. Expect this value to trend + downward as the iteration number increases. + A dash ("-") is produced in lieu of a value + if the current iteration number is 0, + there are no second-stage variables, + or the master problem of the current iteration is not solved successfully. + * - #CViol + - Number of performance constraints found to be violated during + the separation step of the current iteration. + Unless a custom prioritization of the model's performance constraints + is specified (through the ``separation_priority_order`` argument), + expect this number to trend downward as the iteration number increases. + A "+" is appended if not all of the separation problems + were solved successfully, either due to custom prioritization, a time out, + or an issue encountered by the subordinate optimizers. + A dash ("-") is produced in lieu of a value if the separation + routine is not invoked during the current iteration. + * - Max Viol + - Maximum scaled performance constraint violation. + Expect this value to trend downward as the iteration number increases. + A 'g' is appended to the value if the separation problems were solved + globally during the current iteration. + A dash ("-") is produced in lieu of a value if the separation + routine is not invoked during the current iteration, or if there are + no performance constraints. + * - Wall time (s) + - Total time elapsed by the solver, in seconds, up to the end of the + current iteration. + + +Feedback and Reporting Issues +------------------------------- +Please provide feedback and/or report any problems by opening an issue on +the `Pyomo GitHub page `_. diff --git a/doc/OnlineDocs/contribution_guide.rst b/doc/OnlineDocs/contribution_guide.rst index 10670627546..b98dcc3d014 100644 --- a/doc/OnlineDocs/contribution_guide.rst +++ b/doc/OnlineDocs/contribution_guide.rst @@ -71,6 +71,10 @@ at least 70% coverage of the lines modified in the PR and prefer coverage closer to 90%. We also require that all tests pass before a PR will be merged. +.. note:: + If you are having issues getting tests to pass on your Pull Request, + please tag any of the core developers to ask for help. + The Pyomo main branch provides a Github Actions workflow (configured in the ``.github/`` directory) that will test any changes pushed to a branch with a subset of the complete test harness that includes @@ -82,13 +86,16 @@ This will enable the tests to run automatically with each push to your fork. At any point in the development cycle, a "work in progress" pull request may be opened by including '[WIP]' at the beginning of the PR -title. This allows your code changes to be tested by the full suite of -Pyomo's automatic -testing infrastructure. Any pull requests marked '[WIP]' will not be +title. Any pull requests marked '[WIP]' or draft will not be reviewed or merged by the core development team. However, any '[WIP]' pull request left open for an extended period of time without active development may be marked 'stale' and closed. +.. note:: + Draft and WIP Pull Requests will **NOT** trigger tests. This is an effort to + reduce our CI backlog. Please make use of the provided + branch test suite for evaluating / testing draft functionality. + Python Version Support ++++++++++++++++++++++ diff --git a/doc/OnlineDocs/developer_reference/expressions/design.rst b/doc/OnlineDocs/developer_reference/expressions/design.rst index 9a6d5b9412f..ddecb39ad0c 100644 --- a/doc/OnlineDocs/developer_reference/expressions/design.rst +++ b/doc/OnlineDocs/developer_reference/expressions/design.rst @@ -73,7 +73,7 @@ Expression trees can be categorized in four different ways: These three categories are illustrated with the following example: -.. literalinclude:: ../../tests/expr/design_categories.spy +.. literalinclude:: ../../src/expr/design_categories.spy The following table describes four different simple expressions that consist of a single model component, and it shows how they @@ -107,7 +107,7 @@ Named expressions allow for changes to an expression after it has been constructed. For example, consider the expression ``f`` defined with the :class:`Expression ` component: -.. literalinclude:: ../../tests/expr/design_named_expression.spy +.. literalinclude:: ../../src/expr/design_named_expression.spy Although ``f`` is an immutable expression, whose definition is fixed, a sub-expressions is the named expression ``M.e``. Named @@ -227,7 +227,7 @@ The :data:`linear_expression ` object is a context manager that can be used to declare a linear sum. For example, consider the following two loops: -.. literalinclude:: ../../tests/expr/design_cm1.spy +.. literalinclude:: ../../src/expr/design_cm1.spy The first apparent difference in these loops is that the value of ``s`` is explicitly initialized while ``e`` is initialized when the @@ -250,7 +250,7 @@ construct different expressions with different context declarations. Finally, note that these context managers can be passed into the :attr:`start` method for the :func:`quicksum ` function. For example: -.. literalinclude:: ../../tests/expr/design_cm2.spy +.. literalinclude:: ../../src/expr/design_cm2.spy This sum contains terms for ``M.x[i]`` and ``M.y[i]``. The syntax in this example is not intuitive because the sum is being stored diff --git a/doc/OnlineDocs/developer_reference/expressions/index.rst b/doc/OnlineDocs/developer_reference/expressions/index.rst index 769639d50eb..685fde25173 100644 --- a/doc/OnlineDocs/developer_reference/expressions/index.rst +++ b/doc/OnlineDocs/developer_reference/expressions/index.rst @@ -21,7 +21,7 @@ nodes contain operators. Pyomo relies on so-called magic methods to automate the construction of symbolic expressions. For example, consider an expression ``e`` declared as follows: -.. literalinclude:: ../../tests/expr/index_simple.spy +.. literalinclude:: ../../src/expr/index_simple.spy Python determines that the magic method ``__mul__`` is called on the ``M.v`` object, with the argument ``2``. This method returns diff --git a/doc/OnlineDocs/developer_reference/expressions/managing.rst b/doc/OnlineDocs/developer_reference/expressions/managing.rst index db045e55b6c..a4dd2a51436 100644 --- a/doc/OnlineDocs/developer_reference/expressions/managing.rst +++ b/doc/OnlineDocs/developer_reference/expressions/managing.rst @@ -23,19 +23,21 @@ mimics the Python operations used to construct an expression. The :data:`verbose` flag can be set to :const:`True` to generate a string representation that is a nested functional form. For example: -.. literalinclude:: ../../tests/expr/managing_ex1.spy +.. literalinclude:: ../../src/expr/managing_ex1.spy Labeler and Symbol Map ~~~~~~~~~~~~~~~~~~~~~~ -The string representation used for variables in expression can be customized to -define different label formats. If the :data:`labeler` option is specified, then this -function (or class functor) is used to generate a string label used to represent the variable. Pyomo -defines a variety of labelers in the `pyomo.core.base.label` module. For example, the -:class:`NumericLabeler` defines a functor that can be used to sequentially generate -simple labels with a prefix followed by the variable count: +The string representation used for variables in expression can be +customized to define different label formats. If the :data:`labeler` +option is specified, then this function (or class functor) is used to +generate a string label used to represent the variable. Pyomo defines a +variety of labelers in the `pyomo.core.base.label` module. For example, +the :class:`NumericLabeler` defines a functor that can be used to +sequentially generate simple labels with a prefix followed by the +variable count: -.. literalinclude:: ../../tests/expr/managing_ex2.spy +.. literalinclude:: ../../src/expr/managing_ex2.spy The :data:`smap` option is used to specify a symbol map object (:class:`SymbolMap `), which @@ -46,60 +48,22 @@ variables in different expressions have a consistent label in their associated string representations. -Standardized String Representations -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The :data:`standardize` option can be used to re-order the string -representation to print polynomial terms before nonlinear terms. By -default, :data:`standardize` is :const:`False`, and the string -representation reflects the order in which terms were combined to -form the expression. Pyomo does not guarantee that the string -representation exactly matches the Python expression order, since -some simplification and re-ordering of terms is done automatically to -improve the efficiency of expression generation. But in most cases -the string representation will closely correspond to the -Python expression order. - -If :data:`standardize` is :const:`True`, then the pyomo expression -is processed to identify polynomial terms, and the string representation -consists of the constant and linear terms followed by -an expression that contains other nonlinear terms. For example: - -.. literalinclude:: ../../tests/expr/managing_ex3.spy - Other Ways to Generate String Representations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ There are two other standard ways to generate string representations: -* Call the :func:`__str__` magic method (e.g. using the Python :func:`str()` function. This - calls :func:`expression_to_string ` with - the option :data:`standardize` equal to :const:`True` (see below). - -* Call the :func:`to_string` method on the :class:`ExpressionBase ` class. - This defaults to calling :func:`expression_to_string ` with - the option :data:`standardize` equal to :const:`False` (see below). +* Call the :func:`__str__` magic method (e.g. using the Python + :func:`str()` function. This calls :func:`expression_to_string + `, using the default values for + all arguments. -In practice, we expect at the :func:`__str__` magic method will be -used by most users, and the standardization of the output provides -a consistent ordering of terms that should make it easier to interpret -expressions. +* Call the :func:`to_string` method on the + :class:`ExpressionBase` class. This + calls :func:`expression_to_string + ` and accepts the same arguments. -Cloning Expressions -------------------- - -Expressions are automatically cloned only during certain expression -transformations. Since this can be an expensive operation, the -:data:`clone_counter ` context -manager object is provided to track the number of times the -:func:`clone_expression ` -function is executed. - -For example: - -.. literalinclude:: ../../tests/expr/managing_ex4.spy - Evaluating Expressions ---------------------- @@ -108,21 +72,21 @@ the expression have a value. The :func:`value ` function can be used to walk the expression tree and compute the value of an expression. For example: -.. literalinclude:: ../../tests/expr/managing_ex5.spy +.. literalinclude:: ../../src/expr/managing_ex5.spy Additionally, expressions define the :func:`__call__` method, so the following is another way to compute the value of an expression: -.. literalinclude:: ../../tests/expr/managing_ex6.spy +.. literalinclude:: ../../src/expr/managing_ex6.spy If a parameter or variable is undefined, then the :func:`value ` function and :func:`__call__` method will raise an exception. This exception can be suppressed using the :attr:`exception` option. For example: -.. literalinclude:: ../../tests/expr/managing_ex7.spy +.. literalinclude:: ../../src/expr/managing_ex7.spy -This option is useful in contexts where adding a try block is inconvenient +This option is useful in contexts where adding a try block is inconvenient in your modeling script. .. note:: @@ -141,10 +105,10 @@ Expression transformations sometimes need to find all nodes in an expression tree that are of a given type. Pyomo contains two utility functions that support this functionality. First, the :func:`identify_components ` -function is a generator function that walks the expression tree and yields all +function is a generator function that walks the expression tree and yields all nodes whose type is in a specified set of node types. For example: -.. literalinclude:: ../../tests/expr/managing_ex8.spy +.. literalinclude:: ../../src/expr/managing_ex8.spy The :func:`identify_variables ` function is a generator function that yields all nodes that are @@ -153,7 +117,7 @@ but this set of variable types does not need to be specified by the user. However, the :attr:`include_fixed` flag can be specified to omit fixed variables. For example: -.. literalinclude:: ../../tests/expr/managing_ex9.spy +.. literalinclude:: ../../src/expr/managing_ex9.spy Walking an Expression Tree with a Visitor Class ----------------------------------------------- @@ -166,8 +130,15 @@ is computed using the values of its children. Walking an expression tree can be tricky, and the code requires intimate knowledge of the design of the expression system. Pyomo includes -several classes that define so-called visitor patterns for walking -expression tree: +several classes that define visitor patterns for walking expression +tree: + +:class:`StreamBasedExpressionVisitor ` + The most general and extensible visitor class. This visitor + implements an event-based approach for walking the tree inspired by + the ``expat`` library for processing XML files. The visitor has + seven event callbacks that users can hook into, providing very + fine-grained control over the expression walker. :class:`SimpleExpressionVisitor ` A :func:`visitor` method is called for each node in the tree, @@ -187,33 +158,39 @@ expression tree: These classes define a variety of suitable tree search methods: -* :class:`SimpleExpressionVisitor ` +* :class:`StreamBasedExpressionVisitor ` - * **xbfs**: breadth-first search where leaf nodes are immediately visited - * **xbfs_yield_leaves**: breadth-first search where leaf nodes are immediately visited, and the visit method yields a value + * ``walk_expression``: depth-first traversal of the expression tree. -* :class:`ExpressionValueVisitor ` +* :class:`ExpressionReplacementVisitor ` - * **dfs_postorder_stack**: postorder depth-first search using a stack + * ``walk_expression``: depth-first traversal of the expression tree. -* :class:`ExpressionReplacementVisitor ` +* :class:`SimpleExpressionVisitor ` - * **dfs_postorder_stack**: postorder depth-first search using a stack + * ``xbfs``: breadth-first search where leaf nodes are immediately visited + * ``xbfs_yield_leaves``: breadth-first search where leaf nodes are + immediately visited, and the visit method yields a value -.. note:: +* :class:`ExpressionValueVisitor ` - The PyUtilib visitor classes define several other search methods - that could be used with Pyomo expressions. But these are the - only search methods currently used within Pyomo. + * ``dfs_postorder_stack``: postorder depth-first search using a + nonrecursive stack -To implement a visitor object, a user creates a subclass of one of these -classes. Only one of a few methods will need to be defined to -implement the visitor: + +To implement a visitor object, a user needs to provide specializations +for specific events. For legacy visitors based on the PyUtilib +visitor pattern (e.g., :class:`SimpleExpressionVisitor` and +:class:`ExpressionValueVisitor`), one must create a subclass of one of these +classes and override at least one of the following: :func:`visitor` Defines the operation that is performed when a node is visited. In - the :class:`ExpressionValueVisitor ` and :class:`ExpressionReplacementVisitor ` visitor classes, this - method returns a value that is used by its parent node. + the :class:`ExpressionValueVisitor + ` and + :class:`ExpressionReplacementVisitor + ` visitor classes, + this method returns a value that is used by its parent node. :func:`visiting_potential_leaf` Checks if the search should terminate with this node. If no, @@ -225,9 +202,17 @@ implement the visitor: class. :func:`finalize` - This method defines the final value that is returned from the + This method defines the final value that is returned from the visitor. This is not normally redefined. +For modern visitors based on the :class:`StreamBasedExpressionVisitor +`, one can either define a +subclass, pass the callbacks to an instance of the base class, or assign +the callbacks as attributes on an instance of the base class. The +:class:`StreamBasedExpressionVisitor +` provides seven +callbacks, which are documented in the class documentation. + Detailed documentation of the APIs for these methods is provided with the class documentation for these visitors. @@ -238,14 +223,14 @@ In this example, we describe an visitor class that counts the number of nodes in an expression (including leaf nodes). Consider the following class: -.. literalinclude:: ../../tests/expr/managing_visitor1.spy +.. literalinclude:: ../../src/expr/managing_visitor1.spy -The class constructor creates a counter, and the :func:`visit` method +The class constructor creates a counter, and the :func:`visit` method increments this counter for every node that is visited. The :func:`finalize` method returns the value of this counter after the tree has been walked. The following function illustrates this use of this visitor class: -.. literalinclude:: ../../tests/expr/managing_visitor2.spy +.. literalinclude:: ../../src/expr/managing_visitor2.spy ExpressionValueVisitor Example @@ -255,14 +240,14 @@ In this example, we describe an visitor class that clones the expression tree (including leaf nodes). Consider the following class: -.. literalinclude:: ../../tests/expr/managing_visitor3.spy +.. literalinclude:: ../../src/expr/managing_visitor3.spy The :func:`visit` method creates a new expression node with children specified by :attr:`values`. The :func:`visiting_potential_leaf` method performs a :func:`deepcopy` on leaf nodes, which are native Python types or non-expression objects. -.. literalinclude:: ../../tests/expr/managing_visitor4.spy +.. literalinclude:: ../../src/expr/managing_visitor4.spy ExpressionReplacementVisitor Example @@ -273,18 +258,15 @@ variables with scaled variables, using a mutable parameter that can be modified later. the following class: -.. literalinclude:: ../../tests/expr/managing_visitor5.spy +.. literalinclude:: ../../src/expr/managing_visitor5.spy -No :func:`visit` method needs to be defined. The -:func:`visiting_potential_leaf` function identifies variable nodes +No other method need to be defined. The +:func:`beforeChild` method identifies variable nodes and returns a product expression that contains a mutable parameter. -The :class:`_LinearExpression` class has a different representation -that embeds variables. Hence, this class must be handled -in a separate condition that explicitly transforms this sub-expression. -.. literalinclude:: ../../tests/expr/managing_visitor6.spy +.. literalinclude:: ../../src/expr/managing_visitor6.spy -The :func:`scale_expression` function is called with an expression and +The :func:`scale_expression` function is called with an expression and a dictionary, :attr:`scale`, that maps variable ID to model parameter. For example: -.. literalinclude:: ../../tests/expr/managing_visitor7.spy +.. literalinclude:: ../../src/expr/managing_visitor7.spy diff --git a/doc/OnlineDocs/developer_reference/expressions/overview.rst b/doc/OnlineDocs/developer_reference/expressions/overview.rst index 58808a813f1..c1962edec22 100644 --- a/doc/OnlineDocs/developer_reference/expressions/overview.rst +++ b/doc/OnlineDocs/developer_reference/expressions/overview.rst @@ -50,13 +50,13 @@ are: example, the following two loops had dramatically different runtime: - .. literalinclude:: ../../tests/expr/overview_example1.spy + .. literalinclude:: ../../src/expr/overview_example1.spy * Coopr3 eliminates side effects by automatically cloning sub-expressions. Unfortunately, this can easily lead to unexpected cloning in models, which can dramatically slow down Pyomo model generation. For example: - .. literalinclude:: ../../tests/expr/overview_example2.spy + .. literalinclude:: ../../src/expr/overview_example2.spy * Coopr3 leverages recursion in many operations, including expression cloning. Even simple non-linear expressions can result in deep @@ -82,7 +82,7 @@ control for how expressions are managed in Python. For example: * Python variables can point to the same expression tree - .. literalinclude:: ../../tests/expr/overview_tree1.spy + .. literalinclude:: ../../src/expr/overview_tree1.spy This is illustrated as follows: @@ -102,7 +102,7 @@ control for how expressions are managed in Python. For example: * A variable can point to a sub-tree that another variable points to - .. literalinclude:: ../../tests/expr/overview_tree2.spy + .. literalinclude:: ../../src/expr/overview_tree2.spy This is illustrated as follows: @@ -124,7 +124,7 @@ control for how expressions are managed in Python. For example: * Two expression trees can point to the same sub-tree - .. literalinclude:: ../../tests/expr/overview_tree3.spy + .. literalinclude:: ../../src/expr/overview_tree3.spy This is illustrated as follows: @@ -169,7 +169,7 @@ between expressions, we do not consider those expressions entangled. Expression entanglement is problematic because shared expressions complicate the expected behavior when sub-expressions are changed. Consider the following example: -.. literalinclude:: ../../tests/expr/overview_tree4.spy +.. literalinclude:: ../../src/expr/overview_tree4.spy What is the value of ``e`` after ``M.w`` is added to it? What is the value of ``f``? The answers to these questions are not immediately @@ -244,7 +244,7 @@ There is one important exception to the entanglement property described above. The ``Expression`` component is treated as a mutable expression when shared between expressions. For example: -.. literalinclude:: ../../tests/expr/overview_tree5.spy +.. literalinclude:: ../../src/expr/overview_tree5.spy Here, the expression ``M.e`` is a so-called *named expression* that the user has declared. Named expressions are explicitly intended diff --git a/doc/OnlineDocs/developer_reference/expressions/performance.rst b/doc/OnlineDocs/developer_reference/expressions/performance.rst index 4b2c691b729..8e344e50982 100644 --- a/doc/OnlineDocs/developer_reference/expressions/performance.rst +++ b/doc/OnlineDocs/developer_reference/expressions/performance.rst @@ -11,14 +11,14 @@ Expression Generation Pyomo expressions can be constructed using native binary operators in Python. For example, a sum can be created in a simple loop: -.. literalinclude:: ../../tests/expr/performance_loop1.spy +.. literalinclude:: ../../src/expr/performance_loop1.spy Additionally, Pyomo expressions can be constructed using functions that iteratively apply Python binary operators. For example, the Python :func:`sum` function can be used to replace the previous loop: -.. literalinclude:: ../../tests/expr/performance_loop2.spy +.. literalinclude:: ../../src/expr/performance_loop2.spy The :func:`sum` function is both more compact and more efficient. Using :func:`sum` avoids the creation of temporary variables, and @@ -47,7 +47,7 @@ expressions. For example, consider the following quadratic polynomial: -.. literalinclude:: ../../tests/expr/performance_loop3.spy +.. literalinclude:: ../../src/expr/performance_loop3.spy This quadratic polynomial is treated as a nonlinear expression unless the expression is explicitly processed to identify quadratic @@ -78,7 +78,7 @@ The :func:`prod ` function is analogous to the builtin argument list, :attr:`args`, which represents expressions that are multiplied together. For example: -.. literalinclude:: ../../tests/expr/performance_prod.spy +.. literalinclude:: ../../src/expr/performance_prod.spy quicksum ~~~~~~~~ @@ -89,7 +89,7 @@ generates a more compact Pyomo expression. Its main argument is a variable length argument list, :attr:`args`, which represents expressions that are summed together. For example: -.. literalinclude:: ../../tests/expr/performance_quicksum.spy +.. literalinclude:: ../../src/expr/performance_quicksum.spy The summation is customized based on the :attr:`start` and :attr:`linear` arguments. The :attr:`start` defines the initial @@ -111,13 +111,13 @@ more quickly. Consider the following example: -.. literalinclude:: ../../tests/expr/quicksum_runtime.spy +.. literalinclude:: ../../src/expr/quicksum_runtime.spy The sum consists of linear terms because the exponents are one. The following output illustrates that quicksum can identify this linear structure to generate expressions more quickly: -.. literalinclude:: ../../tests/expr/quicksum.log +.. literalinclude:: ../../src/expr/quicksum.log :language: none If :attr:`start` is not a numeric value, then the :func:`quicksum @@ -134,7 +134,7 @@ to be stored in an object that is passed into the function (e.g. the linear cont term in :attr:`args` is misleading. Consider the following example: - .. literalinclude:: ../../tests/expr/performance_warning.spy + .. literalinclude:: ../../src/expr/performance_warning.spy The first term created by the generator is linear, but the subsequent terms are nonlinear. Pyomo gracefully transitions @@ -153,12 +153,12 @@ calling :func:`quicksum `. If two or more components provided, then the result is the summation of their terms multiplied together. For example: -.. literalinclude:: ../../tests/expr/performance_sum_product1.spy +.. literalinclude:: ../../src/expr/performance_sum_product1.spy The :attr:`denom` argument specifies components whose terms are in the denominator. For example: -.. literalinclude:: ../../tests/expr/performance_sum_product2.spy +.. literalinclude:: ../../src/expr/performance_sum_product2.spy The terms summed by this function are explicitly specified, so :func:`sum_product ` can identify diff --git a/doc/OnlineDocs/developer_reference/future.rst b/doc/OnlineDocs/developer_reference/future.rst new file mode 100644 index 00000000000..531c0fdb5c6 --- /dev/null +++ b/doc/OnlineDocs/developer_reference/future.rst @@ -0,0 +1,3 @@ + +.. automodule:: pyomo.__future__ + :noindex: diff --git a/doc/OnlineDocs/developer_reference/index.rst b/doc/OnlineDocs/developer_reference/index.rst index 8c29150015c..0feb33cdab9 100644 --- a/doc/OnlineDocs/developer_reference/index.rst +++ b/doc/OnlineDocs/developer_reference/index.rst @@ -12,3 +12,5 @@ scripts using Pyomo. config.rst deprecation.rst expressions/index.rst + future.rst + solvers.rst diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst new file mode 100644 index 00000000000..9e3281246f4 --- /dev/null +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -0,0 +1,351 @@ +Future Solver Interface Changes +=============================== + +.. note:: + + The new solver interfaces are still under active development. They + are included in the releases as development previews. Please be + aware that APIs and functionality may change with no notice. + + We welcome any feedback and ideas as we develop this capability. + Please post feedback on + `Issue 1030 `_. + +Pyomo offers interfaces into multiple solvers, both commercial and open +source. To support better capabilities for solver interfaces, the Pyomo +team is actively redesigning the existing interfaces to make them more +maintainable and intuitive for use. A preview of the redesigned +interfaces can be found in ``pyomo.contrib.solver``. + +.. currentmodule:: pyomo.contrib.solver + + +New Interface Usage +------------------- + +The new interfaces are not completely backwards compatible with the +existing Pyomo solver interfaces. However, to aid in testing and +evaluation, we are distributing versions of the new solver interfaces +that are compatible with the existing ("legacy") solver interface. +These "legacy" interfaces are registered with the current +``SolverFactory`` using slightly different names (to avoid conflicts +with existing interfaces). + +.. |br| raw:: html + +
+ +.. list-table:: Available Redesigned Solvers and Names Registered + in the SolverFactories + :header-rows: 1 + + * - Solver + - Name registered in the |br| ``pyomo.contrib.solver.factory.SolverFactory`` + - Name registered in the |br| ``pyomo.opt.base.solvers.LegacySolverFactory`` + * - Ipopt + - ``ipopt`` + - ``ipopt_v2`` + * - Gurobi (persistent) + - ``gurobi`` + - ``gurobi_v2`` + * - Gurobi (direct) + - ``gurobi_direct`` + - ``gurobi_direct_v2`` + +Using the new interfaces through the legacy interface +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Here we use the new interface as exposed through the existing (legacy) +solver factory and solver interface wrapper. This provides an API that +is compatible with the existing (legacy) Pyomo solver interface and can +be used with other Pyomo tools / capabilities. + +.. testcode:: + :skipif: not ipopt_available + + import pyomo.environ as pyo + from pyomo.contrib.solver.util import assert_optimal_termination + + model = pyo.ConcreteModel() + model.x = pyo.Var(initialize=1.5) + model.y = pyo.Var(initialize=1.5) + + def rosenbrock(model): + return (1.0 - model.x) ** 2 + 100.0 * (model.y - model.x**2) ** 2 + + model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize) + + status = pyo.SolverFactory('ipopt_v2').solve(model) + assert_optimal_termination(status) + model.pprint() + +.. testoutput:: + :skipif: not ipopt_available + :hide: + + 2 Var Declarations + ... + 3 Declarations: x y obj + +In keeping with our commitment to backwards compatibility, both the legacy and +future methods of specifying solver options are supported: + +.. testcode:: + :skipif: not ipopt_available + + import pyomo.environ as pyo + + model = pyo.ConcreteModel() + model.x = pyo.Var(initialize=1.5) + model.y = pyo.Var(initialize=1.5) + + def rosenbrock(model): + return (1.0 - model.x) ** 2 + 100.0 * (model.y - model.x**2) ** 2 + + model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize) + + # Backwards compatible + status = pyo.SolverFactory('ipopt_v2').solve(model, options={'max_iter' : 6}) + # Forwards compatible + status = pyo.SolverFactory('ipopt_v2').solve(model, solver_options={'max_iter' : 6}) + model.pprint() + +.. testoutput:: + :skipif: not ipopt_available + :hide: + + 2 Var Declarations + ... + 3 Declarations: x y obj + +Using the new interfaces directly +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Here we use the new interface by importing it directly: + +.. testcode:: + :skipif: not ipopt_available + + # Direct import + import pyomo.environ as pyo + from pyomo.contrib.solver.util import assert_optimal_termination + from pyomo.contrib.solver.ipopt import Ipopt + + model = pyo.ConcreteModel() + model.x = pyo.Var(initialize=1.5) + model.y = pyo.Var(initialize=1.5) + + def rosenbrock(model): + return (1.0 - model.x) ** 2 + 100.0 * (model.y - model.x**2) ** 2 + + model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize) + + opt = Ipopt() + status = opt.solve(model) + assert_optimal_termination(status) + # Displays important results information; only available through the new interfaces + status.display() + model.pprint() + +.. testoutput:: + :skipif: not ipopt_available + :hide: + + solution_loader: ... + ... + 3 Declarations: x y obj + +Using the new interfaces through the "new" SolverFactory +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Here we use the new interface by retrieving it from the new ``SolverFactory``: + +.. testcode:: + :skipif: not ipopt_available + + # Import through new SolverFactory + import pyomo.environ as pyo + from pyomo.contrib.solver.util import assert_optimal_termination + from pyomo.contrib.solver.factory import SolverFactory + + model = pyo.ConcreteModel() + model.x = pyo.Var(initialize=1.5) + model.y = pyo.Var(initialize=1.5) + + def rosenbrock(model): + return (1.0 - model.x) ** 2 + 100.0 * (model.y - model.x**2) ** 2 + + model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize) + + opt = SolverFactory('ipopt') + status = opt.solve(model) + assert_optimal_termination(status) + # Displays important results information; only available through the new interfaces + status.display() + model.pprint() + +.. testoutput:: + :skipif: not ipopt_available + :hide: + + solution_loader: ... + ... + 3 Declarations: x y obj + +Switching all of Pyomo to use the new interfaces +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +We also provide a mechanism to get a "preview" of the future where we +replace the existing (legacy) SolverFactory and utilities with the new +(development) version (see :doc:`future`): + +.. testcode:: + :skipif: not ipopt_available + + # Change default SolverFactory version + import pyomo.environ as pyo + from pyomo.contrib.solver.util import assert_optimal_termination + from pyomo.__future__ import solver_factory_v3 + + model = pyo.ConcreteModel() + model.x = pyo.Var(initialize=1.5) + model.y = pyo.Var(initialize=1.5) + + def rosenbrock(model): + return (1.0 - model.x) ** 2 + 100.0 * (model.y - model.x**2) ** 2 + + model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize) + + status = pyo.SolverFactory('ipopt').solve(model) + assert_optimal_termination(status) + # Displays important results information; only available through the new interfaces + status.display() + model.pprint() + +.. testoutput:: + :skipif: not ipopt_available + :hide: + + solution_loader: ... + ... + 3 Declarations: x y obj + +.. testcode:: + :skipif: not ipopt_available + :hide: + + from pyomo.__future__ import solver_factory_v1 + +Linear Presolve and Scaling +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The new interface allows access to new capabilities in the various +problem writers, including the linear presolve and scaling options +recently incorporated into the redesigned NL writer. For example, you +can control the NL writer in the new ``ipopt`` interface through the +solver's ``writer_config`` configuration option: + +.. autoclass:: pyomo.contrib.solver.ipopt.Ipopt + :members: solve + +.. testcode:: + + from pyomo.contrib.solver.ipopt import Ipopt + opt = Ipopt() + opt.config.writer_config.display() + +.. testoutput:: + + show_section_timing: false + skip_trivial_constraints: true + file_determinism: FileDeterminism.ORDERED + symbolic_solver_labels: false + scale_model: true + export_nonlinear_variables: None + row_order: None + column_order: None + export_defined_variables: true + linear_presolve: true + +Note that, by default, both ``linear_presolve`` and ``scale_model`` are enabled. +Users can manipulate ``linear_presolve`` and ``scale_model`` to their preferred +states by changing their values. + +.. code-block:: python + + >>> opt.config.writer_config.linear_presolve = False + + +Interface Implementation +------------------------ + +All new interfaces should be built upon one of two classes (currently): +:class:`SolverBase` or +:class:`PersistentSolverBase`. + +All solvers should have the following: + +.. autoclass:: pyomo.contrib.solver.base.SolverBase + :members: + +Persistent solvers include additional members as well as other configuration options: + +.. autoclass:: pyomo.contrib.solver.base.PersistentSolverBase + :show-inheritance: + :members: + +Results +------- + +Every solver, at the end of a +:meth:`solve` call, will +return a :class:`Results` +object. This object is a :py:class:`pyomo.common.config.ConfigDict`, +which can be manipulated similar to a standard ``dict`` in Python. + +.. autoclass:: pyomo.contrib.solver.results.Results + :show-inheritance: + :members: + :undoc-members: + + +Termination Conditions +^^^^^^^^^^^^^^^^^^^^^^ + +Pyomo offers a standard set of termination conditions to map to solver +returns. The intent of +:class:`TerminationCondition` +is to notify the user of why the solver exited. The user is expected +to inspect the :class:`Results` +object or any returned solver messages or logs for more information. + +.. autoclass:: pyomo.contrib.solver.results.TerminationCondition + :show-inheritance: + + +Solution Status +^^^^^^^^^^^^^^^ + +Pyomo offers a standard set of solution statuses to map to solver +output. The intent of +:class:`SolutionStatus` +is to notify the user of what the solver returned at a high level. The +user is expected to inspect the +:class:`Results` object or any +returned solver messages or logs for more information. + +.. autoclass:: pyomo.contrib.solver.results.SolutionStatus + :show-inheritance: + + +Solution +-------- + +Solutions can be loaded back into a model using a ``SolutionLoader``. A specific +loader should be written for each unique case. Several have already been +implemented. For example, for ``ipopt``: + +.. autoclass:: pyomo.contrib.solver.ipopt.IpoptSolutionLoader + :show-inheritance: + :members: + :inherited-members: diff --git a/doc/OnlineDocs/installation.rst b/doc/OnlineDocs/installation.rst index 323d7be1632..83cd08e7a4a 100644 --- a/doc/OnlineDocs/installation.rst +++ b/doc/OnlineDocs/installation.rst @@ -3,7 +3,7 @@ Installation Pyomo currently supports the following versions of Python: -* CPython: 3.8, 3.9, 3.10, 3.11 +* CPython: 3.8, 3.9, 3.10, 3.11, 3.12 * PyPy: 3 At the time of the first Pyomo release after the end-of-life of a minor Python @@ -12,7 +12,7 @@ version, Pyomo will remove testing for that Python version. Using CONDA ~~~~~~~~~~~ -We recommend installation with *conda*, which is included with the +We recommend installation with ``conda``, which is included with the Anaconda distribution of Python. You can install Pyomo in your system Python installation by executing the following in a shell: @@ -21,7 +21,7 @@ Python installation by executing the following in a shell: conda install -c conda-forge pyomo Optimization solvers are not installed with Pyomo, but some open source -optimization solvers can be installed with conda as well: +optimization solvers can be installed with ``conda`` as well: :: @@ -31,7 +31,7 @@ optimization solvers can be installed with conda as well: Using PIP ~~~~~~~~~ -The standard utility for installing Python packages is *pip*. You +The standard utility for installing Python packages is ``pip``. You can install Pyomo in your system Python installation by executing the following in a shell: @@ -43,14 +43,14 @@ the following in a shell: Conditional Dependencies ~~~~~~~~~~~~~~~~~~~~~~~~ -Extensions to Pyomo, and many of the contributions in `pyomo.contrib`, +Extensions to Pyomo, and many of the contributions in ``pyomo.contrib``, often have conditional dependencies on a variety of third-party Python packages including but not limited to: matplotlib, networkx, numpy, openpyxl, pandas, pint, pymysql, pyodbc, pyro4, scipy, sympy, and xlrd. A full list of conditional dependencies can be found in Pyomo's -`setup.py` and displayed using: +``setup.py`` and displayed using: :: @@ -72,3 +72,28 @@ with the standard Anaconda installation. You can check which Python packages you have installed using the command ``conda list`` or ``pip list``. Additional Python packages may be installed as needed. + + +Installation with Cython +~~~~~~~~~~~~~~~~~~~~~~~~ + +Users can opt to install Pyomo with +`cython `_ +initialized. + +.. note:: + This can only be done via ``pip`` or from source. + +Via ``pip``: + +:: + + pip install pyomo --global-option="--with-cython" + +From source (recommended for advanced users only): + +:: + + git clone https://github.com/Pyomo/pyomo.git + cd pyomo + python setup.py install --with-cython diff --git a/doc/OnlineDocs/library_reference/appsi/appsi.solvers.maingo.rst b/doc/OnlineDocs/library_reference/appsi/appsi.solvers.maingo.rst new file mode 100644 index 00000000000..21e61c38d51 --- /dev/null +++ b/doc/OnlineDocs/library_reference/appsi/appsi.solvers.maingo.rst @@ -0,0 +1,14 @@ +MAiNGO +====== + +.. autoclass:: pyomo.contrib.appsi.solvers.maingo.MAiNGOConfig + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pyomo.contrib.appsi.solvers.maingo.MAiNGO + :members: + :inherited-members: + :undoc-members: + :show-inheritance: diff --git a/doc/OnlineDocs/library_reference/appsi/appsi.solvers.rst b/doc/OnlineDocs/library_reference/appsi/appsi.solvers.rst index 1c598d95628..f4dcb81b4be 100644 --- a/doc/OnlineDocs/library_reference/appsi/appsi.solvers.rst +++ b/doc/OnlineDocs/library_reference/appsi/appsi.solvers.rst @@ -13,3 +13,4 @@ Solvers appsi.solvers.cplex appsi.solvers.cbc appsi.solvers.highs + appsi.solvers.maingo diff --git a/doc/OnlineDocs/library_reference/common/config.rst b/doc/OnlineDocs/library_reference/common/config.rst index 7a400b26ce3..c5dc607977a 100644 --- a/doc/OnlineDocs/library_reference/common/config.rst +++ b/doc/OnlineDocs/library_reference/common/config.rst @@ -36,6 +36,7 @@ Domain validators NonPositiveFloat NonNegativeFloat In + IsInstance InEnum ListOf Module @@ -75,6 +76,7 @@ Domain validators .. autofunction:: NonPositiveFloat .. autofunction:: NonNegativeFloat .. autoclass:: In +.. autoclass:: IsInstance .. autoclass:: InEnum .. autoclass:: ListOf .. autoclass:: Module diff --git a/doc/OnlineDocs/library_reference/common/enums.rst b/doc/OnlineDocs/library_reference/common/enums.rst new file mode 100644 index 00000000000..5ed2dbb1e80 --- /dev/null +++ b/doc/OnlineDocs/library_reference/common/enums.rst @@ -0,0 +1,7 @@ + +pyomo.common.enums +================== + +.. automodule:: pyomo.common.enums + :members: + :member-order: bysource diff --git a/doc/OnlineDocs/library_reference/common/index.rst b/doc/OnlineDocs/library_reference/common/index.rst index c9c99008250..c03436600f2 100644 --- a/doc/OnlineDocs/library_reference/common/index.rst +++ b/doc/OnlineDocs/library_reference/common/index.rst @@ -11,6 +11,7 @@ or rely on any other parts of Pyomo. config.rst dependencies.rst deprecation.rst + enums.rst errors.rst fileutils.rst formatting.rst diff --git a/doc/OnlineDocs/library_reference/expressions/context_managers.rst b/doc/OnlineDocs/library_reference/expressions/context_managers.rst index 521334aef16..ae6884d684f 100644 --- a/doc/OnlineDocs/library_reference/expressions/context_managers.rst +++ b/doc/OnlineDocs/library_reference/expressions/context_managers.rst @@ -8,6 +8,3 @@ Context Managers .. autoclass:: pyomo.core.expr.linear_expression :members: -.. autoclass:: pyomo.core.expr.clone_counter - :members: - diff --git a/doc/OnlineDocs/library_reference/expressions/visitors.rst b/doc/OnlineDocs/library_reference/expressions/visitors.rst index f91107a6e8d..77cffe7905f 100644 --- a/doc/OnlineDocs/library_reference/expressions/visitors.rst +++ b/doc/OnlineDocs/library_reference/expressions/visitors.rst @@ -2,10 +2,19 @@ Visitor Classes =============== +.. autoclass:: pyomo.core.expr.StreamBasedExpressionVisitor + :members: + :inherited-members: + .. autoclass:: pyomo.core.expr.SimpleExpressionVisitor :members: + :inherited-members: + .. autoclass:: pyomo.core.expr.ExpressionValueVisitor :members: + :inherited-members: + .. autoclass:: pyomo.core.expr.ExpressionReplacementVisitor :members: + :inherited-members: diff --git a/doc/OnlineDocs/library_reference/kernel/examples/aml_example.py b/doc/OnlineDocs/library_reference/kernel/examples/aml_example.py index 146048a6046..a640b94cc76 100644 --- a/doc/OnlineDocs/library_reference/kernel/examples/aml_example.py +++ b/doc/OnlineDocs/library_reference/kernel/examples/aml_example.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # @Import_Syntax import pyomo.environ as aml diff --git a/doc/OnlineDocs/library_reference/kernel/examples/conic.py b/doc/OnlineDocs/library_reference/kernel/examples/conic.py index 9282bc67f9a..0418d188722 100644 --- a/doc/OnlineDocs/library_reference/kernel/examples/conic.py +++ b/doc/OnlineDocs/library_reference/kernel/examples/conic.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # @Class import pyomo.kernel as pmo diff --git a/doc/OnlineDocs/library_reference/kernel/examples/kernel_containers.py b/doc/OnlineDocs/library_reference/kernel/examples/kernel_containers.py index f2a4ec25ac5..1931c6d9b56 100644 --- a/doc/OnlineDocs/library_reference/kernel/examples/kernel_containers.py +++ b/doc/OnlineDocs/library_reference/kernel/examples/kernel_containers.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.kernel # @all diff --git a/doc/OnlineDocs/library_reference/kernel/examples/kernel_example.py b/doc/OnlineDocs/library_reference/kernel/examples/kernel_example.py index 1caf064bb2a..1f80bce9788 100644 --- a/doc/OnlineDocs/library_reference/kernel/examples/kernel_example.py +++ b/doc/OnlineDocs/library_reference/kernel/examples/kernel_example.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # @Import_Syntax import pyomo.kernel as pmo diff --git a/doc/OnlineDocs/library_reference/kernel/examples/kernel_solving.py b/doc/OnlineDocs/library_reference/kernel/examples/kernel_solving.py index 5a8eed9fd89..13d7efc052a 100644 --- a/doc/OnlineDocs/library_reference/kernel/examples/kernel_solving.py +++ b/doc/OnlineDocs/library_reference/kernel/examples/kernel_solving.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.kernel as pmo model = pmo.block() diff --git a/doc/OnlineDocs/library_reference/kernel/examples/kernel_subclassing.py b/doc/OnlineDocs/library_reference/kernel/examples/kernel_subclassing.py index c21c6dc890b..d6e38f6b0e0 100644 --- a/doc/OnlineDocs/library_reference/kernel/examples/kernel_subclassing.py +++ b/doc/OnlineDocs/library_reference/kernel/examples/kernel_subclassing.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.kernel diff --git a/doc/OnlineDocs/library_reference/kernel/examples/transformer.py b/doc/OnlineDocs/library_reference/kernel/examples/transformer.py index 3d8449a191d..43a1d0675bf 100644 --- a/doc/OnlineDocs/library_reference/kernel/examples/transformer.py +++ b/doc/OnlineDocs/library_reference/kernel/examples/transformer.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ import pyomo.kernel @@ -37,7 +48,7 @@ def connect_v_out(self, v_out): # @kernel -print(_fmt(pympler.asizeof.asizeof(Transformer()))) +print("Memory:", _fmt(pympler.asizeof.asizeof(Transformer()))) # @aml @@ -52,4 +63,4 @@ def Transformer(): # @aml -print(_fmt(pympler.asizeof.asizeof(Transformer()))) +print("Memory:", _fmt(pympler.asizeof.asizeof(Transformer()))) diff --git a/doc/OnlineDocs/modeling_extensions/__init__.py b/doc/OnlineDocs/modeling_extensions/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/doc/OnlineDocs/modeling_extensions/__init__.py +++ b/doc/OnlineDocs/modeling_extensions/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/doc/OnlineDocs/modeling_extensions/gdp/modeling.rst b/doc/OnlineDocs/modeling_extensions/gdp/modeling.rst index b70e37d5935..996ebcb0366 100644 --- a/doc/OnlineDocs/modeling_extensions/gdp/modeling.rst +++ b/doc/OnlineDocs/modeling_extensions/gdp/modeling.rst @@ -166,7 +166,7 @@ Usage: >>> TransformationFactory('core.logical_to_linear').apply_to(m) >>> # constraint auto-generated by transformation >>> m.logic_to_linear.transformed_constraints.pprint() - transformed_constraints : Size=1, Index=logic_to_linear.transformed_constraints_index, Active=True + transformed_constraints : Size=1, Index={1}, Active=True Key : Lower : Body : Upper : Active 1 : 3.0 : Y_asbinary[1] + Y_asbinary[2] + Y_asbinary[3] + Y_asbinary[4] : +Inf : True diff --git a/doc/OnlineDocs/modeling_extensions/gdp/solving.rst b/doc/OnlineDocs/modeling_extensions/gdp/solving.rst index 2f3076862e6..9fea90ebf5f 100644 --- a/doc/OnlineDocs/modeling_extensions/gdp/solving.rst +++ b/doc/OnlineDocs/modeling_extensions/gdp/solving.rst @@ -140,6 +140,10 @@ For example, to apply the transformation and store the M values, use: From the Pyomo command line, include the ``--transform pyomo.gdp.mbigm`` option. +.. warning:: + The Multiple Big-M transformation does not currently support Suffixes and will + ignore "BigM" Suffixes. + Hull Reformulation (HR) ^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/OnlineDocs/pyomo_modeling_components/Constraints.rst b/doc/OnlineDocs/pyomo_modeling_components/Constraints.rst index a79d380dcab..0cc42cb2abe 100644 --- a/doc/OnlineDocs/pyomo_modeling_components/Constraints.rst +++ b/doc/OnlineDocs/pyomo_modeling_components/Constraints.rst @@ -6,7 +6,7 @@ that are created using a rule, which is a Python function. For example, if the variable ``model.x`` has the indexes 'butter' and 'scones', then this constraint limits the sum over these indexes to be exactly three: -.. literalinclude:: ../tests/scripting/spy4Constraints_Constraint_example.spy +.. literalinclude:: ../src/scripting/spy4Constraints_Constraint_example.spy :language: python Instead of expressions involving equality (==) or inequalities (`<=` or @@ -16,7 +16,7 @@ lb `<=` expr `<=` ub. Variables can appear only in the middle expr. For example, the following two constraint declarations have the same meaning: -.. literalinclude:: ../tests/scripting/spy4Constraints_Inequality_constraints_2expressions.spy +.. literalinclude:: ../src/scripting/spy4Constraints_Inequality_constraints_2expressions.spy :language: python For this simple example, it would also be possible to declare @@ -30,7 +30,7 @@ interpreted as placing a budget of :math:`i` on the :math:`i^{\mbox{th}}` item to buy where the cost per item is given by the parameter ``model.a``: -.. literalinclude:: ../tests/scripting/spy4Constraints_Passing_elements_crossproduct.spy +.. literalinclude:: ../src/scripting/spy4Constraints_Passing_elements_crossproduct.spy :language: python .. note:: diff --git a/doc/OnlineDocs/pyomo_modeling_components/Expressions.rst b/doc/OnlineDocs/pyomo_modeling_components/Expressions.rst index f0558621316..16c206e2fe8 100644 --- a/doc/OnlineDocs/pyomo_modeling_components/Expressions.rst +++ b/doc/OnlineDocs/pyomo_modeling_components/Expressions.rst @@ -20,7 +20,7 @@ possible to build up expressions. The following example illustrates this, along with a reference to global Python data in the form of a Python variable called ``switch``: -.. literalinclude:: ../tests/scripting/spy4Expressions_Buildup_expression_switch.spy +.. literalinclude:: ../src/scripting/spy4Expressions_Buildup_expression_switch.spy :language: python In this example, the constraint that is generated depends on the value @@ -33,7 +33,7 @@ otherwise, the ``model.d`` term is not present. Because model elements result in expressions, not values, the following does not work as expected in an abstract model! - .. literalinclude:: ../tests/scripting/spy4Expressions_Abstract_wrong_usage.spy + .. literalinclude:: ../src/scripting/spy4Expressions_Abstract_wrong_usage.spy :language: python The trouble is that ``model.d >= 2`` results in an expression, not @@ -58,7 +58,7 @@ as described in the paper [Vielma_et_al]_. There are two basic forms for the declaration of the constraint: -.. literalinclude:: ../tests/scripting/spy4Expressions_Declare_piecewise_constraints.spy +.. literalinclude:: ../src/scripting/spy4Expressions_Declare_piecewise_constraints.spy :language: python where ``pwconst`` can be replaced by a name appropriate for the @@ -124,7 +124,7 @@ Keywords: indexing set is used or when all indices use an identical piecewise function). Examples: - .. literalinclude:: ../tests/scripting/spy4Expressions_f_rule_Function_examples.spy + .. literalinclude:: ../src/scripting/spy4Expressions_f_rule_Function_examples.spy :language: python * **force_pw=True/False** @@ -163,7 +163,7 @@ Keywords: Here is an example of an assignment to a Python dictionary variable that has keywords for a picewise constraint: -.. literalinclude:: ../tests/scripting/spy4Expressions_Keyword_assignment_example.spy +.. literalinclude:: ../src/scripting/spy4Expressions_Keyword_assignment_example.spy :language: python Here is a simple example based on the example given earlier in @@ -175,7 +175,7 @@ whimsically just to make the example. The important thing to note is that variables that are going to appear as the independent variable in a piecewise constraint must have bounds. -.. literalinclude:: ../tests/scripting/abstract2piece.py +.. literalinclude:: ../src/scripting/abstract2piece.py :language: python A more advanced example is provided in abstract2piecebuild.py in @@ -193,13 +193,13 @@ variable x times the index. Later in the model file, just to illustrate how to do it, the expression is changed but just for the first index to be x squared. -.. literalinclude:: ../tests/scripting/spy4Expressions_Expression_objects_illustration.spy +.. literalinclude:: ../src/scripting/spy4Expressions_Expression_objects_illustration.spy :language: python An alternative is to create Python functions that, potentially, manipulate model objects. E.g., if you define a function -.. literalinclude:: ../tests/scripting/spy4Expressions_Define_python_function.spy +.. literalinclude:: ../src/scripting/spy4Expressions_Define_python_function.spy :language: python You can call this function with or without Pyomo modeling components as @@ -211,7 +211,7 @@ expression is used to generate another expression (e.g., f(model.x, 3) + 5), the initial expression is always cloned so that the new generated expression is independent of the old. For example: -.. literalinclude:: ../tests/scripting/spy4Expressions_Generate_new_expression.spy +.. literalinclude:: ../src/scripting/spy4Expressions_Generate_new_expression.spy :language: python If you want to create an expression that is shared between other diff --git a/doc/OnlineDocs/pyomo_modeling_components/Sets.rst b/doc/OnlineDocs/pyomo_modeling_components/Sets.rst index f9a692fcb10..73c3539d79d 100644 --- a/doc/OnlineDocs/pyomo_modeling_components/Sets.rst +++ b/doc/OnlineDocs/pyomo_modeling_components/Sets.rst @@ -443,13 +443,13 @@ model is: for this model, a toy data file (in AMPL "``.dat``" format) would be: -.. literalinclude:: ../tests/scripting/Isinglecomm.dat +.. literalinclude:: ../src/scripting/Isinglecomm.dat :language: text .. doctest:: :hide: - >>> inst = model.create_instance('tests/scripting/Isinglecomm.dat') + >>> inst = model.create_instance('src/scripting/Isinglecomm.dat') This can also be done somewhat more efficiently, and perhaps more clearly, using a :class:`BuildAction` (for more information, see :ref:`BuildAction`): diff --git a/doc/OnlineDocs/pyomo_modeling_components/Variables.rst b/doc/OnlineDocs/pyomo_modeling_components/Variables.rst index 038f5769705..7f7ee74af5f 100644 --- a/doc/OnlineDocs/pyomo_modeling_components/Variables.rst +++ b/doc/OnlineDocs/pyomo_modeling_components/Variables.rst @@ -20,13 +20,13 @@ declaring a *singleton* (i.e. unindexed) variable named ``model.LumberJack`` that will take on real values between zero and 6 and it initialized to be 1.5: -.. literalinclude:: ../tests/scripting/spy4Variables_Declare_singleton_variable.spy +.. literalinclude:: ../src/scripting/spy4Variables_Declare_singleton_variable.spy :language: python Instead of the ``initialize`` option, initialization is sometimes done with a Python assignment statement as in -.. literalinclude:: ../tests/scripting/spy4Variables_Assign_value.spy +.. literalinclude:: ../src/scripting/spy4Variables_Assign_value.spy :language: python For indexed variables, bounds and initial values are often specified by @@ -36,7 +36,7 @@ followed by the indexes. This is illustrated in the following code snippet that makes use of Python dictionaries declared as lb and ub that are used by a function to provide bounds: -.. literalinclude:: ../tests/scripting/spy4Variables_Declare_bounds.spy +.. literalinclude:: ../src/scripting/spy4Variables_Declare_bounds.spy :language: python .. note:: diff --git a/doc/OnlineDocs/pyomo_overview/simple_examples.rst b/doc/OnlineDocs/pyomo_overview/simple_examples.rst index 4358c87b678..11305884c54 100644 --- a/doc/OnlineDocs/pyomo_overview/simple_examples.rst +++ b/doc/OnlineDocs/pyomo_overview/simple_examples.rst @@ -91,7 +91,7 @@ One way to implement this in Pyomo is as shown as follows: :hide: >>> # Create an instance to verify that the rules fire correctly - >>> inst = model.create_instance('tests/scripting/abstract1.dat') + >>> inst = model.create_instance('src/scripting/abstract1.dat') .. note:: @@ -261,9 +261,9 @@ parameters. Here is one file that provides data (in AMPL "``.dat``" format). :hide: >>> # Create an instance to verify that the rules fire correctly - >>> inst = model.create_instance('tests/scripting/abstract1.dat') + >>> inst = model.create_instance('src/scripting/abstract1.dat') -.. literalinclude:: ../tests/scripting/abstract1.dat +.. literalinclude:: ../src/scripting/abstract1.dat :language: text There are multiple formats that can be used to provide data to a Pyomo @@ -327,18 +327,18 @@ the same model. To start with an illustration of general indexes, consider a slightly different Pyomo implementation of the model we just presented. -.. literalinclude:: ../tests/scripting/abstract2.py +.. literalinclude:: ../src/scripting/abstract2.py :language: python To get the same instantiated model, the following data file can be used. -.. literalinclude:: ../tests/scripting/abstract2a.dat +.. literalinclude:: ../src/scripting/abstract2a.dat :language: none However, this model can also be fed different data for problems of the same general form using meaningful indexes. -.. literalinclude:: ../tests/scripting/abstract2.dat +.. literalinclude:: ../src/scripting/abstract2.dat :language: none diff --git a/doc/OnlineDocs/tests/data/A.tab b/doc/OnlineDocs/src/data/A.tab similarity index 100% rename from doc/OnlineDocs/tests/data/A.tab rename to doc/OnlineDocs/src/data/A.tab diff --git a/doc/OnlineDocs/tests/data/ABCD.tab b/doc/OnlineDocs/src/data/ABCD.tab similarity index 100% rename from doc/OnlineDocs/tests/data/ABCD.tab rename to doc/OnlineDocs/src/data/ABCD.tab diff --git a/doc/OnlineDocs/tests/data/ABCD.txt b/doc/OnlineDocs/src/data/ABCD.txt similarity index 100% rename from doc/OnlineDocs/tests/data/ABCD.txt rename to doc/OnlineDocs/src/data/ABCD.txt diff --git a/doc/OnlineDocs/tests/data/ABCD.xls b/doc/OnlineDocs/src/data/ABCD.xls similarity index 100% rename from doc/OnlineDocs/tests/data/ABCD.xls rename to doc/OnlineDocs/src/data/ABCD.xls diff --git a/doc/OnlineDocs/tests/data/ABCD1.dat b/doc/OnlineDocs/src/data/ABCD1.dat similarity index 100% rename from doc/OnlineDocs/tests/data/ABCD1.dat rename to doc/OnlineDocs/src/data/ABCD1.dat diff --git a/doc/OnlineDocs/src/data/ABCD1.py b/doc/OnlineDocs/src/data/ABCD1.py new file mode 100644 index 00000000000..aa2f46e71fa --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD1.py @@ -0,0 +1,20 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.Z = Set(dimen=4) + +instance = model.create_instance('ABCD1.dat') + +print(sorted(list(instance.Z.data()))) diff --git a/doc/OnlineDocs/tests/data/ABCD1.txt b/doc/OnlineDocs/src/data/ABCD1.txt similarity index 100% rename from doc/OnlineDocs/tests/data/ABCD1.txt rename to doc/OnlineDocs/src/data/ABCD1.txt diff --git a/doc/OnlineDocs/tests/data/ABCD2.dat b/doc/OnlineDocs/src/data/ABCD2.dat similarity index 100% rename from doc/OnlineDocs/tests/data/ABCD2.dat rename to doc/OnlineDocs/src/data/ABCD2.dat diff --git a/doc/OnlineDocs/src/data/ABCD2.py b/doc/OnlineDocs/src/data/ABCD2.py new file mode 100644 index 00000000000..ec0e7ccb15c --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD2.py @@ -0,0 +1,25 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.Z = Set(initialize=[('A1', 'B1', 1), ('A2', 'B2', 2), ('A3', 'B3', 3)]) +# model.Z = Set(dimen=3) +model.D = Param(model.Z) + +instance = model.create_instance('ABCD2.dat') + +print('Z ' + str(sorted(list(instance.Z.data())))) +print('D') +for key in sorted(instance.D.keys()): + print(name(instance.D, key) + " " + str(value(instance.D[key]))) diff --git a/doc/OnlineDocs/tests/data/ABCD2.txt b/doc/OnlineDocs/src/data/ABCD2.txt similarity index 100% rename from doc/OnlineDocs/tests/data/ABCD2.txt rename to doc/OnlineDocs/src/data/ABCD2.txt diff --git a/doc/OnlineDocs/tests/data/ABCD3.dat b/doc/OnlineDocs/src/data/ABCD3.dat similarity index 100% rename from doc/OnlineDocs/tests/data/ABCD3.dat rename to doc/OnlineDocs/src/data/ABCD3.dat diff --git a/doc/OnlineDocs/src/data/ABCD3.py b/doc/OnlineDocs/src/data/ABCD3.py new file mode 100644 index 00000000000..ba55fd970cc --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD3.py @@ -0,0 +1,24 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.Z = Set(dimen=3) +model.D = Param(model.Z) + +instance = model.create_instance('ABCD3.dat') + +print('Z ' + str(sorted(list(instance.Z.data())))) +print('D') +for key in sorted(instance.D.keys()): + print(name(instance.D, key) + " " + str(value(instance.D[key]))) diff --git a/doc/OnlineDocs/tests/data/ABCD3.txt b/doc/OnlineDocs/src/data/ABCD3.txt similarity index 100% rename from doc/OnlineDocs/tests/data/ABCD3.txt rename to doc/OnlineDocs/src/data/ABCD3.txt diff --git a/doc/OnlineDocs/tests/data/ABCD4.dat b/doc/OnlineDocs/src/data/ABCD4.dat similarity index 100% rename from doc/OnlineDocs/tests/data/ABCD4.dat rename to doc/OnlineDocs/src/data/ABCD4.dat diff --git a/doc/OnlineDocs/src/data/ABCD4.py b/doc/OnlineDocs/src/data/ABCD4.py new file mode 100644 index 00000000000..2fb397aa3b0 --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD4.py @@ -0,0 +1,24 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.Z = Set(dimen=3) +model.Y = Param(model.Z) + +instance = model.create_instance('ABCD4.dat') + +print('Z ' + str(sorted(list(instance.Z.data())))) +print('Y') +for key in sorted(instance.Y.keys()): + print(name(instance.Y, key) + " " + str(value(instance.Y[key]))) diff --git a/doc/OnlineDocs/tests/data/ABCD4.txt b/doc/OnlineDocs/src/data/ABCD4.txt similarity index 100% rename from doc/OnlineDocs/tests/data/ABCD4.txt rename to doc/OnlineDocs/src/data/ABCD4.txt diff --git a/doc/OnlineDocs/tests/data/ABCD5.dat b/doc/OnlineDocs/src/data/ABCD5.dat similarity index 100% rename from doc/OnlineDocs/tests/data/ABCD5.dat rename to doc/OnlineDocs/src/data/ABCD5.dat diff --git a/doc/OnlineDocs/src/data/ABCD5.py b/doc/OnlineDocs/src/data/ABCD5.py new file mode 100644 index 00000000000..abc03505e96 --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD5.py @@ -0,0 +1,30 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.Z = Set() +model.Y = Param(model.Z) +model.W = Param(model.Z) +# @decl + +instance = model.create_instance('ABCD5.dat') + +print('Z ' + str(sorted(list(instance.Z.data())))) +print('Y') +for key in sorted(instance.Y.keys()): + print(name(instance.Y, key) + " " + str(value(instance.Y[key]))) +print('W') +for key in sorted(instance.W.keys()): + print(name(instance.W, key) + " " + str(value(instance.W[key]))) diff --git a/doc/OnlineDocs/tests/data/ABCD5.txt b/doc/OnlineDocs/src/data/ABCD5.txt similarity index 100% rename from doc/OnlineDocs/tests/data/ABCD5.txt rename to doc/OnlineDocs/src/data/ABCD5.txt diff --git a/doc/OnlineDocs/tests/data/ABCD6.dat b/doc/OnlineDocs/src/data/ABCD6.dat similarity index 100% rename from doc/OnlineDocs/tests/data/ABCD6.dat rename to doc/OnlineDocs/src/data/ABCD6.dat diff --git a/doc/OnlineDocs/src/data/ABCD6.py b/doc/OnlineDocs/src/data/ABCD6.py new file mode 100644 index 00000000000..59e0e8e98ae --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD6.py @@ -0,0 +1,24 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.Z = Set(dimen=3) +model.D = Param(model.Z) + +instance = model.create_instance('ABCD6.dat') + +print('Z ' + str(sorted(list(instance.Z.data())))) +print('D') +for key in sorted(instance.D.keys()): + print(name(instance.D, key) + " " + str(value(instance.D[key]))) diff --git a/doc/OnlineDocs/tests/data/ABCD6.txt b/doc/OnlineDocs/src/data/ABCD6.txt similarity index 100% rename from doc/OnlineDocs/tests/data/ABCD6.txt rename to doc/OnlineDocs/src/data/ABCD6.txt diff --git a/doc/OnlineDocs/tests/data/ABCD7.dat b/doc/OnlineDocs/src/data/ABCD7.dat similarity index 100% rename from doc/OnlineDocs/tests/data/ABCD7.dat rename to doc/OnlineDocs/src/data/ABCD7.dat diff --git a/doc/OnlineDocs/src/data/ABCD7.py b/doc/OnlineDocs/src/data/ABCD7.py new file mode 100644 index 00000000000..1bfb4d1e3fb --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD7.py @@ -0,0 +1,30 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * +import pyomo.common +import sys + +model = AbstractModel() + +model.Z = Set(dimen=3) +model.Y = Param(model.Z) + +try: + instance = model.create_instance('ABCD7.dat') +except pyomo.common.errors.ApplicationError as e: + print("ERROR " + str(e)) + sys.exit(1) + +print('Z ' + str(sorted(list(instance.Z.data())))) +print('Y') +for key in sorted(instance.Y.keys()): + print(name(instance.Y, key) + " " + str(value(instance.Y[key]))) diff --git a/doc/OnlineDocs/tests/data/ABCD7.txt b/doc/OnlineDocs/src/data/ABCD7.txt similarity index 100% rename from doc/OnlineDocs/tests/data/ABCD7.txt rename to doc/OnlineDocs/src/data/ABCD7.txt diff --git a/doc/OnlineDocs/tests/data/ABCD8.bad b/doc/OnlineDocs/src/data/ABCD8.bad similarity index 100% rename from doc/OnlineDocs/tests/data/ABCD8.bad rename to doc/OnlineDocs/src/data/ABCD8.bad diff --git a/doc/OnlineDocs/tests/data/ABCD8.dat b/doc/OnlineDocs/src/data/ABCD8.dat similarity index 100% rename from doc/OnlineDocs/tests/data/ABCD8.dat rename to doc/OnlineDocs/src/data/ABCD8.dat diff --git a/doc/OnlineDocs/src/data/ABCD8.py b/doc/OnlineDocs/src/data/ABCD8.py new file mode 100644 index 00000000000..aa1ba0b4cf5 --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD8.py @@ -0,0 +1,30 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * +import pyomo.common +import sys + +model = AbstractModel() + +model.Z = Set(dimen=3) +model.Y = Param(model.Z) + +try: + instance = model.create_instance('ABCD8.dat') +except pyomo.common.errors.ApplicationError as e: + print("ERROR " + str(e)) + sys.exit(1) + +print('Z ' + str(sorted(list(instance.Z.data())))) +print('Y') +for key in sorted(instance.Y.keys()): + print(name(instance.Y, key) + " " + str(value(instance.Y[key]))) diff --git a/doc/OnlineDocs/tests/data/ABCD9.bad b/doc/OnlineDocs/src/data/ABCD9.bad similarity index 100% rename from doc/OnlineDocs/tests/data/ABCD9.bad rename to doc/OnlineDocs/src/data/ABCD9.bad diff --git a/doc/OnlineDocs/tests/data/ABCD9.dat b/doc/OnlineDocs/src/data/ABCD9.dat similarity index 100% rename from doc/OnlineDocs/tests/data/ABCD9.dat rename to doc/OnlineDocs/src/data/ABCD9.dat diff --git a/doc/OnlineDocs/src/data/ABCD9.py b/doc/OnlineDocs/src/data/ABCD9.py new file mode 100644 index 00000000000..194c71486d9 --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD9.py @@ -0,0 +1,30 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * +import pyomo.common +import sys + +model = AbstractModel() + +model.Z = Set(dimen=3) +model.Y = Param(model.Z) + +try: + instance = model.create_instance('ABCD9.dat') +except pyomo.common.errors.ApplicationError as e: + print("ERROR " + str(e)) + sys.exit(1) + +print('Z ' + str(sorted(list(instance.Z.data())))) +print('Y') +for key in sorted(instance.Y.keys()): + print(instance.Y[key] + " " + str(value(instance.Y[key]))) diff --git a/doc/OnlineDocs/tests/data/C.tab b/doc/OnlineDocs/src/data/C.tab similarity index 100% rename from doc/OnlineDocs/tests/data/C.tab rename to doc/OnlineDocs/src/data/C.tab diff --git a/doc/OnlineDocs/tests/data/D.tab b/doc/OnlineDocs/src/data/D.tab similarity index 100% rename from doc/OnlineDocs/tests/data/D.tab rename to doc/OnlineDocs/src/data/D.tab diff --git a/doc/OnlineDocs/tests/data/U.tab b/doc/OnlineDocs/src/data/U.tab similarity index 100% rename from doc/OnlineDocs/tests/data/U.tab rename to doc/OnlineDocs/src/data/U.tab diff --git a/doc/OnlineDocs/tests/data/Y.tab b/doc/OnlineDocs/src/data/Y.tab similarity index 100% rename from doc/OnlineDocs/tests/data/Y.tab rename to doc/OnlineDocs/src/data/Y.tab diff --git a/doc/OnlineDocs/tests/data/Z.tab b/doc/OnlineDocs/src/data/Z.tab similarity index 100% rename from doc/OnlineDocs/tests/data/Z.tab rename to doc/OnlineDocs/src/data/Z.tab diff --git a/doc/OnlineDocs/tests/data/data_managers.txt b/doc/OnlineDocs/src/data/data_managers.txt similarity index 100% rename from doc/OnlineDocs/tests/data/data_managers.txt rename to doc/OnlineDocs/src/data/data_managers.txt diff --git a/doc/OnlineDocs/tests/data/diet.dat b/doc/OnlineDocs/src/data/diet.dat similarity index 100% rename from doc/OnlineDocs/tests/data/diet.dat rename to doc/OnlineDocs/src/data/diet.dat diff --git a/doc/OnlineDocs/tests/data/diet.sql b/doc/OnlineDocs/src/data/diet.sql similarity index 100% rename from doc/OnlineDocs/tests/data/diet.sql rename to doc/OnlineDocs/src/data/diet.sql diff --git a/doc/OnlineDocs/tests/data/diet.sqlite b/doc/OnlineDocs/src/data/diet.sqlite similarity index 100% rename from doc/OnlineDocs/tests/data/diet.sqlite rename to doc/OnlineDocs/src/data/diet.sqlite diff --git a/doc/OnlineDocs/tests/data/diet.sqlite.dat b/doc/OnlineDocs/src/data/diet.sqlite.dat similarity index 100% rename from doc/OnlineDocs/tests/data/diet.sqlite.dat rename to doc/OnlineDocs/src/data/diet.sqlite.dat diff --git a/doc/OnlineDocs/tests/data/diet1.py b/doc/OnlineDocs/src/data/diet1.py similarity index 76% rename from doc/OnlineDocs/tests/data/diet1.py rename to doc/OnlineDocs/src/data/diet1.py index ef0d8096350..40582e16ba0 100644 --- a/doc/OnlineDocs/tests/data/diet1.py +++ b/doc/OnlineDocs/src/data/diet1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # diet1.py from pyomo.environ import * diff --git a/doc/OnlineDocs/tests/data/ex.dat b/doc/OnlineDocs/src/data/ex.dat similarity index 100% rename from doc/OnlineDocs/tests/data/ex.dat rename to doc/OnlineDocs/src/data/ex.dat diff --git a/doc/OnlineDocs/src/data/ex.py b/doc/OnlineDocs/src/data/ex.py new file mode 100644 index 00000000000..a66ee30b494 --- /dev/null +++ b/doc/OnlineDocs/src/data/ex.py @@ -0,0 +1,22 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.z = Param() +# @decl + +instance = model.create_instance('ex.dat') + +print(value(instance.z)) diff --git a/doc/OnlineDocs/tests/data/ex.txt b/doc/OnlineDocs/src/data/ex.txt similarity index 100% rename from doc/OnlineDocs/tests/data/ex.txt rename to doc/OnlineDocs/src/data/ex.txt diff --git a/doc/OnlineDocs/tests/data/ex1.dat b/doc/OnlineDocs/src/data/ex1.dat similarity index 100% rename from doc/OnlineDocs/tests/data/ex1.dat rename to doc/OnlineDocs/src/data/ex1.dat diff --git a/doc/OnlineDocs/tests/data/ex2.dat b/doc/OnlineDocs/src/data/ex2.dat similarity index 100% rename from doc/OnlineDocs/tests/data/ex2.dat rename to doc/OnlineDocs/src/data/ex2.dat diff --git a/doc/OnlineDocs/tests/data/import1.tab.dat b/doc/OnlineDocs/src/data/import1.tab.dat similarity index 100% rename from doc/OnlineDocs/tests/data/import1.tab.dat rename to doc/OnlineDocs/src/data/import1.tab.dat diff --git a/doc/OnlineDocs/src/data/import1.tab.py b/doc/OnlineDocs/src/data/import1.tab.py new file mode 100644 index 00000000000..e160e4fdcde --- /dev/null +++ b/doc/OnlineDocs/src/data/import1.tab.py @@ -0,0 +1,24 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.A = Set(initialize=['A1', 'A2', 'A3', 'A4']) +model.Y = Param(model.A) + +instance = model.create_instance('import1.tab.dat') + +print('Y') +keys = instance.Y.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.Y[key]))) diff --git a/doc/OnlineDocs/tests/data/import1.tab.txt b/doc/OnlineDocs/src/data/import1.tab.txt similarity index 100% rename from doc/OnlineDocs/tests/data/import1.tab.txt rename to doc/OnlineDocs/src/data/import1.tab.txt diff --git a/doc/OnlineDocs/tests/data/import2.tab.dat b/doc/OnlineDocs/src/data/import2.tab.dat similarity index 100% rename from doc/OnlineDocs/tests/data/import2.tab.dat rename to doc/OnlineDocs/src/data/import2.tab.dat diff --git a/doc/OnlineDocs/src/data/import2.tab.py b/doc/OnlineDocs/src/data/import2.tab.py new file mode 100644 index 00000000000..54339551279 --- /dev/null +++ b/doc/OnlineDocs/src/data/import2.tab.py @@ -0,0 +1,25 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.A = Set() +model.Y = Param(model.A) + +instance = model.create_instance('import2.tab.dat') + +print('A ' + str(sorted(list(instance.A.data())))) +print('Y') +keys = instance.Y.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.Y[key]))) diff --git a/doc/OnlineDocs/tests/data/import2.tab.txt b/doc/OnlineDocs/src/data/import2.tab.txt similarity index 100% rename from doc/OnlineDocs/tests/data/import2.tab.txt rename to doc/OnlineDocs/src/data/import2.tab.txt diff --git a/doc/OnlineDocs/tests/data/import3.tab.dat b/doc/OnlineDocs/src/data/import3.tab.dat similarity index 100% rename from doc/OnlineDocs/tests/data/import3.tab.dat rename to doc/OnlineDocs/src/data/import3.tab.dat diff --git a/doc/OnlineDocs/src/data/import3.tab.py b/doc/OnlineDocs/src/data/import3.tab.py new file mode 100644 index 00000000000..664151d1438 --- /dev/null +++ b/doc/OnlineDocs/src/data/import3.tab.py @@ -0,0 +1,20 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.A = Set() + +instance = model.create_instance('import3.tab.dat') + +print('A ' + str(sorted(list(instance.A.data())))) diff --git a/doc/OnlineDocs/tests/data/import3.tab.txt b/doc/OnlineDocs/src/data/import3.tab.txt similarity index 100% rename from doc/OnlineDocs/tests/data/import3.tab.txt rename to doc/OnlineDocs/src/data/import3.tab.txt diff --git a/doc/OnlineDocs/tests/data/import4.tab.dat b/doc/OnlineDocs/src/data/import4.tab.dat similarity index 100% rename from doc/OnlineDocs/tests/data/import4.tab.dat rename to doc/OnlineDocs/src/data/import4.tab.dat diff --git a/doc/OnlineDocs/src/data/import4.tab.py b/doc/OnlineDocs/src/data/import4.tab.py new file mode 100644 index 00000000000..91dd3f26a42 --- /dev/null +++ b/doc/OnlineDocs/src/data/import4.tab.py @@ -0,0 +1,20 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.C = Set(dimen=2) + +instance = model.create_instance('import4.tab.dat') + +print('C ' + str(sorted(list(instance.C.data())))) diff --git a/doc/OnlineDocs/tests/data/import4.tab.txt b/doc/OnlineDocs/src/data/import4.tab.txt similarity index 100% rename from doc/OnlineDocs/tests/data/import4.tab.txt rename to doc/OnlineDocs/src/data/import4.tab.txt diff --git a/doc/OnlineDocs/tests/data/import5.tab.dat b/doc/OnlineDocs/src/data/import5.tab.dat similarity index 100% rename from doc/OnlineDocs/tests/data/import5.tab.dat rename to doc/OnlineDocs/src/data/import5.tab.dat diff --git a/doc/OnlineDocs/src/data/import5.tab.py b/doc/OnlineDocs/src/data/import5.tab.py new file mode 100644 index 00000000000..263677c308c --- /dev/null +++ b/doc/OnlineDocs/src/data/import5.tab.py @@ -0,0 +1,20 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.B = Set(dimen=2) + +instance = model.create_instance('import5.tab.dat') + +print('B ' + str(list(sorted(instance.B.data())))) diff --git a/doc/OnlineDocs/tests/data/import5.tab.txt b/doc/OnlineDocs/src/data/import5.tab.txt similarity index 100% rename from doc/OnlineDocs/tests/data/import5.tab.txt rename to doc/OnlineDocs/src/data/import5.tab.txt diff --git a/doc/OnlineDocs/tests/data/import6.tab.dat b/doc/OnlineDocs/src/data/import6.tab.dat similarity index 100% rename from doc/OnlineDocs/tests/data/import6.tab.dat rename to doc/OnlineDocs/src/data/import6.tab.dat diff --git a/doc/OnlineDocs/src/data/import6.tab.py b/doc/OnlineDocs/src/data/import6.tab.py new file mode 100644 index 00000000000..8f4824ad3fe --- /dev/null +++ b/doc/OnlineDocs/src/data/import6.tab.py @@ -0,0 +1,20 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.p = Param() + +instance = model.create_instance('import6.tab.dat') + +print('p ' + str(value(instance.p))) diff --git a/doc/OnlineDocs/tests/data/import6.tab.txt b/doc/OnlineDocs/src/data/import6.tab.txt similarity index 100% rename from doc/OnlineDocs/tests/data/import6.tab.txt rename to doc/OnlineDocs/src/data/import6.tab.txt diff --git a/doc/OnlineDocs/tests/data/import7.tab.dat b/doc/OnlineDocs/src/data/import7.tab.dat similarity index 100% rename from doc/OnlineDocs/tests/data/import7.tab.dat rename to doc/OnlineDocs/src/data/import7.tab.dat diff --git a/doc/OnlineDocs/src/data/import7.tab.py b/doc/OnlineDocs/src/data/import7.tab.py new file mode 100644 index 00000000000..503f9224323 --- /dev/null +++ b/doc/OnlineDocs/src/data/import7.tab.py @@ -0,0 +1,28 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.I = Set(initialize=['I1', 'I2', 'I3', 'I4']) +model.A = Set(initialize=['A1', 'A2', 'A3']) +model.U = Param(model.I, model.A) +# BUG: This should cause an error +# model.U = Param(model.A,model.I) + +instance = model.create_instance('import7.tab.dat') + +print('I ' + str(sorted(list(instance.I.data())))) +print('A ' + str(sorted(list(instance.A.data())))) +print('U') +for key in sorted(instance.U.keys()): + print(name(instance.U, key) + " " + str(value(instance.U[key]))) diff --git a/doc/OnlineDocs/tests/data/import7.tab.txt b/doc/OnlineDocs/src/data/import7.tab.txt similarity index 100% rename from doc/OnlineDocs/tests/data/import7.tab.txt rename to doc/OnlineDocs/src/data/import7.tab.txt diff --git a/doc/OnlineDocs/tests/data/import8.tab.dat b/doc/OnlineDocs/src/data/import8.tab.dat similarity index 100% rename from doc/OnlineDocs/tests/data/import8.tab.dat rename to doc/OnlineDocs/src/data/import8.tab.dat diff --git a/doc/OnlineDocs/src/data/import8.tab.py b/doc/OnlineDocs/src/data/import8.tab.py new file mode 100644 index 00000000000..02b8724fe45 --- /dev/null +++ b/doc/OnlineDocs/src/data/import8.tab.py @@ -0,0 +1,26 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.I = Set(initialize=['I1', 'I2', 'I3', 'I4']) +model.A = Set(initialize=['A1', 'A2', 'A3']) +model.U = Param(model.A, model.I) + +instance = model.create_instance('import8.tab.dat') + +print('A ' + str(sorted(list(instance.A.data())))) +print('I ' + str(sorted(list(instance.I.data())))) +print('U') +for key in sorted(instance.U.keys()): + print(name(instance.U, key) + " " + str(value(instance.U[key]))) diff --git a/doc/OnlineDocs/tests/data/import8.tab.txt b/doc/OnlineDocs/src/data/import8.tab.txt similarity index 100% rename from doc/OnlineDocs/tests/data/import8.tab.txt rename to doc/OnlineDocs/src/data/import8.tab.txt diff --git a/doc/OnlineDocs/tests/data/namespace1.dat b/doc/OnlineDocs/src/data/namespace1.dat similarity index 100% rename from doc/OnlineDocs/tests/data/namespace1.dat rename to doc/OnlineDocs/src/data/namespace1.dat diff --git a/doc/OnlineDocs/tests/data/param1.dat b/doc/OnlineDocs/src/data/param1.dat similarity index 100% rename from doc/OnlineDocs/tests/data/param1.dat rename to doc/OnlineDocs/src/data/param1.dat diff --git a/doc/OnlineDocs/src/data/param1.py b/doc/OnlineDocs/src/data/param1.py new file mode 100644 index 00000000000..336a04287b9 --- /dev/null +++ b/doc/OnlineDocs/src/data/param1.py @@ -0,0 +1,30 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Param() +model.B = Param() +model.C = Param() +model.D = Param() +model.E = Param() +# @decl + +instance = model.create_instance('param1.dat') + +print(value(instance.A)) +print(value(instance.B)) +print(value(instance.C)) +print(value(instance.D)) +print(value(instance.E)) diff --git a/doc/OnlineDocs/tests/data/param1.txt b/doc/OnlineDocs/src/data/param1.txt similarity index 100% rename from doc/OnlineDocs/tests/data/param1.txt rename to doc/OnlineDocs/src/data/param1.txt diff --git a/doc/OnlineDocs/tests/data/param2.dat b/doc/OnlineDocs/src/data/param2.dat similarity index 100% rename from doc/OnlineDocs/tests/data/param2.dat rename to doc/OnlineDocs/src/data/param2.dat diff --git a/doc/OnlineDocs/src/data/param2.py b/doc/OnlineDocs/src/data/param2.py new file mode 100644 index 00000000000..a7d0feafff9 --- /dev/null +++ b/doc/OnlineDocs/src/data/param2.py @@ -0,0 +1,25 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Set() +model.B = Param(model.A) +# @decl + +instance = model.create_instance('param2.dat') + +keys = instance.B.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.B[key]))) diff --git a/doc/OnlineDocs/tests/data/param2.txt b/doc/OnlineDocs/src/data/param2.txt similarity index 100% rename from doc/OnlineDocs/tests/data/param2.txt rename to doc/OnlineDocs/src/data/param2.txt diff --git a/doc/OnlineDocs/tests/data/param2a.dat b/doc/OnlineDocs/src/data/param2a.dat similarity index 100% rename from doc/OnlineDocs/tests/data/param2a.dat rename to doc/OnlineDocs/src/data/param2a.dat diff --git a/doc/OnlineDocs/src/data/param2a.py b/doc/OnlineDocs/src/data/param2a.py new file mode 100644 index 00000000000..42056793ffd --- /dev/null +++ b/doc/OnlineDocs/src/data/param2a.py @@ -0,0 +1,25 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Set() +model.B = Param(model.A) +# @decl + +instance = model.create_instance('param2a.dat') + +keys = instance.B.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.B[key]))) diff --git a/doc/OnlineDocs/tests/data/param2a.txt b/doc/OnlineDocs/src/data/param2a.txt similarity index 100% rename from doc/OnlineDocs/tests/data/param2a.txt rename to doc/OnlineDocs/src/data/param2a.txt diff --git a/doc/OnlineDocs/tests/data/param3.dat b/doc/OnlineDocs/src/data/param3.dat similarity index 100% rename from doc/OnlineDocs/tests/data/param3.dat rename to doc/OnlineDocs/src/data/param3.dat diff --git a/doc/OnlineDocs/tests/data/param3.py b/doc/OnlineDocs/src/data/param3.py similarity index 50% rename from doc/OnlineDocs/tests/data/param3.py rename to doc/OnlineDocs/src/data/param3.py index 149155ce67d..952f9a9b707 100644 --- a/doc/OnlineDocs/tests/data/param3.py +++ b/doc/OnlineDocs/src/data/param3.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/tests/data/param3.txt b/doc/OnlineDocs/src/data/param3.txt similarity index 100% rename from doc/OnlineDocs/tests/data/param3.txt rename to doc/OnlineDocs/src/data/param3.txt diff --git a/doc/OnlineDocs/tests/data/param3a.dat b/doc/OnlineDocs/src/data/param3a.dat similarity index 100% rename from doc/OnlineDocs/tests/data/param3a.dat rename to doc/OnlineDocs/src/data/param3a.dat diff --git a/doc/OnlineDocs/tests/data/param3a.py b/doc/OnlineDocs/src/data/param3a.py similarity index 50% rename from doc/OnlineDocs/tests/data/param3a.py rename to doc/OnlineDocs/src/data/param3a.py index 0e99cad0c7a..028e1d07296 100644 --- a/doc/OnlineDocs/tests/data/param3a.py +++ b/doc/OnlineDocs/src/data/param3a.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/tests/data/param3a.txt b/doc/OnlineDocs/src/data/param3a.txt similarity index 100% rename from doc/OnlineDocs/tests/data/param3a.txt rename to doc/OnlineDocs/src/data/param3a.txt diff --git a/doc/OnlineDocs/tests/data/param3b.dat b/doc/OnlineDocs/src/data/param3b.dat similarity index 100% rename from doc/OnlineDocs/tests/data/param3b.dat rename to doc/OnlineDocs/src/data/param3b.dat diff --git a/doc/OnlineDocs/tests/data/param3b.py b/doc/OnlineDocs/src/data/param3b.py similarity index 50% rename from doc/OnlineDocs/tests/data/param3b.py rename to doc/OnlineDocs/src/data/param3b.py index deda175ea12..97f8598610a 100644 --- a/doc/OnlineDocs/tests/data/param3b.py +++ b/doc/OnlineDocs/src/data/param3b.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/tests/data/param3b.txt b/doc/OnlineDocs/src/data/param3b.txt similarity index 100% rename from doc/OnlineDocs/tests/data/param3b.txt rename to doc/OnlineDocs/src/data/param3b.txt diff --git a/doc/OnlineDocs/tests/data/param3c.dat b/doc/OnlineDocs/src/data/param3c.dat similarity index 100% rename from doc/OnlineDocs/tests/data/param3c.dat rename to doc/OnlineDocs/src/data/param3c.dat diff --git a/doc/OnlineDocs/tests/data/param3c.py b/doc/OnlineDocs/src/data/param3c.py similarity index 50% rename from doc/OnlineDocs/tests/data/param3c.py rename to doc/OnlineDocs/src/data/param3c.py index 4056dc8107d..582b0f7db75 100644 --- a/doc/OnlineDocs/tests/data/param3c.py +++ b/doc/OnlineDocs/src/data/param3c.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/tests/data/param3c.txt b/doc/OnlineDocs/src/data/param3c.txt similarity index 100% rename from doc/OnlineDocs/tests/data/param3c.txt rename to doc/OnlineDocs/src/data/param3c.txt diff --git a/doc/OnlineDocs/tests/data/param4.dat b/doc/OnlineDocs/src/data/param4.dat similarity index 100% rename from doc/OnlineDocs/tests/data/param4.dat rename to doc/OnlineDocs/src/data/param4.dat diff --git a/doc/OnlineDocs/src/data/param4.py b/doc/OnlineDocs/src/data/param4.py new file mode 100644 index 00000000000..010c46fc9c5 --- /dev/null +++ b/doc/OnlineDocs/src/data/param4.py @@ -0,0 +1,26 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Set() +model.B = Param(model.A) +# @decl + +instance = model.create_instance('param4.dat') + +print('B') +keys = instance.B.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.B[key]))) diff --git a/doc/OnlineDocs/tests/data/param4.txt b/doc/OnlineDocs/src/data/param4.txt similarity index 100% rename from doc/OnlineDocs/tests/data/param4.txt rename to doc/OnlineDocs/src/data/param4.txt diff --git a/doc/OnlineDocs/tests/data/param5.dat b/doc/OnlineDocs/src/data/param5.dat similarity index 100% rename from doc/OnlineDocs/tests/data/param5.dat rename to doc/OnlineDocs/src/data/param5.dat diff --git a/doc/OnlineDocs/src/data/param5.py b/doc/OnlineDocs/src/data/param5.py new file mode 100644 index 00000000000..2db07f3f990 --- /dev/null +++ b/doc/OnlineDocs/src/data/param5.py @@ -0,0 +1,25 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Set(dimen=2) +model.B = Param(model.A) +# @decl + +instance = model.create_instance('param5.dat') + +keys = instance.B.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.B[key]))) diff --git a/doc/OnlineDocs/tests/data/param5.txt b/doc/OnlineDocs/src/data/param5.txt similarity index 100% rename from doc/OnlineDocs/tests/data/param5.txt rename to doc/OnlineDocs/src/data/param5.txt diff --git a/doc/OnlineDocs/tests/data/param5a.dat b/doc/OnlineDocs/src/data/param5a.dat similarity index 100% rename from doc/OnlineDocs/tests/data/param5a.dat rename to doc/OnlineDocs/src/data/param5a.dat diff --git a/doc/OnlineDocs/src/data/param5a.py b/doc/OnlineDocs/src/data/param5a.py new file mode 100644 index 00000000000..32a53d24e9b --- /dev/null +++ b/doc/OnlineDocs/src/data/param5a.py @@ -0,0 +1,25 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Set(dimen=2) +model.B = Param(model.A) +# @decl + +instance = model.create_instance('param5a.dat') + +keys = instance.B.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.B[key]))) diff --git a/doc/OnlineDocs/tests/data/param5a.txt b/doc/OnlineDocs/src/data/param5a.txt similarity index 100% rename from doc/OnlineDocs/tests/data/param5a.txt rename to doc/OnlineDocs/src/data/param5a.txt diff --git a/doc/OnlineDocs/tests/data/param6.dat b/doc/OnlineDocs/src/data/param6.dat similarity index 100% rename from doc/OnlineDocs/tests/data/param6.dat rename to doc/OnlineDocs/src/data/param6.dat diff --git a/doc/OnlineDocs/tests/data/param6.py b/doc/OnlineDocs/src/data/param6.py similarity index 51% rename from doc/OnlineDocs/tests/data/param6.py rename to doc/OnlineDocs/src/data/param6.py index c3e4b25d144..e3364a933cf 100644 --- a/doc/OnlineDocs/tests/data/param6.py +++ b/doc/OnlineDocs/src/data/param6.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/tests/data/param6.txt b/doc/OnlineDocs/src/data/param6.txt similarity index 100% rename from doc/OnlineDocs/tests/data/param6.txt rename to doc/OnlineDocs/src/data/param6.txt diff --git a/doc/OnlineDocs/tests/data/param6a.dat b/doc/OnlineDocs/src/data/param6a.dat similarity index 100% rename from doc/OnlineDocs/tests/data/param6a.dat rename to doc/OnlineDocs/src/data/param6a.dat diff --git a/doc/OnlineDocs/tests/data/param6a.py b/doc/OnlineDocs/src/data/param6a.py similarity index 51% rename from doc/OnlineDocs/tests/data/param6a.py rename to doc/OnlineDocs/src/data/param6a.py index 07e8280cc18..3d2fa645411 100644 --- a/doc/OnlineDocs/tests/data/param6a.py +++ b/doc/OnlineDocs/src/data/param6a.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/tests/data/param6a.txt b/doc/OnlineDocs/src/data/param6a.txt similarity index 100% rename from doc/OnlineDocs/tests/data/param6a.txt rename to doc/OnlineDocs/src/data/param6a.txt diff --git a/doc/OnlineDocs/tests/data/param7a.dat b/doc/OnlineDocs/src/data/param7a.dat similarity index 100% rename from doc/OnlineDocs/tests/data/param7a.dat rename to doc/OnlineDocs/src/data/param7a.dat diff --git a/doc/OnlineDocs/src/data/param7a.py b/doc/OnlineDocs/src/data/param7a.py new file mode 100644 index 00000000000..b3aba9ec23d --- /dev/null +++ b/doc/OnlineDocs/src/data/param7a.py @@ -0,0 +1,25 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Set(dimen=2) +model.B = Param(model.A) +# @decl + +instance = model.create_instance('param7a.dat') + +keys = instance.B.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.B[key]))) diff --git a/doc/OnlineDocs/tests/data/param7a.txt b/doc/OnlineDocs/src/data/param7a.txt similarity index 100% rename from doc/OnlineDocs/tests/data/param7a.txt rename to doc/OnlineDocs/src/data/param7a.txt diff --git a/doc/OnlineDocs/tests/data/param7b.dat b/doc/OnlineDocs/src/data/param7b.dat similarity index 100% rename from doc/OnlineDocs/tests/data/param7b.dat rename to doc/OnlineDocs/src/data/param7b.dat diff --git a/doc/OnlineDocs/src/data/param7b.py b/doc/OnlineDocs/src/data/param7b.py new file mode 100644 index 00000000000..8b022f399a8 --- /dev/null +++ b/doc/OnlineDocs/src/data/param7b.py @@ -0,0 +1,25 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Set(dimen=2) +model.B = Param(model.A) +# @decl + +instance = model.create_instance('param7b.dat') + +keys = instance.B.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.B[key]))) diff --git a/doc/OnlineDocs/tests/data/param7b.txt b/doc/OnlineDocs/src/data/param7b.txt similarity index 100% rename from doc/OnlineDocs/tests/data/param7b.txt rename to doc/OnlineDocs/src/data/param7b.txt diff --git a/doc/OnlineDocs/tests/data/param8a.dat b/doc/OnlineDocs/src/data/param8a.dat similarity index 100% rename from doc/OnlineDocs/tests/data/param8a.dat rename to doc/OnlineDocs/src/data/param8a.dat diff --git a/doc/OnlineDocs/src/data/param8a.py b/doc/OnlineDocs/src/data/param8a.py new file mode 100644 index 00000000000..abfa885ded4 --- /dev/null +++ b/doc/OnlineDocs/src/data/param8a.py @@ -0,0 +1,25 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Set(dimen=4) +model.B = Param(model.A) +# @decl + +instance = model.create_instance('param8a.dat') + +keys = instance.B.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.B[key]))) diff --git a/doc/OnlineDocs/tests/data/param8a.txt b/doc/OnlineDocs/src/data/param8a.txt similarity index 100% rename from doc/OnlineDocs/tests/data/param8a.txt rename to doc/OnlineDocs/src/data/param8a.txt diff --git a/doc/OnlineDocs/tests/data/pyomo.diet1.sh b/doc/OnlineDocs/src/data/pyomo.diet1.sh similarity index 100% rename from doc/OnlineDocs/tests/data/pyomo.diet1.sh rename to doc/OnlineDocs/src/data/pyomo.diet1.sh diff --git a/doc/OnlineDocs/tests/data/pyomo.diet1.txt b/doc/OnlineDocs/src/data/pyomo.diet1.txt similarity index 86% rename from doc/OnlineDocs/tests/data/pyomo.diet1.txt rename to doc/OnlineDocs/src/data/pyomo.diet1.txt index 4b67e92c80c..fd8c87d51d9 100644 --- a/doc/OnlineDocs/tests/data/pyomo.diet1.txt +++ b/doc/OnlineDocs/src/data/pyomo.diet1.txt @@ -1,16 +1,16 @@ [ 0.00] Setting up Pyomo environment [ 0.00] Applying Pyomo preprocessing actions [ 0.00] Creating model -[ 0.02] Applying solver -[ 0.03] Processing results +[ 0.01] Applying solver +[ 0.02] Processing results Number of solutions: 1 Solution Information Gap: 0.0 Status: optimal Function Value: 2.81 Solver results file: results.yml -[ 0.04] Applying Pyomo postprocessing actions -[ 0.04] Pyomo Finished +[ 0.02] Applying Pyomo postprocessing actions +[ 0.02] Pyomo Finished # ========================================================== # = Solver Results = # ========================================================== @@ -22,9 +22,9 @@ Problem: Lower bound: 2.81 Upper bound: 2.81 Number of objectives: 1 - Number of constraints: 4 - Number of variables: 10 - Number of nonzeros: 10 + Number of constraints: 3 + Number of variables: 9 + Number of nonzeros: 9 Sense: minimize # ---------------------------------------------------------- # Solver Information @@ -37,7 +37,7 @@ Solver: Number of bounded subproblems: 1 Number of created subproblems: 1 Error rc: 0 - Time: 0.00816035270690918 + Time: 0.002644062042236328 # ---------------------------------------------------------- # Solution Information # ---------------------------------------------------------- diff --git a/doc/OnlineDocs/tests/data/pyomo.diet2.sh b/doc/OnlineDocs/src/data/pyomo.diet2.sh similarity index 100% rename from doc/OnlineDocs/tests/data/pyomo.diet2.sh rename to doc/OnlineDocs/src/data/pyomo.diet2.sh diff --git a/doc/OnlineDocs/tests/data/pyomo.diet2.txt b/doc/OnlineDocs/src/data/pyomo.diet2.txt similarity index 86% rename from doc/OnlineDocs/tests/data/pyomo.diet2.txt rename to doc/OnlineDocs/src/data/pyomo.diet2.txt index 00405216fe1..7ed879d500f 100644 --- a/doc/OnlineDocs/tests/data/pyomo.diet2.txt +++ b/doc/OnlineDocs/src/data/pyomo.diet2.txt @@ -1,16 +1,16 @@ [ 0.00] Setting up Pyomo environment [ 0.00] Applying Pyomo preprocessing actions [ 0.00] Creating model -[ 0.03] Applying solver -[ 0.05] Processing results +[ 0.01] Applying solver +[ 0.01] Processing results Number of solutions: 1 Solution Information Gap: 0.0 Status: optimal Function Value: 2.81 Solver results file: results.yml -[ 0.05] Applying Pyomo postprocessing actions -[ 0.05] Pyomo Finished +[ 0.01] Applying Pyomo postprocessing actions +[ 0.01] Pyomo Finished # ========================================================== # = Solver Results = # ========================================================== @@ -22,9 +22,9 @@ Problem: Lower bound: 2.81 Upper bound: 2.81 Number of objectives: 1 - Number of constraints: 4 - Number of variables: 10 - Number of nonzeros: 10 + Number of constraints: 3 + Number of variables: 9 + Number of nonzeros: 9 Sense: minimize # ---------------------------------------------------------- # Solver Information @@ -37,7 +37,7 @@ Solver: Number of bounded subproblems: 1 Number of created subproblems: 1 Error rc: 0 - Time: 0.006503582000732422 + Time: 0.0018515586853027344 # ---------------------------------------------------------- # Solution Information # ---------------------------------------------------------- diff --git a/doc/OnlineDocs/tests/data/set1.dat b/doc/OnlineDocs/src/data/set1.dat similarity index 100% rename from doc/OnlineDocs/tests/data/set1.dat rename to doc/OnlineDocs/src/data/set1.dat diff --git a/doc/OnlineDocs/src/data/set1.py b/doc/OnlineDocs/src/data/set1.py new file mode 100644 index 00000000000..c84c1ef0819 --- /dev/null +++ b/doc/OnlineDocs/src/data/set1.py @@ -0,0 +1,24 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.A = Set() +model.B = Set() +model.C = Set() + +instance = model.create_instance('set1.dat') + +print(sorted(list(instance.A.data()))) +print(sorted((instance.B.data()))) +print(sorted(list((instance.C.data())), key=lambda x: x if type(x) is str else str(x))) diff --git a/doc/OnlineDocs/tests/data/set1.txt b/doc/OnlineDocs/src/data/set1.txt similarity index 100% rename from doc/OnlineDocs/tests/data/set1.txt rename to doc/OnlineDocs/src/data/set1.txt diff --git a/doc/OnlineDocs/tests/data/set2.dat b/doc/OnlineDocs/src/data/set2.dat similarity index 100% rename from doc/OnlineDocs/tests/data/set2.dat rename to doc/OnlineDocs/src/data/set2.dat diff --git a/doc/OnlineDocs/src/data/set2.py b/doc/OnlineDocs/src/data/set2.py new file mode 100644 index 00000000000..9048a49fecb --- /dev/null +++ b/doc/OnlineDocs/src/data/set2.py @@ -0,0 +1,22 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Set(dimen=3) +# @decl + +instance = model.create_instance('set2.dat') + +print(sorted(list(instance.A.data()))) diff --git a/doc/OnlineDocs/tests/data/set2.txt b/doc/OnlineDocs/src/data/set2.txt similarity index 100% rename from doc/OnlineDocs/tests/data/set2.txt rename to doc/OnlineDocs/src/data/set2.txt diff --git a/doc/OnlineDocs/tests/data/set2a.dat b/doc/OnlineDocs/src/data/set2a.dat similarity index 100% rename from doc/OnlineDocs/tests/data/set2a.dat rename to doc/OnlineDocs/src/data/set2a.dat diff --git a/doc/OnlineDocs/src/data/set2a.py b/doc/OnlineDocs/src/data/set2a.py new file mode 100644 index 00000000000..f2fa4d71916 --- /dev/null +++ b/doc/OnlineDocs/src/data/set2a.py @@ -0,0 +1,22 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Set(dimen=3) +# @decl + +instance = model.create_instance('set2a.dat') + +print(sorted(list(instance.A.data()))) diff --git a/doc/OnlineDocs/tests/data/set2a.txt b/doc/OnlineDocs/src/data/set2a.txt similarity index 100% rename from doc/OnlineDocs/tests/data/set2a.txt rename to doc/OnlineDocs/src/data/set2a.txt diff --git a/doc/OnlineDocs/tests/data/set3.dat b/doc/OnlineDocs/src/data/set3.dat similarity index 100% rename from doc/OnlineDocs/tests/data/set3.dat rename to doc/OnlineDocs/src/data/set3.dat diff --git a/doc/OnlineDocs/src/data/set3.py b/doc/OnlineDocs/src/data/set3.py new file mode 100644 index 00000000000..9cdacbe39e0 --- /dev/null +++ b/doc/OnlineDocs/src/data/set3.py @@ -0,0 +1,30 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Set() +model.B = Set(model.A) +# @decl +# model.C = Set(model.A,model.A) + +instance = model.create_instance('set3.dat') + +print(sorted(list(instance.A.data()), key=lambda x: x if type(x) is str else str(x))) +print(sorted(list(instance.B[1].data()), key=lambda x: x if type(x) is str else str(x))) +print( + sorted( + list(instance.B['aaa'].data()), key=lambda x: x if type(x) is str else str(x) + ) +) diff --git a/doc/OnlineDocs/tests/data/set3.txt b/doc/OnlineDocs/src/data/set3.txt similarity index 100% rename from doc/OnlineDocs/tests/data/set3.txt rename to doc/OnlineDocs/src/data/set3.txt diff --git a/doc/OnlineDocs/tests/data/set4.dat b/doc/OnlineDocs/src/data/set4.dat similarity index 100% rename from doc/OnlineDocs/tests/data/set4.dat rename to doc/OnlineDocs/src/data/set4.dat diff --git a/doc/OnlineDocs/src/data/set4.py b/doc/OnlineDocs/src/data/set4.py new file mode 100644 index 00000000000..b3485638c6f --- /dev/null +++ b/doc/OnlineDocs/src/data/set4.py @@ -0,0 +1,22 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Set(dimen=2) +# @decl + +instance = model.create_instance('set4.dat') + +print(sorted(list(instance.A.data()))) diff --git a/doc/OnlineDocs/tests/data/set4.txt b/doc/OnlineDocs/src/data/set4.txt similarity index 100% rename from doc/OnlineDocs/tests/data/set4.txt rename to doc/OnlineDocs/src/data/set4.txt diff --git a/doc/OnlineDocs/tests/data/set5.dat b/doc/OnlineDocs/src/data/set5.dat similarity index 100% rename from doc/OnlineDocs/tests/data/set5.dat rename to doc/OnlineDocs/src/data/set5.dat diff --git a/doc/OnlineDocs/src/data/set5.py b/doc/OnlineDocs/src/data/set5.py new file mode 100644 index 00000000000..d745d8408d0 --- /dev/null +++ b/doc/OnlineDocs/src/data/set5.py @@ -0,0 +1,24 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Set(dimen=4) +# @decl + +instance = model.create_instance('set5.dat') + + +for tpl in sorted(list(instance.A.data()), key=lambda x: tuple(map(str, x))): + print(tpl) diff --git a/doc/OnlineDocs/tests/data/set5.txt b/doc/OnlineDocs/src/data/set5.txt similarity index 100% rename from doc/OnlineDocs/tests/data/set5.txt rename to doc/OnlineDocs/src/data/set5.txt diff --git a/doc/OnlineDocs/tests/data/table0.dat b/doc/OnlineDocs/src/data/table0.dat similarity index 100% rename from doc/OnlineDocs/tests/data/table0.dat rename to doc/OnlineDocs/src/data/table0.dat diff --git a/doc/OnlineDocs/src/data/table0.py b/doc/OnlineDocs/src/data/table0.py new file mode 100644 index 00000000000..de0fae0c861 --- /dev/null +++ b/doc/OnlineDocs/src/data/table0.py @@ -0,0 +1,20 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.A = Set(initialize=['A1', 'A2', 'A3']) +model.M = Param(model.A) + +instance = model.create_instance('table0.dat') +instance.pprint() diff --git a/doc/OnlineDocs/tests/data/table0.txt b/doc/OnlineDocs/src/data/table0.txt similarity index 57% rename from doc/OnlineDocs/tests/data/table0.txt rename to doc/OnlineDocs/src/data/table0.txt index ecd2417333d..c2e75dd97a6 100644 --- a/doc/OnlineDocs/tests/data/table0.txt +++ b/doc/OnlineDocs/src/data/table0.txt @@ -1,6 +1,7 @@ 1 Set Declarations - A : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=None - ['A1', 'A2', 'A3'] + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} 1 Param Declarations M : Size=3, Index=A, Domain=Any, Default=None, Mutable=False diff --git a/doc/OnlineDocs/tests/data/table0.ul.dat b/doc/OnlineDocs/src/data/table0.ul.dat similarity index 100% rename from doc/OnlineDocs/tests/data/table0.ul.dat rename to doc/OnlineDocs/src/data/table0.ul.dat diff --git a/doc/OnlineDocs/src/data/table0.ul.py b/doc/OnlineDocs/src/data/table0.ul.py new file mode 100644 index 00000000000..524c3756782 --- /dev/null +++ b/doc/OnlineDocs/src/data/table0.ul.py @@ -0,0 +1,20 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.A = Set(initialize=['A1', 'A2', 'A3']) +model.M = Param(model.A) + +instance = model.create_instance('table0.ul.dat') +instance.pprint() diff --git a/doc/OnlineDocs/tests/data/table0.ul.txt b/doc/OnlineDocs/src/data/table0.ul.txt similarity index 57% rename from doc/OnlineDocs/tests/data/table0.ul.txt rename to doc/OnlineDocs/src/data/table0.ul.txt index ecd2417333d..c2e75dd97a6 100644 --- a/doc/OnlineDocs/tests/data/table0.ul.txt +++ b/doc/OnlineDocs/src/data/table0.ul.txt @@ -1,6 +1,7 @@ 1 Set Declarations - A : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=None - ['A1', 'A2', 'A3'] + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} 1 Param Declarations M : Size=3, Index=A, Domain=Any, Default=None, Mutable=False diff --git a/doc/OnlineDocs/tests/data/table1.dat b/doc/OnlineDocs/src/data/table1.dat similarity index 100% rename from doc/OnlineDocs/tests/data/table1.dat rename to doc/OnlineDocs/src/data/table1.dat diff --git a/doc/OnlineDocs/src/data/table1.py b/doc/OnlineDocs/src/data/table1.py new file mode 100644 index 00000000000..f36714b8f1f --- /dev/null +++ b/doc/OnlineDocs/src/data/table1.py @@ -0,0 +1,20 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.A = Set(initialize=['A1', 'A2', 'A3']) +model.M = Param(model.A) + +instance = model.create_instance('table1.dat') +instance.pprint() diff --git a/doc/OnlineDocs/tests/data/table1.txt b/doc/OnlineDocs/src/data/table1.txt similarity index 57% rename from doc/OnlineDocs/tests/data/table1.txt rename to doc/OnlineDocs/src/data/table1.txt index ecd2417333d..c2e75dd97a6 100644 --- a/doc/OnlineDocs/tests/data/table1.txt +++ b/doc/OnlineDocs/src/data/table1.txt @@ -1,6 +1,7 @@ 1 Set Declarations - A : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=None - ['A1', 'A2', 'A3'] + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} 1 Param Declarations M : Size=3, Index=A, Domain=Any, Default=None, Mutable=False diff --git a/doc/OnlineDocs/tests/data/table2.dat b/doc/OnlineDocs/src/data/table2.dat similarity index 100% rename from doc/OnlineDocs/tests/data/table2.dat rename to doc/OnlineDocs/src/data/table2.dat diff --git a/doc/OnlineDocs/src/data/table2.py b/doc/OnlineDocs/src/data/table2.py new file mode 100644 index 00000000000..03648a00f8c --- /dev/null +++ b/doc/OnlineDocs/src/data/table2.py @@ -0,0 +1,23 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.A = Set(initialize=['A1', 'A2', 'A3']) +model.B = Set(initialize=['B1', 'B2', 'B3']) + +model.M = Param(model.A) +model.N = Param(model.A, model.B) + +instance = model.create_instance('table2.dat') +instance.pprint() diff --git a/doc/OnlineDocs/src/data/table2.txt b/doc/OnlineDocs/src/data/table2.txt new file mode 100644 index 00000000000..a710b6b6042 --- /dev/null +++ b/doc/OnlineDocs/src/data/table2.txt @@ -0,0 +1,21 @@ +2 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + B : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'B1', 'B2', 'B3'} + +2 Param Declarations + M : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 4.3 + A2 : 4.4 + A3 : 4.5 + N : Size=3, Index=A*B, Domain=Any, Default=None, Mutable=False + Key : Value + ('A1', 'B1') : 5.3 + ('A2', 'B2') : 5.4 + ('A3', 'B3') : 5.5 + +4 Declarations: A B M N diff --git a/doc/OnlineDocs/tests/data/table3.dat b/doc/OnlineDocs/src/data/table3.dat similarity index 100% rename from doc/OnlineDocs/tests/data/table3.dat rename to doc/OnlineDocs/src/data/table3.dat diff --git a/doc/OnlineDocs/src/data/table3.py b/doc/OnlineDocs/src/data/table3.py new file mode 100644 index 00000000000..2c598f112df --- /dev/null +++ b/doc/OnlineDocs/src/data/table3.py @@ -0,0 +1,25 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.A = Set() +model.B = Set(initialize=['B1', 'B2', 'B3']) +model.Z = Set(dimen=2) + +model.M = Param(model.A) +model.N = Param(model.A, model.B) + + +instance = model.create_instance('table3.dat') +instance.pprint() diff --git a/doc/OnlineDocs/src/data/table3.txt b/doc/OnlineDocs/src/data/table3.txt new file mode 100644 index 00000000000..c0c61cd5a5b --- /dev/null +++ b/doc/OnlineDocs/src/data/table3.txt @@ -0,0 +1,24 @@ +3 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + B : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'B1', 'B2', 'B3'} + Z : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} + +2 Param Declarations + M : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 4.3 + A2 : 4.4 + A3 : 4.5 + N : Size=3, Index=A*B, Domain=Any, Default=None, Mutable=False + Key : Value + ('A1', 'B1') : 5.3 + ('A2', 'B2') : 5.4 + ('A3', 'B3') : 5.5 + +5 Declarations: A B Z M N diff --git a/doc/OnlineDocs/tests/data/table3.ul.dat b/doc/OnlineDocs/src/data/table3.ul.dat similarity index 100% rename from doc/OnlineDocs/tests/data/table3.ul.dat rename to doc/OnlineDocs/src/data/table3.ul.dat diff --git a/doc/OnlineDocs/src/data/table3.ul.py b/doc/OnlineDocs/src/data/table3.ul.py new file mode 100644 index 00000000000..18ced12b388 --- /dev/null +++ b/doc/OnlineDocs/src/data/table3.ul.py @@ -0,0 +1,25 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.A = Set() +model.B = Set(initialize=['B1', 'B2', 'B3']) +model.Z = Set(dimen=2) + +model.M = Param(model.A) +model.N = Param(model.A, model.B) + + +instance = model.create_instance('table3.ul.dat') +instance.pprint() diff --git a/doc/OnlineDocs/src/data/table3.ul.txt b/doc/OnlineDocs/src/data/table3.ul.txt new file mode 100644 index 00000000000..c0c61cd5a5b --- /dev/null +++ b/doc/OnlineDocs/src/data/table3.ul.txt @@ -0,0 +1,24 @@ +3 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + B : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'B1', 'B2', 'B3'} + Z : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} + +2 Param Declarations + M : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 4.3 + A2 : 4.4 + A3 : 4.5 + N : Size=3, Index=A*B, Domain=Any, Default=None, Mutable=False + Key : Value + ('A1', 'B1') : 5.3 + ('A2', 'B2') : 5.4 + ('A3', 'B3') : 5.5 + +5 Declarations: A B Z M N diff --git a/doc/OnlineDocs/tests/data/table4.dat b/doc/OnlineDocs/src/data/table4.dat similarity index 100% rename from doc/OnlineDocs/tests/data/table4.dat rename to doc/OnlineDocs/src/data/table4.dat diff --git a/doc/OnlineDocs/src/data/table4.py b/doc/OnlineDocs/src/data/table4.py new file mode 100644 index 00000000000..bd20682b5a9 --- /dev/null +++ b/doc/OnlineDocs/src/data/table4.py @@ -0,0 +1,23 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.A = Set() +model.Z = Set(dimen=2) + +model.M = Param(model.A) +model.N = Param(model.Z) + +instance = model.create_instance('table4.dat') +instance.pprint() diff --git a/doc/OnlineDocs/tests/data/table4.txt b/doc/OnlineDocs/src/data/table4.txt similarity index 54% rename from doc/OnlineDocs/tests/data/table4.txt rename to doc/OnlineDocs/src/data/table4.txt index eb49be14de2..f86004c342a 100644 --- a/doc/OnlineDocs/tests/data/table4.txt +++ b/doc/OnlineDocs/src/data/table4.txt @@ -1,8 +1,10 @@ 2 Set Declarations - A : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=None - ['A1', 'A2', 'A3'] - Z : Dim=0, Dimen=2, Size=3, Domain=None, Ordered=False, Bounds=None - [('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')] + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + Z : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} 2 Param Declarations M : Size=3, Index=A, Domain=Any, Default=None, Mutable=False diff --git a/doc/OnlineDocs/tests/data/table4.ul.dat b/doc/OnlineDocs/src/data/table4.ul.dat similarity index 100% rename from doc/OnlineDocs/tests/data/table4.ul.dat rename to doc/OnlineDocs/src/data/table4.ul.dat diff --git a/doc/OnlineDocs/src/data/table4.ul.py b/doc/OnlineDocs/src/data/table4.ul.py new file mode 100644 index 00000000000..9f16f21fe19 --- /dev/null +++ b/doc/OnlineDocs/src/data/table4.ul.py @@ -0,0 +1,23 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.A = Set() +model.Z = Set(dimen=2) + +model.M = Param(model.A) +model.N = Param(model.Z) + +instance = model.create_instance('table4.ul.dat') +instance.pprint() diff --git a/doc/OnlineDocs/tests/data/table4.ul.txt b/doc/OnlineDocs/src/data/table4.ul.txt similarity index 54% rename from doc/OnlineDocs/tests/data/table4.ul.txt rename to doc/OnlineDocs/src/data/table4.ul.txt index eb49be14de2..f86004c342a 100644 --- a/doc/OnlineDocs/tests/data/table4.ul.txt +++ b/doc/OnlineDocs/src/data/table4.ul.txt @@ -1,8 +1,10 @@ 2 Set Declarations - A : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=None - ['A1', 'A2', 'A3'] - Z : Dim=0, Dimen=2, Size=3, Domain=None, Ordered=False, Bounds=None - [('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')] + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + Z : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} 2 Param Declarations M : Size=3, Index=A, Domain=Any, Default=None, Mutable=False diff --git a/doc/OnlineDocs/tests/data/table5.dat b/doc/OnlineDocs/src/data/table5.dat similarity index 100% rename from doc/OnlineDocs/tests/data/table5.dat rename to doc/OnlineDocs/src/data/table5.dat diff --git a/doc/OnlineDocs/src/data/table5.py b/doc/OnlineDocs/src/data/table5.py new file mode 100644 index 00000000000..a3cb01209a2 --- /dev/null +++ b/doc/OnlineDocs/src/data/table5.py @@ -0,0 +1,20 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.Z = Set(dimen=2) +model.Y = Set(dimen=2) + +instance = model.create_instance('table5.dat') +instance.pprint() diff --git a/doc/OnlineDocs/src/data/table5.txt b/doc/OnlineDocs/src/data/table5.txt new file mode 100644 index 00000000000..084757b781b --- /dev/null +++ b/doc/OnlineDocs/src/data/table5.txt @@ -0,0 +1,9 @@ +2 Set Declarations + Y : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 3 : {(4.3, 5.3), (4.4, 5.4), (4.5, 5.5)} + Z : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} + +2 Declarations: Z Y diff --git a/doc/OnlineDocs/tests/data/table6.dat b/doc/OnlineDocs/src/data/table6.dat similarity index 100% rename from doc/OnlineDocs/tests/data/table6.dat rename to doc/OnlineDocs/src/data/table6.dat diff --git a/doc/OnlineDocs/src/data/table6.py b/doc/OnlineDocs/src/data/table6.py new file mode 100644 index 00000000000..1db0a764a23 --- /dev/null +++ b/doc/OnlineDocs/src/data/table6.py @@ -0,0 +1,19 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.pi = Param() + +instance = model.create_instance('table6.dat') +instance.pprint() diff --git a/doc/OnlineDocs/tests/data/table6.txt b/doc/OnlineDocs/src/data/table6.txt similarity index 100% rename from doc/OnlineDocs/tests/data/table6.txt rename to doc/OnlineDocs/src/data/table6.txt diff --git a/doc/OnlineDocs/tests/data/table7.dat b/doc/OnlineDocs/src/data/table7.dat similarity index 100% rename from doc/OnlineDocs/tests/data/table7.dat rename to doc/OnlineDocs/src/data/table7.dat diff --git a/doc/OnlineDocs/src/data/table7.py b/doc/OnlineDocs/src/data/table7.py new file mode 100644 index 00000000000..84a841aca86 --- /dev/null +++ b/doc/OnlineDocs/src/data/table7.py @@ -0,0 +1,21 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.A = Set(initialize=['A1', 'A2', 'A3']) +model.M = Param(model.A) +model.Z = Set(dimen=2) + +instance = model.create_instance('table7.dat') +instance.pprint() diff --git a/doc/OnlineDocs/src/data/table7.txt b/doc/OnlineDocs/src/data/table7.txt new file mode 100644 index 00000000000..8ddbfde38be --- /dev/null +++ b/doc/OnlineDocs/src/data/table7.txt @@ -0,0 +1,16 @@ +2 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + Z : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} + +1 Param Declarations + M : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 4.3 + A2 : 4.4 + A3 : 4.5 + +3 Declarations: A M Z diff --git a/doc/OnlineDocs/tests/dataportal/A.tab b/doc/OnlineDocs/src/dataportal/A.tab similarity index 100% rename from doc/OnlineDocs/tests/dataportal/A.tab rename to doc/OnlineDocs/src/dataportal/A.tab diff --git a/doc/OnlineDocs/tests/dataportal/C.tab b/doc/OnlineDocs/src/dataportal/C.tab similarity index 100% rename from doc/OnlineDocs/tests/dataportal/C.tab rename to doc/OnlineDocs/src/dataportal/C.tab diff --git a/doc/OnlineDocs/tests/dataportal/D.tab b/doc/OnlineDocs/src/dataportal/D.tab similarity index 100% rename from doc/OnlineDocs/tests/dataportal/D.tab rename to doc/OnlineDocs/src/dataportal/D.tab diff --git a/doc/OnlineDocs/tests/dataportal/PP.csv b/doc/OnlineDocs/src/dataportal/PP.csv similarity index 100% rename from doc/OnlineDocs/tests/dataportal/PP.csv rename to doc/OnlineDocs/src/dataportal/PP.csv diff --git a/doc/OnlineDocs/tests/dataportal/PP.json b/doc/OnlineDocs/src/dataportal/PP.json similarity index 100% rename from doc/OnlineDocs/tests/dataportal/PP.json rename to doc/OnlineDocs/src/dataportal/PP.json diff --git a/doc/OnlineDocs/tests/dataportal/PP.sqlite b/doc/OnlineDocs/src/dataportal/PP.sqlite similarity index 100% rename from doc/OnlineDocs/tests/dataportal/PP.sqlite rename to doc/OnlineDocs/src/dataportal/PP.sqlite diff --git a/doc/OnlineDocs/tests/dataportal/PP.tab b/doc/OnlineDocs/src/dataportal/PP.tab similarity index 100% rename from doc/OnlineDocs/tests/dataportal/PP.tab rename to doc/OnlineDocs/src/dataportal/PP.tab diff --git a/doc/OnlineDocs/tests/dataportal/PP.xml b/doc/OnlineDocs/src/dataportal/PP.xml similarity index 100% rename from doc/OnlineDocs/tests/dataportal/PP.xml rename to doc/OnlineDocs/src/dataportal/PP.xml diff --git a/doc/OnlineDocs/tests/dataportal/PP.yaml b/doc/OnlineDocs/src/dataportal/PP.yaml similarity index 100% rename from doc/OnlineDocs/tests/dataportal/PP.yaml rename to doc/OnlineDocs/src/dataportal/PP.yaml diff --git a/doc/OnlineDocs/tests/dataportal/PP_sqlite.py b/doc/OnlineDocs/src/dataportal/PP_sqlite.py similarity index 97% rename from doc/OnlineDocs/tests/dataportal/PP_sqlite.py rename to doc/OnlineDocs/src/dataportal/PP_sqlite.py index 9c6fc5ddc0b..1592e820900 100644 --- a/doc/OnlineDocs/tests/dataportal/PP_sqlite.py +++ b/doc/OnlineDocs/src/dataportal/PP_sqlite.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/tests/dataportal/Pyomo_mysql b/doc/OnlineDocs/src/dataportal/Pyomo_mysql similarity index 100% rename from doc/OnlineDocs/tests/dataportal/Pyomo_mysql rename to doc/OnlineDocs/src/dataportal/Pyomo_mysql diff --git a/doc/OnlineDocs/tests/dataportal/S.tab b/doc/OnlineDocs/src/dataportal/S.tab similarity index 100% rename from doc/OnlineDocs/tests/dataportal/S.tab rename to doc/OnlineDocs/src/dataportal/S.tab diff --git a/doc/OnlineDocs/tests/dataportal/T.json b/doc/OnlineDocs/src/dataportal/T.json similarity index 100% rename from doc/OnlineDocs/tests/dataportal/T.json rename to doc/OnlineDocs/src/dataportal/T.json diff --git a/doc/OnlineDocs/tests/dataportal/T.yaml b/doc/OnlineDocs/src/dataportal/T.yaml similarity index 100% rename from doc/OnlineDocs/tests/dataportal/T.yaml rename to doc/OnlineDocs/src/dataportal/T.yaml diff --git a/doc/OnlineDocs/tests/dataportal/U.tab b/doc/OnlineDocs/src/dataportal/U.tab similarity index 100% rename from doc/OnlineDocs/tests/dataportal/U.tab rename to doc/OnlineDocs/src/dataportal/U.tab diff --git a/doc/OnlineDocs/tests/dataportal/XW.tab b/doc/OnlineDocs/src/dataportal/XW.tab similarity index 100% rename from doc/OnlineDocs/tests/dataportal/XW.tab rename to doc/OnlineDocs/src/dataportal/XW.tab diff --git a/doc/OnlineDocs/tests/dataportal/Y.tab b/doc/OnlineDocs/src/dataportal/Y.tab similarity index 100% rename from doc/OnlineDocs/tests/dataportal/Y.tab rename to doc/OnlineDocs/src/dataportal/Y.tab diff --git a/doc/OnlineDocs/tests/dataportal/Z.tab b/doc/OnlineDocs/src/dataportal/Z.tab similarity index 100% rename from doc/OnlineDocs/tests/dataportal/Z.tab rename to doc/OnlineDocs/src/dataportal/Z.tab diff --git a/doc/OnlineDocs/tests/dataportal/dataportal_tab.py b/doc/OnlineDocs/src/dataportal/dataportal_tab.py similarity index 94% rename from doc/OnlineDocs/tests/dataportal/dataportal_tab.py rename to doc/OnlineDocs/src/dataportal/dataportal_tab.py index d1a75196c99..655329d31de 100644 --- a/doc/OnlineDocs/tests/dataportal/dataportal_tab.py +++ b/doc/OnlineDocs/src/dataportal/dataportal_tab.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * # -------------------------------------------------- diff --git a/doc/OnlineDocs/src/dataportal/dataportal_tab.txt b/doc/OnlineDocs/src/dataportal/dataportal_tab.txt new file mode 100644 index 00000000000..a23c63d90c9 --- /dev/null +++ b/doc/OnlineDocs/src/dataportal/dataportal_tab.txt @@ -0,0 +1,315 @@ +1 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + +1 Declarations: A +1 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + +1 Declarations: A +1 Set Declarations + C : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 9 : {('A1', 1), ('A1', 2), ('A1', 3), ('A2', 1), ('A2', 2), ('A2', 3), ('A3', 1), ('A3', 2), ('A3', 3)} + +1 Declarations: C +1 Set Declarations + D : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 3 : {('A1', 1), ('A2', 2), ('A3', 3)} + +1 Declarations: D +1 Param Declarations + z : Size=1, Index=None, Domain=Any, Default=None, Mutable=False + Key : Value + None : 1.1 + +1 Declarations: z +1 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + +1 Param Declarations + y : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 3.3 + A2 : 3.4 + A3 : 3.5 + +2 Declarations: A y +1 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + +2 Param Declarations + w : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 4.3 + A2 : 4.4 + A3 : 4.5 + x : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 3.3 + A2 : 3.4 + A3 : 3.5 + +3 Declarations: A x w +1 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + +1 Param Declarations + y : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 3.3 + A2 : 3.4 + A3 : 3.5 + +2 Declarations: A y +1 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + +1 Param Declarations + w : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 4.3 + A2 : 4.4 + A3 : 4.5 + +2 Declarations: A w +2 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + I : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 4 : {'I1', 'I2', 'I3', 'I4'} + +1 Param Declarations + u : Size=12, Index=I*A, Domain=Any, Default=None, Mutable=False + Key : Value + ('I1', 'A1') : 1.3 + ('I1', 'A2') : 2.3 + ('I1', 'A3') : 3.3 + ('I2', 'A1') : 1.4 + ('I2', 'A2') : 2.4 + ('I2', 'A3') : 3.4 + ('I3', 'A1') : 1.5 + ('I3', 'A2') : 2.5 + ('I3', 'A3') : 3.5 + ('I4', 'A1') : 1.6 + ('I4', 'A2') : 2.6 + ('I4', 'A3') : 3.6 + +3 Declarations: A I u +2 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + I : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 4 : {'I1', 'I2', 'I3', 'I4'} + +1 Param Declarations + t : Size=12, Index=A*I, Domain=Any, Default=None, Mutable=False + Key : Value + ('A1', 'I1') : 1.3 + ('A1', 'I2') : 1.4 + ('A1', 'I3') : 1.5 + ('A1', 'I4') : 1.6 + ('A2', 'I1') : 2.3 + ('A2', 'I2') : 2.4 + ('A2', 'I3') : 2.5 + ('A2', 'I4') : 2.6 + ('A3', 'I1') : 3.3 + ('A3', 'I2') : 3.4 + ('A3', 'I3') : 3.5 + ('A3', 'I4') : 3.6 + +3 Declarations: A I t +1 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + +1 Param Declarations + s : Size=2, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 3.3 + A3 : 3.5 + +2 Declarations: A s +1 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 4 : {'A1', 'A2', 'A3', 'A4'} + +1 Param Declarations + y : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 3.3 + A2 : 3.4 + A3 : 3.5 + +2 Declarations: A y +1 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} + +1 Param Declarations + p : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + ('A1', 'B1') : 4.3 + ('A2', 'B2') : 4.4 + ('A3', 'B3') : 4.5 + +2 Declarations: A p +1 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + +1 Declarations: A + +2 Param Declarations + y : Size=3, Index={A1, A2, A3}, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 3.3 + A2 : 3.4 + A3 : 3.5 + z : Size=1, Index=None, Domain=Any, Default=None, Mutable=False + Key : Value + None : 1.1 + +2 Declarations: z y +['A1', 'A2', 'A3'] +1.1 +A1 3.3 +A2 3.4 +A3 3.5 +1 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} + +1 Param Declarations + p : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + ('A1', 'B1') : 4.3 + ('A2', 'B2') : 4.4 + ('A3', 'B3') : 4.5 + +2 Declarations: A p +1 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 0 : {} + +1 Param Declarations + p : Size=0, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + +2 Declarations: A p +1 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} + +1 Param Declarations + p : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + ('A1', 'B1') : 4.3 + ('A2', 'B2') : 4.4 + ('A3', 'B3') : 4.5 + +2 Declarations: A p +1 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + +1 Param Declarations + p : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 4.3 + A2 : 4.4 + A3 : 4.5 + +2 Declarations: A p +3 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + B : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 3 : {(1, 'B1'), (2, 'B2'), (3, 'B3')} + C : Size=2, Index=A, Ordered=Insertion + Key : Dimen : Domain : Size : Members + A1 : 1 : Any : 3 : {1, 2, 3} + A3 : 1 : Any : 3 : {10, 20, 30} + +3 Param Declarations + p : Size=1, Index=None, Domain=Any, Default=None, Mutable=False + Key : Value + None : 0.1 + q : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 3.3 + A2 : 3.4 + A3 : 3.5 + r : Size=3, Index=B, Domain=Any, Default=None, Mutable=False + Key : Value + (1, 'B1') : 3.3 + (2, 'B2') : 3.4 + (3, 'B3') : 3.5 + +6 Declarations: A B C p q r +3 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + B : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 3 : {(1, 'B1'), (2, 'B2'), (3, 'B3')} + C : Size=2, Index=A, Ordered=Insertion + Key : Dimen : Domain : Size : Members + A1 : 1 : Any : 3 : {1, 2, 3} + A3 : 1 : Any : 3 : {10, 20, 30} + +3 Param Declarations + p : Size=1, Index=None, Domain=Any, Default=None, Mutable=False + Key : Value + None : 0.1 + q : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 3.3 + A2 : 3.4 + A3 : 3.5 + r : Size=3, Index=B, Domain=Any, Default=None, Mutable=False + Key : Value + (1, 'B1') : 3.3 + (2, 'B2') : 3.4 + (3, 'B3') : 3.5 + +6 Declarations: A B C p q r +1 Set Declarations + C : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 9 : {('A1', 1), ('A1', 2), ('A1', 3), ('A2', 1), ('A2', 2), ('A2', 3), ('A3', 1), ('A3', 2), ('A3', 3)} + +1 Declarations: C +1 Set Declarations + C : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 3 : {('A1', 1), ('A2', 2), ('A3', 3)} + +1 Declarations: C diff --git a/doc/OnlineDocs/tests/dataportal/excel.xls b/doc/OnlineDocs/src/dataportal/excel.xls similarity index 100% rename from doc/OnlineDocs/tests/dataportal/excel.xls rename to doc/OnlineDocs/src/dataportal/excel.xls diff --git a/doc/OnlineDocs/src/dataportal/param_initialization.py b/doc/OnlineDocs/src/dataportal/param_initialization.py new file mode 100644 index 00000000000..7f9270b5fda --- /dev/null +++ b/doc/OnlineDocs/src/dataportal/param_initialization.py @@ -0,0 +1,36 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * +import numpy + +model = ConcreteModel() + +# @decl1 +model.a = Param(initialize=1.1) +# @decl1 + +# Initialize with a dictionary +# @decl2 +model.b = Param([1, 2, 3], initialize={1: 1, 2: 2, 3: 3}) +# @decl2 + + +# Initialize with a function that returns native Python data +# @decl3 +def c(model): + return {1: 1, 2: 2, 3: 3} + + +model.c = Param([1, 2, 3], initialize=c) +# @decl3 + +model.pprint(verbose=True) diff --git a/doc/OnlineDocs/src/dataportal/param_initialization.txt b/doc/OnlineDocs/src/dataportal/param_initialization.txt new file mode 100644 index 00000000000..49ea105f120 --- /dev/null +++ b/doc/OnlineDocs/src/dataportal/param_initialization.txt @@ -0,0 +1,16 @@ +3 Param Declarations + a : Size=1, Index=None, Domain=Any, Default=None, Mutable=False + Key : Value + None : 1.1 + b : Size=3, Index={1, 2, 3}, Domain=Any, Default=None, Mutable=False + Key : Value + 1 : 1 + 2 : 2 + 3 : 3 + c : Size=3, Index={1, 2, 3}, Domain=Any, Default=None, Mutable=False + Key : Value + 1 : 1 + 2 : 2 + 3 : 3 + +3 Declarations: a b c diff --git a/doc/OnlineDocs/tests/dataportal/set_initialization.py b/doc/OnlineDocs/src/dataportal/set_initialization.py similarity index 60% rename from doc/OnlineDocs/tests/dataportal/set_initialization.py rename to doc/OnlineDocs/src/dataportal/set_initialization.py index aa7b426fa82..a5ab03894e3 100644 --- a/doc/OnlineDocs/tests/dataportal/set_initialization.py +++ b/doc/OnlineDocs/src/dataportal/set_initialization.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * import numpy diff --git a/doc/OnlineDocs/src/dataportal/set_initialization.txt b/doc/OnlineDocs/src/dataportal/set_initialization.txt new file mode 100644 index 00000000000..3c2960ce4ef --- /dev/null +++ b/doc/OnlineDocs/src/dataportal/set_initialization.txt @@ -0,0 +1,31 @@ +WARNING: Initializing ordered Set B with a fundamentally unordered data source +(type: set). This WILL potentially lead to nondeterministic behavior in Pyomo +8 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {2, 3, 5} + B : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {2, 3, 5} + C : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {2, 3, 5} + D : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 9 : {0, 1, 2, 3, 4, 5, 6, 7, 8} + E : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 1 : {2,} + F : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {2, 3, 5} + G : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {2, 3, 5} + H : Size=3, Index={2, 3, 4}, Ordered=Insertion + Key : Dimen : Domain : Size : Members + 2 : 1 : Any : 3 : {1, 3, 5} + 3 : 1 : Any : 3 : {2, 4, 6} + 4 : 1 : Any : 3 : {3, 5, 7} + +8 Declarations: A B C D E F G H diff --git a/doc/OnlineDocs/tests/expr/design.py b/doc/OnlineDocs/src/expr/design.py similarity index 64% rename from doc/OnlineDocs/tests/expr/design.py rename to doc/OnlineDocs/src/expr/design.py index b122a5f2bf3..647a4537ca4 100644 --- a/doc/OnlineDocs/tests/expr/design.py +++ b/doc/OnlineDocs/src/expr/design.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * # --------------------------------------------- diff --git a/doc/OnlineDocs/tests/expr/design.txt b/doc/OnlineDocs/src/expr/design.txt similarity index 100% rename from doc/OnlineDocs/tests/expr/design.txt rename to doc/OnlineDocs/src/expr/design.txt diff --git a/doc/OnlineDocs/src/expr/index.py b/doc/OnlineDocs/src/expr/index.py new file mode 100644 index 00000000000..fe5b03461c0 --- /dev/null +++ b/doc/OnlineDocs/src/expr/index.py @@ -0,0 +1,21 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +# --------------------------------------------- +# @simple +M = ConcreteModel() +M.v = Var() + +e = M.v * 2 +# @simple +print(e) diff --git a/doc/OnlineDocs/tests/expr/index.txt b/doc/OnlineDocs/src/expr/index.txt similarity index 100% rename from doc/OnlineDocs/tests/expr/index.txt rename to doc/OnlineDocs/src/expr/index.txt diff --git a/doc/OnlineDocs/tests/expr/managing.py b/doc/OnlineDocs/src/expr/managing.py similarity index 65% rename from doc/OnlineDocs/tests/expr/managing.py rename to doc/OnlineDocs/src/expr/managing.py index 0a2709fe96f..ff149e4fd5c 100644 --- a/doc/OnlineDocs/tests/expr/managing.py +++ b/doc/OnlineDocs/src/expr/managing.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * from math import isclose import math @@ -5,7 +16,7 @@ # --------------------------------------------- # @ex1 -from pyomo.core.expr import current as EXPR +import pyomo.core.expr as EXPR M = ConcreteModel() M.x = Var() @@ -21,7 +32,7 @@ # --------------------------------------------- # @ex2 -from pyomo.core.expr import current as EXPR +import pyomo.core.expr as EXPR M = ConcreteModel() M.x = Var() @@ -33,35 +44,6 @@ print(EXPR.expression_to_string(e, labeler=NumericLabeler('x'))) # @ex2 -# --------------------------------------------- -# @ex3 -from pyomo.core.expr import current as EXPR - -M = ConcreteModel() -M.x = Var() -M.y = Var() - -e = sin(M.x) + 2 * M.y + M.x * M.y - 3 - -# -3 + 2*y + sin(x) + x*y -print(EXPR.expression_to_string(e, standardize=True)) -# @ex3 - -# --------------------------------------------- -# @ex4 -from pyomo.core.expr import current as EXPR - -M = ConcreteModel() -M.x = Var() - -with EXPR.clone_counter() as counter: - start = counter.count - e1 = sin(M.x) - e2 = e1.clone() - total = counter.count - start - assert total == 1 -# @ex4 - # --------------------------------------------- # @ex5 M = ConcreteModel() @@ -85,7 +67,7 @@ # --------------------------------------------- # @ex8 -from pyomo.core.expr import current as EXPR +import pyomo.core.expr as EXPR M = ConcreteModel() M.x = Var() @@ -98,7 +80,7 @@ # --------------------------------------------- # @ex9 -from pyomo.core.expr import current as EXPR +import pyomo.core.expr as EXPR M = ConcreteModel() M.x = Var() @@ -116,7 +98,7 @@ # --------------------------------------------- # @visitor1 -from pyomo.core.expr import current as EXPR +import pyomo.core.expr as EXPR class SizeofVisitor(EXPR.SimpleExpressionVisitor): @@ -129,8 +111,7 @@ def visit(self, node): def finalize(self): return self.counter - -# @visitor1 + # @visitor1 # --------------------------------------------- @@ -144,13 +125,12 @@ def sizeof_expression(expr): # Compute the value using the :func:`xbfs` search method. # return visitor.xbfs(expr) + # @visitor2 -# @visitor2 - # --------------------------------------------- # @visitor3 -from pyomo.core.expr import current as EXPR +import pyomo.core.expr as EXPR class CloneVisitor(EXPR.ExpressionValueVisitor): @@ -161,22 +141,17 @@ def visit(self, node, values): # # Clone the interior node # - return node.construct_clone(tuple(values), self.memo) + return node.create_node_with_local_data(values) def visiting_potential_leaf(self, node): # # Clone leaf nodes in the expression tree # - if ( - node.__class__ in native_numeric_types - or node.__class__ not in pyomo5_expression_types - ): + if node.__class__ in native_numeric_types or not node.is_expression_type(): return True, copy.deepcopy(node, self.memo) return False, None - - -# @visitor3 + # @visitor3 # --------------------------------------------- @@ -191,13 +166,29 @@ def clone_expression(expr): # search method. # return visitor.dfs_postorder_stack(expr) - - -# @visitor4 + # @visitor4 + + +# Test: +m = ConcreteModel() +m.x = Var(range(2)) +m.p = Param(range(5), mutable=True) +e = m.x[0] + 5 * m.x[1] +ce = clone_expression(e) +print(e is not ce) +# True +print(str(e)) +# x[0] + 5*x[1] +print(str(ce)) +# x[0] + 5*x[1] +print(e.arg(0) is ce.arg(0)) +# True +print(e.arg(1) is not ce.arg(1)) +# True # --------------------------------------------- # @visitor5 -from pyomo.core.expr import current as EXPR +import pyomo.core.expr as EXPR class ScalingVisitor(EXPR.ExpressionReplacementVisitor): @@ -205,29 +196,24 @@ def __init__(self, scale): super(ScalingVisitor, self).__init__() self.scale = scale - def visiting_potential_leaf(self, node): + def beforeChild(self, node, child, child_idx): # - # Clone leaf nodes in the expression tree + # Native numeric types are terminal nodes; this also catches all + # nodes that do not conform to the ExpressionBase API (i.e., + # define is_variable_type) # - if node.__class__ in native_numeric_types: - return True, node - - if node.is_variable_type(): - return True, self.scale[id(node)] * node - - if isinstance(node, EXPR.LinearExpression): - node_ = copy.deepcopy(node) - node_.constant = node.constant - node_.linear_vars = copy.copy(node.linear_vars) - node_.linear_coefs = [] - for i, v in enumerate(node.linear_vars): - node_.linear_coefs.append(node.linear_coefs[i] * self.scale[id(v)]) - return True, node_ - - return False, None - - -# @visitor5 + if child.__class__ in native_numeric_types: + return False, child + # + # Replace leaf variables with scaled variables + # + if child.is_variable_type(): + return False, self.scale[id(child)] * child + # + # Everything else can be processed normally + # + return True, None + # @visitor5 # --------------------------------------------- @@ -241,11 +227,10 @@ def scale_expression(expr, scale): # Scale the expression using the :func:`dfs_postorder_stack` # search method. # - return visitor.dfs_postorder_stack(expr) + return visitor.walk_expression(expr) + # @visitor6 -# @visitor6 - # --------------------------------------------- # @visitor7 M = ConcreteModel() diff --git a/doc/OnlineDocs/tests/expr/managing.txt b/doc/OnlineDocs/src/expr/managing.txt similarity index 56% rename from doc/OnlineDocs/tests/expr/managing.txt rename to doc/OnlineDocs/src/expr/managing.txt index 5a22c846a8b..d236c942d25 100644 --- a/doc/OnlineDocs/tests/expr/managing.txt +++ b/doc/OnlineDocs/src/expr/managing.txt @@ -1,5 +1,9 @@ sin(x) + 2*x -sum(sin(x), prod(2, x)) +sum(sin(x), mon(2, x)) sin(x1) + 2*x2 --3 + 2*y + x*y + sin(x) +True +x[0] + 5*x[1] +x[0] + 5*x[1] +True +True p[0]*x[0] + p[1]*x[1] + p[2]*x[2] + p[3]*x[3] + p[4]*x[4] diff --git a/doc/OnlineDocs/tests/expr/overview.py b/doc/OnlineDocs/src/expr/overview.py similarity index 70% rename from doc/OnlineDocs/tests/expr/overview.py rename to doc/OnlineDocs/src/expr/overview.py index 6207a4c4288..d33725edb88 100644 --- a/doc/OnlineDocs/tests/expr/overview.py +++ b/doc/OnlineDocs/src/expr/overview.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * # --------------------------------------------- diff --git a/doc/OnlineDocs/tests/expr/overview.txt b/doc/OnlineDocs/src/expr/overview.txt similarity index 100% rename from doc/OnlineDocs/tests/expr/overview.txt rename to doc/OnlineDocs/src/expr/overview.txt diff --git a/doc/OnlineDocs/tests/expr/performance.py b/doc/OnlineDocs/src/expr/performance.py similarity index 77% rename from doc/OnlineDocs/tests/expr/performance.py rename to doc/OnlineDocs/src/expr/performance.py index 53ac5bb4f9e..8936bd2ed8c 100644 --- a/doc/OnlineDocs/tests/expr/performance.py +++ b/doc/OnlineDocs/src/expr/performance.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * # --------------------------------------------- diff --git a/doc/OnlineDocs/tests/expr/performance.txt b/doc/OnlineDocs/src/expr/performance.txt similarity index 62% rename from doc/OnlineDocs/tests/expr/performance.txt rename to doc/OnlineDocs/src/expr/performance.txt index c1387a51ce4..6bfd0bd1d5a 100644 --- a/doc/OnlineDocs/tests/expr/performance.txt +++ b/doc/OnlineDocs/src/expr/performance.txt @@ -10,5 +10,5 @@ x[0] + x[1]**2 + x[2]**2 + x[3]**2 + x[4]**2 x[0] + x[1] + x[2] + x[3] + x[4] + x[5] + x[6] + x[7] + x[8] + x[9] x[0]*y[0] + x[1]*y[1] + x[2]*y[2] + x[3]*y[3] + x[4]*y[4] + x[5]*y[5] + x[6]*y[6] + x[7]*y[7] + x[8]*y[8] + x[9]*y[9] x[1]*y[1] + x[2]*y[2] + x[3]*y[3] + x[4]*y[4] + x[5]*y[5] -x[0]*(1/y[0]) + x[1]*(1/y[1]) + x[2]*(1/y[2]) + x[3]*(1/y[3]) + x[4]*(1/y[4]) + x[5]*(1/y[5]) + x[6]*(1/y[6]) + x[7]*(1/y[7]) + x[8]*(1/y[8]) + x[9]*(1/y[9]) -(1/(x[0]*y[0])) + (1/(x[1]*y[1])) + (1/(x[2]*y[2])) + (1/(x[3]*y[3])) + (1/(x[4]*y[4])) + (1/(x[5]*y[5])) + (1/(x[6]*y[6])) + (1/(x[7]*y[7])) + (1/(x[8]*y[8])) + (1/(x[9]*y[9])) +x[0]/y[0] + x[1]/y[1] + x[2]/y[2] + x[3]/y[3] + x[4]/y[4] + x[5]/y[5] + x[6]/y[6] + x[7]/y[7] + x[8]/y[8] + x[9]/y[9] +1/(x[0]*y[0]) + 1/(x[1]*y[1]) + 1/(x[2]*y[2]) + 1/(x[3]*y[3]) + 1/(x[4]*y[4]) + 1/(x[5]*y[5]) + 1/(x[6]*y[6]) + 1/(x[7]*y[7]) + 1/(x[8]*y[8]) + 1/(x[9]*y[9]) diff --git a/doc/OnlineDocs/tests/expr/quicksum.log b/doc/OnlineDocs/src/expr/quicksum.log similarity index 100% rename from doc/OnlineDocs/tests/expr/quicksum.log rename to doc/OnlineDocs/src/expr/quicksum.log diff --git a/doc/OnlineDocs/tests/expr/quicksum.py b/doc/OnlineDocs/src/expr/quicksum.py similarity index 53% rename from doc/OnlineDocs/tests/expr/quicksum.py rename to doc/OnlineDocs/src/expr/quicksum.py index a1ad9660664..1b6cd3f9909 100644 --- a/doc/OnlineDocs/tests/expr/quicksum.py +++ b/doc/OnlineDocs/src/expr/quicksum.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * from pyomo.repn import generate_standard_repn import time diff --git a/doc/OnlineDocs/tests/kernel/examples.sh b/doc/OnlineDocs/src/kernel/examples.sh similarity index 100% rename from doc/OnlineDocs/tests/kernel/examples.sh rename to doc/OnlineDocs/src/kernel/examples.sh diff --git a/doc/OnlineDocs/tests/kernel/examples.txt b/doc/OnlineDocs/src/kernel/examples.txt similarity index 66% rename from doc/OnlineDocs/tests/kernel/examples.txt rename to doc/OnlineDocs/src/kernel/examples.txt index c8a0cde2e36..8ba072d28b1 100644 --- a/doc/OnlineDocs/tests/kernel/examples.txt +++ b/doc/OnlineDocs/src/kernel/examples.txt @@ -1,20 +1,12 @@ -6 Set Declarations - cd_index : Dim=0, Dimen=2, Size=6, Domain=None, Ordered=True, Bounds=None - Virtual - cl_index : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=None - [1, 2, 3] - ol_index : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=None - [1, 2, 3] - s : Dim=0, Dimen=1, Size=2, Domain=None, Ordered=Insertion, Bounds=(1, 2) - [1, 2] - sd_index : Dim=0, Dimen=1, Size=2, Domain=None, Ordered=False, Bounds=(1, 2) - [1, 2] - vl_index : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=None - [1, 2, 3] +1 Set Declarations + s : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 2 : {1, 2} 1 RangeSet Declarations - q : Dim=0, Dimen=1, Size=3, Domain=Integers, Ordered=True, Bounds=(1, 3) - Virtual + q : Dimen=1, Size=3, Bounds=(1, 3) + Key : Finite : Members + None : True : [1:3] 2 Param Declarations p : Size=1, Index=None, Domain=Any, Default=None, Mutable=True @@ -36,7 +28,7 @@ Key : Lower : Value : Upper : Fixed : Stale : Domain 1 : None : None : 9 : False : True : Reals 2 : None : None : 9 : False : True : Reals - vl : Size=3, Index=vl_index + vl : Size=3, Index={1, 2, 3} Key : Lower : Value : Upper : Fixed : Stale : Domain 1 : 1 : None : None : False : True : Reals 2 : 2 : None : None : False : True : Reals @@ -59,7 +51,7 @@ Key : Active : Sense : Expression 1 : True : minimize : - vd[1] 2 : True : minimize : - vd[2] - ol : Size=3, Index=ol_index, Active=True + ol : Size=3, Index={1, 2, 3}, Active=True Key : Active : Sense : Expression 1 : True : minimize : - vl[1] 2 : True : minimize : - vl[2] @@ -69,7 +61,7 @@ c : Size=1, Index=None, Active=True Key : Lower : Body : Upper : Active None : -Inf : vd[1] + vd[2] : 9.0 : True - cd : Size=6, Index=cd_index, Active=True + cd : Size=6, Index=s*q, Active=True Key : Lower : Body : Upper : Active (1, 1) : 1.0 : vd[1] : 1.0 : True (1, 2) : 2.0 : vd[1] : 2.0 : True @@ -77,77 +69,72 @@ (2, 1) : 1.0 : vd[2] : 1.0 : True (2, 2) : 2.0 : vd[2] : 2.0 : True (2, 3) : 3.0 : vd[2] : 3.0 : True - cl : Size=3, Index=cl_index, Active=True + cl : Size=3, Index={1, 2, 3}, Active=True Key : Lower : Body : Upper : Active 1 : -5.0 : vl[1] - v : 5.0 : True 2 : -5.0 : vl[2] - v : 5.0 : True 3 : -5.0 : vl[3] - v : 5.0 : True 3 SOSConstraint Declarations - sd : Size=2 Index= sd_index - 1 - Type=1 - Weight : Variable - 1 : vd[1] - 2 : vd[2] - 2 - Type=1 - Weight : Variable - 1 : vl[1] - 2 : vl[2] - 3 : vl[3] - sos1 : Size=1 - Type=1 - Weight : Variable - 1 : vl[1] - 2 : vl[2] - 3 : vl[3] - sos2 : Size=1 - Type=2 - Weight : Variable - 1 : vd[1] - 2 : vd[2] + sd : Size=2 Index= OrderedScalarSet + 1 + Type=1 + Weight : Variable + 1 : vd[1] + 2 : vd[2] + 2 + Type=1 + Weight : Variable + 1 : vl[1] + 2 : vl[2] + 3 : vl[3] + sos1 : Size=1 + Type=1 + Weight : Variable + 1 : vl[1] + 2 : vl[2] + 3 : vl[3] + sos2 : Size=1 + Type=2 + Weight : Variable + 1 : vd[1] + 2 : vd[2] 2 Block Declarations b : Size=1, Index=None, Active=True 0 Declarations: - 2 Set Declarations - SOS2_constraint_index : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=None - [1, 2, 3] - SOS2_y_index : Dim=0, Dimen=1, Size=4, Domain=None, Ordered=False, Bounds=(0, 3) - [0, 1, 2, 3] + pw : Size=1, Index=None, Active=True + 1 Var Declarations + SOS2_y : Size=4, Index={0, 1, 2, 3} + Key : Lower : Value : Upper : Fixed : Stale : Domain + 0 : 0 : None : None : False : True : NonNegativeReals + 1 : 0 : None : None : False : True : NonNegativeReals + 2 : 0 : None : None : False : True : NonNegativeReals + 3 : 0 : None : None : False : True : NonNegativeReals - 1 Var Declarations - SOS2_y : Size=4, Index=pw.SOS2_y_index - Key : Lower : Value : Upper : Fixed : Stale : Domain - 0 : 0 : None : None : False : True : NonNegativeReals - 1 : 0 : None : None : False : True : NonNegativeReals - 2 : 0 : None : None : False : True : NonNegativeReals - 3 : 0 : None : None : False : True : NonNegativeReals + 1 Constraint Declarations + SOS2_constraint : Size=3, Index={1, 2, 3}, Active=True + Key : Lower : Body : Upper : Active + 1 : 0.0 : v - (pw.SOS2_y[0] + 2*pw.SOS2_y[1] + 3*pw.SOS2_y[2] + 4*pw.SOS2_y[3]) : 0.0 : True + 2 : 0.0 : f - (pw.SOS2_y[0] + 2*pw.SOS2_y[1] + pw.SOS2_y[2] + 2*pw.SOS2_y[3]) : 0.0 : True + 3 : 1.0 : pw.SOS2_y[0] + pw.SOS2_y[1] + pw.SOS2_y[2] + pw.SOS2_y[3] : 1.0 : True - 1 Constraint Declarations - SOS2_constraint : Size=3, Index=pw.SOS2_constraint_index, Active=True - Key : Lower : Body : Upper : Active - 1 : 0.0 : v - (pw.SOS2_y[0] + 2*pw.SOS2_y[1] + 3*pw.SOS2_y[2] + 4*pw.SOS2_y[3]) : 0.0 : True - 2 : 0.0 : f - (pw.SOS2_y[0] + 2*pw.SOS2_y[1] + pw.SOS2_y[2] + 2*pw.SOS2_y[3]) : 0.0 : True - 3 : 1.0 : pw.SOS2_y[0] + pw.SOS2_y[1] + pw.SOS2_y[2] + pw.SOS2_y[3] : 1.0 : True + 1 SOSConstraint Declarations + SOS2_sosconstraint : Size=1 + Type=2 + Weight : Variable + 1 : pw.SOS2_y[0] + 2 : pw.SOS2_y[1] + 3 : pw.SOS2_y[2] + 4 : pw.SOS2_y[3] - 1 SOSConstraint Declarations - SOS2_sosconstraint : Size=1 - Type=2 - Weight : Variable - 1 : pw.SOS2_y[0] - 2 : pw.SOS2_y[1] - 3 : pw.SOS2_y[2] - 4 : pw.SOS2_y[3] - - 5 Declarations: SOS2_y_index SOS2_y SOS2_constraint_index SOS2_constraint SOS2_sosconstraint + 3 Declarations: SOS2_y SOS2_constraint SOS2_sosconstraint 1 Suffix Declarations - dual : Direction=Suffix.IMPORT, Datatype=Suffix.FLOAT + dual : Direction=IMPORT, Datatype=FLOAT Key : Value -27 Declarations: b s q p pd v vd vl_index vl c cd_index cd cl_index cl e ed o od ol_index ol sos1 sos2 sd_index sd dual f pw +22 Declarations: b s q p pd v vd vl c cd cl e ed o od ol sos1 sos2 sd dual f pw : block(active=True, ctype=IBlock) - b: block(active=True, ctype=IBlock) - p: parameter(active=True, value=0) @@ -166,14 +153,14 @@ - vl[0]: variable(active=True, value=None, bounds=(2,None), domain_type=RealSet, fixed=False, stale=True) - vl[1]: variable(active=True, value=None, bounds=(2,None), domain_type=RealSet, fixed=False, stale=True) - vl[2]: variable(active=True, value=None, bounds=(2,None), domain_type=RealSet, fixed=False, stale=True) - - c: constraint(active=True, expr=vd[1] + vd[2] <= 9.0) + - c: constraint(active=True, expr=vd[1] + vd[2] <= 9) - cd: constraint_dict(active=True, ctype=IConstraint) - - cd[(1, 0)]: constraint(active=True, expr=vd[1] == 0.0) - - cd[(1, 1)]: constraint(active=True, expr=vd[1] == 1.0) - - cd[(1, 2)]: constraint(active=True, expr=vd[1] == 2.0) - - cd[(2, 0)]: constraint(active=True, expr=vd[2] == 0.0) - - cd[(2, 1)]: constraint(active=True, expr=vd[2] == 1.0) - - cd[(2, 2)]: constraint(active=True, expr=vd[2] == 2.0) + - cd[(1, 0)]: constraint(active=True, expr=vd[1] == 0) + - cd[(1, 1)]: constraint(active=True, expr=vd[1] == 1) + - cd[(1, 2)]: constraint(active=True, expr=vd[1] == 2) + - cd[(2, 0)]: constraint(active=True, expr=vd[2] == 0) + - cd[(2, 1)]: constraint(active=True, expr=vd[2] == 1) + - cd[(2, 2)]: constraint(active=True, expr=vd[2] == 2) - cl: constraint_list(active=True, ctype=IConstraint) - cl[0]: constraint(active=True, expr=-5 <= vl[0] - v <= 5) - cl[1]: constraint(active=True, expr=-5 <= vl[1] - v <= 5) @@ -216,9 +203,9 @@ - pw.v[2]: variable(active=True, value=None, bounds=(0,None), domain_type=RealSet, fixed=False, stale=True) - pw.v[3]: variable(active=True, value=None, bounds=(0,None), domain_type=RealSet, fixed=False, stale=True) - pw.c: constraint_list(active=True, ctype=IConstraint) - - pw.c[0]: linear_constraint(active=True, expr=pw.v[0] + 2*pw.v[1] + 3*pw.v[2] + 4*pw.v[3] - v == 0.0) - - pw.c[1]: linear_constraint(active=True, expr=pw.v[0] + 2*pw.v[1] + pw.v[2] + 2*pw.v[3] - f == 0.0) - - pw.c[2]: linear_constraint(active=True, expr=pw.v[0] + pw.v[1] + pw.v[2] + pw.v[3] == 1.0) + - pw.c[0]: linear_constraint(active=True, expr=pw.v[0] + 2*pw.v[1] + 3*pw.v[2] + 4*pw.v[3] - v == 0) + - pw.c[1]: linear_constraint(active=True, expr=pw.v[0] + 2*pw.v[1] + pw.v[2] + 2*pw.v[3] - f == 0) + - pw.c[2]: linear_constraint(active=True, expr=pw.v[0] + pw.v[1] + pw.v[2] + pw.v[3] == 1) - pw.s: sos(active=True, level=2, entries=['(pw.v[0],1)', '(pw.v[1],2)', '(pw.v[2],3)', '(pw.v[3],4)']) -2.0 KB -8.4 KB +Memory: 1.9 KB +Memory: 9.7 KB diff --git a/doc/OnlineDocs/src/scripting/AbstractSuffixes.py b/doc/OnlineDocs/src/scripting/AbstractSuffixes.py new file mode 100644 index 00000000000..1c064042c6b --- /dev/null +++ b/doc/OnlineDocs/src/scripting/AbstractSuffixes.py @@ -0,0 +1,35 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() +model.I = RangeSet(1, 4) +model.x = Var(model.I) + + +def c_rule(m, i): + return m.x[i] >= i + + +model.c = Constraint(model.I, rule=c_rule) + + +def foo_rule(m): + return ((m.x[i], 3.0 * i) for i in m.I) + + +model.foo = Suffix(rule=foo_rule) + +# instantiate the model +inst = model.create_instance() +for i in inst.I: + print(i, inst.foo[inst.x[i]]) diff --git a/doc/OnlineDocs/tests/scripting/Isinglebuild.py b/doc/OnlineDocs/src/scripting/Isinglebuild.py similarity index 68% rename from doc/OnlineDocs/tests/scripting/Isinglebuild.py rename to doc/OnlineDocs/src/scripting/Isinglebuild.py index 00f79c9a750..344f8905a4a 100644 --- a/doc/OnlineDocs/tests/scripting/Isinglebuild.py +++ b/doc/OnlineDocs/src/scripting/Isinglebuild.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # Isinglebuild.py # NodesIn and NodesOut are created by a build action using the Arcs from pyomo.environ import * diff --git a/doc/OnlineDocs/tests/scripting/Isinglecomm.dat b/doc/OnlineDocs/src/scripting/Isinglecomm.dat similarity index 100% rename from doc/OnlineDocs/tests/scripting/Isinglecomm.dat rename to doc/OnlineDocs/src/scripting/Isinglecomm.dat diff --git a/doc/OnlineDocs/src/scripting/NodesIn_init.py b/doc/OnlineDocs/src/scripting/NodesIn_init.py new file mode 100644 index 00000000000..c17b70150bc --- /dev/null +++ b/doc/OnlineDocs/src/scripting/NodesIn_init.py @@ -0,0 +1,21 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + +def NodesIn_init(model, node): + retval = [] + for i, j in model.Arcs: + if j == node: + retval.append(i) + return retval + + +model.NodesIn = Set(model.Nodes, initialize=NodesIn_init) diff --git a/doc/OnlineDocs/src/scripting/Z_init.py b/doc/OnlineDocs/src/scripting/Z_init.py new file mode 100644 index 00000000000..1dd2843f4f0 --- /dev/null +++ b/doc/OnlineDocs/src/scripting/Z_init.py @@ -0,0 +1,19 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + +def Z_init(model, i): + if i > 10: + return Set.End + return 2 * i + 1 + + +model.Z = Set(initialize=Z_init) diff --git a/doc/OnlineDocs/tests/scripting/abstract1.dat b/doc/OnlineDocs/src/scripting/abstract1.dat similarity index 100% rename from doc/OnlineDocs/tests/scripting/abstract1.dat rename to doc/OnlineDocs/src/scripting/abstract1.dat diff --git a/doc/OnlineDocs/tests/scripting/abstract2.dat b/doc/OnlineDocs/src/scripting/abstract2.dat similarity index 100% rename from doc/OnlineDocs/tests/scripting/abstract2.dat rename to doc/OnlineDocs/src/scripting/abstract2.dat diff --git a/doc/OnlineDocs/tests/scripting/abstract2.py b/doc/OnlineDocs/src/scripting/abstract2.py similarity index 56% rename from doc/OnlineDocs/tests/scripting/abstract2.py rename to doc/OnlineDocs/src/scripting/abstract2.py index 7eb444914db..544399a8a42 100644 --- a/doc/OnlineDocs/tests/scripting/abstract2.py +++ b/doc/OnlineDocs/src/scripting/abstract2.py @@ -1,6 +1,17 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # abstract2.py -from __future__ import division + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/tests/scripting/abstract2a.dat b/doc/OnlineDocs/src/scripting/abstract2a.dat similarity index 100% rename from doc/OnlineDocs/tests/scripting/abstract2a.dat rename to doc/OnlineDocs/src/scripting/abstract2a.dat diff --git a/doc/OnlineDocs/tests/scripting/abstract2piece.py b/doc/OnlineDocs/src/scripting/abstract2piece.py similarity index 70% rename from doc/OnlineDocs/tests/scripting/abstract2piece.py rename to doc/OnlineDocs/src/scripting/abstract2piece.py index 225ec0d1a64..03c5139004e 100644 --- a/doc/OnlineDocs/tests/scripting/abstract2piece.py +++ b/doc/OnlineDocs/src/scripting/abstract2piece.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # abstract2piece.py # Similar to abstract2.py, but the objective is now c times x to the fourth power diff --git a/doc/OnlineDocs/tests/scripting/abstract2piecebuild.py b/doc/OnlineDocs/src/scripting/abstract2piecebuild.py similarity index 77% rename from doc/OnlineDocs/tests/scripting/abstract2piecebuild.py rename to doc/OnlineDocs/src/scripting/abstract2piecebuild.py index 1f00cdb0265..d454d7fbc79 100644 --- a/doc/OnlineDocs/tests/scripting/abstract2piecebuild.py +++ b/doc/OnlineDocs/src/scripting/abstract2piecebuild.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # abstract2piecebuild.py # Similar to abstract2piece.py, but the breakpoints are created using a build action diff --git a/doc/OnlineDocs/tests/scripting/block_iter_example.py b/doc/OnlineDocs/src/scripting/block_iter_example.py similarity index 62% rename from doc/OnlineDocs/tests/scripting/block_iter_example.py rename to doc/OnlineDocs/src/scripting/block_iter_example.py index 680e0d1728b..10c8a4ea43d 100644 --- a/doc/OnlineDocs/tests/scripting/block_iter_example.py +++ b/doc/OnlineDocs/src/scripting/block_iter_example.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # written by jds, adapted for doc by dlw from pyomo.environ import * diff --git a/doc/OnlineDocs/src/scripting/concrete1.py b/doc/OnlineDocs/src/scripting/concrete1.py new file mode 100644 index 00000000000..399715efde6 --- /dev/null +++ b/doc/OnlineDocs/src/scripting/concrete1.py @@ -0,0 +1,20 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = ConcreteModel() + +model.x = Var([1, 2], domain=NonNegativeReals) + +model.OBJ = Objective(expr=2 * model.x[1] + 3 * model.x[2]) + +model.Constraint1 = Constraint(expr=3 * model.x[1] + 4 * model.x[2] >= 1) diff --git a/doc/OnlineDocs/src/scripting/doubleA.py b/doc/OnlineDocs/src/scripting/doubleA.py new file mode 100644 index 00000000000..abf35979a05 --- /dev/null +++ b/doc/OnlineDocs/src/scripting/doubleA.py @@ -0,0 +1,17 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + +def doubleA_init(model): + return (i * 2 for i in model.A) + + +model.C = Set(initialize=DoubleA_init) diff --git a/doc/OnlineDocs/tests/scripting/driveabs2.py b/doc/OnlineDocs/src/scripting/driveabs2.py similarity index 64% rename from doc/OnlineDocs/tests/scripting/driveabs2.py rename to doc/OnlineDocs/src/scripting/driveabs2.py index 67ab7468864..f8f972460b1 100644 --- a/doc/OnlineDocs/tests/scripting/driveabs2.py +++ b/doc/OnlineDocs/src/scripting/driveabs2.py @@ -1,5 +1,16 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # driveabs2.py -from __future__ import division + import pyomo.environ as pyo from pyomo.opt import SolverFactory diff --git a/doc/OnlineDocs/tests/scripting/driveconc1.py b/doc/OnlineDocs/src/scripting/driveconc1.py similarity index 53% rename from doc/OnlineDocs/tests/scripting/driveconc1.py rename to doc/OnlineDocs/src/scripting/driveconc1.py index ca5d6fc1593..49b92f32d09 100644 --- a/doc/OnlineDocs/tests/scripting/driveconc1.py +++ b/doc/OnlineDocs/src/scripting/driveconc1.py @@ -1,5 +1,16 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # driveconc1.py -from __future__ import division + import pyomo.environ as pyo from pyomo.opt import SolverFactory diff --git a/doc/OnlineDocs/tests/scripting/iterative1.py b/doc/OnlineDocs/src/scripting/iterative1.py similarity index 74% rename from doc/OnlineDocs/tests/scripting/iterative1.py rename to doc/OnlineDocs/src/scripting/iterative1.py index 61b0fd3828e..939120e834f 100644 --- a/doc/OnlineDocs/tests/scripting/iterative1.py +++ b/doc/OnlineDocs/src/scripting/iterative1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # @Import_symbols_for_pyomo # iterative1.py import pyomo.environ as pyo diff --git a/doc/OnlineDocs/tests/scripting/iterative2.py b/doc/OnlineDocs/src/scripting/iterative2.py similarity index 60% rename from doc/OnlineDocs/tests/scripting/iterative2.py rename to doc/OnlineDocs/src/scripting/iterative2.py index e559a2c8400..7506337a491 100644 --- a/doc/OnlineDocs/tests/scripting/iterative2.py +++ b/doc/OnlineDocs/src/scripting/iterative2.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # iterative2.py import pyomo.environ as pyo diff --git a/doc/OnlineDocs/tests/scripting/noiteration1.py b/doc/OnlineDocs/src/scripting/noiteration1.py similarity index 52% rename from doc/OnlineDocs/tests/scripting/noiteration1.py rename to doc/OnlineDocs/src/scripting/noiteration1.py index be9fb529855..c7a86e9d1e9 100644 --- a/doc/OnlineDocs/tests/scripting/noiteration1.py +++ b/doc/OnlineDocs/src/scripting/noiteration1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # noiteration1.py import pyomo.environ as pyo diff --git a/doc/OnlineDocs/tests/scripting/parallel.py b/doc/OnlineDocs/src/scripting/parallel.py similarity index 57% rename from doc/OnlineDocs/tests/scripting/parallel.py rename to doc/OnlineDocs/src/scripting/parallel.py index cf9b55d9605..e6cfa002780 100644 --- a/doc/OnlineDocs/tests/scripting/parallel.py +++ b/doc/OnlineDocs/src/scripting/parallel.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # parallel.py # run with mpirun -np 2 python -m mpi4py parallel.py import pyomo.environ as pyo diff --git a/doc/OnlineDocs/tests/scripting/spy4Constraints.py b/doc/OnlineDocs/src/scripting/spy4Constraints.py similarity index 64% rename from doc/OnlineDocs/tests/scripting/spy4Constraints.py rename to doc/OnlineDocs/src/scripting/spy4Constraints.py index ac42b4d38b3..66f82802402 100644 --- a/doc/OnlineDocs/tests/scripting/spy4Constraints.py +++ b/doc/OnlineDocs/src/scripting/spy4Constraints.py @@ -1,7 +1,19 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ David L. Woodruff and Mingye Yang, Spring 2018 Code snippets for Constraints.rst in testable form """ + from pyomo.environ import * model = ConcreteModel() diff --git a/doc/OnlineDocs/tests/scripting/spy4Expressions.py b/doc/OnlineDocs/src/scripting/spy4Expressions.py similarity index 81% rename from doc/OnlineDocs/tests/scripting/spy4Expressions.py rename to doc/OnlineDocs/src/scripting/spy4Expressions.py index d4a5cad321a..cf7ed1f112f 100644 --- a/doc/OnlineDocs/tests/scripting/spy4Expressions.py +++ b/doc/OnlineDocs/src/scripting/spy4Expressions.py @@ -1,7 +1,19 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ David L. Woodruff and Mingye Yang, Spring 2018 Code snippets for Expressions.rst in testable form """ + from pyomo.environ import * model = ConcreteModel() diff --git a/doc/OnlineDocs/tests/scripting/spy4PyomoCommand.py b/doc/OnlineDocs/src/scripting/spy4PyomoCommand.py similarity index 57% rename from doc/OnlineDocs/tests/scripting/spy4PyomoCommand.py rename to doc/OnlineDocs/src/scripting/spy4PyomoCommand.py index c03ee1e5039..9f6698d63c9 100644 --- a/doc/OnlineDocs/tests/scripting/spy4PyomoCommand.py +++ b/doc/OnlineDocs/src/scripting/spy4PyomoCommand.py @@ -1,7 +1,19 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ David L. Woodruff and Mingye Yang, Spring 2018 Code snippets for PyomoCommand.rst in testable form """ + from pyomo.environ import * model = ConcreteModel() diff --git a/doc/OnlineDocs/tests/scripting/spy4Variables.py b/doc/OnlineDocs/src/scripting/spy4Variables.py similarity index 52% rename from doc/OnlineDocs/tests/scripting/spy4Variables.py rename to doc/OnlineDocs/src/scripting/spy4Variables.py index 802226247c5..1bc2dc9f1ef 100644 --- a/doc/OnlineDocs/tests/scripting/spy4Variables.py +++ b/doc/OnlineDocs/src/scripting/spy4Variables.py @@ -1,7 +1,19 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ David L. Woodruff and Mingye Yang, Spring 2018 Code snippets for Variables.rst in testable form """ + from pyomo.environ import * model = ConcreteModel() diff --git a/doc/OnlineDocs/tests/scripting/spy4scripts.py b/doc/OnlineDocs/src/scripting/spy4scripts.py similarity index 91% rename from doc/OnlineDocs/tests/scripting/spy4scripts.py rename to doc/OnlineDocs/src/scripting/spy4scripts.py index 48ba923d09c..f71a1b67b11 100644 --- a/doc/OnlineDocs/tests/scripting/spy4scripts.py +++ b/doc/OnlineDocs/src/scripting/spy4scripts.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + ###NOTE: as of May 16, this will not even come close to running. DLW ### and it is "wrong" in a lot of places. ### Someone should edit this file, then delete these comment lines. DLW may 16 diff --git a/doc/OnlineDocs/tests/strip_examples.py b/doc/OnlineDocs/src/strip_examples.py similarity index 78% rename from doc/OnlineDocs/tests/strip_examples.py rename to doc/OnlineDocs/src/strip_examples.py index 045af6b87cc..2fd03256499 100644 --- a/doc/OnlineDocs/tests/strip_examples.py +++ b/doc/OnlineDocs/src/strip_examples.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # # This script finds all *.py files in the current and subdirectories. # It processes these files to find blocks that start/end with "# @" diff --git a/doc/OnlineDocs/src/test_examples.py b/doc/OnlineDocs/src/test_examples.py new file mode 100644 index 00000000000..c5c9a135ee9 --- /dev/null +++ b/doc/OnlineDocs/src/test_examples.py @@ -0,0 +1,76 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest +import glob +import os +from pyomo.common.dependencies import attempt_import, matplotlib_available +from pyomo.common.fileutils import this_file_dir +import pyomo.environ as pyo + + +currdir = this_file_dir() + +parameterized, param_available = attempt_import('parameterized') +if not param_available: + raise unittest.SkipTest('Parameterized is not available.') + +# Needed for testing (triggers matplotlib import and switches its backend): +bool(matplotlib_available) + + +class TestOnlineDocExamples(unittest.BaselineTestDriver, unittest.TestCase): + # Only test files in directories ending in -ch. These directories + # contain the updated python and scripting files corresponding to + # each chapter in the book. + py_tests, sh_tests = unittest.BaselineTestDriver.gather_tests( + list(filter(os.path.isdir, glob.glob(os.path.join(currdir, '*')))) + ) + + solver_dependencies = { + 'test_data_pyomo_diet1': ['glpk'], + 'test_data_pyomo_diet2': ['glpk'], + 'test_kernel_examples': ['glpk'], + } + # Note on package dependencies: two tests actually need + # pyutilib.excel.spreadsheet; however, the pyutilib importer is + # broken on Python>=3.12, so instead of checking for spreadsheet, we + # will check for pyutilib.component, which triggers the importer + # (and catches the error on 3.12) + package_dependencies = { + # data + 'test_data_ABCD9': ['pyodbc'], + 'test_data_ABCD8': ['pyodbc'], + 'test_data_ABCD7': ['win32com', 'pyutilib.component'], + # dataportal + 'test_dataportal_dataportal_tab': ['xlrd', 'pyutilib.component'], + 'test_dataportal_set_initialization': ['numpy'], + 'test_dataportal_param_initialization': ['numpy'], + # kernel + 'test_kernel_examples': ['pympler'], + } + + @parameterized.parameterized.expand( + sh_tests, name_func=unittest.BaselineTestDriver.custom_name_func + ) + def test_sh(self, tname, test_file, base_file): + self.shell_test_driver(tname, test_file, base_file) + + @parameterized.parameterized.expand( + py_tests, name_func=unittest.BaselineTestDriver.custom_name_func + ) + def test_py(self, tname, test_file, base_file): + self.python_test_driver(tname, test_file, base_file) + + +# Execute the tests +if __name__ == '__main__': + unittest.main() diff --git a/doc/OnlineDocs/tests/data/ABCD1.py b/doc/OnlineDocs/tests/data/ABCD1.py deleted file mode 100644 index 32600b226e1..00000000000 --- a/doc/OnlineDocs/tests/data/ABCD1.py +++ /dev/null @@ -1,9 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -model.Z = Set(dimen=4) - -instance = model.create_instance('ABCD1.dat') - -print(sorted(list(instance.Z.data()))) diff --git a/doc/OnlineDocs/tests/data/ABCD2.py b/doc/OnlineDocs/tests/data/ABCD2.py deleted file mode 100644 index 65a46415368..00000000000 --- a/doc/OnlineDocs/tests/data/ABCD2.py +++ /dev/null @@ -1,14 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -model.Z = Set(initialize=[('A1', 'B1', 1), ('A2', 'B2', 2), ('A3', 'B3', 3)]) -# model.Z = Set(dimen=3) -model.D = Param(model.Z) - -instance = model.create_instance('ABCD2.dat') - -print('Z ' + str(sorted(list(instance.Z.data())))) -print('D') -for key in sorted(instance.D.keys()): - print(name(instance.D, key) + " " + str(value(instance.D[key]))) diff --git a/doc/OnlineDocs/tests/data/ABCD3.py b/doc/OnlineDocs/tests/data/ABCD3.py deleted file mode 100644 index 48797ced5bb..00000000000 --- a/doc/OnlineDocs/tests/data/ABCD3.py +++ /dev/null @@ -1,13 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -model.Z = Set(dimen=3) -model.D = Param(model.Z) - -instance = model.create_instance('ABCD3.dat') - -print('Z ' + str(sorted(list(instance.Z.data())))) -print('D') -for key in sorted(instance.D.keys()): - print(name(instance.D, key) + " " + str(value(instance.D[key]))) diff --git a/doc/OnlineDocs/tests/data/ABCD4.py b/doc/OnlineDocs/tests/data/ABCD4.py deleted file mode 100644 index 20f6a21c011..00000000000 --- a/doc/OnlineDocs/tests/data/ABCD4.py +++ /dev/null @@ -1,13 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -model.Z = Set(dimen=3) -model.Y = Param(model.Z) - -instance = model.create_instance('ABCD4.dat') - -print('Z ' + str(sorted(list(instance.Z.data())))) -print('Y') -for key in sorted(instance.Y.keys()): - print(name(instance.Y, key) + " " + str(value(instance.Y[key]))) diff --git a/doc/OnlineDocs/tests/data/ABCD5.py b/doc/OnlineDocs/tests/data/ABCD5.py deleted file mode 100644 index 58461af056b..00000000000 --- a/doc/OnlineDocs/tests/data/ABCD5.py +++ /dev/null @@ -1,19 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.Z = Set() -model.Y = Param(model.Z) -model.W = Param(model.Z) -# @decl - -instance = model.create_instance('ABCD5.dat') - -print('Z ' + str(sorted(list(instance.Z.data())))) -print('Y') -for key in sorted(instance.Y.keys()): - print(name(instance.Y, key) + " " + str(value(instance.Y[key]))) -print('W') -for key in sorted(instance.W.keys()): - print(name(instance.W, key) + " " + str(value(instance.W[key]))) diff --git a/doc/OnlineDocs/tests/data/ABCD6.py b/doc/OnlineDocs/tests/data/ABCD6.py deleted file mode 100644 index 961408dbc7e..00000000000 --- a/doc/OnlineDocs/tests/data/ABCD6.py +++ /dev/null @@ -1,13 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -model.Z = Set(dimen=3) -model.D = Param(model.Z) - -instance = model.create_instance('ABCD6.dat') - -print('Z ' + str(sorted(list(instance.Z.data())))) -print('D') -for key in sorted(instance.D.keys()): - print(name(instance.D, key) + " " + str(value(instance.D[key]))) diff --git a/doc/OnlineDocs/tests/data/ABCD7.py b/doc/OnlineDocs/tests/data/ABCD7.py deleted file mode 100644 index a97e764fa5a..00000000000 --- a/doc/OnlineDocs/tests/data/ABCD7.py +++ /dev/null @@ -1,19 +0,0 @@ -from pyomo.environ import * -import pyomo.common -import sys - -model = AbstractModel() - -model.Z = Set(dimen=3) -model.Y = Param(model.Z) - -try: - instance = model.create_instance('ABCD7.dat') -except pyomo.common.errors.ApplicationError as e: - print("ERROR " + str(e)) - sys.exit(1) - -print('Z ' + str(sorted(list(instance.Z.data())))) -print('Y') -for key in sorted(instance.Y.keys()): - print(name(instance.Y, key) + " " + str(value(instance.Y[key]))) diff --git a/doc/OnlineDocs/tests/data/ABCD8.py b/doc/OnlineDocs/tests/data/ABCD8.py deleted file mode 100644 index 9bcd950c681..00000000000 --- a/doc/OnlineDocs/tests/data/ABCD8.py +++ /dev/null @@ -1,19 +0,0 @@ -from pyomo.environ import * -import pyomo.common -import sys - -model = AbstractModel() - -model.Z = Set(dimen=3) -model.Y = Param(model.Z) - -try: - instance = model.create_instance('ABCD8.dat') -except pyomo.common.errors.ApplicationError as e: - print("ERROR " + str(e)) - sys.exit(1) - -print('Z ' + str(sorted(list(instance.Z.data())))) -print('Y') -for key in sorted(instance.Y.keys()): - print(name(instance.Y, key) + " " + str(value(instance.Y[key]))) diff --git a/doc/OnlineDocs/tests/data/ABCD9.py b/doc/OnlineDocs/tests/data/ABCD9.py deleted file mode 100644 index 29fcb6426db..00000000000 --- a/doc/OnlineDocs/tests/data/ABCD9.py +++ /dev/null @@ -1,19 +0,0 @@ -from pyomo.environ import * -import pyomo.common -import sys - -model = AbstractModel() - -model.Z = Set(dimen=3) -model.Y = Param(model.Z) - -try: - instance = model.create_instance('ABCD9.dat') -except pyomo.common.errors.ApplicationError as e: - print("ERROR " + str(e)) - sys.exit(1) - -print('Z ' + str(sorted(list(instance.Z.data())))) -print('Y') -for key in sorted(instance.Y.keys()): - print(instance.Y[key] + " " + str(value(instance.Y[key]))) diff --git a/doc/OnlineDocs/tests/data/ex.py b/doc/OnlineDocs/tests/data/ex.py deleted file mode 100644 index 8c9473f2852..00000000000 --- a/doc/OnlineDocs/tests/data/ex.py +++ /dev/null @@ -1,11 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.z = Param() -# @decl - -instance = model.create_instance('ex.dat') - -print(value(instance.z)) diff --git a/doc/OnlineDocs/tests/data/import1.tab.py b/doc/OnlineDocs/tests/data/import1.tab.py deleted file mode 100644 index c9164ab73ec..00000000000 --- a/doc/OnlineDocs/tests/data/import1.tab.py +++ /dev/null @@ -1,13 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -model.A = Set(initialize=['A1', 'A2', 'A3', 'A4']) -model.Y = Param(model.A) - -instance = model.create_instance('import1.tab.dat') - -print('Y') -keys = instance.Y.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.Y[key]))) diff --git a/doc/OnlineDocs/tests/data/import2.tab.py b/doc/OnlineDocs/tests/data/import2.tab.py deleted file mode 100644 index d03f053d090..00000000000 --- a/doc/OnlineDocs/tests/data/import2.tab.py +++ /dev/null @@ -1,14 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -model.A = Set() -model.Y = Param(model.A) - -instance = model.create_instance('import2.tab.dat') - -print('A ' + str(sorted(list(instance.A.data())))) -print('Y') -keys = instance.Y.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.Y[key]))) diff --git a/doc/OnlineDocs/tests/data/import3.tab.py b/doc/OnlineDocs/tests/data/import3.tab.py deleted file mode 100644 index e86557677ee..00000000000 --- a/doc/OnlineDocs/tests/data/import3.tab.py +++ /dev/null @@ -1,9 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -model.A = Set() - -instance = model.create_instance('import3.tab.dat') - -print('A ' + str(sorted(list(instance.A.data())))) diff --git a/doc/OnlineDocs/tests/data/import4.tab.py b/doc/OnlineDocs/tests/data/import4.tab.py deleted file mode 100644 index 93df9c761ab..00000000000 --- a/doc/OnlineDocs/tests/data/import4.tab.py +++ /dev/null @@ -1,9 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -model.C = Set(dimen=2) - -instance = model.create_instance('import4.tab.dat') - -print('C ' + str(sorted(list(instance.C.data())))) diff --git a/doc/OnlineDocs/tests/data/import5.tab.py b/doc/OnlineDocs/tests/data/import5.tab.py deleted file mode 100644 index 1d20476a16f..00000000000 --- a/doc/OnlineDocs/tests/data/import5.tab.py +++ /dev/null @@ -1,9 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -model.B = Set(dimen=2) - -instance = model.create_instance('import5.tab.dat') - -print('B ' + str(list(sorted(instance.B.data())))) diff --git a/doc/OnlineDocs/tests/data/import6.tab.py b/doc/OnlineDocs/tests/data/import6.tab.py deleted file mode 100644 index 8a1ab232f86..00000000000 --- a/doc/OnlineDocs/tests/data/import6.tab.py +++ /dev/null @@ -1,9 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -model.p = Param() - -instance = model.create_instance('import6.tab.dat') - -print('p ' + str(value(instance.p))) diff --git a/doc/OnlineDocs/tests/data/import7.tab.py b/doc/OnlineDocs/tests/data/import7.tab.py deleted file mode 100644 index 747d884be31..00000000000 --- a/doc/OnlineDocs/tests/data/import7.tab.py +++ /dev/null @@ -1,17 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -model.I = Set(initialize=['I1', 'I2', 'I3', 'I4']) -model.A = Set(initialize=['A1', 'A2', 'A3']) -model.U = Param(model.I, model.A) -# BUG: This should cause an error -# model.U = Param(model.A,model.I) - -instance = model.create_instance('import7.tab.dat') - -print('I ' + str(sorted(list(instance.I.data())))) -print('A ' + str(sorted(list(instance.A.data())))) -print('U') -for key in sorted(instance.U.keys()): - print(name(instance.U, key) + " " + str(value(instance.U[key]))) diff --git a/doc/OnlineDocs/tests/data/import8.tab.py b/doc/OnlineDocs/tests/data/import8.tab.py deleted file mode 100644 index b7866d7a3e5..00000000000 --- a/doc/OnlineDocs/tests/data/import8.tab.py +++ /dev/null @@ -1,15 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -model.I = Set(initialize=['I1', 'I2', 'I3', 'I4']) -model.A = Set(initialize=['A1', 'A2', 'A3']) -model.U = Param(model.A, model.I) - -instance = model.create_instance('import8.tab.dat') - -print('A ' + str(sorted(list(instance.A.data())))) -print('I ' + str(sorted(list(instance.I.data())))) -print('U') -for key in sorted(instance.U.keys()): - print(name(instance.U, key) + " " + str(value(instance.U[key]))) diff --git a/doc/OnlineDocs/tests/data/param1.py b/doc/OnlineDocs/tests/data/param1.py deleted file mode 100644 index c4bc8de5acc..00000000000 --- a/doc/OnlineDocs/tests/data/param1.py +++ /dev/null @@ -1,19 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Param() -model.B = Param() -model.C = Param() -model.D = Param() -model.E = Param() -# @decl - -instance = model.create_instance('param1.dat') - -print(value(instance.A)) -print(value(instance.B)) -print(value(instance.C)) -print(value(instance.D)) -print(value(instance.E)) diff --git a/doc/OnlineDocs/tests/data/param2.py b/doc/OnlineDocs/tests/data/param2.py deleted file mode 100644 index f46f05ceebc..00000000000 --- a/doc/OnlineDocs/tests/data/param2.py +++ /dev/null @@ -1,14 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Set() -model.B = Param(model.A) -# @decl - -instance = model.create_instance('param2.dat') - -keys = instance.B.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.B[key]))) diff --git a/doc/OnlineDocs/tests/data/param2a.py b/doc/OnlineDocs/tests/data/param2a.py deleted file mode 100644 index 4557f63d841..00000000000 --- a/doc/OnlineDocs/tests/data/param2a.py +++ /dev/null @@ -1,14 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Set() -model.B = Param(model.A) -# @decl - -instance = model.create_instance('param2a.dat') - -keys = instance.B.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.B[key]))) diff --git a/doc/OnlineDocs/tests/data/param4.py b/doc/OnlineDocs/tests/data/param4.py deleted file mode 100644 index 1190dae8dec..00000000000 --- a/doc/OnlineDocs/tests/data/param4.py +++ /dev/null @@ -1,15 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Set() -model.B = Param(model.A) -# @decl - -instance = model.create_instance('param4.dat') - -print('B') -keys = instance.B.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.B[key]))) diff --git a/doc/OnlineDocs/tests/data/param5.py b/doc/OnlineDocs/tests/data/param5.py deleted file mode 100644 index 69f6cc46552..00000000000 --- a/doc/OnlineDocs/tests/data/param5.py +++ /dev/null @@ -1,14 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Set(dimen=2) -model.B = Param(model.A) -# @decl - -instance = model.create_instance('param5.dat') - -keys = instance.B.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.B[key]))) diff --git a/doc/OnlineDocs/tests/data/param5a.py b/doc/OnlineDocs/tests/data/param5a.py deleted file mode 100644 index 303b92f9f2e..00000000000 --- a/doc/OnlineDocs/tests/data/param5a.py +++ /dev/null @@ -1,14 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Set(dimen=2) -model.B = Param(model.A) -# @decl - -instance = model.create_instance('param5a.dat') - -keys = instance.B.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.B[key]))) diff --git a/doc/OnlineDocs/tests/data/param7a.py b/doc/OnlineDocs/tests/data/param7a.py deleted file mode 100644 index 3bb68b3f3b7..00000000000 --- a/doc/OnlineDocs/tests/data/param7a.py +++ /dev/null @@ -1,14 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Set(dimen=2) -model.B = Param(model.A) -# @decl - -instance = model.create_instance('param7a.dat') - -keys = instance.B.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.B[key]))) diff --git a/doc/OnlineDocs/tests/data/param7b.py b/doc/OnlineDocs/tests/data/param7b.py deleted file mode 100644 index 6e5c857851f..00000000000 --- a/doc/OnlineDocs/tests/data/param7b.py +++ /dev/null @@ -1,14 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Set(dimen=2) -model.B = Param(model.A) -# @decl - -instance = model.create_instance('param7b.dat') - -keys = instance.B.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.B[key]))) diff --git a/doc/OnlineDocs/tests/data/param8a.py b/doc/OnlineDocs/tests/data/param8a.py deleted file mode 100644 index 57c9b08ca43..00000000000 --- a/doc/OnlineDocs/tests/data/param8a.py +++ /dev/null @@ -1,14 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Set(dimen=4) -model.B = Param(model.A) -# @decl - -instance = model.create_instance('param8a.dat') - -keys = instance.B.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.B[key]))) diff --git a/doc/OnlineDocs/tests/data/set1.py b/doc/OnlineDocs/tests/data/set1.py deleted file mode 100644 index 5248e9d5dc9..00000000000 --- a/doc/OnlineDocs/tests/data/set1.py +++ /dev/null @@ -1,13 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -model.A = Set() -model.B = Set() -model.C = Set() - -instance = model.create_instance('set1.dat') - -print(sorted(list(instance.A.data()))) -print(sorted((instance.B.data()))) -print(sorted(list((instance.C.data())), key=lambda x: x if type(x) is str else str(x))) diff --git a/doc/OnlineDocs/tests/data/set2.py b/doc/OnlineDocs/tests/data/set2.py deleted file mode 100644 index 82772f48e46..00000000000 --- a/doc/OnlineDocs/tests/data/set2.py +++ /dev/null @@ -1,11 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Set(dimen=3) -# @decl - -instance = model.create_instance('set2.dat') - -print(sorted(list(instance.A.data()))) diff --git a/doc/OnlineDocs/tests/data/set2a.py b/doc/OnlineDocs/tests/data/set2a.py deleted file mode 100644 index edf28757f96..00000000000 --- a/doc/OnlineDocs/tests/data/set2a.py +++ /dev/null @@ -1,11 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Set(dimen=3) -# @decl - -instance = model.create_instance('set2a.dat') - -print(sorted(list(instance.A.data()))) diff --git a/doc/OnlineDocs/tests/data/set3.py b/doc/OnlineDocs/tests/data/set3.py deleted file mode 100644 index d58e0c0dd43..00000000000 --- a/doc/OnlineDocs/tests/data/set3.py +++ /dev/null @@ -1,19 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Set() -model.B = Set(model.A) -# @decl -# model.C = Set(model.A,model.A) - -instance = model.create_instance('set3.dat') - -print(sorted(list(instance.A.data()), key=lambda x: x if type(x) is str else str(x))) -print(sorted(list(instance.B[1].data()), key=lambda x: x if type(x) is str else str(x))) -print( - sorted( - list(instance.B['aaa'].data()), key=lambda x: x if type(x) is str else str(x) - ) -) diff --git a/doc/OnlineDocs/tests/data/set4.py b/doc/OnlineDocs/tests/data/set4.py deleted file mode 100644 index 29548519571..00000000000 --- a/doc/OnlineDocs/tests/data/set4.py +++ /dev/null @@ -1,11 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Set(dimen=2) -# @decl - -instance = model.create_instance('set4.dat') - -print(sorted(list(instance.A.data()))) diff --git a/doc/OnlineDocs/tests/data/set5.py b/doc/OnlineDocs/tests/data/set5.py deleted file mode 100644 index 35acd4e4317..00000000000 --- a/doc/OnlineDocs/tests/data/set5.py +++ /dev/null @@ -1,13 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Set(dimen=4) -# @decl - -instance = model.create_instance('set5.dat') - - -for tpl in sorted(list(instance.A.data()), key=lambda x: tuple(map(str, x))): - print(tpl) diff --git a/doc/OnlineDocs/tests/data/table0.py b/doc/OnlineDocs/tests/data/table0.py deleted file mode 100644 index af7f634bd34..00000000000 --- a/doc/OnlineDocs/tests/data/table0.py +++ /dev/null @@ -1,9 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -model.A = Set(initialize=['A1', 'A2', 'A3']) -model.M = Param(model.A) - -instance = model.create_instance('table0.dat') -instance.pprint() diff --git a/doc/OnlineDocs/tests/data/table0.ul.py b/doc/OnlineDocs/tests/data/table0.ul.py deleted file mode 100644 index 213407b071c..00000000000 --- a/doc/OnlineDocs/tests/data/table0.ul.py +++ /dev/null @@ -1,9 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -model.A = Set(initialize=['A1', 'A2', 'A3']) -model.M = Param(model.A) - -instance = model.create_instance('table0.ul.dat') -instance.pprint() diff --git a/doc/OnlineDocs/tests/data/table1.py b/doc/OnlineDocs/tests/data/table1.py deleted file mode 100644 index 1f86508c60a..00000000000 --- a/doc/OnlineDocs/tests/data/table1.py +++ /dev/null @@ -1,9 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -model.A = Set(initialize=['A1', 'A2', 'A3']) -model.M = Param(model.A) - -instance = model.create_instance('table1.dat') -instance.pprint() diff --git a/doc/OnlineDocs/tests/data/table2.py b/doc/OnlineDocs/tests/data/table2.py deleted file mode 100644 index d7708b9277f..00000000000 --- a/doc/OnlineDocs/tests/data/table2.py +++ /dev/null @@ -1,12 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -model.A = Set(initialize=['A1', 'A2', 'A3']) -model.B = Set(initialize=['B1', 'B2', 'B3']) - -model.M = Param(model.A) -model.N = Param(model.A, model.B) - -instance = model.create_instance('table2.dat') -instance.pprint() diff --git a/doc/OnlineDocs/tests/data/table2.txt b/doc/OnlineDocs/tests/data/table2.txt deleted file mode 100644 index 6621e27f70c..00000000000 --- a/doc/OnlineDocs/tests/data/table2.txt +++ /dev/null @@ -1,21 +0,0 @@ -3 Set Declarations - A : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=None - ['A1', 'A2', 'A3'] - B : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=None - ['B1', 'B2', 'B3'] - N_index : Dim=0, Dimen=2, Size=9, Domain=None, Ordered=False, Bounds=None - Virtual - -2 Param Declarations - M : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 4.3 - A2 : 4.4 - A3 : 4.5 - N : Size=3, Index=N_index, Domain=Any, Default=None, Mutable=False - Key : Value - ('A1', 'B1') : 5.3 - ('A2', 'B2') : 5.4 - ('A3', 'B3') : 5.5 - -5 Declarations: A B M N_index N diff --git a/doc/OnlineDocs/tests/data/table3.py b/doc/OnlineDocs/tests/data/table3.py deleted file mode 100644 index fa871a4f79c..00000000000 --- a/doc/OnlineDocs/tests/data/table3.py +++ /dev/null @@ -1,14 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -model.A = Set() -model.B = Set(initialize=['B1', 'B2', 'B3']) -model.Z = Set(dimen=2) - -model.M = Param(model.A) -model.N = Param(model.A, model.B) - - -instance = model.create_instance('table3.dat') -instance.pprint() diff --git a/doc/OnlineDocs/tests/data/table3.txt b/doc/OnlineDocs/tests/data/table3.txt deleted file mode 100644 index e4194f6790d..00000000000 --- a/doc/OnlineDocs/tests/data/table3.txt +++ /dev/null @@ -1,23 +0,0 @@ -4 Set Declarations - A : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=None - ['A1', 'A2', 'A3'] - B : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=None - ['B1', 'B2', 'B3'] - N_index : Dim=0, Dimen=2, Size=9, Domain=None, Ordered=False, Bounds=None - Virtual - Z : Dim=0, Dimen=2, Size=3, Domain=None, Ordered=False, Bounds=None - [('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')] - -2 Param Declarations - M : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 4.3 - A2 : 4.4 - A3 : 4.5 - N : Size=3, Index=N_index, Domain=Any, Default=None, Mutable=False - Key : Value - ('A1', 'B1') : 5.3 - ('A2', 'B2') : 5.4 - ('A3', 'B3') : 5.5 - -6 Declarations: A B Z M N_index N diff --git a/doc/OnlineDocs/tests/data/table3.ul.py b/doc/OnlineDocs/tests/data/table3.ul.py deleted file mode 100644 index 713d36b9f3a..00000000000 --- a/doc/OnlineDocs/tests/data/table3.ul.py +++ /dev/null @@ -1,14 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -model.A = Set() -model.B = Set(initialize=['B1', 'B2', 'B3']) -model.Z = Set(dimen=2) - -model.M = Param(model.A) -model.N = Param(model.A, model.B) - - -instance = model.create_instance('table3.ul.dat') -instance.pprint() diff --git a/doc/OnlineDocs/tests/data/table3.ul.txt b/doc/OnlineDocs/tests/data/table3.ul.txt deleted file mode 100644 index e4194f6790d..00000000000 --- a/doc/OnlineDocs/tests/data/table3.ul.txt +++ /dev/null @@ -1,23 +0,0 @@ -4 Set Declarations - A : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=None - ['A1', 'A2', 'A3'] - B : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=None - ['B1', 'B2', 'B3'] - N_index : Dim=0, Dimen=2, Size=9, Domain=None, Ordered=False, Bounds=None - Virtual - Z : Dim=0, Dimen=2, Size=3, Domain=None, Ordered=False, Bounds=None - [('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')] - -2 Param Declarations - M : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 4.3 - A2 : 4.4 - A3 : 4.5 - N : Size=3, Index=N_index, Domain=Any, Default=None, Mutable=False - Key : Value - ('A1', 'B1') : 5.3 - ('A2', 'B2') : 5.4 - ('A3', 'B3') : 5.5 - -6 Declarations: A B Z M N_index N diff --git a/doc/OnlineDocs/tests/data/table4.py b/doc/OnlineDocs/tests/data/table4.py deleted file mode 100644 index 1af9fe47a44..00000000000 --- a/doc/OnlineDocs/tests/data/table4.py +++ /dev/null @@ -1,12 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -model.A = Set() -model.Z = Set(dimen=2) - -model.M = Param(model.A) -model.N = Param(model.Z) - -instance = model.create_instance('table4.dat') -instance.pprint() diff --git a/doc/OnlineDocs/tests/data/table4.ul.py b/doc/OnlineDocs/tests/data/table4.ul.py deleted file mode 100644 index 2acf8e21ca8..00000000000 --- a/doc/OnlineDocs/tests/data/table4.ul.py +++ /dev/null @@ -1,12 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -model.A = Set() -model.Z = Set(dimen=2) - -model.M = Param(model.A) -model.N = Param(model.Z) - -instance = model.create_instance('table4.ul.dat') -instance.pprint() diff --git a/doc/OnlineDocs/tests/data/table5.py b/doc/OnlineDocs/tests/data/table5.py deleted file mode 100644 index 2fe3d08fe91..00000000000 --- a/doc/OnlineDocs/tests/data/table5.py +++ /dev/null @@ -1,9 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -model.Z = Set(dimen=2) -model.Y = Set(dimen=2) - -instance = model.create_instance('table5.dat') -instance.pprint() diff --git a/doc/OnlineDocs/tests/data/table5.txt b/doc/OnlineDocs/tests/data/table5.txt deleted file mode 100644 index 76d8c59010f..00000000000 --- a/doc/OnlineDocs/tests/data/table5.txt +++ /dev/null @@ -1,7 +0,0 @@ -2 Set Declarations - Y : Dim=0, Dimen=2, Size=3, Domain=None, Ordered=False, Bounds=None - [(4.3, 5.3), (4.4, 5.4), (4.5, 5.5)] - Z : Dim=0, Dimen=2, Size=3, Domain=None, Ordered=False, Bounds=None - [('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')] - -2 Declarations: Z Y diff --git a/doc/OnlineDocs/tests/data/table6.py b/doc/OnlineDocs/tests/data/table6.py deleted file mode 100644 index fcbc2f10860..00000000000 --- a/doc/OnlineDocs/tests/data/table6.py +++ /dev/null @@ -1,8 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -model.pi = Param() - -instance = model.create_instance('table6.dat') -instance.pprint() diff --git a/doc/OnlineDocs/tests/data/table7.py b/doc/OnlineDocs/tests/data/table7.py deleted file mode 100644 index f8f8e769b2e..00000000000 --- a/doc/OnlineDocs/tests/data/table7.py +++ /dev/null @@ -1,10 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() - -model.A = Set(initialize=['A1', 'A2', 'A3']) -model.M = Param(model.A) -model.Z = Set(dimen=2) - -instance = model.create_instance('table7.dat') -instance.pprint() diff --git a/doc/OnlineDocs/tests/data/table7.txt b/doc/OnlineDocs/tests/data/table7.txt deleted file mode 100644 index 275e8543528..00000000000 --- a/doc/OnlineDocs/tests/data/table7.txt +++ /dev/null @@ -1,14 +0,0 @@ -2 Set Declarations - A : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=None - ['A1', 'A2', 'A3'] - Z : Dim=0, Dimen=2, Size=3, Domain=None, Ordered=False, Bounds=None - [('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')] - -1 Param Declarations - M : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 4.3 - A2 : 4.4 - A3 : 4.5 - -3 Declarations: A M Z diff --git a/doc/OnlineDocs/tests/dataportal/dataportal_tab.txt b/doc/OnlineDocs/tests/dataportal/dataportal_tab.txt deleted file mode 100644 index a3fa2bd8067..00000000000 --- a/doc/OnlineDocs/tests/dataportal/dataportal_tab.txt +++ /dev/null @@ -1,296 +0,0 @@ -1 Set Declarations - A : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=None - ['A1', 'A2', 'A3'] - -1 Declarations: A -1 Set Declarations - A : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=None - ['A1', 'A2', 'A3'] - -1 Declarations: A -1 Set Declarations - C : Dim=0, Dimen=2, Size=9, Domain=None, Ordered=False, Bounds=None - [('A1', 1), ('A1', 2), ('A1', 3), ('A2', 1), ('A2', 2), ('A2', 3), ('A3', 1), ('A3', 2), ('A3', 3)] - -1 Declarations: C -1 Set Declarations - D : Dim=0, Dimen=2, Size=3, Domain=None, Ordered=False, Bounds=None - [('A1', 1), ('A2', 2), ('A3', 3)] - -1 Declarations: D -1 Param Declarations - z : Size=1, Index=None, Domain=Any, Default=None, Mutable=False - Key : Value - None : 1.1 - -1 Declarations: z -1 Set Declarations - A : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=None - ['A1', 'A2', 'A3'] - -1 Param Declarations - y : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 3.3 - A2 : 3.4 - A3 : 3.5 - -2 Declarations: A y -1 Set Declarations - A : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=None - ['A1', 'A2', 'A3'] - -2 Param Declarations - w : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 4.3 - A2 : 4.4 - A3 : 4.5 - x : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 3.3 - A2 : 3.4 - A3 : 3.5 - -3 Declarations: A x w -1 Set Declarations - A : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=None - ['A1', 'A2', 'A3'] - -1 Param Declarations - y : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 3.3 - A2 : 3.4 - A3 : 3.5 - -2 Declarations: A y -1 Set Declarations - A : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=None - ['A1', 'A2', 'A3'] - -1 Param Declarations - w : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 4.3 - A2 : 4.4 - A3 : 4.5 - -2 Declarations: A w -3 Set Declarations - A : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=None - ['A1', 'A2', 'A3'] - I : Dim=0, Dimen=1, Size=4, Domain=None, Ordered=False, Bounds=None - ['I1', 'I2', 'I3', 'I4'] - u_index : Dim=0, Dimen=2, Size=12, Domain=None, Ordered=False, Bounds=None - Virtual - -1 Param Declarations - u : Size=12, Index=u_index, Domain=Any, Default=None, Mutable=False - Key : Value - ('I1', 'A1') : 1.3 - ('I1', 'A2') : 2.3 - ('I1', 'A3') : 3.3 - ('I2', 'A1') : 1.4 - ('I2', 'A2') : 2.4 - ('I2', 'A3') : 3.4 - ('I3', 'A1') : 1.5 - ('I3', 'A2') : 2.5 - ('I3', 'A3') : 3.5 - ('I4', 'A1') : 1.6 - ('I4', 'A2') : 2.6 - ('I4', 'A3') : 3.6 - -4 Declarations: A I u_index u -3 Set Declarations - A : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=None - ['A1', 'A2', 'A3'] - I : Dim=0, Dimen=1, Size=4, Domain=None, Ordered=False, Bounds=None - ['I1', 'I2', 'I3', 'I4'] - t_index : Dim=0, Dimen=2, Size=12, Domain=None, Ordered=False, Bounds=None - Virtual - -1 Param Declarations - t : Size=12, Index=t_index, Domain=Any, Default=None, Mutable=False - Key : Value - ('A1', 'I1') : 1.3 - ('A1', 'I2') : 1.4 - ('A1', 'I3') : 1.5 - ('A1', 'I4') : 1.6 - ('A2', 'I1') : 2.3 - ('A2', 'I2') : 2.4 - ('A2', 'I3') : 2.5 - ('A2', 'I4') : 2.6 - ('A3', 'I1') : 3.3 - ('A3', 'I2') : 3.4 - ('A3', 'I3') : 3.5 - ('A3', 'I4') : 3.6 - -4 Declarations: A I t_index t -1 Set Declarations - A : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=None - ['A1', 'A2', 'A3'] - -1 Param Declarations - s : Size=2, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 3.3 - A3 : 3.5 - -2 Declarations: A s -1 Set Declarations - A : Dim=0, Dimen=1, Size=4, Domain=None, Ordered=False, Bounds=None - ['A1', 'A2', 'A3', 'A4'] - -1 Param Declarations - y : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 3.3 - A2 : 3.4 - A3 : 3.5 - -2 Declarations: A y -1 Set Declarations - A : Dim=0, Dimen=2, Size=3, Domain=None, Ordered=False, Bounds=None - [('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')] - -1 Param Declarations - p : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - ('A1', 'B1') : 4.3 - ('A2', 'B2') : 4.4 - ('A3', 'B3') : 4.5 - -2 Declarations: A p -1 Set Declarations - A : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=None - ['A1', 'A2', 'A3'] - -1 Declarations: A -1 Set Declarations - y_index : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=None - ['A1', 'A2', 'A3'] - -2 Param Declarations - y : Size=3, Index=y_index, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 3.3 - A2 : 3.4 - A3 : 3.5 - z : Size=1, Index=None, Domain=Any, Default=None, Mutable=False - Key : Value - None : 1.1 - -3 Declarations: z y_index y -['A1', 'A2', 'A3'] -1.1 -A1 3.3 -A2 3.4 -A3 3.5 -1 Set Declarations - A : Dim=0, Dimen=2, Size=3, Domain=None, Ordered=False, Bounds=None - [('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')] - -1 Param Declarations - p : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - ('A1', 'B1') : 4.3 - ('A2', 'B2') : 4.4 - ('A3', 'B3') : 4.5 - -2 Declarations: A p -1 Set Declarations - A : Dim=0, Dimen=2, Size=0, Domain=None, Ordered=False, Bounds=None - [] - -1 Param Declarations - p : Size=0, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - -2 Declarations: A p -1 Set Declarations - A : Dim=0, Dimen=2, Size=3, Domain=None, Ordered=False, Bounds=None - [('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')] - -1 Param Declarations - p : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - ('A1', 'B1') : 4.3 - ('A2', 'B2') : 4.4 - ('A3', 'B3') : 4.5 - -2 Declarations: A p -1 Set Declarations - A : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=None - ['A1', 'A2', 'A3'] - -1 Param Declarations - p : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 4.3 - A2 : 4.4 - A3 : 4.5 - -2 Declarations: A p -3 Set Declarations - A : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=None - ['A1', 'A2', 'A3'] - B : Dim=0, Dimen=2, Size=3, Domain=None, Ordered=False, Bounds=None - [(1, 'B1'), (2, 'B2'), (3, 'B3')] - C : Dim=1, Dimen=1, Size=6, Domain=None, ArraySize=2, Ordered=False, Bounds=None - Key : Members - A1 : [1, 2, 3] - A3 : [10, 20, 30] - -3 Param Declarations - p : Size=1, Index=None, Domain=Any, Default=None, Mutable=False - Key : Value - None : 0.1 - q : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 3.3 - A2 : 3.4 - A3 : 3.5 - r : Size=3, Index=B, Domain=Any, Default=None, Mutable=False - Key : Value - (1, 'B1') : 3.3 - (2, 'B2') : 3.4 - (3, 'B3') : 3.5 - -6 Declarations: A B C p q r -3 Set Declarations - A : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=None - ['A1', 'A2', 'A3'] - B : Dim=0, Dimen=2, Size=3, Domain=None, Ordered=False, Bounds=None - [(1, 'B1'), (2, 'B2'), (3, 'B3')] - C : Dim=1, Dimen=1, Size=6, Domain=None, ArraySize=2, Ordered=False, Bounds=None - Key : Members - A1 : [1, 2, 3] - A3 : [10, 20, 30] - -3 Param Declarations - p : Size=1, Index=None, Domain=Any, Default=None, Mutable=False - Key : Value - None : 0.1 - q : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 3.3 - A2 : 3.4 - A3 : 3.5 - r : Size=3, Index=B, Domain=Any, Default=None, Mutable=False - Key : Value - (1, 'B1') : 3.3 - (2, 'B2') : 3.4 - (3, 'B3') : 3.5 - -6 Declarations: A B C p q r -1 Set Declarations - C : Dim=0, Dimen=2, Size=9, Domain=None, Ordered=False, Bounds=None - [('A1', 1), ('A1', 2), ('A1', 3), ('A2', 1), ('A2', 2), ('A2', 3), ('A3', 1), ('A3', 2), ('A3', 3)] - -1 Declarations: C -1 Set Declarations - C : Dim=0, Dimen=2, Size=3, Domain=None, Ordered=False, Bounds=None - [('A1', 1), ('A2', 2), ('A3', 3)] - -1 Declarations: C diff --git a/doc/OnlineDocs/tests/dataportal/param_initialization.py b/doc/OnlineDocs/tests/dataportal/param_initialization.py deleted file mode 100644 index 5567b01f284..00000000000 --- a/doc/OnlineDocs/tests/dataportal/param_initialization.py +++ /dev/null @@ -1,25 +0,0 @@ -from pyomo.environ import * -import numpy - -model = ConcreteModel() - -# @decl1 -model.a = Param(initialize=1.1) -# @decl1 - -# Initialize with a dictionary -# @decl2 -model.b = Param([1, 2, 3], initialize={1: 1, 2: 2, 3: 3}) -# @decl2 - - -# Initialize with a function that returns native Python data -# @decl3 -def c(model): - return {1: 1, 2: 2, 3: 3} - - -model.c = Param([1, 2, 3], initialize=c) -# @decl3 - -model.pprint(verbose=True) diff --git a/doc/OnlineDocs/tests/dataportal/param_initialization.txt b/doc/OnlineDocs/tests/dataportal/param_initialization.txt deleted file mode 100644 index baf24eac293..00000000000 --- a/doc/OnlineDocs/tests/dataportal/param_initialization.txt +++ /dev/null @@ -1,22 +0,0 @@ -2 Set Declarations - b_index : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=(1, 3) - [1, 2, 3] - c_index : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=(1, 3) - [1, 2, 3] - -3 Param Declarations - a : Size=1, Index=None, Domain=Any, Default=None, Mutable=False - Key : Value - None : 1.1 - b : Size=3, Index=b_index, Domain=Any, Default=None, Mutable=False - Key : Value - 1 : 1 - 2 : 2 - 3 : 3 - c : Size=3, Index=c_index, Domain=Any, Default=None, Mutable=False - Key : Value - 1 : 1 - 2 : 2 - 3 : 3 - -5 Declarations: a b_index b c_index c diff --git a/doc/OnlineDocs/tests/dataportal/set_initialization.txt b/doc/OnlineDocs/tests/dataportal/set_initialization.txt deleted file mode 100644 index 3bfbdad1cdc..00000000000 --- a/doc/OnlineDocs/tests/dataportal/set_initialization.txt +++ /dev/null @@ -1,24 +0,0 @@ -9 Set Declarations - A : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=(2, 5) - [2, 3, 5] - B : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=(2, 5) - [2, 3, 5] - C : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=(2, 5) - [2, 3, 5] - D : Dim=0, Dimen=1, Size=9, Domain=None, Ordered=False, Bounds=(0, 8) - [0, 1, 2, 3, 4, 5, 6, 7, 8] - E : Dim=0, Dimen=1, Size=1, Domain=None, Ordered=False, Bounds=(2, 2) - [2] - F : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=(2, 5) - [2, 3, 5] - G : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=(2, 5) - [2, 3, 5] - H : Dim=1, Dimen=1, Size=9, Domain=None, ArraySize=3, Ordered=False, Bounds=None - Key : Members - 2 : [1, 3, 5] - 3 : [2, 4, 6] - 4 : [3, 5, 7] - H_index : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=(2, 4) - [2, 3, 4] - -9 Declarations: A B C D E F G H_index H diff --git a/doc/OnlineDocs/tests/expr/index.py b/doc/OnlineDocs/tests/expr/index.py deleted file mode 100644 index 9c9c79bf7be..00000000000 --- a/doc/OnlineDocs/tests/expr/index.py +++ /dev/null @@ -1,10 +0,0 @@ -from pyomo.environ import * - -# --------------------------------------------- -# @simple -M = ConcreteModel() -M.v = Var() - -e = M.v * 2 -# @simple -print(e) diff --git a/doc/OnlineDocs/tests/scripting/AbstractSuffixes.py b/doc/OnlineDocs/tests/scripting/AbstractSuffixes.py deleted file mode 100644 index 20a4cc20581..00000000000 --- a/doc/OnlineDocs/tests/scripting/AbstractSuffixes.py +++ /dev/null @@ -1,24 +0,0 @@ -from pyomo.environ import * - -model = AbstractModel() -model.I = RangeSet(1, 4) -model.x = Var(model.I) - - -def c_rule(m, i): - return m.x[i] >= i - - -model.c = Constraint(model.I, rule=c_rule) - - -def foo_rule(m): - return ((m.x[i], 3.0 * i) for i in m.I) - - -model.foo = Suffix(rule=foo_rule) - -# instantiate the model -inst = model.create_instance() -for i in inst.I: - print(i, inst.foo[inst.x[i]]) diff --git a/doc/OnlineDocs/tests/scripting/NodesIn_init.py b/doc/OnlineDocs/tests/scripting/NodesIn_init.py deleted file mode 100644 index 4a90029baa3..00000000000 --- a/doc/OnlineDocs/tests/scripting/NodesIn_init.py +++ /dev/null @@ -1,9 +0,0 @@ -def NodesIn_init(model, node): - retval = [] - for i, j in model.Arcs: - if j == node: - retval.append(i) - return retval - - -model.NodesIn = Set(model.Nodes, initialize=NodesIn_init) diff --git a/doc/OnlineDocs/tests/scripting/Z_init.py b/doc/OnlineDocs/tests/scripting/Z_init.py deleted file mode 100644 index 426de6f7d08..00000000000 --- a/doc/OnlineDocs/tests/scripting/Z_init.py +++ /dev/null @@ -1,7 +0,0 @@ -def Z_init(model, i): - if i > 10: - return Set.End - return 2 * i + 1 - - -model.Z = Set(initialize=Z_init) diff --git a/doc/OnlineDocs/tests/scripting/concrete1.py b/doc/OnlineDocs/tests/scripting/concrete1.py deleted file mode 100644 index 1c1f1517e17..00000000000 --- a/doc/OnlineDocs/tests/scripting/concrete1.py +++ /dev/null @@ -1,10 +0,0 @@ -from __future__ import division -from pyomo.environ import * - -model = ConcreteModel() - -model.x = Var([1, 2], domain=NonNegativeReals) - -model.OBJ = Objective(expr=2 * model.x[1] + 3 * model.x[2]) - -model.Constraint1 = Constraint(expr=3 * model.x[1] + 4 * model.x[2] >= 1) diff --git a/doc/OnlineDocs/tests/scripting/doubleA.py b/doc/OnlineDocs/tests/scripting/doubleA.py deleted file mode 100644 index 12a07944db3..00000000000 --- a/doc/OnlineDocs/tests/scripting/doubleA.py +++ /dev/null @@ -1,5 +0,0 @@ -def doubleA_init(model): - return (i * 2 for i in model.A) - - -model.C = Set(initialize=DoubleA_init) diff --git a/doc/OnlineDocs/tests/test_examples.py b/doc/OnlineDocs/tests/test_examples.py deleted file mode 100644 index 0ee6a249c38..00000000000 --- a/doc/OnlineDocs/tests/test_examples.py +++ /dev/null @@ -1,270 +0,0 @@ -# Imports -import pyomo.common.unittest as unittest -import glob -import os -import os.path -import sys -import pyomo.environ - -try: - import yaml - - yaml_available = True -except: - yaml_available = False - -# Find all *.txt files, and use them to define baseline tests -currdir = os.path.dirname(os.path.abspath(__file__)) -datadir = currdir -testdirs = [currdir] - -solver_dependencies = { - 'Test_nonlinear_ch': { - 'test_rosen_pyomo_rosen': 'ipopt', - 'test_react_design_run_pyomo_reactor_table': 'ipopt', - 'test_react_design_run_pyomo_reactor': 'ipopt', - 'test_multimodal_pyomo_multimodal_init1': 'ipopt', - 'test_multimodal_pyomo_multimodal_init2': 'ipopt', - 'test_disease_est_run_disease_summary': 'ipopt', - 'test_disease_est_run_disease_callback': 'ipopt', - 'test_deer_run_deer': 'ipopt', - }, - 'Test_mpec_ch': {'test_mpec_ch_path1': 'path'}, - 'Test_dae_ch': {'test_run_path_constraint_tester': 'ipopt'}, -} -package_dependencies = { - 'Test_data': { - 'test_data_ABCD9': ['pyodbc'], - 'test_data_ABCD8': ['pyodbc'], - 'test_data_ABCD7': ['win32com'], - }, - 'Test_dataportal': { - 'test_dataportal_dataportal_tab': ['xlrd'], - 'test_dataportal_set_initialization': ['numpy'], - 'test_dataportal_param_initialization': ['numpy'], - }, -} -solver_available = {} -package_available = {} - -only_book_tests = set(['Test_nonlinear_ch', 'Test_scripts_ch']) - - -def _check_available(name): - from pyomo.opt.base.solvers import check_available_solvers - - return bool(check_available_solvers(name)) - - -def check_skip(tfname_, name): - # - # Skip if YAML isn't installed - # - if not yaml_available: - return "YAML is not available" - # - # Initialize the availability data - # - if len(solver_available) == 0: - for tf_ in solver_dependencies: - for n_ in solver_dependencies[tf_]: - solver_ = solver_dependencies[tf_][n_] - if not solver_ in solver_available: - solver_available[solver_] = _check_available(solver_) - for tf_ in package_dependencies: - for n_ in package_dependencies[tf_]: - packages_ = package_dependencies[tf_][n_] - for package_ in packages_: - if not package_ in package_available: - try: - __import__(package_) - package_available[package_] = True - except: - package_available[package_] = False - # - # Return a boolean if the test should be skipped - # - if tfname_ in solver_dependencies: - if ( - name in solver_dependencies[tfname_] - and not solver_available[solver_dependencies[tfname_][name]] - ): - # Skip the test because a solver is not available - # print('Skipping %s because of missing solver' %(name)) - return 'Solver "%s" is not available' % ( - solver_dependencies[tfname_][name], - ) - if tfname_ in package_dependencies: - if name in package_dependencies[tfname_]: - packages_ = package_dependencies[tfname_][name] - if not all([package_available[i] for i in packages_]): - # Skip the test because a package is not available - # print('Skipping %s because of missing package' %(name)) - _missing = [] - for i in packages_: - if not package_available[i]: - _missing.append(i) - return "Package%s %s %s not available" % ( - 's' if len(_missing) > 1 else '', - ", ".join(_missing), - 'are' if len(_missing) > 1 else 'is', - ) - return False - - -def filter(line): - # Ignore certain text when comparing output with baseline - - # Ipopt 3.12.4 puts BACKSPACE (chr(8) / ^H) into the output. - line = line.strip(" \n\t" + chr(8)) - - if not line: - return True - for field in ( - '[', - 'password:', - 'http:', - 'Job ', - 'Importing module', - 'Function', - 'File', - ): - if line.startswith(field): - return True - for field in ( - 'Total CPU', - 'Ipopt', - 'Status: optimal', - 'Status: feasible', - 'time:', - 'Time:', - 'with format cpxlp', - 'usermodel = `_ +* `Pyomo — Optimization Modeling in Python + `_ ([PyomoBookIII]_) -`Pyomo Gallery -`_ +* `Pyomo Workshop Slides and Exercises + `_ + +* `Prof. Jeffrey Kantor's Pyomo Cookbook + `_ + +* The `companion notebooks `_ + for *Hands-On Mathematical Optimization with Python* + +* `Pyomo Gallery `_ diff --git a/doc/OnlineDocs/working_abstractmodels/BuildAction.rst b/doc/OnlineDocs/working_abstractmodels/BuildAction.rst index 5706ce97e6f..6840e15a1d5 100644 --- a/doc/OnlineDocs/working_abstractmodels/BuildAction.rst +++ b/doc/OnlineDocs/working_abstractmodels/BuildAction.rst @@ -13,7 +13,7 @@ trigger actions to be done as part of the model building process. The takes as arguments optional index sets and a function to perform the action. For example, -.. literalinclude:: ../tests/scripting/abstract2piecebuild_BuildAction_example.spy +.. literalinclude:: ../src/scripting/abstract2piecebuild_BuildAction_example.spy :language: python calls the function ``bpts_build`` for each member of ``model.J``. The @@ -21,14 +21,14 @@ function ``bpts_build`` should have the model and a variable for the members of ``model.J`` as formal arguments. In this example, the following would be a valid declaration for the function: -.. literalinclude:: ../tests/scripting/abstract2piecebuild_Function_valid_declaration.spy +.. literalinclude:: ../src/scripting/abstract2piecebuild_Function_valid_declaration.spy :language: python A full example, which extends the :ref:`abstract2.py` and :ref:`abstract2piece.py` examples, is -.. literalinclude:: ../tests/scripting/abstract2piecebuild.spy +.. literalinclude:: ../src/scripting/abstract2piecebuild.spy :language: python This example uses the build action to create a model component with @@ -51,13 +51,13 @@ clearer, to use a build action. The full model is: -.. literalinclude:: ../tests/scripting/Isinglebuild.py +.. literalinclude:: ../src/scripting/Isinglebuild.py :language: python for this model, the same data file can be used as for Isinglecomm.py in :ref:`Isinglecomm.py` such as the toy data file: -.. literalinclude:: ../tests/scripting/Isinglecomm.dat +.. literalinclude:: ../src/scripting/Isinglecomm.dat Build actions can also be a way to implement data validation, particularly when multiple Sets or Parameters must be analyzed. However, diff --git a/doc/OnlineDocs/working_abstractmodels/data/dataportals.rst b/doc/OnlineDocs/working_abstractmodels/data/dataportals.rst index 88b89653a37..5ce907fda2a 100644 --- a/doc/OnlineDocs/working_abstractmodels/data/dataportals.rst +++ b/doc/OnlineDocs/working_abstractmodels/data/dataportals.rst @@ -62,14 +62,14 @@ can be used to initialize both concrete and abstract Pyomo models. Consider the file ``A.tab``, which defines a simple set with a tabular format: -.. literalinclude:: ../../tests/dataportal/A.tab +.. literalinclude:: ../../src/dataportal/A.tab :language: none The ``load`` method is used to load data into a :class:`~pyomo.environ.DataPortal` object. Components in a concrete model can be explicitly initialized with data loaded by a :class:`~pyomo.environ.DataPortal` object: -.. literalinclude:: ../../tests/dataportal/dataportal_tab_concrete1.spy +.. literalinclude:: ../../src/dataportal/dataportal_tab_concrete1.spy :language: python All data needed to initialize an abstract model *must* be provided by a @@ -77,7 +77,7 @@ All data needed to initialize an abstract model *must* be provided by a and the use of the :class:`~pyomo.environ.DataPortal` object to initialize components is automated for the user: -.. literalinclude:: ../../tests/dataportal/dataportal_tab_load.spy +.. literalinclude:: ../../src/dataportal/dataportal_tab_load.spy :language: python Note the difference in the execution of the ``load`` method in these two @@ -126,7 +126,7 @@ that are loaded from different data sources. The ``[]`` operator is used to access set and parameter values. Consider the following example, which loads data and prints the value of the ``[]`` operator: -.. literalinclude:: ../../tests/dataportal/dataportal_tab_getitem.spy +.. literalinclude:: ../../src/dataportal/dataportal_tab_getitem.spy :language: python The :class:`~pyomo.environ.DataPortal` @@ -162,12 +162,12 @@ with lists and dictionaries: For example, consider the following JSON file: -.. literalinclude:: ../../tests/dataportal/T.json +.. literalinclude:: ../../src/dataportal/T.json :language: none The data in this file can be used to load the following model: -.. literalinclude:: ../../tests/dataportal/dataportal_tab_json1.spy +.. literalinclude:: ../../src/dataportal/dataportal_tab_json1.spy :language: python Note that no ``set`` or ``param`` option needs to be specified when @@ -178,13 +178,13 @@ needed for model construction is used. The following YAML file has a similar structure: -.. literalinclude:: ../../tests/dataportal/T.yaml +.. literalinclude:: ../../src/dataportal/T.yaml :language: none The data in this file can be used to load a Pyomo model with the same syntax as a JSON file: -.. literalinclude:: ../../tests/dataportal/dataportal_tab_yaml1.spy +.. literalinclude:: ../../src/dataportal/dataportal_tab_yaml1.spy :language: python @@ -212,7 +212,7 @@ TAB files represent tabular data in an ascii file using whitespace as a delimiter. A TAB file consists of rows of values, where each row has the same length. For example, the file ``PP.tab`` has the format: -.. literalinclude:: ../../tests/dataportal/PP.tab +.. literalinclude:: ../../src/dataportal/PP.tab :language: none CSV files represent tabular data in a format that is very similar to TAB @@ -220,7 +220,7 @@ files. Pyomo assumes that a CSV file consists of rows of values, where each row has the same length. For example, the file ``PP.csv`` has the format: -.. literalinclude:: ../../tests/dataportal/PP.csv +.. literalinclude:: ../../src/dataportal/PP.csv :language: none Excel spreadsheets can express complex data relationships. A *range* is @@ -242,7 +242,7 @@ sub-element of a ``row`` element represents a different column, where each row has the same length. For example, the file ``PP.xml`` has the format: -.. literalinclude:: ../../tests/dataportal/PP.xml +.. literalinclude:: ../../src/dataportal/PP.xml :language: none Loading Set Data @@ -256,13 +256,13 @@ Loading a Simple Set Consider the file ``A.tab``, which defines a simple set: -.. literalinclude:: ../../tests/dataportal/A.tab +.. literalinclude:: ../../src/dataportal/A.tab :language: none In the following example, a :class:`~pyomo.environ.DataPortal` object loads data for a simple set ``A``: -.. literalinclude:: ../../tests/dataportal/dataportal_tab_set1.spy +.. literalinclude:: ../../src/dataportal/dataportal_tab_set1.spy :language: python Loading a Set of Tuples @@ -270,13 +270,13 @@ Loading a Set of Tuples Consider the file ``C.tab``: -.. literalinclude:: ../../tests/dataportal/C.tab +.. literalinclude:: ../../src/dataportal/C.tab :language: none In the following example, a :class:`~pyomo.environ.DataPortal` object loads data for a two-dimensional set ``C``: -.. literalinclude:: ../../tests/dataportal/dataportal_tab_set2.spy +.. literalinclude:: ../../src/dataportal/dataportal_tab_set2.spy :language: python In this example, the column titles do not directly impact the process of @@ -289,13 +289,13 @@ Loading a Set Array Consider the file ``D.tab``, which defines an array representation of a two-dimensional set: -.. literalinclude:: ../../tests/dataportal/D.tab +.. literalinclude:: ../../src/dataportal/D.tab :language: none In the following example, a :class:`~pyomo.environ.DataPortal` object loads data for a two-dimensional set ``D``: -.. literalinclude:: ../../tests/dataportal/dataportal_tab_set3.spy +.. literalinclude:: ../../src/dataportal/dataportal_tab_set3.spy :language: python The ``format`` option indicates that the set data is declared in a array @@ -313,13 +313,13 @@ Loading a Simple Parameter The simplest parameter is simply a singleton value. Consider the file ``Z.tab``: -.. literalinclude:: ../../tests/dataportal/Z.tab +.. literalinclude:: ../../src/dataportal/Z.tab :language: none In the following example, a :class:`~pyomo.environ.DataPortal` object loads data for a simple parameter ``z``: -.. literalinclude:: ../../tests/dataportal/dataportal_tab_param1.spy +.. literalinclude:: ../../src/dataportal/dataportal_tab_param1.spy :language: python Loading an Indexed Parameter @@ -328,13 +328,13 @@ Loading an Indexed Parameter An indexed parameter can be defined by a single column in a table. For example, consider the file ``Y.tab``: -.. literalinclude:: ../../tests/dataportal/Y.tab +.. literalinclude:: ../../src/dataportal/Y.tab :language: none In the following example, a :class:`~pyomo.environ.DataPortal` object loads data for an indexed parameter ``y``: -.. literalinclude:: ../../tests/dataportal/dataportal_tab_param2.spy +.. literalinclude:: ../../src/dataportal/dataportal_tab_param2.spy :language: python When column names are not used to specify the index and parameter data, @@ -351,19 +351,19 @@ The index set can be loaded with the parameter data using the ``index`` option. In the following example, a :class:`~pyomo.environ.DataPortal` object loads data for set ``A`` and the indexed parameter ``y`` -.. literalinclude:: ../../tests/dataportal/dataportal_tab_param3.spy +.. literalinclude:: ../../src/dataportal/dataportal_tab_param3.spy :language: python An index set with multiple dimensions can also be loaded with an indexed parameter. Consider the file ``PP.tab``: -.. literalinclude:: ../../tests/dataportal/PP.tab +.. literalinclude:: ../../src/dataportal/PP.tab :language: none In the following example, a :class:`~pyomo.environ.DataPortal` object loads data for a tuple set and an indexed parameter: -.. literalinclude:: ../../tests/dataportal/dataportal_tab_param10.spy +.. literalinclude:: ../../src/dataportal/dataportal_tab_param10.spy :language: python Loading a Parameter with Missing Values @@ -373,7 +373,7 @@ Missing parameter data can be expressed in two ways. First, parameter data can be defined with indices that are a subset of valid indices in the model. The following example loads the indexed parameter ``y``: -.. literalinclude:: ../../tests/dataportal/dataportal_tab_param9.spy +.. literalinclude:: ../../src/dataportal/dataportal_tab_param9.spy :language: python The model defines an index set with four values, but only three @@ -382,13 +382,13 @@ parameter values are declared in the data file ``Y.tab``. Parameter data can also be declared with missing values using the period (``.``) symbol. For example, consider the file ``S.tab``: -.. literalinclude:: ../../tests/dataportal/PP.tab +.. literalinclude:: ../../src/dataportal/PP.tab :language: none In the following example, a :class:`~pyomo.environ.DataPortal` object loads data for the index set ``A`` and indexed parameter ``y``: -.. literalinclude:: ../../tests/dataportal/dataportal_tab_param8.spy +.. literalinclude:: ../../src/dataportal/dataportal_tab_param8.spy :language: python The period (``.``) symbol indicates a missing parameter value, but the @@ -400,13 +400,13 @@ Loading Multiple Parameters Multiple parameters can be initialized at once by specifying a list (or tuple) of component parameters. Consider the file ``XW.tab``: -.. literalinclude:: ../../tests/dataportal/XW.tab +.. literalinclude:: ../../src/dataportal/XW.tab :language: none In the following example, a :class:`~pyomo.environ.DataPortal` object loads data for parameters ``x`` and ``w``: -.. literalinclude:: ../../tests/dataportal/dataportal_tab_param4.spy +.. literalinclude:: ../../src/dataportal/dataportal_tab_param4.spy :language: python Selecting Parameter Columns @@ -421,7 +421,7 @@ component data. For example, consider the following load declaration: -.. literalinclude:: ../../tests/dataportal/dataportal_tab_param5.spy +.. literalinclude:: ../../src/dataportal/dataportal_tab_param5.spy :language: python The columns ``A`` and ``W`` are selected from the file ``XW.tab``, and a @@ -433,20 +433,20 @@ Loading a Parameter Array Consider the file ``U.tab``, which defines an array representation of a multiply-indexed parameter: -.. literalinclude:: ../../tests/dataportal/U.tab +.. literalinclude:: ../../src/dataportal/U.tab :language: none In the following example, a :class:`~pyomo.environ.DataPortal` object loads data for a two-dimensional parameter ``u``: -.. literalinclude:: ../../tests/dataportal/dataportal_tab_param6.spy +.. literalinclude:: ../../src/dataportal/dataportal_tab_param6.spy :language: python The ``format`` option indicates that the parameter data is declared in a array format. The ``format`` option can also indicate that the parameter data should be transposed. -.. literalinclude:: ../../tests/dataportal/dataportal_tab_param7.spy +.. literalinclude:: ../../src/dataportal/dataportal_tab_param7.spy :language: python Note that the transposed parameter data changes the index set for the @@ -467,7 +467,7 @@ the following range of cells, which is named ``PPtable``: In the following example, a :class:`~pyomo.environ.DataPortal` object loads the named range ``PPtable`` from the file ``excel.xls``: -.. literalinclude:: ../../tests/dataportal/dataportal_tab_excel1.spy +.. literalinclude:: ../../src/dataportal/dataportal_tab_excel1.spy :language: python Note that the ``range`` option is required to specify the table of cell @@ -477,14 +477,14 @@ There are a variety of ways that data can be loaded from a relational database. In the simplest case, a table can be specified within a database: -.. literalinclude:: ../../tests/dataportal/dataportal_tab_db1.spy +.. literalinclude:: ../../src/dataportal/dataportal_tab_db1.spy :language: python In this example, the interface ``sqlite3`` is used to load data from an SQLite database in the file ``PP.sqlite``. More generally, an SQL query can be specified to dynamically generate a table. For example: -.. literalinclude:: ../../tests/dataportal/dataportal_tab_db2.spy +.. literalinclude:: ../../src/dataportal/dataportal_tab_db2.spy :language: python Data Namespaces @@ -524,7 +524,7 @@ components. For example, the following script generates two model instances from an abstract model using data loaded into different namespaces: -.. literalinclude:: ../../tests/dataportal/dataportal_tab_namespaces1.spy +.. literalinclude:: ../../src/dataportal/dataportal_tab_namespaces1.spy :language: python diff --git a/doc/OnlineDocs/working_abstractmodels/data/datfiles.rst b/doc/OnlineDocs/working_abstractmodels/data/datfiles.rst index 06d28093cf4..4982ed2fff0 100644 --- a/doc/OnlineDocs/working_abstractmodels/data/datfiles.rst +++ b/doc/OnlineDocs/working_abstractmodels/data/datfiles.rst @@ -104,7 +104,7 @@ A set may be empty, and it may contain any combination of numeric and non-numeric string values. For example, the following are valid ``set`` commands: -.. literalinclude:: ../../tests/data/set1.dat +.. literalinclude:: ../../src/data/set1.dat :language: python @@ -115,19 +115,19 @@ The ``set`` data command can also specify tuple data with the standard notation for tuples. For example, suppose that set ``A`` contains 3-tuples: -.. literalinclude:: ../../tests/data/set2_decl.spy +.. literalinclude:: ../../src/data/set2_decl.spy :language: python The following ``set`` data command then specifies that ``A`` is the set containing the tuples ``(1,2,3)`` and ``(4,5,6)``: -.. literalinclude:: ../../tests/data/set2a.dat +.. literalinclude:: ../../src/data/set2a.dat :language: none Alternatively, set data can simply be listed in the order that the tuple is represented: -.. literalinclude:: ../../tests/data/set2.dat +.. literalinclude:: ../../src/data/set2.dat :language: none Obviously, the number of data elements specified using this syntax @@ -138,7 +138,7 @@ membership. For example, the following ``set`` data command declares 2-tuples in ``A`` using plus (``+``) to denote valid tuples and minus (``-``) to denote invalid tuples: -.. literalinclude:: ../../tests/data/set4.dat +.. literalinclude:: ../../src/data/set4.dat :language: none This data command declares the following five 2-tuples: ``('A1',1)``, @@ -148,13 +148,13 @@ Finally, a set of tuple data can be concisely represented with tuple *templates* that represent a *slice* of tuple data. For example, suppose that the set ``A`` contains 4-tuples: -.. literalinclude:: ../../tests/data/set5_decl.spy +.. literalinclude:: ../../src/data/set5_decl.spy :language: python The following ``set`` data command declares groups of tuples that are defined by a template and data to complete this template: -.. literalinclude:: ../../tests/data/set5.dat +.. literalinclude:: ../../src/data/set5.dat :language: none A tuple template consists of a tuple that contains one or more asterisk @@ -163,7 +163,7 @@ tuple value is replaced by the values from the list of values that follows the tuple template. In this example, the following tuples are in set ``A``: -.. literalinclude:: ../../tests/data/set5.txt +.. literalinclude:: ../../src/data/set5.txt :language: none Set Arrays @@ -183,12 +183,12 @@ list of string values. Suppose that a set ``A`` is used to index a set ``B`` as follows: -.. literalinclude:: ../../tests/data/set3_decl.spy +.. literalinclude:: ../../src/data/set3_decl.spy :language: python Then set ``B`` is indexed using the values declared for set ``A``: -.. literalinclude:: ../../tests/data/set3.dat +.. literalinclude:: ../../src/data/set3.dat :language: none The ``param`` Command @@ -197,7 +197,7 @@ The ``param`` Command Simple or non-indexed parameters are declared in an obvious way, as shown by these examples: -.. literalinclude:: ../../tests/data/param1.dat +.. literalinclude:: ../../src/data/param1.dat :language: none Parameters can be defined with numeric data, simple strings and quoted @@ -213,33 +213,33 @@ parameter data. One-dimensional parameter data is indexed over a single set. Suppose that the parameter ``B`` is a parameter indexed by the set ``A``: -.. literalinclude:: ../../tests/data/param2_decl.spy +.. literalinclude:: ../../src/data/param2_decl.spy :language: python A ``param`` data command can specify values for ``B`` with a list of index-value pairs: -.. literalinclude:: ../../tests/data/param2.dat +.. literalinclude:: ../../src/data/param2.dat :language: none Because whitespace is ignored, this example data command file can be reorganized to specify the same data in a tabular format: -.. literalinclude:: ../../tests/data/param2a.dat +.. literalinclude:: ../../src/data/param2a.dat :language: none Multiple parameters can be defined using a single ``param`` data command. For example, suppose that parameters ``B``, ``C``, and ``D`` are one-dimensional parameters all indexed by the set ``A``: -.. literalinclude:: ../../tests/data/param3_decl.spy +.. literalinclude:: ../../src/data/param3_decl.spy :language: python Values for these parameters can be specified using a single ``param`` data command that declares these parameter names followed by a list of index and parameter values: -.. literalinclude:: ../../tests/data/param3.dat +.. literalinclude:: ../../src/data/param3.dat :language: none The values in the ``param`` data command are interpreted as a list of @@ -249,7 +249,7 @@ corresponding numeric value. Note that parameter values do not need to be defined for all indices. For example, the following data command file is valid: -.. literalinclude:: ../../tests/data/param3a.dat +.. literalinclude:: ../../src/data/param3a.dat :language: none The index ``g`` is omitted from the ``param`` command, and consequently @@ -259,7 +259,7 @@ More complex patterns of missing data can be specified using the period specifying multiple parameters that do not necessarily have the same index values: -.. literalinclude:: ../../tests/data/param3b.dat +.. literalinclude:: ../../src/data/param3b.dat :language: none This example provides a concise representation of parameters that share @@ -270,13 +270,13 @@ Note that this data file specifies the data for set ``A`` twice: defined. An alternate syntax for ``param`` allows the user to concisely specify the definition of an index set along with associated parameters: -.. literalinclude:: ../../tests/data/param3c.dat +.. literalinclude:: ../../src/data/param3c.dat :language: none Finally, we note that default values for missing data can also be specified using the ``default`` keyword: -.. literalinclude:: ../../tests/data/param4.dat +.. literalinclude:: ../../src/data/param4.dat :language: none Note that default values can only be specified in ``param`` commands @@ -290,58 +290,58 @@ Multi-dimensional parameter data is indexed over either multiple sets or a single multi-dimensional set. Suppose that parameter ``B`` is a parameter indexed by set ``A`` that has dimension 2: -.. literalinclude:: ../../tests/data/param5_decl.spy +.. literalinclude:: ../../src/data/param5_decl.spy :language: python The syntax of the ``param`` data command remains essentially the same when specifying values for ``B`` with a list of index and parameter values: -.. literalinclude:: ../../tests/data/param5.dat +.. literalinclude:: ../../src/data/param5.dat :language: none Missing and default values are also handled in the same way with multi-dimensional index sets: -.. literalinclude:: ../../tests/data/param5a.dat +.. literalinclude:: ../../src/data/param5a.dat :language: none Similarly, multiple parameters can defined with a single ``param`` data command. Suppose that parameters ``B``, ``C``, and ``D`` are parameters indexed over set ``A`` that has dimension 2: -.. literalinclude:: ../../tests/data/param6_decl.spy +.. literalinclude:: ../../src/data/param6_decl.spy :language: python These parameters can be defined with a single ``param`` command that declares the parameter names followed by a list of index and parameter values: -.. literalinclude:: ../../tests/data/param6.dat +.. literalinclude:: ../../src/data/param6.dat :language: none Similarly, the following ``param`` data command defines the index set along with the parameters: -.. literalinclude:: ../../tests/data/param6a.dat +.. literalinclude:: ../../src/data/param6a.dat :language: none The ``param`` command also supports a matrix syntax for specifying the values in a parameter that has a 2-dimensional index. Suppose parameter ``B`` is indexed over set ``A`` that has dimension 2: -.. literalinclude:: ../../tests/data/param7a_decl.spy +.. literalinclude:: ../../src/data/param7a_decl.spy :language: python The following ``param`` command defines a matrix of parameter values: -.. literalinclude:: ../../tests/data/param7a.dat +.. literalinclude:: ../../src/data/param7a.dat :language: none Additionally, the following syntax can be used to specify a transposed matrix of parameter values: -.. literalinclude:: ../../tests/data/param7b.dat +.. literalinclude:: ../../src/data/param7b.dat :language: none This functionality facilitates the presentation of parameter data in a @@ -355,13 +355,13 @@ be specified as a series of slices. Each slice is defined by a template followed by a list of index and parameter values. Suppose that parameter ``B`` is indexed over set ``A`` that has dimension 4: -.. literalinclude:: ../../tests/data/param8a_decl.spy +.. literalinclude:: ../../src/data/param8a_decl.spy :language: python The following ``param`` command defines a matrix of parameter values with multiple templates: -.. literalinclude:: ../../tests/data/param8a.dat +.. literalinclude:: ../../src/data/param8a.dat :language: none The ``B`` parameter consists of four values: ``B[a,1,a,1]=10``, @@ -376,7 +376,7 @@ data declaration than is possible with a ``param`` declaration. The following example illustrates a simple ``table`` command that declares data for a single parameter: -.. literalinclude:: ../../tests/data/table0.dat +.. literalinclude:: ../../src/data/table0.dat :language: none The parameter ``M`` is indexed by column ``A``, which must be @@ -385,20 +385,20 @@ are provided after the colon and before the colon-equal (``:=``). Subsequently, the table data is provided. The syntax is not sensitive to whitespace, so the following is an equivalent ``table`` command: -.. literalinclude:: ../../tests/data/table1.dat +.. literalinclude:: ../../src/data/table1.dat :language: none Multiple parameters can be declared by simply including additional parameter names. For example: -.. literalinclude:: ../../tests/data/table2.dat +.. literalinclude:: ../../src/data/table2.dat :language: none This example declares data for the ``M`` and ``N`` parameters, which have different indexing columns. The indexing columns represent set data, which is specified separately. For example: -.. literalinclude:: ../../tests/data/table3.dat +.. literalinclude:: ../../src/data/table3.dat :language: none This example declares data for the ``M`` and ``N`` parameters, along @@ -406,12 +406,12 @@ with the ``A`` and ``Z`` indexing sets. The correspondence between the index set ``Z`` and the indices of parameter ``N`` can be made more explicit by indexing ``N`` by ``Z``: -.. literalinclude:: ../../tests/data/table4.dat +.. literalinclude:: ../../src/data/table4.dat :language: none Set data can also be specified independent of parameter data: -.. literalinclude:: ../../tests/data/table5.dat +.. literalinclude:: ../../src/data/table5.dat :language: none .. warning:: @@ -423,13 +423,13 @@ Set data can also be specified independent of parameter data: that is initialized. For example, the ``table`` command initializes a set ``Z`` and a parameter ``M`` that are not related: - .. literalinclude:: ../../tests/data/table7.dat + .. literalinclude:: ../../src/data/table7.dat :language: none Finally, simple parameter values can also be specified with a ``table`` command: -.. literalinclude:: ../../tests/data/table6.dat +.. literalinclude:: ../../src/data/table6.dat :language: none The previous examples considered examples of the ``table`` command where @@ -437,7 +437,7 @@ column labels are provided. The ``table`` command can also be used without column labels. For example, the first example can be revised to omit column labels as follows: -.. literalinclude:: ../../tests/data/table0.ul.dat +.. literalinclude:: ../../src/data/table0.ul.dat :language: none The ``columns=4`` is a keyword-value pair that defines the number of @@ -450,12 +450,12 @@ braces syntax declares the column where the ``M`` data is provided. Similarly, set data can be declared referencing the integer column labels: -.. literalinclude:: ../../tests/data/table3.ul.dat +.. literalinclude:: ../../src/data/table3.ul.dat :language: none Declared set names can also be used to index parameters: -.. literalinclude:: ../../tests/data/table4.ul.dat +.. literalinclude:: ../../src/data/table4.ul.dat :language: none Finally, we compare and contrast the ``table`` and ``param`` commands. @@ -521,13 +521,13 @@ Simple Load Examples The simplest illustration of the ``load`` command is specifying data for an indexed parameter. Consider the file ``Y.tab``: -.. literalinclude:: ../../tests/data/Y.tab +.. literalinclude:: ../../src/data/Y.tab :language: none This file specifies the values of parameter ``Y`` which is indexed by set ``A``. The following ``load`` command loads the parameter data: -.. literalinclude:: ../../tests/data/import1.tab.dat +.. literalinclude:: ../../src/data/import1.tab.dat :language: none The first argument is the filename. The options after the colon @@ -538,7 +538,7 @@ indicates the parameter that is initialized. Similarly, the following load command loads both the parameter data as well as the index set ``A``: -.. literalinclude:: ../../tests/data/import2.tab.dat +.. literalinclude:: ../../src/data/import2.tab.dat :language: none The difference is the specification of the index set, ``A=[A]``, which @@ -548,24 +548,24 @@ ASCII table file. Set data can also be loaded from a ASCII table file that contains a single column of data: -.. literalinclude:: ../../tests/data/A.tab +.. literalinclude:: ../../src/data/A.tab :language: none The ``format`` option must be specified to denote the fact that the relational data is being interpreted as a set: -.. literalinclude:: ../../tests/data/import3.tab.dat +.. literalinclude:: ../../src/data/import3.tab.dat :language: none Note that this allows for specifying set data that contains tuples. Consider file ``C.tab``: -.. literalinclude:: ../../tests/data/C.tab +.. literalinclude:: ../../src/data/C.tab :language: none A similar ``load`` syntax will load this data into set ``C``: -.. literalinclude:: ../../tests/data/import4.tab.dat +.. literalinclude:: ../../src/data/import4.tab.dat :language: none Note that this example requires that ``C`` be declared with dimension @@ -609,7 +609,7 @@ describes different specifications and how they define how data is loaded into a model. Suppose file ``ABCD.tab`` defines the following relational table: -.. literalinclude:: ../../tests/data/ABCD.tab +.. literalinclude:: ../../src/data/ABCD.tab :language: none There are many ways to interpret this relational table. It could @@ -621,7 +621,7 @@ for specifying how a table is interpreted. A simple specification is to interpret the relational table as a set: -.. literalinclude:: ../../tests/data/ABCD1.dat +.. literalinclude:: ../../src/data/ABCD1.dat :language: none Note that ``Z`` is a set in the model that the data is being loaded @@ -631,7 +631,7 @@ data from this table. Another simple specification is to interpret the relational table as a parameter with indexed by 3-tuples: -.. literalinclude:: ../../tests/data/ABCD2.dat +.. literalinclude:: ../../src/data/ABCD2.dat :language: none Again, this requires that ``D`` be a parameter in the model that the @@ -639,14 +639,14 @@ data is being loaded into. Additionally, the index set for ``D`` must contain the indices that are specified in the table. The ``load`` command also allows for the specification of the index set: -.. literalinclude:: ../../tests/data/ABCD3.dat +.. literalinclude:: ../../src/data/ABCD3.dat :language: none This specifies that the index set is loaded into the ``Z`` set in the model. Similarly, data can be loaded into another parameter than what is specified in the relational table: -.. literalinclude:: ../../tests/data/ABCD4.dat +.. literalinclude:: ../../src/data/ABCD4.dat :language: none This specifies that the index set is loaded into the ``Z`` set and that @@ -658,13 +658,13 @@ specification of data mappings from columns in a relational table into index sets and parameters. For example, suppose that a model is defined with set ``Z`` and parameters ``Y`` and ``W``: -.. literalinclude:: ../../tests/data/ABCD5_decl.spy +.. literalinclude:: ../../src/data/ABCD5_decl.spy :language: python Then the following command defines how these data items are loaded using columns ``B``, ``C`` and ``D``: -.. literalinclude:: ../../tests/data/ABCD5.dat +.. literalinclude:: ../../src/data/ABCD5.dat :language: none When the ``using`` option is omitted the data manager is inferred from @@ -672,13 +672,13 @@ the filename suffix. However, the filename suffix does not always reflect the format of the data it contains. For example, consider the relational table in the file ``ABCD.txt``: -.. literalinclude:: ../../tests/data/ABCD.txt +.. literalinclude:: ../../src/data/ABCD.txt :language: none We can specify the ``using`` option to load from this file into parameter ``D`` and set ``Z``: -.. literalinclude:: ../../tests/data/ABCD6.dat +.. literalinclude:: ../../src/data/ABCD6.dat :language: none .. note:: @@ -692,7 +692,7 @@ parameter ``D`` and set ``Z``: The following data managers are supported in Pyomo 5.1: - .. literalinclude:: ../../tests/data/data_managers.txt + .. literalinclude:: ../../src/data/data_managers.txt :language: none Interpreting Tabular Data @@ -725,12 +725,12 @@ A table with a single value can be interpreted as a simple parameter using the ``param`` format value. Suppose that ``Z.tab`` contains the following table: -.. literalinclude:: ../../tests/data/Z.tab +.. literalinclude:: ../../src/data/Z.tab :language: none The following load command then loads this value into parameter ``p``: -.. literalinclude:: ../../tests/data/import6.tab.dat +.. literalinclude:: ../../src/data/import6.tab.dat :language: none Sets with 2-tuple data can be represented with a matrix format that @@ -739,12 +739,12 @@ relational table as a matrix that defines a set of 2-tuples where ``+`` denotes a valid tuple and ``-`` denotes an invalid tuple. Suppose that ``D.tab`` contains the following relational table: -.. literalinclude:: ../../tests/data/D.tab +.. literalinclude:: ../../src/data/D.tab :language: none Then the following load command loads data into set ``B``: -.. literalinclude:: ../../tests/data/import5.tab.dat +.. literalinclude:: ../../src/data/import5.tab.dat :language: none This command declares the following 2-tuples: ``('A1',1)``, @@ -754,19 +754,19 @@ Parameters with 2-tuple indices can be interpreted with a matrix format that where rows and columns are different indices. Suppose that ``U.tab`` contains the following table: -.. literalinclude:: ../../tests/data/U.tab +.. literalinclude:: ../../src/data/U.tab :language: none Then the following load command loads this value into parameter ``U`` with a 2-dimensional index using the ``array`` format value.: -.. literalinclude:: ../../tests/data/import7.tab.dat +.. literalinclude:: ../../src/data/import7.tab.dat :language: none The ``transpose_array`` format value also interprets the table as a matrix, but it loads the data in a transposed format: -.. literalinclude:: ../../tests/data/import8.tab.dat +.. literalinclude:: ../../src/data/import8.tab.dat :language: none Note that these format values do not support the initialization of the @@ -789,7 +789,7 @@ in the following figure: The following command loads this data to initialize parameter ``D`` and index ``Z``: -.. literalinclude:: ../../tests/data/ABCD7.dat +.. literalinclude:: ../../src/data/ABCD7.dat :language: none Thus, the syntax for loading data from spreadsheets only differs from @@ -809,7 +809,7 @@ command loads data from the Excel spreadsheet ``ABCD.xls`` using the ``pyodbc`` interface. The command loads this data to initialize parameter ``D`` and index ``Z``: -.. literalinclude:: ../../tests/data/ABCD8.dat +.. literalinclude:: ../../src/data/ABCD8.dat :language: none The ``using`` option specifies that the ``pyodbc`` package will be @@ -818,7 +818,7 @@ specifies that the table ``ABCD`` is loaded from this spreadsheet. Similarly, the following command specifies a data connection string to specify the ODBC driver explicitly: -.. literalinclude:: ../../tests/data/ABCD9.dat +.. literalinclude:: ../../src/data/ABCD9.dat :language: none ODBC drivers are generally tailored to the type of data source that @@ -836,7 +836,7 @@ task of minimizing the cost for a meal at a fast food restaurant -- they must purchase a sandwich, side, and a drink for the lowest cost. The following is a Pyomo model for this problem: -.. literalinclude:: ../../tests/data/diet1.py +.. literalinclude:: ../../src/data/diet1.py :language: python Suppose that the file ``diet1.sqlite`` be a SQLite database file that @@ -884,7 +884,7 @@ We can solve the ``diet1`` model using the Python definition in ``diet.sqlite.dat`` specifies a ``load`` command that uses that ``sqlite3`` data manager and embeds a SQL query to retrieve the data: -.. literalinclude:: ../../tests/data/diet.sqlite.dat +.. literalinclude:: ../../src/data/diet.sqlite.dat :language: none The PyODBC driver module will pass the SQL query through an Access ODBC @@ -904,7 +904,7 @@ The ``include`` command allows a data command file to execute data commands from another file. For example, the following command file executes data commands from ``ex1.dat`` and then ``ex2.dat``: -.. literalinclude:: ../../tests/data/ex.dat +.. literalinclude:: ../../src/data/ex.dat :language: none Pyomo is sensitive to the order of execution of data commands, since @@ -921,7 +921,7 @@ to structure the specification of Pyomo's data commands. Specifically, a namespace declaration is used to group data commands and to provide a group label. Consider the following data command file: -.. literalinclude:: ../../tests/data/namespace1.dat +.. literalinclude:: ../../src/data/namespace1.dat :language: none This data file defines two namespaces: ``ns1`` and ``ns2`` that diff --git a/doc/OnlineDocs/working_abstractmodels/data/native.rst b/doc/OnlineDocs/working_abstractmodels/data/native.rst index 2a52c8356aa..ed92d78d78e 100644 --- a/doc/OnlineDocs/working_abstractmodels/data/native.rst +++ b/doc/OnlineDocs/working_abstractmodels/data/native.rst @@ -34,29 +34,29 @@ can be initialized with: * list, set and tuple data: - .. literalinclude:: ../../tests/dataportal/set_initialization_decl2.spy + .. literalinclude:: ../../src/dataportal/set_initialization_decl2.spy :language: python * generators: - .. literalinclude:: ../../tests/dataportal/set_initialization_decl3.spy + .. literalinclude:: ../../src/dataportal/set_initialization_decl3.spy :language: python * numpy arrays: - .. literalinclude:: ../../tests/dataportal/set_initialization_decl4.spy + .. literalinclude:: ../../src/dataportal/set_initialization_decl4.spy :language: python Sets can also be indirectly initialized with functions that return native Python data: -.. literalinclude:: ../../tests/dataportal/set_initialization_decl5.spy +.. literalinclude:: ../../src/dataportal/set_initialization_decl5.spy :language: python Indexed sets can be initialized with dictionary data where the dictionary values are iterable data: -.. literalinclude:: ../../tests/dataportal/set_initialization_decl6.spy +.. literalinclude:: ../../src/dataportal/set_initialization_decl6.spy :language: python @@ -66,19 +66,19 @@ Parameter Components When a parameter is a single value, then a :class:`~pyomo.environ.Param` component can be simply initialized with a value: -.. literalinclude:: ../../tests/dataportal/param_initialization_decl1.spy +.. literalinclude:: ../../src/dataportal/param_initialization_decl1.spy :language: python More generally, :class:`~pyomo.environ.Param` components can be initialized with dictionary data where the dictionary values are single values: -.. literalinclude:: ../../tests/dataportal/param_initialization_decl2.spy +.. literalinclude:: ../../src/dataportal/param_initialization_decl2.spy :language: python Parameters can also be indirectly initialized with functions that return native Python data: -.. literalinclude:: ../../tests/dataportal/param_initialization_decl3.spy +.. literalinclude:: ../../src/dataportal/param_initialization_decl3.spy :language: python diff --git a/doc/OnlineDocs/working_abstractmodels/data/raw_dicts.rst b/doc/OnlineDocs/working_abstractmodels/data/raw_dicts.rst index e10042b3ceb..f78e349c28b 100644 --- a/doc/OnlineDocs/working_abstractmodels/data/raw_dicts.rst +++ b/doc/OnlineDocs/working_abstractmodels/data/raw_dicts.rst @@ -28,13 +28,10 @@ components, the required data dictionary maps the implicit index ... }} >>> i = m.create_instance(data) >>> i.pprint() - 2 Set Declarations + 1 Set Declarations I : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {1, 2, 3} - r_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : I*I : 9 : {(1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3), (3, 1), (3, 2), (3, 3)} 3 Param Declarations p : Size=1, Index=None, Domain=Any, Default=None, Mutable=False @@ -45,12 +42,12 @@ components, the required data dictionary maps the implicit index 1 : 10 2 : 20 3 : 30 - r : Size=9, Index=r_index, Domain=Any, Default=0, Mutable=False + r : Size=9, Index=I*I, Domain=Any, Default=0, Mutable=False Key : Value (1, 1) : 110 (1, 2) : 120 (2, 3) : 230 - 5 Declarations: I p q r_index r + 4 Declarations: I p q r diff --git a/doc/OnlineDocs/working_abstractmodels/pyomo_command.rst b/doc/OnlineDocs/working_abstractmodels/pyomo_command.rst index 1d22798d8ce..aabfc8667f7 100644 --- a/doc/OnlineDocs/working_abstractmodels/pyomo_command.rst +++ b/doc/OnlineDocs/working_abstractmodels/pyomo_command.rst @@ -90,7 +90,7 @@ When there seem to be troubles expressing the model, it is often useful to embed print commands in the model in places that will yield helpful information. Consider the following snippet: -.. literalinclude:: ../tests/scripting/spy4PyomoCommand_Troubleshooting_printed_command.spy +.. literalinclude:: ../src/scripting/spy4PyomoCommand_Troubleshooting_printed_command.spy :language: python The effect will be to output every member of the set ``model.I`` at the diff --git a/doc/OnlineDocs/working_models.rst b/doc/OnlineDocs/working_models.rst index 2b9b664c548..dbd7aa383e3 100644 --- a/doc/OnlineDocs/working_models.rst +++ b/doc/OnlineDocs/working_models.rst @@ -58,7 +58,7 @@ computer to solve the problem or even to iterate over solutions. This example is provided just to illustrate some elementary aspects of scripting. -.. literalinclude:: tests/scripting/iterative1.spy +.. literalinclude:: src/scripting/iterative1.spy :language: python Let us now analyze this script. The first line is a comment that happens @@ -66,7 +66,7 @@ to give the name of the file. This is followed by two lines that import symbols for Pyomo. The pyomo namespace is imported as ``pyo``. Therefore, ``pyo.`` must precede each use of a Pyomo name. -.. literalinclude:: tests/scripting/iterative1_Import_symbols_for_pyomo.spy +.. literalinclude:: src/scripting/iterative1_Import_symbols_for_pyomo.spy :language: python An object to perform optimization is created by calling @@ -74,7 +74,7 @@ An object to perform optimization is created by calling argument would be ``'gurobi'`` if, e.g., Gurobi was desired instead of glpk: -.. literalinclude:: tests/scripting/iterative1_Call_SolverFactory_with_argument.spy +.. literalinclude:: src/scripting/iterative1_Call_SolverFactory_with_argument.spy :language: python The next lines after a comment create a model. For our discussion here, @@ -86,13 +86,13 @@ to keep it simple. Constraints could be present in the base model. Even though it is an abstract model, the base model is fully specified by these commands because it requires no external data: -.. literalinclude:: tests/scripting/iterative1_Create_base_model.spy +.. literalinclude:: src/scripting/iterative1_Create_base_model.spy :language: python The next line is not part of the base model specification. It creates an empty constraint list that the script will use to add constraints. -.. literalinclude:: tests/scripting/iterative1_Create_empty_constraint_list.spy +.. literalinclude:: src/scripting/iterative1_Create_empty_constraint_list.spy :language: python The next non-comment line creates the instantiated model and refers to @@ -103,19 +103,19 @@ the ``create`` function is called without arguments because none are needed; however, the name of a file with data commands is given as an argument in many scripts. -.. literalinclude:: tests/scripting/iterative1_Create_instantiated_model.spy +.. literalinclude:: src/scripting/iterative1_Create_instantiated_model.spy :language: python The next line invokes the solver and refers to the object contain results with the Python variable ``results``. -.. literalinclude:: tests/scripting/iterative1_Solve_and_refer_to_results.spy +.. literalinclude:: src/scripting/iterative1_Solve_and_refer_to_results.spy :language: python The solve function loads the results into the instance, so the next line writes out the updated values. -.. literalinclude:: tests/scripting/iterative1_Display_updated_value.spy +.. literalinclude:: src/scripting/iterative1_Display_updated_value.spy :language: python The next non-comment line is a Python iteration command that will @@ -123,7 +123,7 @@ successively assign the integers from 0 to 4 to the Python variable ``i``, although that variable is not used in script. This loop is what causes the script to generate five more solutions: -.. literalinclude:: tests/scripting/iterative1_Assign_integers.spy +.. literalinclude:: src/scripting/iterative1_Assign_integers.spy :language: python An expression is built up in the Python variable named ``expr``. The @@ -135,7 +135,7 @@ zero and the expression in ``expr`` is augmented accordingly. Although Pyomo expression when it is assigned expressions involving Pyomo variable objects: -.. literalinclude:: tests/scripting/iterative1_Iteratively_assign_and_test.spy +.. literalinclude:: src/scripting/iterative1_Iteratively_assign_and_test.spy :language: python During the first iteration (when ``i`` is 0), we know that all values of @@ -159,7 +159,7 @@ function to get it. The next line adds to the constraint list called ``c`` the requirement that the expression be greater than or equal to one: -.. literalinclude:: tests/scripting/iterative1_Add_expression_constraint.spy +.. literalinclude:: src/scripting/iterative1_Add_expression_constraint.spy :language: python The proof that this precludes the last solution is left as an exerise @@ -167,7 +167,7 @@ for the reader. The final lines in the outer for loop find a solution and display it: -.. literalinclude:: tests/scripting/iterative1_Find_and_display_solution.spy +.. literalinclude:: src/scripting/iterative1_Find_and_display_solution.spy :language: python .. note:: @@ -268,14 +268,14 @@ Fixing Variables and Re-solving Instead of changing model data, scripts are often used to fix variable values. The following example illustrates this. -.. literalinclude:: tests/scripting/iterative2.spy +.. literalinclude:: src/scripting/iterative2.spy :language: python In this example, the variables are binary. The model is solved and then the value of ``model.x[2]`` is flipped to the opposite value before solving the model again. The main lines of interest are: -.. literalinclude:: tests/scripting/iterative2_Flip_value_before_solve_again.spy +.. literalinclude:: src/scripting/iterative2_Flip_value_before_solve_again.spy :language: python This could also have been accomplished by setting the upper and lower @@ -430,7 +430,7 @@ Consider the following very simple example, which is similar to the iterative example. This is a concrete model. In this example, the value of ``x[2]`` is accessed. -.. literalinclude:: tests/scripting/noiteration1.py +.. literalinclude:: src/scripting/noiteration1.py :language: python .. note:: @@ -476,7 +476,7 @@ Another way to access all of the variables (particularly if there are blocks) is as follows (this particular snippet assumes that instead of `import pyomo.environ as pyo` `from pyo.environ import *` was used): -.. literalinclude:: tests/scripting/block_iter_example_compprintloop.spy +.. literalinclude:: src/scripting/block_iter_example_compprintloop.spy :language: python .. _ParamAccess: @@ -521,21 +521,21 @@ To signal that duals are desired, declare a Suffix component with the name "dual" on the model or instance with an IMPORT or IMPORT_EXPORT direction. -.. literalinclude:: tests/scripting/driveabs2_Create_dual_suffix_component.spy +.. literalinclude:: src/scripting/driveabs2_Create_dual_suffix_component.spy :language: python See the section on Suffixes :ref:`Suffixes` for more information on Pyomo's Suffix component. After the results are obtained and loaded into an instance, duals can be accessed in the following fashion. -.. literalinclude:: tests/scripting/driveabs2_Access_all_dual.spy +.. literalinclude:: src/scripting/driveabs2_Access_all_dual.spy :language: python The following snippet will only work, of course, if there is a constraint with the name ``AxbConstraint`` that has and index, which is the string ``Film``. -.. literalinclude:: tests/scripting/driveabs2_Access_one_dual.spy +.. literalinclude:: src/scripting/driveabs2_Access_one_dual.spy :language: python Here is a complete example that relies on the file ``abstract2.py`` to @@ -544,14 +544,14 @@ data. Note that the model in ``abstract2.py`` does contain a constraint named ``AxbConstraint`` and ``abstract2.dat`` does specify an index for it named ``Film``. -.. literalinclude:: tests/scripting/driveabs2.spy +.. literalinclude:: src/scripting/driveabs2.spy :language: python Concrete models are slightly different because the model is the instance. Here is a complete example that relies on the file ``concrete1.py`` to provide the model and instantiate it. -.. literalinclude:: tests/scripting/driveconc1.py +.. literalinclude:: src/scripting/driveconc1.py :language: python Accessing Slacks @@ -568,7 +568,7 @@ After a solve, the results object has a member ``Solution.Status`` that contains the solver status. The following snippet shows an example of access via a ``print`` statement: -.. literalinclude:: tests/scripting/spy4scripts_Print_solver_status.spy +.. literalinclude:: src/scripting/spy4scripts_Print_solver_status.spy :language: python The use of the Python ``str`` function to cast the value to a be string @@ -576,12 +576,12 @@ makes it easy to test it. In particular, the value 'optimal' indicates that the solver succeeded. It is also possible to access Pyomo data that can be compared with the solver status as in the following code snippet: -.. literalinclude:: tests/scripting/spy4scripts_Pyomo_data_comparedwith_solver_status_1.spy +.. literalinclude:: src/scripting/spy4scripts_Pyomo_data_comparedwith_solver_status_1.spy :language: python Alternatively, -.. literalinclude:: tests/scripting/spy4scripts_Pyomo_data_comparedwith_solver_status_2.spy +.. literalinclude:: src/scripting/spy4scripts_Pyomo_data_comparedwith_solver_status_2.spy :language: python .. _TeeTrue: @@ -592,7 +592,7 @@ Display of Solver Output To see the output of the solver, use the option ``tee=True`` as in -.. literalinclude:: tests/scripting/spy4scripts_See_solver_output.spy +.. literalinclude:: src/scripting/spy4scripts_See_solver_output.spy :language: python This can be useful for troubleshooting solver difficulties. @@ -607,7 +607,7 @@ solver. In scripts or callbacks, the options can be attached to the solver object by adding to its options dictionary as illustrated by this snippet: -.. literalinclude:: tests/scripting/spy4scripts_Add_option_to_solver.spy +.. literalinclude:: src/scripting/spy4scripts_Add_option_to_solver.spy :language: python If multiple options are needed, then multiple dictionary entries should @@ -616,7 +616,7 @@ be added. Sometimes it is desirable to pass options as part of the call to the solve function as in this snippet: -.. literalinclude:: tests/scripting/spy4scripts_Add_multiple_options_to_solver.spy +.. literalinclude:: src/scripting/spy4scripts_Add_multiple_options_to_solver.spy :language: python The quoted string is passed directly to the solver. If multiple options @@ -644,7 +644,7 @@ situations where they are not, the SolverFactory function accepts the keyword ``executable``, which you can use to set an absolute or relative path to a solver executable. E.g., -.. literalinclude:: tests/scripting/spy4scripts_Set_path_to_solver_executable.spy +.. literalinclude:: src/scripting/spy4scripts_Set_path_to_solver_executable.spy :language: python Warm Starts @@ -654,7 +654,7 @@ Some solvers support a warm start based on current values of variables. To use this feature, set the values of variables in the instance and pass ``warmstart=True`` to the ``solve()`` method. E.g., -.. literalinclude:: tests/scripting/spy4scripts_Pass_warmstart_to_solver.spy +.. literalinclude:: src/scripting/spy4scripts_Pass_warmstart_to_solver.spy :language: python .. note:: @@ -686,7 +686,7 @@ parallel. The example can be run with the following command: mpirun -np 2 python -m mpi4py parallel.py -.. literalinclude:: tests/scripting/parallel.py +.. literalinclude:: src/scripting/parallel.py :language: python @@ -700,5 +700,5 @@ The pyomo command-line ``--tempdir`` option propagates through to the TempFileManager service. One can accomplish the same through the following few lines of code in a script: -.. literalinclude:: tests/scripting/spy4scripts_Specify_temporary_directory_name.spy +.. literalinclude:: src/scripting/spy4scripts_Specify_temporary_directory_name.spy :language: python diff --git a/examples/dae/Heat_Conduction.py b/examples/dae/Heat_Conduction.py index 11f35fddd13..7e11ec59263 100644 --- a/examples/dae/Heat_Conduction.py +++ b/examples/dae/Heat_Conduction.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/Optimal_Control.py b/examples/dae/Optimal_Control.py index ed44d5eeb59..676c95271f2 100644 --- a/examples/dae/Optimal_Control.py +++ b/examples/dae/Optimal_Control.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/PDE_example.py b/examples/dae/PDE_example.py index 6cb7eb4a7fe..0aea173415b 100644 --- a/examples/dae/PDE_example.py +++ b/examples/dae/PDE_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/Parameter_Estimation.py b/examples/dae/Parameter_Estimation.py index 7ee2f112b94..332a21d93dc 100644 --- a/examples/dae/Parameter_Estimation.py +++ b/examples/dae/Parameter_Estimation.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/Path_Constraint.py b/examples/dae/Path_Constraint.py index 866b4b3b90a..69f31980c63 100644 --- a/examples/dae/Path_Constraint.py +++ b/examples/dae/Path_Constraint.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/ReactionKinetics.py b/examples/dae/ReactionKinetics.py index ef760820c4b..fa747cf8b21 100644 --- a/examples/dae/ReactionKinetics.py +++ b/examples/dae/ReactionKinetics.py @@ -2,7 +2,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/car_example.py b/examples/dae/car_example.py index a157159cf6c..b6ca2203860 100644 --- a/examples/dae/car_example.py +++ b/examples/dae/car_example.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # Ampl Car Example # # Shows how to convert a minimize final time optimal control problem diff --git a/examples/dae/disease_DAE.py b/examples/dae/disease_DAE.py index 59e598aa504..bfeb2530fc9 100644 --- a/examples/dae/disease_DAE.py +++ b/examples/dae/disease_DAE.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + ### # SIR disease model using radau collocation ### diff --git a/examples/dae/distill_DAE.py b/examples/dae/distill_DAE.py index cdfd543f9a8..e822cfb1752 100644 --- a/examples/dae/distill_DAE.py +++ b/examples/dae/distill_DAE.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/dynamic_scheduling.py b/examples/dae/dynamic_scheduling.py index 13cabeb5bcf..137307e31a9 100644 --- a/examples/dae/dynamic_scheduling.py +++ b/examples/dae/dynamic_scheduling.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/laplace_BVP.py b/examples/dae/laplace_BVP.py index 6b2e2841575..61f911b3826 100644 --- a/examples/dae/laplace_BVP.py +++ b/examples/dae/laplace_BVP.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/run_Optimal_Control.py b/examples/dae/run_Optimal_Control.py index 2523bd8c607..2e7bc79dff4 100644 --- a/examples/dae/run_Optimal_Control.py +++ b/examples/dae/run_Optimal_Control.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/run_Parameter_Estimation.py b/examples/dae/run_Parameter_Estimation.py index a319000cb59..c9b649df8dd 100644 --- a/examples/dae/run_Parameter_Estimation.py +++ b/examples/dae/run_Parameter_Estimation.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/run_Path_Constraint.py b/examples/dae/run_Path_Constraint.py index 17a576a57d8..996b432a555 100644 --- a/examples/dae/run_Path_Constraint.py +++ b/examples/dae/run_Path_Constraint.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/run_disease.py b/examples/dae/run_disease.py index 139046d434e..5d9595a89d5 100644 --- a/examples/dae/run_disease.py +++ b/examples/dae/run_disease.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * from pyomo.dae import * from disease_DAE import model diff --git a/examples/dae/run_distill.py b/examples/dae/run_distill.py index d9ececf34fc..9b09850f90a 100644 --- a/examples/dae/run_distill.py +++ b/examples/dae/run_distill.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/run_stochpdegas_automatic.py b/examples/dae/run_stochpdegas_automatic.py index dd710588406..6fc9f6d594c 100644 --- a/examples/dae/run_stochpdegas_automatic.py +++ b/examples/dae/run_stochpdegas_automatic.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import time from pyomo.environ import * diff --git a/examples/dae/simulator_dae_example.py b/examples/dae/simulator_dae_example.py index ef6484be6c6..4ea1f9fd5f0 100644 --- a/examples/dae/simulator_dae_example.py +++ b/examples/dae/simulator_dae_example.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # # Batch reactor example from Biegler book on Nonlinear Programming Chapter 9 # diff --git a/examples/dae/simulator_dae_multindex_example.py b/examples/dae/simulator_dae_multindex_example.py index d1a97fec79f..775eb4f8c79 100644 --- a/examples/dae/simulator_dae_multindex_example.py +++ b/examples/dae/simulator_dae_multindex_example.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # # Batch reactor example from Biegler book on Nonlinear Programming Chapter 9 # diff --git a/examples/dae/simulator_ode_example.py b/examples/dae/simulator_ode_example.py index bf600cf163e..f6f28b87d07 100644 --- a/examples/dae/simulator_ode_example.py +++ b/examples/dae/simulator_ode_example.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # # Example from Scipy odeint examples # diff --git a/examples/dae/simulator_ode_multindex_example.py b/examples/dae/simulator_ode_multindex_example.py index fa2623f4cc2..b1b9111084b 100644 --- a/examples/dae/simulator_ode_multindex_example.py +++ b/examples/dae/simulator_ode_multindex_example.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # # Example from Scipy odeint examples # diff --git a/examples/dae/stochpdegas_automatic.py b/examples/dae/stochpdegas_automatic.py index 3cd5c34f011..397b4a18100 100644 --- a/examples/dae/stochpdegas_automatic.py +++ b/examples/dae/stochpdegas_automatic.py @@ -1,7 +1,18 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # stochastic pde model for natural gas network # victor m. zavala / 2013 -# from __future__ import division +# from pyomo.environ import * from pyomo.dae import * diff --git a/examples/doc/samples/__init__.py b/examples/doc/samples/__init__.py index 3115f06ef53..0110902b288 100644 --- a/examples/doc/samples/__init__.py +++ b/examples/doc/samples/__init__.py @@ -1 +1,12 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # Dummy file for pytest diff --git a/examples/doc/samples/case_studies/deer/DeerProblem.py b/examples/doc/samples/case_studies/deer/DeerProblem.py index 0b6b7252aaa..d09c9b53887 100644 --- a/examples/doc/samples/case_studies/deer/DeerProblem.py +++ b/examples/doc/samples/case_studies/deer/DeerProblem.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core import * # diff --git a/examples/doc/samples/case_studies/diet/DietProblem.py b/examples/doc/samples/case_studies/diet/DietProblem.py index f070201c28e..64624310943 100644 --- a/examples/doc/samples/case_studies/diet/DietProblem.py +++ b/examples/doc/samples/case_studies/diet/DietProblem.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core import * model = AbstractModel() diff --git a/examples/doc/samples/case_studies/disease_est/DiseaseEstimation.py b/examples/doc/samples/case_studies/disease_est/DiseaseEstimation.py index c685a6ee67f..6a0edb38350 100644 --- a/examples/doc/samples/case_studies/disease_est/DiseaseEstimation.py +++ b/examples/doc/samples/case_studies/disease_est/DiseaseEstimation.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core import * model = AbstractModel() diff --git a/examples/doc/samples/case_studies/max_flow/MaxFlow.py b/examples/doc/samples/case_studies/max_flow/MaxFlow.py index c6eb42ccf7d..1e75fa4e79d 100644 --- a/examples/doc/samples/case_studies/max_flow/MaxFlow.py +++ b/examples/doc/samples/case_studies/max_flow/MaxFlow.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core import * model = AbstractModel() diff --git a/examples/doc/samples/case_studies/network_flow/networkFlow1.py b/examples/doc/samples/case_studies/network_flow/networkFlow1.py index adfaab4476b..eb8c8e48a1a 100644 --- a/examples/doc/samples/case_studies/network_flow/networkFlow1.py +++ b/examples/doc/samples/case_studies/network_flow/networkFlow1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core import * model = AbstractModel() diff --git a/examples/doc/samples/case_studies/rosen/Rosenbrock.py b/examples/doc/samples/case_studies/rosen/Rosenbrock.py index 9677cea95dd..51e7d51b57d 100644 --- a/examples/doc/samples/case_studies/rosen/Rosenbrock.py +++ b/examples/doc/samples/case_studies/rosen/Rosenbrock.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # @intro: from pyomo.core import * diff --git a/examples/doc/samples/case_studies/transportation/transportation.py b/examples/doc/samples/case_studies/transportation/transportation.py index 26fcb5f0b66..588ae764953 100644 --- a/examples/doc/samples/case_studies/transportation/transportation.py +++ b/examples/doc/samples/case_studies/transportation/transportation.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core import * model = AbstractModel() diff --git a/examples/doc/samples/comparisons/cutstock/cutstock_cplex.py b/examples/doc/samples/comparisons/cutstock/cutstock_cplex.py index 796c39810f8..f49c5b591ae 100644 --- a/examples/doc/samples/comparisons/cutstock/cutstock_cplex.py +++ b/examples/doc/samples/comparisons/cutstock/cutstock_cplex.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import cplex from cutstock_util import * from cplex.exceptions import CplexSolverError diff --git a/examples/doc/samples/comparisons/cutstock/cutstock_grb.py b/examples/doc/samples/comparisons/cutstock/cutstock_grb.py index 4fa4556fc96..483d84b02e6 100644 --- a/examples/doc/samples/comparisons/cutstock/cutstock_grb.py +++ b/examples/doc/samples/comparisons/cutstock/cutstock_grb.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from gurobipy import * from cutstock_util import * diff --git a/examples/doc/samples/comparisons/cutstock/cutstock_lpsolve.py b/examples/doc/samples/comparisons/cutstock/cutstock_lpsolve.py index 658ee006c30..9a6c8301e8f 100644 --- a/examples/doc/samples/comparisons/cutstock/cutstock_lpsolve.py +++ b/examples/doc/samples/comparisons/cutstock/cutstock_lpsolve.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from lpsolve55 import * from cutstock_util import * diff --git a/examples/doc/samples/comparisons/cutstock/cutstock_pulpor.py b/examples/doc/samples/comparisons/cutstock/cutstock_pulpor.py index 2f2506ba3d6..d14b0fe46c1 100644 --- a/examples/doc/samples/comparisons/cutstock/cutstock_pulpor.py +++ b/examples/doc/samples/comparisons/cutstock/cutstock_pulpor.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pulp import * from cutstock_util import * diff --git a/examples/doc/samples/comparisons/cutstock/cutstock_pyomo.py b/examples/doc/samples/comparisons/cutstock/cutstock_pyomo.py index a67ebdd0675..48d7e6b26fd 100644 --- a/examples/doc/samples/comparisons/cutstock/cutstock_pyomo.py +++ b/examples/doc/samples/comparisons/cutstock/cutstock_pyomo.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core import * import pyomo.opt from cutstock_util import * diff --git a/examples/doc/samples/comparisons/cutstock/cutstock_util.py b/examples/doc/samples/comparisons/cutstock/cutstock_util.py index 1cd8c61922f..da5349ec06c 100644 --- a/examples/doc/samples/comparisons/cutstock/cutstock_util.py +++ b/examples/doc/samples/comparisons/cutstock/cutstock_util.py @@ -1,3 +1,15 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + def getCutCount(): cutCount = 0 fout1 = open('WidthDemand.csv', 'r') diff --git a/examples/doc/samples/comparisons/sched/pyomo/sched.py b/examples/doc/samples/comparisons/sched/pyomo/sched.py index 627bc083fbe..cf781713641 100644 --- a/examples/doc/samples/comparisons/sched/pyomo/sched.py +++ b/examples/doc/samples/comparisons/sched/pyomo/sched.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core import * model = AbstractModel() diff --git a/examples/doc/samples/scripts/__init__.py b/examples/doc/samples/scripts/__init__.py index 3115f06ef53..0110902b288 100644 --- a/examples/doc/samples/scripts/__init__.py +++ b/examples/doc/samples/scripts/__init__.py @@ -1 +1,12 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # Dummy file for pytest diff --git a/examples/doc/samples/scripts/s1/knapsack.py b/examples/doc/samples/scripts/s1/knapsack.py index 642e0faaaed..cee3937b668 100644 --- a/examples/doc/samples/scripts/s1/knapsack.py +++ b/examples/doc/samples/scripts/s1/knapsack.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core import * diff --git a/examples/doc/samples/scripts/s1/script.py b/examples/doc/samples/scripts/s1/script.py index 02b6b406922..4ddaea45e19 100644 --- a/examples/doc/samples/scripts/s1/script.py +++ b/examples/doc/samples/scripts/s1/script.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core import * import pyomo.opt import pyomo.environ diff --git a/examples/doc/samples/scripts/s2/knapsack.py b/examples/doc/samples/scripts/s2/knapsack.py index a7d693f5d35..3131cee7bc5 100644 --- a/examples/doc/samples/scripts/s2/knapsack.py +++ b/examples/doc/samples/scripts/s2/knapsack.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core import * diff --git a/examples/doc/samples/scripts/s2/script.py b/examples/doc/samples/scripts/s2/script.py index 88de1dec680..fe97d6ab8fd 100644 --- a/examples/doc/samples/scripts/s2/script.py +++ b/examples/doc/samples/scripts/s2/script.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core import * import pyomo.opt import pyomo.environ diff --git a/examples/doc/samples/scripts/test_scripts.py b/examples/doc/samples/scripts/test_scripts.py index ca0c8a7cc4e..691a44aea2d 100644 --- a/examples/doc/samples/scripts/test_scripts.py +++ b/examples/doc/samples/scripts/test_scripts.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/doc/samples/update.py b/examples/doc/samples/update.py index 9eae2f4b694..8789413303c 100644 --- a/examples/doc/samples/update.py +++ b/examples/doc/samples/update.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + #!/usr/bin/env python # # This is a Python script that regenerates the top-level TRAC.txt file, which diff --git a/examples/gdp/batchProcessing.py b/examples/gdp/batchProcessing.py index f0980dd5034..9810f5d63f1 100644 --- a/examples/gdp/batchProcessing.py +++ b/examples/gdp/batchProcessing.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * from pyomo.gdp import * diff --git a/examples/gdp/circles/circles.py b/examples/gdp/circles/circles.py index ae905998403..a8b7a156fad 100644 --- a/examples/gdp/circles/circles.py +++ b/examples/gdp/circles/circles.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ The "circles" GDP example problem originating in Lee and Grossman (2000). The goal is to choose a point to minimize a convex quadratic function over a set of diff --git a/examples/gdp/constrained_layout/cons_layout_model.py b/examples/gdp/constrained_layout/cons_layout_model.py index 10595db4c22..d38fd0cc66b 100644 --- a/examples/gdp/constrained_layout/cons_layout_model.py +++ b/examples/gdp/constrained_layout/cons_layout_model.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """2-D constrained layout example. Example based on: https://www.minlp.org/library/problem/index.php?i=107&lib=GDP @@ -9,7 +20,6 @@ with each other. """ -from __future__ import division from pyomo.environ import ConcreteModel, Objective, Param, RangeSet, Set, Var, value diff --git a/examples/gdp/disease_model.py b/examples/gdp/disease_model.py index bc3e69600ec..498337e35e6 100644 --- a/examples/gdp/disease_model.py +++ b/examples/gdp/disease_model.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/gdp/eight_process/eight_proc_logical.py b/examples/gdp/eight_process/eight_proc_logical.py index 7e183dfc397..4496427d421 100644 --- a/examples/gdp/eight_process/eight_proc_logical.py +++ b/examples/gdp/eight_process/eight_proc_logical.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Disjunctive re-implementation of eight-process problem. Re-implementation of Duran example 3 superstructure synthesis problem in Pyomo @@ -22,7 +33,6 @@ http://dx.doi.org/10.1016/0098-1354(95)00219-7 """ -from __future__ import division from pyomo.core.expr.logical_expr import land, lor from pyomo.core.plugins.transform.logical_to_linear import ( diff --git a/examples/gdp/eight_process/eight_proc_model.py b/examples/gdp/eight_process/eight_proc_model.py index d4bd4dbd102..41bb6d462f1 100644 --- a/examples/gdp/eight_process/eight_proc_model.py +++ b/examples/gdp/eight_process/eight_proc_model.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Disjunctive re-implementation of eight-process problem. Re-implementation of Duran example 3 superstructure synthesis problem in Pyomo @@ -22,7 +33,6 @@ http://dx.doi.org/10.1016/0098-1354(95)00219-7 """ -from __future__ import division from pyomo.environ import ( ConcreteModel, diff --git a/examples/gdp/eight_process/eight_proc_verbose_model.py b/examples/gdp/eight_process/eight_proc_verbose_model.py index 78da347e564..1fd68909146 100644 --- a/examples/gdp/eight_process/eight_proc_verbose_model.py +++ b/examples/gdp/eight_process/eight_proc_verbose_model.py @@ -1,10 +1,20 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Disjunctive re-implementation of eight-process problem. This is the more verbose formulation of the same problem given in eight_proc_model.py. """ -from __future__ import division from pyomo.environ import ( ConcreteModel, diff --git a/examples/gdp/farm_layout/farm_layout.py b/examples/gdp/farm_layout/farm_layout.py index 411e2de3242..1b232b9cfa6 100644 --- a/examples/gdp/farm_layout/farm_layout.py +++ b/examples/gdp/farm_layout/farm_layout.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ Farm layout example from Sawaya (2006). The goal is to determine optimal placements and dimensions for farm plots of specified areas to minimize the perimeter of a minimal enclosing fence. This is a GDP problem with diff --git a/examples/gdp/jobshop-nodisjuncts.py b/examples/gdp/jobshop-nodisjuncts.py index bc656dc4717..0cd5b5ab274 100644 --- a/examples/gdp/jobshop-nodisjuncts.py +++ b/examples/gdp/jobshop-nodisjuncts.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/gdp/jobshop.py b/examples/gdp/jobshop.py index 619ece47e72..7119ee7655c 100644 --- a/examples/gdp/jobshop.py +++ b/examples/gdp/jobshop.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/gdp/medTermPurchasing_Literal.py b/examples/gdp/medTermPurchasing_Literal.py index c9b27920396..b6d16c216fe 100755 --- a/examples/gdp/medTermPurchasing_Literal.py +++ b/examples/gdp/medTermPurchasing_Literal.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * from pyomo.gdp import * diff --git a/examples/gdp/nine_process/small_process.py b/examples/gdp/nine_process/small_process.py index 7f96f32c65c..2abffef1af6 100644 --- a/examples/gdp/nine_process/small_process.py +++ b/examples/gdp/nine_process/small_process.py @@ -1,6 +1,18 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Small process synthesis-inspired toy GDP example. """ + from pyomo.core import ConcreteModel, RangeSet, Var, Constraint, Objective from pyomo.core.expr.current import exp, log, sqrt from pyomo.gdp import Disjunction diff --git a/examples/gdp/simple1.py b/examples/gdp/simple1.py index f7c77b111f0..de41c0bfd00 100644 --- a/examples/gdp/simple1.py +++ b/examples/gdp/simple1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # Example: modeling a complementarity condition as a # disjunction # diff --git a/examples/gdp/simple2.py b/examples/gdp/simple2.py index 6bcc7bbf747..b066d705036 100644 --- a/examples/gdp/simple2.py +++ b/examples/gdp/simple2.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # Example: modeling a complementarity condition as a # disjunction # diff --git a/examples/gdp/simple3.py b/examples/gdp/simple3.py index 6b3d6ec46c4..890daf8882b 100644 --- a/examples/gdp/simple3.py +++ b/examples/gdp/simple3.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # Example: modeling a complementarity condition as a # disjunction # diff --git a/examples/gdp/small_lit/basic_step.py b/examples/gdp/small_lit/basic_step.py index 16d134500e7..2d9da97167c 100644 --- a/examples/gdp/small_lit/basic_step.py +++ b/examples/gdp/small_lit/basic_step.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ Example from Section 3.2 in paper of Pseudo Basic Steps Ref: @@ -9,6 +20,7 @@ Pyomo model implementation by @RomeoV """ + from pyomo.environ import * from pyomo.gdp import * from pyomo.gdp.basic_step import apply_basic_step diff --git a/examples/gdp/small_lit/contracts_problem.py b/examples/gdp/small_lit/contracts_problem.py index 500fe15cb2a..0c59d2264ee 100644 --- a/examples/gdp/small_lit/contracts_problem.py +++ b/examples/gdp/small_lit/contracts_problem.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ Example from 'Lagrangean Relaxation of the Hull-Reformulation of Linear \ Generalized Disjunctive Programs and its use in Disjunctive Branch \ and Bound' Page 25 f. diff --git a/examples/gdp/small_lit/ex1_Lee.py b/examples/gdp/small_lit/ex1_Lee.py index ddd2e1c3d2f..abbf470a1c3 100644 --- a/examples/gdp/small_lit/ex1_Lee.py +++ b/examples/gdp/small_lit/ex1_Lee.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Simple example of nonlinear problem modeled with GDP framework. Taken from Example 1 of the paper "New Algorithms for Nonlinear Generalized Disjunctive Programming" by Lee and Grossmann diff --git a/examples/gdp/small_lit/ex_633_trespalacios.py b/examples/gdp/small_lit/ex_633_trespalacios.py index b281e009d1f..b0c5fbd85ac 100644 --- a/examples/gdp/small_lit/ex_633_trespalacios.py +++ b/examples/gdp/small_lit/ex_633_trespalacios.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Analytical example from Section 6.3.3 of F. Trespalacions Ph.D. Thesis (2015) Analytical example for a nonconvex GDP with 2 disjunctions, each with 2 disjuncts. @@ -14,7 +25,6 @@ Pyomo model implementation by @bernalde and @qtothec. """ -from __future__ import division from pyomo.environ import * from pyomo.gdp import * diff --git a/examples/gdp/small_lit/nonconvex_HEN.py b/examples/gdp/small_lit/nonconvex_HEN.py index 61c24c3187a..05fad970b84 100644 --- a/examples/gdp/small_lit/nonconvex_HEN.py +++ b/examples/gdp/small_lit/nonconvex_HEN.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ Example from 'Systematic Modeling of Discrete-Continuous Optimization \ Models through Generalized Disjunctive Programming' Ignacio E. Grossmann and Francisco Trespalacios, 2013 @@ -7,7 +18,6 @@ Pyomo model implementation by @RomeoV """ - from pyomo.environ import ( ConcreteModel, Constraint, diff --git a/examples/gdp/stickies.py b/examples/gdp/stickies.py index 75beb911415..73b537ff13d 100644 --- a/examples/gdp/stickies.py +++ b/examples/gdp/stickies.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import os from pyomo.common.fileutils import this_file_dir diff --git a/examples/gdp/strip_packing/stripPacking.py b/examples/gdp/strip_packing/stripPacking.py index 0e8902c5ee4..39f7208b838 100644 --- a/examples/gdp/strip_packing/stripPacking.py +++ b/examples/gdp/strip_packing/stripPacking.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core import * from pyomo.gdp import * diff --git a/examples/gdp/strip_packing/strip_packing_8rect.py b/examples/gdp/strip_packing/strip_packing_8rect.py index eba3c82dc05..2bd7c4840ca 100644 --- a/examples/gdp/strip_packing/strip_packing_8rect.py +++ b/examples/gdp/strip_packing/strip_packing_8rect.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Strip packing example from MINLP.org library. Strip-packing example from http://minlp.org/library/lib.php?lib=GDP This model packs a set of rectangles without rotation or overlap within a @@ -11,8 +22,6 @@ """ -from __future__ import division - from pyomo.environ import ( ConcreteModel, NonNegativeReals, diff --git a/examples/gdp/strip_packing/strip_packing_concrete.py b/examples/gdp/strip_packing/strip_packing_concrete.py index 4fa6172a8d1..b0907cdea61 100644 --- a/examples/gdp/strip_packing/strip_packing_concrete.py +++ b/examples/gdp/strip_packing/strip_packing_concrete.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Strip packing example from MINLP.org library. Strip-packing example from http://minlp.org/library/lib.php?lib=GDP @@ -9,7 +20,6 @@ cutting fabric. """ -from __future__ import division from pyomo.environ import ConcreteModel, NonNegativeReals, Objective, Param, Set, Var diff --git a/examples/gdp/two_rxn_lee/two_rxn_model.py b/examples/gdp/two_rxn_lee/two_rxn_model.py index 9057ef8c006..98e4cc2e878 100644 --- a/examples/gdp/two_rxn_lee/two_rxn_model.py +++ b/examples/gdp/two_rxn_lee/two_rxn_model.py @@ -1,5 +1,15 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Two reactor model from literature. See README.md.""" -from __future__ import division from pyomo.core import ConcreteModel, Constraint, Objective, Param, Var, maximize diff --git a/examples/kernel/blocks.py b/examples/kernel/blocks.py index 7036981dcc8..db1cb6655c2 100644 --- a/examples/kernel/blocks.py +++ b/examples/kernel/blocks.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.kernel as pmo # diff --git a/examples/kernel/conic.py b/examples/kernel/conic.py index a2a787794a4..5ee66a00ee9 100644 --- a/examples/kernel/conic.py +++ b/examples/kernel/conic.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.kernel as pmo # diff --git a/examples/kernel/constraints.py b/examples/kernel/constraints.py index 6495ad12f63..69823a6ebbe 100644 --- a/examples/kernel/constraints.py +++ b/examples/kernel/constraints.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.kernel as pmo v = pmo.variable() diff --git a/examples/kernel/containers.py b/examples/kernel/containers.py index 9b525e87af6..9ec749b8c3e 100644 --- a/examples/kernel/containers.py +++ b/examples/kernel/containers.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.kernel as pmo # diff --git a/examples/kernel/expressions.py b/examples/kernel/expressions.py index 1756e5d3fd4..faef8d1d4ad 100644 --- a/examples/kernel/expressions.py +++ b/examples/kernel/expressions.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.kernel as pmo v = pmo.variable(value=2) diff --git a/examples/kernel/mosek/geometric1.py b/examples/kernel/mosek/geometric1.py index b5ec59541c4..8148e707819 100644 --- a/examples/kernel/mosek/geometric1.py +++ b/examples/kernel/mosek/geometric1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # Source: https://docs.mosek.com/9.0/pythonapi/tutorial-gp-shared.html import pyomo.kernel as pmo diff --git a/examples/kernel/mosek/geometric2.py b/examples/kernel/mosek/geometric2.py index 84825c0a39b..3fb62c86312 100644 --- a/examples/kernel/mosek/geometric2.py +++ b/examples/kernel/mosek/geometric2.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # Source: https://docs.mosek.com/modeling-cookbook/expo.html # (first example in Section 5.3.1) diff --git a/examples/kernel/mosek/maximum_volume_cuboid.py b/examples/kernel/mosek/maximum_volume_cuboid.py index 92e210cf400..df200cc801c 100644 --- a/examples/kernel/mosek/maximum_volume_cuboid.py +++ b/examples/kernel/mosek/maximum_volume_cuboid.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from scipy.spatial import ConvexHull from mpl_toolkits.mplot3d import Axes3D from mpl_toolkits.mplot3d.art3d import Poly3DCollection diff --git a/examples/kernel/mosek/power1.py b/examples/kernel/mosek/power1.py index 7274b587dae..a6d6ebbe47d 100644 --- a/examples/kernel/mosek/power1.py +++ b/examples/kernel/mosek/power1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # Source: https://docs.mosek.com/9.0/pythonapi/tutorial-pow-shared.html import pyomo.kernel as pmo @@ -12,9 +23,7 @@ def solve_nonlinear(): m.c = pmo.constraint(body=m.x + m.y + 0.5 * m.z, rhs=2) - m.o = pmo.objective( - (m.x**0.2) * (m.y**0.8) + (m.z**0.4) - m.x, sense=pmo.maximize - ) + m.o = pmo.objective((m.x**0.2) * (m.y**0.8) + (m.z**0.4) - m.x, sense=pmo.maximize) m.x.value, m.y.value, m.z.value = (1, 1, 1) ipopt = pmo.SolverFactory("ipopt") diff --git a/examples/kernel/mosek/semidefinite.py b/examples/kernel/mosek/semidefinite.py index 44ab7c95a68..6be47d85451 100644 --- a/examples/kernel/mosek/semidefinite.py +++ b/examples/kernel/mosek/semidefinite.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # Source: https://docs.mosek.com/latest/pythonfusion/tutorial-sdo-shared.html#doc-tutorial-sdo # This examples illustrates SDP formulations in Pyomo using diff --git a/examples/kernel/objectives.py b/examples/kernel/objectives.py index 7d87671ef8d..27a41f4edb5 100644 --- a/examples/kernel/objectives.py +++ b/examples/kernel/objectives.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.kernel as pmo v = pmo.variable(value=2) diff --git a/examples/kernel/parameters.py b/examples/kernel/parameters.py index 55b230add6b..e9e412525bb 100644 --- a/examples/kernel/parameters.py +++ b/examples/kernel/parameters.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.kernel as pmo # diff --git a/examples/kernel/piecewise_functions.py b/examples/kernel/piecewise_functions.py index 528d4c16791..73a7f680725 100644 --- a/examples/kernel/piecewise_functions.py +++ b/examples/kernel/piecewise_functions.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.kernel as pmo # diff --git a/examples/kernel/piecewise_nd_functions.py b/examples/kernel/piecewise_nd_functions.py index 847bb5f4a84..7de37fcbfc6 100644 --- a/examples/kernel/piecewise_nd_functions.py +++ b/examples/kernel/piecewise_nd_functions.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import random import sys diff --git a/examples/kernel/special_ordered_sets.py b/examples/kernel/special_ordered_sets.py index 9526a551c12..abacc3d4205 100644 --- a/examples/kernel/special_ordered_sets.py +++ b/examples/kernel/special_ordered_sets.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.kernel as pmo v1 = pmo.variable() diff --git a/examples/kernel/suffixes.py b/examples/kernel/suffixes.py index 39caa5b8652..ae95fbbdd09 100644 --- a/examples/kernel/suffixes.py +++ b/examples/kernel/suffixes.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.kernel as pmo # diff --git a/examples/kernel/variables.py b/examples/kernel/variables.py index 7ab571245a1..36865b58183 100644 --- a/examples/kernel/variables.py +++ b/examples/kernel/variables.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.kernel as pmo # diff --git a/examples/mpec/bard1.py b/examples/mpec/bard1.py index dbe666a7004..59955eefb8e 100644 --- a/examples/mpec/bard1.py +++ b/examples/mpec/bard1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # bard1.py QQR2-MN-8-5 # Original Pyomo coding by William Hart # Adapted from AMPL coding by Sven Leyffer diff --git a/examples/mpec/df.py b/examples/mpec/df.py index 41984992bdd..7bb25b11e07 100644 --- a/examples/mpec/df.py +++ b/examples/mpec/df.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/mpec/indexed.py b/examples/mpec/indexed.py index b69d5093477..0aff5de5b20 100644 --- a/examples/mpec/indexed.py +++ b/examples/mpec/indexed.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/mpec/linear1.py b/examples/mpec/linear1.py index eba04759ae3..f24fd357e62 100644 --- a/examples/mpec/linear1.py +++ b/examples/mpec/linear1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/mpec/munson1.py b/examples/mpec/munson1.py index debdf709db9..99c240b5c06 100644 --- a/examples/mpec/munson1.py +++ b/examples/mpec/munson1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/mpec/munson1a.py b/examples/mpec/munson1a.py index 519db4e6ec2..67f8f318531 100644 --- a/examples/mpec/munson1a.py +++ b/examples/mpec/munson1a.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/mpec/munson1b.py b/examples/mpec/munson1b.py index ff2b7b51294..46fff90a785 100644 --- a/examples/mpec/munson1b.py +++ b/examples/mpec/munson1b.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/mpec/munson1c.py b/examples/mpec/munson1c.py index 2592b25c515..dee5b224e75 100644 --- a/examples/mpec/munson1c.py +++ b/examples/mpec/munson1c.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/mpec/munson1d.py b/examples/mpec/munson1d.py index 0fb08ce73fb..157177f2eb0 100644 --- a/examples/mpec/munson1d.py +++ b/examples/mpec/munson1d.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/mpec/scholtes4.py b/examples/mpec/scholtes4.py index 904729780cf..8d574dd1916 100644 --- a/examples/mpec/scholtes4.py +++ b/examples/mpec/scholtes4.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # scholtes4.py LQR2-MN-3-2 # Original Pyomo coding by William Hart # Adapted from AMPL coding by Sven Leyffer diff --git a/examples/performance/dae/run_stochpdegas1_automatic.py b/examples/performance/dae/run_stochpdegas1_automatic.py index 993e22c7c86..fffa1a71ae1 100644 --- a/examples/performance/dae/run_stochpdegas1_automatic.py +++ b/examples/performance/dae/run_stochpdegas1_automatic.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import time from pyomo.environ import * diff --git a/examples/performance/dae/stochpdegas1_automatic.py b/examples/performance/dae/stochpdegas1_automatic.py index cd0153eee61..ce6132e6cf5 100644 --- a/examples/performance/dae/stochpdegas1_automatic.py +++ b/examples/performance/dae/stochpdegas1_automatic.py @@ -1,7 +1,18 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # stochastic pde model for natural gas network # victor m. zavala / 2013 -# from __future__ import division +# from pyomo.environ import * from pyomo.dae import * diff --git a/examples/performance/jump/clnlbeam.py b/examples/performance/jump/clnlbeam.py index d2ceda790ec..410068a6753 100644 --- a/examples/performance/jump/clnlbeam.py +++ b/examples/performance/jump/clnlbeam.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/examples/performance/jump/facility.py b/examples/performance/jump/facility.py index 6832e8d32ac..fa0c306d6e5 100644 --- a/examples/performance/jump/facility.py +++ b/examples/performance/jump/facility.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core import * model = AbstractModel() diff --git a/examples/performance/jump/lqcp.py b/examples/performance/jump/lqcp.py index bb3e66b36f5..b8ef096d7be 100644 --- a/examples/performance/jump/lqcp.py +++ b/examples/performance/jump/lqcp.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core import * model = ConcreteModel() diff --git a/examples/performance/jump/opf_66200bus.py b/examples/performance/jump/opf_66200bus.py index f3e1822fbfb..702ff59a61c 100644 --- a/examples/performance/jump/opf_66200bus.py +++ b/examples/performance/jump/opf_66200bus.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * diff --git a/examples/performance/jump/opf_6620bus.py b/examples/performance/jump/opf_6620bus.py index 64348ae931e..34b910f43c0 100644 --- a/examples/performance/jump/opf_6620bus.py +++ b/examples/performance/jump/opf_6620bus.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * diff --git a/examples/performance/jump/opf_662bus.py b/examples/performance/jump/opf_662bus.py index 6ff97c577e3..8a768ca16e0 100644 --- a/examples/performance/jump/opf_662bus.py +++ b/examples/performance/jump/opf_662bus.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * diff --git a/examples/performance/misc/bilinear1_100.py b/examples/performance/misc/bilinear1_100.py index e68fbba6283..d86091c4c76 100644 --- a/examples/performance/misc/bilinear1_100.py +++ b/examples/performance/misc/bilinear1_100.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * diff --git a/examples/performance/misc/bilinear1_100000.py b/examples/performance/misc/bilinear1_100000.py index 924d7233d24..0fa2eafedc6 100644 --- a/examples/performance/misc/bilinear1_100000.py +++ b/examples/performance/misc/bilinear1_100000.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * diff --git a/examples/performance/misc/bilinear2_100.py b/examples/performance/misc/bilinear2_100.py index 4dd9f9ead57..227bfe000e0 100644 --- a/examples/performance/misc/bilinear2_100.py +++ b/examples/performance/misc/bilinear2_100.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * diff --git a/examples/performance/misc/bilinear2_100000.py b/examples/performance/misc/bilinear2_100000.py index 90eeaf82271..9d2a4d6fb7c 100644 --- a/examples/performance/misc/bilinear2_100000.py +++ b/examples/performance/misc/bilinear2_100000.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * diff --git a/examples/performance/misc/diag1_100.py b/examples/performance/misc/diag1_100.py index e47a9179974..369d81982f0 100644 --- a/examples/performance/misc/diag1_100.py +++ b/examples/performance/misc/diag1_100.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * diff --git a/examples/performance/misc/diag1_100000.py b/examples/performance/misc/diag1_100000.py index a110c0d9d67..536758fda5d 100644 --- a/examples/performance/misc/diag1_100000.py +++ b/examples/performance/misc/diag1_100000.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * diff --git a/examples/performance/misc/diag2_100.py b/examples/performance/misc/diag2_100.py index fe820e8590b..6ad47528ff2 100644 --- a/examples/performance/misc/diag2_100.py +++ b/examples/performance/misc/diag2_100.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * diff --git a/examples/performance/misc/diag2_100000.py b/examples/performance/misc/diag2_100000.py index 38563de57b9..b95e2dd1d6f 100644 --- a/examples/performance/misc/diag2_100000.py +++ b/examples/performance/misc/diag2_100000.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * diff --git a/examples/performance/misc/set1.py b/examples/performance/misc/set1.py index 53227a3ee73..8a8b84fdcc3 100644 --- a/examples/performance/misc/set1.py +++ b/examples/performance/misc/set1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = ConcreteModel() diff --git a/examples/performance/misc/sparse1.py b/examples/performance/misc/sparse1.py index 264862760f9..b4883d379bc 100644 --- a/examples/performance/misc/sparse1.py +++ b/examples/performance/misc/sparse1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # # This is a performance test that we cannot easily execute right now # diff --git a/examples/performance/pmedian/pmedian1.py b/examples/performance/pmedian/pmedian1.py index 3d3f6c5407f..a22540efdd5 100644 --- a/examples/performance/pmedian/pmedian1.py +++ b/examples/performance/pmedian/pmedian1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/performance/pmedian/pmedian2.py b/examples/performance/pmedian/pmedian2.py index 434ded6dcbc..ff25a6c15eb 100644 --- a/examples/performance/pmedian/pmedian2.py +++ b/examples/performance/pmedian/pmedian2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/amplbook2/diet.py b/examples/pyomo/amplbook2/diet.py index 8cdffefa20f..cc52eacae20 100644 --- a/examples/pyomo/amplbook2/diet.py +++ b/examples/pyomo/amplbook2/diet.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/amplbook2/dieti.py b/examples/pyomo/amplbook2/dieti.py index 0934dcf83c6..45d403dd810 100644 --- a/examples/pyomo/amplbook2/dieti.py +++ b/examples/pyomo/amplbook2/dieti.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/amplbook2/econ2min.py b/examples/pyomo/amplbook2/econ2min.py index 0d27df780bb..fb870e02364 100644 --- a/examples/pyomo/amplbook2/econ2min.py +++ b/examples/pyomo/amplbook2/econ2min.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/amplbook2/econmin.py b/examples/pyomo/amplbook2/econmin.py index 84e41107ff2..d9c95758d4d 100644 --- a/examples/pyomo/amplbook2/econmin.py +++ b/examples/pyomo/amplbook2/econmin.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/amplbook2/prod.py b/examples/pyomo/amplbook2/prod.py index 74e456e013f..236f7254b29 100644 --- a/examples/pyomo/amplbook2/prod.py +++ b/examples/pyomo/amplbook2/prod.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/amplbook2/steel.py b/examples/pyomo/amplbook2/steel.py index 43bea775526..8c5c9b2a1d3 100644 --- a/examples/pyomo/amplbook2/steel.py +++ b/examples/pyomo/amplbook2/steel.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/amplbook2/steel3.py b/examples/pyomo/amplbook2/steel3.py index e9e494b6a1a..dd3b3ac202f 100644 --- a/examples/pyomo/amplbook2/steel3.py +++ b/examples/pyomo/amplbook2/steel3.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/amplbook2/steel4.py b/examples/pyomo/amplbook2/steel4.py index b6709e478e9..10cb0979d24 100644 --- a/examples/pyomo/amplbook2/steel4.py +++ b/examples/pyomo/amplbook2/steel4.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/benders/master.py b/examples/pyomo/benders/master.py index a457bf28b06..372810dc024 100644 --- a/examples/pyomo/benders/master.py +++ b/examples/pyomo/benders/master.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/benders/subproblem.py b/examples/pyomo/benders/subproblem.py index 886f71ff321..ae46dad2d41 100644 --- a/examples/pyomo/benders/subproblem.py +++ b/examples/pyomo/benders/subproblem.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/callbacks/sc.py b/examples/pyomo/callbacks/sc.py index ce32b0a1074..0882815c6b7 100644 --- a/examples/pyomo/callbacks/sc.py +++ b/examples/pyomo/callbacks/sc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/callbacks/sc_callback.py b/examples/pyomo/callbacks/sc_callback.py index 0dae9e1befc..cacc438b380 100644 --- a/examples/pyomo/callbacks/sc_callback.py +++ b/examples/pyomo/callbacks/sc_callback.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/callbacks/sc_script.py b/examples/pyomo/callbacks/sc_script.py index 8e4ade21b51..d3044e4d667 100644 --- a/examples/pyomo/callbacks/sc_script.py +++ b/examples/pyomo/callbacks/sc_script.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/callbacks/scalability/run.py b/examples/pyomo/callbacks/scalability/run.py index 8465e3f5019..cf95076fcc3 100644 --- a/examples/pyomo/callbacks/scalability/run.py +++ b/examples/pyomo/callbacks/scalability/run.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/callbacks/tsp.py b/examples/pyomo/callbacks/tsp.py index d3e28a98d3f..8526a540b66 100644 --- a/examples/pyomo/callbacks/tsp.py +++ b/examples/pyomo/callbacks/tsp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/columngeneration/cutting_stock.py b/examples/pyomo/columngeneration/cutting_stock.py index 58df6a5ad16..2d9399c7db4 100644 --- a/examples/pyomo/columngeneration/cutting_stock.py +++ b/examples/pyomo/columngeneration/cutting_stock.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/concrete/Whiskas.py b/examples/pyomo/concrete/Whiskas.py index 9bc8dd87e9d..3d3c19e94ac 100644 --- a/examples/pyomo/concrete/Whiskas.py +++ b/examples/pyomo/concrete/Whiskas.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/concrete/knapsack-abstract.py b/examples/pyomo/concrete/knapsack-abstract.py index bbef95f7810..9766d902722 100644 --- a/examples/pyomo/concrete/knapsack-abstract.py +++ b/examples/pyomo/concrete/knapsack-abstract.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/concrete/knapsack-concrete.py b/examples/pyomo/concrete/knapsack-concrete.py index cd115ab40a3..8966d0b8498 100644 --- a/examples/pyomo/concrete/knapsack-concrete.py +++ b/examples/pyomo/concrete/knapsack-concrete.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/concrete/rosen.py b/examples/pyomo/concrete/rosen.py index a8e8a175127..ae51ae50ac0 100644 --- a/examples/pyomo/concrete/rosen.py +++ b/examples/pyomo/concrete/rosen.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # rosen.py from pyomo.environ import * diff --git a/examples/pyomo/concrete/sodacan.py b/examples/pyomo/concrete/sodacan.py index 3c0cfd3aab2..5429b27a9d5 100644 --- a/examples/pyomo/concrete/sodacan.py +++ b/examples/pyomo/concrete/sodacan.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # sodacan.py from pyomo.environ import * from math import pi diff --git a/examples/pyomo/concrete/sodacan_fig.py b/examples/pyomo/concrete/sodacan_fig.py index bf9ae476b4c..b263eaf558d 100644 --- a/examples/pyomo/concrete/sodacan_fig.py +++ b/examples/pyomo/concrete/sodacan_fig.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from mpl_toolkits.mplot3d import Axes3D from matplotlib import cm from matplotlib.ticker import LinearLocator, FormatStrFormatter diff --git a/examples/pyomo/concrete/sp.py b/examples/pyomo/concrete/sp.py index edc2d68b170..e82a4bca0a9 100644 --- a/examples/pyomo/concrete/sp.py +++ b/examples/pyomo/concrete/sp.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # sp.py from pyomo.environ import * from sp_data import * # define c, b, h, and d diff --git a/examples/pyomo/concrete/sp_data.py b/examples/pyomo/concrete/sp_data.py index 58210126819..4453a10cead 100644 --- a/examples/pyomo/concrete/sp_data.py +++ b/examples/pyomo/concrete/sp_data.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + c = 1.0 b = 1.5 h = 0.1 diff --git a/examples/pyomo/connectors/network_flow.py b/examples/pyomo/connectors/network_flow.py index cb75ca7ecf2..d5587fdf4c8 100644 --- a/examples/pyomo/connectors/network_flow.py +++ b/examples/pyomo/connectors/network_flow.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/connectors/network_flow_proposed.py b/examples/pyomo/connectors/network_flow_proposed.py index ed603ff6626..f234f2decf4 100644 --- a/examples/pyomo/connectors/network_flow_proposed.py +++ b/examples/pyomo/connectors/network_flow_proposed.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/core/block1.py b/examples/pyomo/core/block1.py index 96f8114f19c..161fc2ca2f7 100644 --- a/examples/pyomo/core/block1.py +++ b/examples/pyomo/core/block1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/core/integrality1.py b/examples/pyomo/core/integrality1.py index db81805555f..0ab3a433dac 100644 --- a/examples/pyomo/core/integrality1.py +++ b/examples/pyomo/core/integrality1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/core/integrality2.py b/examples/pyomo/core/integrality2.py index 2d85c9f2455..6461d36f923 100644 --- a/examples/pyomo/core/integrality2.py +++ b/examples/pyomo/core/integrality2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/core/simple.py b/examples/pyomo/core/simple.py index d0359c143bf..6976f3d25ad 100644 --- a/examples/pyomo/core/simple.py +++ b/examples/pyomo/core/simple.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/core/t1.py b/examples/pyomo/core/t1.py index 4135049d4be..5d5416985a9 100644 --- a/examples/pyomo/core/t1.py +++ b/examples/pyomo/core/t1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/core/t2.py b/examples/pyomo/core/t2.py index 5d687917fba..4d3f1934cbe 100644 --- a/examples/pyomo/core/t2.py +++ b/examples/pyomo/core/t2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/core/t5.py b/examples/pyomo/core/t5.py index 38605751015..6b9d94e0ff1 100644 --- a/examples/pyomo/core/t5.py +++ b/examples/pyomo/core/t5.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/diet/diet-sqlite.py b/examples/pyomo/diet/diet-sqlite.py index e8963485294..dccd3c338d0 100644 --- a/examples/pyomo/diet/diet-sqlite.py +++ b/examples/pyomo/diet/diet-sqlite.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/diet/diet1.py b/examples/pyomo/diet/diet1.py index 1fd61ca268c..217f80b9c25 100644 --- a/examples/pyomo/diet/diet1.py +++ b/examples/pyomo/diet/diet1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/diet/diet2.py b/examples/pyomo/diet/diet2.py index 526dbcef484..291261b0901 100644 --- a/examples/pyomo/diet/diet2.py +++ b/examples/pyomo/diet/diet2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/draft/api.py b/examples/pyomo/draft/api.py index 5b506882d9b..d785f41935e 100644 --- a/examples/pyomo/draft/api.py +++ b/examples/pyomo/draft/api.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/draft/bpack.py b/examples/pyomo/draft/bpack.py index 697ce531013..7b076f7737b 100644 --- a/examples/pyomo/draft/bpack.py +++ b/examples/pyomo/draft/bpack.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/draft/diet2.py b/examples/pyomo/draft/diet2.py index 9e4d2c5d9c4..d23fa3cf5db 100644 --- a/examples/pyomo/draft/diet2.py +++ b/examples/pyomo/draft/diet2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/p-median/decorated_pmedian.py b/examples/pyomo/p-median/decorated_pmedian.py index 90345daf78d..c66971945f3 100644 --- a/examples/pyomo/p-median/decorated_pmedian.py +++ b/examples/pyomo/p-median/decorated_pmedian.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * import random diff --git a/examples/pyomo/p-median/pmedian.py b/examples/pyomo/p-median/pmedian.py index 88731f287d8..865aa7cb61f 100644 --- a/examples/pyomo/p-median/pmedian.py +++ b/examples/pyomo/p-median/pmedian.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/p-median/solver1.py b/examples/pyomo/p-median/solver1.py index 113bf9fdd29..2652ab13943 100644 --- a/examples/pyomo/p-median/solver1.py +++ b/examples/pyomo/p-median/solver1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/p-median/solver2.py b/examples/pyomo/p-median/solver2.py index c62f161fd24..50ec5388811 100644 --- a/examples/pyomo/p-median/solver2.py +++ b/examples/pyomo/p-median/solver2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/piecewise/convex.py b/examples/pyomo/piecewise/convex.py index a3233ae5c3e..fb8095f80e3 100644 --- a/examples/pyomo/piecewise/convex.py +++ b/examples/pyomo/piecewise/convex.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/piecewise/indexed.py b/examples/pyomo/piecewise/indexed.py index dea56df3911..cde21ec847e 100644 --- a/examples/pyomo/piecewise/indexed.py +++ b/examples/pyomo/piecewise/indexed.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/piecewise/indexed_nonlinear.py b/examples/pyomo/piecewise/indexed_nonlinear.py index e871508d1be..d72fbc8a899 100644 --- a/examples/pyomo/piecewise/indexed_nonlinear.py +++ b/examples/pyomo/piecewise/indexed_nonlinear.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/piecewise/indexed_points.py b/examples/pyomo/piecewise/indexed_points.py index 15b1c33a7ec..66110bea342 100644 --- a/examples/pyomo/piecewise/indexed_points.py +++ b/examples/pyomo/piecewise/indexed_points.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/piecewise/nonconvex.py b/examples/pyomo/piecewise/nonconvex.py index 004748ab2eb..5300278d5b9 100644 --- a/examples/pyomo/piecewise/nonconvex.py +++ b/examples/pyomo/piecewise/nonconvex.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/piecewise/points.py b/examples/pyomo/piecewise/points.py index c822ceb5860..91d45684c4f 100644 --- a/examples/pyomo/piecewise/points.py +++ b/examples/pyomo/piecewise/points.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/piecewise/step.py b/examples/pyomo/piecewise/step.py index c3fbb4762ab..95aac74d7f7 100644 --- a/examples/pyomo/piecewise/step.py +++ b/examples/pyomo/piecewise/step.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/quadratic/example1.py b/examples/pyomo/quadratic/example1.py index dff911a0f0c..ab77c5a1733 100644 --- a/examples/pyomo/quadratic/example1.py +++ b/examples/pyomo/quadratic/example1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/quadratic/example2.py b/examples/pyomo/quadratic/example2.py index 981f2ef0bfb..ce02c6f70c8 100644 --- a/examples/pyomo/quadratic/example2.py +++ b/examples/pyomo/quadratic/example2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/quadratic/example3.py b/examples/pyomo/quadratic/example3.py index 4d96afe3328..bdba936f694 100644 --- a/examples/pyomo/quadratic/example3.py +++ b/examples/pyomo/quadratic/example3.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/quadratic/example4.py b/examples/pyomo/quadratic/example4.py index 256fc862a16..ecfc9981162 100644 --- a/examples/pyomo/quadratic/example4.py +++ b/examples/pyomo/quadratic/example4.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/radertext/Ex2_1.py b/examples/pyomo/radertext/Ex2_1.py index d352325798a..981388d4c72 100644 --- a/examples/pyomo/radertext/Ex2_1.py +++ b/examples/pyomo/radertext/Ex2_1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/radertext/Ex2_2.py b/examples/pyomo/radertext/Ex2_2.py index 13c23dd1816..41b56e52669 100644 --- a/examples/pyomo/radertext/Ex2_2.py +++ b/examples/pyomo/radertext/Ex2_2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/radertext/Ex2_3.py b/examples/pyomo/radertext/Ex2_3.py index d4dc3109ea1..7dc39afa773 100644 --- a/examples/pyomo/radertext/Ex2_3.py +++ b/examples/pyomo/radertext/Ex2_3.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/radertext/Ex2_5.py b/examples/pyomo/radertext/Ex2_5.py index da90b473b1f..fee49b46cb0 100644 --- a/examples/pyomo/radertext/Ex2_5.py +++ b/examples/pyomo/radertext/Ex2_5.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/radertext/Ex2_6a.py b/examples/pyomo/radertext/Ex2_6a.py index dc33a9b64e2..24bb866ec51 100644 --- a/examples/pyomo/radertext/Ex2_6a.py +++ b/examples/pyomo/radertext/Ex2_6a.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/radertext/Ex2_6b.py b/examples/pyomo/radertext/Ex2_6b.py index 8049d4ebb05..1be55461b9e 100644 --- a/examples/pyomo/radertext/Ex2_6b.py +++ b/examples/pyomo/radertext/Ex2_6b.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/sos/DepotSiting.py b/examples/pyomo/sos/DepotSiting.py index 98697681f44..40826e989b7 100644 --- a/examples/pyomo/sos/DepotSiting.py +++ b/examples/pyomo/sos/DepotSiting.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/sos/basic_sos2_example.py b/examples/pyomo/sos/basic_sos2_example.py index 655169ffe54..3aa0887356c 100644 --- a/examples/pyomo/sos/basic_sos2_example.py +++ b/examples/pyomo/sos/basic_sos2_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/sos/sos2_piecewise.py b/examples/pyomo/sos/sos2_piecewise.py index 4e79ce2ee62..79195761f3d 100644 --- a/examples/pyomo/sos/sos2_piecewise.py +++ b/examples/pyomo/sos/sos2_piecewise.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/suffixes/duals_pyomo.py b/examples/pyomo/suffixes/duals_pyomo.py index 9743add3ddd..6ce88fde429 100644 --- a/examples/pyomo/suffixes/duals_pyomo.py +++ b/examples/pyomo/suffixes/duals_pyomo.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/suffixes/duals_script.py b/examples/pyomo/suffixes/duals_script.py index a9db615cad3..e8ef9aef1bc 100644 --- a/examples/pyomo/suffixes/duals_script.py +++ b/examples/pyomo/suffixes/duals_script.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/suffixes/gurobi_ampl_basis.py b/examples/pyomo/suffixes/gurobi_ampl_basis.py index cd8e4e8f129..eab86f8aa47 100644 --- a/examples/pyomo/suffixes/gurobi_ampl_basis.py +++ b/examples/pyomo/suffixes/gurobi_ampl_basis.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/suffixes/gurobi_ampl_example.py b/examples/pyomo/suffixes/gurobi_ampl_example.py index d133fa422dc..4f3364c09dc 100644 --- a/examples/pyomo/suffixes/gurobi_ampl_example.py +++ b/examples/pyomo/suffixes/gurobi_ampl_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/suffixes/gurobi_ampl_iis.py b/examples/pyomo/suffixes/gurobi_ampl_iis.py index ccba226db78..da5bad073e7 100644 --- a/examples/pyomo/suffixes/gurobi_ampl_iis.py +++ b/examples/pyomo/suffixes/gurobi_ampl_iis.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/suffixes/ipopt_scaling.py b/examples/pyomo/suffixes/ipopt_scaling.py index c192a98dd98..7113128c21d 100644 --- a/examples/pyomo/suffixes/ipopt_scaling.py +++ b/examples/pyomo/suffixes/ipopt_scaling.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/suffixes/ipopt_warmstart.py b/examples/pyomo/suffixes/ipopt_warmstart.py index 6975bbaaa62..4882c48c8c8 100644 --- a/examples/pyomo/suffixes/ipopt_warmstart.py +++ b/examples/pyomo/suffixes/ipopt_warmstart.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/suffixes/sipopt_hicks.py b/examples/pyomo/suffixes/sipopt_hicks.py index dbf4e07b8f7..c7e058d5907 100644 --- a/examples/pyomo/suffixes/sipopt_hicks.py +++ b/examples/pyomo/suffixes/sipopt_hicks.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/suffixes/sipopt_parametric.py b/examples/pyomo/suffixes/sipopt_parametric.py index 29bba934bd8..0cb1c35f441 100644 --- a/examples/pyomo/suffixes/sipopt_parametric.py +++ b/examples/pyomo/suffixes/sipopt_parametric.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/transform/scaling_ex.py b/examples/pyomo/transform/scaling_ex.py index a5960393e75..34f937cbb45 100644 --- a/examples/pyomo/transform/scaling_ex.py +++ b/examples/pyomo/transform/scaling_ex.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/tutorials/data.out b/examples/pyomo/tutorials/data.out index d1353f87858..7dce6012e2f 100644 --- a/examples/pyomo/tutorials/data.out +++ b/examples/pyomo/tutorials/data.out @@ -1,4 +1,4 @@ -20 Set Declarations +14 Set Declarations A : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {'A1', 'A2', 'A3'} @@ -9,30 +9,18 @@ Key : Dimen : Domain : Size : Members None : 2 : A*B : 9 : {('A1', 1), ('A1', 2), ('A1', 3), ('A2', 1), ('A2', 2), ('A2', 3), ('A3', 1), ('A3', 2), ('A3', 3)} D : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 2 : D_domain : 3 : {('A1', 1), ('A2', 2), ('A3', 3)} - D_domain : Size=1, Index=None, Ordered=True Key : Dimen : Domain : Size : Members - None : 2 : A*B : 9 : {('A1', 1), ('A1', 2), ('A1', 3), ('A2', 1), ('A2', 2), ('A2', 3), ('A3', 1), ('A3', 2), ('A3', 3)} + None : 2 : A*B : 3 : {('A1', 1), ('A2', 2), ('A3', 3)} E : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 3 : E_domain : 6 : {('A1', 1, 'A1'), ('A1', 1, 'A2'), ('A2', 2, 'A2'), ('A2', 2, 'A3'), ('A3', 3, 'A1'), ('A3', 3, 'A3')} - E_domain : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 3 : E_domain_index_0*A : 27 : {('A1', 1, 'A1'), ('A1', 1, 'A2'), ('A1', 1, 'A3'), ('A1', 2, 'A1'), ('A1', 2, 'A2'), ('A1', 2, 'A3'), ('A1', 3, 'A1'), ('A1', 3, 'A2'), ('A1', 3, 'A3'), ('A2', 1, 'A1'), ('A2', 1, 'A2'), ('A2', 1, 'A3'), ('A2', 2, 'A1'), ('A2', 2, 'A2'), ('A2', 2, 'A3'), ('A2', 3, 'A1'), ('A2', 3, 'A2'), ('A2', 3, 'A3'), ('A3', 1, 'A1'), ('A3', 1, 'A2'), ('A3', 1, 'A3'), ('A3', 2, 'A1'), ('A3', 2, 'A2'), ('A3', 2, 'A3'), ('A3', 3, 'A1'), ('A3', 3, 'A2'), ('A3', 3, 'A3')} - E_domain_index_0 : Size=1, Index=None, Ordered=True Key : Dimen : Domain : Size : Members - None : 2 : A*B : 9 : {('A1', 1), ('A1', 2), ('A1', 3), ('A2', 1), ('A2', 2), ('A2', 3), ('A3', 1), ('A3', 2), ('A3', 3)} + None : 3 : A*B*A : 6 : {('A1', 1, 'A1'), ('A1', 1, 'A2'), ('A2', 2, 'A2'), ('A2', 2, 'A3'), ('A3', 3, 'A1'), ('A3', 3, 'A3')} F : Size=3, Index=A, Ordered=Insertion Key : Dimen : Domain : Size : Members A1 : 1 : Any : 3 : {1, 3, 5} A2 : 1 : Any : 3 : {2, 4, 6} A3 : 1 : Any : 3 : {3, 5, 7} - G : Size=0, Index=G_index, Ordered=Insertion + G : Size=0, Index=A*B, Ordered=Insertion Key : Dimen : Domain : Size : Members - G_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*B : 9 : {('A1', 1), ('A1', 2), ('A1', 3), ('A2', 1), ('A2', 2), ('A2', 3), ('A3', 1), ('A3', 2), ('A3', 3)} H : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {'H1', 'H2', 'H3'} @@ -45,12 +33,6 @@ K : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} - T_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*I : 12 : {('A1', 'I1'), ('A1', 'I2'), ('A1', 'I3'), ('A1', 'I4'), ('A2', 'I1'), ('A2', 'I2'), ('A2', 'I3'), ('A2', 'I4'), ('A3', 'I1'), ('A3', 'I2'), ('A3', 'I3'), ('A3', 'I4')} - U_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : I*A : 12 : {('I1', 'A1'), ('I1', 'A2'), ('I1', 'A3'), ('I2', 'A1'), ('I2', 'A2'), ('I2', 'A3'), ('I3', 'A1'), ('I3', 'A2'), ('I3', 'A3'), ('I4', 'A1'), ('I4', 'A2'), ('I4', 'A3')} x : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {'A1', 'A2', 'A3'} @@ -116,7 +98,7 @@ Key : Value A1 : 3.3 A3 : 3.5 - T : Size=12, Index=T_index, Domain=Any, Default=None, Mutable=False + T : Size=12, Index=A*I, Domain=Any, Default=None, Mutable=False Key : Value ('A1', 'I1') : 1.3 ('A1', 'I2') : 1.4 @@ -130,7 +112,7 @@ ('A3', 'I2') : 3.4 ('A3', 'I3') : 3.5 ('A3', 'I4') : 3.6 - U : Size=12, Index=U_index, Domain=Any, Default=None, Mutable=False + U : Size=12, Index=I*A, Domain=Any, Default=None, Mutable=False Key : Value ('I1', 'A1') : 1.3 ('I1', 'A2') : 2.3 @@ -166,4 +148,4 @@ Key : Value None : 2 -38 Declarations: A B C D_domain D E_domain_index_0 E_domain E F G_index G H I J K Z ZZ Y X W U_index U T_index T S R Q P PP O z y x M N MM MMM NNN +32 Declarations: A B C D E F G H I J K Z ZZ Y X W U T S R Q P PP O z y x M N MM MMM NNN diff --git a/examples/pyomo/tutorials/data.py b/examples/pyomo/tutorials/data.py index d065c9ff9bc..ea2569af934 100644 --- a/examples/pyomo/tutorials/data.py +++ b/examples/pyomo/tutorials/data.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/tutorials/excel.out b/examples/pyomo/tutorials/excel.out index 5064d4fa511..5e30827f7ae 100644 --- a/examples/pyomo/tutorials/excel.out +++ b/examples/pyomo/tutorials/excel.out @@ -1,4 +1,4 @@ -16 Set Declarations +10 Set Declarations A : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {'A1', 'A2', 'A3'} @@ -9,27 +9,15 @@ Key : Dimen : Domain : Size : Members None : 2 : A*B : 9 : {('A1', 1.0), ('A1', 2.0), ('A1', 3.0), ('A2', 1.0), ('A2', 2.0), ('A2', 3.0), ('A3', 1.0), ('A3', 2.0), ('A3', 3.0)} D : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 2 : D_domain : 3 : {('A1', 1.0), ('A2', 2.0), ('A3', 3.0)} - D_domain : Size=1, Index=None, Ordered=True Key : Dimen : Domain : Size : Members - None : 2 : A*B : 9 : {('A1', 1.0), ('A1', 2.0), ('A1', 3.0), ('A2', 1.0), ('A2', 2.0), ('A2', 3.0), ('A3', 1.0), ('A3', 2.0), ('A3', 3.0)} + None : 2 : A*B : 3 : {('A1', 1.0), ('A2', 2.0), ('A3', 3.0)} E : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 3 : E_domain : 6 : {('A1', 1.0, 'A1'), ('A1', 1.0, 'A2'), ('A2', 2.0, 'A2'), ('A2', 2.0, 'A3'), ('A3', 3.0, 'A1'), ('A3', 3.0, 'A3')} - E_domain : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 3 : E_domain_index_0*A : 27 : {('A1', 1.0, 'A1'), ('A1', 1.0, 'A2'), ('A1', 1.0, 'A3'), ('A1', 2.0, 'A1'), ('A1', 2.0, 'A2'), ('A1', 2.0, 'A3'), ('A1', 3.0, 'A1'), ('A1', 3.0, 'A2'), ('A1', 3.0, 'A3'), ('A2', 1.0, 'A1'), ('A2', 1.0, 'A2'), ('A2', 1.0, 'A3'), ('A2', 2.0, 'A1'), ('A2', 2.0, 'A2'), ('A2', 2.0, 'A3'), ('A2', 3.0, 'A1'), ('A2', 3.0, 'A2'), ('A2', 3.0, 'A3'), ('A3', 1.0, 'A1'), ('A3', 1.0, 'A2'), ('A3', 1.0, 'A3'), ('A3', 2.0, 'A1'), ('A3', 2.0, 'A2'), ('A3', 2.0, 'A3'), ('A3', 3.0, 'A1'), ('A3', 3.0, 'A2'), ('A3', 3.0, 'A3')} - E_domain_index_0 : Size=1, Index=None, Ordered=True Key : Dimen : Domain : Size : Members - None : 2 : A*B : 9 : {('A1', 1.0), ('A1', 2.0), ('A1', 3.0), ('A2', 1.0), ('A2', 2.0), ('A2', 3.0), ('A3', 1.0), ('A3', 2.0), ('A3', 3.0)} + None : 3 : A*B : 6 : {('A1', 1.0, 'A1'), ('A1', 1.0, 'A2'), ('A2', 2.0, 'A2'), ('A2', 2.0, 'A3'), ('A3', 3.0, 'A1'), ('A3', 3.0, 'A3')} F : Size=0, Index=A, Ordered=Insertion Key : Dimen : Domain : Size : Members - G : Size=0, Index=G_index, Ordered=Insertion + G : Size=0, Index=A*B, Ordered=Insertion Key : Dimen : Domain : Size : Members - G_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*B : 9 : {('A1', 1.0), ('A1', 2.0), ('A1', 3.0), ('A2', 1.0), ('A2', 2.0), ('A2', 3.0), ('A3', 1.0), ('A3', 2.0), ('A3', 3.0)} H : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {'H1', 'H2', 'H3'} @@ -39,12 +27,6 @@ J : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} - T_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*I : 12 : {('A1', 'I1'), ('A1', 'I2'), ('A1', 'I3'), ('A1', 'I4'), ('A2', 'I1'), ('A2', 'I2'), ('A2', 'I3'), ('A2', 'I4'), ('A3', 'I1'), ('A3', 'I2'), ('A3', 'I3'), ('A3', 'I4')} - U_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : I*A : 12 : {('I1', 'A1'), ('I1', 'A2'), ('I1', 'A3'), ('I2', 'A1'), ('I2', 'A2'), ('I2', 'A3'), ('I3', 'A1'), ('I3', 'A2'), ('I3', 'A3'), ('I4', 'A1'), ('I4', 'A2'), ('I4', 'A3')} 12 Param Declarations O : Size=3, Index=J, Domain=Reals, Default=None, Mutable=False @@ -76,7 +58,7 @@ Key : Value A1 : 3.3 A3 : 3.5 - T : Size=12, Index=T_index, Domain=Any, Default=None, Mutable=False + T : Size=12, Index=A*I, Domain=Any, Default=None, Mutable=False Key : Value ('A1', 'I1') : 1.3 ('A1', 'I2') : 1.4 @@ -90,7 +72,7 @@ ('A3', 'I2') : 3.4 ('A3', 'I3') : 3.5 ('A3', 'I4') : 3.6 - U : Size=12, Index=U_index, Domain=Any, Default=None, Mutable=False + U : Size=12, Index=I*A, Domain=Any, Default=None, Mutable=False Key : Value ('I1', 'A1') : 1.3 ('I1', 'A2') : 2.3 @@ -123,4 +105,4 @@ Key : Value None : 1.01 -28 Declarations: A B C D_domain D E_domain_index_0 E_domain E F G_index G H I J Z Y X W U_index U T_index T S R Q P PP O +22 Declarations: A B C D E F G H I J Z Y X W U T S R Q P PP O diff --git a/examples/pyomo/tutorials/excel.py b/examples/pyomo/tutorials/excel.py index 127db722c07..f9a5f66826b 100644 --- a/examples/pyomo/tutorials/excel.py +++ b/examples/pyomo/tutorials/excel.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/tutorials/param.out b/examples/pyomo/tutorials/param.out index 57e6a752ea5..ea258f5b493 100644 --- a/examples/pyomo/tutorials/param.out +++ b/examples/pyomo/tutorials/param.out @@ -1,22 +1,13 @@ -5 Set Declarations +2 Set Declarations A : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 4 : {2, 4, 6, 8} B : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {1, 2, 3} - R_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*B : 12 : {(2, 1), (2, 2), (2, 3), (4, 1), (4, 2), (4, 3), (6, 1), (6, 2), (6, 3), (8, 1), (8, 2), (8, 3)} - W_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*B : 12 : {(2, 1), (2, 2), (2, 3), (4, 1), (4, 2), (4, 3), (6, 1), (6, 2), (6, 3), (8, 1), (8, 2), (8, 3)} - X_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*B : 12 : {(2, 1), (2, 2), (2, 3), (4, 1), (4, 2), (4, 3), (6, 1), (6, 2), (6, 3), (8, 1), (8, 2), (8, 3)} 9 Param Declarations - R : Size=12, Index=R_index, Domain=Any, Default=99.0, Mutable=False + R : Size=12, Index=A*B, Domain=Any, Default=99.0, Mutable=False Key : Value (2, 1) : 1 (2, 2) : 1 @@ -35,7 +26,7 @@ 1 : 1 2 : 2 3 : 9 - W : Size=12, Index=W_index, Domain=Any, Default=None, Mutable=False + W : Size=12, Index=A*B, Domain=Any, Default=None, Mutable=False Key : Value (2, 1) : 2 (2, 2) : 4 @@ -49,7 +40,7 @@ (8, 1) : 8 (8, 2) : 16 (8, 3) : 24 - X : Size=12, Index=X_index, Domain=Any, Default=None, Mutable=False + X : Size=12, Index=A*B, Domain=Any, Default=None, Mutable=False Key : Value (2, 1) : 1.3 (2, 2) : 1.4 @@ -73,4 +64,4 @@ Key : Value None : 1.1 -14 Declarations: A B Z Y X_index X W_index W V U T S R_index R +11 Declarations: A B Z Y X W V U T S R diff --git a/examples/pyomo/tutorials/param.py b/examples/pyomo/tutorials/param.py index ba31975ab4b..5a94bafaa5e 100644 --- a/examples/pyomo/tutorials/param.py +++ b/examples/pyomo/tutorials/param.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/tutorials/set.out b/examples/pyomo/tutorials/set.out index b01b666c012..818977f6155 100644 --- a/examples/pyomo/tutorials/set.out +++ b/examples/pyomo/tutorials/set.out @@ -1,15 +1,12 @@ -28 Set Declarations +23 Set Declarations A : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {1, 2, 3} B : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 4 : {2, 3, 4, 5} - C : Size=0, Index=C_index, Ordered=Insertion + C : Size=0, Index=A*B, Ordered=Insertion Key : Dimen : Domain : Size : Members - C_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*B : 12 : {(1, 2), (1, 3), (1, 4), (1, 5), (2, 2), (2, 3), (2, 4), (2, 5), (3, 2), (3, 3), (3, 4), (3, 5)} D : Size=1, Index=None, Ordered=True Key : Dimen : Domain : Size : Members None : 1 : A | B : 5 : {1, 2, 3, 4, 5} @@ -26,15 +23,9 @@ Key : Dimen : Domain : Size : Members None : 2 : A*B : 12 : {(1, 2), (1, 3), (1, 4), (1, 5), (2, 2), (2, 3), (2, 4), (2, 5), (3, 2), (3, 3), (3, 4), (3, 5)} Hsub : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 2 : Hsub_domain : 3 : {(1, 2), (1, 3), (3, 3)} - Hsub_domain : Size=1, Index=None, Ordered=True Key : Dimen : Domain : Size : Members - None : 2 : A*B : 12 : {(1, 2), (1, 3), (1, 4), (1, 5), (2, 2), (2, 3), (2, 4), (2, 5), (3, 2), (3, 3), (3, 4), (3, 5)} + None : 2 : A*B : 3 : {(1, 2), (1, 3), (3, 3)} I : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 2 : I_domain : 12 : {(1, 2), (1, 3), (1, 4), (1, 5), (2, 2), (2, 3), (2, 4), (2, 5), (3, 2), (3, 3), (3, 4), (3, 5)} - I_domain : Size=1, Index=None, Ordered=True Key : Dimen : Domain : Size : Members None : 2 : A*B : 12 : {(1, 2), (1, 3), (1, 4), (1, 5), (2, 2), (2, 3), (2, 4), (2, 5), (3, 2), (3, 3), (3, 4), (3, 5)} J : Size=1, Index=None, Ordered=Insertion @@ -53,15 +44,12 @@ Key : Dimen : Domain : Size : Members None : 1 : Any : 2 : {1, 3} N : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 2 : N_domain : 0 : {} - N_domain : Size=1, Index=None, Ordered=True Key : Dimen : Domain : Size : Members - None : 2 : A*B : 12 : {(1, 2), (1, 3), (1, 4), (1, 5), (2, 2), (2, 3), (2, 4), (2, 5), (3, 2), (3, 3), (3, 4), (3, 5)} + None : 2 : A*B : 0 : {} O : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : -- : Any : 0 : {} - P : Size=16, Index=P_index, Ordered=Insertion + P : Size=16, Index=B*B, Ordered=Insertion Key : Dimen : Domain : Size : Members (2, 2) : 1 : Any : 4 : {0, 1, 2, 3} (2, 3) : 1 : Any : 6 : {0, 1, 2, 3, 4, 5} @@ -79,9 +67,6 @@ (5, 3) : 1 : Any : 15 : {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14} (5, 4) : 1 : Any : 20 : {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19} (5, 5) : 1 : Any : 25 : {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24} - P_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : B*B : 16 : {(2, 2), (2, 3), (2, 4), (2, 5), (3, 2), (3, 3), (3, 4), (3, 5), (4, 2), (4, 3), (4, 4), (4, 5), (5, 2), (5, 3), (5, 4), (5, 5)} R : Size=3, Index=B, Ordered=Insertion Key : Dimen : Domain : Size : Members 2 : 1 : Any : 3 : {1, 3, 5} @@ -98,16 +83,11 @@ U : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 5 : {1, 2, 6, 24, 120} - V : Size=4, Index=V_index, Ordered=Insertion + V : Size=4, Index=[1:4], Ordered=Insertion Key : Dimen : Domain : Size : Members 1 : 1 : Any : 5 : {1, 2, 3, 4, 5} 2 : 1 : Any : 5 : {1, 3, 5, 7, 9} 3 : 1 : Any : 5 : {1, 4, 7, 10, 13} 4 : 1 : Any : 5 : {1, 5, 9, 13, 17} -1 RangeSet Declarations - V_index : Dimen=1, Size=4, Bounds=(1, 4) - Key : Finite : Members - None : True : [1:4] - -29 Declarations: A B C_index C D E F G H Hsub_domain Hsub I_domain I J K K_2 L M N_domain N O P_index P R S T U V_index V +23 Declarations: A B C D E F G H Hsub I J K K_2 L M N O P R S T U V diff --git a/examples/pyomo/tutorials/set.py b/examples/pyomo/tutorials/set.py index 78f2656d739..a14301484c9 100644 --- a/examples/pyomo/tutorials/set.py +++ b/examples/pyomo/tutorials/set.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/tutorials/table.out b/examples/pyomo/tutorials/table.out index 1eba28afd19..75e2b0aee33 100644 --- a/examples/pyomo/tutorials/table.out +++ b/examples/pyomo/tutorials/table.out @@ -1,4 +1,4 @@ -16 Set Declarations +10 Set Declarations A : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {'A1', 'A2', 'A3'} @@ -9,27 +9,15 @@ Key : Dimen : Domain : Size : Members None : 2 : A*B : 9 : {('A1', 1), ('A1', 2), ('A1', 3), ('A2', 1), ('A2', 2), ('A2', 3), ('A3', 1), ('A3', 2), ('A3', 3)} D : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 2 : D_domain : 3 : {('A1', 1), ('A2', 2), ('A3', 3)} - D_domain : Size=1, Index=None, Ordered=True Key : Dimen : Domain : Size : Members - None : 2 : A*B : 9 : {('A1', 1), ('A1', 2), ('A1', 3), ('A2', 1), ('A2', 2), ('A2', 3), ('A3', 1), ('A3', 2), ('A3', 3)} + None : 2 : A*B : 3 : {('A1', 1), ('A2', 2), ('A3', 3)} E : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 3 : E_domain : 6 : {('A1', 1, 'A1'), ('A1', 1, 'A2'), ('A2', 2, 'A2'), ('A2', 2, 'A3'), ('A3', 3, 'A1'), ('A3', 3, 'A3')} - E_domain : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 3 : E_domain_index_0*A : 27 : {('A1', 1, 'A1'), ('A1', 1, 'A2'), ('A1', 1, 'A3'), ('A1', 2, 'A1'), ('A1', 2, 'A2'), ('A1', 2, 'A3'), ('A1', 3, 'A1'), ('A1', 3, 'A2'), ('A1', 3, 'A3'), ('A2', 1, 'A1'), ('A2', 1, 'A2'), ('A2', 1, 'A3'), ('A2', 2, 'A1'), ('A2', 2, 'A2'), ('A2', 2, 'A3'), ('A2', 3, 'A1'), ('A2', 3, 'A2'), ('A2', 3, 'A3'), ('A3', 1, 'A1'), ('A3', 1, 'A2'), ('A3', 1, 'A3'), ('A3', 2, 'A1'), ('A3', 2, 'A2'), ('A3', 2, 'A3'), ('A3', 3, 'A1'), ('A3', 3, 'A2'), ('A3', 3, 'A3')} - E_domain_index_0 : Size=1, Index=None, Ordered=True Key : Dimen : Domain : Size : Members - None : 2 : A*B : 9 : {('A1', 1), ('A1', 2), ('A1', 3), ('A2', 1), ('A2', 2), ('A2', 3), ('A3', 1), ('A3', 2), ('A3', 3)} + None : 3 : A*B*A : 6 : {('A1', 1, 'A1'), ('A1', 1, 'A2'), ('A2', 2, 'A2'), ('A2', 2, 'A3'), ('A3', 3, 'A1'), ('A3', 3, 'A3')} F : Size=0, Index=A, Ordered=Insertion Key : Dimen : Domain : Size : Members - G : Size=0, Index=G_index, Ordered=Insertion + G : Size=0, Index=A*B, Ordered=Insertion Key : Dimen : Domain : Size : Members - G_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*B : 9 : {('A1', 1), ('A1', 2), ('A1', 3), ('A2', 1), ('A2', 2), ('A2', 3), ('A3', 1), ('A3', 2), ('A3', 3)} H : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {'H1', 'H2', 'H3'} @@ -39,12 +27,6 @@ J : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} - T_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*I : 12 : {('A1', 'I1'), ('A1', 'I2'), ('A1', 'I3'), ('A1', 'I4'), ('A2', 'I1'), ('A2', 'I2'), ('A2', 'I3'), ('A2', 'I4'), ('A3', 'I1'), ('A3', 'I2'), ('A3', 'I3'), ('A3', 'I4')} - U_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : I*A : 12 : {('I1', 'A1'), ('I1', 'A2'), ('I1', 'A3'), ('I2', 'A1'), ('I2', 'A2'), ('I2', 'A3'), ('I3', 'A1'), ('I3', 'A2'), ('I3', 'A3'), ('I4', 'A1'), ('I4', 'A2'), ('I4', 'A3')} 12 Param Declarations O : Size=3, Index=J, Domain=Reals, Default=None, Mutable=False @@ -76,7 +58,7 @@ Key : Value A1 : 3.3 A3 : 3.5 - T : Size=12, Index=T_index, Domain=Any, Default=None, Mutable=False + T : Size=12, Index=A*I, Domain=Any, Default=None, Mutable=False Key : Value ('A1', 'I1') : 1.3 ('A1', 'I2') : 1.4 @@ -90,7 +72,7 @@ ('A3', 'I2') : 3.4 ('A3', 'I3') : 3.5 ('A3', 'I4') : 3.6 - U : Size=12, Index=U_index, Domain=Any, Default=None, Mutable=False + U : Size=12, Index=I*A, Domain=Any, Default=None, Mutable=False Key : Value ('I1', 'A1') : 1.3 ('I1', 'A2') : 2.3 @@ -123,4 +105,4 @@ Key : Value None : 1.01 -28 Declarations: A B C D_domain D E_domain_index_0 E_domain E F G_index G H I J Z Y X W U_index U T_index T S R Q P PP O +22 Declarations: A B C D E F G H I J Z Y X W U T S R Q P PP O diff --git a/examples/pyomo/tutorials/table.py b/examples/pyomo/tutorials/table.py index 16951352ee1..7d9fceda14a 100644 --- a/examples/pyomo/tutorials/table.py +++ b/examples/pyomo/tutorials/table.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/__init__.py b/examples/pyomobook/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/examples/pyomobook/__init__.py +++ b/examples/pyomobook/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/examples/pyomobook/abstract-ch/AbstHLinScript.py b/examples/pyomobook/abstract-ch/AbstHLinScript.py index adf700bfd5c..687d3fc4e6b 100644 --- a/examples/pyomobook/abstract-ch/AbstHLinScript.py +++ b/examples/pyomobook/abstract-ch/AbstHLinScript.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # AbstHLinScript.py - Script for a simple linear version of (H) import pyomo.environ as pyo diff --git a/examples/pyomobook/abstract-ch/AbstractH.py b/examples/pyomobook/abstract-ch/AbstractH.py index da9f0a4931c..7595cbc4933 100644 --- a/examples/pyomobook/abstract-ch/AbstractH.py +++ b/examples/pyomobook/abstract-ch/AbstractH.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # AbstractH.py - Implement model (H) import pyomo.environ as pyo diff --git a/examples/pyomobook/abstract-ch/AbstractHLinear.py b/examples/pyomobook/abstract-ch/AbstractHLinear.py index 575487d3e95..f312020a9d5 100644 --- a/examples/pyomobook/abstract-ch/AbstractHLinear.py +++ b/examples/pyomobook/abstract-ch/AbstractHLinear.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # AbstractHLinear.py - A simple linear version of (H) import pyomo.environ as pyo diff --git a/examples/pyomobook/abstract-ch/abstract5.py b/examples/pyomobook/abstract-ch/abstract5.py index 3a06256dff8..8849d2dfe7f 100644 --- a/examples/pyomobook/abstract-ch/abstract5.py +++ b/examples/pyomobook/abstract-ch/abstract5.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # abstract5.py import pyomo.environ as pyo diff --git a/examples/pyomobook/abstract-ch/abstract6.py b/examples/pyomobook/abstract-ch/abstract6.py index d11a4652f64..121b12a51fa 100644 --- a/examples/pyomobook/abstract-ch/abstract6.py +++ b/examples/pyomobook/abstract-ch/abstract6.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # abstract6.py import pyomo.environ as pyo diff --git a/examples/pyomobook/abstract-ch/abstract7.py b/examples/pyomobook/abstract-ch/abstract7.py index 2fd5d467d3e..3e8131bf42b 100644 --- a/examples/pyomobook/abstract-ch/abstract7.py +++ b/examples/pyomobook/abstract-ch/abstract7.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # abstract7.py import pyomo.environ as pyo import pickle diff --git a/examples/pyomobook/abstract-ch/buildactions.py b/examples/pyomobook/abstract-ch/buildactions.py index ad918e2b5f2..6963f285c4c 100644 --- a/examples/pyomobook/abstract-ch/buildactions.py +++ b/examples/pyomobook/abstract-ch/buildactions.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # buildactions.py: Warehouse location problem showing build actions import pyomo.environ as pyo diff --git a/examples/pyomobook/abstract-ch/concrete1.py b/examples/pyomobook/abstract-ch/concrete1.py index 0ad41c79ea3..2c89fbafaad 100644 --- a/examples/pyomobook/abstract-ch/concrete1.py +++ b/examples/pyomobook/abstract-ch/concrete1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/abstract-ch/concrete2.py b/examples/pyomobook/abstract-ch/concrete2.py index 6aee434d556..f68c4d6e242 100644 --- a/examples/pyomobook/abstract-ch/concrete2.py +++ b/examples/pyomobook/abstract-ch/concrete2.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/abstract-ch/diet1.py b/examples/pyomobook/abstract-ch/diet1.py index eb8b071cdb5..fa8bf5f549f 100644 --- a/examples/pyomobook/abstract-ch/diet1.py +++ b/examples/pyomobook/abstract-ch/diet1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # diet1.py import pyomo.environ as pyo diff --git a/examples/pyomobook/abstract-ch/ex.py b/examples/pyomobook/abstract-ch/ex.py index 88005b7dc0c..83cfd445e01 100644 --- a/examples/pyomobook/abstract-ch/ex.py +++ b/examples/pyomobook/abstract-ch/ex.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/param1.py b/examples/pyomobook/abstract-ch/param1.py index fc9fac99ff4..3ff8b648661 100644 --- a/examples/pyomobook/abstract-ch/param1.py +++ b/examples/pyomobook/abstract-ch/param1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/param2.py b/examples/pyomobook/abstract-ch/param2.py index d51cbeffe84..aca8fac0baf 100644 --- a/examples/pyomobook/abstract-ch/param2.py +++ b/examples/pyomobook/abstract-ch/param2.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/param2a.py b/examples/pyomobook/abstract-ch/param2a.py index fe928eb4197..6b6f77f2a8f 100644 --- a/examples/pyomobook/abstract-ch/param2a.py +++ b/examples/pyomobook/abstract-ch/param2a.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/param3.py b/examples/pyomobook/abstract-ch/param3.py index 64efba5c5ad..7545b47dadc 100644 --- a/examples/pyomobook/abstract-ch/param3.py +++ b/examples/pyomobook/abstract-ch/param3.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/param3a.py b/examples/pyomobook/abstract-ch/param3a.py index 857d96f8318..4c52b6432fb 100644 --- a/examples/pyomobook/abstract-ch/param3a.py +++ b/examples/pyomobook/abstract-ch/param3a.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/param3b.py b/examples/pyomobook/abstract-ch/param3b.py index 655694c33dd..786d6b58a16 100644 --- a/examples/pyomobook/abstract-ch/param3b.py +++ b/examples/pyomobook/abstract-ch/param3b.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/param3c.py b/examples/pyomobook/abstract-ch/param3c.py index 7d58b8b6a39..3f5da5f837e 100644 --- a/examples/pyomobook/abstract-ch/param3c.py +++ b/examples/pyomobook/abstract-ch/param3c.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/param4.py b/examples/pyomobook/abstract-ch/param4.py index c902b9034ad..c1926ddea74 100644 --- a/examples/pyomobook/abstract-ch/param4.py +++ b/examples/pyomobook/abstract-ch/param4.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/param5.py b/examples/pyomobook/abstract-ch/param5.py index 488e1debda8..7e0020f70b7 100644 --- a/examples/pyomobook/abstract-ch/param5.py +++ b/examples/pyomobook/abstract-ch/param5.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/param5a.py b/examples/pyomobook/abstract-ch/param5a.py index 7e814b917cc..efdd1855f3f 100644 --- a/examples/pyomobook/abstract-ch/param5a.py +++ b/examples/pyomobook/abstract-ch/param5a.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/param6.py b/examples/pyomobook/abstract-ch/param6.py index d9c49a548b2..f6d60f11e4b 100644 --- a/examples/pyomobook/abstract-ch/param6.py +++ b/examples/pyomobook/abstract-ch/param6.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/param6a.py b/examples/pyomobook/abstract-ch/param6a.py index e9aca384ee6..280e942d01d 100644 --- a/examples/pyomobook/abstract-ch/param6a.py +++ b/examples/pyomobook/abstract-ch/param6a.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/param7a.py b/examples/pyomobook/abstract-ch/param7a.py index 2a18cceabf6..21839bf3b64 100644 --- a/examples/pyomobook/abstract-ch/param7a.py +++ b/examples/pyomobook/abstract-ch/param7a.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/param7b.py b/examples/pyomobook/abstract-ch/param7b.py index acf02ddd62f..a4d79b6dee9 100644 --- a/examples/pyomobook/abstract-ch/param7b.py +++ b/examples/pyomobook/abstract-ch/param7b.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/param8a.py b/examples/pyomobook/abstract-ch/param8a.py index e68378961ed..f00ed649c30 100644 --- a/examples/pyomobook/abstract-ch/param8a.py +++ b/examples/pyomobook/abstract-ch/param8a.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/postprocess_fn.py b/examples/pyomobook/abstract-ch/postprocess_fn.py index f96a5b4dac1..2f2d114c216 100644 --- a/examples/pyomobook/abstract-ch/postprocess_fn.py +++ b/examples/pyomobook/abstract-ch/postprocess_fn.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import csv diff --git a/examples/pyomobook/abstract-ch/set1.py b/examples/pyomobook/abstract-ch/set1.py index ee281bd10bd..5a23fe683e0 100644 --- a/examples/pyomobook/abstract-ch/set1.py +++ b/examples/pyomobook/abstract-ch/set1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/set2.py b/examples/pyomobook/abstract-ch/set2.py index 27af609cead..5ecc0914bee 100644 --- a/examples/pyomobook/abstract-ch/set2.py +++ b/examples/pyomobook/abstract-ch/set2.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/set2a.py b/examples/pyomobook/abstract-ch/set2a.py index bf8f06dd7a8..7252ec0ad69 100644 --- a/examples/pyomobook/abstract-ch/set2a.py +++ b/examples/pyomobook/abstract-ch/set2a.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/set3.py b/examples/pyomobook/abstract-ch/set3.py index 7661963d19d..f3e3efc33c7 100644 --- a/examples/pyomobook/abstract-ch/set3.py +++ b/examples/pyomobook/abstract-ch/set3.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/set4.py b/examples/pyomobook/abstract-ch/set4.py index c9125dad657..0c29798b816 100644 --- a/examples/pyomobook/abstract-ch/set4.py +++ b/examples/pyomobook/abstract-ch/set4.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/set5.py b/examples/pyomobook/abstract-ch/set5.py index 9f79870d3ff..781b956404e 100644 --- a/examples/pyomobook/abstract-ch/set5.py +++ b/examples/pyomobook/abstract-ch/set5.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/wl_abstract.py b/examples/pyomobook/abstract-ch/wl_abstract.py index f35a5327bfb..61eeed6b506 100644 --- a/examples/pyomobook/abstract-ch/wl_abstract.py +++ b/examples/pyomobook/abstract-ch/wl_abstract.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # wl_abstract.py: AbstractModel version of warehouse location determination problem import pyomo.environ as pyo diff --git a/examples/pyomobook/abstract-ch/wl_abstract_script.py b/examples/pyomobook/abstract-ch/wl_abstract_script.py index 0b042405714..7f0871350fc 100644 --- a/examples/pyomobook/abstract-ch/wl_abstract_script.py +++ b/examples/pyomobook/abstract-ch/wl_abstract_script.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # wl_abstract_script.py: Scripting using an AbstractModel import pyomo.environ as pyo diff --git a/examples/pyomobook/blocks-ch/blocks_gen.py b/examples/pyomobook/blocks-ch/blocks_gen.py index 109e881cad5..31a4462f7d6 100644 --- a/examples/pyomobook/blocks-ch/blocks_gen.py +++ b/examples/pyomobook/blocks-ch/blocks_gen.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo time = range(5) diff --git a/examples/pyomobook/blocks-ch/blocks_gen.txt b/examples/pyomobook/blocks-ch/blocks_gen.txt index 63d634b3b95..1636f7e4590 100644 --- a/examples/pyomobook/blocks-ch/blocks_gen.txt +++ b/examples/pyomobook/blocks-ch/blocks_gen.txt @@ -9,13 +9,8 @@ 1 Block Declarations Generator : Size=2, Index=GEN_UNITS, Active=True Generator[G_EAST] : Active=True - 1 Set Declarations - CostCoef_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 2 : {1, 2} - 3 Param Declarations - CostCoef : Size=0, Index=Generator[G_EAST].CostCoef_index, Domain=Any, Default=None, Mutable=False + CostCoef : Size=0, Index={1, 2}, Domain=Any, Default=None, Mutable=False Key : Value MaxPower : Size=1, Index=None, Domain=NonNegativeReals, Default=None, Mutable=False Key : Value @@ -27,11 +22,11 @@ 2 Var Declarations Power : Size=5, Index=TIME Key : Lower : Value : Upper : Fixed : Stale : Domain - 0 : 0 : 120.0 : 500 : False : False : Reals - 1 : 0 : 145.0 : 500 : False : False : Reals - 2 : 0 : 119.0 : 500 : False : False : Reals - 3 : 0 : 42.0 : 500 : False : False : Reals - 4 : 0 : 190.0 : 500 : False : False : Reals + 0 : 0 : 120.0 : 500.0 : False : False : Reals + 1 : 0 : 145.0 : 500.0 : False : False : Reals + 2 : 0 : 119.0 : 500.0 : False : False : Reals + 3 : 0 : 42.0 : 500.0 : False : False : Reals + 4 : 0 : 190.0 : 500.0 : False : False : Reals UnitOn : Size=5, Index=TIME Key : Lower : Value : Upper : Fixed : Stale : Domain 0 : 0 : None : 1 : False : True : Binary @@ -57,15 +52,10 @@ 3 : -50.0 : Generator[G_EAST].Power[3] - Generator[G_EAST].Power[2] : Generator[G_EAST].RampLimit : True 4 : -50.0 : Generator[G_EAST].Power[4] - Generator[G_EAST].Power[3] : Generator[G_EAST].RampLimit : True - 8 Declarations: MaxPower RampLimit Power UnitOn limit_ramp CostCoef_index CostCoef Cost + 7 Declarations: MaxPower RampLimit Power UnitOn limit_ramp CostCoef Cost Generator[G_MAIN] : Active=True - 1 Set Declarations - CostCoef_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 2 : {1, 2} - 3 Param Declarations - CostCoef : Size=0, Index=Generator[G_MAIN].CostCoef_index, Domain=Any, Default=None, Mutable=False + CostCoef : Size=0, Index={1, 2}, Domain=Any, Default=None, Mutable=False Key : Value MaxPower : Size=1, Index=None, Domain=NonNegativeReals, Default=None, Mutable=False Key : Value @@ -77,11 +67,11 @@ 2 Var Declarations Power : Size=5, Index=TIME Key : Lower : Value : Upper : Fixed : Stale : Domain - 0 : 0 : 120.0 : 500 : False : False : Reals - 1 : 0 : 145.0 : 500 : False : False : Reals - 2 : 0 : 119.0 : 500 : False : False : Reals - 3 : 0 : 42.0 : 500 : False : False : Reals - 4 : 0 : 190.0 : 500 : False : False : Reals + 0 : 0 : 120.0 : 500.0 : False : False : Reals + 1 : 0 : 145.0 : 500.0 : False : False : Reals + 2 : 0 : 119.0 : 500.0 : False : False : Reals + 3 : 0 : 42.0 : 500.0 : False : False : Reals + 4 : 0 : 190.0 : 500.0 : False : False : Reals UnitOn : Size=5, Index=TIME Key : Lower : Value : Upper : Fixed : Stale : Domain 0 : 0 : None : 1 : False : True : Binary @@ -107,7 +97,7 @@ 3 : -50.0 : Generator[G_MAIN].Power[3] - Generator[G_MAIN].Power[2] : Generator[G_MAIN].RampLimit : True 4 : -50.0 : Generator[G_MAIN].Power[4] - Generator[G_MAIN].Power[3] : Generator[G_MAIN].RampLimit : True - 8 Declarations: MaxPower RampLimit Power UnitOn limit_ramp CostCoef_index CostCoef Cost + 7 Declarations: MaxPower RampLimit Power UnitOn limit_ramp CostCoef Cost 3 Declarations: TIME GEN_UNITS Generator 2 Set Declarations @@ -121,13 +111,8 @@ 1 Block Declarations Generator : Size=2, Index=GEN_UNITS, Active=True Generator[G_EAST] : Active=True - 1 Set Declarations - CostCoef_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 2 : {1, 2} - 3 Param Declarations - CostCoef : Size=0, Index=Generator[G_EAST].CostCoef_index, Domain=Any, Default=None, Mutable=False + CostCoef : Size=0, Index={1, 2}, Domain=Any, Default=None, Mutable=False Key : Value MaxPower : Size=1, Index=None, Domain=NonNegativeReals, Default=None, Mutable=False Key : Value @@ -139,11 +124,11 @@ 2 Var Declarations Power : Size=5, Index=TIME Key : Lower : Value : Upper : Fixed : Stale : Domain - 0 : 0 : 120.0 : 500 : False : False : Reals - 1 : 0 : 145.0 : 500 : False : False : Reals - 2 : 0 : 119.0 : 500 : False : False : Reals - 3 : 0 : 42.0 : 500 : False : False : Reals - 4 : 0 : 190.0 : 500 : False : False : Reals + 0 : 0 : 120.0 : 500.0 : False : False : Reals + 1 : 0 : 145.0 : 500.0 : False : False : Reals + 2 : 0 : 119.0 : 500.0 : False : False : Reals + 3 : 0 : 42.0 : 500.0 : False : False : Reals + 4 : 0 : 190.0 : 500.0 : False : False : Reals UnitOn : Size=5, Index=TIME Key : Lower : Value : Upper : Fixed : Stale : Domain 0 : 0 : None : 1 : False : True : Binary @@ -169,15 +154,10 @@ 3 : -50.0 : Generator[G_EAST].Power[3] - Generator[G_EAST].Power[2] : Generator[G_EAST].RampLimit : True 4 : -50.0 : Generator[G_EAST].Power[4] - Generator[G_EAST].Power[3] : Generator[G_EAST].RampLimit : True - 8 Declarations: MaxPower RampLimit Power UnitOn limit_ramp CostCoef_index CostCoef Cost + 7 Declarations: MaxPower RampLimit Power UnitOn limit_ramp CostCoef Cost Generator[G_MAIN] : Active=True - 1 Set Declarations - CostCoef_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 2 : {1, 2} - 3 Param Declarations - CostCoef : Size=0, Index=Generator[G_MAIN].CostCoef_index, Domain=Any, Default=None, Mutable=False + CostCoef : Size=0, Index={1, 2}, Domain=Any, Default=None, Mutable=False Key : Value MaxPower : Size=1, Index=None, Domain=NonNegativeReals, Default=None, Mutable=False Key : Value @@ -189,11 +169,11 @@ 2 Var Declarations Power : Size=5, Index=TIME Key : Lower : Value : Upper : Fixed : Stale : Domain - 0 : 0 : 120.0 : 500 : False : False : Reals - 1 : 0 : 145.0 : 500 : False : False : Reals - 2 : 0 : 119.0 : 500 : False : False : Reals - 3 : 0 : 42.0 : 500 : False : False : Reals - 4 : 0 : 190.0 : 500 : False : False : Reals + 0 : 0 : 120.0 : 500.0 : False : False : Reals + 1 : 0 : 145.0 : 500.0 : False : False : Reals + 2 : 0 : 119.0 : 500.0 : False : False : Reals + 3 : 0 : 42.0 : 500.0 : False : False : Reals + 4 : 0 : 190.0 : 500.0 : False : False : Reals UnitOn : Size=5, Index=TIME Key : Lower : Value : Upper : Fixed : Stale : Domain 0 : 0 : None : 1 : False : True : Binary @@ -219,7 +199,7 @@ 3 : -50.0 : Generator[G_MAIN].Power[3] - Generator[G_MAIN].Power[2] : Generator[G_MAIN].RampLimit : True 4 : -50.0 : Generator[G_MAIN].Power[4] - Generator[G_MAIN].Power[3] : Generator[G_MAIN].RampLimit : True - 8 Declarations: MaxPower RampLimit Power UnitOn limit_ramp CostCoef_index CostCoef Cost + 7 Declarations: MaxPower RampLimit Power UnitOn limit_ramp CostCoef Cost 3 Declarations: TIME GEN_UNITS Generator Generator[G_MAIN].Power[4] = 190.0 diff --git a/examples/pyomobook/blocks-ch/blocks_intro.py b/examples/pyomobook/blocks-ch/blocks_intro.py index ad3ceaa4349..ba2bd9d3a97 100644 --- a/examples/pyomobook/blocks-ch/blocks_intro.py +++ b/examples/pyomobook/blocks-ch/blocks_intro.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo # @hierarchy: diff --git a/examples/pyomobook/blocks-ch/blocks_lotsizing.py b/examples/pyomobook/blocks-ch/blocks_lotsizing.py index fe0717d8c7c..758ad964dc5 100644 --- a/examples/pyomobook/blocks-ch/blocks_lotsizing.py +++ b/examples/pyomobook/blocks-ch/blocks_lotsizing.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/blocks-ch/lotsizing.py b/examples/pyomobook/blocks-ch/lotsizing.py index 47ea265246e..ece4d6b541c 100644 --- a/examples/pyomobook/blocks-ch/lotsizing.py +++ b/examples/pyomobook/blocks-ch/lotsizing.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/blocks-ch/lotsizing_no_time.py b/examples/pyomobook/blocks-ch/lotsizing_no_time.py index 901467a0cbb..60e8ba44424 100644 --- a/examples/pyomobook/blocks-ch/lotsizing_no_time.py +++ b/examples/pyomobook/blocks-ch/lotsizing_no_time.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/blocks-ch/lotsizing_uncertain.py b/examples/pyomobook/blocks-ch/lotsizing_uncertain.py index 6d16de7e3a7..f72161db5c6 100644 --- a/examples/pyomobook/blocks-ch/lotsizing_uncertain.py +++ b/examples/pyomobook/blocks-ch/lotsizing_uncertain.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/blocks-ch/lotsizing_uncertain.txt b/examples/pyomobook/blocks-ch/lotsizing_uncertain.txt index db9eee79cc3..08f92ae9262 100644 --- a/examples/pyomobook/blocks-ch/lotsizing_uncertain.txt +++ b/examples/pyomobook/blocks-ch/lotsizing_uncertain.txt @@ -1,20 +1,3 @@ -5 Set Declarations - i_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : T*S : 25 : {(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (3, 1), (3, 2), (3, 3), (3, 4), (3, 5), (4, 1), (4, 2), (4, 3), (4, 4), (4, 5), (5, 1), (5, 2), (5, 3), (5, 4), (5, 5)} - i_neg_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : T*S : 25 : {(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (3, 1), (3, 2), (3, 3), (3, 4), (3, 5), (4, 1), (4, 2), (4, 3), (4, 4), (4, 5), (5, 1), (5, 2), (5, 3), (5, 4), (5, 5)} - i_pos_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : T*S : 25 : {(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (3, 1), (3, 2), (3, 3), (3, 4), (3, 5), (4, 1), (4, 2), (4, 3), (4, 4), (4, 5), (5, 1), (5, 2), (5, 3), (5, 4), (5, 5)} - x_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : T*S : 25 : {(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (3, 1), (3, 2), (3, 3), (3, 4), (3, 5), (4, 1), (4, 2), (4, 3), (4, 4), (4, 5), (5, 1), (5, 2), (5, 3), (5, 4), (5, 5)} - y_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : T*S : 25 : {(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (3, 1), (3, 2), (3, 3), (3, 4), (3, 5), (4, 1), (4, 2), (4, 3), (4, 4), (4, 5), (5, 1), (5, 2), (5, 3), (5, 4), (5, 5)} - 2 RangeSet Declarations S : Dimen=1, Size=5, Bounds=(1, 5) Key : Finite : Members @@ -24,7 +7,7 @@ None : True : [1:5] 5 Var Declarations - i : Size=25, Index=i_index + i : Size=25, Index=T*S Key : Lower : Value : Upper : Fixed : Stale : Domain (1, 1) : None : None : None : False : True : Reals (1, 2) : None : None : None : False : True : Reals @@ -51,7 +34,7 @@ (5, 3) : None : None : None : False : True : Reals (5, 4) : None : None : None : False : True : Reals (5, 5) : None : None : None : False : True : Reals - i_neg : Size=25, Index=i_neg_index + i_neg : Size=25, Index=T*S Key : Lower : Value : Upper : Fixed : Stale : Domain (1, 1) : 0 : None : None : False : True : NonNegativeReals (1, 2) : 0 : None : None : False : True : NonNegativeReals @@ -78,7 +61,7 @@ (5, 3) : 0 : None : None : False : True : NonNegativeReals (5, 4) : 0 : None : None : False : True : NonNegativeReals (5, 5) : 0 : None : None : False : True : NonNegativeReals - i_pos : Size=25, Index=i_pos_index + i_pos : Size=25, Index=T*S Key : Lower : Value : Upper : Fixed : Stale : Domain (1, 1) : 0 : None : None : False : True : NonNegativeReals (1, 2) : 0 : None : None : False : True : NonNegativeReals @@ -105,7 +88,7 @@ (5, 3) : 0 : None : None : False : True : NonNegativeReals (5, 4) : 0 : None : None : False : True : NonNegativeReals (5, 5) : 0 : None : None : False : True : NonNegativeReals - x : Size=25, Index=x_index + x : Size=25, Index=T*S Key : Lower : Value : Upper : Fixed : Stale : Domain (1, 1) : 0 : None : None : False : True : NonNegativeReals (1, 2) : 0 : None : None : False : True : NonNegativeReals @@ -132,7 +115,7 @@ (5, 3) : 0 : None : None : False : True : NonNegativeReals (5, 4) : 0 : None : None : False : True : NonNegativeReals (5, 5) : 0 : None : None : False : True : NonNegativeReals - y : Size=25, Index=y_index + y : Size=25, Index=T*S Key : Lower : Value : Upper : Fixed : Stale : Domain (1, 1) : 0 : None : 1 : False : True : Binary (1, 2) : 0 : None : 1 : False : True : Binary @@ -160,4 +143,4 @@ (5, 4) : 0 : None : 1 : False : True : Binary (5, 5) : 0 : None : 1 : False : True : Binary -12 Declarations: T S y_index y x_index x i_index i i_pos_index i_pos i_neg_index i_neg +7 Declarations: T S y x i i_pos i_neg diff --git a/examples/pyomobook/dae-ch/dae_tester_model.py b/examples/pyomobook/dae-ch/dae_tester_model.py index 9e0da9f4a62..396b8a53db1 100644 --- a/examples/pyomobook/dae-ch/dae_tester_model.py +++ b/examples/pyomobook/dae-ch/dae_tester_model.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # This is a file for testing miscellaneous code snippets from the DAE chapter import pyomo.environ as pyo import pyomo.dae as dae diff --git a/examples/pyomobook/dae-ch/path_constraint.py b/examples/pyomobook/dae-ch/path_constraint.py index 5fe41dd132d..5e252d1b99f 100644 --- a/examples/pyomobook/dae-ch/path_constraint.py +++ b/examples/pyomobook/dae-ch/path_constraint.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/dae-ch/path_constraint.txt b/examples/pyomobook/dae-ch/path_constraint.txt index 421692b33e9..97e56ab8816 100644 --- a/examples/pyomobook/dae-ch/path_constraint.txt +++ b/examples/pyomobook/dae-ch/path_constraint.txt @@ -1,8 +1,3 @@ -1 RangeSet Declarations - t_domain : Dimen=1, Size=Inf, Bounds=(0, 1) - Key : Finite : Members - None : False : [0..1] - 1 Param Declarations tf : Size=1, Index=None, Domain=Any, Default=None, Mutable=False Key : Value @@ -68,4 +63,4 @@ 0 : None : None : None : False : True : Reals 1 : None : None : None : False : True : Reals -15 Declarations: tf t_domain t u x1 x2 x3 dx1 dx2 dx3 x1dotcon x2dotcon x3dotcon obj con +14 Declarations: tf t u x1 x2 x3 dx1 dx2 dx3 x1dotcon x2dotcon x3dotcon obj con diff --git a/examples/pyomobook/dae-ch/plot_path_constraint.py b/examples/pyomobook/dae-ch/plot_path_constraint.py index 4c04bc1b6b6..be86f13cbc0 100644 --- a/examples/pyomobook/dae-ch/plot_path_constraint.py +++ b/examples/pyomobook/dae-ch/plot_path_constraint.py @@ -1,3 +1,15 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + # @plot_path: def plotter(subplot, x, *y, **kwds): plt.subplot(subplot) diff --git a/examples/pyomobook/dae-ch/run_path_constraint.py b/examples/pyomobook/dae-ch/run_path_constraint.py index b819d6a7127..fc115f5649c 100644 --- a/examples/pyomobook/dae-ch/run_path_constraint.py +++ b/examples/pyomobook/dae-ch/run_path_constraint.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo from pyomo.dae import * from path_constraint import m diff --git a/examples/pyomobook/dae-ch/run_path_constraint_tester.py b/examples/pyomobook/dae-ch/run_path_constraint_tester.py index bbcd83f5da5..22d887e9b11 100644 --- a/examples/pyomobook/dae-ch/run_path_constraint_tester.py +++ b/examples/pyomobook/dae-ch/run_path_constraint_tester.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.common.tee import capture_output from six import StringIO diff --git a/examples/pyomobook/gdp-ch/gdp_uc.py b/examples/pyomobook/gdp-ch/gdp_uc.py index 2495ed9bef1..6268bcce068 100644 --- a/examples/pyomobook/gdp-ch/gdp_uc.py +++ b/examples/pyomobook/gdp-ch/gdp_uc.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # gdp_uc.py import pyomo.environ as pyo from pyomo.gdp import * diff --git a/examples/pyomobook/gdp-ch/scont.py b/examples/pyomobook/gdp-ch/scont.py index 76597326700..d1cf4b172bd 100644 --- a/examples/pyomobook/gdp-ch/scont.py +++ b/examples/pyomobook/gdp-ch/scont.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # scont.py import pyomo.environ as pyo from pyomo.gdp import Disjunct, Disjunction diff --git a/examples/pyomobook/gdp-ch/scont2.py b/examples/pyomobook/gdp-ch/scont2.py index 94e510b358a..2c77fe670d5 100644 --- a/examples/pyomobook/gdp-ch/scont2.py +++ b/examples/pyomobook/gdp-ch/scont2.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo import scont diff --git a/examples/pyomobook/gdp-ch/scont_script.py b/examples/pyomobook/gdp-ch/scont_script.py index 22c9b88ad0c..fe0702dc262 100644 --- a/examples/pyomobook/gdp-ch/scont_script.py +++ b/examples/pyomobook/gdp-ch/scont_script.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo import scont diff --git a/examples/pyomobook/gdp-ch/verify_scont.py b/examples/pyomobook/gdp-ch/verify_scont.py index db44024fe66..222453560b6 100644 --- a/examples/pyomobook/gdp-ch/verify_scont.py +++ b/examples/pyomobook/gdp-ch/verify_scont.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import os diff --git a/examples/pyomobook/intro-ch/abstract5.py b/examples/pyomobook/intro-ch/abstract5.py index 2184ed7b3aa..2caad5f9351 100644 --- a/examples/pyomobook/intro-ch/abstract5.py +++ b/examples/pyomobook/intro-ch/abstract5.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/intro-ch/coloring_concrete.py b/examples/pyomobook/intro-ch/coloring_concrete.py index 107a31668c4..9931b5d80de 100644 --- a/examples/pyomobook/intro-ch/coloring_concrete.py +++ b/examples/pyomobook/intro-ch/coloring_concrete.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # # Graph coloring example adapted from # diff --git a/examples/pyomobook/intro-ch/concrete1.py b/examples/pyomobook/intro-ch/concrete1.py index a39ca1d41cd..169fbeb281c 100644 --- a/examples/pyomobook/intro-ch/concrete1.py +++ b/examples/pyomobook/intro-ch/concrete1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/intro-ch/concrete1_generic.py b/examples/pyomobook/intro-ch/concrete1_generic.py index de648470469..9a2d26bded8 100644 --- a/examples/pyomobook/intro-ch/concrete1_generic.py +++ b/examples/pyomobook/intro-ch/concrete1_generic.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo import mydata diff --git a/examples/pyomobook/intro-ch/mydata.py b/examples/pyomobook/intro-ch/mydata.py index 83aa26bacd9..209546ebeaf 100644 --- a/examples/pyomobook/intro-ch/mydata.py +++ b/examples/pyomobook/intro-ch/mydata.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + N = [1, 2] M = [1, 2] c = {1: 1, 2: 2} diff --git a/examples/pyomobook/mpec-ch/ex1a.py b/examples/pyomobook/mpec-ch/ex1a.py index 30cd2842556..e6f1c33fbbc 100644 --- a/examples/pyomobook/mpec-ch/ex1a.py +++ b/examples/pyomobook/mpec-ch/ex1a.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ex1a.py import pyomo.environ as pyo from pyomo.mpec import Complementarity, complements diff --git a/examples/pyomobook/mpec-ch/ex1b.py b/examples/pyomobook/mpec-ch/ex1b.py index 9592c81c4f6..2b0ac2ce1b7 100644 --- a/examples/pyomobook/mpec-ch/ex1b.py +++ b/examples/pyomobook/mpec-ch/ex1b.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ex1b.py import pyomo.environ as pyo from pyomo.mpec import ComplementarityList, complements diff --git a/examples/pyomobook/mpec-ch/ex1c.py b/examples/pyomobook/mpec-ch/ex1c.py index aad9c9b0d47..eaf0292b50d 100644 --- a/examples/pyomobook/mpec-ch/ex1c.py +++ b/examples/pyomobook/mpec-ch/ex1c.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ex1c.py import pyomo.environ as pyo from pyomo.mpec import ComplementarityList, complements diff --git a/examples/pyomobook/mpec-ch/ex1d.py b/examples/pyomobook/mpec-ch/ex1d.py index fa5247ff831..4c0e0d9fd0f 100644 --- a/examples/pyomobook/mpec-ch/ex1d.py +++ b/examples/pyomobook/mpec-ch/ex1d.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ex1d.py import pyomo.environ as pyo from pyomo.mpec import Complementarity, complements diff --git a/examples/pyomobook/mpec-ch/ex1e.py b/examples/pyomobook/mpec-ch/ex1e.py index bf714411396..c552847fcfb 100644 --- a/examples/pyomobook/mpec-ch/ex1e.py +++ b/examples/pyomobook/mpec-ch/ex1e.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ex1e.py import pyomo.environ as pyo from pyomo.mpec import ComplementarityList, complements diff --git a/examples/pyomobook/mpec-ch/ex2.py b/examples/pyomobook/mpec-ch/ex2.py index c192ccc7a34..6981af33376 100644 --- a/examples/pyomobook/mpec-ch/ex2.py +++ b/examples/pyomobook/mpec-ch/ex2.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ex2.py import pyomo.environ as pyo from pyomo.mpec import * diff --git a/examples/pyomobook/mpec-ch/munson1.py b/examples/pyomobook/mpec-ch/munson1.py index c7d171eb416..e85d9359768 100644 --- a/examples/pyomobook/mpec-ch/munson1.py +++ b/examples/pyomobook/mpec-ch/munson1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # munson1.py import pyomo.environ as pyo from pyomo.mpec import Complementarity, complements diff --git a/examples/pyomobook/mpec-ch/ralph1.py b/examples/pyomobook/mpec-ch/ralph1.py index 1d44a303b84..b6a8b45e8df 100644 --- a/examples/pyomobook/mpec-ch/ralph1.py +++ b/examples/pyomobook/mpec-ch/ralph1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ralph1.py import pyomo.environ as pyo from pyomo.mpec import Complementarity, complements diff --git a/examples/pyomobook/nonlinear-ch/deer/DeerProblem.py b/examples/pyomobook/nonlinear-ch/deer/DeerProblem.py index c076a7f4687..dc3ca179a58 100644 --- a/examples/pyomobook/nonlinear-ch/deer/DeerProblem.py +++ b/examples/pyomobook/nonlinear-ch/deer/DeerProblem.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # DeerProblem.py import pyomo.environ as pyo diff --git a/examples/pyomobook/nonlinear-ch/disease_est/disease_estimation.py b/examples/pyomobook/nonlinear-ch/disease_est/disease_estimation.py index 4eb859dc349..5675d7a715b 100644 --- a/examples/pyomobook/nonlinear-ch/disease_est/disease_estimation.py +++ b/examples/pyomobook/nonlinear-ch/disease_est/disease_estimation.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # disease_estimation.py import pyomo.environ as pyo diff --git a/examples/pyomobook/nonlinear-ch/multimodal/multimodal_init1.py b/examples/pyomobook/nonlinear-ch/multimodal/multimodal_init1.py index c435cafc3d5..a50bf3321d6 100644 --- a/examples/pyomobook/nonlinear-ch/multimodal/multimodal_init1.py +++ b/examples/pyomobook/nonlinear-ch/multimodal/multimodal_init1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # multimodal_init1.py import pyomo.environ as pyo from math import pi diff --git a/examples/pyomobook/nonlinear-ch/multimodal/multimodal_init2.py b/examples/pyomobook/nonlinear-ch/multimodal/multimodal_init2.py index aa0dbae1e66..6a209334521 100644 --- a/examples/pyomobook/nonlinear-ch/multimodal/multimodal_init2.py +++ b/examples/pyomobook/nonlinear-ch/multimodal/multimodal_init2.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo from math import pi diff --git a/examples/pyomobook/nonlinear-ch/react_design/ReactorDesign.py b/examples/pyomobook/nonlinear-ch/react_design/ReactorDesign.py index 814b4a5938e..1cfe3b7193f 100644 --- a/examples/pyomobook/nonlinear-ch/react_design/ReactorDesign.py +++ b/examples/pyomobook/nonlinear-ch/react_design/ReactorDesign.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ import pyomo.environ as pyo @@ -33,9 +44,7 @@ def create_model(k1, k2, k3, caf): model.cc_bal = pyo.Constraint(expr=(0 == -model.sv * model.cc + k2 * model.cb)) - model.cd_bal = pyo.Constraint( - expr=(0 == -model.sv * model.cd + k3 * model.ca**2.0) - ) + model.cd_bal = pyo.Constraint(expr=(0 == -model.sv * model.cd + k3 * model.ca**2.0)) return model diff --git a/examples/pyomobook/nonlinear-ch/react_design/ReactorDesignTable.py b/examples/pyomobook/nonlinear-ch/react_design/ReactorDesignTable.py index a242c85fbc2..2bd9574b427 100644 --- a/examples/pyomobook/nonlinear-ch/react_design/ReactorDesignTable.py +++ b/examples/pyomobook/nonlinear-ch/react_design/ReactorDesignTable.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo from ReactorDesign import create_model diff --git a/examples/pyomobook/nonlinear-ch/rosen/rosenbrock.py b/examples/pyomobook/nonlinear-ch/rosen/rosenbrock.py index e1633e2df69..bec1d04c12c 100644 --- a/examples/pyomobook/nonlinear-ch/rosen/rosenbrock.py +++ b/examples/pyomobook/nonlinear-ch/rosen/rosenbrock.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # rosenbrock.py # A Pyomo model for the Rosenbrock problem import pyomo.environ as pyo diff --git a/examples/pyomobook/optimization-ch/ConcHLinScript.py b/examples/pyomobook/optimization-ch/ConcHLinScript.py index 8481a83afbf..b94903585dc 100644 --- a/examples/pyomobook/optimization-ch/ConcHLinScript.py +++ b/examples/pyomobook/optimization-ch/ConcHLinScript.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ConcHLinScript.py - Linear (H) as a script import pyomo.environ as pyo diff --git a/examples/pyomobook/optimization-ch/ConcHLinScript.txt b/examples/pyomobook/optimization-ch/ConcHLinScript.txt index c04591c94dc..0d34868ed99 100644 --- a/examples/pyomobook/optimization-ch/ConcHLinScript.txt +++ b/examples/pyomobook/optimization-ch/ConcHLinScript.txt @@ -1,7 +1,7 @@ Model 'Linear (H)' Variables: - x : Size=2, Index=x_index + x : Size=2, Index={I_C_Scoops, Peanuts} Key : Lower : Value : Upper : Fixed : Stale : Domain I_C_Scoops : 0 : 0.0 : 100 : False : False : Reals Peanuts : 0 : 40.6 : 40.6 : False : False : Reals @@ -9,7 +9,7 @@ Model 'Linear (H)' Objectives: z : Size=1, Index=None, Active=True Key : Active : Value - None : True : 3.83388751715 + None : True : 3.8338875171467763 Constraints: budgetconstr : Size=1 diff --git a/examples/pyomobook/optimization-ch/ConcreteH.py b/examples/pyomobook/optimization-ch/ConcreteH.py index 1bf2a9446c1..d7474291d0d 100644 --- a/examples/pyomobook/optimization-ch/ConcreteH.py +++ b/examples/pyomobook/optimization-ch/ConcreteH.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ConcreteH.py - Implement a particular instance of (H) # @fct: diff --git a/examples/pyomobook/optimization-ch/ConcreteH.txt b/examples/pyomobook/optimization-ch/ConcreteH.txt index 5e669ff71e0..04bbbdab857 100644 --- a/examples/pyomobook/optimization-ch/ConcreteH.txt +++ b/examples/pyomobook/optimization-ch/ConcreteH.txt @@ -1,10 +1,5 @@ -1 Set Declarations - x_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 2 : {'I_C_Scoops', 'Peanuts'} - 1 Var Declarations - x : Size=2, Index=x_index + x : Size=2, Index={I_C_Scoops, Peanuts} Key : Lower : Value : Upper : Fixed : Stale : Domain I_C_Scoops : 0 : None : 100 : False : True : Reals Peanuts : 0 : None : 40.6 : False : True : Reals @@ -19,4 +14,4 @@ Key : Lower : Body : Upper : Active None : -Inf : 3.14*x[I_C_Scoops] + 0.2718*x[Peanuts] : 12.0 : True -4 Declarations: x_index x z budgetconstr +3 Declarations: x z budgetconstr diff --git a/examples/pyomobook/optimization-ch/ConcreteHLinear.py b/examples/pyomobook/optimization-ch/ConcreteHLinear.py index 0b42d5e2187..772c18cb6d5 100644 --- a/examples/pyomobook/optimization-ch/ConcreteHLinear.py +++ b/examples/pyomobook/optimization-ch/ConcreteHLinear.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ConcreteHLinear.py - Linear (H) import pyomo.environ as pyo diff --git a/examples/pyomobook/optimization-ch/ConcreteHLinear.txt b/examples/pyomobook/optimization-ch/ConcreteHLinear.txt index 2e778c2bd1b..7f19aca87ec 100644 --- a/examples/pyomobook/optimization-ch/ConcreteHLinear.txt +++ b/examples/pyomobook/optimization-ch/ConcreteHLinear.txt @@ -1,10 +1,5 @@ -1 Set Declarations - x_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 2 : {'I_C_Scoops', 'Peanuts'} - 1 Var Declarations - x : Size=2, Index=x_index + x : Size=2, Index={I_C_Scoops, Peanuts} Key : Lower : Value : Upper : Fixed : Stale : Domain I_C_Scoops : 0 : None : 100 : False : True : Reals Peanuts : 0 : None : 40.6 : False : True : Reals @@ -19,4 +14,4 @@ Key : Lower : Body : Upper : Active None : -Inf : 3.14*x[I_C_Scoops] + 0.2718*x[Peanuts] : 12.0 : True -4 Declarations: x_index x z budgetconstr +3 Declarations: x z budgetconstr diff --git a/examples/pyomobook/optimization-ch/IC_model_dict.py b/examples/pyomobook/optimization-ch/IC_model_dict.py index 4c54ef83701..b7e359777c7 100644 --- a/examples/pyomobook/optimization-ch/IC_model_dict.py +++ b/examples/pyomobook/optimization-ch/IC_model_dict.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # IC_model_dict.py - Implement a particular instance of (H) # @fct: diff --git a/examples/pyomobook/overview-ch/var_obj_con_snippet.py b/examples/pyomobook/overview-ch/var_obj_con_snippet.py index 49bb7c1276b..22524b5815a 100644 --- a/examples/pyomobook/overview-ch/var_obj_con_snippet.py +++ b/examples/pyomobook/overview-ch/var_obj_con_snippet.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/overview-ch/wl_abstract.py b/examples/pyomobook/overview-ch/wl_abstract.py index f35a5327bfb..61eeed6b506 100644 --- a/examples/pyomobook/overview-ch/wl_abstract.py +++ b/examples/pyomobook/overview-ch/wl_abstract.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # wl_abstract.py: AbstractModel version of warehouse location determination problem import pyomo.environ as pyo diff --git a/examples/pyomobook/overview-ch/wl_abstract_script.py b/examples/pyomobook/overview-ch/wl_abstract_script.py index 0b042405714..7f0871350fc 100644 --- a/examples/pyomobook/overview-ch/wl_abstract_script.py +++ b/examples/pyomobook/overview-ch/wl_abstract_script.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # wl_abstract_script.py: Scripting using an AbstractModel import pyomo.environ as pyo diff --git a/examples/pyomobook/overview-ch/wl_concrete.py b/examples/pyomobook/overview-ch/wl_concrete.py index 29316304f0a..c1bf70b07f1 100644 --- a/examples/pyomobook/overview-ch/wl_concrete.py +++ b/examples/pyomobook/overview-ch/wl_concrete.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # wl_concrete.py # ConcreteModel version of warehouse location problem import pyomo.environ as pyo diff --git a/examples/pyomobook/overview-ch/wl_concrete_script.py b/examples/pyomobook/overview-ch/wl_concrete_script.py index 278937f5aed..b369521994c 100644 --- a/examples/pyomobook/overview-ch/wl_concrete_script.py +++ b/examples/pyomobook/overview-ch/wl_concrete_script.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # wl_concrete_script.py # Solve an instance of the warehouse location problem diff --git a/examples/pyomobook/overview-ch/wl_concrete_script.txt b/examples/pyomobook/overview-ch/wl_concrete_script.txt index dae31e1a035..165289552d3 100644 --- a/examples/pyomobook/overview-ch/wl_concrete_script.txt +++ b/examples/pyomobook/overview-ch/wl_concrete_script.txt @@ -1,4 +1,4 @@ -y : Size=3, Index=y_index +y : Size=3, Index={Harlingen, Memphis, Ashland} Key : Lower : Value : Upper : Fixed : Stale : Domain Ashland : 0 : 1.0 : 1 : False : False : Binary Harlingen : 0 : 1.0 : 1 : False : False : Binary diff --git a/examples/pyomobook/overview-ch/wl_excel.py b/examples/pyomobook/overview-ch/wl_excel.py index 1c4ad997225..180e36422fe 100644 --- a/examples/pyomobook/overview-ch/wl_excel.py +++ b/examples/pyomobook/overview-ch/wl_excel.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # wl_excel.py: Loading Excel data using Pandas import pandas import pyomo.environ as pyo diff --git a/examples/pyomobook/overview-ch/wl_excel.txt b/examples/pyomobook/overview-ch/wl_excel.txt index dae31e1a035..165289552d3 100644 --- a/examples/pyomobook/overview-ch/wl_excel.txt +++ b/examples/pyomobook/overview-ch/wl_excel.txt @@ -1,4 +1,4 @@ -y : Size=3, Index=y_index +y : Size=3, Index={Harlingen, Memphis, Ashland} Key : Lower : Value : Upper : Fixed : Stale : Domain Ashland : 0 : 1.0 : 1 : False : False : Binary Harlingen : 0 : 1.0 : 1 : False : False : Binary diff --git a/examples/pyomobook/overview-ch/wl_list.py b/examples/pyomobook/overview-ch/wl_list.py index 64db76be548..37cba5a9595 100644 --- a/examples/pyomobook/overview-ch/wl_list.py +++ b/examples/pyomobook/overview-ch/wl_list.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # wl_list.py: Warehouse location problem using constraint lists import pyomo.environ as pyo diff --git a/examples/pyomobook/overview-ch/wl_list.txt b/examples/pyomobook/overview-ch/wl_list.txt index 2054efe153d..c0d44f1a0c9 100644 --- a/examples/pyomobook/overview-ch/wl_list.txt +++ b/examples/pyomobook/overview-ch/wl_list.txt @@ -1,25 +1,5 @@ -6 Set Declarations - demand_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 4 : {1, 2, 3, 4} - warehouse_active_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 12 : {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12} - x_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : x_index_0*x_index_1 : 12 : {('Harlingen', 'NYC'), ('Harlingen', 'LA'), ('Harlingen', 'Chicago'), ('Harlingen', 'Houston'), ('Memphis', 'NYC'), ('Memphis', 'LA'), ('Memphis', 'Chicago'), ('Memphis', 'Houston'), ('Ashland', 'NYC'), ('Ashland', 'LA'), ('Ashland', 'Chicago'), ('Ashland', 'Houston')} - x_index_0 : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'Harlingen', 'Memphis', 'Ashland'} - x_index_1 : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 4 : {'NYC', 'LA', 'Chicago', 'Houston'} - y_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'Harlingen', 'Memphis', 'Ashland'} - 2 Var Declarations - x : Size=12, Index=x_index + x : Size=12, Index={Harlingen, Memphis, Ashland}*{NYC, LA, Chicago, Houston} Key : Lower : Value : Upper : Fixed : Stale : Domain ('Ashland', 'Chicago') : 0 : None : 1 : False : True : Reals ('Ashland', 'Houston') : 0 : None : 1 : False : True : Reals @@ -33,7 +13,7 @@ ('Memphis', 'Houston') : 0 : None : 1 : False : True : Reals ('Memphis', 'LA') : 0 : None : 1 : False : True : Reals ('Memphis', 'NYC') : 0 : None : 1 : False : True : Reals - y : Size=3, Index=y_index + y : Size=3, Index={Harlingen, Memphis, Ashland} Key : Lower : Value : Upper : Fixed : Stale : Domain Ashland : 0 : None : 1 : False : True : Binary Harlingen : 0 : None : 1 : False : True : Binary @@ -45,7 +25,7 @@ None : True : minimize : 1956*x[Harlingen,NYC] + 1606*x[Harlingen,LA] + 1410*x[Harlingen,Chicago] + 330*x[Harlingen,Houston] + 1096*x[Memphis,NYC] + 1792*x[Memphis,LA] + 531*x[Memphis,Chicago] + 567*x[Memphis,Houston] + 485*x[Ashland,NYC] + 2322*x[Ashland,LA] + 324*x[Ashland,Chicago] + 1236*x[Ashland,Houston] 3 Constraint Declarations - demand : Size=4, Index=demand_index, Active=True + demand : Size=4, Index={1, 2, 3, 4}, Active=True Key : Lower : Body : Upper : Active 1 : 1.0 : x[Harlingen,NYC] + x[Memphis,NYC] + x[Ashland,NYC] : 1.0 : True 2 : 1.0 : x[Harlingen,LA] + x[Memphis,LA] + x[Ashland,LA] : 1.0 : True @@ -54,7 +34,7 @@ num_warehouses : Size=1, Index=None, Active=True Key : Lower : Body : Upper : Active None : -Inf : y[Harlingen] + y[Memphis] + y[Ashland] : 2.0 : True - warehouse_active : Size=12, Index=warehouse_active_index, Active=True + warehouse_active : Size=12, Index={1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, Active=True Key : Lower : Body : Upper : Active 1 : -Inf : x[Harlingen,NYC] - y[Harlingen] : 0.0 : True 2 : -Inf : x[Harlingen,LA] - y[Harlingen] : 0.0 : True @@ -69,4 +49,4 @@ 11 : -Inf : x[Ashland,Chicago] - y[Ashland] : 0.0 : True 12 : -Inf : x[Ashland,Houston] - y[Ashland] : 0.0 : True -12 Declarations: x_index_0 x_index_1 x_index x y_index y obj demand_index demand warehouse_active_index warehouse_active num_warehouses +6 Declarations: x y obj demand warehouse_active num_warehouses diff --git a/examples/pyomobook/overview-ch/wl_mutable.py b/examples/pyomobook/overview-ch/wl_mutable.py index e5c4f5e9dbb..8e129dd3c49 100644 --- a/examples/pyomobook/overview-ch/wl_mutable.py +++ b/examples/pyomobook/overview-ch/wl_mutable.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # wl_mutable.py: warehouse location problem with mutable param import pyomo.environ as pyo diff --git a/examples/pyomobook/overview-ch/wl_mutable_excel.py b/examples/pyomobook/overview-ch/wl_mutable_excel.py index 0906fbb25b3..935fa4963e5 100644 --- a/examples/pyomobook/overview-ch/wl_mutable_excel.py +++ b/examples/pyomobook/overview-ch/wl_mutable_excel.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # wl_mutable_excel.py: solve problem with different values for P import pandas import pyomo.environ as pyo diff --git a/examples/pyomobook/overview-ch/wl_scalar.py b/examples/pyomobook/overview-ch/wl_scalar.py index ac10fbe8265..6f538baedb8 100644 --- a/examples/pyomobook/overview-ch/wl_scalar.py +++ b/examples/pyomobook/overview-ch/wl_scalar.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # wl_scalar.py: snippets that show the warehouse location problem implemented as scalar quantities import pyomo.environ as pyo diff --git a/examples/pyomobook/performance-ch/SparseSets.py b/examples/pyomobook/performance-ch/SparseSets.py index 90d097b53aa..519808306de 100644 --- a/examples/pyomobook/performance-ch/SparseSets.py +++ b/examples/pyomobook/performance-ch/SparseSets.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/performance-ch/lin_expr.py b/examples/pyomobook/performance-ch/lin_expr.py index 75f4e70ec2a..af50ddd6228 100644 --- a/examples/pyomobook/performance-ch/lin_expr.py +++ b/examples/pyomobook/performance-ch/lin_expr.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo from pyomo.common.timing import TicTocTimer from pyomo.core.expr.numeric_expr import LinearExpression diff --git a/examples/pyomobook/performance-ch/persistent.py b/examples/pyomobook/performance-ch/persistent.py index 98207909cb6..67f8c656cfe 100644 --- a/examples/pyomobook/performance-ch/persistent.py +++ b/examples/pyomobook/performance-ch/persistent.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # @model: import pyomo.environ as pyo diff --git a/examples/pyomobook/performance-ch/wl.py b/examples/pyomobook/performance-ch/wl.py index 34c8a73f36e..000f81272a1 100644 --- a/examples/pyomobook/performance-ch/wl.py +++ b/examples/pyomobook/performance-ch/wl.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # wl.py # define a script to demonstrate performance profiling and improvements # @imports: import pyomo.environ as pyo # import pyomo environment diff --git a/examples/pyomobook/performance-ch/wl.txt b/examples/pyomobook/performance-ch/wl.txt index fbbd11fa32a..b762acf55cf 100644 --- a/examples/pyomobook/performance-ch/wl.txt +++ b/examples/pyomobook/performance-ch/wl.txt @@ -3,96 +3,102 @@ Building model 0 seconds to construct Block ConcreteModel; 1 index total 0 seconds to construct Set Any; 1 index total 0 seconds to construct Param P; 1 index total + 0 seconds to construct SetOf OrderedSetOf 0 seconds to construct Set OrderedScalarSet; 1 index total + 0 seconds to construct SetOf OrderedSetOf 0 seconds to construct Set OrderedScalarSet; 1 index total 0 seconds to construct Set SetProduct_OrderedSet; 1 index total - 0 seconds to construct Set SetProduct_OrderedSet; 1 index total - 0.15 seconds to construct Var x; 40000 indices total + 0.02 seconds to construct Var x; 40000 indices total + 0 seconds to construct SetOf OrderedSetOf 0 seconds to construct Set OrderedScalarSet; 1 index total 0 seconds to construct Var y; 200 indices total - 0.26 seconds to construct Objective obj; 1 index total + 0.14 seconds to construct Objective obj; 1 index total + 0 seconds to construct SetOf OrderedSetOf 0 seconds to construct Set OrderedScalarSet; 1 index total 0.13 seconds to construct Constraint demand; 200 indices total + 0 seconds to construct SetOf OrderedSetOf 0 seconds to construct Set OrderedScalarSet; 1 index total + 0 seconds to construct SetOf OrderedSetOf 0 seconds to construct Set OrderedScalarSet; 1 index total 0 seconds to construct Set SetProduct_OrderedSet; 1 index total - 0 seconds to construct Set SetProduct_OrderedSet; 1 index total - 0.82 seconds to construct Constraint warehouse_active; 40000 indices total + 0.50 seconds to construct Constraint warehouse_active; 40000 indices total 0 seconds to construct Constraint num_warehouses; 1 index total Building model with LinearExpression ------------------------------------ 0 seconds to construct Block ConcreteModel; 1 index total 0 seconds to construct Set Any; 1 index total 0 seconds to construct Param P; 1 index total + 0 seconds to construct SetOf OrderedSetOf 0 seconds to construct Set OrderedScalarSet; 1 index total + 0 seconds to construct SetOf OrderedSetOf 0 seconds to construct Set OrderedScalarSet; 1 index total 0 seconds to construct Set SetProduct_OrderedSet; 1 index total - 0 seconds to construct Set SetProduct_OrderedSet; 1 index total - 0.08 seconds to construct Var x; 40000 indices total + 0.02 seconds to construct Var x; 40000 indices total + 0 seconds to construct SetOf OrderedSetOf 0 seconds to construct Set OrderedScalarSet; 1 index total 0 seconds to construct Var y; 200 indices total - 0.33 seconds to construct Objective obj; 1 index total + 0.20 seconds to construct Objective obj; 1 index total + 0 seconds to construct SetOf OrderedSetOf 0 seconds to construct Set OrderedScalarSet; 1 index total - 0.13 seconds to construct Constraint demand; 200 indices total + 0.05 seconds to construct Constraint demand; 200 indices total + 0 seconds to construct SetOf OrderedSetOf 0 seconds to construct Set OrderedScalarSet; 1 index total + 0 seconds to construct SetOf OrderedSetOf 0 seconds to construct Set OrderedScalarSet; 1 index total 0 seconds to construct Set SetProduct_OrderedSet; 1 index total - 0 seconds to construct Set SetProduct_OrderedSet; 1 index total - 0.59 seconds to construct Constraint warehouse_active; 40000 indices total + 0.34 seconds to construct Constraint warehouse_active; 40000 indices total 0 seconds to construct Constraint num_warehouses; 1 index total [ 0.00] start -[+ 1.74] Built model -[+ 7.39] Wrote LP file and solved -[+ 11.36] finished parameter sweep - 14919301 function calls (14916699 primitive calls) in 15.948 seconds +[+ 1.00] Built model +[+ 2.28] Wrote LP file and solved +[+ 9.06] Finished parameter sweep + 7294708 function calls (7291012 primitive calls) in 10.989 seconds Ordered by: cumulative time - List reduced from 590 to 15 due to restriction <15> + List reduced from 673 to 15 due to restriction <15> ncalls tottime percall cumtime percall filename:lineno(function) - 1 0.002 0.002 15.948 15.948 /export/home/dlwoodruff/Documents/BookIII/trunk/pyomo/examples/doc/pyomobook/performance-ch/wl.py:112(solve_parametric) - 30 0.007 0.000 15.721 0.524 /export/home/dlwoodruff/software/pyomo/pyomo/opt/base/solvers.py:511(solve) - 30 0.001 0.000 9.150 0.305 /export/home/dlwoodruff/software/pyomo/pyomo/solvers/plugins/solvers/GUROBI.py:191(_presolve) - 30 0.001 0.000 9.149 0.305 /export/home/dlwoodruff/software/pyomo/pyomo/opt/solver/shellcmd.py:188(_presolve) - 30 0.001 0.000 9.134 0.304 /export/home/dlwoodruff/software/pyomo/pyomo/opt/base/solvers.py:651(_presolve) - 30 0.000 0.000 9.133 0.304 /export/home/dlwoodruff/software/pyomo/pyomo/opt/base/solvers.py:719(_convert_problem) - 30 0.002 0.000 9.133 0.304 /export/home/dlwoodruff/software/pyomo/pyomo/opt/base/convert.py:31(convert_problem) - 30 0.001 0.000 9.093 0.303 /export/home/dlwoodruff/software/pyomo/pyomo/solvers/plugins/converter/model.py:43(apply) - 30 0.001 0.000 9.080 0.303 /export/home/dlwoodruff/software/pyomo/pyomo/core/base/block.py:1756(write) - 30 0.008 0.000 9.077 0.303 /export/home/dlwoodruff/software/pyomo/pyomo/repn/plugins/cpxlp.py:81(__call__) - 30 1.308 0.044 9.065 0.302 /export/home/dlwoodruff/software/pyomo/pyomo/repn/plugins/cpxlp.py:377(_print_model_LP) - 30 0.002 0.000 5.016 0.167 /export/home/dlwoodruff/software/pyomo/pyomo/opt/solver/shellcmd.py:223(_apply_solver) - 30 0.002 0.000 5.013 0.167 /export/home/dlwoodruff/software/pyomo/pyomo/opt/solver/shellcmd.py:289(_execute_command) - 30 0.006 0.000 5.011 0.167 /export/home/dlwoodruff/software/pyutilib/pyutilib/subprocess/processmngr.py:433(run_command) - 30 0.001 0.000 4.388 0.146 /export/home/dlwoodruff/software/pyutilib/pyutilib/subprocess/processmngr.py:829(wait) + 1 0.001 0.001 10.989 10.989 pyomo/examples/pyomobook/performance-ch/wl.py:132(solve_parametric) + 30 0.002 0.000 10.913 0.364 pyomo/pyomo/opt/base/solvers.py:530(solve) + 30 0.001 0.000 7.816 0.261 pyomo/pyomo/opt/solver/shellcmd.py:247(_apply_solver) + 30 0.002 0.000 7.814 0.260 pyomo/pyomo/opt/solver/shellcmd.py:310(_execute_command) + 30 0.001 0.000 7.793 0.260 /lib/python3.11/subprocess.py:506(run) + 30 0.000 0.000 7.609 0.254 /lib/python3.11/subprocess.py:1165(communicate) + 60 0.000 0.000 7.608 0.127 /lib/python3.11/subprocess.py:1259(wait) + 60 0.000 0.000 7.608 0.127 /lib/python3.11/subprocess.py:2014(_wait) + 30 0.000 0.000 7.608 0.254 /lib/python3.11/subprocess.py:2001(_try_wait) + 30 7.607 0.254 7.607 0.254 {built-in method posix.waitpid} + 30 0.000 0.000 2.166 0.072 pyomo/pyomo/solvers/plugins/solvers/GUROBI.py:214(_presolve) + 30 0.000 0.000 2.166 0.072 pyomo/pyomo/opt/solver/shellcmd.py:215(_presolve) + 30 0.000 0.000 2.156 0.072 pyomo/pyomo/opt/base/solvers.py:687(_presolve) + 30 0.000 0.000 2.156 0.072 pyomo/pyomo/opt/base/solvers.py:754(_convert_problem) + 30 0.001 0.000 2.156 0.072 pyomo/pyomo/opt/base/convert.py:27(convert_problem) - 14919301 function calls (14916699 primitive calls) in 15.948 seconds + 7294708 function calls (7291012 primitive calls) in 10.989 seconds Ordered by: internal time - List reduced from 590 to 15 due to restriction <15> + List reduced from 673 to 15 due to restriction <15> ncalls tottime percall cumtime percall filename:lineno(function) - 30 4.381 0.146 4.381 0.146 {built-in method posix.waitpid} - 30 1.308 0.044 9.065 0.302 /export/home/dlwoodruff/software/pyomo/pyomo/repn/plugins/cpxlp.py:377(_print_model_LP) - 76560 0.703 0.000 1.165 0.000 /export/home/dlwoodruff/software/pyomo/pyomo/repn/plugins/cpxlp.py:178(_print_expr_canonical) - 76560 0.682 0.000 0.858 0.000 /export/home/dlwoodruff/software/pyomo/pyomo/repn/standard_repn.py:424(_collect_sum) - 30 0.544 0.018 0.791 0.026 /export/home/dlwoodruff/software/pyomo/pyomo/solvers/plugins/solvers/GUROBI.py:365(process_soln_file) - 76560 0.539 0.000 1.691 0.000 /export/home/dlwoodruff/software/pyomo/pyomo/repn/standard_repn.py:973(_generate_standard_repn) - 306000 0.507 0.000 0.893 0.000 /export/home/dlwoodruff/software/pyomo/pyomo/core/base/set.py:581(bounds) - 30 0.367 0.012 0.367 0.012 {built-in method posix.read} - 76560 0.323 0.000 2.291 0.000 /export/home/dlwoodruff/software/pyomo/pyomo/repn/standard_repn.py:245(generate_standard_repn) - 76560 0.263 0.000 2.923 0.000 /export/home/dlwoodruff/software/pyomo/pyomo/repn/plugins/cpxlp.py:569(constraint_generator) - 225090 0.262 0.000 0.336 0.000 /export/home/dlwoodruff/software/pyomo/pyomo/core/base/constraint.py:228(has_ub) - 153060 0.249 0.000 0.422 0.000 /export/home/dlwoodruff/software/pyomo/pyomo/core/expr/symbol_map.py:82(createSymbol) - 77220 0.220 0.000 0.457 0.000 {built-in method builtins.sorted} - 30 0.201 0.007 0.202 0.007 {built-in method _posixsubprocess.fork_exec} - 153000 0.185 0.000 0.690 0.000 /export/home/dlwoodruff/software/pyomo/pyomo/core/base/var.py:407(ub) + 30 7.607 0.254 7.607 0.254 {built-in method posix.waitpid} + 30 0.328 0.011 2.101 0.070 pyomo/pyomo/repn/plugins/lp_writer.py:250(write) + 76560 0.284 0.000 0.680 0.000 pyomo/pyomo/repn/plugins/lp_writer.py:576(write_expression) + 76560 0.220 0.000 0.388 0.000 pyomo/pyomo/repn/linear.py:664(_before_linear) + 30 0.209 0.007 0.438 0.015 pyomo/pyomo/solvers/plugins/solvers/GUROBI.py:394(process_soln_file) + 30 0.175 0.006 0.175 0.006 {built-in method _posixsubprocess.fork_exec} + 301530 0.134 0.000 0.181 0.000 pyomo/pyomo/core/expr/symbol_map.py:133(getSymbol) + 30 0.109 0.004 0.178 0.006 pyomo/pyomo/core/base/PyomoModel.py:461(select) + 77190 0.105 0.000 0.145 0.000 pyomo/pyomo/solvers/plugins/solvers/GUROBI.py:451() + 30 0.104 0.003 0.257 0.009 pyomo/pyomo/core/base/PyomoModel.py:337(add_solution) + 76530 0.081 0.000 0.109 0.000 pyomo/pyomo/core/expr/symbol_map.py:63(addSymbol) + 1062470 0.079 0.000 0.079 0.000 {built-in method builtins.id} + 76560 0.073 0.000 0.079 0.000 pyomo/pyomo/repn/linear.py:834(finalizeResult) + 239550 0.073 0.000 0.073 0.000 pyomo/pyomo/core/base/indexed_component.py:612(__getitem__) + 153150 0.070 0.000 0.179 0.000 pyomo/pyomo/core/base/block.py:1463(_component_data_itervalues) -[ 36.46] Resetting the tic/toc delta timer -Using license file /export/home/dlwoodruff/software/gurobi900/linux64/../lic/gurobi.lic -Academic license - for non-commercial use only -[+ 1.21] finished parameter sweep with persistent interface +[ 0.00] Resetting the tic/toc delta timer +[+ 0.49] Finished parameter sweep with persistent interface diff --git a/examples/pyomobook/pyomo-components-ch/con_declaration.py b/examples/pyomobook/pyomo-components-ch/con_declaration.py index 7775c1b26a0..0890ba4771b 100644 --- a/examples/pyomobook/pyomo-components-ch/con_declaration.py +++ b/examples/pyomobook/pyomo-components-ch/con_declaration.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/pyomo-components-ch/con_declaration.txt b/examples/pyomobook/pyomo-components-ch/con_declaration.txt index 019cd448eb0..b4709bd5490 100644 --- a/examples/pyomobook/pyomo-components-ch/con_declaration.txt +++ b/examples/pyomobook/pyomo-components-ch/con_declaration.txt @@ -1,10 +1,5 @@ -1 Set Declarations - x_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 2 : {1, 2} - 1 Var Declarations - x : Size=2, Index=x_index + x : Size=2, Index={1, 2} Key : Lower : Value : Upper : Fixed : Stale : Domain 1 : None : 1.0 : None : False : False : Reals 2 : None : 1.0 : None : False : False : Reals @@ -14,14 +9,9 @@ Key : Lower : Body : Upper : Active None : -Inf : x[2] - x[1] : 7.5 : True -3 Declarations: x_index x diff -1 Set Declarations - x_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 2 : {1, 2} - +2 Declarations: x diff 1 Var Declarations - x : Size=2, Index=x_index + x : Size=2, Index={1, 2} Key : Lower : Value : Upper : Fixed : Stale : Domain 1 : None : 1.0 : None : False : False : Reals 2 : None : 1.0 : None : False : False : Reals @@ -31,40 +21,24 @@ Key : Lower : Body : Upper : Active None : -Inf : x[2] - x[1] : 7.5 : True -3 Declarations: x_index x diff -2 Set Declarations - CoverConstr_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {1, 2, 3} - y_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {1, 2, 3} - +2 Declarations: x diff 1 Var Declarations - y : Size=3, Index=y_index + y : Size=3, Index={1, 2, 3} Key : Lower : Value : Upper : Fixed : Stale : Domain 1 : 0 : 0.0 : None : False : False : NonNegativeReals 2 : 0 : 0.0 : None : False : False : NonNegativeReals 3 : 0 : 0.0 : None : False : False : NonNegativeReals 1 Constraint Declarations - CoverConstr : Size=3, Index=CoverConstr_index, Active=True + CoverConstr : Size=3, Index={1, 2, 3}, Active=True Key : Lower : Body : Upper : Active 1 : 1.0 : y[1] : +Inf : True 2 : 2.9 : 3.1*y[2] : +Inf : True 3 : 3.1 : 4.5*y[3] : +Inf : True -4 Declarations: y_index y CoverConstr_index CoverConstr -2 Set Declarations - Pred_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 5 : {1, 2, 3, 4, 5} - StartTime_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 5 : {1, 2, 3, 4, 5} - +2 Declarations: y CoverConstr 1 Var Declarations - StartTime : Size=5, Index=StartTime_index + StartTime : Size=5, Index={1, 2, 3, 4, 5} Key : Lower : Value : Upper : Fixed : Stale : Domain 1 : None : 1.0 : None : False : False : Reals 2 : None : 1.0 : None : False : False : Reals @@ -73,14 +47,14 @@ 5 : None : 1.0 : None : False : False : Reals 1 Constraint Declarations - Pred : Size=4, Index=Pred_index, Active=True + Pred : Size=4, Index={1, 2, 3, 4, 5}, Active=True Key : Lower : Body : Upper : Active 1 : -Inf : StartTime[1] - StartTime[2] : 0.0 : True 2 : -Inf : StartTime[2] - StartTime[3] : 0.0 : True 3 : -Inf : StartTime[3] - StartTime[4] : 0.0 : True 4 : -Inf : StartTime[4] - StartTime[5] : 0.0 : True -4 Declarations: StartTime_index StartTime Pred_index Pred +2 Declarations: StartTime Pred 0.0 inf 7.5 diff --git a/examples/pyomobook/pyomo-components-ch/examples.py b/examples/pyomobook/pyomo-components-ch/examples.py index 6ba96792e28..1a59e9e308e 100644 --- a/examples/pyomobook/pyomo-components-ch/examples.py +++ b/examples/pyomobook/pyomo-components-ch/examples.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo print("indexed1") diff --git a/examples/pyomobook/pyomo-components-ch/examples.txt b/examples/pyomobook/pyomo-components-ch/examples.txt index 635b988cbcd..27ea1ba130b 100644 --- a/examples/pyomobook/pyomo-components-ch/examples.txt +++ b/examples/pyomobook/pyomo-components-ch/examples.txt @@ -1,20 +1,17 @@ indexed1 -3 Set Declarations +2 Set Declarations A : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {1, 2, 3} B : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 2 : {'Q', 'R'} - y_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*B : 6 : {(1, 'Q'), (1, 'R'), (2, 'Q'), (2, 'R'), (3, 'Q'), (3, 'R')} 2 Var Declarations x : Size=1, Index=None Key : Lower : Value : Upper : Fixed : Stale : Domain None : None : None : None : False : True : Reals - y : Size=6, Index=y_index + y : Size=6, Index=A*B Key : Lower : Value : Upper : Fixed : Stale : Domain (1, 'Q') : None : None : None : False : True : Reals (1, 'R') : None : None : None : False : True : Reals @@ -38,4 +35,4 @@ indexed1 2 : -Inf : 2*x : 0.0 : True 3 : -Inf : 3*x : 0.0 : True -8 Declarations: A B x y_index y o c d +7 Declarations: A B x y o c d diff --git a/examples/pyomobook/pyomo-components-ch/expr_declaration.py b/examples/pyomobook/pyomo-components-ch/expr_declaration.py index 8974a4d406a..da0d854e513 100644 --- a/examples/pyomobook/pyomo-components-ch/expr_declaration.py +++ b/examples/pyomobook/pyomo-components-ch/expr_declaration.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/pyomo-components-ch/expr_declaration.txt b/examples/pyomobook/pyomo-components-ch/expr_declaration.txt index 66c99f6502a..86e0feac27f 100644 --- a/examples/pyomobook/pyomo-components-ch/expr_declaration.txt +++ b/examples/pyomobook/pyomo-components-ch/expr_declaration.txt @@ -18,28 +18,20 @@ None : x + 2 3 Declarations: x e1 e2 -2 Set Declarations - e_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {1, 2, 3} - x_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {1, 2, 3} - 1 Var Declarations - x : Size=3, Index=x_index + x : Size=3, Index={1, 2, 3} Key : Lower : Value : Upper : Fixed : Stale : Domain 1 : None : None : None : False : True : Reals 2 : None : None : None : False : True : Reals 3 : None : None : None : False : True : Reals 1 Expression Declarations - e : Size=2, Index=e_index + e : Size=2, Index={1, 2, 3} Key : Expression 2 : x[2]**2 3 : x[3]**2 -4 Declarations: x_index x e_index e +2 Declarations: x e 1 Var Declarations x : Size=1, Index=None Key : Lower : Value : Upper : Fixed : Stale : Domain diff --git a/examples/pyomobook/pyomo-components-ch/obj_declaration.py b/examples/pyomobook/pyomo-components-ch/obj_declaration.py index 2c26c2b3363..a63fc441206 100644 --- a/examples/pyomobook/pyomo-components-ch/obj_declaration.py +++ b/examples/pyomobook/pyomo-components-ch/obj_declaration.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/pyomo-components-ch/obj_declaration.txt b/examples/pyomobook/pyomo-components-ch/obj_declaration.txt index e43134b8d92..e4d4b02a252 100644 --- a/examples/pyomobook/pyomo-components-ch/obj_declaration.txt +++ b/examples/pyomobook/pyomo-components-ch/obj_declaration.txt @@ -14,7 +14,7 @@ declexprrule Model unknown Variables: - x : Size=2, Index=x_index + x : Size=2, Index={1, 2} Key : Lower : Value : Upper : Fixed : Stale : Domain 1 : None : 1.0 : None : False : False : Reals 2 : None : 1.0 : None : False : False : Reals @@ -34,19 +34,19 @@ declskip Model unknown Variables: - x : Size=3, Index=x_index + x : Size=3, Index={Q, R, S} Key : Lower : Value : Upper : Fixed : Stale : Domain Q : None : 1.0 : None : False : False : Reals R : None : 1.0 : None : False : False : Reals S : None : 1.0 : None : False : False : Reals Objectives: - d : Size=3, Index=d_index, Active=True + d : Size=3, Index={Q, R, S}, Active=True Key : Active : Value Q : True : 1.0 R : True : 1.0 S : True : 1.0 - e : Size=2, Index=e_index, Active=True + e : Size=2, Index={Q, R, S}, Active=True Key : Active : Value Q : True : 1.0 S : True : 1.0 @@ -55,12 +55,12 @@ Model unknown None value x[Q] + 2*x[R] -1 +minimize 6.5 Model unknown Variables: - x : Size=2, Index=x_index + x : Size=2, Index={Q, R} Key : Lower : Value : Upper : Fixed : Stale : Domain Q : None : 1.5 : None : False : False : Reals R : None : 2.5 : None : False : False : Reals diff --git a/examples/pyomobook/pyomo-components-ch/param_declaration.py b/examples/pyomobook/pyomo-components-ch/param_declaration.py index a9d3256abfe..98b16548c28 100644 --- a/examples/pyomobook/pyomo-components-ch/param_declaration.py +++ b/examples/pyomobook/pyomo-components-ch/param_declaration.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/pyomo-components-ch/param_declaration.txt b/examples/pyomobook/pyomo-components-ch/param_declaration.txt index 9b8ce9cacdb..8c8a49eedc6 100644 --- a/examples/pyomobook/pyomo-components-ch/param_declaration.txt +++ b/examples/pyomobook/pyomo-components-ch/param_declaration.txt @@ -1,16 +1,13 @@ -3 Set Declarations +2 Set Declarations A : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {1, 2, 3} B : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 2 : {'A', 'B'} - T_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*B : 6 : {(1, 'A'), (1, 'B'), (2, 'A'), (2, 'B'), (3, 'A'), (3, 'B')} 3 Param Declarations - T : Size=3, Index=T_index, Domain=Any, Default=None, Mutable=False + T : Size=3, Index=A*B, Domain=Any, Default=None, Mutable=False Key : Value (1, 'A') : 10 (2, 'B') : 20 @@ -24,4 +21,4 @@ Key : Value None : 32 -6 Declarations: Z A B U T_index T +5 Declarations: Z A B U T diff --git a/examples/pyomobook/pyomo-components-ch/param_initialization.py b/examples/pyomobook/pyomo-components-ch/param_initialization.py index 11c257d2c31..88da8a68354 100644 --- a/examples/pyomobook/pyomo-components-ch/param_initialization.py +++ b/examples/pyomobook/pyomo-components-ch/param_initialization.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/pyomo-components-ch/param_initialization.txt b/examples/pyomobook/pyomo-components-ch/param_initialization.txt index d1ac6aba989..e0bcdf11a71 100644 --- a/examples/pyomobook/pyomo-components-ch/param_initialization.txt +++ b/examples/pyomobook/pyomo-components-ch/param_initialization.txt @@ -1,27 +1,15 @@ -6 Set Declarations +2 Set Declarations A : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {1, 2, 3} B : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {1, 2, 3} - T_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*B : 9 : {(1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3), (3, 1), (3, 2), (3, 3)} - U_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*A : 9 : {(1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3), (3, 1), (3, 2), (3, 3)} - XX_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*A : 9 : {(1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3), (3, 1), (3, 2), (3, 3)} - X_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*A : 9 : {(1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3), (3, 1), (3, 2), (3, 3)} 5 Param Declarations - T : Size=0, Index=T_index, Domain=Any, Default=None, Mutable=False + T : Size=0, Index=A*B, Domain=Any, Default=None, Mutable=False Key : Value - U : Size=9, Index=U_index, Domain=Any, Default=0, Mutable=False + U : Size=9, Index=A*A, Domain=Any, Default=0, Mutable=False Key : Value (1, 1) : 10 (2, 2) : 20 @@ -30,7 +18,7 @@ Key : Value 1 : 10 3 : 30 - X : Size=9, Index=X_index, Domain=Any, Default=None, Mutable=False + X : Size=9, Index=A*A, Domain=Any, Default=None, Mutable=False Key : Value (1, 1) : 1 (1, 2) : 2 @@ -41,7 +29,7 @@ (3, 1) : 3 (3, 2) : 6 (3, 3) : 9 - XX : Size=9, Index=XX_index, Domain=Any, Default=None, Mutable=False + XX : Size=9, Index=A*A, Domain=Any, Default=None, Mutable=False Key : Value (1, 1) : 1 (1, 2) : 2 @@ -53,7 +41,7 @@ (3, 2) : 8 (3, 3) : 14 -11 Declarations: A X_index X XX_index XX B W U_index U T_index T +7 Declarations: A X XX B W U T 2 3 False diff --git a/examples/pyomobook/pyomo-components-ch/param_misc.py b/examples/pyomobook/pyomo-components-ch/param_misc.py index baf76cc7c03..72fca60f787 100644 --- a/examples/pyomobook/pyomo-components-ch/param_misc.py +++ b/examples/pyomobook/pyomo-components-ch/param_misc.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo # @mutable1: diff --git a/examples/pyomobook/pyomo-components-ch/param_validation.py b/examples/pyomobook/pyomo-components-ch/param_validation.py index c82657c8d0f..baf5f0ac1e2 100644 --- a/examples/pyomobook/pyomo-components-ch/param_validation.py +++ b/examples/pyomobook/pyomo-components-ch/param_validation.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/pyomo-components-ch/rangeset.py b/examples/pyomobook/pyomo-components-ch/rangeset.py index d5e1015064c..169060e9ab2 100644 --- a/examples/pyomobook/pyomo-components-ch/rangeset.py +++ b/examples/pyomobook/pyomo-components-ch/rangeset.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/pyomo-components-ch/set_declaration.py b/examples/pyomobook/pyomo-components-ch/set_declaration.py index 1a507d4f588..bf3cfa1be15 100644 --- a/examples/pyomobook/pyomo-components-ch/set_declaration.py +++ b/examples/pyomobook/pyomo-components-ch/set_declaration.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/pyomo-components-ch/set_declaration.txt b/examples/pyomobook/pyomo-components-ch/set_declaration.txt index bdbb7376de4..a588e5601b6 100644 --- a/examples/pyomobook/pyomo-components-ch/set_declaration.txt +++ b/examples/pyomobook/pyomo-components-ch/set_declaration.txt @@ -5,22 +5,16 @@ 1 Declarations: A 0 Declarations: -4 Set Declarations - E : Size=1, Index=E_index, Ordered=Insertion +2 Set Declarations + E : Size=1, Index={1, 2, 3}, Ordered=Insertion Key : Dimen : Domain : Size : Members 2 : 1 : Any : 3 : {21, 22, 23} - E_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {1, 2, 3} - F : Size=2, Index=F_index, Ordered=Insertion + F : Size=2, Index={1, 2, 3}, Ordered=Insertion Key : Dimen : Domain : Size : Members 1 : 1 : Any : 3 : {11, 12, 13} 3 : 1 : Any : 3 : {31, 32, 33} - F_index : Size=1, Index=None, Ordered=False - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {1, 2, 3} -4 Declarations: E_index E F_index F +2 Declarations: E F 6 Set Declarations A : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members diff --git a/examples/pyomobook/pyomo-components-ch/set_initialization.py b/examples/pyomobook/pyomo-components-ch/set_initialization.py index 89dbaa713db..bdfd662c985 100644 --- a/examples/pyomobook/pyomo-components-ch/set_initialization.py +++ b/examples/pyomobook/pyomo-components-ch/set_initialization.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/pyomo-components-ch/set_initialization.txt b/examples/pyomobook/pyomo-components-ch/set_initialization.txt index af2ba54a8d2..29900ccb7b2 100644 --- a/examples/pyomobook/pyomo-components-ch/set_initialization.txt +++ b/examples/pyomobook/pyomo-components-ch/set_initialization.txt @@ -1,19 +1,16 @@ -10 Set Declarations +7 Set Declarations B : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {2, 3, 4} C : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 2 : Any : 2 : {(1, 4), (9, 16)} - F : Size=3, Index=F_index, Ordered=Insertion + F : Size=3, Index={2, 3, 4}, Ordered=Insertion Key : Dimen : Domain : Size : Members 2 : 1 : Any : 3 : {1, 3, 5} 3 : 1 : Any : 3 : {2, 4, 6} 4 : 1 : Any : 3 : {3, 5, 7} - F_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {2, 3, 4} - J : Size=9, Index=J_index, Ordered=Insertion + J : Size=9, Index=B*B, Ordered=Insertion Key : Dimen : Domain : Size : Members (2, 2) : 1 : Any : 4 : {0, 1, 2, 3} (2, 3) : 1 : Any : 6 : {0, 1, 2, 3, 4, 5} @@ -24,21 +21,15 @@ (4, 2) : 1 : Any : 8 : {0, 1, 2, 3, 4, 5, 6, 7} (4, 3) : 1 : Any : 12 : {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11} (4, 4) : 1 : Any : 16 : {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} - J_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : B*B : 9 : {(2, 2), (2, 3), (2, 4), (3, 2), (3, 3), (3, 4), (4, 2), (4, 3), (4, 4)} P : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 5 : {1, 2, 3, 5, 7} Q : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 4 : {4, 6, 8, 9} - R : Size=2, Index=R_index, Ordered=Insertion + R : Size=2, Index={1, 2, 3}, Ordered=Insertion Key : Dimen : Domain : Size : Members 1 : 1 : Any : 1 : {1,} 2 : 1 : Any : 2 : {1, 2} - R_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {1, 2, 3} -10 Declarations: B C F_index F J_index J P Q R_index R +7 Declarations: B C F J P Q R diff --git a/examples/pyomobook/pyomo-components-ch/set_misc.py b/examples/pyomobook/pyomo-components-ch/set_misc.py index 9a795b196b8..20ed9518f52 100644 --- a/examples/pyomobook/pyomo-components-ch/set_misc.py +++ b/examples/pyomobook/pyomo-components-ch/set_misc.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/pyomo-components-ch/set_options.py b/examples/pyomobook/pyomo-components-ch/set_options.py index 8d49882de2f..30c0b49706d 100644 --- a/examples/pyomobook/pyomo-components-ch/set_options.py +++ b/examples/pyomobook/pyomo-components-ch/set_options.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/pyomo-components-ch/set_validation.py b/examples/pyomobook/pyomo-components-ch/set_validation.py index a55dfc9ab7c..2300c0be693 100644 --- a/examples/pyomobook/pyomo-components-ch/set_validation.py +++ b/examples/pyomobook/pyomo-components-ch/set_validation.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/pyomo-components-ch/suffix_declaration.py b/examples/pyomobook/pyomo-components-ch/suffix_declaration.py index 650669ef5a6..619093712f1 100644 --- a/examples/pyomobook/pyomo-components-ch/suffix_declaration.py +++ b/examples/pyomobook/pyomo-components-ch/suffix_declaration.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo print('') diff --git a/examples/pyomobook/pyomo-components-ch/suffix_declaration.txt b/examples/pyomobook/pyomo-components-ch/suffix_declaration.txt index d5e38a44dcc..e0237e3ef46 100644 --- a/examples/pyomobook/pyomo-components-ch/suffix_declaration.txt +++ b/examples/pyomobook/pyomo-components-ch/suffix_declaration.txt @@ -1,9 +1,9 @@ *** suffixsimple *** 2 Suffix Declarations - dual : Direction=Suffix.IMPORT_EXPORT, Datatype=Suffix.FLOAT + dual : Direction=IMPORT_EXPORT, Datatype=FLOAT Key : Value - priority : Direction=Suffix.EXPORT, Datatype=Suffix.INT + priority : Direction=EXPORT, Datatype=INT Key : Value 2 Declarations: priority dual @@ -16,7 +16,7 @@ Not constructed 1 Suffix Declarations - foo : Direction=Suffix.LOCAL, Datatype=Suffix.FLOAT + foo : Direction=LOCAL, Datatype=FLOAT Not constructed 3 Declarations: x c foo diff --git a/examples/pyomobook/pyomo-components-ch/var_declaration.py b/examples/pyomobook/pyomo-components-ch/var_declaration.py index 538cbea1842..2ee5d7fb749 100644 --- a/examples/pyomobook/pyomo-components-ch/var_declaration.py +++ b/examples/pyomobook/pyomo-components-ch/var_declaration.py @@ -1,4 +1,14 @@ -from __future__ import print_function +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/python-ch/BadIndent.py b/examples/pyomobook/python-ch/BadIndent.py index 6ab545a6f46..4a00cae12ef 100644 --- a/examples/pyomobook/python-ch/BadIndent.py +++ b/examples/pyomobook/python-ch/BadIndent.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # This comment is the first line of BadIndent.py, # which will cause Python to give an error message # concerning indentation. diff --git a/examples/pyomobook/python-ch/LineExample.py b/examples/pyomobook/python-ch/LineExample.py index 0109a64167e..31cface5760 100644 --- a/examples/pyomobook/python-ch/LineExample.py +++ b/examples/pyomobook/python-ch/LineExample.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # This comment is the first line of LineExample.py # all characters on a line after the #-character are # ignored by Python diff --git a/examples/pyomobook/python-ch/class.py b/examples/pyomobook/python-ch/class.py index 562cef07ea7..a09f991d37b 100644 --- a/examples/pyomobook/python-ch/class.py +++ b/examples/pyomobook/python-ch/class.py @@ -1,25 +1,36 @@ -# class.py - - -# @all: -class IntLocker: - sint = None - - def __init__(self, i): - self.set_value(i) - - def set_value(self, i): - if type(i) is not int: - print("Error: %d is not integer." % i) - else: - self.sint = i - - def pprint(self): - print("The Int Locker has " + str(self.sint)) - - -a = IntLocker(3) -a.pprint() # prints: The Int Locker has 3 -a.set_value(5) -a.pprint() # prints: The Int Locker has 5 -# @:all +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +# class.py + + +# @all: +class IntLocker: + sint = None + + def __init__(self, i): + self.set_value(i) + + def set_value(self, i): + if type(i) is not int: + print("Error: %d is not integer." % i) + else: + self.sint = i + + def pprint(self): + print("The Int Locker has " + str(self.sint)) + + +a = IntLocker(3) +a.pprint() # prints: The Int Locker has 3 +a.set_value(5) +a.pprint() # prints: The Int Locker has 5 +# @:all diff --git a/examples/pyomobook/python-ch/ctob.py b/examples/pyomobook/python-ch/ctob.py index e418d27f103..8945e4863de 100644 --- a/examples/pyomobook/python-ch/ctob.py +++ b/examples/pyomobook/python-ch/ctob.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # An example of a silly decorator to change 'c' to 'b' # in the return value of a function. diff --git a/examples/pyomobook/python-ch/example.py b/examples/pyomobook/python-ch/example.py index 0a404add58d..184153545a3 100644 --- a/examples/pyomobook/python-ch/example.py +++ b/examples/pyomobook/python-ch/example.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # This is a comment line, which is ignored by Python print("Hello World") diff --git a/examples/pyomobook/python-ch/example2.py b/examples/pyomobook/python-ch/example2.py index da7d14e24ae..9a6a28bedbd 100644 --- a/examples/pyomobook/python-ch/example2.py +++ b/examples/pyomobook/python-ch/example2.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # A modified example.py program print("Hello World") diff --git a/examples/pyomobook/python-ch/functions.py b/examples/pyomobook/python-ch/functions.py index 7948c5e55df..97fb77edbe4 100644 --- a/examples/pyomobook/python-ch/functions.py +++ b/examples/pyomobook/python-ch/functions.py @@ -1,24 +1,35 @@ -# functions.py - - -# @all: -def Apply(f, a): - r = [] - for i in range(len(a)): - r.append(f(a[i])) - return r - - -def SqifOdd(x): - # if x is odd, 2*int(x/2) is not x - # due to integer divide of x/2 - if 2 * int(x / 2) == x: - return x - else: - return x * x - - -ShortList = range(4) -B = Apply(SqifOdd, ShortList) -print(B) -# @:all +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +# functions.py + + +# @all: +def Apply(f, a): + r = [] + for i in range(len(a)): + r.append(f(a[i])) + return r + + +def SqifOdd(x): + # if x is odd, 2*int(x/2) is not x + # due to integer divide of x/2 + if 2 * int(x / 2) == x: + return x + else: + return x * x + + +ShortList = range(4) +B = Apply(SqifOdd, ShortList) +print(B) +# @:all diff --git a/examples/pyomobook/python-ch/iterate.py b/examples/pyomobook/python-ch/iterate.py index 3a3422b2a09..50d74f93da7 100644 --- a/examples/pyomobook/python-ch/iterate.py +++ b/examples/pyomobook/python-ch/iterate.py @@ -1,18 +1,29 @@ -# iterate.py - -# @all: -D = {'Mary': 231} -D['Bob'] = 123 -D['Alice'] = 331 -D['Ted'] = 987 - -for i in sorted(D): - if i == 'Alice': - continue - if i == 'John': - print("Loop ends. Cleese alert!") - break - print(i + " " + str(D[i])) -else: - print("Cleese is not in the list.") -# @:all +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +# iterate.py + +# @all: +D = {'Mary': 231} +D['Bob'] = 123 +D['Alice'] = 331 +D['Ted'] = 987 + +for i in sorted(D): + if i == 'Alice': + continue + if i == 'John': + print("Loop ends. Cleese alert!") + break + print(i + " " + str(D[i])) +else: + print("Cleese is not in the list.") +# @:all diff --git a/examples/pyomobook/python-ch/pythonconditional.py b/examples/pyomobook/python-ch/pythonconditional.py index 205428e5ad1..a39e148622b 100644 --- a/examples/pyomobook/python-ch/pythonconditional.py +++ b/examples/pyomobook/python-ch/pythonconditional.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # pythonconditional.py # @all: diff --git a/examples/pyomobook/scripts-ch/attributes.py b/examples/pyomobook/scripts-ch/attributes.py index 643162082b6..c406bbf3e1c 100644 --- a/examples/pyomobook/scripts-ch/attributes.py +++ b/examples/pyomobook/scripts-ch/attributes.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import json import pyomo.environ as pyo from warehouse_model import create_wl_model diff --git a/examples/pyomobook/scripts-ch/prob_mod_ex.py b/examples/pyomobook/scripts-ch/prob_mod_ex.py index 6d610e9b44a..dceafe9d4f0 100644 --- a/examples/pyomobook/scripts-ch/prob_mod_ex.py +++ b/examples/pyomobook/scripts-ch/prob_mod_ex.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/scripts-ch/sudoku/sudoku.py b/examples/pyomobook/scripts-ch/sudoku/sudoku.py index ea0c0044e1d..8aa39f91203 100644 --- a/examples/pyomobook/scripts-ch/sudoku/sudoku.py +++ b/examples/pyomobook/scripts-ch/sudoku/sudoku.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo # create a standard python dict for mapping subsquares to diff --git a/examples/pyomobook/scripts-ch/sudoku/sudoku_run.py b/examples/pyomobook/scripts-ch/sudoku/sudoku_run.py index 266362308fa..b3f861f86b5 100644 --- a/examples/pyomobook/scripts-ch/sudoku/sudoku_run.py +++ b/examples/pyomobook/scripts-ch/sudoku/sudoku_run.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.opt import SolverFactory, TerminationCondition from sudoku import create_sudoku_model, print_solution, add_integer_cut diff --git a/examples/pyomobook/scripts-ch/value_expression.py b/examples/pyomobook/scripts-ch/value_expression.py index 51c07500ea8..00c79fec501 100644 --- a/examples/pyomobook/scripts-ch/value_expression.py +++ b/examples/pyomobook/scripts-ch/value_expression.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/scripts-ch/warehouse_cuts.py b/examples/pyomobook/scripts-ch/warehouse_cuts.py index c6516e796af..345dc5540cb 100644 --- a/examples/pyomobook/scripts-ch/warehouse_cuts.py +++ b/examples/pyomobook/scripts-ch/warehouse_cuts.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import warnings warnings.filterwarnings("ignore") diff --git a/examples/pyomobook/scripts-ch/warehouse_cuts.txt b/examples/pyomobook/scripts-ch/warehouse_cuts.txt index 9afe6c4e944..1f097e06cea 100644 --- a/examples/pyomobook/scripts-ch/warehouse_cuts.txt +++ b/examples/pyomobook/scripts-ch/warehouse_cuts.txt @@ -1,7 +1,7 @@ --- Solver Status: optimal --- Optimal Obj. Value = 2745.0 -y : Size=3, Index=y_index +y : Size=3, Index={Harlingen, Memphis, Ashland} Key : Lower : Value : Upper : Fixed : Stale : Domain Ashland : 0 : 1.0 : 1 : False : False : Binary Harlingen : 0 : 1.0 : 1 : False : False : Binary @@ -9,7 +9,7 @@ y : Size=3, Index=y_index --- Solver Status: optimal --- Optimal Obj. Value = 3168.0 -y : Size=3, Index=y_index +y : Size=3, Index={Harlingen, Memphis, Ashland} Key : Lower : Value : Upper : Fixed : Stale : Domain Ashland : 0 : 1.0 : 1 : False : False : Binary Harlingen : 0 : 0.0 : 1 : False : False : Binary @@ -17,7 +17,7 @@ y : Size=3, Index=y_index --- Solver Status: optimal --- Optimal Obj. Value = 3563.0 -y : Size=3, Index=y_index +y : Size=3, Index={Harlingen, Memphis, Ashland} Key : Lower : Value : Upper : Fixed : Stale : Domain Ashland : 0 : 0.0 : 1 : False : False : Binary Harlingen : 0 : 1.0 : 1 : False : False : Binary @@ -25,7 +25,7 @@ y : Size=3, Index=y_index --- Solver Status: optimal --- Optimal Obj. Value = 3986.0 -y : Size=3, Index=y_index +y : Size=3, Index={Harlingen, Memphis, Ashland} Key : Lower : Value : Upper : Fixed : Stale : Domain Ashland : 0 : 0.0 : 1 : False : False : Binary Harlingen : 0 : 0.0 : 1 : False : False : Binary @@ -33,7 +33,7 @@ y : Size=3, Index=y_index --- Solver Status: optimal --- Optimal Obj. Value = 4367.0 -y : Size=3, Index=y_index +y : Size=3, Index={Harlingen, Memphis, Ashland} Key : Lower : Value : Upper : Fixed : Stale : Domain Ashland : 0 : 1.0 : 1 : False : False : Binary Harlingen : 0 : 0.0 : 1 : False : False : Binary @@ -41,7 +41,7 @@ y : Size=3, Index=y_index --- Solver Status: optimal --- Optimal Obj. Value = 5302.0 -y : Size=3, Index=y_index +y : Size=3, Index={Harlingen, Memphis, Ashland} Key : Lower : Value : Upper : Fixed : Stale : Domain Ashland : 0 : 0.0 : 1 : False : False : Binary Harlingen : 0 : 1.0 : 1 : False : False : Binary diff --git a/examples/pyomobook/scripts-ch/warehouse_load_solutions.py b/examples/pyomobook/scripts-ch/warehouse_load_solutions.py index 790333a0e64..d38412f84df 100644 --- a/examples/pyomobook/scripts-ch/warehouse_load_solutions.py +++ b/examples/pyomobook/scripts-ch/warehouse_load_solutions.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import json import pyomo.environ as pyo from warehouse_model import create_wl_model diff --git a/examples/pyomobook/scripts-ch/warehouse_model.py b/examples/pyomobook/scripts-ch/warehouse_model.py index f5983d3cd89..149eb212759 100644 --- a/examples/pyomobook/scripts-ch/warehouse_model.py +++ b/examples/pyomobook/scripts-ch/warehouse_model.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo diff --git a/examples/pyomobook/scripts-ch/warehouse_print.py b/examples/pyomobook/scripts-ch/warehouse_print.py index e0e2f961345..8c862506bf0 100644 --- a/examples/pyomobook/scripts-ch/warehouse_print.py +++ b/examples/pyomobook/scripts-ch/warehouse_print.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import json import pyomo.environ as pyo from warehouse_model import create_wl_model diff --git a/examples/pyomobook/scripts-ch/warehouse_script.py b/examples/pyomobook/scripts-ch/warehouse_script.py index f2635a45d3d..617b8036abf 100644 --- a/examples/pyomobook/scripts-ch/warehouse_script.py +++ b/examples/pyomobook/scripts-ch/warehouse_script.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # @script: import json import pyomo.environ as pyo diff --git a/examples/pyomobook/scripts-ch/warehouse_script.txt b/examples/pyomobook/scripts-ch/warehouse_script.txt index b922643dd2b..fac3aef0880 100644 --- a/examples/pyomobook/scripts-ch/warehouse_script.txt +++ b/examples/pyomobook/scripts-ch/warehouse_script.txt @@ -1,36 +1,10 @@ -y : Size=3, Index=y_index +y : Size=3, Index={Harlingen, Memphis, Ashland} Key : Lower : Value : Upper : Fixed : Stale : Domain Ashland : 0 : 1.0 : 1 : False : False : Binary Harlingen : 0 : 1.0 : 1 : False : False : Binary Memphis : 0 : 0.0 : 1 : False : False : Binary -8 Set Declarations - one_per_cust_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 4 : {'NYC', 'LA', 'Chicago', 'Houston'} - warehouse_active_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : warehouse_active_index_0*warehouse_active_index_1 : 12 : {('Harlingen', 'NYC'), ('Harlingen', 'LA'), ('Harlingen', 'Chicago'), ('Harlingen', 'Houston'), ('Memphis', 'NYC'), ('Memphis', 'LA'), ('Memphis', 'Chicago'), ('Memphis', 'Houston'), ('Ashland', 'NYC'), ('Ashland', 'LA'), ('Ashland', 'Chicago'), ('Ashland', 'Houston')} - warehouse_active_index_0 : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'Harlingen', 'Memphis', 'Ashland'} - warehouse_active_index_1 : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 4 : {'NYC', 'LA', 'Chicago', 'Houston'} - x_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : x_index_0*x_index_1 : 12 : {('Harlingen', 'NYC'), ('Harlingen', 'LA'), ('Harlingen', 'Chicago'), ('Harlingen', 'Houston'), ('Memphis', 'NYC'), ('Memphis', 'LA'), ('Memphis', 'Chicago'), ('Memphis', 'Houston'), ('Ashland', 'NYC'), ('Ashland', 'LA'), ('Ashland', 'Chicago'), ('Ashland', 'Houston')} - x_index_0 : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'Harlingen', 'Memphis', 'Ashland'} - x_index_1 : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 4 : {'NYC', 'LA', 'Chicago', 'Houston'} - y_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'Harlingen', 'Memphis', 'Ashland'} - 2 Var Declarations - x : Size=12, Index=x_index + x : Size=12, Index={Harlingen, Memphis, Ashland}*{NYC, LA, Chicago, Houston} Key : Lower : Value : Upper : Fixed : Stale : Domain ('Ashland', 'Chicago') : 0 : 1.0 : 1 : False : False : Reals ('Ashland', 'Houston') : 0 : 0.0 : 1 : False : False : Reals @@ -40,11 +14,11 @@ y : Size=3, Index=y_index ('Harlingen', 'Houston') : 0 : 1.0 : 1 : False : False : Reals ('Harlingen', 'LA') : 0 : 1.0 : 1 : False : False : Reals ('Harlingen', 'NYC') : 0 : 0.0 : 1 : False : False : Reals - ('Memphis', 'Chicago') : 0 : -0.0 : 1 : False : False : Reals + ('Memphis', 'Chicago') : 0 : 0.0 : 1 : False : False : Reals ('Memphis', 'Houston') : 0 : 0.0 : 1 : False : False : Reals ('Memphis', 'LA') : 0 : 0.0 : 1 : False : False : Reals ('Memphis', 'NYC') : 0 : 0.0 : 1 : False : False : Reals - y : Size=3, Index=y_index + y : Size=3, Index={Harlingen, Memphis, Ashland} Key : Lower : Value : Upper : Fixed : Stale : Domain Ashland : 0 : 1.0 : 1 : False : False : Binary Harlingen : 0 : 1.0 : 1 : False : False : Binary @@ -59,13 +33,13 @@ y : Size=3, Index=y_index num_warehouses : Size=1, Index=None, Active=True Key : Lower : Body : Upper : Active None : -Inf : y[Harlingen] + y[Memphis] + y[Ashland] : 2.0 : True - one_per_cust : Size=4, Index=one_per_cust_index, Active=True + one_per_cust : Size=4, Index={NYC, LA, Chicago, Houston}, Active=True Key : Lower : Body : Upper : Active Chicago : 1.0 : x[Harlingen,Chicago] + x[Memphis,Chicago] + x[Ashland,Chicago] : 1.0 : True Houston : 1.0 : x[Harlingen,Houston] + x[Memphis,Houston] + x[Ashland,Houston] : 1.0 : True LA : 1.0 : x[Harlingen,LA] + x[Memphis,LA] + x[Ashland,LA] : 1.0 : True NYC : 1.0 : x[Harlingen,NYC] + x[Memphis,NYC] + x[Ashland,NYC] : 1.0 : True - warehouse_active : Size=12, Index=warehouse_active_index, Active=True + warehouse_active : Size=12, Index={Harlingen, Memphis, Ashland}*{NYC, LA, Chicago, Houston}, Active=True Key : Lower : Body : Upper : Active ('Ashland', 'Chicago') : -Inf : x[Ashland,Chicago] - y[Ashland] : 0.0 : True ('Ashland', 'Houston') : -Inf : x[Ashland,Houston] - y[Ashland] : 0.0 : True @@ -80,4 +54,4 @@ y : Size=3, Index=y_index ('Memphis', 'LA') : -Inf : x[Memphis,LA] - y[Memphis] : 0.0 : True ('Memphis', 'NYC') : -Inf : x[Memphis,NYC] - y[Memphis] : 0.0 : True -14 Declarations: x_index_0 x_index_1 x_index x y_index y obj one_per_cust_index one_per_cust warehouse_active_index_0 warehouse_active_index_1 warehouse_active_index warehouse_active num_warehouses +6 Declarations: x y obj one_per_cust warehouse_active num_warehouses diff --git a/examples/pyomobook/scripts-ch/warehouse_solver_options.py b/examples/pyomobook/scripts-ch/warehouse_solver_options.py index c8eaf11a0f3..4e79e158d50 100644 --- a/examples/pyomobook/scripts-ch/warehouse_solver_options.py +++ b/examples/pyomobook/scripts-ch/warehouse_solver_options.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # @script: import json import pyomo.environ as pyo diff --git a/examples/pyomobook/strip_examples.py b/examples/pyomobook/strip_examples.py index 0a65eef7c04..84017299fb6 100644 --- a/examples/pyomobook/strip_examples.py +++ b/examples/pyomobook/strip_examples.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import glob import sys import os diff --git a/examples/pyomobook/test_book_examples.py b/examples/pyomobook/test_book_examples.py index 1aa2a3ed6b3..192330dc1bf 100644 --- a/examples/pyomobook/test_book_examples.py +++ b/examples/pyomobook/test_book_examples.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -10,541 +10,146 @@ # ___________________________________________________________________________ import pyomo.common.unittest as unittest -import filecmp import glob import os -import os.path -import re -import subprocess -import sys -from itertools import zip_longest -from pyomo.opt import check_available_solvers -from pyomo.common.dependencies import attempt_import, check_min_version -from pyomo.common.fileutils import this_file_dir, import_file -from pyomo.common.log import LoggingIntercept, pyomo_formatter -from pyomo.common.tee import capture_output +from pyomo.common.dependencies import attempt_import, matplotlib_available +from pyomo.common.fileutils import this_file_dir import pyomo.environ as pyo -def gurobi_fully_licensed(): - m = pyo.ConcreteModel() - m.x = pyo.Var(list(range(2001)), within=pyo.NonNegativeReals) - m.o = pyo.Objective(expr=sum(m.x.values())) - try: - results = pyo.SolverFactory('gurobi').solve(m, tee=True) - pyo.assert_optimal_termination(results) - return True - except: - return False - +currdir = this_file_dir() parameterized, param_available = attempt_import('parameterized') if not param_available: raise unittest.SkipTest('Parameterized is not available.') -# Needed for testing (switches the matplotlib backend): -from pyomo.common.dependencies import matplotlib_available - +# Needed for testing (triggers matplotlib import and switches its backend): bool(matplotlib_available) -# Find all *.txt files, and use them to define baseline tests -currdir = this_file_dir() -datadir = currdir - -solver_dependencies = { - # abstract_ch - 'test_abstract_ch_wl_abstract_script': ['glpk'], - 'test_abstract_ch_pyomo_wl_abstract': ['glpk'], - 'test_abstract_ch_pyomo_solve1': ['glpk'], - 'test_abstract_ch_pyomo_solve2': ['glpk'], - 'test_abstract_ch_pyomo_solve3': ['glpk'], - 'test_abstract_ch_pyomo_solve4': ['glpk'], - 'test_abstract_ch_pyomo_solve5': ['glpk'], - 'test_abstract_ch_pyomo_diet1': ['glpk'], - 'test_abstract_ch_pyomo_buildactions_works': ['glpk'], - 'test_abstract_ch_pyomo_abstract5_ns1': ['glpk'], - 'test_abstract_ch_pyomo_abstract5_ns2': ['glpk'], - 'test_abstract_ch_pyomo_abstract5_ns3': ['glpk'], - 'test_abstract_ch_pyomo_abstract6': ['glpk'], - 'test_abstract_ch_pyomo_abstract7': ['glpk'], - 'test_abstract_ch_pyomo_AbstractH': ['ipopt'], - 'test_abstract_ch_AbstHLinScript': ['glpk'], - 'test_abstract_ch_pyomo_AbstractHLinear': ['glpk'], - # blocks_ch - 'test_blocks_ch_lotsizing': ['glpk'], - 'test_blocks_ch_blocks_lotsizing': ['glpk'], - # dae_ch - 'test_dae_ch_run_path_constraint_tester': ['ipopt'], - # gdp_ch - 'test_gdp_ch_pyomo_gdp_uc': ['glpk'], - 'test_gdp_ch_pyomo_scont': ['glpk'], - 'test_gdp_ch_pyomo_scont2': ['glpk'], - 'test_gdp_ch_scont_script': ['glpk'], - # intro_ch' - 'test_intro_ch_pyomo_concrete1_generic': ['glpk'], - 'test_intro_ch_pyomo_concrete1': ['glpk'], - 'test_intro_ch_pyomo_coloring_concrete': ['glpk'], - 'test_intro_ch_pyomo_abstract5': ['glpk'], - # mpec_ch - 'test_mpec_ch_path1': ['path'], - 'test_mpec_ch_nlp_ex1b': ['ipopt'], - 'test_mpec_ch_nlp_ex1c': ['ipopt'], - 'test_mpec_ch_nlp_ex1d': ['ipopt'], - 'test_mpec_ch_nlp_ex1e': ['ipopt'], - 'test_mpec_ch_nlp_ex2': ['ipopt'], - 'test_mpec_ch_nlp1': ['ipopt'], - 'test_mpec_ch_nlp2': ['ipopt'], - 'test_mpec_ch_nlp3': ['ipopt'], - 'test_mpec_ch_mip1': ['glpk'], - # nonlinear_ch - 'test_rosen_rosenbrock': ['ipopt'], - 'test_react_design_ReactorDesign': ['ipopt'], - 'test_react_design_ReactorDesignTable': ['ipopt'], - 'test_multimodal_multimodal_init1': ['ipopt'], - 'test_multimodal_multimodal_init2': ['ipopt'], - 'test_disease_est_disease_estimation': ['ipopt'], - 'test_deer_DeerProblem': ['ipopt'], - # scripts_ch - 'test_sudoku_sudoku_run': ['glpk'], - 'test_scripts_ch_warehouse_script': ['glpk'], - 'test_scripts_ch_warehouse_print': ['glpk'], - 'test_scripts_ch_warehouse_cuts': ['glpk'], - 'test_scripts_ch_prob_mod_ex': ['glpk'], - 'test_scripts_ch_attributes': ['glpk'], - # optimization_ch - 'test_optimization_ch_ConcHLinScript': ['glpk'], - # overview_ch - 'test_overview_ch_wl_mutable_excel': ['glpk'], - 'test_overview_ch_wl_excel': ['glpk'], - 'test_overview_ch_wl_concrete_script': ['glpk'], - 'test_overview_ch_wl_abstract_script': ['glpk'], - 'test_overview_ch_pyomo_wl_abstract': ['glpk'], - # performance_ch - 'test_performance_ch_wl': ['gurobi', 'gurobi_persistent', 'gurobi_license'], - 'test_performance_ch_persistent': ['gurobi_persistent'], -} -package_dependencies = { - # abstract_ch' - 'test_abstract_ch_pyomo_solve4': ['yaml'], - 'test_abstract_ch_pyomo_solve5': ['yaml'], - # gdp_ch - 'test_gdp_ch_pyomo_scont': ['yaml'], - 'test_gdp_ch_pyomo_scont2': ['yaml'], - 'test_gdp_ch_pyomo_gdp_uc': ['sympy'], - # overview_ch' - 'test_overview_ch_wl_excel': ['pandas', 'xlrd'], - 'test_overview_ch_wl_mutable_excel': ['pandas', 'xlrd'], - # scripts_ch' - 'test_scripts_ch_warehouse_cuts': ['matplotlib'], - # performance_ch' - 'test_performance_ch_wl': ['numpy', 'matplotlib'], -} - -# -# Initialize the availability data -# -solvers_used = set(sum(list(solver_dependencies.values()), [])) -available_solvers = check_available_solvers(*solvers_used) -if gurobi_fully_licensed(): - available_solvers.append('gurobi_license') -solver_available = {solver_: (solver_ in available_solvers) for solver_ in solvers_used} - -package_available = {} -package_modules = {} -packages_used = set(sum(list(package_dependencies.values()), [])) -for package_ in packages_used: - pack, pack_avail = attempt_import(package_) - package_available[package_] = pack_avail - package_modules[package_] = pack - - -def check_skip(name): - """ - Return a boolean if the test should be skipped - """ - - if name in solver_dependencies: - solvers_ = solver_dependencies[name] - if not all([solver_available[i] for i in solvers_]): - # Skip the test because a solver is not available - _missing = [] - for i in solvers_: - if not solver_available[i]: - _missing.append(i) - return "Solver%s %s %s not available" % ( - 's' if len(_missing) > 1 else '', - ", ".join(_missing), - 'are' if len(_missing) > 1 else 'is', - ) - - if name in package_dependencies: - packages_ = package_dependencies[name] - if not all([package_available[i] for i in packages_]): - # Skip the test because a package is not available - _missing = [] - for i in packages_: - if not package_available[i]: - _missing.append(i) - return "Package%s %s %s not available" % ( - 's' if len(_missing) > 1 else '', - ", ".join(_missing), - 'are' if len(_missing) > 1 else 'is', - ) - - # This is a hack, xlrd dropped support for .xlsx files in 2.0.1 which - # causes problems with older versions of Pandas<=1.1.5 so skipping - # tests requiring both these packages when incompatible versions are found - if ( - 'pandas' in package_dependencies[name] - and 'xlrd' in package_dependencies[name] - ): - if check_min_version( - package_modules['xlrd'], '2.0.1' - ) and not check_min_version(package_modules['pandas'], '1.1.6'): - return "Incompatible versions of xlrd and pandas" - return False - - -def filter(line): - """ - Ignore certain text when comparing output with baseline - """ - for field in ( - '[', - 'password:', - 'http:', - 'Job ', - 'Importing module', - 'Function', - 'File', - 'Matplotlib', - '-------', - '=======', - ' ^', - ): - if line.startswith(field): - return True - for field in ( - 'Total CPU', - 'Ipopt', - 'license', - 'Status: optimal', - 'Status: feasible', - 'time:', - 'Time:', - 'with format cpxlp', - 'usermodel = >> from pyomo.__future__ import solver_factory + >>> solver_factory() + 1 + + The active factory can be set either by passing the appropriate + version to this function: + + .. doctest:: + + >>> solver_factory(3) + + + or by importing the "special" name: + + .. doctest:: + + >>> from pyomo.__future__ import solver_factory_v3 + + .. doctest:: + :hide: + + >>> from pyomo.__future__ import solver_factory_v1 + + """ + import pyomo.opt.base.solvers as _solvers + import pyomo.contrib.solver.factory as _contrib + import pyomo.contrib.appsi.base as _appsi + + versions = { + 1: _solvers.LegacySolverFactory, + 2: _appsi.SolverFactory, + 3: _contrib.SolverFactory, + } + + current = getattr(solver_factory, '_active_version', None) + # First time through, _active_version is not defined. Go look and + # see what it was initialized to in pyomo.environ + if current is None: + for ver, cls in versions.items(): + if cls._cls is _environ.SolverFactory._cls: + solver_factory._active_version = ver + break + return solver_factory._active_version + # + # The user is just asking what the current SolverFactory is; tell them. + if version is None: + return solver_factory._active_version + # + # Update the current SolverFactory to be a shim around (shallow copy + # of) the new active factory + src = versions.get(version, None) + if version is not None: + solver_factory._active_version = version + for attr in ('_description', '_cls', '_doc'): + setattr(_environ.SolverFactory, attr, getattr(src, attr)) + else: + raise ValueError( + "Invalid value for target solver factory version; expected {1, 2, 3}, " + f"received {version}" + ) + return src + + +solver_factory._active_version = solver_factory() diff --git a/pyomo/__init__.py b/pyomo/__init__.py index 20ee59d48b2..14cc42b626e 100644 --- a/pyomo/__init__.py +++ b/pyomo/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/__init__.py b/pyomo/common/__init__.py index 563974b5617..d7297c067c9 100644 --- a/pyomo/common/__init__.py +++ b/pyomo/common/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/_command.py b/pyomo/common/_command.py index ae633648ace..ad521659aa7 100644 --- a/pyomo/common/_command.py +++ b/pyomo/common/_command.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -13,8 +13,6 @@ Management of Pyomo commands """ -__all__ = ['pyomo_command', 'get_pyomo_commands'] - import logging logger = logging.getLogger('pyomo.common') diff --git a/pyomo/common/_common.py b/pyomo/common/_common.py index 21a5ddcc7bc..0d50f74537a 100644 --- a/pyomo/common/_common.py +++ b/pyomo/common/_common.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/autoslots.py b/pyomo/common/autoslots.py index 1b55a818b83..89fefaf4f21 100644 --- a/pyomo/common/autoslots.py +++ b/pyomo/common/autoslots.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -29,7 +29,7 @@ def _deepcopy_tuple(obj, memo, _id): unchanged = False if unchanged: # Python does not duplicate "unchanged" tuples (i.e. allows the - # original objecct to be returned from deepcopy()). We will + # original object to be returned from deepcopy()). We will # preserve that behavior here. # # It also appears to be faster *not* to cache the fact that this diff --git a/pyomo/common/backports.py b/pyomo/common/backports.py index 0854715baeb..e70b0f6d267 100644 --- a/pyomo/common/backports.py +++ b/pyomo/common/backports.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -11,6 +11,4 @@ from pyomo.common.deprecation import relocated_module_attribute -relocated_module_attribute( - 'nullcontext', 'contextlib.nullcontext', version='6.7.0.dev0' -) +relocated_module_attribute('nullcontext', 'contextlib.nullcontext', version='6.7.0') diff --git a/pyomo/common/cmake_builder.py b/pyomo/common/cmake_builder.py index 71358c29fb2..523dbf64c91 100644 --- a/pyomo/common/cmake_builder.py +++ b/pyomo/common/cmake_builder.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -32,11 +32,8 @@ def handleReadonly(function, path, excinfo): def build_cmake_project( targets, package_name=None, description=None, user_args=[], parallel=None ): - # Note: setuptools must be imported before distutils to avoid - # warnings / errors with recent setuptools distributions - from setuptools import Extension - import distutils.core - from distutils.command.build_ext import build_ext + from setuptools import Extension, Distribution + from setuptools.command.build_ext import build_ext class _CMakeBuild(build_ext, object): def run(self): @@ -122,7 +119,7 @@ def __init__(self, target_dir, user_args, parallel): 'ext_modules': ext_modules, 'cmdclass': {'build_ext': _CMakeBuild}, } - dist = distutils.core.Distribution(package_config) + dist = Distribution(package_config) basedir = os.path.abspath(os.path.curdir) try: tmpdir = os.path.abspath(tempfile.mkdtemp()) diff --git a/pyomo/common/collections/__init__.py b/pyomo/common/collections/__init__.py index 9ffd1e931f6..717caf87b2c 100644 --- a/pyomo/common/collections/__init__.py +++ b/pyomo/common/collections/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -14,6 +14,6 @@ from collections import UserDict from .orderedset import OrderedDict, OrderedSet -from .component_map import ComponentMap +from .component_map import ComponentMap, DefaultComponentMap from .component_set import ComponentSet from .bunch import Bunch diff --git a/pyomo/common/collections/bunch.py b/pyomo/common/collections/bunch.py index f19e4ad64e3..2ae9cf8c517 100644 --- a/pyomo/common/collections/bunch.py +++ b/pyomo/common/collections/bunch.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/collections/component_map.py b/pyomo/common/collections/component_map.py index ceb4174ecca..8dcfdb6c837 100644 --- a/pyomo/common/collections/component_map.py +++ b/pyomo/common/collections/component_map.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,21 +9,49 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from collections.abc import MutableMapping as collections_MutableMapping +import collections from collections.abc import Mapping as collections_Mapping from pyomo.common.autoslots import AutoSlots -def _rebuild_ids(encode, val): +def _rehash_keys(encode, val): if encode: return val else: # object id() may have changed after unpickling, # so we rebuild the dictionary keys - return {id(obj): (obj, v) for obj, v in val.values()} + return {_hasher[obj.__class__](obj): (obj, v) for obj, v in val.values()} -class ComponentMap(AutoSlots.Mixin, collections_MutableMapping): +class _Hasher(collections.defaultdict): + def __init__(self, *args, **kwargs): + super().__init__(lambda: self._missing_impl, *args, **kwargs) + self[tuple] = self._tuple + + def _missing_impl(self, val): + try: + hash(val) + self[val.__class__] = self._hashable + except: + self[val.__class__] = self._unhashable + return self[val.__class__](val) + + @staticmethod + def _hashable(val): + return val + + @staticmethod + def _unhashable(val): + return id(val) + + def _tuple(self, val): + return tuple(self[i.__class__](i) for i in val) + + +_hasher = _Hasher() + + +class ComponentMap(AutoSlots.Mixin, collections.abc.MutableMapping): """ This class is a replacement for dict that allows Pyomo modeling components to be used as entry keys. The @@ -49,18 +77,18 @@ class ComponentMap(AutoSlots.Mixin, collections_MutableMapping): """ __slots__ = ("_dict",) - __autoslot_mappers__ = {'_dict': _rebuild_ids} + __autoslot_mappers__ = {'_dict': _rehash_keys} def __init__(self, *args, **kwds): - # maps id(obj) -> (obj,val) + # maps id_hash(obj) -> (obj,val) self._dict = {} # handle the dict-style initialization scenarios self.update(*args, **kwds) def __str__(self): """String representation of the mapping.""" - tmp = {str(c) + " (id=" + str(id(c)) + ")": v for c, v in self.items()} - return "ComponentMap(" + str(tmp) + ")" + tmp = {f"{v[0]} (key={k})": v[1] for k, v in self._dict.items()} + return f"ComponentMap({tmp})" # # Implement MutableMapping abstract methods @@ -68,18 +96,20 @@ def __str__(self): def __getitem__(self, obj): try: - return self._dict[id(obj)][1] + return self._dict[_hasher[obj.__class__](obj)][1] except KeyError: - raise KeyError("Component with id '%s': %s" % (id(obj), str(obj))) + _id = _hasher[obj.__class__](obj) + raise KeyError(f"{obj} (key={_id})") from None def __setitem__(self, obj, val): - self._dict[id(obj)] = (obj, val) + self._dict[_hasher[obj.__class__](obj)] = (obj, val) def __delitem__(self, obj): try: - del self._dict[id(obj)] + del self._dict[_hasher[obj.__class__](obj)] except KeyError: - raise KeyError("Component with id '%s': %s" % (id(obj), str(obj))) + _id = _hasher[obj.__class__](obj) + raise KeyError(f"{obj} (key={_id})") from None def __iter__(self): return (obj for obj, val in self._dict.values()) @@ -91,16 +121,32 @@ def __len__(self): # Overload MutableMapping default implementations # - # We want to avoid generating Pyomo expressions due to - # comparison of values, so we convert both objects to a - # plain dictionary mapping key->(type(val), id(val)) and - # compare that instead. + # We want a specialization of update() to avoid unnecessary calls to + # id() when copying / merging ComponentMaps + def update(self, *args, **kwargs): + if len(args) == 1 and not kwargs and isinstance(args[0], ComponentMap): + return self._dict.update(args[0]._dict) + return super().update(*args, **kwargs) + + # We want to avoid generating Pyomo expressions due to comparing the + # keys, so look up each entry from other in this dict. def __eq__(self, other): - if not isinstance(other, collections_Mapping): + if self is other: + return True + if not isinstance(other, collections_Mapping) or len(self) != len(other): return False - return {(type(key), id(key)): val for key, val in self.items()} == { - (type(key), id(key)): val for key, val in other.items() - } + # Note we have already verified the dicts are the same size + for key, val in other.items(): + other_id = _hasher[key.__class__](key) + if other_id not in self._dict: + return False + self_val = self._dict[other_id][1] + # Note: check "is" first to help avoid creation of Pyomo + # expressions (for the case that the values contain the same + # Pyomo component) + if self_val is not val and self_val != val: + return False + return True def __ne__(self, other): return not (self == other) @@ -114,7 +160,7 @@ def __ne__(self, other): # def __contains__(self, obj): - return id(obj) in self._dict + return _hasher[obj.__class__](obj) in self._dict def clear(self): 'D.clear() -> None. Remove all items from D.' @@ -133,3 +179,32 @@ def setdefault(self, key, default=None): else: self[key] = default return default + + +class DefaultComponentMap(ComponentMap): + """A :py:class:`defaultdict` admitting Pyomo Components as keys + + This class is a replacement for defaultdict that allows Pyomo + modeling components to be used as entry keys. The base + implementation builds on :py:class:`ComponentMap`. + + """ + + __slots__ = ('default_factory',) + + def __init__(self, default_factory=None, *args, **kwargs): + super().__init__(*args, **kwargs) + self.default_factory = default_factory + + def __missing__(self, key): + if self.default_factory is None: + raise KeyError(key) + self[key] = ans = self.default_factory() + return ans + + def __getitem__(self, obj): + _key = _hasher[obj.__class__](obj) + if _key in self._dict: + return self._dict[_key][1] + else: + return self.__missing__(obj) diff --git a/pyomo/common/collections/component_set.py b/pyomo/common/collections/component_set.py index 0b16acd00be..6e12bad7277 100644 --- a/pyomo/common/collections/component_set.py +++ b/pyomo/common/collections/component_set.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -12,8 +12,30 @@ from collections.abc import MutableSet as collections_MutableSet from collections.abc import Set as collections_Set +from pyomo.common.autoslots import AutoSlots +from pyomo.common.collections.component_map import _hasher + + +def _rehash_keys(encode, val): + if encode: + # TBD [JDS 2/2024]: if we + # + # return list(val.values()) + # + # here, then we get a strange failure when deepcopying + # ComponentSets containing an _ImplicitAny domain. We could + # track it down to the implementation of + # autoslots.fast_deepcopy, but couldn't find an obvious bug. + # There is no error if we just return the original dict, or if + # we return a tuple(val.values) + return val + else: + # object id() may have changed after unpickling, + # so we rebuild the dictionary keys + return {_hasher[obj.__class__](obj): obj for obj in val.values()} + -class ComponentSet(collections_MutableSet): +class ComponentSet(AutoSlots.Mixin, collections_MutableSet): """ This class is a replacement for set that allows Pyomo modeling components to be used as entries. The @@ -38,47 +60,32 @@ class ComponentSet(collections_MutableSet): """ __slots__ = ("_data",) + __autoslot_mappers__ = {'_data': _rehash_keys} - def __init__(self, *args): - self._data = dict() - if len(args) > 0: - if len(args) > 1: - raise TypeError( - "%s expected at most 1 arguments, " - "got %s" % (self.__class__.__name__, len(args)) - ) - self.update(args[0]) + def __init__(self, iterable=None): + # maps id_hash(obj) -> obj + self._data = {} + if iterable is not None: + self.update(iterable) def __str__(self): """String representation of the mapping.""" - tmp = [] - for objid, obj in self._data.items(): - tmp.append(str(obj) + " (id=" + str(objid) + ")") - return "ComponentSet(" + str(tmp) + ")" + tmp = [f"{v} (key={k})" for k, v in self._data.items()] + return f"ComponentSet({tmp})" - def update(self, args): + def update(self, iterable): """Update a set with the union of itself and others.""" - self._data.update((id(obj), obj) for obj in args) - - # - # This method must be defined for deepcopy/pickling - # because this class relies on Python ids. - # - def __setstate__(self, state): - # object id() may have changed after unpickling, - # so we rebuild the dictionary keys - assert len(state) == 1 - self._data = {id(obj): obj for obj in state['_data']} - - def __getstate__(self): - return {'_data': tuple(self._data.values())} + if isinstance(iterable, ComponentSet): + self._data.update(iterable._data) + else: + self._data.update((_hasher[val.__class__](val), val) for val in iterable) # # Implement MutableSet abstract methods # def __contains__(self, val): - return self._data.__contains__(id(val)) + return _hasher[val.__class__](val) in self._data def __iter__(self): return iter(self._data.values()) @@ -88,26 +95,25 @@ def __len__(self): def add(self, val): """Add an element.""" - self._data[id(val)] = val + self._data[_hasher[val.__class__](val)] = val def discard(self, val): """Remove an element. Do not raise an exception if absent.""" - if id(val) in self._data: - del self._data[id(val)] + _id = _hasher[val.__class__](val) + if _id in self._data: + del self._data[_id] # # Overload MutableSet default implementations # - # We want to avoid generating Pyomo expressions due to - # comparison of values, so we convert both objects to a - # plain dictionary mapping key->(type(val), id(val)) and - # compare that instead. def __eq__(self, other): + if self is other: + return True if not isinstance(other, collections_Set): return False - return set((type(val), id(val)) for val in self) == set( - (type(val), id(val)) for val in other + return len(self) == len(other) and all( + _hasher[val.__class__](val) in self._data for val in other ) def __ne__(self, other): @@ -125,6 +131,7 @@ def clear(self): def remove(self, val): """Remove an element. If not a member, raise a KeyError.""" try: - del self._data[id(val)] + del self._data[_hasher[val.__class__](val)] except KeyError: - raise KeyError("Component with id '%s': %s" % (id(val), str(val))) + _id = _hasher[val.__class__](val) + raise KeyError(f"{val} (key={_id})") from None diff --git a/pyomo/common/collections/orderedset.py b/pyomo/common/collections/orderedset.py index 448939c8822..834101e3896 100644 --- a/pyomo/common/collections/orderedset.py +++ b/pyomo/common/collections/orderedset.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,42 +9,30 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from collections.abc import MutableSet from collections import OrderedDict +from collections.abc import MutableSet +from pyomo.common.autoslots import AutoSlots -class OrderedSet(MutableSet): +class OrderedSet(AutoSlots.Mixin, MutableSet): __slots__ = ('_dict',) def __init__(self, iterable=None): - # TODO: Starting in Python 3.7, dict is ordered (and is faster - # than OrderedDict). dict began supporting reversed() in 3.8. - # We should consider changing the underlying data type here from - # OrderedDict to dict. - self._dict = OrderedDict() + # Starting in Python 3.7, dict is ordered (and is faster than + # OrderedDict). dict began supporting reversed() in 3.8. + self._dict = {} if iterable is not None: - if iterable.__class__ is OrderedSet: - self._dict.update(iterable._dict) - else: - self.update(iterable) + self.update(iterable) def __str__(self): """String representation of the mapping.""" return "OrderedSet(%s)" % (', '.join(repr(x) for x in self)) def update(self, iterable): - for val in iterable: - self.add(val) - - # - # This method must be defined for deepcopy/pickling - # because this class is slotized. - # - def __setstate__(self, state): - self._dict = state - - def __getstate__(self): - return self._dict + if isinstance(iterable, OrderedSet): + self._dict.update(iterable._dict) + else: + self._dict.update((val, None) for val in iterable) # # Implement MutableSet abstract methods diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 61e4f682a2a..f9c3a725bb8 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -31,13 +31,14 @@ import textwrap import types +from operator import attrgetter + from pyomo.common.collections import Sequence, Mapping from pyomo.common.deprecation import ( deprecated, deprecation_warning, relocated_module_attribute, ) -from pyomo.common.errors import DeveloperError from pyomo.common.fileutils import import_file from pyomo.common.formatting import wrap_reStructuredText from pyomo.common.modeling import NOTSET @@ -300,6 +301,70 @@ def domain_name(self): return f'InEnum[{self._domain.__name__}]' +class IsInstance(object): + """ + Domain validator for type checking. + + Parameters + ---------- + *bases : tuple of type + Valid types. + document_full_base_names : bool, optional + True to prepend full module qualifier to the name of each + member of `bases` in ``self.domain_name()`` and/or any + error messages generated by this object, False otherwise. + """ + + def __init__(self, *bases, document_full_base_names=False): + assert bases + self.baseClasses = bases + self.document_full_base_names = document_full_base_names + + @staticmethod + def _fullname(klass): + """ + Get full name of class, including appropriate module qualifier. + """ + module_name = klass.__module__ + module_qual = "" if module_name == "builtins" else f"{module_name}." + return f"{module_qual}{klass.__name__}" + + def _get_class_name(self, klass): + """ + Get name of class. Module qualifier may be included, + depending on value of `self.document_full_base_names`. + """ + if self.document_full_base_names: + return self._fullname(klass) + else: + return klass.__name__ + + def __call__(self, obj): + if isinstance(obj, self.baseClasses): + return obj + if len(self.baseClasses) > 1: + class_names = ", ".join( + f"{self._get_class_name(kls)!r}" for kls in self.baseClasses + ) + msg = ( + "Expected an instance of one of these types: " + f"{class_names}, but received value {obj!r} of type " + f"{self._get_class_name(type(obj))!r}" + ) + else: + msg = ( + f"Expected an instance of " + f"{self._get_class_name(self.baseClasses[0])!r}, " + f"but received value {obj!r} of type " + f"{self._get_class_name(type(obj))!r}" + ) + raise ValueError(msg) + + def domain_name(self): + class_names = (self._get_class_name(kls) for kls in self.baseClasses) + return f"IsInstance({', '.join(class_names)})" + + class ListOf(object): """Domain validator for lists of a specified type @@ -422,9 +487,14 @@ def __call__(self, module_id): class Path(object): - """Domain validator for path-like options. + """ + Domain validator for a + :py:term:`path-like object `. - This will admit any object and convert it to a string. It will then + This will admit a path-like object + and get the object's file system representation + through :py:obj:`os.fsdecode`. + It will then expand any environment variables and leading usernames (e.g., "~myuser" or "~/") appearing in either the value or the base path before concatenating the base path and value, expanding the path to @@ -452,7 +522,7 @@ def __init__(self, basePath=None, expandPath=None): self.expandPath = expandPath def __call__(self, path): - path = str(path) + path = os.fsdecode(path) _expand = self.expandPath if _expand is None: _expand = not Path.SuppressPathExpansion @@ -487,14 +557,21 @@ def __call__(self, path): ) return ans + def domain_name(self): + return type(self).__name__ + class PathList(Path): - """Domain validator for a list of path-like objects. + """ + Domain validator for a list of + :py:term:`path-like objects `. - This will admit any iterable or object convertible to a string. - Iterable objects (other than strings) will have each member - normalized using :py:class:`Path`. Other types will be passed to - :py:class:`Path`, returning a list with the single resulting path. + This admits a path-like object or iterable of such. + If a path-like object is passed, then + a singleton list containing the object normalized through + :py:class:`Path` is returned. + An iterable of path-like objects is cast to a list, each + entry of which is normalized through :py:class:`Path`. Parameters ---------- @@ -511,7 +588,8 @@ class PathList(Path): """ def __call__(self, data): - if hasattr(data, "__iter__") and not isinstance(data, str): + is_path_like = isinstance(data, (str, bytes)) or hasattr(data, "__fspath__") + if hasattr(data, "__iter__") and not is_path_like: return [super(PathList, self).__call__(i) for i in data] else: return [super(PathList, self).__call__(data)] @@ -707,6 +785,7 @@ def from_enum_or_string(cls, arg): NonNegativeFloat In InEnum + IsInstance ListOf Module Path @@ -1026,8 +1105,11 @@ class will still create ``c`` instances that only have the single def _dump(*args, **kwds): + # TODO: Change the default behavior to no longer be YAML. + # This was a legacy decision that may no longer be the best + # decision, given changes to technology over the years. try: - from yaml import dump + from yaml import safe_dump as dump except ImportError: # dump = lambda x,**y: str(x) # YAML uses lowercase True/False @@ -1052,7 +1134,11 @@ def _domain_name(domain): if domain is None: return "" elif hasattr(domain, 'domain_name'): - return domain.domain_name() + dn = domain.domain_name + if hasattr(dn, '__call__'): + return dn() + else: + return dn elif domain.__class__ is type: return domain.__name__ elif inspect.isfunction(domain): @@ -1088,7 +1174,9 @@ def _value2string(prefix, value, obj): try: _data = value._data if value is obj else value if getattr(builtins, _data.__class__.__name__, None) is not None: - _str += _dump(_data, default_flow_style=True).rstrip() + _str += _dump( + _data, default_flow_style=True, allow_unicode=True + ).rstrip() if _str.endswith("..."): _str = _str[:-3].rstrip() else: @@ -1396,9 +1484,11 @@ def _item_body(self, indent, obj): None, [ 'dict' if isinstance(obj, ConfigDict) else obj.domain_name(), - 'optional' - if obj._default is None - else f'default={repr(obj._default)}', + ( + 'optional' + if obj._default is None + else f'default={repr(obj._default)}' + ), ], ) ) @@ -1686,11 +1776,9 @@ def __call__( ans.reset() else: # Copy over any Dict definitions - for k in self._decl_order: + for k, v in self._data.items(): if preserve_implicit or k in self._declared: - v = self._data[k] ans._data[k] = _tmp = v(preserve_implicit=preserve_implicit) - ans._decl_order.append(k) if k in self._declared: ans._declared.add(k) _tmp._parent = ans @@ -2381,12 +2469,7 @@ class ConfigDict(ConfigBase, Mapping): content_filters = {None, 'all', 'userdata'} - __slots__ = ( - '_decl_order', - '_declared', - '_implicit_declaration', - '_implicit_domain', - ) + __slots__ = ('_declared', '_implicit_declaration', '_implicit_domain') _all_slots = set(__slots__ + ConfigBase.__slots__) def __init__( @@ -2397,7 +2480,6 @@ def __init__( implicit_domain=None, visibility=0, ): - self._decl_order = [] self._declared = set() self._implicit_declaration = implicit if ( @@ -2476,7 +2558,6 @@ def __delitem__(self, key): _key = str(key).replace(' ', '_') del self._data[_key] # Clean up the other data structures - self._decl_order.remove(_key) self._declared.discard(_key) def __contains__(self, key): @@ -2484,10 +2565,10 @@ def __contains__(self, key): return _key in self._data def __len__(self): - return self._decl_order.__len__() + return len(self._data) def __iter__(self): - return (self._data[key]._name for key in self._decl_order) + return map(attrgetter('_name'), self._data.values()) def __getattr__(self, name): # Note: __getattr__ is only called after all "usual" attribute @@ -2524,13 +2605,12 @@ def keys(self): def values(self): self._userAccessed = True - for key in self._decl_order: - yield self[key] + return map(self.__getitem__, self._data) def items(self): self._userAccessed = True - for key in self._decl_order: - yield (self._data[key]._name, self[key]) + for key, val in self._data.items(): + yield (val._name, self[key]) @deprecated('The iterkeys method is deprecated. Use dict.keys().', version='6.0') def iterkeys(self): @@ -2559,7 +2639,6 @@ def _add(self, name, config): % (name, self.name(True)) ) self._data[_name] = config - self._decl_order.append(_name) config._parent = self config._name = name return config @@ -2611,10 +2690,7 @@ def add(self, name, config): def value(self, accessValue=True): if accessValue: self._userAccessed = True - return { - cfg._name: cfg.value(accessValue) - for cfg in map(self._data.__getitem__, self._decl_order) - } + return {cfg._name: cfg.value(accessValue) for cfg in self._data.values()} def set_value(self, value, skip_implicit=False): if value is None: @@ -2634,7 +2710,7 @@ def set_value(self, value, skip_implicit=False): _key = str(key).replace(' ', '_') if _key in self._data: # str(key) may not be key... store the mapping so that - # when we later iterate over the _decl_order, we can map + # when we later iterate over the _data, we can map # the local keys back to the incoming value keys. _decl_map[_key] = key else: @@ -2657,7 +2733,7 @@ def set_value(self, value, skip_implicit=False): # We want to set the values in declaration order (so that # things are deterministic and in case a validation depends # on the order) - for key in self._decl_order: + for key in self._data: if key in _decl_map: self[key] = value[_decl_map[key]] # implicit data is declared at the end (in sorted order) @@ -2673,16 +2749,11 @@ def set_value(self, value, skip_implicit=False): def reset(self): # Reset the values in the order they were declared. This # allows reset functions to have a deterministic ordering. - def _keep(self, key): - keep = key in self._declared - if keep: - self._data[key].reset() + for key, val in list(self._data.items()): + if key in self._declared: + val.reset() else: del self._data[key] - return keep - - # this is an in-place slice of a list... - self._decl_order[:] = [x for x in self._decl_order if _keep(self, x)] self._userAccessed = False self._userSet = False @@ -2693,8 +2764,7 @@ def _data_collector(self, level, prefix, visibility=None, docMode=False): yield (level, prefix, None, self) if level is not None: level += 1 - for key in self._decl_order: - cfg = self._data[key] + for cfg in self._data.values(): yield from cfg._data_collector(level, cfg._name + ': ', visibility, docMode) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 4ddbe1c9ee8..4c9e43002ef 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,16 +9,18 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from collections.abc import Mapping import inspect import importlib import logging import sys import warnings +from collections.abc import Mapping +from types import ModuleType +from typing import List + from .deprecation import deprecated, deprecation_warning, in_testing_environment from .errors import DeferredImportError -from . import numeric_types SUPPRESS_DEPENDENCY_WARNINGS = False @@ -128,7 +130,7 @@ class DeferredImportModule(object): This object is returned by :py:func:`attempt_import()` in lieu of the module when :py:func:`attempt_import()` is called with - ``defer_check=True``. Any attempts to access attributes on this + ``defer_import=True``. Any attempts to access attributes on this object will trigger the actual module import and return either the appropriate module attribute or else if the module import fails, raise a :py:class:`.DeferredImportError` exception. @@ -313,6 +315,12 @@ def __init__( self._module = None self._available = None self._deferred_submodules = deferred_submodules + # If this import has a callback, then record this deferred + # import so that any direct imports of this module also trigger + # the resolution of this DeferredImportIndicator (and the + # corresponding callback) + if callback is not None: + DeferredImportCallbackFinder._callbacks.setdefault(name, []).append(self) def __bool__(self): self.resolve() @@ -434,6 +442,83 @@ def check_min_version(module, min_version): check_min_version._parser = None +# +# Note that we are duck-typing the Loader and MetaPathFinder base +# classes from importlib.abc. This avoids a (surprisingly costly) +# import of importlib.abc +# +class DeferredImportCallbackLoader: + """Custom Loader to resolve registered :py:class:`DeferredImportIndicator` objects + + This :py:class:`importlib.abc.Loader` loader wraps a regular loader + and automatically resolves the registered + :py:class:`DeferredImportIndicator` objects after the module is + loaded. + + """ + + def __init__(self, loader, deferred_indicators: List[DeferredImportIndicator]): + self._loader = loader + self._deferred_indicators = deferred_indicators + + def module_repr(self, module: ModuleType) -> str: + return self._loader.module_repr(module) + + def create_module(self, spec) -> ModuleType: + return self._loader.create_module(spec) + + def exec_module(self, module: ModuleType) -> None: + self._loader.exec_module(module) + # Now that the module has been loaded, trigger the resolution of + # the deferred indicators (and their associated callbacks) + for deferred in self._deferred_indicators: + deferred.resolve() + + def load_module(self, fullname) -> ModuleType: + return self._loader.load_module(fullname) + + +class DeferredImportCallbackFinder: + """Custom Finder that will wrap the normal loader to trigger callbacks + + This :py:class:`importlib.abc.MetaPathFinder` finder will wrap the + normal loader returned by ``PathFinder`` with a loader that will + trigger custom callbacks after the module is loaded. We use this to + trigger the post import callbacks registered through + :py:func:`attempt_import` even when a user imports the target library + directly (and not through attribute access on the + :py:class:`DeferredImportModule`. + + """ + + _callbacks = {} + + def find_spec(self, fullname, path, target=None): + if fullname not in self._callbacks: + return None + + spec = importlib.machinery.PathFinder.find_spec(fullname, path, target) + if spec is None: + # Module not found. Returning None will proceed to the next + # finder (which is likely to raise a ModuleNotFoundError) + return None + spec.loader = DeferredImportCallbackLoader( + spec.loader, self._callbacks[fullname] + ) + return spec + + def invalidate_caches(self): + pass + + +_DeferredImportCallbackFinder = DeferredImportCallbackFinder() +# Insert the DeferredImportCallbackFinder at the beginning of the +# sys.meta_path so that it is found before the standard finders (so that +# we can correctly inject the resolution of the DeferredImportIndicators +# -- which triggers the needed callbacks) +sys.meta_path.insert(0, _DeferredImportCallbackFinder) + + def attempt_import( name, error_message=None, @@ -442,7 +527,8 @@ def attempt_import( alt_names=None, callback=None, importer=None, - defer_check=True, + defer_check=None, + defer_import=None, deferred_submodules=None, catch_exceptions=None, ): @@ -496,7 +582,8 @@ def attempt_import( The message for the exception raised by :py:class:`ModuleUnavailable` only_catch_importerror: bool, optional - DEPRECATED: use catch_exceptions instead or only_catch_importerror. + DEPRECATED: use ``catch_exceptions`` instead of ``only_catch_importerror``. + If True (the default), exceptions other than ``ImportError`` raised during module import will be reraised. If False, any exception will result in returning a :py:class:`ModuleUnavailable` object. @@ -507,13 +594,14 @@ def attempt_import( ``module.__version__``) alt_names: list, optional - DEPRECATED: alt_names no longer needs to be specified and is ignored. + DEPRECATED: ``alt_names`` no longer needs to be specified and is ignored. + A list of common alternate names by which to look for this module in the ``globals()`` namespaces. For example, the alt_names for NumPy would be ``['np']``. (deprecated in version 6.0) - callback: function, optional - A function with the signature "``fcn(module, available)``" that + callback: Callable[[ModuleType, bool], None], optional + A function with the signature ``fcn(module, available)`` that will be called after the import is first attempted. importer: function, optional @@ -523,10 +611,16 @@ def attempt_import( want to import/return the first one that is available. defer_check: bool, optional - If True (the default), then the attempted import is deferred - until the first use of either the module or the availability - flag. The method will return instances of :py:class:`DeferredImportModule` - and :py:class:`DeferredImportIndicator`. + DEPRECATED: renamed to ``defer_import`` (deprecated in version 6.7.2) + + defer_import: bool, optional + If True, then the attempted import is deferred until the first + use of either the module or the availability flag. The method + will return instances of :py:class:`DeferredImportModule` and + :py:class:`DeferredImportIndicator`. If False, the import will + be attempted immediately. If not set, then the import will be + deferred unless the ``name`` is already present in + ``sys.modules``. deferred_submodules: Iterable[str], optional If provided, an iterable of submodule names within this module @@ -577,9 +671,26 @@ def attempt_import( if catch_exceptions is None: catch_exceptions = (ImportError,) + if defer_check is not None: + deprecation_warning( + 'defer_check=%s is deprecated. Please use defer_import' % (defer_check,), + version='6.7.2', + ) + assert defer_import is None + defer_import = defer_check + + # If the module has already been imported, there is no reason to + # further defer things: just import it. + if defer_import is None: + if name in sys.modules: + defer_import = False + deferred_submodules = None + else: + defer_import = True + # If we are going to defer the check until later, return the # deferred import module object - if defer_check: + if defer_import: if deferred_submodules: if isinstance(deferred_submodules, Mapping): deprecation_warning( @@ -622,7 +733,7 @@ def attempt_import( return DeferredImportModule(indicator, deferred, None), indicator if deferred_submodules: - raise ValueError("deferred_submodules is only valid if defer_check==True") + raise ValueError("deferred_submodules is only valid if defer_import==True") return _perform_import( name=name, @@ -673,6 +784,11 @@ def _perform_import( return module, False +@deprecated( + "``declare_deferred_modules_as_importable()`` is deprecated. " + "Use the :py:class:`declare_modules_as_importable` context manager.", + version='6.7.2', +) def declare_deferred_modules_as_importable(globals_dict): """Make all :py:class:`DeferredImportModules` in ``globals_dict`` importable @@ -699,6 +815,7 @@ def declare_deferred_modules_as_importable(globals_dict): ... 'scipy', callback=_finalize_scipy, ... deferred_submodules=['stats', 'sparse', 'spatial', 'integrate']) >>> declare_deferred_modules_as_importable(globals()) + WARNING: DEPRECATED: ... Which enables users to use: @@ -713,20 +830,87 @@ def declare_deferred_modules_as_importable(globals_dict): :py:class:`ModuleUnavailable` instance. """ - _global_name = globals_dict['__name__'] + '.' - deferred = list( - (k, v) for k, v in globals_dict.items() if type(v) is DeferredImportModule - ) - while deferred: - name, mod = deferred.pop(0) - mod.__path__ = None - mod.__spec__ = None - sys.modules[_global_name + name] = mod - deferred.extend( - (name + '.' + k, v) - for k, v in mod.__dict__.items() - if type(v) is DeferredImportModule - ) + return declare_modules_as_importable(globals_dict).__exit__(None, None, None) + + +class declare_modules_as_importable(object): + """Make all :py:class:`ModuleType` and :py:class:`DeferredImportModules` + importable through the ``globals_dict`` context. + + This context manager will detect all modules imported into the + specified ``globals_dict`` environment (either directly or through + :py:func:`attempt_import`) and will make those modules importable + from the specified ``globals_dict`` context. It works by detecting + changes in the specified ``globals_dict`` dictionary and adding any new + modules or instances of :py:class:`DeferredImportModule` that it + finds (and any of their deferred submodules) to ``sys.modules`` so + that the modules can be imported through the ``globals_dict`` + namespace. + + For example, ``pyomo/common/dependencies.py`` declares: + + .. doctest:: + :hide: + + >>> from pyomo.common.dependencies import ( + ... attempt_import, _finalize_scipy, __dict__ as dep_globals, + ... declare_modules_as_importable, ) + >>> # Sphinx does not provide a proper globals() + >>> def globals(): return dep_globals + + .. doctest:: + + >>> with declare_modules_as_importable(globals()): + ... scipy, scipy_available = attempt_import( + ... 'scipy', callback=_finalize_scipy, + ... deferred_submodules=['stats', 'sparse', 'spatial', 'integrate']) + + Which enables users to use: + + .. doctest:: + + >>> import pyomo.common.dependencies.scipy.sparse as spa + + If the deferred import has not yet been triggered, then the + :py:class:`DeferredImportModule` is returned and named ``spa``. + However, if the import has already been triggered, then ``spa`` will + either be the ``scipy.sparse`` module, or a + :py:class:`ModuleUnavailable` instance. + + """ + + def __init__(self, globals_dict): + self.globals_dict = globals_dict + self.init_dict = {} + self.init_modules = None + + def __enter__(self): + self.init_dict.update(self.globals_dict) + self.init_modules = set(sys.modules) + + def __exit__(self, exc_type, exc_value, traceback): + _global_name = self.globals_dict['__name__'] + '.' + deferred = { + k: v + for k, v in self.globals_dict.items() + if k not in self.init_dict + and isinstance(v, (ModuleType, DeferredImportModule)) + } + if self.init_modules: + for name in set(sys.modules) - self.init_modules: + if '.' in name and name.split('.', 1)[0] in deferred: + sys.modules[_global_name + name] = sys.modules[name] + while deferred: + name, mod = deferred.popitem() + sys.modules[_global_name + name] = mod + if isinstance(mod, DeferredImportModule): + mod.__path__ = None + mod.__spec__ = None + deferred.update( + (name + '.' + k, v) + for k, v in mod.__dict__.items() + if type(v) is DeferredImportModule + ) # @@ -743,6 +927,12 @@ def _finalize_yaml(module, available): yaml_load_args['Loader'] = module.SafeLoader +def _finalize_ctypes(module, available): + # ctypes.util must be explicitly imported (and fileutils assumes + # this has already happened) + import ctypes.util + + def _finalize_scipy(module, available): if available: # Import key subpackages that we will want to assume are present @@ -773,11 +963,22 @@ def _finalize_matplotlib(module, available): if in_testing_environment(): module.use('Agg') import matplotlib.pyplot + import matplotlib.pylab + import matplotlib.backends def _finalize_numpy(np, available): if not available: return + # scipy has a dependence on numpy.testing, and if we don't import it + # as part of resolving numpy, then certain deferred scipy imports + # fail when run under pytest. + import numpy.testing + + from . import numeric_types + + # Register ndarray as a native type to prevent 1-element ndarrays + # from accidentally registering ndarray as a native_numeric_type. numeric_types.native_types.add(np.ndarray) numeric_types.RegisterLogicalType(np.bool_) for t in ( @@ -798,41 +999,77 @@ def _finalize_numpy(np, available): # registration here (to bypass the deprecation warning) until we # finally remove all support for it numeric_types._native_boolean_types.add(t) - for t in (np.float_, np.float16, np.float32, np.float64): + _floats = [np.float_, np.float16, np.float32, np.float64] + # float96 and float128 may or may not be defined in this particular + # numpy build (it depends on platform and version). + # Register them only if they are present + if hasattr(np, 'float96'): + _floats.append(np.float96) + if hasattr(np, 'float128'): + _floats.append(np.float128) + for t in _floats: numeric_types.RegisterNumericType(t) # We have deprecated RegisterBooleanType, so we will mock up the # registration here (to bypass the deprecation warning) until we # finally remove all support for it numeric_types._native_boolean_types.add(t) - - -dill, dill_available = attempt_import('dill') -mpi4py, mpi4py_available = attempt_import('mpi4py') -networkx, networkx_available = attempt_import('networkx') -numpy, numpy_available = attempt_import('numpy', callback=_finalize_numpy) -pandas, pandas_available = attempt_import('pandas') -plotly, plotly_available = attempt_import('plotly') -pympler, pympler_available = attempt_import('pympler', callback=_finalize_pympler) -pyutilib, pyutilib_available = attempt_import('pyutilib') -scipy, scipy_available = attempt_import( - 'scipy', - callback=_finalize_scipy, - deferred_submodules=['stats', 'sparse', 'spatial', 'integrate'], -) -yaml, yaml_available = attempt_import('yaml', callback=_finalize_yaml) - -# Note that matplotlib.pyplot can generate a runtime error on OSX when -# not installed as a Framework (as is the case in the CI systems) -matplotlib, matplotlib_available = attempt_import( - 'matplotlib', - callback=_finalize_matplotlib, - deferred_submodules=['pyplot', 'pylab'], - catch_exceptions=(ImportError, RuntimeError), -) + _complex = [np.complex_, np.complex64, np.complex128] + # complex192 and complex256 may or may not be defined in this + # particular numpy build (it depends on platform and version). + # Register them only if they are present + if hasattr(np, 'complex192'): + _complex.append(np.complex192) + if hasattr(np, 'complex256'): + _complex.append(np.complex256) + for t in _complex: + numeric_types.RegisterComplexType(t) + + +def _pyutilib_importer(): + # On newer Pythons, PyUtilib import will fail, but only if a + # second-level module is imported. We will arbitrarily choose to + # check pyutilib.component (as that is the path exercised by the + # pyomo.common.tempfiles deprecation path) + importlib.import_module('pyutilib.component') + return importlib.import_module('pyutilib') + + +with declare_modules_as_importable(globals()): + # Standard libraries that are slower to import and not strictly required + # on all platforms / situations. + ctypes, _ = attempt_import( + 'ctypes', deferred_submodules=['util'], callback=_finalize_ctypes + ) + random, _ = attempt_import('random') + + # Commonly-used optional dependencies + dill, dill_available = attempt_import('dill') + mpi4py, mpi4py_available = attempt_import('mpi4py') + networkx, networkx_available = attempt_import('networkx') + numpy, numpy_available = attempt_import('numpy', callback=_finalize_numpy) + pandas, pandas_available = attempt_import('pandas') + plotly, plotly_available = attempt_import('plotly') + pympler, pympler_available = attempt_import('pympler', callback=_finalize_pympler) + pyutilib, pyutilib_available = attempt_import( + 'pyutilib', importer=_pyutilib_importer + ) + scipy, scipy_available = attempt_import( + 'scipy', + callback=_finalize_scipy, + deferred_submodules=['stats', 'sparse', 'spatial', 'integrate'], + ) + yaml, yaml_available = attempt_import('yaml', callback=_finalize_yaml) + + # Note that matplotlib.pyplot can generate a runtime error on OSX when + # not installed as a Framework (as is the case in the CI systems) + matplotlib, matplotlib_available = attempt_import( + 'matplotlib', + callback=_finalize_matplotlib, + deferred_submodules=['pyplot', 'pylab', 'backends'], + catch_exceptions=(ImportError, RuntimeError), + ) try: import cPickle as pickle except ImportError: import pickle - -declare_deferred_modules_as_importable(globals()) diff --git a/pyomo/common/deprecation.py b/pyomo/common/deprecation.py index 2e39083770d..c674dcddc78 100644 --- a/pyomo/common/deprecation.py +++ b/pyomo/common/deprecation.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -542,7 +542,7 @@ def __renamed__warning__(msg): if new_class is None and '__renamed__new_class__' not in classdict: if not any( - hasattr(base, '__renamed__new_class__') + hasattr(mro, '__renamed__new_class__') for mro in itertools.chain.from_iterable( base.__mro__ for base in renamed_bases ) diff --git a/pyomo/common/download.py b/pyomo/common/download.py index 79d5302a58e..ad3b64060e9 100644 --- a/pyomo/common/download.py +++ b/pyomo/common/download.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -29,6 +29,7 @@ urllib_error = attempt_import('urllib.error')[0] ssl = attempt_import('ssl')[0] zipfile = attempt_import('zipfile')[0] +tarfile = attempt_import('tarfile')[0] gzip = attempt_import('gzip')[0] distro, distro_available = attempt_import('distro') @@ -371,7 +372,7 @@ def get_zip_archive(self, url, dirOffset=0): # Simple sanity checks for info in zip_file.infolist(): f = info.filename - if f[0] in '\\/' or '..' in f: + if f[0] in '\\/' or '..' in f or os.path.isabs(f): logger.error( "malformed (potentially insecure) filename (%s) " "found in zip archive. Skipping file." % (f,) @@ -387,6 +388,61 @@ def get_zip_archive(self, url, dirOffset=0): info.filename = target[-1] + '/' if f[-1] == '/' else target[-1] zip_file.extract(f, os.path.join(self._fname, *tuple(target[dirOffset:-1]))) + def get_tar_archive(self, url, dirOffset=0): + if self._fname is None: + raise DeveloperError( + "target file name has not been initialized " + "with set_destination_filename" + ) + if os.path.exists(self._fname) and not os.path.isdir(self._fname): + raise RuntimeError( + "Target directory (%s) exists, but is not a directory" % (self._fname,) + ) + + def filter_fcn(info): + # this mocks up the `tarfile` filter introduced in Python + # 3.12 and backported to later releases of Python (e.g., + # 3.8.17, 3.9.17, 3.10.12, and 3.11.4) + f = info.name + if os.path.isabs(f) or '..' in f or f.startswith(('/', os.sep)): + logger.error( + "malformed or potentially insecure filename (%s). " + "Skipping file." % (f,) + ) + return False + target = self._splitpath(f) + if len(target) <= dirOffset: + if not info.isdir(): + logger.warning( + "Skipping file (%s) in tar archive due to dirOffset." % (f,) + ) + return False + info.name = f = '/'.join(target[dirOffset:]) + target = os.path.realpath(os.path.join(dest, f)) + try: + if os.path.commonpath([target, dest]) != dest: + logger.error( + "potentially insecure filename (%s) resolves outside target " + "directory. Skipping file." % (f,) + ) + return False + except ValueError: + # commonpath() will raise ValueError for paths that + # don't have anything in common (notably, when files are + # on different drives on Windows) + logger.error( + "potentially insecure filename (%s) resolves outside target " + "directory. Skipping file." % (f,) + ) + return False + # Strip high bits & group/other write bits + info.mode &= 0o755 + return True + + with tarfile.open(fileobj=io.BytesIO(self.retrieve_url(url))) as TAR: + dest = os.path.realpath(self._fname) + TAR.extractall(dest, filter(filter_fcn, TAR.getmembers())) + def get_gzipped_binary_file(self, url): if self._fname is None: raise DeveloperError( diff --git a/pyomo/common/enums.py b/pyomo/common/enums.py new file mode 100644 index 00000000000..121155d4ae8 --- /dev/null +++ b/pyomo/common/enums.py @@ -0,0 +1,170 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +"""This module provides standard :py:class:`enum.Enum` definitions used in +Pyomo, along with additional utilities for working with custom Enums + +Utilities: + +.. autosummary:: + + ExtendedEnumType + NamedIntEnum + +Standard Enums: + +.. autosummary:: + + ObjectiveSense + +""" + +import enum +import itertools +import sys + +if sys.version_info[:2] < (3, 11): + _EnumType = enum.EnumMeta +else: + _EnumType = enum.EnumType + + +class ExtendedEnumType(_EnumType): + """Metaclass for creating an :py:class:`enum.Enum` that extends another Enum + + In general, :py:class:`enum.Enum` classes are not extensible: that is, + they are frozen when defined and cannot be the base class of another + Enum. This Metaclass provides a workaround for creating a new Enum + that extends an existing enum. Members in the base Enum are all + present as members on the extended enum. + + Example + ------- + + .. testcode:: + :hide: + + import enum + from pyomo.common.enums import ExtendedEnumType + + .. testcode:: + + class ObjectiveSense(enum.IntEnum): + minimize = 1 + maximize = -1 + + class ProblemSense(enum.IntEnum, metaclass=ExtendedEnumType): + __base_enum__ = ObjectiveSense + + unknown = 0 + + .. doctest:: + + >>> list(ProblemSense) + [, , ] + >>> ProblemSense.unknown + + >>> ProblemSense.maximize + + >>> ProblemSense(0) + + >>> ProblemSense(1) + + >>> ProblemSense('unknown') + + >>> ProblemSense('maximize') + + >>> hasattr(ProblemSense, 'minimize') + True + >>> ProblemSense.minimize is ObjectiveSense.minimize + True + >>> ProblemSense.minimize in ProblemSense + True + + """ + + def __getattr__(cls, attr): + try: + return getattr(cls.__base_enum__, attr) + except: + return super().__getattr__(attr) + + def __iter__(cls): + # The members of this Enum are the base enum members joined with + # the local members + return itertools.chain(super().__iter__(), cls.__base_enum__.__iter__()) + + def __contains__(cls, member): + # This enum "contains" both its local members and the members in + # the __base_enum__ (necessary for good auto-enum[sphinx] docs) + return super().__contains__(member) or member in cls.__base_enum__ + + def __instancecheck__(cls, instance): + if cls.__subclasscheck__(type(instance)): + return True + # Also pretend that members of the extended enum are subclasses + # of the __base_enum__. This is needed to circumvent error + # checking in enum.__new__ (e.g., for `ProblemSense('minimize')`) + return cls.__base_enum__.__subclasscheck__(type(instance)) + + def _missing_(cls, value): + # Support attribute lookup by value or name + for attr in ('value', 'name'): + for member in cls: + if getattr(member, attr) == value: + return member + return None + + def __new__(metacls, cls, bases, classdict, **kwds): + # Support lookup by name - but only if the new Enum doesn't + # specify its own implementation of _missing_ + if '_missing_' not in classdict: + classdict['_missing_'] = classmethod(ExtendedEnumType._missing_) + return super().__new__(metacls, cls, bases, classdict, **kwds) + + +class NamedIntEnum(enum.IntEnum): + """An extended version of :py:class:`enum.IntEnum` that supports + creating members by name as well as value. + + """ + + @classmethod + def _missing_(cls, value): + for member in cls: + if member.name == value: + return member + return None + + +class ObjectiveSense(NamedIntEnum): + """Flag indicating if an objective is minimizing (1) or maximizing (-1). + + While the numeric values are arbitrary, there are parts of Pyomo + that rely on this particular choice of value. These values are also + consistent with some solvers (notably Gurobi). + + """ + + minimize = 1 + maximize = -1 + + # Overloading __str__ is needed to match the behavior of the old + # pyutilib.enum class (removed June 2020). There are spots in the + # code base that expect the string representation for items in the + # enum to not include the class name. New uses of enum shouldn't + # need to do this. + def __str__(self): + return self.name + + +minimize = ObjectiveSense.minimize +maximize = ObjectiveSense.maximize diff --git a/pyomo/common/env.py b/pyomo/common/env.py index a90efcc2787..ee07cdc1e6a 100644 --- a/pyomo/common/env.py +++ b/pyomo/common/env.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,9 +9,10 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import ctypes import os +from .dependencies import ctypes + def _as_bytes(val): """Helper function to coerce a string to a bytes() object""" diff --git a/pyomo/common/envvar.py b/pyomo/common/envvar.py index d74cb764641..1f933d4b08c 100644 --- a/pyomo/common/envvar.py +++ b/pyomo/common/envvar.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/errors.py b/pyomo/common/errors.py index 17013ce4dca..3c82f2b07c1 100644 --- a/pyomo/common/errors.py +++ b/pyomo/common/errors.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/extensions.py b/pyomo/common/extensions.py index e4f7b047bb3..0ac27f125a7 100644 --- a/pyomo/common/extensions.py +++ b/pyomo/common/extensions.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/factory.py b/pyomo/common/factory.py index 6a97759c714..c449cf826b4 100644 --- a/pyomo/common/factory.py +++ b/pyomo/common/factory.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/fileutils.py b/pyomo/common/fileutils.py index 16933df64af..7b6520327a0 100644 --- a/pyomo/common/fileutils.py +++ b/pyomo/common/fileutils.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -32,16 +32,17 @@ PathData """ -import ctypes.util import glob import inspect import logging import os import platform import importlib.util +import subprocess import sys from . import envvar +from .dependencies import ctypes from .deprecation import deprecated, relocated_module_attribute relocated_module_attribute('StreamIndenter', 'pyomo.common.formatting', version='6.2') @@ -375,9 +376,27 @@ def find_library(libname, cwd=True, include_PATH=True, pathlist=None): if libname_base.startswith('lib') and _system() != 'windows': libname_base = libname_base[3:] if ext.lower().startswith(('.so', '.dll', '.dylib')): - return ctypes.util.find_library(libname_base) + lib = ctypes.util.find_library(libname_base) else: - return ctypes.util.find_library(libname) + lib = ctypes.util.find_library(libname) + if lib and os.path.sep not in lib: + # work around https://github.com/python/cpython/issues/65241, + # where python does not return the absolute path on *nix + try: + libname = lib + ' ' + with subprocess.Popen( + ['/sbin/ldconfig', '-p'], + stdin=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + stdout=subprocess.PIPE, + env={'LC_ALL': 'C', 'LANG': 'C'}, + ) as p: + for line in os.fsdecode(p.stdout.read()).splitlines(): + if line.lstrip().startswith(libname): + return os.path.realpath(line.split()[-1]) + except: + pass + return lib def find_executable(exename, cwd=True, include_PATH=True, pathlist=None): diff --git a/pyomo/common/formatting.py b/pyomo/common/formatting.py index f76d16880df..430ec96ca09 100644 --- a/pyomo/common/formatting.py +++ b/pyomo/common/formatting.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -257,7 +257,8 @@ def writelines(self, sequence): r'|(?:\[\s*[A-Za-z0-9\.]+\s*\] +)' # [PASS]|[FAIL]|[ OK ] ) _verbatim_line_start = re.compile( - r'(\| )' r'|(\+((-{3,})|(={3,}))\+)' # line blocks # grid table + r'(\| )' # line blocks + r'|(\+((-{3,})|(={3,}))\+)' # grid table ) _verbatim_line = re.compile( r'(={3,}[ =]+)' # simple tables, ======== sections diff --git a/pyomo/common/gc_manager.py b/pyomo/common/gc_manager.py index 54fbca32736..751eb95cf18 100644 --- a/pyomo/common/gc_manager.py +++ b/pyomo/common/gc_manager.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/getGSL.py b/pyomo/common/getGSL.py index e8b2507ab81..66b75b45665 100644 --- a/pyomo/common/getGSL.py +++ b/pyomo/common/getGSL.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/gsl.py b/pyomo/common/gsl.py index 5243758a0de..1c14b64bd70 100644 --- a/pyomo/common/gsl.py +++ b/pyomo/common/gsl.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/log.py b/pyomo/common/log.py index bf2ae1e4c96..d61ed62f373 100644 --- a/pyomo/common/log.py +++ b/pyomo/common/log.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -139,7 +139,8 @@ def format(self, record): # # A standard approach is to use inspect.cleandoc, which # allows for the first line to have 0 indent. - msg = inspect.cleandoc(msg) + if getattr(record, 'cleandoc', True): + msg = inspect.cleandoc(msg) # Split the formatted log message (that currently has _flag in # lieu of the actual message content) into lines, then diff --git a/pyomo/common/modeling.py b/pyomo/common/modeling.py index b3a6d59fcf0..4c07048d77a 100644 --- a/pyomo/common/modeling.py +++ b/pyomo/common/modeling.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,8 +9,8 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from random import random import sys +from .dependencies import random def randint(a, b): @@ -21,7 +21,7 @@ def randint(a, b): can support deterministic testing (i.e., setting the random.seed and expecting the same sequence), we will implement a simple, but stable version of randint().""" - return int((b - a + 1) * random()) + return int((b - a + 1) * random.random()) def unique_component_name(instance, name): diff --git a/pyomo/common/multithread.py b/pyomo/common/multithread.py index 415d8aaba7e..a2dace2be0f 100644 --- a/pyomo/common/multithread.py +++ b/pyomo/common/multithread.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from collections import defaultdict from threading import get_ident, main_thread diff --git a/pyomo/common/numeric_types.py b/pyomo/common/numeric_types.py index dbad3ef0853..2b63038e125 100644 --- a/pyomo/common/numeric_types.py +++ b/pyomo/common/numeric_types.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -41,10 +41,14 @@ #: Python set used to identify numeric constants. This set includes #: native Python types as well as numeric types from Python packages #: like numpy, which may be registered by users. -native_numeric_types = {int, float, complex} +#: +#: Note that :data:`native_numeric_types` does NOT include +#: :py:`complex`, as that is not a valid constant in Pyomo numeric +#: expressions. +native_numeric_types = {int, float} native_integer_types = {int} native_logical_types = {bool} -pyomo_constant_types = set() # includes NumericConstant +native_complex_types = {complex} _native_boolean_types = {int, bool, str, bytes} relocated_module_attribute( @@ -56,6 +60,16 @@ "be treated as if they were bool (as was the case for the other " "native_*_types sets). Users likely should use native_logical_types.", ) +_pyomo_constant_types = set() # includes NumericConstant, _PythonCallbackFunctionID +relocated_module_attribute( + 'pyomo_constant_types', + 'pyomo.common.numeric_types._pyomo_constant_types', + version='6.7.2', + msg="The pyomo_constant_types set will be removed in the future: the set " + "contained only NumericConstant and _PythonCallbackFunctionID, and provided " + "no meaningful value to clients or walkers. Users should likely handle " + "these types in the same manner as immutable Params.", +) #: Python set used to identify numeric constants and related native @@ -64,34 +78,53 @@ #: like numpy. #: #: :data:`native_types` = :data:`native_numeric_types ` + { str } -native_types = set([bool, str, type(None), slice, bytes]) +native_types = {bool, str, type(None), slice, bytes} native_types.update(native_numeric_types) native_types.update(native_integer_types) -native_types.update(_native_boolean_types) +native_types.update(native_complex_types) native_types.update(native_logical_types) +native_types.update(_native_boolean_types) nonpyomo_leaf_types.update(native_types) -def RegisterNumericType(new_type): - """ - A utility function for updating the set of types that are - recognized to handle numeric values. +def RegisterNumericType(new_type: type): + """Register the specified type as a "numeric type". + + A utility function for registering new types as "native numeric + types" that can be leaf nodes in Pyomo numeric expressions. The + type should be compatible with :py:class:`float` (that is, store a + scalar and be castable to a Python float). + + Parameters + ---------- + new_type: type + The new numeric type (e.g, numpy.float64) - The argument should be a class (e.g, numpy.float64). """ native_numeric_types.add(new_type) native_types.add(new_type) nonpyomo_leaf_types.add(new_type) -def RegisterIntegerType(new_type): - """ - A utility function for updating the set of types that are - recognized to handle integer values. This also registers the type - as numeric but does not register it as boolean. +def RegisterIntegerType(new_type: type): + """Register the specified type as an "integer type". + + A utility function for registering new types as "native integer + types". Integer types can be leaf nodes in Pyomo numeric + expressions. The type should be compatible with :py:class:`float` + (that is, store a scalar and be castable to a Python float). + + Registering a type as an integer type implies + :py:func:`RegisterNumericType`. + + Note that integer types are NOT registered as logical / Boolean types. + + Parameters + ---------- + new_type: type + The new integer type (e.g, numpy.int64) - The argument should be a class (e.g., numpy.int64). """ native_numeric_types.add(new_type) native_integer_types.add(new_type) @@ -104,26 +137,64 @@ def RegisterIntegerType(new_type): "is deprecated. Users likely should use RegisterLogicalType.", version='6.6.0', ) -def RegisterBooleanType(new_type): - """ - A utility function for updating the set of types that are - recognized as handling boolean values. This function does not - register the type of integer or numeric. +def RegisterBooleanType(new_type: type): + """Register the specified type as a "logical type". + + A utility function for registering new types as "native logical + types". Logical types can be leaf nodes in Pyomo logical + expressions. The type should be compatible with :py:class:`bool` + (that is, store a scalar and be castable to a Python bool). + + Note that logical types are NOT registered as numeric types. + + Parameters + ---------- + new_type: type + The new logical type (e.g, numpy.bool_) - The argument should be a class (e.g., numpy.bool_). """ _native_boolean_types.add(new_type) native_types.add(new_type) nonpyomo_leaf_types.add(new_type) -def RegisterLogicalType(new_type): +def RegisterComplexType(new_type: type): + """Register the specified type as an "complex type". + + A utility function for registering new types as "native complex + types". Complex types can NOT be leaf nodes in Pyomo numeric + expressions. The type should be compatible with :py:class:`complex` + (that is, store a scalar complex value and be castable to a Python + complex). + + Note that complex types are NOT registered as logical or numeric types. + + Parameters + ---------- + new_type: type + The new complex type (e.g, numpy.complex128) + """ - A utility function for updating the set of types that are - recognized as handling boolean values. This function does not - register the type of integer or numeric. + native_types.add(new_type) + native_complex_types.add(new_type) + nonpyomo_leaf_types.add(new_type) + + +def RegisterLogicalType(new_type: type): + """Register the specified type as a "logical type". + + A utility function for registering new types as "native logical + types". Logical types can be leaf nodes in Pyomo logical + expressions. The type should be compatible with :py:class:`bool` + (that is, store a scalar and be castable to a Python bool). + + Note that logical types are NOT registered as numeric types. + + Parameters + ---------- + new_type: type + The new logical type (e.g, numpy.bool_) - The argument should be a class (e.g., numpy.bool_). """ _native_boolean_types.add(new_type) native_logical_types.add(new_type) @@ -131,12 +202,74 @@ def RegisterLogicalType(new_type): nonpyomo_leaf_types.add(new_type) +def check_if_native_type(obj): + if isinstance(obj, (str, bytes)): + native_types.add(obj.__class__) + return True + if check_if_logical_type(obj): + return True + if check_if_numeric_type(obj): + return True + return False + + +def check_if_logical_type(obj): + """Test if the argument behaves like a logical type. + + We check for "logical types" by checking if the type returns sane + results for Boolean operators (``^``, ``|``, ``&``) and if it maps + ``1`` and ``2`` both to the same equivalent instance. If that + works, then we register the type in :py:attr:`native_logical_types`. + + """ + obj_class = obj.__class__ + # Do not re-evaluate known native types + if obj_class in native_types: + return obj_class in native_logical_types + + try: + # It is not an error if you can't initialize the type from an + # int, but if you can, it should map !0 to True + if obj_class(1) != obj_class(2): + return False + except: + pass + + try: + # Native logical types *must* be hashable + hash(obj) + # Native logical types must honor standard Boolean operators + if all( + ( + obj_class(False) != obj_class(True), + obj_class(False) ^ obj_class(False) == obj_class(False), + obj_class(False) ^ obj_class(True) == obj_class(True), + obj_class(True) ^ obj_class(False) == obj_class(True), + obj_class(True) ^ obj_class(True) == obj_class(False), + obj_class(False) | obj_class(False) == obj_class(False), + obj_class(False) | obj_class(True) == obj_class(True), + obj_class(True) | obj_class(False) == obj_class(True), + obj_class(True) | obj_class(True) == obj_class(True), + obj_class(False) & obj_class(False) == obj_class(False), + obj_class(False) & obj_class(True) == obj_class(False), + obj_class(True) & obj_class(False) == obj_class(False), + obj_class(True) & obj_class(True) == obj_class(True), + ) + ): + RegisterLogicalType(obj_class) + return True + except: + pass + return False + + def check_if_numeric_type(obj): """Test if the argument behaves like a numeric type. We check for "numeric types" by checking if we can add zero to it - without changing the object's type. If that works, then we register - the type in native_numeric_types. + without changing the object's type, and that the object compares to + 0 in a meaningful way. If that works, then we register the type in + :py:attr:`native_numeric_types`. """ obj_class = obj.__class__ @@ -147,78 +280,82 @@ def check_if_numeric_type(obj): try: obj_plus_0 = obj + 0 obj_p0_class = obj_plus_0.__class__ - # ensure that the object is comparable to 0 in a meaningful way - # (among other things, this prevents numpy.ndarray objects from - # being added to native_numeric_types) + # Native numeric types *must* be hashable + hash(obj) + except: + return False + if obj_p0_class is not obj_class and obj_p0_class not in native_numeric_types: + return False + # + # Check if the numeric type behaves like a complex type + # + try: + if 1.41 < abs(obj_class(1j + 1)) < 1.42: + RegisterComplexType(obj_class) + return False + except: + pass + # + # Ensure that the object is comparable to 0 in a meaningful way + # + try: if not ((obj < 0) ^ (obj >= 0)): return False - # Native types *must* be hashable - hash(obj) except: return False - if obj_p0_class is obj_class or obj_p0_class in native_numeric_types: - # - # If we get here, this is a reasonably well-behaving - # numeric type: add it to the native numeric types - # so that future lookups will be faster. - # - RegisterNumericType(obj_class) - # - # Generate a warning, since Pyomo's management of third-party - # numeric types is more robust when registering explicitly. - # - logger.warning( - f"""Dynamically registering the following numeric type: + # + # If we get here, this is a reasonably well-behaving + # numeric type: add it to the native numeric types + # so that future lookups will be faster. + # + RegisterNumericType(obj_class) + try: + if obj_class(0.4) == obj_class(0): + RegisterIntegerType(obj_class) + except: + pass + # + # Generate a warning, since Pyomo's management of third-party + # numeric types is more robust when registering explicitly. + # + logger.warning( + f"""Dynamically registering the following numeric type: {obj_class.__module__}.{obj_class.__name__} Dynamic registration is supported for convenience, but there are known limitations to this approach. We recommend explicitly registering numeric types using RegisterNumericType() or RegisterIntegerType().""" - ) - return True - else: - return False + ) + return True def value(obj, exception=True): """ - A utility function that returns the value of a Pyomo object or - expression. - - Args: - obj: The argument to evaluate. If it is None, a - string, or any other primitive numeric type, - then this function simply returns the argument. - Otherwise, if the argument is a NumericValue - then the __call__ method is executed. - exception (bool): If :const:`True`, then an exception should - be raised when instances of NumericValue fail to - s evaluate due to one or more objects not being - initialized to a numeric value (e.g, one or more - variables in an algebraic expression having the - value None). If :const:`False`, then the function - returns :const:`None` when an exception occurs. - Default is True. - - Returns: A numeric value or None. + A utility function that returns the value of a Pyomo object or + expression. + + Args: + obj: The argument to evaluate. If it is None, a + string, or any other primitive numeric type, + then this function simply returns the argument. + Otherwise, if the argument is a NumericValue + then the __call__ method is executed. + exception (bool): If :const:`True`, then an exception should + be raised when instances of NumericValue fail to + evaluate due to one or more objects not being + initialized to a numeric value (e.g, one or more + variables in an algebraic expression having the + value None). If :const:`False`, then the function + returns :const:`None` when an exception occurs. + Default is True. + + Returns: A numeric value or None. """ if obj.__class__ in native_types: return obj - if obj.__class__ in pyomo_constant_types: - # - # I'm commenting this out for now, but I think we should never expect - # to see a numeric constant with value None. - # - # if exception and obj.value is None: - # raise ValueError( - # "No value for uninitialized NumericConstant object %s" - # % (obj.name,)) - return obj.value # # Test if we have a duck typed Pyomo expression # - try: - obj.is_numeric_type() - except AttributeError: + if not hasattr(obj, 'is_numeric_type'): # # TODO: Historically we checked for new *numeric* types and # raised exceptions for anything else. That is inconsistent @@ -233,7 +370,7 @@ def value(obj, exception=True): return None raise TypeError( "Cannot evaluate object with unknown type: %s" % obj.__class__.__name__ - ) from None + ) # # Evaluate the expression object # diff --git a/pyomo/common/plugin.py b/pyomo/common/plugin.py index b48fa96a483..ac88388ebc0 100644 --- a/pyomo/common/plugin.py +++ b/pyomo/common/plugin.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/plugin_base.py b/pyomo/common/plugin_base.py index 67960ebbb12..75b8657d1a9 100644 --- a/pyomo/common/plugin_base.py +++ b/pyomo/common/plugin_base.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/plugins.py b/pyomo/common/plugins.py index 7db8077855a..ed44f8bf776 100644 --- a/pyomo/common/plugins.py +++ b/pyomo/common/plugins.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/pyomo_typing.py b/pyomo/common/pyomo_typing.py index 64ab2ddafc9..22ec3480842 100644 --- a/pyomo/common/pyomo_typing.py +++ b/pyomo/common/pyomo_typing.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/shutdown.py b/pyomo/common/shutdown.py index 5054fd21279..a96a6bc04fc 100644 --- a/pyomo/common/shutdown.py +++ b/pyomo/common/shutdown.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import atexit diff --git a/pyomo/common/sorting.py b/pyomo/common/sorting.py index 31e796c6a9e..4f78a7892b8 100644 --- a/pyomo/common/sorting.py +++ b/pyomo/common/sorting.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tee.py b/pyomo/common/tee.py index 029d66f5767..500f7b6f58d 100644 --- a/pyomo/common/tee.py +++ b/pyomo/common/tee.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tempfiles.py b/pyomo/common/tempfiles.py index e981d26d84e..b9dface71b2 100644 --- a/pyomo/common/tempfiles.py +++ b/pyomo/common/tempfiles.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -22,18 +22,15 @@ import logging import shutil import weakref + +from pyomo.common.dependencies import attempt_import, pyutilib_available from pyomo.common.deprecation import deprecated, deprecation_warning from pyomo.common.errors import TempfileContextError from pyomo.common.multithread import MultiThreadWrapperWithMain -try: - from pyutilib.component.config.tempfiles import TempfileManager as pyutilib_mngr -except ImportError: - pyutilib_mngr = None - deletion_errors_are_fatal = True - logger = logging.getLogger(__name__) +pyutilib_tempfiles, _ = attempt_import('pyutilib.component.config.tempfiles') class TempfileManagerClass(object): @@ -432,16 +429,17 @@ def _resolve_tempdir(self, dir=None): return self.manager().tempdir elif TempfileManager.main_thread.tempdir is not None: return TempfileManager.main_thread.tempdir - elif pyutilib_mngr is not None and pyutilib_mngr.tempdir is not None: - deprecation_warning( - "The use of the PyUtilib TempfileManager.tempdir " - "to specify the default location for Pyomo " - "temporary files has been deprecated. " - "Please set TempfileManager.tempdir in " - "pyomo.common.tempfiles", - version='5.7.2', - ) - return pyutilib_mngr.tempdir + elif pyutilib_available: + if pyutilib_tempfiles.TempfileManager.tempdir is not None: + deprecation_warning( + "The use of the PyUtilib TempfileManager.tempdir " + "to specify the default location for Pyomo " + "temporary files has been deprecated. " + "Please set TempfileManager.tempdir in " + "pyomo.common.tempfiles", + version='5.7.2', + ) + return pyutilib_tempfiles.TempfileManager.tempdir return None def _remove_filesystem_object(self, name): diff --git a/pyomo/common/tests/__init__.py b/pyomo/common/tests/__init__.py index bc8dfa27c9c..d8d8856e52f 100644 --- a/pyomo/common/tests/__init__.py +++ b/pyomo/common/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/config_plugin.py b/pyomo/common/tests/config_plugin.py index ada788fd7d4..6aebc40806a 100644 --- a/pyomo/common/tests/config_plugin.py +++ b/pyomo/common/tests/config_plugin.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/dep_mod.py b/pyomo/common/tests/dep_mod.py index 54530393783..34c7219c6eb 100644 --- a/pyomo/common/tests/dep_mod.py +++ b/pyomo/common/tests/dep_mod.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -13,8 +13,8 @@ __version__ = '1.5' -numpy, numpy_available = attempt_import('numpy', defer_check=True) +numpy, numpy_available = attempt_import('numpy', defer_import=True) bogus_nonexisting_module, bogus_nonexisting_module_available = attempt_import( - 'bogus_nonexisting_module', alt_names=['bogus_nem'], defer_check=True + 'bogus_nonexisting_module', alt_names=['bogus_nem'], defer_import=True ) diff --git a/pyomo/common/tests/dep_mod_except.py b/pyomo/common/tests/dep_mod_except.py index 8132e8a08ac..16936996eeb 100644 --- a/pyomo/common/tests/dep_mod_except.py +++ b/pyomo/common/tests/dep_mod_except.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/deps.py b/pyomo/common/tests/deps.py index e5236d0f7ec..5f8c1fffdf8 100644 --- a/pyomo/common/tests/deps.py +++ b/pyomo/common/tests/deps.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -23,15 +23,16 @@ bogus_nonexisting_module_available as has_bogus_nem, ) -bogus, bogus_available = attempt_import('nonexisting.module.bogus', defer_check=True) +bogus, bogus_available = attempt_import('nonexisting.module.bogus', defer_import=True) pkl_test, pkl_available = attempt_import( - 'nonexisting.module.pickle_test', deferred_submodules=['submod'], defer_check=True + 'nonexisting.module.pickle_test', deferred_submodules=['submod'], defer_import=True ) pyo, pyo_available = attempt_import( 'pyomo', alt_names=['pyo'], + defer_import=True, deferred_submodules={'version': None, 'common.tests.dep_mod': ['dm']}, ) diff --git a/pyomo/common/tests/import_ex.py b/pyomo/common/tests/import_ex.py index e19ad956044..73375bdc819 100644 --- a/pyomo/common/tests/import_ex.py +++ b/pyomo/common/tests/import_ex.py @@ -1,3 +1,15 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + def a(): pass diff --git a/pyomo/common/tests/relo_mod.py b/pyomo/common/tests/relo_mod.py index 20b0712e09b..4881caba671 100644 --- a/pyomo/common/tests/relo_mod.py +++ b/pyomo/common/tests/relo_mod.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/relo_mod_new.py b/pyomo/common/tests/relo_mod_new.py index 1ef27681b66..0f59f3beebc 100644 --- a/pyomo/common/tests/relo_mod_new.py +++ b/pyomo/common/tests/relo_mod_new.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/relocated.py b/pyomo/common/tests/relocated.py index 9de63e0cec9..90cb28c23ba 100644 --- a/pyomo/common/tests/relocated.py +++ b/pyomo/common/tests/relocated.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_bunch.py b/pyomo/common/tests/test_bunch.py index a8daf5a0071..8c10df83005 100644 --- a/pyomo/common/tests/test_bunch.py +++ b/pyomo/common/tests/test_bunch.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_component_map.py b/pyomo/common/tests/test_component_map.py new file mode 100644 index 00000000000..7cd4ec2c458 --- /dev/null +++ b/pyomo/common/tests/test_component_map.py @@ -0,0 +1,90 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest + +from pyomo.common.collections import ComponentMap, ComponentSet, DefaultComponentMap +from pyomo.environ import ConcreteModel, Block, Var, Constraint + + +class TestComponentMap(unittest.TestCase): + def test_tuple(self): + m = ConcreteModel() + m.v = Var() + m.c = Constraint(expr=m.v >= 0) + m.cm = cm = ComponentMap() + + cm[(1, 2)] = 5 + self.assertEqual(len(cm), 1) + self.assertIn((1, 2), cm) + self.assertEqual(cm[1, 2], 5) + + cm[(1, 2)] = 50 + self.assertEqual(len(cm), 1) + self.assertIn((1, 2), cm) + self.assertEqual(cm[1, 2], 50) + + cm[(1, (2, m.v))] = 10 + self.assertEqual(len(cm), 2) + self.assertIn((1, (2, m.v)), cm) + self.assertEqual(cm[1, (2, m.v)], 10) + + cm[(1, (2, m.v))] = 100 + self.assertEqual(len(cm), 2) + self.assertIn((1, (2, m.v)), cm) + self.assertEqual(cm[1, (2, m.v)], 100) + + i = m.clone() + self.assertIn((1, 2), i.cm) + self.assertIn((1, (2, i.v)), i.cm) + self.assertNotIn((1, (2, i.v)), m.cm) + self.assertIn((1, (2, m.v)), m.cm) + self.assertNotIn((1, (2, m.v)), i.cm) + + +class TestDefaultComponentMap(unittest.TestCase): + def test_default_component_map(self): + dcm = DefaultComponentMap(ComponentSet) + + m = ConcreteModel() + m.x = Var() + m.b = Block() + m.b.y = Var() + + self.assertEqual(len(dcm), 0) + + dcm[m.x].add(m) + self.assertEqual(len(dcm), 1) + self.assertIn(m.x, dcm) + self.assertIn(m, dcm[m.x]) + + dcm[m.b.y].add(m.b) + self.assertEqual(len(dcm), 2) + self.assertIn(m.b.y, dcm) + self.assertNotIn(m, dcm[m.b.y]) + self.assertIn(m.b, dcm[m.b.y]) + + dcm[m.b.y].add(m) + self.assertEqual(len(dcm), 2) + self.assertIn(m.b.y, dcm) + self.assertIn(m, dcm[m.b.y]) + self.assertIn(m.b, dcm[m.b.y]) + + def test_no_default_factory(self): + dcm = DefaultComponentMap() + + dcm['found'] = 5 + self.assertEqual(len(dcm), 1) + self.assertIn('found', dcm) + self.assertEqual(dcm['found'], 5) + + with self.assertRaisesRegex(KeyError, "'missing'"): + dcm["missing"] diff --git a/pyomo/common/tests/test_config.py b/pyomo/common/tests/test_config.py index 9bafd852eb9..a47f5e0d8af 100644 --- a/pyomo/common/tests/test_config.py +++ b/pyomo/common/tests/test_config.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -60,6 +60,7 @@ def yaml_load(arg): NonPositiveFloat, NonNegativeFloat, In, + IsInstance, ListOf, Module, Path, @@ -448,12 +449,85 @@ class TestEnum(enum.Enum): with self.assertRaisesRegex(ValueError, '.*invalid value'): cfg.enum = 'ITEM_THREE' + def test_IsInstance(self): + c = ConfigDict() + c.declare("val", ConfigValue(None, IsInstance(int))) + c.val = 1 + self.assertEqual(c.val, 1) + exc_str = ( + "Expected an instance of 'int', but received value 2.4 of type 'float'" + ) + with self.assertRaisesRegex(ValueError, exc_str): + c.val = 2.4 + + class TestClass: + def __repr__(self): + return f"{TestClass.__name__}()" + + c.declare("val2", ConfigValue(None, IsInstance(TestClass))) + testinst = TestClass() + c.val2 = testinst + self.assertEqual(c.val2, testinst) + exc_str = ( + r"Expected an instance of 'TestClass', " + "but received value 2.4 of type 'float'" + ) + with self.assertRaisesRegex(ValueError, exc_str): + c.val2 = 2.4 + + c.declare( + "val3", + ConfigValue( + None, IsInstance(int, TestClass, document_full_base_names=True) + ), + ) + self.assertRegex( + c.get("val3").domain_name(), r"IsInstance\(int, .*\.TestClass\)" + ) + c.val3 = 2 + self.assertEqual(c.val3, 2) + exc_str = ( + r"Expected an instance of one of these types: 'int', '.*\.TestClass'" + r", but received value 2.4 of type 'float'" + ) + with self.assertRaisesRegex(ValueError, exc_str): + c.val3 = 2.4 + + c.declare( + "val4", + ConfigValue( + None, IsInstance(int, TestClass, document_full_base_names=False) + ), + ) + self.assertEqual(c.get("val4").domain_name(), "IsInstance(int, TestClass)") + c.val4 = 2 + self.assertEqual(c.val4, 2) + exc_str = ( + r"Expected an instance of one of these types: 'int', 'TestClass'" + r", but received value 2.4 of type 'float'" + ) + with self.assertRaisesRegex(ValueError, exc_str): + c.val4 = 2.4 + def test_Path(self): def norm(x): if cwd[1] == ':' and x[0] == '/': x = cwd[:2] + x return x.replace('/', os.path.sep) + class ExamplePathLike: + def __init__(self, path_str_or_bytes): + self.path = path_str_or_bytes + + def __fspath__(self): + return self.path + + def __str__(self): + path_str = str(self.path) + return f"{type(self).__name__}({path_str})" + + self.assertEqual(Path().domain_name(), "Path") + cwd = os.getcwd() + os.path.sep c = ConfigDict() @@ -462,12 +536,30 @@ def norm(x): c.a = "/a/b/c" self.assertTrue(os.path.sep in c.a) self.assertEqual(c.a, norm('/a/b/c')) + c.a = b"/a/b/c" + self.assertTrue(os.path.sep in c.a) + self.assertEqual(c.a, norm('/a/b/c')) + c.a = ExamplePathLike("/a/b/c") + self.assertTrue(os.path.sep in c.a) + self.assertEqual(c.a, norm('/a/b/c')) c.a = "a/b/c" self.assertTrue(os.path.sep in c.a) self.assertEqual(c.a, norm(cwd + 'a/b/c')) + c.a = b'a/b/c' + self.assertTrue(os.path.sep in c.a) + self.assertEqual(c.a, norm(cwd + 'a/b/c')) + c.a = ExamplePathLike('a/b/c') + self.assertTrue(os.path.sep in c.a) + self.assertEqual(c.a, norm(cwd + 'a/b/c')) c.a = "${CWD}/a/b/c" self.assertTrue(os.path.sep in c.a) self.assertEqual(c.a, norm(cwd + 'a/b/c')) + c.a = b'${CWD}/a/b/c' + self.assertTrue(os.path.sep in c.a) + self.assertEqual(c.a, norm(cwd + 'a/b/c')) + c.a = ExamplePathLike('${CWD}/a/b/c') + self.assertTrue(os.path.sep in c.a) + self.assertEqual(c.a, norm(cwd + 'a/b/c')) c.a = None self.assertIs(c.a, None) @@ -476,12 +568,30 @@ def norm(x): c.b = "/a/b/c" self.assertTrue(os.path.sep in c.b) self.assertEqual(c.b, norm('/a/b/c')) + c.b = b"/a/b/c" + self.assertTrue(os.path.sep in c.b) + self.assertEqual(c.b, norm('/a/b/c')) + c.b = ExamplePathLike("/a/b/c") + self.assertTrue(os.path.sep in c.b) + self.assertEqual(c.b, norm('/a/b/c')) c.b = "a/b/c" self.assertTrue(os.path.sep in c.b) self.assertEqual(c.b, norm(cwd + 'rel/path/a/b/c')) + c.b = b"a/b/c" + self.assertTrue(os.path.sep in c.b) + self.assertEqual(c.b, norm(cwd + 'rel/path/a/b/c')) + c.b = ExamplePathLike("a/b/c") + self.assertTrue(os.path.sep in c.b) + self.assertEqual(c.b, norm(cwd + "rel/path/a/b/c")) c.b = "${CWD}/a/b/c" self.assertTrue(os.path.sep in c.b) self.assertEqual(c.b, norm(cwd + 'a/b/c')) + c.b = b"${CWD}/a/b/c" + self.assertTrue(os.path.sep in c.b) + self.assertEqual(c.b, norm(cwd + 'a/b/c')) + c.b = ExamplePathLike("${CWD}/a/b/c") + self.assertTrue(os.path.sep in c.b) + self.assertEqual(c.b, norm(cwd + 'a/b/c')) c.b = None self.assertIs(c.b, None) @@ -490,12 +600,30 @@ def norm(x): c.c = "/a/b/c" self.assertTrue(os.path.sep in c.c) self.assertEqual(c.c, norm('/a/b/c')) + c.c = b"/a/b/c" + self.assertTrue(os.path.sep in c.c) + self.assertEqual(c.c, norm('/a/b/c')) + c.c = ExamplePathLike("/a/b/c") + self.assertTrue(os.path.sep in c.c) + self.assertEqual(c.c, norm('/a/b/c')) c.c = "a/b/c" self.assertTrue(os.path.sep in c.c) self.assertEqual(c.c, norm('/my/dir/a/b/c')) + c.c = b"a/b/c" + self.assertTrue(os.path.sep in c.c) + self.assertEqual(c.c, norm('/my/dir/a/b/c')) + c.c = ExamplePathLike("a/b/c") + self.assertTrue(os.path.sep in c.c) + self.assertEqual(c.c, norm("/my/dir/a/b/c")) c.c = "${CWD}/a/b/c" self.assertTrue(os.path.sep in c.c) self.assertEqual(c.c, norm(cwd + 'a/b/c')) + c.c = b"${CWD}/a/b/c" + self.assertTrue(os.path.sep in c.c) + self.assertEqual(c.c, norm(cwd + 'a/b/c')) + c.c = ExamplePathLike("${CWD}/a/b/c") + self.assertTrue(os.path.sep in c.c) + self.assertEqual(c.c, norm(cwd + 'a/b/c')) c.c = None self.assertIs(c.c, None) @@ -505,12 +633,30 @@ def norm(x): c.d = "/a/b/c" self.assertTrue(os.path.sep in c.d) self.assertEqual(c.d, norm('/a/b/c')) + c.d = b"/a/b/c" + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm('/a/b/c')) + c.d = ExamplePathLike("/a/b/c") + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm('/a/b/c')) c.d = "a/b/c" self.assertTrue(os.path.sep in c.d) self.assertEqual(c.d, norm(cwd + 'a/b/c')) + c.d = b"a/b/c" + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm(cwd + 'a/b/c')) + c.d = ExamplePathLike("a/b/c") + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm(cwd + 'a/b/c')) c.d = "${CWD}/a/b/c" self.assertTrue(os.path.sep in c.d) self.assertEqual(c.d, norm(cwd + 'a/b/c')) + c.d = b"${CWD}/a/b/c" + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm(cwd + 'a/b/c')) + c.d = ExamplePathLike("${CWD}/a/b/c") + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm(cwd + 'a/b/c')) c.d_base = '/my/dir' c.d = "/a/b/c" @@ -527,12 +673,30 @@ def norm(x): c.d = "/a/b/c" self.assertTrue(os.path.sep in c.d) self.assertEqual(c.d, norm('/a/b/c')) + c.d = b"/a/b/c" + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm('/a/b/c')) + c.d = ExamplePathLike("/a/b/c") + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm('/a/b/c')) c.d = "a/b/c" self.assertTrue(os.path.sep in c.d) self.assertEqual(c.d, norm(cwd + 'rel/path/a/b/c')) + c.d = b"a/b/c" + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm(cwd + 'rel/path/a/b/c')) + c.d = ExamplePathLike("a/b/c") + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm(cwd + 'rel/path/a/b/c')) c.d = "${CWD}/a/b/c" self.assertTrue(os.path.sep in c.d) self.assertEqual(c.d, norm(cwd + 'a/b/c')) + c.d = b"${CWD}/a/b/c" + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm(cwd + 'a/b/c')) + c.d = ExamplePathLike("${CWD}/a/b/c") + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm(cwd + 'a/b/c')) try: Path.SuppressPathExpansion = True @@ -540,14 +704,38 @@ def norm(x): self.assertTrue('/' in c.d) self.assertTrue('\\' not in c.d) self.assertEqual(c.d, '/a/b/c') + c.d = b"/a/b/c" + self.assertTrue('/' in c.d) + self.assertTrue('\\' not in c.d) + self.assertEqual(c.d, '/a/b/c') + c.d = ExamplePathLike("/a/b/c") + self.assertTrue('/' in c.d) + self.assertTrue('\\' not in c.d) + self.assertEqual(c.d, '/a/b/c') c.d = "a/b/c" self.assertTrue('/' in c.d) self.assertTrue('\\' not in c.d) self.assertEqual(c.d, 'a/b/c') + c.d = b"a/b/c" + self.assertTrue('/' in c.d) + self.assertTrue('\\' not in c.d) + self.assertEqual(c.d, 'a/b/c') + c.d = ExamplePathLike("a/b/c") + self.assertTrue('/' in c.d) + self.assertTrue('\\' not in c.d) + self.assertEqual(c.d, 'a/b/c') c.d = "${CWD}/a/b/c" self.assertTrue('/' in c.d) self.assertTrue('\\' not in c.d) self.assertEqual(c.d, "${CWD}/a/b/c") + c.d = b"${CWD}/a/b/c" + self.assertTrue('/' in c.d) + self.assertTrue('\\' not in c.d) + self.assertEqual(c.d, "${CWD}/a/b/c") + c.d = ExamplePathLike("${CWD}/a/b/c") + self.assertTrue('/' in c.d) + self.assertTrue('\\' not in c.d) + self.assertEqual(c.d, "${CWD}/a/b/c") finally: Path.SuppressPathExpansion = False @@ -560,6 +748,8 @@ def norm(x): cwd = os.getcwd() + os.path.sep c = ConfigDict() + self.assertEqual(PathList().domain_name(), "PathList") + c.declare('a', ConfigValue(None, PathList())) self.assertEqual(c.a, None) c.a = "/a/b/c" @@ -582,6 +772,13 @@ def norm(x): self.assertEqual(len(c.a), 0) self.assertIs(type(c.a), list) + exc_str = r".*expected str, bytes or os.PathLike.*int" + + with self.assertRaisesRegex(ValueError, exc_str): + c.a = 2 + with self.assertRaisesRegex(ValueError, exc_str): + c.a = ["/a/b/c", 2] + def test_ListOf(self): c = ConfigDict() c.declare('a', ConfigValue(domain=ListOf(int), default=None)) @@ -1473,7 +1670,7 @@ def test_parseDisplay_userdata_add_block_nonDefault(self): self.config.add("bar", ConfigDict(implicit=True)).add("baz", ConfigDict()) test = _display(self.config, 'userdata') sys.stdout.write(test) - self.assertEqual(yaml_load(test), {'bar': {'baz': None}, foo: 0}) + self.assertEqual(yaml_load(test), {'bar': {'baz': None}, 'foo': 0}) @unittest.skipIf(not yaml_available, "Test requires PyYAML") def test_parseDisplay_userdata_add_block(self): @@ -1901,7 +2098,6 @@ def test_generate_custom_documentation(self): "generate_documentation is deprecated.", LOG, ) - self.maxDiff = None # print(test) self.assertEqual(test, reference) @@ -1916,7 +2112,6 @@ def test_generate_custom_documentation(self): ) ) self.assertEqual(LOG.getvalue(), "") - self.maxDiff = None # print(test) self.assertEqual(test, reference) @@ -1962,7 +2157,6 @@ def test_generate_custom_documentation(self): "generate_documentation is deprecated.", LOG, ) - self.maxDiff = None # print(test) self.assertEqual(test, reference) @@ -2380,7 +2574,6 @@ def test_argparse_help_implicit_disable(self): parser = argparse.ArgumentParser(prog='tester') self.config.initialize_argparse(parser) help = parser.format_help() - self.maxDiff = None self.assertIn( """ -h, --help show this help message and exit @@ -2909,8 +3102,6 @@ def test_declare_from(self): cfg2.declare_from({}) def test_docstring_decorator(self): - self.maxDiff = None - @document_kwargs_from_configdict('CONFIG') class ExampleClass(object): CONFIG = ExampleConfig() @@ -3027,6 +3218,82 @@ def fcn(self): self.assertEqual(add_docstring_list("", ExampleClass.CONFIG), ref) self.assertIn('add_docstring_list is deprecated', LOG.getvalue()) + def test_declaration_in_init(self): + class CustomConfig(ConfigDict): + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.declare('time_limit', ConfigValue(domain=NonNegativeFloat)) + self.declare('stream_solver', ConfigValue(domain=bool)) + + cfg = CustomConfig() + OUT = StringIO() + cfg.display(ostream=OUT) + # Note: pypy outputs "None" as "null" + self.assertEqual( + "time_limit: None\nstream_solver: None\n", + OUT.getvalue().replace('null', 'None'), + ) + + # Test that creating a copy of a ConfigDict with declared fields + # in the __init__ does not result in duplicate outputs in the + # display (reported in PR #3113) + cfg2 = cfg({'time_limit': 10, 'stream_solver': 0}) + OUT = StringIO() + cfg2.display(ostream=OUT) + self.assertEqual( + "time_limit: 10.0\nstream_solver: false\n", + OUT.getvalue().replace('null', 'None'), + ) + + def test_domain_name(self): + cfg = ConfigDict() + + cfg.declare('none', ConfigValue()) + self.assertEqual(cfg.get('none').domain_name(), '') + + def fcn(val): + return val + + cfg.declare('fcn', ConfigValue(domain=fcn)) + self.assertEqual(cfg.get('fcn').domain_name(), 'fcn') + + fcn.domain_name = 'custom fcn' + self.assertEqual(cfg.get('fcn').domain_name(), 'custom fcn') + + class functor: + def __call__(self, val): + return val + + cfg.declare('functor', ConfigValue(domain=functor())) + self.assertEqual(cfg.get('functor').domain_name(), 'functor') + + class cfunctor: + def __call__(self, val): + return val + + def domain_name(self): + return 'custom functor' + + cfg.declare('cfunctor', ConfigValue(domain=cfunctor())) + self.assertEqual(cfg.get('cfunctor').domain_name(), 'custom functor') + + cfg.declare('type', ConfigValue(domain=int)) + self.assertEqual(cfg.get('type').domain_name(), 'int') + if __name__ == "__main__": unittest.main() diff --git a/pyomo/common/tests/test_dependencies.py b/pyomo/common/tests/test_dependencies.py index 65058e01812..31f9520b613 100644 --- a/pyomo/common/tests/test_dependencies.py +++ b/pyomo/common/tests/test_dependencies.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -45,7 +45,7 @@ def test_import_error(self): module_obj, module_available = attempt_import( '__there_is_no_module_named_this__', 'Testing import of a non-existent module', - defer_check=False, + defer_import=False, ) self.assertFalse(module_available) with self.assertRaisesRegex( @@ -85,7 +85,7 @@ def test_pickle(self): def test_import_success(self): module_obj, module_available = attempt_import( - 'ply', 'Testing import of ply', defer_check=False + 'ply', 'Testing import of ply', defer_import=False ) self.assertTrue(module_available) import ply @@ -123,7 +123,7 @@ def test_imported_deferred_import(self): def test_min_version(self): mod, avail = attempt_import( - 'pyomo.common.tests.dep_mod', minimum_version='1.0', defer_check=False + 'pyomo.common.tests.dep_mod', minimum_version='1.0', defer_import=False ) self.assertTrue(avail) self.assertTrue(inspect.ismodule(mod)) @@ -131,7 +131,7 @@ def test_min_version(self): self.assertFalse(check_min_version(mod, '2.0')) mod, avail = attempt_import( - 'pyomo.common.tests.dep_mod', minimum_version='2.0', defer_check=False + 'pyomo.common.tests.dep_mod', minimum_version='2.0', defer_import=False ) self.assertFalse(avail) self.assertIs(type(mod), ModuleUnavailable) @@ -146,7 +146,7 @@ def test_min_version(self): 'pyomo.common.tests.dep_mod', error_message="Failed import", minimum_version='2.0', - defer_check=False, + defer_import=False, ) self.assertFalse(avail) self.assertIs(type(mod), ModuleUnavailable) @@ -159,10 +159,10 @@ def test_min_version(self): # Verify check_min_version works with deferred imports - mod, avail = attempt_import('pyomo.common.tests.dep_mod', defer_check=True) + mod, avail = attempt_import('pyomo.common.tests.dep_mod', defer_import=True) self.assertTrue(check_min_version(mod, '1.0')) - mod, avail = attempt_import('pyomo.common.tests.dep_mod', defer_check=True) + mod, avail = attempt_import('pyomo.common.tests.dep_mod', defer_import=True) self.assertFalse(check_min_version(mod, '2.0')) # Verify check_min_version works when called directly @@ -174,10 +174,10 @@ def test_min_version(self): self.assertFalse(check_min_version(mod, '1.0')) def test_and_or(self): - mod0, avail0 = attempt_import('ply', defer_check=True) - mod1, avail1 = attempt_import('pyomo.common.tests.dep_mod', defer_check=True) + mod0, avail0 = attempt_import('ply', defer_import=True) + mod1, avail1 = attempt_import('pyomo.common.tests.dep_mod', defer_import=True) mod2, avail2 = attempt_import( - 'pyomo.common.tests.dep_mod', minimum_version='2.0', defer_check=True + 'pyomo.common.tests.dep_mod', minimum_version='2.0', defer_import=True ) _and = avail0 & avail1 @@ -233,11 +233,11 @@ def test_callbacks(self): def _record_avail(module, avail): ans.append(avail) - mod0, avail0 = attempt_import('ply', defer_check=True, callback=_record_avail) + mod0, avail0 = attempt_import('ply', defer_import=True, callback=_record_avail) mod1, avail1 = attempt_import( 'pyomo.common.tests.dep_mod', minimum_version='2.0', - defer_check=True, + defer_import=True, callback=_record_avail, ) @@ -250,7 +250,7 @@ def _record_avail(module, avail): def test_import_exceptions(self): mod, avail = attempt_import( 'pyomo.common.tests.dep_mod_except', - defer_check=True, + defer_import=True, only_catch_importerror=True, ) with self.assertRaisesRegex(ValueError, "cannot import module"): @@ -260,7 +260,7 @@ def test_import_exceptions(self): mod, avail = attempt_import( 'pyomo.common.tests.dep_mod_except', - defer_check=True, + defer_import=True, only_catch_importerror=False, ) self.assertFalse(avail) @@ -268,7 +268,7 @@ def test_import_exceptions(self): mod, avail = attempt_import( 'pyomo.common.tests.dep_mod_except', - defer_check=True, + defer_import=True, catch_exceptions=(ImportError, ValueError), ) self.assertFalse(avail) @@ -280,7 +280,7 @@ def test_import_exceptions(self): ): mod, avail = attempt_import( 'pyomo.common.tests.dep_mod_except', - defer_check=True, + defer_import=True, only_catch_importerror=True, catch_exceptions=(ImportError,), ) @@ -288,7 +288,7 @@ def test_import_exceptions(self): def test_generate_warning(self): mod, avail = attempt_import( 'pyomo.common.tests.dep_mod_except', - defer_check=True, + defer_import=True, only_catch_importerror=False, ) @@ -324,7 +324,7 @@ def test_generate_warning(self): def test_log_warning(self): mod, avail = attempt_import( 'pyomo.common.tests.dep_mod_except', - defer_check=True, + defer_import=True, only_catch_importerror=False, ) log = StringIO() @@ -366,9 +366,9 @@ def test_importer(self): def _importer(): attempted_import.append(True) - return attempt_import('pyomo.common.tests.dep_mod', defer_check=False)[0] + return attempt_import('pyomo.common.tests.dep_mod', defer_import=False)[0] - mod, avail = attempt_import('foo', importer=_importer, defer_check=True) + mod, avail = attempt_import('foo', importer=_importer, defer_import=True) self.assertEqual(attempted_import, []) self.assertIsInstance(mod, DeferredImportModule) @@ -401,17 +401,17 @@ def test_deferred_submodules(self): self.assertTrue(inspect.ismodule(deps.dm)) with self.assertRaisesRegex( - ValueError, "deferred_submodules is only valid if defer_check==True" + ValueError, "deferred_submodules is only valid if defer_import==True" ): mod, mod_available = attempt_import( 'nonexisting.module', - defer_check=False, + defer_import=False, deferred_submodules={'submod': None}, ) mod, mod_available = attempt_import( 'nonexisting.module', - defer_check=True, + defer_import=True, deferred_submodules={'submod.subsubmod': None}, ) self.assertIs(type(mod), DeferredImportModule) @@ -427,7 +427,7 @@ def test_UnavailableClass(self): module_obj, module_available = attempt_import( '__there_is_no_module_named_this__', 'Testing import of a non-existent module', - defer_check=False, + defer_import=False, ) class A_Class(UnavailableClass(module_obj)): diff --git a/pyomo/common/tests/test_deprecated.py b/pyomo/common/tests/test_deprecated.py index 1fb4a471740..37e1ba81bb3 100644 --- a/pyomo/common/tests/test_deprecated.py +++ b/pyomo/common/tests/test_deprecated.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -529,7 +529,10 @@ class DeprecatedClassSubclass(DeprecatedClass): out = StringIO() with LoggingIntercept(out): - class DeprecatedClassSubSubclass(DeprecatedClassSubclass): + class otherClass: + pass + + class DeprecatedClassSubSubclass(DeprecatedClassSubclass, otherClass): attr = 'DeprecatedClassSubSubclass' self.assertEqual(out.getvalue(), "") diff --git a/pyomo/common/tests/test_download.py b/pyomo/common/tests/test_download.py index 8c41edc1512..8fee0ba7e31 100644 --- a/pyomo/common/tests/test_download.py +++ b/pyomo/common/tests/test_download.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,12 +9,14 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import io import os import platform import re import shutil -import tempfile import subprocess +import tarfile +import tempfile import pyomo.common.unittest as unittest import pyomo.common.envvar as envvar @@ -22,6 +24,7 @@ from pyomo.common import DeveloperError from pyomo.common.fileutils import this_file from pyomo.common.download import FileDownloader, distro_available +from pyomo.common.log import LoggingIntercept from pyomo.common.tee import capture_output @@ -242,7 +245,7 @@ def test_get_files_requires_set_destination(self): ): f.get_gzipped_binary_file('bogus') - def test_get_test_binary_file(self): + def test_get_text_binary_file(self): tmpdir = tempfile.mkdtemp() try: f = FileDownloader() @@ -263,3 +266,66 @@ def test_get_test_binary_file(self): self.assertEqual(os.path.getsize(target), len(os.linesep)) finally: shutil.rmtree(tmpdir) + + def test_get_tar_archive(self): + tmpdir = tempfile.mkdtemp() + try: + f = FileDownloader() + + # Mock retrieve_url so network connections are not necessary + buf = io.BytesIO() + with tarfile.open(mode="w:gz", fileobj=buf) as TAR: + info = tarfile.TarInfo('b/lnk') + info.size = 0 + info.type = tarfile.SYMTYPE + info.linkname = envvar.PYOMO_CONFIG_DIR + TAR.addfile(info) + for fname in ('a', 'b/c', 'b/d', '/root', 'b/lnk/test'): + info = tarfile.TarInfo(fname) + info.size = 0 + info.type = tarfile.REGTYPE + info.mode = 0o644 + info.mtime = info.uid = info.gid = 0 + info.uname = info.gname = 'root' + TAR.addfile(info) + f.retrieve_url = lambda url: buf.getvalue() + + with self.assertRaisesRegex( + DeveloperError, + r"(?s)target file name has not been initialized " + r"with set_destination_filename".replace(' ', r'\s+'), + ): + f.get_tar_archive(None, 1) + + _tmp = os.path.join(tmpdir, 'a_file') + with open(_tmp, 'w'): + pass + f.set_destination_filename(_tmp) + with self.assertRaisesRegex( + RuntimeError, + r"Target directory \(.*a_file\) exists, but is not a directory", + ): + f.get_tar_archive(None, 1) + + f.set_destination_filename(tmpdir) + with LoggingIntercept() as LOG: + f.get_tar_archive(None, 1) + + self.assertEqual( + LOG.getvalue().strip(), + """ +Skipping file (a) in tar archive due to dirOffset. +malformed or potentially insecure filename (/root). Skipping file. +potentially insecure filename (lnk/test) resolves outside target directory. Skipping file. +""".strip(), + ) + for f in ('c', 'd'): + fname = os.path.join(tmpdir, f) + self.assertTrue(os.path.exists(fname)) + self.assertTrue(os.path.isfile(fname)) + for f in ('lnk',): + fname = os.path.join(tmpdir, f) + self.assertTrue(os.path.exists(fname)) + self.assertTrue(os.path.islink(fname)) + finally: + shutil.rmtree(tmpdir) diff --git a/pyomo/common/tests/test_enums.py b/pyomo/common/tests/test_enums.py new file mode 100644 index 00000000000..80d081505e9 --- /dev/null +++ b/pyomo/common/tests/test_enums.py @@ -0,0 +1,97 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import enum + +import pyomo.common.unittest as unittest + +from pyomo.common.enums import ExtendedEnumType, ObjectiveSense + + +class ProblemSense(enum.IntEnum, metaclass=ExtendedEnumType): + __base_enum__ = ObjectiveSense + + unknown = 0 + + +class TestExtendedEnumType(unittest.TestCase): + def test_members(self): + self.assertEqual( + list(ProblemSense), + [ProblemSense.unknown, ObjectiveSense.minimize, ObjectiveSense.maximize], + ) + + def test_isinstance(self): + self.assertIsInstance(ProblemSense.unknown, ProblemSense) + self.assertIsInstance(ProblemSense.minimize, ProblemSense) + self.assertIsInstance(ProblemSense.maximize, ProblemSense) + + self.assertTrue(ProblemSense.__instancecheck__(ProblemSense.unknown)) + self.assertTrue(ProblemSense.__instancecheck__(ProblemSense.minimize)) + self.assertTrue(ProblemSense.__instancecheck__(ProblemSense.maximize)) + + def test_getattr(self): + self.assertIs(ProblemSense.unknown, ProblemSense.unknown) + self.assertIs(ProblemSense.minimize, ObjectiveSense.minimize) + self.assertIs(ProblemSense.maximize, ObjectiveSense.maximize) + + def test_hasattr(self): + self.assertTrue(hasattr(ProblemSense, 'unknown')) + self.assertTrue(hasattr(ProblemSense, 'minimize')) + self.assertTrue(hasattr(ProblemSense, 'maximize')) + + def test_call(self): + self.assertIs(ProblemSense(0), ProblemSense.unknown) + self.assertIs(ProblemSense(1), ObjectiveSense.minimize) + self.assertIs(ProblemSense(-1), ObjectiveSense.maximize) + + self.assertIs(ProblemSense('unknown'), ProblemSense.unknown) + self.assertIs(ProblemSense('minimize'), ObjectiveSense.minimize) + self.assertIs(ProblemSense('maximize'), ObjectiveSense.maximize) + + with self.assertRaisesRegex(ValueError, "'foo' is not a valid ProblemSense"): + ProblemSense('foo') + with self.assertRaisesRegex(ValueError, "2 is not a valid ProblemSense"): + ProblemSense(2) + + def test_contains(self): + self.assertIn(ProblemSense.unknown, ProblemSense) + self.assertIn(ProblemSense.minimize, ProblemSense) + self.assertIn(ProblemSense.maximize, ProblemSense) + + self.assertNotIn(ProblemSense.unknown, ObjectiveSense) + self.assertIn(ProblemSense.minimize, ObjectiveSense) + self.assertIn(ProblemSense.maximize, ObjectiveSense) + + +class TestObjectiveSense(unittest.TestCase): + def test_members(self): + self.assertEqual( + list(ObjectiveSense), [ObjectiveSense.minimize, ObjectiveSense.maximize] + ) + + def test_hasattr(self): + self.assertTrue(hasattr(ProblemSense, 'minimize')) + self.assertTrue(hasattr(ProblemSense, 'maximize')) + + def test_call(self): + self.assertIs(ObjectiveSense(1), ObjectiveSense.minimize) + self.assertIs(ObjectiveSense(-1), ObjectiveSense.maximize) + + self.assertIs(ObjectiveSense('minimize'), ObjectiveSense.minimize) + self.assertIs(ObjectiveSense('maximize'), ObjectiveSense.maximize) + + with self.assertRaisesRegex(ValueError, "'foo' is not a valid ObjectiveSense"): + ObjectiveSense('foo') + + def test_str(self): + self.assertEqual(str(ObjectiveSense.minimize), 'minimize') + self.assertEqual(str(ObjectiveSense.maximize), 'maximize') diff --git a/pyomo/common/tests/test_env.py b/pyomo/common/tests/test_env.py index d14326ddc19..93802fc40bb 100644 --- a/pyomo/common/tests/test_env.py +++ b/pyomo/common/tests/test_env.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_errors.py b/pyomo/common/tests/test_errors.py index ec77643f722..67a200e84e3 100644 --- a/pyomo/common/tests/test_errors.py +++ b/pyomo/common/tests/test_errors.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_fileutils.py b/pyomo/common/tests/test_fileutils.py index 63570774e5b..068360b55cb 100644 --- a/pyomo/common/tests/test_fileutils.py +++ b/pyomo/common/tests/test_fileutils.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_formatting.py b/pyomo/common/tests/test_formatting.py index d502c81da5a..29db26676ab 100644 --- a/pyomo/common/tests/test_formatting.py +++ b/pyomo/common/tests/test_formatting.py @@ -2,7 +2,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_gc.py b/pyomo/common/tests/test_gc.py index b2f23102a0e..176010b8d0d 100644 --- a/pyomo/common/tests/test_gc.py +++ b/pyomo/common/tests/test_gc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_log.py b/pyomo/common/tests/test_log.py index 39fab153e98..166e1e44cdb 100644 --- a/pyomo/common/tests/test_log.py +++ b/pyomo/common/tests/test_log.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -511,7 +511,6 @@ def test_verbatim(self): "\n" " quote block\n" ) - self.maxDiff = None self.assertEqual(self.stream.getvalue(), ans) diff --git a/pyomo/common/tests/test_modeling.py b/pyomo/common/tests/test_modeling.py index 0684d77b2e9..97bef76c2c0 100644 --- a/pyomo/common/tests/test_modeling.py +++ b/pyomo/common/tests/test_modeling.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_multithread.py b/pyomo/common/tests/test_multithread.py index ae1bc48be44..fa1a46fa25f 100644 --- a/pyomo/common/tests/test_multithread.py +++ b/pyomo/common/tests/test_multithread.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import threading import pyomo.common.unittest as unittest from pyomo.common.multithread import * diff --git a/pyomo/common/tests/test_numeric_types.py b/pyomo/common/tests/test_numeric_types.py new file mode 100644 index 00000000000..b7ffb5fb255 --- /dev/null +++ b/pyomo/common/tests/test_numeric_types.py @@ -0,0 +1,219 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.numeric_types as nt +import pyomo.common.unittest as unittest + +from pyomo.common.dependencies import numpy, numpy_available +from pyomo.core.expr import LinearExpression +from pyomo.environ import Var + +_type_sets = ( + 'native_types', + 'native_numeric_types', + 'native_logical_types', + 'native_integer_types', + 'native_complex_types', +) + + +class TestNativeTypes(unittest.TestCase): + def setUp(self): + bool(numpy_available) + for s in _type_sets: + setattr(self, s, set(getattr(nt, s))) + getattr(nt, s).clear() + + def tearDown(self): + for s in _type_sets: + getattr(nt, s).clear() + getattr(nt, s).update(getattr(self, s)) + + def test_check_if_native_type(self): + self.assertEqual(nt.native_types, set()) + self.assertEqual(nt.native_logical_types, set()) + self.assertEqual(nt.native_numeric_types, set()) + self.assertEqual(nt.native_integer_types, set()) + self.assertEqual(nt.native_complex_types, set()) + + self.assertTrue(nt.check_if_native_type("a")) + self.assertIn(str, nt.native_types) + self.assertNotIn(str, nt.native_logical_types) + self.assertNotIn(str, nt.native_numeric_types) + self.assertNotIn(str, nt.native_integer_types) + self.assertNotIn(str, nt.native_complex_types) + + self.assertTrue(nt.check_if_native_type(1)) + self.assertIn(int, nt.native_types) + self.assertNotIn(int, nt.native_logical_types) + self.assertIn(int, nt.native_numeric_types) + self.assertIn(int, nt.native_integer_types) + self.assertNotIn(int, nt.native_complex_types) + + self.assertTrue(nt.check_if_native_type(1.5)) + self.assertIn(float, nt.native_types) + self.assertNotIn(float, nt.native_logical_types) + self.assertIn(float, nt.native_numeric_types) + self.assertNotIn(float, nt.native_integer_types) + self.assertNotIn(float, nt.native_complex_types) + + self.assertTrue(nt.check_if_native_type(True)) + self.assertIn(bool, nt.native_types) + self.assertIn(bool, nt.native_logical_types) + self.assertNotIn(bool, nt.native_numeric_types) + self.assertNotIn(bool, nt.native_integer_types) + self.assertNotIn(bool, nt.native_complex_types) + + self.assertFalse(nt.check_if_native_type(slice(None, None, None))) + self.assertNotIn(slice, nt.native_types) + self.assertNotIn(slice, nt.native_logical_types) + self.assertNotIn(slice, nt.native_numeric_types) + self.assertNotIn(slice, nt.native_integer_types) + self.assertNotIn(slice, nt.native_complex_types) + + def test_check_if_logical_type(self): + self.assertEqual(nt.native_types, set()) + self.assertEqual(nt.native_logical_types, set()) + self.assertEqual(nt.native_numeric_types, set()) + self.assertEqual(nt.native_integer_types, set()) + self.assertEqual(nt.native_complex_types, set()) + + self.assertFalse(nt.check_if_logical_type("a")) + self.assertNotIn(str, nt.native_types) + self.assertNotIn(str, nt.native_logical_types) + self.assertNotIn(str, nt.native_numeric_types) + self.assertNotIn(str, nt.native_integer_types) + self.assertNotIn(str, nt.native_complex_types) + + self.assertFalse(nt.check_if_logical_type("a")) + + self.assertTrue(nt.check_if_logical_type(True)) + self.assertIn(bool, nt.native_types) + self.assertIn(bool, nt.native_logical_types) + self.assertNotIn(bool, nt.native_numeric_types) + self.assertNotIn(bool, nt.native_integer_types) + self.assertNotIn(bool, nt.native_complex_types) + + self.assertTrue(nt.check_if_logical_type(True)) + + self.assertFalse(nt.check_if_logical_type(1)) + self.assertNotIn(int, nt.native_types) + self.assertNotIn(int, nt.native_logical_types) + self.assertNotIn(int, nt.native_numeric_types) + self.assertNotIn(int, nt.native_integer_types) + self.assertNotIn(int, nt.native_complex_types) + + if numpy_available: + self.assertTrue(nt.check_if_logical_type(numpy.bool_(1))) + self.assertIn(numpy.bool_, nt.native_types) + self.assertIn(numpy.bool_, nt.native_logical_types) + self.assertNotIn(numpy.bool_, nt.native_numeric_types) + self.assertNotIn(numpy.bool_, nt.native_integer_types) + self.assertNotIn(numpy.bool_, nt.native_complex_types) + + def test_check_if_numeric_type(self): + self.assertEqual(nt.native_types, set()) + self.assertEqual(nt.native_logical_types, set()) + self.assertEqual(nt.native_numeric_types, set()) + self.assertEqual(nt.native_integer_types, set()) + self.assertEqual(nt.native_complex_types, set()) + + self.assertFalse(nt.check_if_numeric_type("a")) + self.assertFalse(nt.check_if_numeric_type("a")) + self.assertNotIn(str, nt.native_types) + self.assertNotIn(str, nt.native_logical_types) + self.assertNotIn(str, nt.native_numeric_types) + self.assertNotIn(str, nt.native_integer_types) + self.assertNotIn(str, nt.native_complex_types) + + self.assertFalse(nt.check_if_numeric_type(True)) + self.assertFalse(nt.check_if_numeric_type(True)) + self.assertNotIn(bool, nt.native_types) + self.assertNotIn(bool, nt.native_logical_types) + self.assertNotIn(bool, nt.native_numeric_types) + self.assertNotIn(bool, nt.native_integer_types) + self.assertNotIn(bool, nt.native_complex_types) + + self.assertTrue(nt.check_if_numeric_type(1)) + self.assertTrue(nt.check_if_numeric_type(1)) + self.assertIn(int, nt.native_types) + self.assertNotIn(int, nt.native_logical_types) + self.assertIn(int, nt.native_numeric_types) + self.assertIn(int, nt.native_integer_types) + self.assertNotIn(int, nt.native_complex_types) + + self.assertTrue(nt.check_if_numeric_type(1.5)) + self.assertTrue(nt.check_if_numeric_type(1.5)) + self.assertIn(float, nt.native_types) + self.assertNotIn(float, nt.native_logical_types) + self.assertIn(float, nt.native_numeric_types) + self.assertNotIn(float, nt.native_integer_types) + self.assertNotIn(float, nt.native_complex_types) + + self.assertFalse(nt.check_if_numeric_type(1j)) + self.assertIn(complex, nt.native_types) + self.assertNotIn(complex, nt.native_logical_types) + self.assertNotIn(complex, nt.native_numeric_types) + self.assertNotIn(complex, nt.native_integer_types) + self.assertIn(complex, nt.native_complex_types) + + v = Var() + v.construct() + self.assertFalse(nt.check_if_numeric_type(v)) + self.assertNotIn(type(v), nt.native_types) + self.assertNotIn(type(v), nt.native_logical_types) + self.assertNotIn(type(v), nt.native_numeric_types) + self.assertNotIn(type(v), nt.native_integer_types) + self.assertNotIn(type(v), nt.native_complex_types) + + e = LinearExpression([1]) + self.assertFalse(nt.check_if_numeric_type(e)) + self.assertNotIn(type(e), nt.native_types) + self.assertNotIn(type(e), nt.native_logical_types) + self.assertNotIn(type(e), nt.native_numeric_types) + self.assertNotIn(type(e), nt.native_integer_types) + self.assertNotIn(type(e), nt.native_complex_types) + + if numpy_available: + self.assertFalse(nt.check_if_numeric_type(numpy.bool_(1))) + self.assertNotIn(numpy.bool_, nt.native_types) + self.assertNotIn(numpy.bool_, nt.native_logical_types) + self.assertNotIn(numpy.bool_, nt.native_numeric_types) + self.assertNotIn(numpy.bool_, nt.native_integer_types) + self.assertNotIn(numpy.bool_, nt.native_complex_types) + + self.assertFalse(nt.check_if_numeric_type(numpy.array([1]))) + self.assertNotIn(numpy.ndarray, nt.native_types) + self.assertNotIn(numpy.ndarray, nt.native_logical_types) + self.assertNotIn(numpy.ndarray, nt.native_numeric_types) + self.assertNotIn(numpy.ndarray, nt.native_integer_types) + self.assertNotIn(numpy.ndarray, nt.native_complex_types) + + self.assertTrue(nt.check_if_numeric_type(numpy.float64(1))) + self.assertIn(numpy.float64, nt.native_types) + self.assertNotIn(numpy.float64, nt.native_logical_types) + self.assertIn(numpy.float64, nt.native_numeric_types) + self.assertNotIn(numpy.float64, nt.native_integer_types) + self.assertNotIn(numpy.float64, nt.native_complex_types) + + self.assertTrue(nt.check_if_numeric_type(numpy.int64(1))) + self.assertIn(numpy.int64, nt.native_types) + self.assertNotIn(numpy.int64, nt.native_logical_types) + self.assertIn(numpy.int64, nt.native_numeric_types) + self.assertIn(numpy.int64, nt.native_integer_types) + self.assertNotIn(numpy.int64, nt.native_complex_types) + + self.assertFalse(nt.check_if_numeric_type(numpy.complex128(1))) + self.assertIn(numpy.complex128, nt.native_types) + self.assertNotIn(numpy.complex128, nt.native_logical_types) + self.assertNotIn(numpy.complex128, nt.native_numeric_types) + self.assertNotIn(numpy.complex128, nt.native_integer_types) + self.assertIn(numpy.complex128, nt.native_complex_types) diff --git a/pyomo/common/tests/test_orderedset.py b/pyomo/common/tests/test_orderedset.py index d87bebc1e4a..8f944e66bd7 100644 --- a/pyomo/common/tests/test_orderedset.py +++ b/pyomo/common/tests/test_orderedset.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_plugin.py b/pyomo/common/tests/test_plugin.py index 86d136dd9d1..54431334d5b 100644 --- a/pyomo/common/tests/test_plugin.py +++ b/pyomo/common/tests/test_plugin.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_sorting.py b/pyomo/common/tests/test_sorting.py index 7a9fe5ac923..7fbefda6a19 100644 --- a/pyomo/common/tests/test_sorting.py +++ b/pyomo/common/tests/test_sorting.py @@ -2,7 +2,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_tee.py b/pyomo/common/tests/test_tee.py index 666a431631f..a5c6ee894b2 100644 --- a/pyomo/common/tests/test_tee.py +++ b/pyomo/common/tests/test_tee.py @@ -2,7 +2,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_tempfile.py b/pyomo/common/tests/test_tempfile.py index b82082ac1af..c49aa8c6771 100644 --- a/pyomo/common/tests/test_tempfile.py +++ b/pyomo/common/tests/test_tempfile.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -30,6 +30,7 @@ import pyomo.common.tempfiles as tempfiles +from pyomo.common.dependencies import pyutilib_available from pyomo.common.log import LoggingIntercept from pyomo.common.tempfiles import ( TempfileManager, @@ -37,11 +38,6 @@ TempfileContextError, ) -try: - from pyutilib.component.config.tempfiles import TempfileManager as pyutilib_mngr -except ImportError: - pyutilib_mngr = None - old_tempdir = TempfileManager.tempdir tempdir = None @@ -528,13 +524,13 @@ def test_open_tempfile_windows(self): f.close() os.remove(fname) - @unittest.skipIf(pyutilib_mngr is None, "deprecation test requires pyutilib") + @unittest.skipUnless(pyutilib_available, "deprecation test requires pyutilib") def test_deprecated_tempdir(self): self.TM.push() try: tmpdir = self.TM.create_tempdir() - _orig = pyutilib_mngr.tempdir - pyutilib_mngr.tempdir = tmpdir + _orig = tempfiles.pyutilib_tempfiles.TempfileManager.tempdir + tempfiles.pyutilib_tempfiles.TempfileManager.tempdir = tmpdir self.TM.tempdir = None with LoggingIntercept() as LOG: @@ -556,7 +552,7 @@ def test_deprecated_tempdir(self): ) finally: self.TM.pop() - pyutilib_mngr.tempdir = _orig + tempfiles.pyutilib_tempfiles.TempfileManager.tempdir = _orig def test_context(self): with self.assertRaisesRegex( diff --git a/pyomo/common/tests/test_timing.py b/pyomo/common/tests/test_timing.py index d2ce6175801..90f4cdcd034 100644 --- a/pyomo/common/tests/test_timing.py +++ b/pyomo/common/tests/test_timing.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -14,6 +14,7 @@ import gc from io import StringIO +from itertools import zip_longest import logging import sys import time @@ -26,8 +27,15 @@ TicTocTimer, HierarchicalTimer, ) -from pyomo.environ import ConcreteModel, RangeSet, Var, Any, TransformationFactory -from pyomo.core.base.var import _VarData +from pyomo.environ import ( + AbstractModel, + ConcreteModel, + RangeSet, + Var, + Any, + TransformationFactory, +) +from pyomo.core.base.var import VarData class _pseudo_component(Var): @@ -54,7 +62,7 @@ def test_raw_construction_timer(self): ) v = Var() v.construct() - a = ConstructionTimer(_VarData(v)) + a = ConstructionTimer(VarData(v)) self.assertRegex( str(a), r"ConstructionTimer object for Var ScalarVar\[NOTSET\]; " @@ -99,7 +107,6 @@ def test_report_timing(self): m.y = Var(Any, dense=False) xfrm.apply_to(m) result = out.getvalue().strip() - self.maxDiff = None for l, r in zip(result.splitlines(), ref.splitlines()): self.assertRegex(str(l.strip()), str(r.strip())) finally: @@ -114,7 +121,6 @@ def test_report_timing(self): m.y = Var(Any, dense=False) xfrm.apply_to(m) result = os.getvalue().strip() - self.maxDiff = None for l, r in zip(result.splitlines(), ref.splitlines()): self.assertRegex(str(l.strip()), str(r.strip())) finally: @@ -127,11 +133,51 @@ def test_report_timing(self): m.y = Var(Any, dense=False) xfrm.apply_to(m) result = os.getvalue().strip() - self.maxDiff = None for l, r in zip(result.splitlines(), ref.splitlines()): self.assertRegex(str(l.strip()), str(r.strip())) self.assertEqual(buf.getvalue().strip(), "") + def test_report_timing_context_manager(self): + ref = r""" + (0(\.\d+)?) seconds to construct Var x; 2 indices total + (0(\.\d+)?) seconds to construct Var y; 0 indices total + (0(\.\d+)?) seconds to construct Suffix Suffix + (0(\.\d+)?) seconds to apply Transformation RelaxIntegerVars \(in-place\) + """.strip() + + xfrm = TransformationFactory('core.relax_integer_vars') + + model = AbstractModel() + model.r = RangeSet(2) + model.x = Var(model.r) + model.y = Var(Any, dense=False) + + OS = StringIO() + + with report_timing(False): + with report_timing(OS): + with report_timing(False): + # Active reporting is False: nothing should be emitted + with capture_output() as OUT: + m = model.create_instance() + xfrm.apply_to(m) + self.assertEqual(OUT.getvalue(), "") + self.assertEqual(OS.getvalue(), "") + # Active reporting: we should log the timing + with capture_output() as OUT: + m = model.create_instance() + xfrm.apply_to(m) + self.assertEqual(OUT.getvalue(), "") + result = OS.getvalue().strip() + for l, r in zip_longest(result.splitlines(), ref.splitlines()): + self.assertRegex(str(l.strip()), str(r.strip())) + # Active reporting is False: the previous log should not have changed + with capture_output() as OUT: + m = model.create_instance() + xfrm.apply_to(m) + self.assertEqual(OUT.getvalue(), "") + self.assertEqual(result, OS.getvalue().strip()) + def test_TicTocTimer_tictoc(self): SLEEP = 0.1 RES = 0.02 # resolution (seconds): 1/5 the sleep diff --git a/pyomo/common/tests/test_typing.py b/pyomo/common/tests/test_typing.py index 982462f8a8d..e65effe7f29 100644 --- a/pyomo/common/tests/test_typing.py +++ b/pyomo/common/tests/test_typing.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_unittest.py b/pyomo/common/tests/test_unittest.py index a87fc57da9e..9344853b737 100644 --- a/pyomo/common/tests/test_unittest.py +++ b/pyomo/common/tests/test_unittest.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -11,11 +11,14 @@ import datetime import multiprocessing -from io import StringIO +import os import time +from io import StringIO import pyomo.common.unittest as unittest from pyomo.common.log import LoggingIntercept +from pyomo.common.tee import capture_output +from pyomo.common.tempfiles import TempfileManager from pyomo.environ import ConcreteModel, Var, Param @@ -236,5 +239,319 @@ def test_bound_function_require_fork(self): self.bound_function_require_fork() +baseline = """ +[ 0.00] Setting up Pyomo environment +[ 0.00] Applying Pyomo preprocessing actions +[ 0.00] Creating model +[ 0.00] Applying solver +[ 0.05] Processing results + Number of solutions: 1 + Solution Information + Gap: None + Status: optimal + Function Value: -9.99943939749e-05 + Solver results file: results.yml +[ 0.05] Applying Pyomo postprocessing actions +[ 0.05] Pyomo Finished +# ========================================================== +# = Solver Results = +# ========================================================== +# ---------------------------------------------------------- +# Problem Information +# ---------------------------------------------------------- +Problem: +- Lower bound: -inf + Upper bound: inf + Number of objectives: 1 + Number of constraints: 3 + Number of variables: 3 + Sense: unknown +# ---------------------------------------------------------- +# Solver Information +# ---------------------------------------------------------- +Solver: +- Status: ok + Message: Ipopt 3.12.3\x3a Optimal Solution Found + Termination condition: optimal + Id: 0 + Error rc: 0 + Time: 0.0408430099487 +# ---------------------------------------------------------- +# Solution Information +# ---------------------------------------------------------- +Solution: +- number of solutions: 1 + number of solutions displayed: 1 +- Gap: None + Status: optimal + Message: Ipopt 3.12.3\x3a Optimal Solution Found + Objective: + f1: + Value: -9.99943939749e-05 + Variable: + compl.v: + Value: 9.99943939749e-05 + y: + Value: 9.99943939749e-05 + Constraint: No values +""" + +pass_ref = """ +[ 0.00] Setting up Pyomo environment +[ 0.00] Applying Pyomo preprocessing actions +WARNING: DEPRECATED: The Model.preprocess() method is deprecated and no longer + performs any actions (deprecated in 6.0) (called from :1) +[ 0.00] Creating model +[ 0.01] Applying solver +[ 0.06] Processing results + Number of solutions: 1 + Solution Information + Gap: None + Status: optimal + Function Value: -0.00010001318188373491 + Solver results file: results.yml +[ 0.06] Applying Pyomo postprocessing actions +[ 0.06] Pyomo Finished +# ========================================================== +# = Solver Results = +# ========================================================== +# ---------------------------------------------------------- +# Problem Information +# ---------------------------------------------------------- +Problem: +- Lower bound: -inf + Upper bound: inf + Number of objectives: 1 + Number of constraints: 3 + Number of variables: 3 + Sense: unknown +# ---------------------------------------------------------- +# Solver Information +# ---------------------------------------------------------- +Solver: +- Status: ok + Message: Ipopt 3.14.13\x3a Optimal Solution Found + Termination condition: optimal + Id: 0 + Error rc: 0 + Time: 0.04224729537963867 +# ---------------------------------------------------------- +# Solution Information +# ---------------------------------------------------------- +Solution: +- number of solutions: 1 + number of solutions displayed: 1 +- Gap: None + Status: optimal + Message: Ipopt 3.14.13\x3a Optimal Solution Found + Objective: + f1: + Value: -0.00010001318188373491 + Variable: + compl.v: + Value: 9.99943939749205e-05 + x: + Value: -9.39395440720558e-09 + y: + Value: 9.99943939749205e-05 + Constraint: No values + +""" + +fail_ref = """ +[ 0.00] Setting up Pyomo environment +[ 0.00] Applying Pyomo preprocessing actions +[ 0.00] Creating model +[ 0.01] Applying solver +[ 0.06] Processing results + Number of solutions: 1 + Solution Information + Gap: None + Status: optimal + Function Value: -0.00010001318188373491 + Solver results file: results.yml +[ 0.06] Applying Pyomo postprocessing actions +[ 0.06] Pyomo Finished +# ========================================================== +# = Solver Results = +# ========================================================== +# ---------------------------------------------------------- +# Problem Information +# ---------------------------------------------------------- +Problem: +- Lower bound: -inf + Upper bound: inf + Number of objectives: 1 + Number of constraints: 3 + Number of variables: 3 + Sense: unknown +# ---------------------------------------------------------- +# Solver Information +# ---------------------------------------------------------- +Solver: +- Status: ok + Message: Ipopt 3.14.13\x3a Optimal Solution Found + Termination condition: optimal + Id: 0 + Error rc: 0 + Time: 0.04224729537963867 +# ---------------------------------------------------------- +# Solution Information +# ---------------------------------------------------------- +Solution: +- number of solutions: 1 + number of solutions displayed: 1 +- Gap: None + Status: optimal + Message: Ipopt 3.14.13\x3a Optimal Solution Found + Objective: + f1: + Value: -0.00010001318188373491 + Variable: + compl.v: + Value: 9.79943939749205e-05 + x: + Value: -9.39395440720558e-09 + y: + Value: 9.99943939749205e-05 + Constraint: No values + +""" + + +class TestBaselineTestDriver(unittest.BaselineTestDriver, unittest.TestCase): + solver_dependencies = {} + package_dependencies = {} + + def test_baseline_pass(self): + self.compare_baseline(pass_ref, baseline, abstol=1e-6) + + with self.assertRaises(self.failureException): + with capture_output() as OUT: + self.compare_baseline(pass_ref, baseline, None) + self.assertEqual( + OUT.getvalue(), + f"""--------------------------------- +BASELINE FILE +--------------------------------- +{baseline} +================================= +--------------------------------- +TEST OUTPUT FILE +--------------------------------- +{pass_ref} +""", + ) + + def test_baseline_fail(self): + with self.assertRaises(self.failureException): + with capture_output() as OUT: + self.compare_baseline(fail_ref, baseline) + self.assertEqual( + OUT.getvalue(), + f"""--------------------------------- +BASELINE FILE +--------------------------------- +{baseline} +================================= +--------------------------------- +TEST OUTPUT FILE +--------------------------------- +{fail_ref} +""", + ) + + def test_testcase_collection(self): + with TempfileManager.new_context() as TMP: + tmpdir = TMP.create_tempdir() + for fname in ( + 'a.py', + 'b.py', + 'b.txt', + 'c.py', + 'c.sh', + 'c.yml', + 'd.sh', + 'd.txt', + 'e.sh', + ): + with open(os.path.join(tmpdir, fname), 'w'): + pass + + py_tests, sh_tests = unittest.BaselineTestDriver.gather_tests([tmpdir]) + self.assertEqual( + py_tests, + [ + ( + os.path.basename(tmpdir) + '_b', + os.path.join(tmpdir, 'b.py'), + os.path.join(tmpdir, 'b.txt'), + ) + ], + ) + self.assertEqual( + sh_tests, + [ + ( + os.path.basename(tmpdir) + '_c', + os.path.join(tmpdir, 'c.sh'), + os.path.join(tmpdir, 'c.yml'), + ), + ( + os.path.basename(tmpdir) + '_d', + os.path.join(tmpdir, 'd.sh'), + os.path.join(tmpdir, 'd.txt'), + ), + ], + ) + + self.python_test_driver(*py_tests[0]) + + _update_baselines = os.environ.pop('PYOMO_TEST_UPDATE_BASELINES', None) + try: + with open(os.path.join(tmpdir, 'b.py'), 'w') as FILE: + FILE.write('print("Hello, World")\n') + + with self.assertRaises(self.failureException): + self.python_test_driver(*py_tests[0]) + with open(os.path.join(tmpdir, 'b.txt'), 'r') as FILE: + self.assertEqual(FILE.read(), "") + + os.environ['PYOMO_TEST_UPDATE_BASELINES'] = '1' + + with self.assertRaises(self.failureException): + self.python_test_driver(*py_tests[0]) + with open(os.path.join(tmpdir, 'b.txt'), 'r') as FILE: + self.assertEqual(FILE.read(), "Hello, World\n") + + finally: + os.environ.pop('PYOMO_TEST_UPDATE_BASELINES', None) + if _update_baselines is not None: + os.environ['PYOMO_TEST_UPDATE_BASELINES'] = _update_baselines + + self.shell_test_driver(*sh_tests[1]) + _update_baselines = os.environ.pop('PYOMO_TEST_UPDATE_BASELINES', None) + try: + with open(os.path.join(tmpdir, 'd.sh'), 'w') as FILE: + FILE.write('echo "Hello, World"\n') + + with self.assertRaises(self.failureException): + self.shell_test_driver(*sh_tests[1]) + with open(os.path.join(tmpdir, 'd.txt'), 'r') as FILE: + self.assertEqual(FILE.read(), "") + + os.environ['PYOMO_TEST_UPDATE_BASELINES'] = '1' + + with self.assertRaises(self.failureException): + self.shell_test_driver(*sh_tests[1]) + with open(os.path.join(tmpdir, 'd.txt'), 'r') as FILE: + self.assertEqual(FILE.read(), "Hello, World\n") + + finally: + os.environ.pop('PYOMO_TEST_UPDATE_BASELINES', None) + if _update_baselines is not None: + os.environ['PYOMO_TEST_UPDATE_BASELINES'] = _update_baselines + + if __name__ == '__main__': unittest.main() diff --git a/pyomo/common/timing.py b/pyomo/common/timing.py index 96360c61a1b..d502b38d12d 100644 --- a/pyomo/common/timing.py +++ b/pyomo/common/timing.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -44,30 +44,59 @@ _transform_logger = logging.getLogger('pyomo.common.timing.transformation') -def report_timing(stream=True, level=logging.INFO): - """Set reporting of Pyomo timing information. +class report_timing(object): + def __init__(self, stream=True, level=logging.INFO): + """Set reporting of Pyomo timing information. - Parameters - ---------- - stream: bool, TextIOBase - The destination stream to emit timing information. If ``True``, - defaults to ``sys.stdout``. If ``False`` or ``None``, disables - reporting of timing information. - level: int - The logging level for the timing logger - """ - if stream: - _logger.setLevel(level) - if stream is True: - stream = sys.stdout - handler = logging.StreamHandler(stream) - handler.setFormatter(logging.Formatter(" %(message)s")) - _logger.addHandler(handler) - return handler - else: - _logger.setLevel(logging.WARNING) - for h in _logger.handlers: - _logger.removeHandler(h) + For historical reasons, this class may be used as a function + (the reporting logger is configured as part of the instance + initializer). However, the preferred usage is as a context + manager (thereby ensuring that the timing logger is restored + upon exit). + + Parameters + ---------- + stream: bool, TextIOBase + + The destination stream to emit timing information. If + ``True``, defaults to ``sys.stdout``. If ``False`` or + ``None``, disables reporting of timing information. + + level: int + + The logging level for the timing logger + + """ + self._old_level = _logger.level + # For historical reasons (because report_timing() used to be a + # function), we will do what you think should be done in + # __enter__ here in __init__. + if stream: + _logger.setLevel(level) + if stream is True: + stream = sys.stdout + self._handler = logging.StreamHandler(stream) + self._handler.setFormatter(logging.Formatter(" %(message)s")) + _logger.addHandler(self._handler) + else: + self._handler = list(_logger.handlers) + _logger.setLevel(logging.WARNING) + for h in list(_logger.handlers): + _logger.removeHandler(h) + + def reset(self): + _logger.setLevel(self._old_level) + if type(self._handler) is list: + for h in self._handler: + _logger.addHandler(h) + else: + _logger.removeHandler(self._handler) + + def __enter__(self): + return self + + def __exit__(self, et, ev, tb): + self.reset() class GeneralTimer(object): @@ -194,19 +223,14 @@ def __str__(self): # # Setup the timer # -# TODO: Remove this bit for Pyomo 6.0 - we won't care about older versions -if sys.version_info >= (3, 3): - # perf_counter is guaranteed to be monotonic and the most accurate timer - default_timer = time.perf_counter -elif sys.platform.startswith('win'): - # On old Pythons, clock() is more accurate than time() on Windows - # (.35us vs 15ms), but time() is more accurate than clock() on Linux - # (1ns vs 1us). It is unfortunate that time() is not monotonic, but - # since the TicTocTimer is used for (potentially very accurate) - # timers, we will sacrifice monotonicity on Linux for resolution. - default_timer = time.clock -else: - default_timer = time.time +# perf_counter is guaranteed to be monotonic and the most accurate +# timer. It became available in Python 3.3. Prior to that, clock() was +# more accurate than time() on Windows (.35us vs 15ms), but time() was +# more accurate than clock() on Linux (1ns vs 1us). It is unfortunate +# that time() is not monotonic, but since the TicTocTimer is used for +# (potentially very accurate) timers, we will sacrifice monotonicity on +# Linux for resolution. +default_timer = time.perf_counter class TicTocTimer(object): diff --git a/pyomo/common/unittest.py b/pyomo/common/unittest.py index c53b7f50398..84d962eb784 100644 --- a/pyomo/common/unittest.py +++ b/pyomo/common/unittest.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -17,9 +17,13 @@ # ___________________________________________________________________________ import enum +import glob import logging import math +import os +import operator import re +import subprocess import sys from io import StringIO @@ -31,7 +35,10 @@ import pytest as pytest from pyomo.common.collections import Mapping, Sequence +from pyomo.common.dependencies import attempt_import, check_min_version from pyomo.common.errors import InvalidValueError +from pyomo.common.fileutils import import_file +from pyomo.common.log import LoggingIntercept, pyomo_formatter from pyomo.common.tee import capture_output from unittest import mock @@ -491,6 +498,10 @@ class TestCase(_unittest.TestCase): __doc__ += _unittest.TestCase.__doc__ + # By default, we always want to spend the time to create the full + # diff of the test reault and the baseline + maxDiff = None + def assertStructuredAlmostEqual( self, first, @@ -547,3 +558,385 @@ def assertRaisesRegex(self, expected_exception, expected_regex, *args, **kwargs) contextClass = _unittest.case._AssertRaisesContext context = contextClass(expected_exception, self, expected_regex) return context.handle('assertRaisesRegex', args, kwargs) + + def assertExpressionsEqual(self, a, b, include_named_exprs=True, places=None): + from pyomo.core.expr.compare import assertExpressionsEqual + + return assertExpressionsEqual(self, a, b, include_named_exprs, places) + + def assertExpressionsStructurallyEqual( + self, a, b, include_named_exprs=True, places=None + ): + from pyomo.core.expr.compare import assertExpressionsStructurallyEqual + + return assertExpressionsStructurallyEqual( + self, a, b, include_named_exprs, places + ) + + +class BaselineTestDriver(object): + """Generic driver for performing baseline tests in bulk + + This test driver was originally crafted for testing the examples in + the Pyomo Book, and has since been generalized to reuse in testing + ".. literalinclude:" examples from the Online Docs. + + We expect that consumers of this class will derive from both this + class and `pyomo.common.unittest.TestCase`, and then use + `parameterized` to declare tests that call either the + :py:meth:`python_test_driver` or :py:meth:`shell_test_driver` + methods. + + Note that derived classes must declare two class attributes: + + Class Attributes + ---------------- + solver_dependencies: Dict[str, List[str]] + + maps the test name to a list of required solvers. If any solver + is not available, then the test will be skipped. + + package_dependencies: Dict[str, List[str]] + + maps the test name to a list of required modules. If any module + is not available, then the test will be skipped. + + """ + + @staticmethod + def custom_name_func(test_func, test_num, test_params): + func_name = test_func.__name__ + return "test_%s_%s" % (test_params.args[0], func_name[-2:]) + + def __init__(self, test): + # Finalize the class, if necessary... + if getattr(self.__class__, 'solver_available', None) is None: + self.initialize_dependencies() + super().__init__(test) + + def initialize_dependencies(self): + # Note: as a rule, pyomo.common is not allowed to import from + # the rest of Pyomo. we permit it here because a) this is not + # at module scope, and b) there is really no better / more + # logical place in pyomo to put this code. + from pyomo.opt import check_available_solvers + + cls = self.__class__ + # + # Initialize the availability data + # + solvers_used = set(sum(list(cls.solver_dependencies.values()), [])) + available_solvers = check_available_solvers(*solvers_used) + cls.solver_available = { + solver_: (solver_ in available_solvers) for solver_ in solvers_used + } + + cls.package_available = {} + cls.package_modules = {} + packages_used = set(sum(list(cls.package_dependencies.values()), [])) + for package_ in packages_used: + pack, pack_avail = attempt_import(package_, defer_import=False) + cls.package_available[package_] = pack_avail + cls.package_modules[package_] = pack + + @classmethod + def _find_tests(cls, test_dirs, pattern): + test_tuples = [] + for testdir in test_dirs: + # Find all pattern files in the test directory and any immediate + # sub-directories + for fname in list(glob.glob(os.path.join(testdir, pattern))) + list( + glob.glob(os.path.join(testdir, '*', pattern)) + ): + test_file = os.path.abspath(fname) + bname = os.path.basename(test_file) + dir_ = os.path.dirname(test_file) + name = os.path.splitext(bname)[0] + tname = os.path.basename(dir_) + '_' + name + + suffix = None + # Look for txt and yml file names matching py file names. Add + # a test for any found + for suffix_ in ['.txt', '.yml']: + if os.path.exists(os.path.join(dir_, name + suffix_)): + suffix = suffix_ + break + if suffix is not None: + tname = tname.replace('-', '_') + tname = tname.replace('.', '_') + + # Create list of tuples with (test_name, test_file, baseline_file) + test_tuples.append( + (tname, test_file, os.path.join(dir_, name + suffix)) + ) + + # Ensure a deterministic test ordering + test_tuples.sort() + return test_tuples + + @classmethod + def gather_tests(cls, test_dirs): + # Find all .sh files in the test directories + sh_test_tuples = cls._find_tests(test_dirs, '*.sh') + + # Find all .py files in the test directories + py_test_tuples = cls._find_tests(test_dirs, '*.py') + + # If there is both a .py and a .sh, defer to the sh + sh_files = set(map(operator.itemgetter(1), sh_test_tuples)) + py_test_tuples = list( + filter(lambda t: t[1][:-3] + '.sh' not in sh_files, py_test_tuples) + ) + + return py_test_tuples, sh_test_tuples + + def check_skip(self, name): + """ + Return a boolean if the test should be skipped + """ + + if name in self.solver_dependencies: + solvers_ = self.solver_dependencies[name] + if not all([self.solver_available[i] for i in solvers_]): + # Skip the test because a solver is not available + _missing = [] + for i in solvers_: + if not self.solver_available[i]: + _missing.append(i) + return "Solver%s %s %s not available" % ( + 's' if len(_missing) > 1 else '', + ", ".join(_missing), + 'are' if len(_missing) > 1 else 'is', + ) + + if name in self.package_dependencies: + packages_ = self.package_dependencies[name] + if not all([self.package_available[i] for i in packages_]): + # Skip the test because a package is not available + _missing = [] + for i in packages_: + if not self.package_available[i]: + _missing.append(i) + return "Package%s %s %s not available" % ( + 's' if len(_missing) > 1 else '', + ", ".join(_missing), + 'are' if len(_missing) > 1 else 'is', + ) + + # This is a hack, xlrd dropped support for .xlsx files in 2.0.1 which + # causes problems with older versions of Pandas<=1.1.5 so skipping + # tests requiring both these packages when incompatible versions are found + if ( + 'pandas' in self.package_dependencies[name] + and 'xlrd' in self.package_dependencies[name] + ): + if check_min_version( + self.package_modules['xlrd'], '2.0.1' + ) and not check_min_version(self.package_modules['pandas'], '1.1.6'): + return "Incompatible versions of xlrd and pandas" + + return False + + def filter_fcn(self, line): + """ + Ignore certain text when comparing output with baseline + """ + for field in ( + '[', + 'password:', + 'http:', + 'Job ', + 'Importing module', + 'Function', + 'File', + 'Matplotlib', + 'Memory:', + '-------', + '=======', + ' ^', + ): + if line.startswith(field): + return True + for field in ( + 'Total CPU', + 'Ipopt', + 'license', + #'Status: optimal', + #'Status: feasible', + 'time:', + 'Time:', + 'with format cpxlp', + 'usermodel = NoReturn: + def load_vars(self, vars_to_load: Optional[Sequence[VarData]] = None) -> NoReturn: """ Load the solution of the primal variables into the value attribute of the variables. @@ -186,8 +195,8 @@ def load_vars( @abc.abstractmethod def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: """ Returns a ComponentMap mapping variable to var value. @@ -205,8 +214,8 @@ def get_primals( pass def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: """ Returns a dictionary mapping constraint to dual value. @@ -224,8 +233,8 @@ def get_duals( raise NotImplementedError(f'{type(self)} does not support the get_duals method') def get_slacks( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: """ Returns a dictionary mapping constraint to slack. @@ -245,8 +254,8 @@ def get_slacks( ) def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: """ Returns a ComponentMap mapping variable to reduced cost. @@ -292,8 +301,8 @@ def __init__( self._reduced_costs = reduced_costs def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: if self._primals is None: raise RuntimeError( 'Solution loader does not currently have a valid solution. Please ' @@ -308,8 +317,8 @@ def get_primals( return primals def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: if self._duals is None: raise RuntimeError( 'Solution loader does not currently have valid duals. Please ' @@ -325,8 +334,8 @@ def get_duals( return duals def get_slacks( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: if self._slacks is None: raise RuntimeError( 'Solution loader does not currently have valid slacks. Please ' @@ -342,8 +351,8 @@ def get_slacks( return slacks def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: if self._reduced_costs is None: raise RuntimeError( 'Solution loader does not currently have valid reduced costs. Please ' @@ -610,13 +619,13 @@ def __str__(self): return self.name @abc.abstractmethod - def solve(self, model: _BlockData, timer: HierarchicalTimer = None) -> Results: + def solve(self, model: BlockData, timer: HierarchicalTimer = None) -> Results: """ Solve a Pyomo model. Parameters ---------- - model: _BlockData + model: BlockData The Pyomo model to be solved timer: HierarchicalTimer An option timer for reporting timing @@ -697,9 +706,7 @@ class PersistentSolver(Solver): def is_persistent(self): return True - def load_vars( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> NoReturn: + def load_vars(self, vars_to_load: Optional[Sequence[VarData]] = None) -> NoReturn: """ Load the solution of the primal variables into the value attribute of the variables. @@ -715,13 +722,13 @@ def load_vars( @abc.abstractmethod def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: pass def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: """ Declare sign convention in docstring here. @@ -741,8 +748,8 @@ def get_duals( ) def get_slacks( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: """ Parameters ---------- @@ -760,8 +767,8 @@ def get_slacks( ) def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: """ Parameters ---------- @@ -788,43 +795,43 @@ def set_instance(self, model): pass @abc.abstractmethod - def add_variables(self, variables: List[_GeneralVarData]): + def add_variables(self, variables: List[VarData]): pass @abc.abstractmethod - def add_params(self, params: List[_ParamData]): + def add_params(self, params: List[ParamData]): pass @abc.abstractmethod - def add_constraints(self, cons: List[_GeneralConstraintData]): + def add_constraints(self, cons: List[ConstraintData]): pass @abc.abstractmethod - def add_block(self, block: _BlockData): + def add_block(self, block: BlockData): pass @abc.abstractmethod - def remove_variables(self, variables: List[_GeneralVarData]): + def remove_variables(self, variables: List[VarData]): pass @abc.abstractmethod - def remove_params(self, params: List[_ParamData]): + def remove_params(self, params: List[ParamData]): pass @abc.abstractmethod - def remove_constraints(self, cons: List[_GeneralConstraintData]): + def remove_constraints(self, cons: List[ConstraintData]): pass @abc.abstractmethod - def remove_block(self, block: _BlockData): + def remove_block(self, block: BlockData): pass @abc.abstractmethod - def set_objective(self, obj: _GeneralObjectiveData): + def set_objective(self, obj: ObjectiveData): pass @abc.abstractmethod - def update_variables(self, variables: List[_GeneralVarData]): + def update_variables(self, variables: List[VarData]): pass @abc.abstractmethod @@ -846,20 +853,20 @@ def get_primals(self, vars_to_load=None): return self._solver.get_primals(vars_to_load=vars_to_load) def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: self._assert_solution_still_valid() return self._solver.get_duals(cons_to_load=cons_to_load) def get_slacks( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: self._assert_solution_still_valid() return self._solver.get_slacks(cons_to_load=cons_to_load) def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: self._assert_solution_still_valid() return self._solver.get_reduced_costs(vars_to_load=vars_to_load) @@ -933,7 +940,7 @@ def update_config(self, val: UpdateConfig): def set_instance(self, model): saved_update_config = self.update_config - self.__init__() + self.__init__(only_child_vars=self._only_child_vars) self.update_config = saved_update_config self._model = model if self.use_extensions and cmodel_available: @@ -943,10 +950,10 @@ def set_instance(self, model): self.set_objective(None) @abc.abstractmethod - def _add_variables(self, variables: List[_GeneralVarData]): + def _add_variables(self, variables: List[VarData]): pass - def add_variables(self, variables: List[_GeneralVarData]): + def add_variables(self, variables: List[VarData]): for v in variables: if id(v) in self._referenced_variables: raise ValueError( @@ -964,19 +971,19 @@ def add_variables(self, variables: List[_GeneralVarData]): self._add_variables(variables) @abc.abstractmethod - def _add_params(self, params: List[_ParamData]): + def _add_params(self, params: List[ParamData]): pass - def add_params(self, params: List[_ParamData]): + def add_params(self, params: List[ParamData]): for p in params: self._params[id(p)] = p self._add_params(params) @abc.abstractmethod - def _add_constraints(self, cons: List[_GeneralConstraintData]): + def _add_constraints(self, cons: List[ConstraintData]): pass - def _check_for_new_vars(self, variables: List[_GeneralVarData]): + def _check_for_new_vars(self, variables: List[VarData]): new_vars = dict() for v in variables: v_id = id(v) @@ -984,7 +991,7 @@ def _check_for_new_vars(self, variables: List[_GeneralVarData]): new_vars[v_id] = v self.add_variables(list(new_vars.values())) - def _check_to_remove_vars(self, variables: List[_GeneralVarData]): + def _check_to_remove_vars(self, variables: List[VarData]): vars_to_remove = dict() for v in variables: v_id = id(v) @@ -993,7 +1000,7 @@ def _check_to_remove_vars(self, variables: List[_GeneralVarData]): vars_to_remove[v_id] = v self.remove_variables(list(vars_to_remove.values())) - def add_constraints(self, cons: List[_GeneralConstraintData]): + def add_constraints(self, cons: List[ConstraintData]): all_fixed_vars = dict() for con in cons: if con in self._named_expressions: @@ -1023,10 +1030,10 @@ def add_constraints(self, cons: List[_GeneralConstraintData]): v.fix() @abc.abstractmethod - def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + def _add_sos_constraints(self, cons: List[SOSConstraintData]): pass - def add_sos_constraints(self, cons: List[_SOSConstraintData]): + def add_sos_constraints(self, cons: List[SOSConstraintData]): for con in cons: if con in self._vars_referenced_by_con: raise ValueError( @@ -1043,10 +1050,10 @@ def add_sos_constraints(self, cons: List[_SOSConstraintData]): self._add_sos_constraints(cons) @abc.abstractmethod - def _set_objective(self, obj: _GeneralObjectiveData): + def _set_objective(self, obj: ObjectiveData): pass - def set_objective(self, obj: _GeneralObjectiveData): + def set_objective(self, obj: ObjectiveData): if self._objective is not None: for v in self._vars_referenced_by_obj: self._referenced_variables[id(v)][2] = None @@ -1121,10 +1128,10 @@ def add_block(self, block): self.set_objective(obj) @abc.abstractmethod - def _remove_constraints(self, cons: List[_GeneralConstraintData]): + def _remove_constraints(self, cons: List[ConstraintData]): pass - def remove_constraints(self, cons: List[_GeneralConstraintData]): + def remove_constraints(self, cons: List[ConstraintData]): self._remove_constraints(cons) for con in cons: if con not in self._named_expressions: @@ -1143,10 +1150,10 @@ def remove_constraints(self, cons: List[_GeneralConstraintData]): del self._vars_referenced_by_con[con] @abc.abstractmethod - def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + def _remove_sos_constraints(self, cons: List[SOSConstraintData]): pass - def remove_sos_constraints(self, cons: List[_SOSConstraintData]): + def remove_sos_constraints(self, cons: List[SOSConstraintData]): self._remove_sos_constraints(cons) for con in cons: if con not in self._vars_referenced_by_con: @@ -1163,10 +1170,10 @@ def remove_sos_constraints(self, cons: List[_SOSConstraintData]): del self._vars_referenced_by_con[con] @abc.abstractmethod - def _remove_variables(self, variables: List[_GeneralVarData]): + def _remove_variables(self, variables: List[VarData]): pass - def remove_variables(self, variables: List[_GeneralVarData]): + def remove_variables(self, variables: List[VarData]): self._remove_variables(variables) for v in variables: v_id = id(v) @@ -1187,10 +1194,10 @@ def remove_variables(self, variables: List[_GeneralVarData]): del self._vars[v_id] @abc.abstractmethod - def _remove_params(self, params: List[_ParamData]): + def _remove_params(self, params: List[ParamData]): pass - def remove_params(self, params: List[_ParamData]): + def remove_params(self, params: List[ParamData]): self._remove_params(params) for p in params: del self._params[id(p)] @@ -1235,10 +1242,10 @@ def remove_block(self, block): ) @abc.abstractmethod - def _update_variables(self, variables: List[_GeneralVarData]): + def _update_variables(self, variables: List[VarData]): pass - def update_variables(self, variables: List[_GeneralVarData]): + def update_variables(self, variables: List[VarData]): for v in variables: self._vars[id(v)] = ( v, @@ -1323,12 +1330,12 @@ def update(self, timer: HierarchicalTimer = None): for c in self._vars_referenced_by_con.keys(): if c not in current_cons_dict and c not in current_sos_dict: if (c.ctype is Constraint) or ( - c.ctype is None and isinstance(c, _GeneralConstraintData) + c.ctype is None and isinstance(c, ConstraintData) ): old_cons.append(c) else: assert (c.ctype is SOSConstraint) or ( - c.ctype is None and isinstance(c, _SOSConstraintData) + c.ctype is None and isinstance(c, SOSConstraintData) ) old_sos.append(c) self.remove_constraints(old_cons) @@ -1518,7 +1525,7 @@ def update(self, timer: HierarchicalTimer = None): class LegacySolverInterface(object): def solve( self, - model: _BlockData, + model: BlockData, tee: bool = False, load_solutions: bool = True, logfile: Optional[str] = None, @@ -1654,7 +1661,7 @@ def license_is_valid(self) -> bool: @property def options(self): - for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs']: + for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs', 'maingo']: if hasattr(self, solver_name + '_options'): return getattr(self, solver_name + '_options') raise NotImplementedError('Could not find the correct options') @@ -1662,7 +1669,7 @@ def options(self): @options.setter def options(self, val): found = False - for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs']: + for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs', 'maingo']: if hasattr(self, solver_name + '_options'): setattr(self, solver_name + '_options', val) found = True @@ -1685,7 +1692,7 @@ def decorator(cls): class LegacySolver(LegacySolverInterface, cls): pass - LegacySolverFactory.register(name, doc)(LegacySolver) + LegacySolverFactory.register('appsi_' + name, doc)(LegacySolver) return cls diff --git a/pyomo/contrib/appsi/build.py b/pyomo/contrib/appsi/build.py index 2a4e7bb785e..38f8cb713ca 100644 --- a/pyomo/contrib/appsi/build.py +++ b/pyomo/contrib/appsi/build.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -16,15 +16,6 @@ import tempfile -def handleReadonly(function, path, excinfo): - excvalue = excinfo[1] - if excvalue.errno == errno.EACCES: - os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) # 0777 - function(path) - else: - raise - - def get_appsi_extension(in_setup=False, appsi_root=None): from pybind11.setup_helpers import Pybind11Extension @@ -63,10 +54,10 @@ def get_appsi_extension(in_setup=False, appsi_root=None): def build_appsi(args=[]): print('\n\n**** Building APPSI ****') - import setuptools - from distutils.dist import Distribution + from setuptools import Distribution from pybind11.setup_helpers import build_ext import pybind11.setup_helpers + from pyomo.common.cmake_builder import handleReadonly from pyomo.common.envvar import PYOMO_CONFIG_DIR from pyomo.common.fileutils import this_file_dir diff --git a/pyomo/contrib/appsi/cmodel/__init__.py b/pyomo/contrib/appsi/cmodel/__init__.py index 9c276b518de..cc2aec28241 100644 --- a/pyomo/contrib/appsi/cmodel/__init__.py +++ b/pyomo/contrib/appsi/cmodel/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/cmodel/src/cmodel_bindings.cpp b/pyomo/contrib/appsi/cmodel/src/cmodel_bindings.cpp index db9d3112069..5a838ffd786 100644 --- a/pyomo/contrib/appsi/cmodel/src/cmodel_bindings.cpp +++ b/pyomo/contrib/appsi/cmodel/src/cmodel_bindings.cpp @@ -1,7 +1,7 @@ /**___________________________________________________________________________ * * Pyomo: Python Optimization Modeling Objects - * Copyright (c) 2008-2022 + * Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC * Under the terms of Contract DE-NA0003525 with National Technology and * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -63,7 +63,8 @@ PYBIND11_MODULE(appsi_cmodel, m) { m.def("appsi_exprs_from_pyomo_exprs", &appsi_exprs_from_pyomo_exprs); m.def("appsi_expr_from_pyomo_expr", &appsi_expr_from_pyomo_expr); m.def("prep_for_repn", &prep_for_repn); - py::class_(m, "PyomoExprTypes").def(py::init<>()); + py::class_(m, "PyomoExprTypes", py::module_local()) + .def(py::init<>()); py::class_>(m, "Node") .def("is_variable_type", &Node::is_variable_type) .def("is_param_type", &Node::is_param_type) @@ -165,7 +166,7 @@ PYBIND11_MODULE(appsi_cmodel, m) { .def(py::init<>()) .def("write", &LPWriter::write) .def("get_solve_cons", &LPWriter::get_solve_cons); - py::enum_(m, "ExprType") + py::enum_(m, "ExprType", py::module_local()) .value("py_float", ExprType::py_float) .value("var", ExprType::var) .value("param", ExprType::param) diff --git a/pyomo/contrib/appsi/cmodel/src/common.cpp b/pyomo/contrib/appsi/cmodel/src/common.cpp index 255a0a3a70f..6f8002cb50e 100644 --- a/pyomo/contrib/appsi/cmodel/src/common.cpp +++ b/pyomo/contrib/appsi/cmodel/src/common.cpp @@ -1,3 +1,15 @@ +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2024 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + #include "common.hpp" double inf; diff --git a/pyomo/contrib/appsi/cmodel/src/common.hpp b/pyomo/contrib/appsi/cmodel/src/common.hpp index 36afd549116..9edc9571a4d 100644 --- a/pyomo/contrib/appsi/cmodel/src/common.hpp +++ b/pyomo/contrib/appsi/cmodel/src/common.hpp @@ -1,3 +1,15 @@ +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2024 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + #include #include diff --git a/pyomo/contrib/appsi/cmodel/src/expression.cpp b/pyomo/contrib/appsi/cmodel/src/expression.cpp index 1923d3a1894..a49d6f2e499 100644 --- a/pyomo/contrib/appsi/cmodel/src/expression.cpp +++ b/pyomo/contrib/appsi/cmodel/src/expression.cpp @@ -1,1970 +1,1986 @@ -#include "expression.hpp" - -bool Leaf::is_leaf() { return true; } - -bool Var::is_variable_type() { return true; } - -bool Param::is_param_type() { return true; } - -bool Constant::is_constant_type() { return true; } - -bool Expression::is_expression_type() { return true; } - -double Leaf::evaluate() { return value; } - -double Var::get_lb() { - if (fixed) - return value; - else - return std::max(lb->evaluate(), domain_lb); -} - -double Var::get_ub() { - if (fixed) - return value; - else - return std::min(ub->evaluate(), domain_ub); -} - -Domain Var::get_domain() { return domain; } - -bool Operator::is_operator_type() { return true; } - -std::vector> Expression::get_operators() { - std::vector> res(n_operators); - for (unsigned int i = 0; i < n_operators; ++i) { - res[i] = operators[i]; - } - return res; -} - -double Leaf::get_value_from_array(double *val_array) { return value; } - -double Expression::get_value_from_array(double *val_array) { - return val_array[n_operators - 1]; -} - -double Operator::get_value_from_array(double *val_array) { - return val_array[index]; -} - -void MultiplyOperator::evaluate(double *values) { - values[index] = operand1->get_value_from_array(values) * - operand2->get_value_from_array(values); -} - -void ExternalOperator::evaluate(double *values) { - // It would be nice to implement this, but it will take some more work. - // This would require dynamic linking to the external function. - throw std::runtime_error("cannot evaluate ExternalOperator yet"); -} - -void LinearOperator::evaluate(double *values) { - values[index] = constant->evaluate(); - for (unsigned int i = 0; i < nterms; ++i) { - values[index] += coefficients[i]->evaluate() * variables[i]->evaluate(); - } -} - -void SumOperator::evaluate(double *values) { - values[index] = 0.0; - for (unsigned int i = 0; i < nargs; ++i) { - values[index] += operands[i]->get_value_from_array(values); - } -} - -void DivideOperator::evaluate(double *values) { - values[index] = operand1->get_value_from_array(values) / - operand2->get_value_from_array(values); -} - -void PowerOperator::evaluate(double *values) { - values[index] = std::pow(operand1->get_value_from_array(values), - operand2->get_value_from_array(values)); -} - -void NegationOperator::evaluate(double *values) { - values[index] = -operand->get_value_from_array(values); -} - -void ExpOperator::evaluate(double *values) { - values[index] = std::exp(operand->get_value_from_array(values)); -} - -void LogOperator::evaluate(double *values) { - values[index] = std::log(operand->get_value_from_array(values)); -} - -void AbsOperator::evaluate(double *values) { - values[index] = std::fabs(operand->get_value_from_array(values)); -} - -void SqrtOperator::evaluate(double *values) { - values[index] = std::pow(operand->get_value_from_array(values), 0.5); -} - -void Log10Operator::evaluate(double *values) { - values[index] = std::log10(operand->get_value_from_array(values)); -} - -void SinOperator::evaluate(double *values) { - values[index] = std::sin(operand->get_value_from_array(values)); -} - -void CosOperator::evaluate(double *values) { - values[index] = std::cos(operand->get_value_from_array(values)); -} - -void TanOperator::evaluate(double *values) { - values[index] = std::tan(operand->get_value_from_array(values)); -} - -void AsinOperator::evaluate(double *values) { - values[index] = std::asin(operand->get_value_from_array(values)); -} - -void AcosOperator::evaluate(double *values) { - values[index] = std::acos(operand->get_value_from_array(values)); -} - -void AtanOperator::evaluate(double *values) { - values[index] = std::atan(operand->get_value_from_array(values)); -} - -double Expression::evaluate() { - double *values = new double[n_operators]; - for (unsigned int i = 0; i < n_operators; ++i) { - operators[i]->index = i; - operators[i]->evaluate(values); - } - double res = get_value_from_array(values); - delete[] values; - return res; -} - -void UnaryOperator::identify_variables( - std::set> &var_set, - std::shared_ptr>> var_vec) { - if (operand->is_variable_type()) { - if (var_set.count(operand) == 0) { - var_vec->push_back(std::dynamic_pointer_cast(operand)); - var_set.insert(operand); - } - } -} - -void BinaryOperator::identify_variables( - std::set> &var_set, - std::shared_ptr>> var_vec) { - if (operand1->is_variable_type()) { - if (var_set.count(operand1) == 0) { - var_vec->push_back(std::dynamic_pointer_cast(operand1)); - var_set.insert(operand1); - } - } - if (operand2->is_variable_type()) { - if (var_set.count(operand2) == 0) { - var_vec->push_back(std::dynamic_pointer_cast(operand2)); - var_set.insert(operand2); - } - } -} - -void ExternalOperator::identify_variables( - std::set> &var_set, - std::shared_ptr>> var_vec) { - for (unsigned int i = 0; i < nargs; ++i) { - if (operands[i]->is_variable_type()) { - if (var_set.count(operands[i]) == 0) { - var_vec->push_back(std::dynamic_pointer_cast(operands[i])); - var_set.insert(operands[i]); - } - } - } -} - -void LinearOperator::identify_variables( - std::set> &var_set, - std::shared_ptr>> var_vec) { - for (unsigned int i = 0; i < nterms; ++i) { - if (var_set.count(variables[i]) == 0) { - var_vec->push_back(std::dynamic_pointer_cast(variables[i])); - var_set.insert(variables[i]); - } - } -} - -void SumOperator::identify_variables( - std::set> &var_set, - std::shared_ptr>> var_vec) { - for (unsigned int i = 0; i < nargs; ++i) { - if (operands[i]->is_variable_type()) { - if (var_set.count(operands[i]) == 0) { - var_vec->push_back(std::dynamic_pointer_cast(operands[i])); - var_set.insert(operands[i]); - } - } - } -} - -std::shared_ptr>> -Expression::identify_variables() { - std::set> var_set; - std::shared_ptr>> res = - std::make_shared>>(var_set.size()); - for (unsigned int i = 0; i < n_operators; ++i) { - operators[i]->identify_variables(var_set, res); - } - return res; -} - -std::shared_ptr>> Var::identify_variables() { - std::shared_ptr>> res = - std::make_shared>>(); - res->push_back(shared_from_this()); - return res; -} - -std::shared_ptr>> -Constant::identify_variables() { - std::shared_ptr>> res = - std::make_shared>>(); - return res; -} - -std::shared_ptr>> Param::identify_variables() { - std::shared_ptr>> res = - std::make_shared>>(); - return res; -} - -std::shared_ptr>> -Expression::identify_external_operators() { - std::set> external_set; - for (unsigned int i = 0; i < n_operators; ++i) { - if (operators[i]->is_external_operator()) { - external_set.insert(operators[i]); - } - } - std::shared_ptr>> res = - std::make_shared>>( - external_set.size()); - int ndx = 0; - for (std::shared_ptr n : external_set) { - (*res)[ndx] = std::dynamic_pointer_cast(n); - ndx += 1; - } - return res; -} - -std::shared_ptr>> -Var::identify_external_operators() { - std::shared_ptr>> res = - std::make_shared>>(); - return res; -} - -std::shared_ptr>> -Constant::identify_external_operators() { - std::shared_ptr>> res = - std::make_shared>>(); - return res; -} - -std::shared_ptr>> -Param::identify_external_operators() { - std::shared_ptr>> res = - std::make_shared>>(); - return res; -} - -int Var::get_degree_from_array(int *degree_array) { return 1; } - -int Param::get_degree_from_array(int *degree_array) { return 0; } - -int Constant::get_degree_from_array(int *degree_array) { return 0; } - -int Expression::get_degree_from_array(int *degree_array) { - return degree_array[n_operators - 1]; -} - -int Operator::get_degree_from_array(int *degree_array) { - return degree_array[index]; -} - -void LinearOperator::propagate_degree_forward(int *degrees, double *values) { - degrees[index] = 1; -} - -void SumOperator::propagate_degree_forward(int *degrees, double *values) { - int deg = 0; - int _deg; - for (unsigned int i = 0; i < nargs; ++i) { - _deg = operands[i]->get_degree_from_array(degrees); - if (_deg > deg) { - deg = _deg; - } - } - degrees[index] = deg; -} - -void MultiplyOperator::propagate_degree_forward(int *degrees, double *values) { - degrees[index] = operand1->get_degree_from_array(degrees) + - operand2->get_degree_from_array(degrees); -} - -void ExternalOperator::propagate_degree_forward(int *degrees, double *values) { - // External functions are always considered nonlinear - // Anything larger than 2 is nonlinear - degrees[index] = 3; -} - -void DivideOperator::propagate_degree_forward(int *degrees, double *values) { - // anything larger than 2 is nonlinear - degrees[index] = std::max(operand1->get_degree_from_array(degrees), - 3 * (operand2->get_degree_from_array(degrees))); -} - -void PowerOperator::propagate_degree_forward(int *degrees, double *values) { - if (operand2->get_degree_from_array(degrees) != 0) { - degrees[index] = 3; - } else { - double val2 = operand2->get_value_from_array(values); - double intpart; - if (std::modf(val2, &intpart) == 0.0) { - degrees[index] = operand1->get_degree_from_array(degrees) * (int)val2; - } else { - degrees[index] = 3; - } - } -} - -void NegationOperator::propagate_degree_forward(int *degrees, double *values) { - degrees[index] = operand->get_degree_from_array(degrees); -} - -void UnaryOperator::propagate_degree_forward(int *degrees, double *values) { - if (operand->get_degree_from_array(degrees) == 0) { - degrees[index] = 0; - } else { - degrees[index] = 3; - } -} - -std::string Var::__str__() { return name; } - -std::string Param::__str__() { return name; } - -std::string Constant::__str__() { return std::to_string(value); } - -std::string Expression::__str__() { - std::string *string_array = new std::string[n_operators]; - std::shared_ptr oper; - for (unsigned int i = 0; i < n_operators; ++i) { - oper = operators[i]; - oper->index = i; - oper->print(string_array); - } - std::string res = string_array[n_operators - 1]; - delete[] string_array; - return res; -} - -std::string Leaf::get_string_from_array(std::string *string_array) { - return __str__(); -} - -std::string Expression::get_string_from_array(std::string *string_array) { - return string_array[n_operators - 1]; -} - -std::string Operator::get_string_from_array(std::string *string_array) { - return string_array[index]; -} - -void MultiplyOperator::print(std::string *string_array) { - string_array[index] = - ("(" + operand1->get_string_from_array(string_array) + "*" + - operand2->get_string_from_array(string_array) + ")"); -} - -void ExternalOperator::print(std::string *string_array) { - std::string res = function_name + "("; - for (unsigned int i = 0; i < (nargs - 1); ++i) { - res += operands[i]->get_string_from_array(string_array); - res += ", "; - } - res += operands[nargs - 1]->get_string_from_array(string_array); - res += ")"; - string_array[index] = res; -} - -void DivideOperator::print(std::string *string_array) { - string_array[index] = - ("(" + operand1->get_string_from_array(string_array) + "/" + - operand2->get_string_from_array(string_array) + ")"); -} - -void PowerOperator::print(std::string *string_array) { - string_array[index] = - ("(" + operand1->get_string_from_array(string_array) + "**" + - operand2->get_string_from_array(string_array) + ")"); -} - -void NegationOperator::print(std::string *string_array) { - string_array[index] = - ("(-" + operand->get_string_from_array(string_array) + ")"); -} - -void ExpOperator::print(std::string *string_array) { - string_array[index] = - ("exp(" + operand->get_string_from_array(string_array) + ")"); -} - -void LogOperator::print(std::string *string_array) { - string_array[index] = - ("log(" + operand->get_string_from_array(string_array) + ")"); -} - -void AbsOperator::print(std::string *string_array) { - string_array[index] = - ("abs(" + operand->get_string_from_array(string_array) + ")"); -} - -void SqrtOperator::print(std::string *string_array) { - string_array[index] = - ("sqrt(" + operand->get_string_from_array(string_array) + ")"); -} - -void Log10Operator::print(std::string *string_array) { - string_array[index] = - ("log10(" + operand->get_string_from_array(string_array) + ")"); -} - -void SinOperator::print(std::string *string_array) { - string_array[index] = - ("sin(" + operand->get_string_from_array(string_array) + ")"); -} - -void CosOperator::print(std::string *string_array) { - string_array[index] = - ("cos(" + operand->get_string_from_array(string_array) + ")"); -} - -void TanOperator::print(std::string *string_array) { - string_array[index] = - ("tan(" + operand->get_string_from_array(string_array) + ")"); -} - -void AsinOperator::print(std::string *string_array) { - string_array[index] = - ("asin(" + operand->get_string_from_array(string_array) + ")"); -} - -void AcosOperator::print(std::string *string_array) { - string_array[index] = - ("acos(" + operand->get_string_from_array(string_array) + ")"); -} - -void AtanOperator::print(std::string *string_array) { - string_array[index] = - ("atan(" + operand->get_string_from_array(string_array) + ")"); -} - -void LinearOperator::print(std::string *string_array) { - std::string res = "(" + constant->__str__(); - for (unsigned int i = 0; i < nterms; ++i) { - res += " + " + coefficients[i]->__str__() + "*" + variables[i]->__str__(); - } - res += ")"; - string_array[index] = res; -} - -void SumOperator::print(std::string *string_array) { - std::string res = "(" + operands[0]->get_string_from_array(string_array); - for (unsigned int i = 1; i < nargs; ++i) { - res += " + " + operands[i]->get_string_from_array(string_array); - } - res += ")"; - string_array[index] = res; -} - -std::shared_ptr>> -Leaf::get_prefix_notation() { - std::shared_ptr>> res = - std::make_shared>>(); - res->push_back(shared_from_this()); - return res; -} - -std::shared_ptr>> -Expression::get_prefix_notation() { - std::shared_ptr>> res = - std::make_shared>>(); - std::shared_ptr>> stack = - std::make_shared>>(); - std::shared_ptr node; - stack->push_back(operators[n_operators - 1]); - while (stack->size() > 0) { - node = stack->back(); - stack->pop_back(); - res->push_back(node); - node->fill_prefix_notation_stack(stack); - } - - return res; -} - -void BinaryOperator::fill_prefix_notation_stack( - std::shared_ptr>> stack) { - stack->push_back(operand2); - stack->push_back(operand1); -} - -void UnaryOperator::fill_prefix_notation_stack( - std::shared_ptr>> stack) { - stack->push_back(operand); -} - -void SumOperator::fill_prefix_notation_stack( - std::shared_ptr>> stack) { - int ndx = nargs - 1; - while (ndx >= 0) { - stack->push_back(operands[ndx]); - ndx -= 1; - } -} - -void LinearOperator::fill_prefix_notation_stack( - std::shared_ptr>> stack) { - ; // This is treated as a leaf in this context; write_nl_string will take care - // of it -} - -void ExternalOperator::fill_prefix_notation_stack( - std::shared_ptr>> stack) { - int i = nargs - 1; - while (i >= 0) { - stack->push_back(operands[i]); - i -= 1; - } -} - -void Var::write_nl_string(std::ofstream &f) { f << "v" << index << "\n"; } - -void Param::write_nl_string(std::ofstream &f) { f << "n" << value << "\n"; } - -void Constant::write_nl_string(std::ofstream &f) { f << "n" << value << "\n"; } - -void Expression::write_nl_string(std::ofstream &f) { - std::shared_ptr>> prefix_notation = - get_prefix_notation(); - for (std::shared_ptr &node : *(prefix_notation)) { - node->write_nl_string(f); - } -} - -void MultiplyOperator::write_nl_string(std::ofstream &f) { f << "o2\n"; } - -void ExternalOperator::write_nl_string(std::ofstream &f) { - f << "f" << external_function_index << " " << nargs << "\n"; -} - -void SumOperator::write_nl_string(std::ofstream &f) { - if (nargs == 2) { - f << "o0\n"; - } else { - f << "o54\n"; - f << nargs << "\n"; - } -} - -void LinearOperator::write_nl_string(std::ofstream &f) { - bool has_const = - (!constant->is_constant_type()) || (constant->evaluate() != 0); - unsigned int n_sum_args = nterms + (has_const ? 1 : 0); - if (n_sum_args == 2) { - f << "o0\n"; - } else { - f << "o54\n"; - f << n_sum_args << "\n"; - } - if (has_const) - f << "n" << constant->evaluate() << "\n"; - for (unsigned int ndx = 0; ndx < nterms; ++ndx) { - f << "o2\n"; - f << "n" << coefficients[ndx]->evaluate() << "\n"; - variables[ndx]->write_nl_string(f); - } -} - -void DivideOperator::write_nl_string(std::ofstream &f) { f << "o3\n"; } - -void PowerOperator::write_nl_string(std::ofstream &f) { f << "o5\n"; } - -void NegationOperator::write_nl_string(std::ofstream &f) { f << "o16\n"; } - -void ExpOperator::write_nl_string(std::ofstream &f) { f << "o44\n"; } - -void LogOperator::write_nl_string(std::ofstream &f) { f << "o43\n"; } - -void AbsOperator::write_nl_string(std::ofstream &f) { f << "o15\n"; } - -void SqrtOperator::write_nl_string(std::ofstream &f) { f << "o39\n"; } - -void Log10Operator::write_nl_string(std::ofstream &f) { f << "o42\n"; } - -void SinOperator::write_nl_string(std::ofstream &f) { f << "o41\n"; } - -void CosOperator::write_nl_string(std::ofstream &f) { f << "o46\n"; } - -void TanOperator::write_nl_string(std::ofstream &f) { f << "o38\n"; } - -void AsinOperator::write_nl_string(std::ofstream &f) { f << "o51\n"; } - -void AcosOperator::write_nl_string(std::ofstream &f) { f << "o53\n"; } - -void AtanOperator::write_nl_string(std::ofstream &f) { f << "o49\n"; } - -bool BinaryOperator::is_binary_operator() { return true; } - -bool UnaryOperator::is_unary_operator() { return true; } - -bool LinearOperator::is_linear_operator() { return true; } - -bool SumOperator::is_sum_operator() { return true; } - -bool MultiplyOperator::is_multiply_operator() { return true; } - -bool DivideOperator::is_divide_operator() { return true; } - -bool PowerOperator::is_power_operator() { return true; } - -bool NegationOperator::is_negation_operator() { return true; } - -bool ExpOperator::is_exp_operator() { return true; } - -bool LogOperator::is_log_operator() { return true; } - -bool AbsOperator::is_abs_operator() { return true; } - -bool SqrtOperator::is_sqrt_operator() { return true; } - -bool ExternalOperator::is_external_operator() { return true; } - -void Leaf::fill_expression(std::shared_ptr *oper_array, - int &oper_ndx) { - ; -} - -void Expression::fill_expression(std::shared_ptr *oper_array, - int &oper_ndx) { - throw std::runtime_error("This should not happen"); -} - -void BinaryOperator::fill_expression(std::shared_ptr *oper_array, - int &oper_ndx) { - oper_ndx -= 1; - oper_array[oper_ndx] = shared_from_this(); - // The order does not actually matter here. It - // will just be easier to debug this way. - operand2->fill_expression(oper_array, oper_ndx); - operand1->fill_expression(oper_array, oper_ndx); -} - -void UnaryOperator::fill_expression(std::shared_ptr *oper_array, - int &oper_ndx) { - oper_ndx -= 1; - oper_array[oper_ndx] = shared_from_this(); - operand->fill_expression(oper_array, oper_ndx); -} - -void LinearOperator::fill_expression(std::shared_ptr *oper_array, - int &oper_ndx) { - oper_ndx -= 1; - oper_array[oper_ndx] = shared_from_this(); -} - -void SumOperator::fill_expression(std::shared_ptr *oper_array, - int &oper_ndx) { - oper_ndx -= 1; - oper_array[oper_ndx] = shared_from_this(); - // The order does not actually matter here. It - // will just be easier to debug this way. - int arg_ndx = nargs - 1; - while (arg_ndx >= 0) { - operands[arg_ndx]->fill_expression(oper_array, oper_ndx); - arg_ndx -= 1; - } -} - -void ExternalOperator::fill_expression(std::shared_ptr *oper_array, - int &oper_ndx) { - oper_ndx -= 1; - oper_array[oper_ndx] = shared_from_this(); - // The order does not actually matter here. It - // will just be easier to debug this way. - int arg_ndx = nargs - 1; - while (arg_ndx >= 0) { - operands[arg_ndx]->fill_expression(oper_array, oper_ndx); - arg_ndx -= 1; - } -} - -double Leaf::get_lb_from_array(double *lbs) { return value; } - -double Leaf::get_ub_from_array(double *ubs) { return value; } - -double Var::get_lb_from_array(double *lbs) { return get_lb(); } - -double Var::get_ub_from_array(double *ubs) { return get_ub(); } - -double Expression::get_lb_from_array(double *lbs) { - return lbs[n_operators - 1]; -} - -double Expression::get_ub_from_array(double *ubs) { - return ubs[n_operators - 1]; -} - -double Operator::get_lb_from_array(double *lbs) { return lbs[index]; } - -double Operator::get_ub_from_array(double *ubs) { return ubs[index]; } - -void Leaf::set_bounds_in_array(double new_lb, double new_ub, double *lbs, - double *ubs, double feasibility_tol, - double integer_tol, double improvement_tol, - std::set> &improved_vars) { - if (new_lb < value - feasibility_tol || new_lb > value + feasibility_tol) { - throw InfeasibleConstraintException( - "Infeasible constraint; bounds computed on parameter or constant " - "disagree with the value of the parameter or constant\n value: " + - std::to_string(value) + "\n computed LB: " + std::to_string(new_lb) + - "\n computed UB: " + std::to_string(new_ub)); - } - - if (new_ub < value - feasibility_tol || new_ub > value + feasibility_tol) { - throw InfeasibleConstraintException( - "Infeasible constraint; bounds computed on parameter or constant " - "disagree with the value of the parameter or constant\n value: " + - std::to_string(value) + "\n computed LB: " + std::to_string(new_lb) + - "\n computed UB: " + std::to_string(new_ub)); - } -} - -void Var::set_bounds_in_array(double new_lb, double new_ub, double *lbs, - double *ubs, double feasibility_tol, - double integer_tol, double improvement_tol, - std::set> &improved_vars) { - if (new_lb > new_ub) { - if (new_lb - feasibility_tol > new_ub) - throw InfeasibleConstraintException( - "Infeasible constraint; The computed lower bound for a variable is " - "larger than the computed upper bound.\n computed LB: " + - std::to_string(new_lb) + - "\n computed UB: " + std::to_string(new_ub)); - else { - new_lb -= feasibility_tol; - new_ub += feasibility_tol; - } - } - if (new_lb >= inf) - throw InfeasibleConstraintException( - "Infeasible constraint; The compute lower bound for " + name + - " is inf"); - if (new_ub <= -inf) - throw InfeasibleConstraintException( - "Infeasible constraint; The computed upper bound for " + name + - " is -inf"); - - if (domain == integers || domain == binary) { - if (new_lb > -inf) { - double lb_floor = floor(new_lb); - double lb_ceil = ceil(new_lb - integer_tol); - if (lb_floor > lb_ceil) - new_lb = lb_floor; - else - new_lb = lb_ceil; - } - if (new_ub < inf) { - double ub_ceil = ceil(new_ub); - double ub_floor = floor(new_ub + integer_tol); - if (ub_ceil < ub_floor) - new_ub = ub_ceil; - else - new_ub = ub_floor; - } - } - - double current_lb = get_lb(); - double current_ub = get_ub(); - - if (new_lb > current_lb + improvement_tol || - new_ub < current_ub - improvement_tol) - improved_vars.insert(shared_from_this()); - - if (new_lb > current_lb) { - if (lb->is_leaf()) - std::dynamic_pointer_cast(lb)->value = new_lb; - else - throw py::value_error( - "variable bounds cannot be expressions when performing FBBT"); - } - - if (new_ub < current_ub) { - if (ub->is_leaf()) - std::dynamic_pointer_cast(ub)->value = new_ub; - else - throw py::value_error( - "variable bounds cannot be expressions when performing FBBT"); - } -} - -void Expression::set_bounds_in_array( - double new_lb, double new_ub, double *lbs, double *ubs, - double feasibility_tol, double integer_tol, double improvement_tol, - std::set> &improved_vars) { - lbs[n_operators - 1] = new_lb; - ubs[n_operators - 1] = new_ub; -} - -void Operator::set_bounds_in_array( - double new_lb, double new_ub, double *lbs, double *ubs, - double feasibility_tol, double integer_tol, double improvement_tol, - std::set> &improved_vars) { - lbs[index] = new_lb; - ubs[index] = new_ub; -} - -void Expression::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - for (unsigned int ndx = 0; ndx < n_operators; ++ndx) { - operators[ndx]->index = ndx; - operators[ndx]->propagate_bounds_forward(lbs, ubs, feasibility_tol, - integer_tol); - } -} - -void Expression::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - int ndx = n_operators - 1; - while (ndx >= 0) { - operators[ndx]->propagate_bounds_backward( - lbs, ubs, feasibility_tol, integer_tol, improvement_tol, improved_vars); - ndx -= 1; - } -} - -void Operator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - lbs[index] = -inf; - ubs[index] = inf; -} - -void Operator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - ; -} - -void MultiplyOperator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - if (operand1 == operand2) { - interval_power(operand1->get_lb_from_array(lbs), - operand1->get_ub_from_array(ubs), 2, 2, &lbs[index], - &ubs[index], feasibility_tol); - } else { - interval_mul(operand1->get_lb_from_array(lbs), - operand1->get_ub_from_array(ubs), - operand2->get_lb_from_array(lbs), - operand2->get_ub_from_array(ubs), &lbs[index], &ubs[index]); - } -} - -void MultiplyOperator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double xl = operand1->get_lb_from_array(lbs); - double xu = operand1->get_ub_from_array(ubs); - double yl = operand2->get_lb_from_array(lbs); - double yu = operand2->get_ub_from_array(ubs); - double lb = get_lb_from_array(lbs); - double ub = get_ub_from_array(ubs); - - double new_xl, new_xu, new_yl, new_yu; - - if (operand1 == operand2) { - _inverse_power1(lb, ub, 2, 2, xl, xu, &new_xl, &new_xu, feasibility_tol); - new_yl = new_xl; - new_yu = new_xu; - } else { - interval_div(lb, ub, yl, yu, &new_xl, &new_xu, feasibility_tol); - interval_div(lb, ub, xl, xu, &new_yl, &new_yu, feasibility_tol); - } - - if (new_xl > xl) - xl = new_xl; - if (new_xu < xu) - xu = new_xu; - operand1->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); - - if (new_yl > yl) - yl = new_yl; - if (new_yu < yu) - yu = new_yu; - operand2->set_bounds_in_array(yl, yu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); -} - -void SumOperator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - double lb = operands[0]->get_lb_from_array(lbs); - double ub = operands[0]->get_ub_from_array(ubs); - double tmp_lb; - double tmp_ub; - - for (unsigned int ndx = 1; ndx < nargs; ++ndx) { - interval_add(lb, ub, operands[ndx]->get_lb_from_array(lbs), - operands[ndx]->get_ub_from_array(ubs), &tmp_lb, &tmp_ub); - lb = tmp_lb; - ub = tmp_ub; - } - - lbs[index] = lb; - ubs[index] = ub; -} - -void SumOperator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double *accumulated_lbs = new double[nargs]; - double *accumulated_ubs = new double[nargs]; - - accumulated_lbs[0] = operands[0]->get_lb_from_array(lbs); - accumulated_ubs[0] = operands[0]->get_ub_from_array(ubs); - for (unsigned int ndx = 1; ndx < nargs; ++ndx) { - interval_add(accumulated_lbs[ndx - 1], accumulated_ubs[ndx - 1], - operands[ndx]->get_lb_from_array(lbs), - operands[ndx]->get_ub_from_array(ubs), &accumulated_lbs[ndx], - &accumulated_ubs[ndx]); - } - - double new_sum_lb = get_lb_from_array(lbs); - double new_sum_ub = get_ub_from_array(ubs); - - if (new_sum_lb > accumulated_lbs[nargs - 1]) - accumulated_lbs[nargs - 1] = new_sum_lb; - if (new_sum_ub < accumulated_ubs[nargs - 1]) - accumulated_ubs[nargs - 1] = new_sum_ub; - - double lb0, ub0, lb1, ub1, lb2, ub2, _lb1, _ub1, _lb2, _ub2; - - int ndx = nargs - 1; - while (ndx >= 1) { - lb0 = accumulated_lbs[ndx]; - ub0 = accumulated_ubs[ndx]; - lb1 = accumulated_lbs[ndx - 1]; - ub1 = accumulated_ubs[ndx - 1]; - lb2 = operands[ndx]->get_lb_from_array(lbs); - ub2 = operands[ndx]->get_ub_from_array(ubs); - interval_sub(lb0, ub0, lb2, ub2, &_lb1, &_ub1); - interval_sub(lb0, ub0, lb1, ub1, &_lb2, &_ub2); - if (_lb1 > lb1) - lb1 = _lb1; - if (_ub1 < ub1) - ub1 = _ub1; - if (_lb2 > lb2) - lb2 = _lb2; - if (_ub2 < ub2) - ub2 = _ub2; - accumulated_lbs[ndx - 1] = lb1; - accumulated_ubs[ndx - 1] = ub1; - operands[ndx]->set_bounds_in_array(lb2, ub2, lbs, ubs, feasibility_tol, - integer_tol, improvement_tol, - improved_vars); - ndx -= 1; - } - - // take care of ndx = 0 - lb1 = operands[0]->get_lb_from_array(lbs); - ub1 = operands[0]->get_ub_from_array(ubs); - _lb1 = accumulated_lbs[0]; - _ub1 = accumulated_ubs[0]; - if (_lb1 > lb1) - lb1 = _lb1; - if (_ub1 < ub1) - ub1 = _ub1; - operands[0]->set_bounds_in_array(lb1, ub1, lbs, ubs, feasibility_tol, - integer_tol, improvement_tol, improved_vars); - - delete[] accumulated_lbs; - delete[] accumulated_ubs; -} - -void LinearOperator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - double lb = constant->evaluate(); - double ub = lb; - double tmp_lb; - double tmp_ub; - double coef; - - for (unsigned int ndx = 0; ndx < nterms; ++ndx) { - coef = coefficients[ndx]->evaluate(); - interval_mul(coef, coef, variables[ndx]->get_lb(), variables[ndx]->get_ub(), - &tmp_lb, &tmp_ub); - interval_add(lb, ub, tmp_lb, tmp_ub, &lb, &ub); - } - - lbs[index] = lb; - ubs[index] = ub; -} - -void LinearOperator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double *accumulated_lbs = new double[nterms + 1]; - double *accumulated_ubs = new double[nterms + 1]; - - double coef; - - accumulated_lbs[0] = constant->evaluate(); - accumulated_ubs[0] = constant->evaluate(); - for (unsigned int ndx = 0; ndx < nterms; ++ndx) { - coef = coefficients[ndx]->evaluate(); - interval_mul(coef, coef, variables[ndx]->get_lb(), variables[ndx]->get_ub(), - &accumulated_lbs[ndx + 1], &accumulated_ubs[ndx + 1]); - interval_add(accumulated_lbs[ndx], accumulated_ubs[ndx], - accumulated_lbs[ndx + 1], accumulated_ubs[ndx + 1], - &accumulated_lbs[ndx + 1], &accumulated_ubs[ndx + 1]); - } - - double new_sum_lb = get_lb_from_array(lbs); - double new_sum_ub = get_ub_from_array(ubs); - - if (new_sum_lb > accumulated_lbs[nterms]) - accumulated_lbs[nterms] = new_sum_lb; - if (new_sum_ub < accumulated_ubs[nterms]) - accumulated_ubs[nterms] = new_sum_ub; - - double lb0, ub0, lb1, ub1, lb2, ub2, _lb1, _ub1, _lb2, _ub2, new_v_lb, - new_v_ub; - - int ndx = nterms - 1; - while (ndx >= 0) { - lb0 = accumulated_lbs[ndx + 1]; - ub0 = accumulated_ubs[ndx + 1]; - lb1 = accumulated_lbs[ndx]; - ub1 = accumulated_ubs[ndx]; - coef = coefficients[ndx]->evaluate(); - interval_mul(coef, coef, variables[ndx]->get_lb(), variables[ndx]->get_ub(), - &lb2, &ub2); - interval_sub(lb0, ub0, lb2, ub2, &_lb1, &_ub1); - interval_sub(lb0, ub0, lb1, ub1, &_lb2, &_ub2); - if (_lb1 > lb1) - lb1 = _lb1; - if (_ub1 < ub1) - ub1 = _ub1; - if (_lb2 > lb2) - lb2 = _lb2; - if (_ub2 < ub2) - ub2 = _ub2; - accumulated_lbs[ndx] = lb1; - accumulated_ubs[ndx] = ub1; - interval_div(lb2, ub2, coef, coef, &new_v_lb, &new_v_ub, feasibility_tol); - variables[ndx]->set_bounds_in_array(new_v_lb, new_v_ub, lbs, ubs, - feasibility_tol, integer_tol, - improvement_tol, improved_vars); - ndx -= 1; - } - - delete[] accumulated_lbs; - delete[] accumulated_ubs; -} - -void DivideOperator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - interval_div( - operand1->get_lb_from_array(lbs), operand1->get_ub_from_array(ubs), - operand2->get_lb_from_array(lbs), operand2->get_ub_from_array(ubs), - &lbs[index], &ubs[index], feasibility_tol); -} - -void DivideOperator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double xl = operand1->get_lb_from_array(lbs); - double xu = operand1->get_ub_from_array(ubs); - double yl = operand2->get_lb_from_array(lbs); - double yu = operand2->get_ub_from_array(ubs); - double lb = get_lb_from_array(lbs); - double ub = get_ub_from_array(ubs); - - double new_xl; - double new_xu; - double new_yl; - double new_yu; - - interval_mul(lb, ub, yl, yu, &new_xl, &new_xu); - interval_div(xl, xu, lb, ub, &new_yl, &new_yu, feasibility_tol); - - if (new_xl > xl) - xl = new_xl; - if (new_xu < xu) - xu = new_xu; - operand1->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); - - if (new_yl > yl) - yl = new_yl; - if (new_yu < yu) - yu = new_yu; - operand2->set_bounds_in_array(yl, yu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); -} - -void NegationOperator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - interval_sub(0, 0, operand->get_lb_from_array(lbs), - operand->get_ub_from_array(ubs), &lbs[index], &ubs[index]); -} - -void NegationOperator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double xl = operand->get_lb_from_array(lbs); - double xu = operand->get_ub_from_array(ubs); - double lb = get_lb_from_array(lbs); - double ub = get_ub_from_array(ubs); - - double new_xl; - double new_xu; - - interval_sub(0, 0, lb, ub, &new_xl, &new_xu); - - if (new_xl > xl) - xl = new_xl; - if (new_xu < xu) - xu = new_xu; - operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); -} - -void PowerOperator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - interval_power( - operand1->get_lb_from_array(lbs), operand1->get_ub_from_array(ubs), - operand2->get_lb_from_array(lbs), operand2->get_ub_from_array(ubs), - &lbs[index], &ubs[index], feasibility_tol); -} - -void PowerOperator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double xl = operand1->get_lb_from_array(lbs); - double xu = operand1->get_ub_from_array(ubs); - double yl = operand2->get_lb_from_array(lbs); - double yu = operand2->get_ub_from_array(ubs); - double lb = get_lb_from_array(lbs); - double ub = get_ub_from_array(ubs); - - double new_xl, new_xu, new_yl, new_yu; - _inverse_power1(lb, ub, yl, yu, xl, xu, &new_xl, &new_xu, feasibility_tol); - if (yl != yu) - _inverse_power2(lb, ub, xl, xu, &new_yl, &new_yu, feasibility_tol); - else { - new_yl = yl; - new_yu = yu; - } - - if (new_xl > xl) - xl = new_xl; - if (new_xu < xu) - xu = new_xu; - operand1->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); - - if (new_yl > yl) - yl = new_yl; - if (new_yu < yu) - yu = new_yu; - operand2->set_bounds_in_array(yl, yu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); -} - -void SqrtOperator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - interval_power(operand->get_lb_from_array(lbs), - operand->get_ub_from_array(ubs), 0.5, 0.5, &lbs[index], - &ubs[index], feasibility_tol); -} - -void SqrtOperator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double xl = operand->get_lb_from_array(lbs); - double xu = operand->get_ub_from_array(ubs); - double yl = 0.5; - double yu = 0.5; - double lb = get_lb_from_array(lbs); - double ub = get_ub_from_array(ubs); - - double new_xl, new_xu; - _inverse_power1(lb, ub, yl, yu, xl, xu, &new_xl, &new_xu, feasibility_tol); - - if (new_xl > xl) - xl = new_xl; - if (new_xu < xu) - xu = new_xu; - operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); -} - -void ExpOperator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - interval_exp(operand->get_lb_from_array(lbs), operand->get_ub_from_array(ubs), - &lbs[index], &ubs[index]); -} - -void ExpOperator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double xl = operand->get_lb_from_array(lbs); - double xu = operand->get_ub_from_array(ubs); - double lb = get_lb_from_array(lbs); - double ub = get_ub_from_array(ubs); - - double new_xl, new_xu; - interval_log(lb, ub, &new_xl, &new_xu); - - if (new_xl > xl) - xl = new_xl; - if (new_xu < xu) - xu = new_xu; - operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); -} - -void LogOperator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - interval_log(operand->get_lb_from_array(lbs), operand->get_ub_from_array(ubs), - &lbs[index], &ubs[index]); -} - -void LogOperator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double xl = operand->get_lb_from_array(lbs); - double xu = operand->get_ub_from_array(ubs); - double lb = get_lb_from_array(lbs); - double ub = get_ub_from_array(ubs); - - double new_xl, new_xu; - interval_exp(lb, ub, &new_xl, &new_xu); - - if (new_xl > xl) - xl = new_xl; - if (new_xu < xu) - xu = new_xu; - operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); -} - -void AbsOperator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - interval_abs(operand->get_lb_from_array(lbs), operand->get_ub_from_array(ubs), - &lbs[index], &ubs[index]); -} - -void AbsOperator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double xl = operand->get_lb_from_array(lbs); - double xu = operand->get_ub_from_array(ubs); - double lb = get_lb_from_array(lbs); - double ub = get_ub_from_array(ubs); - - double new_xl, new_xu; - _inverse_abs(lb, ub, &new_xl, &new_xu); - - if (new_xl > xl) - xl = new_xl; - if (new_xu < xu) - xu = new_xu; - operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); -} - -void Log10Operator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - interval_log10(operand->get_lb_from_array(lbs), - operand->get_ub_from_array(ubs), &lbs[index], &ubs[index]); -} - -void Log10Operator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double xl = operand->get_lb_from_array(lbs); - double xu = operand->get_ub_from_array(ubs); - double lb = get_lb_from_array(lbs); - double ub = get_ub_from_array(ubs); - - double new_xl, new_xu; - interval_power(10, 10, lb, ub, &new_xl, &new_xu, feasibility_tol); - - if (new_xl > xl) - xl = new_xl; - if (new_xu < xu) - xu = new_xu; - operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); -} - -void SinOperator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - interval_sin(operand->get_lb_from_array(lbs), operand->get_ub_from_array(ubs), - &lbs[index], &ubs[index]); -} - -void SinOperator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double xl = operand->get_lb_from_array(lbs); - double xu = operand->get_ub_from_array(ubs); - double lb = get_lb_from_array(lbs); - double ub = get_ub_from_array(ubs); - - double new_xl, new_xu; - interval_asin(lb, ub, xl, xu, &new_xl, &new_xu, feasibility_tol); - - if (new_xl > xl) - xl = new_xl; - if (new_xu < xu) - xu = new_xu; - operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); -} - -void CosOperator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - interval_cos(operand->get_lb_from_array(lbs), operand->get_ub_from_array(ubs), - &lbs[index], &ubs[index]); -} - -void CosOperator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double xl = operand->get_lb_from_array(lbs); - double xu = operand->get_ub_from_array(ubs); - double lb = get_lb_from_array(lbs); - double ub = get_ub_from_array(ubs); - - double new_xl, new_xu; - interval_acos(lb, ub, xl, xu, &new_xl, &new_xu, feasibility_tol); - - if (new_xl > xl) - xl = new_xl; - if (new_xu < xu) - xu = new_xu; - operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); -} - -void TanOperator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - interval_tan(operand->get_lb_from_array(lbs), operand->get_ub_from_array(ubs), - &lbs[index], &ubs[index]); -} - -void TanOperator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double xl = operand->get_lb_from_array(lbs); - double xu = operand->get_ub_from_array(ubs); - double lb = get_lb_from_array(lbs); - double ub = get_ub_from_array(ubs); - - double new_xl, new_xu; - interval_atan(lb, ub, xl, xu, &new_xl, &new_xu); - - if (new_xl > xl) - xl = new_xl; - if (new_xu < xu) - xu = new_xu; - operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); -} - -void AsinOperator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - interval_asin(operand->get_lb_from_array(lbs), - operand->get_ub_from_array(ubs), -inf, inf, &lbs[index], - &ubs[index], feasibility_tol); -} - -void AsinOperator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double xl = operand->get_lb_from_array(lbs); - double xu = operand->get_ub_from_array(ubs); - double lb = get_lb_from_array(lbs); - double ub = get_ub_from_array(ubs); - - double new_xl, new_xu; - interval_sin(lb, ub, &new_xl, &new_xu); - - if (new_xl > xl) - xl = new_xl; - if (new_xu < xu) - xu = new_xu; - operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); -} - -void AcosOperator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - interval_acos(operand->get_lb_from_array(lbs), - operand->get_ub_from_array(ubs), -inf, inf, &lbs[index], - &ubs[index], feasibility_tol); -} - -void AcosOperator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double xl = operand->get_lb_from_array(lbs); - double xu = operand->get_ub_from_array(ubs); - double lb = get_lb_from_array(lbs); - double ub = get_ub_from_array(ubs); - - double new_xl, new_xu; - interval_cos(lb, ub, &new_xl, &new_xu); - - if (new_xl > xl) - xl = new_xl; - if (new_xu < xu) - xu = new_xu; - operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); -} - -void AtanOperator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - interval_atan(operand->get_lb_from_array(lbs), - operand->get_ub_from_array(ubs), -inf, inf, &lbs[index], - &ubs[index]); -} - -void AtanOperator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double xl = operand->get_lb_from_array(lbs); - double xu = operand->get_ub_from_array(ubs); - double lb = get_lb_from_array(lbs); - double ub = get_ub_from_array(ubs); - - double new_xl, new_xu; - interval_tan(lb, ub, &new_xl, &new_xu); - - if (new_xl > xl) - xl = new_xl; - if (new_xu < xu) - xu = new_xu; - operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); -} - -std::vector> create_vars(int n_vars) { - std::vector> res; - for (int i = 0; i < n_vars; ++i) { - res.push_back(std::make_shared()); - } - return res; -} - -std::vector> create_params(int n_params) { - std::vector> res; - for (int i = 0; i < n_params; ++i) { - res.push_back(std::make_shared()); - } - return res; -} - -std::vector> create_constants(int n_constants) { - std::vector> res; - for (int i = 0; i < n_constants; ++i) { - res.push_back(std::make_shared()); - } - return res; -} - -std::shared_ptr -appsi_operator_from_pyomo_expr(py::handle expr, py::handle var_map, - py::handle param_map, - PyomoExprTypes &expr_types) { - std::shared_ptr res; - ExprType tmp_type = - expr_types.expr_type_map[py::type::of(expr)].cast(); - - switch (tmp_type) { - case py_float: { - res = std::make_shared(expr.cast()); - break; - } - case var: { - res = var_map[expr_types.id(expr)].cast>(); - break; - } - case param: { - res = param_map[expr_types.id(expr)].cast>(); - break; - } - case product: { - res = std::make_shared(); - break; - } - case sum: { - res = std::make_shared(expr.attr("nargs")().cast()); - break; - } - case negation: { - res = std::make_shared(); - break; - } - case external_func: { - res = std::make_shared(expr.attr("nargs")().cast()); - std::shared_ptr oper = - std::dynamic_pointer_cast(res); - oper->function_name = - expr.attr("_fcn").attr("_function").cast(); - break; - } - case power: { - res = std::make_shared(); - break; - } - case division: { - res = std::make_shared(); - break; - } - case unary_func: { - std::string function_name = expr.attr("getname")().cast(); - if (function_name == "exp") - res = std::make_shared(); - else if (function_name == "log") - res = std::make_shared(); - else if (function_name == "log10") - res = std::make_shared(); - else if (function_name == "sin") - res = std::make_shared(); - else if (function_name == "cos") - res = std::make_shared(); - else if (function_name == "tan") - res = std::make_shared(); - else if (function_name == "asin") - res = std::make_shared(); - else if (function_name == "acos") - res = std::make_shared(); - else if (function_name == "atan") - res = std::make_shared(); - else if (function_name == "sqrt") - res = std::make_shared(); - else - throw py::value_error("Unrecognized expression type: " + function_name); - break; - } - case linear: { - res = std::make_shared( - expr_types.len(expr.attr("linear_vars")).cast()); - break; - } - case named_expr: { - res = appsi_operator_from_pyomo_expr(expr.attr("expr"), var_map, param_map, - expr_types); - break; - } - case numeric_constant: { - res = std::make_shared(expr.attr("value").cast()); - break; - } - case pyomo_unit: { - res = std::make_shared(1.0); - break; - } - case unary_abs: { - res = std::make_shared(); - break; - } - default: { - throw py::value_error("Unrecognized expression type: " + - expr_types.builtins.attr("str")(py::type::of(expr)) - .cast()); - break; - } - } - return res; -} - -void prep_for_repn_helper(py::handle expr, py::handle named_exprs, - py::handle variables, py::handle fixed_vars, - py::handle external_funcs, - PyomoExprTypes &expr_types) { - ExprType tmp_type = - expr_types.expr_type_map[py::type::of(expr)].cast(); - - switch (tmp_type) { - case py_float: { - break; - } - case var: { - variables[expr_types.id(expr)] = expr; - if (expr.attr("fixed").cast()) { - fixed_vars[expr_types.id(expr)] = expr; - } - break; - } - case param: { - break; - } - case product: { - py::tuple args = expr.attr("_args_"); - for (py::handle arg : args) { - prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, - external_funcs, expr_types); - } - break; - } - case sum: { - py::tuple args = expr.attr("args"); - for (py::handle arg : args) { - prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, - external_funcs, expr_types); - } - break; - } - case negation: { - py::tuple args = expr.attr("_args_"); - for (py::handle arg : args) { - prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, - external_funcs, expr_types); - } - break; - } - case external_func: { - external_funcs[expr_types.id(expr)] = expr; - py::tuple args = expr.attr("args"); - for (py::handle arg : args) { - prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, - external_funcs, expr_types); - } - break; - } - case power: { - py::tuple args = expr.attr("_args_"); - for (py::handle arg : args) { - prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, - external_funcs, expr_types); - } - break; - } - case division: { - py::tuple args = expr.attr("_args_"); - for (py::handle arg : args) { - prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, - external_funcs, expr_types); - } - break; - } - case unary_func: { - py::tuple args = expr.attr("_args_"); - for (py::handle arg : args) { - prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, - external_funcs, expr_types); - } - break; - } - case linear: { - py::list linear_vars = expr.attr("linear_vars"); - py::list linear_coefs = expr.attr("linear_coefs"); - for (py::handle arg : linear_vars) { - prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, - external_funcs, expr_types); - } - for (py::handle arg : linear_coefs) { - prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, - external_funcs, expr_types); - } - prep_for_repn_helper(expr.attr("constant"), named_exprs, variables, - fixed_vars, external_funcs, expr_types); - break; - } - case named_expr: { - named_exprs[expr_types.id(expr)] = expr; - prep_for_repn_helper(expr.attr("expr"), named_exprs, variables, fixed_vars, - external_funcs, expr_types); - break; - } - case numeric_constant: { - break; - } - case pyomo_unit: { - break; - } - case unary_abs: { - py::tuple args = expr.attr("_args_"); - for (py::handle arg : args) { - prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, - external_funcs, expr_types); - } - break; - } - default: { - if (expr_types.builtins.attr("hasattr")(expr, "is_constant").cast()) { - if (expr.attr("is_constant")().cast()) - break; - } - throw py::value_error("Unrecognized expression type: " + - expr_types.builtins.attr("str")(py::type::of(expr)) - .cast()); - break; - } - } -} - -py::tuple prep_for_repn(py::handle expr, PyomoExprTypes &expr_types) { - py::dict named_exprs; - py::dict variables; - py::dict fixed_vars; - py::dict external_funcs; - - prep_for_repn_helper(expr, named_exprs, variables, fixed_vars, external_funcs, - expr_types); - - py::list named_expr_list = named_exprs.attr("values")(); - py::list variable_list = variables.attr("values")(); - py::list fixed_var_list = fixed_vars.attr("values")(); - py::list external_func_list = external_funcs.attr("values")(); - - py::tuple res = py::make_tuple(named_expr_list, variable_list, fixed_var_list, - external_func_list); - return res; -} - -int build_expression_tree(py::handle pyomo_expr, - std::shared_ptr appsi_expr, py::handle var_map, - py::handle param_map, PyomoExprTypes &expr_types) { - int num_nodes = 0; - - if (expr_types.expr_type_map[py::type::of(pyomo_expr)].cast() == - named_expr) - pyomo_expr = pyomo_expr.attr("expr"); - - if (appsi_expr->is_leaf()) { - ; - } else if (appsi_expr->is_binary_operator()) { - num_nodes += 1; - std::shared_ptr oper = - std::dynamic_pointer_cast(appsi_expr); - py::list pyomo_args = pyomo_expr.attr("args"); - oper->operand1 = appsi_operator_from_pyomo_expr(pyomo_args[0], var_map, - param_map, expr_types); - oper->operand2 = appsi_operator_from_pyomo_expr(pyomo_args[1], var_map, - param_map, expr_types); - num_nodes += build_expression_tree(pyomo_args[0], oper->operand1, var_map, - param_map, expr_types); - num_nodes += build_expression_tree(pyomo_args[1], oper->operand2, var_map, - param_map, expr_types); - } else if (appsi_expr->is_unary_operator()) { - num_nodes += 1; - std::shared_ptr oper = - std::dynamic_pointer_cast(appsi_expr); - py::list pyomo_args = pyomo_expr.attr("args"); - oper->operand = appsi_operator_from_pyomo_expr(pyomo_args[0], var_map, - param_map, expr_types); - num_nodes += build_expression_tree(pyomo_args[0], oper->operand, var_map, - param_map, expr_types); - } else if (appsi_expr->is_sum_operator()) { - num_nodes += 1; - std::shared_ptr oper = - std::dynamic_pointer_cast(appsi_expr); - py::list pyomo_args = pyomo_expr.attr("args"); - for (unsigned int arg_ndx = 0; arg_ndx < oper->nargs; ++arg_ndx) { - oper->operands[arg_ndx] = appsi_operator_from_pyomo_expr( - pyomo_args[arg_ndx], var_map, param_map, expr_types); - num_nodes += - build_expression_tree(pyomo_args[arg_ndx], oper->operands[arg_ndx], - var_map, param_map, expr_types); - } - } else if (appsi_expr->is_linear_operator()) { - num_nodes += 1; - std::shared_ptr oper = - std::dynamic_pointer_cast(appsi_expr); - oper->constant = appsi_expr_from_pyomo_expr(pyomo_expr.attr("constant"), - var_map, param_map, expr_types); - py::list pyomo_vars = pyomo_expr.attr("linear_vars"); - py::list pyomo_coefs = pyomo_expr.attr("linear_coefs"); - for (unsigned int arg_ndx = 0; arg_ndx < oper->nterms; ++arg_ndx) { - oper->variables[arg_ndx] = var_map[expr_types.id(pyomo_vars[arg_ndx])] - .cast>(); - oper->coefficients[arg_ndx] = appsi_expr_from_pyomo_expr( - pyomo_coefs[arg_ndx], var_map, param_map, expr_types); - } - } else if (appsi_expr->is_external_operator()) { - num_nodes += 1; - std::shared_ptr oper = - std::dynamic_pointer_cast(appsi_expr); - py::list pyomo_args = pyomo_expr.attr("args"); - for (unsigned int arg_ndx = 0; arg_ndx < oper->nargs; ++arg_ndx) { - oper->operands[arg_ndx] = appsi_operator_from_pyomo_expr( - pyomo_args[arg_ndx], var_map, param_map, expr_types); - num_nodes += - build_expression_tree(pyomo_args[arg_ndx], oper->operands[arg_ndx], - var_map, param_map, expr_types); - } - } else { - throw py::value_error( - "Unrecognized expression type: " + - expr_types.builtins.attr("str")(py::type::of(pyomo_expr)) - .cast()); - } - return num_nodes; -} - -std::shared_ptr -appsi_expr_from_pyomo_expr(py::handle expr, py::handle var_map, - py::handle param_map, PyomoExprTypes &expr_types) { - std::shared_ptr node = - appsi_operator_from_pyomo_expr(expr, var_map, param_map, expr_types); - int num_nodes = - build_expression_tree(expr, node, var_map, param_map, expr_types); - if (num_nodes == 0) { - return std::dynamic_pointer_cast(node); - } else { - std::shared_ptr res = std::make_shared(num_nodes); - node->fill_expression(res->operators, num_nodes); - return res; - } -} - -std::vector> -appsi_exprs_from_pyomo_exprs(py::list expr_list, py::dict var_map, - py::dict param_map) { - PyomoExprTypes expr_types = PyomoExprTypes(); - int num_exprs = expr_types.builtins.attr("len")(expr_list).cast(); - std::vector> res(num_exprs); - - int ndx = 0; - for (py::handle expr : expr_list) { - res[ndx] = appsi_expr_from_pyomo_expr(expr, var_map, param_map, expr_types); - ndx += 1; - } - return res; -} - -void process_pyomo_vars(PyomoExprTypes &expr_types, py::list pyomo_vars, - py::dict var_map, py::dict param_map, - py::dict var_attrs, py::dict rev_var_map, - py::bool_ _set_name, py::handle symbol_map, - py::handle labeler, py::bool_ _update) { - py::tuple v_attrs; - std::shared_ptr cv; - py::handle v_lb; - py::handle v_ub; - py::handle v_val; - py::tuple domain_interval; - py::handle interval_lb; - py::handle interval_ub; - py::handle interval_step; - bool v_fixed; - bool set_name = _set_name.cast(); - bool update = _update.cast(); - double domain_step; - - for (py::handle v : pyomo_vars) { - v_attrs = var_attrs[expr_types.id(v)]; - v_lb = v_attrs[1]; - v_ub = v_attrs[2]; - v_fixed = v_attrs[3].cast(); - domain_interval = v_attrs[4]; - v_val = v_attrs[5]; - - interval_lb = domain_interval[0]; - interval_ub = domain_interval[1]; - interval_step = domain_interval[2]; - domain_step = interval_step.cast(); - - if (update) { - cv = var_map[expr_types.id(v)].cast>(); - } else { - cv = std::make_shared(); - } - - if (!(v_lb.is(py::none()))) { - cv->lb = appsi_expr_from_pyomo_expr(v_lb, var_map, param_map, expr_types); - } else { - cv->lb = std::make_shared(-inf); - } - if (!(v_ub.is(py::none()))) { - cv->ub = appsi_expr_from_pyomo_expr(v_ub, var_map, param_map, expr_types); - } else { - cv->ub = std::make_shared(inf); - } - - if (!(v_val.is(py::none()))) { - cv->value = v_val.cast(); - } - - if (v_fixed) { - cv->fixed = true; - } else { - cv->fixed = false; - } - - if (set_name && !update) { - cv->name = symbol_map.attr("getSymbol")(v, labeler).cast(); - } - - if (interval_lb.is(py::none())) - cv->domain_lb = -inf; - else - cv->domain_lb = interval_lb.cast(); - if (interval_ub.is(py::none())) - cv->domain_ub = inf; - else - cv->domain_ub = interval_ub.cast(); - if (domain_step == 0) - cv->domain = continuous; - else if (domain_step == 1) { - if ((cv->domain_lb == 0) && (cv->domain_ub == 1)) - cv->domain = binary; - else - cv->domain = integers; - } else - throw py::value_error("Unrecognized domain step"); - - if (!update) { - var_map[expr_types.id(v)] = py::cast(cv); - rev_var_map[py::cast(cv)] = v; - } - } -} +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2024 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + +#include "expression.hpp" + +bool Leaf::is_leaf() { return true; } + +bool Var::is_variable_type() { return true; } + +bool Param::is_param_type() { return true; } + +bool Constant::is_constant_type() { return true; } + +bool Expression::is_expression_type() { return true; } + +double Leaf::evaluate() { return value; } + +double Var::get_lb() { + if (fixed) + return value; + else + return std::max(lb->evaluate(), domain_lb); +} + +double Var::get_ub() { + if (fixed) + return value; + else + return std::min(ub->evaluate(), domain_ub); +} + +Domain Var::get_domain() { return domain; } + +bool Operator::is_operator_type() { return true; } + +std::vector> Expression::get_operators() { + std::vector> res(n_operators); + for (unsigned int i = 0; i < n_operators; ++i) { + res[i] = operators[i]; + } + return res; +} + +double Leaf::get_value_from_array(double *val_array) { return value; } + +double Expression::get_value_from_array(double *val_array) { + return val_array[n_operators - 1]; +} + +double Operator::get_value_from_array(double *val_array) { + return val_array[index]; +} + +void MultiplyOperator::evaluate(double *values) { + values[index] = operand1->get_value_from_array(values) * + operand2->get_value_from_array(values); +} + +void ExternalOperator::evaluate(double *values) { + // It would be nice to implement this, but it will take some more work. + // This would require dynamic linking to the external function. + throw std::runtime_error("cannot evaluate ExternalOperator yet"); +} + +void LinearOperator::evaluate(double *values) { + values[index] = constant->evaluate(); + for (unsigned int i = 0; i < nterms; ++i) { + values[index] += coefficients[i]->evaluate() * variables[i]->evaluate(); + } +} + +void SumOperator::evaluate(double *values) { + values[index] = 0.0; + for (unsigned int i = 0; i < nargs; ++i) { + values[index] += operands[i]->get_value_from_array(values); + } +} + +void DivideOperator::evaluate(double *values) { + values[index] = operand1->get_value_from_array(values) / + operand2->get_value_from_array(values); +} + +void PowerOperator::evaluate(double *values) { + values[index] = std::pow(operand1->get_value_from_array(values), + operand2->get_value_from_array(values)); +} + +void NegationOperator::evaluate(double *values) { + values[index] = -operand->get_value_from_array(values); +} + +void ExpOperator::evaluate(double *values) { + values[index] = std::exp(operand->get_value_from_array(values)); +} + +void LogOperator::evaluate(double *values) { + values[index] = std::log(operand->get_value_from_array(values)); +} + +void AbsOperator::evaluate(double *values) { + values[index] = std::fabs(operand->get_value_from_array(values)); +} + +void SqrtOperator::evaluate(double *values) { + values[index] = std::pow(operand->get_value_from_array(values), 0.5); +} + +void Log10Operator::evaluate(double *values) { + values[index] = std::log10(operand->get_value_from_array(values)); +} + +void SinOperator::evaluate(double *values) { + values[index] = std::sin(operand->get_value_from_array(values)); +} + +void CosOperator::evaluate(double *values) { + values[index] = std::cos(operand->get_value_from_array(values)); +} + +void TanOperator::evaluate(double *values) { + values[index] = std::tan(operand->get_value_from_array(values)); +} + +void AsinOperator::evaluate(double *values) { + values[index] = std::asin(operand->get_value_from_array(values)); +} + +void AcosOperator::evaluate(double *values) { + values[index] = std::acos(operand->get_value_from_array(values)); +} + +void AtanOperator::evaluate(double *values) { + values[index] = std::atan(operand->get_value_from_array(values)); +} + +double Expression::evaluate() { + double *values = new double[n_operators]; + for (unsigned int i = 0; i < n_operators; ++i) { + operators[i]->index = i; + operators[i]->evaluate(values); + } + double res = get_value_from_array(values); + delete[] values; + return res; +} + +void UnaryOperator::identify_variables( + std::set> &var_set, + std::shared_ptr>> var_vec) { + if (operand->is_variable_type()) { + if (var_set.count(operand) == 0) { + var_vec->push_back(std::dynamic_pointer_cast(operand)); + var_set.insert(operand); + } + } +} + +void BinaryOperator::identify_variables( + std::set> &var_set, + std::shared_ptr>> var_vec) { + if (operand1->is_variable_type()) { + if (var_set.count(operand1) == 0) { + var_vec->push_back(std::dynamic_pointer_cast(operand1)); + var_set.insert(operand1); + } + } + if (operand2->is_variable_type()) { + if (var_set.count(operand2) == 0) { + var_vec->push_back(std::dynamic_pointer_cast(operand2)); + var_set.insert(operand2); + } + } +} + +void ExternalOperator::identify_variables( + std::set> &var_set, + std::shared_ptr>> var_vec) { + for (unsigned int i = 0; i < nargs; ++i) { + if (operands[i]->is_variable_type()) { + if (var_set.count(operands[i]) == 0) { + var_vec->push_back(std::dynamic_pointer_cast(operands[i])); + var_set.insert(operands[i]); + } + } + } +} + +void LinearOperator::identify_variables( + std::set> &var_set, + std::shared_ptr>> var_vec) { + for (unsigned int i = 0; i < nterms; ++i) { + if (var_set.count(variables[i]) == 0) { + var_vec->push_back(std::dynamic_pointer_cast(variables[i])); + var_set.insert(variables[i]); + } + } +} + +void SumOperator::identify_variables( + std::set> &var_set, + std::shared_ptr>> var_vec) { + for (unsigned int i = 0; i < nargs; ++i) { + if (operands[i]->is_variable_type()) { + if (var_set.count(operands[i]) == 0) { + var_vec->push_back(std::dynamic_pointer_cast(operands[i])); + var_set.insert(operands[i]); + } + } + } +} + +std::shared_ptr>> +Expression::identify_variables() { + std::set> var_set; + std::shared_ptr>> res = + std::make_shared>>(var_set.size()); + for (unsigned int i = 0; i < n_operators; ++i) { + operators[i]->identify_variables(var_set, res); + } + return res; +} + +std::shared_ptr>> Var::identify_variables() { + std::shared_ptr>> res = + std::make_shared>>(); + res->push_back(shared_from_this()); + return res; +} + +std::shared_ptr>> +Constant::identify_variables() { + std::shared_ptr>> res = + std::make_shared>>(); + return res; +} + +std::shared_ptr>> Param::identify_variables() { + std::shared_ptr>> res = + std::make_shared>>(); + return res; +} + +std::shared_ptr>> +Expression::identify_external_operators() { + std::set> external_set; + for (unsigned int i = 0; i < n_operators; ++i) { + if (operators[i]->is_external_operator()) { + external_set.insert(operators[i]); + } + } + std::shared_ptr>> res = + std::make_shared>>( + external_set.size()); + int ndx = 0; + for (std::shared_ptr n : external_set) { + (*res)[ndx] = std::dynamic_pointer_cast(n); + ndx += 1; + } + return res; +} + +std::shared_ptr>> +Var::identify_external_operators() { + std::shared_ptr>> res = + std::make_shared>>(); + return res; +} + +std::shared_ptr>> +Constant::identify_external_operators() { + std::shared_ptr>> res = + std::make_shared>>(); + return res; +} + +std::shared_ptr>> +Param::identify_external_operators() { + std::shared_ptr>> res = + std::make_shared>>(); + return res; +} + +int Var::get_degree_from_array(int *degree_array) { return 1; } + +int Param::get_degree_from_array(int *degree_array) { return 0; } + +int Constant::get_degree_from_array(int *degree_array) { return 0; } + +int Expression::get_degree_from_array(int *degree_array) { + return degree_array[n_operators - 1]; +} + +int Operator::get_degree_from_array(int *degree_array) { + return degree_array[index]; +} + +void LinearOperator::propagate_degree_forward(int *degrees, double *values) { + degrees[index] = 1; +} + +void SumOperator::propagate_degree_forward(int *degrees, double *values) { + int deg = 0; + int _deg; + for (unsigned int i = 0; i < nargs; ++i) { + _deg = operands[i]->get_degree_from_array(degrees); + if (_deg > deg) { + deg = _deg; + } + } + degrees[index] = deg; +} + +void MultiplyOperator::propagate_degree_forward(int *degrees, double *values) { + degrees[index] = operand1->get_degree_from_array(degrees) + + operand2->get_degree_from_array(degrees); +} + +void ExternalOperator::propagate_degree_forward(int *degrees, double *values) { + // External functions are always considered nonlinear + // Anything larger than 2 is nonlinear + degrees[index] = 3; +} + +void DivideOperator::propagate_degree_forward(int *degrees, double *values) { + // anything larger than 2 is nonlinear + degrees[index] = std::max(operand1->get_degree_from_array(degrees), + 3 * (operand2->get_degree_from_array(degrees))); +} + +void PowerOperator::propagate_degree_forward(int *degrees, double *values) { + if (operand2->get_degree_from_array(degrees) != 0) { + degrees[index] = 3; + } else { + double val2 = operand2->get_value_from_array(values); + double intpart; + if (std::modf(val2, &intpart) == 0.0) { + degrees[index] = operand1->get_degree_from_array(degrees) * (int)val2; + } else { + degrees[index] = 3; + } + } +} + +void NegationOperator::propagate_degree_forward(int *degrees, double *values) { + degrees[index] = operand->get_degree_from_array(degrees); +} + +void UnaryOperator::propagate_degree_forward(int *degrees, double *values) { + if (operand->get_degree_from_array(degrees) == 0) { + degrees[index] = 0; + } else { + degrees[index] = 3; + } +} + +std::string Var::__str__() { return name; } + +std::string Param::__str__() { return name; } + +std::string Constant::__str__() { return std::to_string(value); } + +std::string Expression::__str__() { + std::string *string_array = new std::string[n_operators]; + std::shared_ptr oper; + for (unsigned int i = 0; i < n_operators; ++i) { + oper = operators[i]; + oper->index = i; + oper->print(string_array); + } + std::string res = string_array[n_operators - 1]; + delete[] string_array; + return res; +} + +std::string Leaf::get_string_from_array(std::string *string_array) { + return __str__(); +} + +std::string Expression::get_string_from_array(std::string *string_array) { + return string_array[n_operators - 1]; +} + +std::string Operator::get_string_from_array(std::string *string_array) { + return string_array[index]; +} + +void MultiplyOperator::print(std::string *string_array) { + string_array[index] = + ("(" + operand1->get_string_from_array(string_array) + "*" + + operand2->get_string_from_array(string_array) + ")"); +} + +void ExternalOperator::print(std::string *string_array) { + std::string res = function_name + "("; + for (unsigned int i = 0; i < (nargs - 1); ++i) { + res += operands[i]->get_string_from_array(string_array); + res += ", "; + } + res += operands[nargs - 1]->get_string_from_array(string_array); + res += ")"; + string_array[index] = res; +} + +void DivideOperator::print(std::string *string_array) { + string_array[index] = + ("(" + operand1->get_string_from_array(string_array) + "/" + + operand2->get_string_from_array(string_array) + ")"); +} + +void PowerOperator::print(std::string *string_array) { + string_array[index] = + ("(" + operand1->get_string_from_array(string_array) + "**" + + operand2->get_string_from_array(string_array) + ")"); +} + +void NegationOperator::print(std::string *string_array) { + string_array[index] = + ("(-" + operand->get_string_from_array(string_array) + ")"); +} + +void ExpOperator::print(std::string *string_array) { + string_array[index] = + ("exp(" + operand->get_string_from_array(string_array) + ")"); +} + +void LogOperator::print(std::string *string_array) { + string_array[index] = + ("log(" + operand->get_string_from_array(string_array) + ")"); +} + +void AbsOperator::print(std::string *string_array) { + string_array[index] = + ("abs(" + operand->get_string_from_array(string_array) + ")"); +} + +void SqrtOperator::print(std::string *string_array) { + string_array[index] = + ("sqrt(" + operand->get_string_from_array(string_array) + ")"); +} + +void Log10Operator::print(std::string *string_array) { + string_array[index] = + ("log10(" + operand->get_string_from_array(string_array) + ")"); +} + +void SinOperator::print(std::string *string_array) { + string_array[index] = + ("sin(" + operand->get_string_from_array(string_array) + ")"); +} + +void CosOperator::print(std::string *string_array) { + string_array[index] = + ("cos(" + operand->get_string_from_array(string_array) + ")"); +} + +void TanOperator::print(std::string *string_array) { + string_array[index] = + ("tan(" + operand->get_string_from_array(string_array) + ")"); +} + +void AsinOperator::print(std::string *string_array) { + string_array[index] = + ("asin(" + operand->get_string_from_array(string_array) + ")"); +} + +void AcosOperator::print(std::string *string_array) { + string_array[index] = + ("acos(" + operand->get_string_from_array(string_array) + ")"); +} + +void AtanOperator::print(std::string *string_array) { + string_array[index] = + ("atan(" + operand->get_string_from_array(string_array) + ")"); +} + +void LinearOperator::print(std::string *string_array) { + std::string res = "(" + constant->__str__(); + for (unsigned int i = 0; i < nterms; ++i) { + res += " + " + coefficients[i]->__str__() + "*" + variables[i]->__str__(); + } + res += ")"; + string_array[index] = res; +} + +void SumOperator::print(std::string *string_array) { + std::string res = "(" + operands[0]->get_string_from_array(string_array); + for (unsigned int i = 1; i < nargs; ++i) { + res += " + " + operands[i]->get_string_from_array(string_array); + } + res += ")"; + string_array[index] = res; +} + +std::shared_ptr>> +Leaf::get_prefix_notation() { + std::shared_ptr>> res = + std::make_shared>>(); + res->push_back(shared_from_this()); + return res; +} + +std::shared_ptr>> +Expression::get_prefix_notation() { + std::shared_ptr>> res = + std::make_shared>>(); + std::shared_ptr>> stack = + std::make_shared>>(); + std::shared_ptr node; + stack->push_back(operators[n_operators - 1]); + while (stack->size() > 0) { + node = stack->back(); + stack->pop_back(); + res->push_back(node); + node->fill_prefix_notation_stack(stack); + } + + return res; +} + +void BinaryOperator::fill_prefix_notation_stack( + std::shared_ptr>> stack) { + stack->push_back(operand2); + stack->push_back(operand1); +} + +void UnaryOperator::fill_prefix_notation_stack( + std::shared_ptr>> stack) { + stack->push_back(operand); +} + +void SumOperator::fill_prefix_notation_stack( + std::shared_ptr>> stack) { + int ndx = nargs - 1; + while (ndx >= 0) { + stack->push_back(operands[ndx]); + ndx -= 1; + } +} + +void LinearOperator::fill_prefix_notation_stack( + std::shared_ptr>> stack) { + ; // This is treated as a leaf in this context; write_nl_string will take care + // of it +} + +void ExternalOperator::fill_prefix_notation_stack( + std::shared_ptr>> stack) { + int i = nargs - 1; + while (i >= 0) { + stack->push_back(operands[i]); + i -= 1; + } +} + +void Var::write_nl_string(std::ofstream &f) { f << "v" << index << "\n"; } + +void Param::write_nl_string(std::ofstream &f) { f << "n" << value << "\n"; } + +void Constant::write_nl_string(std::ofstream &f) { f << "n" << value << "\n"; } + +void Expression::write_nl_string(std::ofstream &f) { + std::shared_ptr>> prefix_notation = + get_prefix_notation(); + for (std::shared_ptr &node : *(prefix_notation)) { + node->write_nl_string(f); + } +} + +void MultiplyOperator::write_nl_string(std::ofstream &f) { f << "o2\n"; } + +void ExternalOperator::write_nl_string(std::ofstream &f) { + f << "f" << external_function_index << " " << nargs << "\n"; +} + +void SumOperator::write_nl_string(std::ofstream &f) { + if (nargs == 2) { + f << "o0\n"; + } else { + f << "o54\n"; + f << nargs << "\n"; + } +} + +void LinearOperator::write_nl_string(std::ofstream &f) { + bool has_const = + (!constant->is_constant_type()) || (constant->evaluate() != 0); + unsigned int n_sum_args = nterms + (has_const ? 1 : 0); + if (n_sum_args == 2) { + f << "o0\n"; + } else { + f << "o54\n"; + f << n_sum_args << "\n"; + } + if (has_const) + f << "n" << constant->evaluate() << "\n"; + for (unsigned int ndx = 0; ndx < nterms; ++ndx) { + f << "o2\n"; + f << "n" << coefficients[ndx]->evaluate() << "\n"; + variables[ndx]->write_nl_string(f); + } +} + +void DivideOperator::write_nl_string(std::ofstream &f) { f << "o3\n"; } + +void PowerOperator::write_nl_string(std::ofstream &f) { f << "o5\n"; } + +void NegationOperator::write_nl_string(std::ofstream &f) { f << "o16\n"; } + +void ExpOperator::write_nl_string(std::ofstream &f) { f << "o44\n"; } + +void LogOperator::write_nl_string(std::ofstream &f) { f << "o43\n"; } + +void AbsOperator::write_nl_string(std::ofstream &f) { f << "o15\n"; } + +void SqrtOperator::write_nl_string(std::ofstream &f) { f << "o39\n"; } + +void Log10Operator::write_nl_string(std::ofstream &f) { f << "o42\n"; } + +void SinOperator::write_nl_string(std::ofstream &f) { f << "o41\n"; } + +void CosOperator::write_nl_string(std::ofstream &f) { f << "o46\n"; } + +void TanOperator::write_nl_string(std::ofstream &f) { f << "o38\n"; } + +void AsinOperator::write_nl_string(std::ofstream &f) { f << "o51\n"; } + +void AcosOperator::write_nl_string(std::ofstream &f) { f << "o53\n"; } + +void AtanOperator::write_nl_string(std::ofstream &f) { f << "o49\n"; } + +bool BinaryOperator::is_binary_operator() { return true; } + +bool UnaryOperator::is_unary_operator() { return true; } + +bool LinearOperator::is_linear_operator() { return true; } + +bool SumOperator::is_sum_operator() { return true; } + +bool MultiplyOperator::is_multiply_operator() { return true; } + +bool DivideOperator::is_divide_operator() { return true; } + +bool PowerOperator::is_power_operator() { return true; } + +bool NegationOperator::is_negation_operator() { return true; } + +bool ExpOperator::is_exp_operator() { return true; } + +bool LogOperator::is_log_operator() { return true; } + +bool AbsOperator::is_abs_operator() { return true; } + +bool SqrtOperator::is_sqrt_operator() { return true; } + +bool ExternalOperator::is_external_operator() { return true; } + +void Leaf::fill_expression(std::shared_ptr *oper_array, + int &oper_ndx) { + ; +} + +void Expression::fill_expression(std::shared_ptr *oper_array, + int &oper_ndx) { + throw std::runtime_error("This should not happen"); +} + +void BinaryOperator::fill_expression(std::shared_ptr *oper_array, + int &oper_ndx) { + oper_ndx -= 1; + oper_array[oper_ndx] = shared_from_this(); + // The order does not actually matter here. It + // will just be easier to debug this way. + operand2->fill_expression(oper_array, oper_ndx); + operand1->fill_expression(oper_array, oper_ndx); +} + +void UnaryOperator::fill_expression(std::shared_ptr *oper_array, + int &oper_ndx) { + oper_ndx -= 1; + oper_array[oper_ndx] = shared_from_this(); + operand->fill_expression(oper_array, oper_ndx); +} + +void LinearOperator::fill_expression(std::shared_ptr *oper_array, + int &oper_ndx) { + oper_ndx -= 1; + oper_array[oper_ndx] = shared_from_this(); +} + +void SumOperator::fill_expression(std::shared_ptr *oper_array, + int &oper_ndx) { + oper_ndx -= 1; + oper_array[oper_ndx] = shared_from_this(); + // The order does not actually matter here. It + // will just be easier to debug this way. + int arg_ndx = nargs - 1; + while (arg_ndx >= 0) { + operands[arg_ndx]->fill_expression(oper_array, oper_ndx); + arg_ndx -= 1; + } +} + +void ExternalOperator::fill_expression(std::shared_ptr *oper_array, + int &oper_ndx) { + oper_ndx -= 1; + oper_array[oper_ndx] = shared_from_this(); + // The order does not actually matter here. It + // will just be easier to debug this way. + int arg_ndx = nargs - 1; + while (arg_ndx >= 0) { + operands[arg_ndx]->fill_expression(oper_array, oper_ndx); + arg_ndx -= 1; + } +} + +double Leaf::get_lb_from_array(double *lbs) { return value; } + +double Leaf::get_ub_from_array(double *ubs) { return value; } + +double Var::get_lb_from_array(double *lbs) { return get_lb(); } + +double Var::get_ub_from_array(double *ubs) { return get_ub(); } + +double Expression::get_lb_from_array(double *lbs) { + return lbs[n_operators - 1]; +} + +double Expression::get_ub_from_array(double *ubs) { + return ubs[n_operators - 1]; +} + +double Operator::get_lb_from_array(double *lbs) { return lbs[index]; } + +double Operator::get_ub_from_array(double *ubs) { return ubs[index]; } + +void Leaf::set_bounds_in_array(double new_lb, double new_ub, double *lbs, + double *ubs, double feasibility_tol, + double integer_tol, double improvement_tol, + std::set> &improved_vars) { + if (new_lb < value - feasibility_tol || new_lb > value + feasibility_tol) { + throw InfeasibleConstraintException( + "Infeasible constraint; bounds computed on parameter or constant " + "disagree with the value of the parameter or constant\n value: " + + std::to_string(value) + "\n computed LB: " + std::to_string(new_lb) + + "\n computed UB: " + std::to_string(new_ub)); + } + + if (new_ub < value - feasibility_tol || new_ub > value + feasibility_tol) { + throw InfeasibleConstraintException( + "Infeasible constraint; bounds computed on parameter or constant " + "disagree with the value of the parameter or constant\n value: " + + std::to_string(value) + "\n computed LB: " + std::to_string(new_lb) + + "\n computed UB: " + std::to_string(new_ub)); + } +} + +void Var::set_bounds_in_array(double new_lb, double new_ub, double *lbs, + double *ubs, double feasibility_tol, + double integer_tol, double improvement_tol, + std::set> &improved_vars) { + if (new_lb > new_ub) { + if (new_lb - feasibility_tol > new_ub) + throw InfeasibleConstraintException( + "Infeasible constraint; The computed lower bound for a variable is " + "larger than the computed upper bound.\n computed LB: " + + std::to_string(new_lb) + + "\n computed UB: " + std::to_string(new_ub)); + else { + new_lb -= feasibility_tol; + new_ub += feasibility_tol; + } + } + if (new_lb >= inf) + throw InfeasibleConstraintException( + "Infeasible constraint; The compute lower bound for " + name + + " is inf"); + if (new_ub <= -inf) + throw InfeasibleConstraintException( + "Infeasible constraint; The computed upper bound for " + name + + " is -inf"); + + if (domain == integers || domain == binary) { + if (new_lb > -inf) { + double lb_floor = floor(new_lb); + double lb_ceil = ceil(new_lb - integer_tol); + if (lb_floor > lb_ceil) + new_lb = lb_floor; + else + new_lb = lb_ceil; + } + if (new_ub < inf) { + double ub_ceil = ceil(new_ub); + double ub_floor = floor(new_ub + integer_tol); + if (ub_ceil < ub_floor) + new_ub = ub_ceil; + else + new_ub = ub_floor; + } + } + + double current_lb = get_lb(); + double current_ub = get_ub(); + + if (new_lb > current_lb + improvement_tol || + new_ub < current_ub - improvement_tol) + improved_vars.insert(shared_from_this()); + + if (new_lb > current_lb) { + if (lb->is_leaf()) + std::dynamic_pointer_cast(lb)->value = new_lb; + else + throw py::value_error( + "variable bounds cannot be expressions when performing FBBT"); + } + + if (new_ub < current_ub) { + if (ub->is_leaf()) + std::dynamic_pointer_cast(ub)->value = new_ub; + else + throw py::value_error( + "variable bounds cannot be expressions when performing FBBT"); + } +} + +void Expression::set_bounds_in_array( + double new_lb, double new_ub, double *lbs, double *ubs, + double feasibility_tol, double integer_tol, double improvement_tol, + std::set> &improved_vars) { + lbs[n_operators - 1] = new_lb; + ubs[n_operators - 1] = new_ub; +} + +void Operator::set_bounds_in_array( + double new_lb, double new_ub, double *lbs, double *ubs, + double feasibility_tol, double integer_tol, double improvement_tol, + std::set> &improved_vars) { + lbs[index] = new_lb; + ubs[index] = new_ub; +} + +void Expression::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + for (unsigned int ndx = 0; ndx < n_operators; ++ndx) { + operators[ndx]->index = ndx; + operators[ndx]->propagate_bounds_forward(lbs, ubs, feasibility_tol, + integer_tol); + } +} + +void Expression::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + int ndx = n_operators - 1; + while (ndx >= 0) { + operators[ndx]->propagate_bounds_backward( + lbs, ubs, feasibility_tol, integer_tol, improvement_tol, improved_vars); + ndx -= 1; + } +} + +void Operator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + lbs[index] = -inf; + ubs[index] = inf; +} + +void Operator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + ; +} + +void MultiplyOperator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + if (operand1 == operand2) { + interval_power(operand1->get_lb_from_array(lbs), + operand1->get_ub_from_array(ubs), 2, 2, &lbs[index], + &ubs[index], feasibility_tol); + } else { + interval_mul(operand1->get_lb_from_array(lbs), + operand1->get_ub_from_array(ubs), + operand2->get_lb_from_array(lbs), + operand2->get_ub_from_array(ubs), &lbs[index], &ubs[index]); + } +} + +void MultiplyOperator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double xl = operand1->get_lb_from_array(lbs); + double xu = operand1->get_ub_from_array(ubs); + double yl = operand2->get_lb_from_array(lbs); + double yu = operand2->get_ub_from_array(ubs); + double lb = get_lb_from_array(lbs); + double ub = get_ub_from_array(ubs); + + double new_xl, new_xu, new_yl, new_yu; + + if (operand1 == operand2) { + _inverse_power1(lb, ub, 2, 2, xl, xu, &new_xl, &new_xu, feasibility_tol); + new_yl = new_xl; + new_yu = new_xu; + } else { + interval_div(lb, ub, yl, yu, &new_xl, &new_xu, feasibility_tol); + interval_div(lb, ub, xl, xu, &new_yl, &new_yu, feasibility_tol); + } + + if (new_xl > xl) + xl = new_xl; + if (new_xu < xu) + xu = new_xu; + operand1->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); + + if (new_yl > yl) + yl = new_yl; + if (new_yu < yu) + yu = new_yu; + operand2->set_bounds_in_array(yl, yu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); +} + +void SumOperator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + double lb = operands[0]->get_lb_from_array(lbs); + double ub = operands[0]->get_ub_from_array(ubs); + double tmp_lb; + double tmp_ub; + + for (unsigned int ndx = 1; ndx < nargs; ++ndx) { + interval_add(lb, ub, operands[ndx]->get_lb_from_array(lbs), + operands[ndx]->get_ub_from_array(ubs), &tmp_lb, &tmp_ub); + lb = tmp_lb; + ub = tmp_ub; + } + + lbs[index] = lb; + ubs[index] = ub; +} + +void SumOperator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double *accumulated_lbs = new double[nargs]; + double *accumulated_ubs = new double[nargs]; + + accumulated_lbs[0] = operands[0]->get_lb_from_array(lbs); + accumulated_ubs[0] = operands[0]->get_ub_from_array(ubs); + for (unsigned int ndx = 1; ndx < nargs; ++ndx) { + interval_add(accumulated_lbs[ndx - 1], accumulated_ubs[ndx - 1], + operands[ndx]->get_lb_from_array(lbs), + operands[ndx]->get_ub_from_array(ubs), &accumulated_lbs[ndx], + &accumulated_ubs[ndx]); + } + + double new_sum_lb = get_lb_from_array(lbs); + double new_sum_ub = get_ub_from_array(ubs); + + if (new_sum_lb > accumulated_lbs[nargs - 1]) + accumulated_lbs[nargs - 1] = new_sum_lb; + if (new_sum_ub < accumulated_ubs[nargs - 1]) + accumulated_ubs[nargs - 1] = new_sum_ub; + + double lb0, ub0, lb1, ub1, lb2, ub2, _lb1, _ub1, _lb2, _ub2; + + int ndx = nargs - 1; + while (ndx >= 1) { + lb0 = accumulated_lbs[ndx]; + ub0 = accumulated_ubs[ndx]; + lb1 = accumulated_lbs[ndx - 1]; + ub1 = accumulated_ubs[ndx - 1]; + lb2 = operands[ndx]->get_lb_from_array(lbs); + ub2 = operands[ndx]->get_ub_from_array(ubs); + interval_sub(lb0, ub0, lb2, ub2, &_lb1, &_ub1); + interval_sub(lb0, ub0, lb1, ub1, &_lb2, &_ub2); + if (_lb1 > lb1) + lb1 = _lb1; + if (_ub1 < ub1) + ub1 = _ub1; + if (_lb2 > lb2) + lb2 = _lb2; + if (_ub2 < ub2) + ub2 = _ub2; + accumulated_lbs[ndx - 1] = lb1; + accumulated_ubs[ndx - 1] = ub1; + operands[ndx]->set_bounds_in_array(lb2, ub2, lbs, ubs, feasibility_tol, + integer_tol, improvement_tol, + improved_vars); + ndx -= 1; + } + + // take care of ndx = 0 + lb1 = operands[0]->get_lb_from_array(lbs); + ub1 = operands[0]->get_ub_from_array(ubs); + _lb1 = accumulated_lbs[0]; + _ub1 = accumulated_ubs[0]; + if (_lb1 > lb1) + lb1 = _lb1; + if (_ub1 < ub1) + ub1 = _ub1; + operands[0]->set_bounds_in_array(lb1, ub1, lbs, ubs, feasibility_tol, + integer_tol, improvement_tol, improved_vars); + + delete[] accumulated_lbs; + delete[] accumulated_ubs; +} + +void LinearOperator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + double lb = constant->evaluate(); + double ub = lb; + double tmp_lb; + double tmp_ub; + double coef; + + for (unsigned int ndx = 0; ndx < nterms; ++ndx) { + coef = coefficients[ndx]->evaluate(); + interval_mul(coef, coef, variables[ndx]->get_lb(), variables[ndx]->get_ub(), + &tmp_lb, &tmp_ub); + interval_add(lb, ub, tmp_lb, tmp_ub, &lb, &ub); + } + + lbs[index] = lb; + ubs[index] = ub; +} + +void LinearOperator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double *accumulated_lbs = new double[nterms + 1]; + double *accumulated_ubs = new double[nterms + 1]; + + double coef; + + accumulated_lbs[0] = constant->evaluate(); + accumulated_ubs[0] = constant->evaluate(); + for (unsigned int ndx = 0; ndx < nterms; ++ndx) { + coef = coefficients[ndx]->evaluate(); + interval_mul(coef, coef, variables[ndx]->get_lb(), variables[ndx]->get_ub(), + &accumulated_lbs[ndx + 1], &accumulated_ubs[ndx + 1]); + interval_add(accumulated_lbs[ndx], accumulated_ubs[ndx], + accumulated_lbs[ndx + 1], accumulated_ubs[ndx + 1], + &accumulated_lbs[ndx + 1], &accumulated_ubs[ndx + 1]); + } + + double new_sum_lb = get_lb_from_array(lbs); + double new_sum_ub = get_ub_from_array(ubs); + + if (new_sum_lb > accumulated_lbs[nterms]) + accumulated_lbs[nterms] = new_sum_lb; + if (new_sum_ub < accumulated_ubs[nterms]) + accumulated_ubs[nterms] = new_sum_ub; + + double lb0, ub0, lb1, ub1, lb2, ub2, _lb1, _ub1, _lb2, _ub2, new_v_lb, + new_v_ub; + + int ndx = nterms - 1; + while (ndx >= 0) { + lb0 = accumulated_lbs[ndx + 1]; + ub0 = accumulated_ubs[ndx + 1]; + lb1 = accumulated_lbs[ndx]; + ub1 = accumulated_ubs[ndx]; + coef = coefficients[ndx]->evaluate(); + interval_mul(coef, coef, variables[ndx]->get_lb(), variables[ndx]->get_ub(), + &lb2, &ub2); + interval_sub(lb0, ub0, lb2, ub2, &_lb1, &_ub1); + interval_sub(lb0, ub0, lb1, ub1, &_lb2, &_ub2); + if (_lb1 > lb1) + lb1 = _lb1; + if (_ub1 < ub1) + ub1 = _ub1; + if (_lb2 > lb2) + lb2 = _lb2; + if (_ub2 < ub2) + ub2 = _ub2; + accumulated_lbs[ndx] = lb1; + accumulated_ubs[ndx] = ub1; + interval_div(lb2, ub2, coef, coef, &new_v_lb, &new_v_ub, feasibility_tol); + variables[ndx]->set_bounds_in_array(new_v_lb, new_v_ub, lbs, ubs, + feasibility_tol, integer_tol, + improvement_tol, improved_vars); + ndx -= 1; + } + + delete[] accumulated_lbs; + delete[] accumulated_ubs; +} + +void DivideOperator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + interval_div( + operand1->get_lb_from_array(lbs), operand1->get_ub_from_array(ubs), + operand2->get_lb_from_array(lbs), operand2->get_ub_from_array(ubs), + &lbs[index], &ubs[index], feasibility_tol); +} + +void DivideOperator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double xl = operand1->get_lb_from_array(lbs); + double xu = operand1->get_ub_from_array(ubs); + double yl = operand2->get_lb_from_array(lbs); + double yu = operand2->get_ub_from_array(ubs); + double lb = get_lb_from_array(lbs); + double ub = get_ub_from_array(ubs); + + double new_xl; + double new_xu; + double new_yl; + double new_yu; + + interval_mul(lb, ub, yl, yu, &new_xl, &new_xu); + interval_div(xl, xu, lb, ub, &new_yl, &new_yu, feasibility_tol); + + if (new_xl > xl) + xl = new_xl; + if (new_xu < xu) + xu = new_xu; + operand1->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); + + if (new_yl > yl) + yl = new_yl; + if (new_yu < yu) + yu = new_yu; + operand2->set_bounds_in_array(yl, yu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); +} + +void NegationOperator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + interval_sub(0, 0, operand->get_lb_from_array(lbs), + operand->get_ub_from_array(ubs), &lbs[index], &ubs[index]); +} + +void NegationOperator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double xl = operand->get_lb_from_array(lbs); + double xu = operand->get_ub_from_array(ubs); + double lb = get_lb_from_array(lbs); + double ub = get_ub_from_array(ubs); + + double new_xl; + double new_xu; + + interval_sub(0, 0, lb, ub, &new_xl, &new_xu); + + if (new_xl > xl) + xl = new_xl; + if (new_xu < xu) + xu = new_xu; + operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); +} + +void PowerOperator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + interval_power( + operand1->get_lb_from_array(lbs), operand1->get_ub_from_array(ubs), + operand2->get_lb_from_array(lbs), operand2->get_ub_from_array(ubs), + &lbs[index], &ubs[index], feasibility_tol); +} + +void PowerOperator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double xl = operand1->get_lb_from_array(lbs); + double xu = operand1->get_ub_from_array(ubs); + double yl = operand2->get_lb_from_array(lbs); + double yu = operand2->get_ub_from_array(ubs); + double lb = get_lb_from_array(lbs); + double ub = get_ub_from_array(ubs); + + double new_xl, new_xu, new_yl, new_yu; + _inverse_power1(lb, ub, yl, yu, xl, xu, &new_xl, &new_xu, feasibility_tol); + if (yl != yu) + _inverse_power2(lb, ub, xl, xu, &new_yl, &new_yu, feasibility_tol); + else { + new_yl = yl; + new_yu = yu; + } + + if (new_xl > xl) + xl = new_xl; + if (new_xu < xu) + xu = new_xu; + operand1->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); + + if (new_yl > yl) + yl = new_yl; + if (new_yu < yu) + yu = new_yu; + operand2->set_bounds_in_array(yl, yu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); +} + +void SqrtOperator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + interval_power(operand->get_lb_from_array(lbs), + operand->get_ub_from_array(ubs), 0.5, 0.5, &lbs[index], + &ubs[index], feasibility_tol); +} + +void SqrtOperator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double xl = operand->get_lb_from_array(lbs); + double xu = operand->get_ub_from_array(ubs); + double yl = 0.5; + double yu = 0.5; + double lb = get_lb_from_array(lbs); + double ub = get_ub_from_array(ubs); + + double new_xl, new_xu; + _inverse_power1(lb, ub, yl, yu, xl, xu, &new_xl, &new_xu, feasibility_tol); + + if (new_xl > xl) + xl = new_xl; + if (new_xu < xu) + xu = new_xu; + operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); +} + +void ExpOperator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + interval_exp(operand->get_lb_from_array(lbs), operand->get_ub_from_array(ubs), + &lbs[index], &ubs[index]); +} + +void ExpOperator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double xl = operand->get_lb_from_array(lbs); + double xu = operand->get_ub_from_array(ubs); + double lb = get_lb_from_array(lbs); + double ub = get_ub_from_array(ubs); + + double new_xl, new_xu; + interval_log(lb, ub, &new_xl, &new_xu); + + if (new_xl > xl) + xl = new_xl; + if (new_xu < xu) + xu = new_xu; + operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); +} + +void LogOperator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + interval_log(operand->get_lb_from_array(lbs), operand->get_ub_from_array(ubs), + &lbs[index], &ubs[index]); +} + +void LogOperator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double xl = operand->get_lb_from_array(lbs); + double xu = operand->get_ub_from_array(ubs); + double lb = get_lb_from_array(lbs); + double ub = get_ub_from_array(ubs); + + double new_xl, new_xu; + interval_exp(lb, ub, &new_xl, &new_xu); + + if (new_xl > xl) + xl = new_xl; + if (new_xu < xu) + xu = new_xu; + operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); +} + +void AbsOperator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + interval_abs(operand->get_lb_from_array(lbs), operand->get_ub_from_array(ubs), + &lbs[index], &ubs[index]); +} + +void AbsOperator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double xl = operand->get_lb_from_array(lbs); + double xu = operand->get_ub_from_array(ubs); + double lb = get_lb_from_array(lbs); + double ub = get_ub_from_array(ubs); + + double new_xl, new_xu; + _inverse_abs(lb, ub, &new_xl, &new_xu); + + if (new_xl > xl) + xl = new_xl; + if (new_xu < xu) + xu = new_xu; + operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); +} + +void Log10Operator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + interval_log10(operand->get_lb_from_array(lbs), + operand->get_ub_from_array(ubs), &lbs[index], &ubs[index]); +} + +void Log10Operator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double xl = operand->get_lb_from_array(lbs); + double xu = operand->get_ub_from_array(ubs); + double lb = get_lb_from_array(lbs); + double ub = get_ub_from_array(ubs); + + double new_xl, new_xu; + interval_power(10, 10, lb, ub, &new_xl, &new_xu, feasibility_tol); + + if (new_xl > xl) + xl = new_xl; + if (new_xu < xu) + xu = new_xu; + operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); +} + +void SinOperator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + interval_sin(operand->get_lb_from_array(lbs), operand->get_ub_from_array(ubs), + &lbs[index], &ubs[index]); +} + +void SinOperator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double xl = operand->get_lb_from_array(lbs); + double xu = operand->get_ub_from_array(ubs); + double lb = get_lb_from_array(lbs); + double ub = get_ub_from_array(ubs); + + double new_xl, new_xu; + interval_asin(lb, ub, xl, xu, &new_xl, &new_xu, feasibility_tol); + + if (new_xl > xl) + xl = new_xl; + if (new_xu < xu) + xu = new_xu; + operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); +} + +void CosOperator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + interval_cos(operand->get_lb_from_array(lbs), operand->get_ub_from_array(ubs), + &lbs[index], &ubs[index]); +} + +void CosOperator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double xl = operand->get_lb_from_array(lbs); + double xu = operand->get_ub_from_array(ubs); + double lb = get_lb_from_array(lbs); + double ub = get_ub_from_array(ubs); + + double new_xl, new_xu; + interval_acos(lb, ub, xl, xu, &new_xl, &new_xu, feasibility_tol); + + if (new_xl > xl) + xl = new_xl; + if (new_xu < xu) + xu = new_xu; + operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); +} + +void TanOperator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + interval_tan(operand->get_lb_from_array(lbs), operand->get_ub_from_array(ubs), + &lbs[index], &ubs[index]); +} + +void TanOperator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double xl = operand->get_lb_from_array(lbs); + double xu = operand->get_ub_from_array(ubs); + double lb = get_lb_from_array(lbs); + double ub = get_ub_from_array(ubs); + + double new_xl, new_xu; + interval_atan(lb, ub, xl, xu, &new_xl, &new_xu); + + if (new_xl > xl) + xl = new_xl; + if (new_xu < xu) + xu = new_xu; + operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); +} + +void AsinOperator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + interval_asin(operand->get_lb_from_array(lbs), + operand->get_ub_from_array(ubs), -inf, inf, &lbs[index], + &ubs[index], feasibility_tol); +} + +void AsinOperator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double xl = operand->get_lb_from_array(lbs); + double xu = operand->get_ub_from_array(ubs); + double lb = get_lb_from_array(lbs); + double ub = get_ub_from_array(ubs); + + double new_xl, new_xu; + interval_sin(lb, ub, &new_xl, &new_xu); + + if (new_xl > xl) + xl = new_xl; + if (new_xu < xu) + xu = new_xu; + operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); +} + +void AcosOperator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + interval_acos(operand->get_lb_from_array(lbs), + operand->get_ub_from_array(ubs), -inf, inf, &lbs[index], + &ubs[index], feasibility_tol); +} + +void AcosOperator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double xl = operand->get_lb_from_array(lbs); + double xu = operand->get_ub_from_array(ubs); + double lb = get_lb_from_array(lbs); + double ub = get_ub_from_array(ubs); + + double new_xl, new_xu; + interval_cos(lb, ub, &new_xl, &new_xu); + + if (new_xl > xl) + xl = new_xl; + if (new_xu < xu) + xu = new_xu; + operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); +} + +void AtanOperator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + interval_atan(operand->get_lb_from_array(lbs), + operand->get_ub_from_array(ubs), -inf, inf, &lbs[index], + &ubs[index]); +} + +void AtanOperator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double xl = operand->get_lb_from_array(lbs); + double xu = operand->get_ub_from_array(ubs); + double lb = get_lb_from_array(lbs); + double ub = get_ub_from_array(ubs); + + double new_xl, new_xu; + interval_tan(lb, ub, &new_xl, &new_xu); + + if (new_xl > xl) + xl = new_xl; + if (new_xu < xu) + xu = new_xu; + operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); +} + +std::vector> create_vars(int n_vars) { + std::vector> res; + for (int i = 0; i < n_vars; ++i) { + res.push_back(std::make_shared()); + } + return res; +} + +std::vector> create_params(int n_params) { + std::vector> res; + for (int i = 0; i < n_params; ++i) { + res.push_back(std::make_shared()); + } + return res; +} + +std::vector> create_constants(int n_constants) { + std::vector> res; + for (int i = 0; i < n_constants; ++i) { + res.push_back(std::make_shared()); + } + return res; +} + +std::shared_ptr +appsi_operator_from_pyomo_expr(py::handle expr, py::handle var_map, + py::handle param_map, + PyomoExprTypes &expr_types) { + std::shared_ptr res; + ExprType tmp_type = + expr_types.expr_type_map[py::type::of(expr)].cast(); + + switch (tmp_type) { + case py_float: { + res = std::make_shared(expr.cast()); + break; + } + case var: { + res = var_map[expr_types.id(expr)].cast>(); + break; + } + case param: { + if (expr.attr("parent_component")().attr("mutable").cast()) + res = param_map[expr_types.id(expr)].cast>(); + else + res = std::make_shared(expr.attr("value").cast()); + break; + } + case product: { + res = std::make_shared(); + break; + } + case sum: { + res = std::make_shared(expr.attr("nargs")().cast()); + break; + } + case negation: { + res = std::make_shared(); + break; + } + case external_func: { + res = std::make_shared(expr.attr("nargs")().cast()); + std::shared_ptr oper = + std::dynamic_pointer_cast(res); + oper->function_name = + expr.attr("_fcn").attr("_function").cast(); + break; + } + case power: { + res = std::make_shared(); + break; + } + case division: { + res = std::make_shared(); + break; + } + case unary_func: { + std::string function_name = expr.attr("getname")().cast(); + if (function_name == "exp") + res = std::make_shared(); + else if (function_name == "log") + res = std::make_shared(); + else if (function_name == "log10") + res = std::make_shared(); + else if (function_name == "sin") + res = std::make_shared(); + else if (function_name == "cos") + res = std::make_shared(); + else if (function_name == "tan") + res = std::make_shared(); + else if (function_name == "asin") + res = std::make_shared(); + else if (function_name == "acos") + res = std::make_shared(); + else if (function_name == "atan") + res = std::make_shared(); + else if (function_name == "sqrt") + res = std::make_shared(); + else + throw py::value_error("Unrecognized expression type: " + function_name); + break; + } + case linear: { + res = std::make_shared( + expr_types.len(expr.attr("linear_vars")).cast()); + break; + } + case named_expr: { + res = appsi_operator_from_pyomo_expr(expr.attr("expr"), var_map, param_map, + expr_types); + break; + } + case numeric_constant: { + res = std::make_shared(expr.attr("value").cast()); + break; + } + case pyomo_unit: { + res = std::make_shared(1.0); + break; + } + case unary_abs: { + res = std::make_shared(); + break; + } + default: { + throw py::value_error("Unrecognized expression type: " + + expr_types.builtins.attr("str")(py::type::of(expr)) + .cast()); + break; + } + } + return res; +} + +void prep_for_repn_helper(py::handle expr, py::handle named_exprs, + py::handle variables, py::handle fixed_vars, + py::handle external_funcs, + PyomoExprTypes &expr_types) { + ExprType tmp_type = + expr_types.expr_type_map[py::type::of(expr)].cast(); + + switch (tmp_type) { + case py_float: { + break; + } + case var: { + variables[expr_types.id(expr)] = expr; + if (expr.attr("fixed").cast()) { + fixed_vars[expr_types.id(expr)] = expr; + } + break; + } + case param: { + break; + } + case product: { + py::tuple args = expr.attr("_args_"); + for (py::handle arg : args) { + prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, + external_funcs, expr_types); + } + break; + } + case sum: { + py::tuple args = expr.attr("args"); + for (py::handle arg : args) { + prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, + external_funcs, expr_types); + } + break; + } + case negation: { + py::tuple args = expr.attr("_args_"); + for (py::handle arg : args) { + prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, + external_funcs, expr_types); + } + break; + } + case external_func: { + external_funcs[expr_types.id(expr)] = expr; + py::tuple args = expr.attr("args"); + for (py::handle arg : args) { + prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, + external_funcs, expr_types); + } + break; + } + case power: { + py::tuple args = expr.attr("_args_"); + for (py::handle arg : args) { + prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, + external_funcs, expr_types); + } + break; + } + case division: { + py::tuple args = expr.attr("_args_"); + for (py::handle arg : args) { + prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, + external_funcs, expr_types); + } + break; + } + case unary_func: { + py::tuple args = expr.attr("_args_"); + for (py::handle arg : args) { + prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, + external_funcs, expr_types); + } + break; + } + case linear: { + py::list linear_vars = expr.attr("linear_vars"); + py::list linear_coefs = expr.attr("linear_coefs"); + for (py::handle arg : linear_vars) { + prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, + external_funcs, expr_types); + } + for (py::handle arg : linear_coefs) { + prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, + external_funcs, expr_types); + } + prep_for_repn_helper(expr.attr("constant"), named_exprs, variables, + fixed_vars, external_funcs, expr_types); + break; + } + case named_expr: { + named_exprs[expr_types.id(expr)] = expr; + prep_for_repn_helper(expr.attr("expr"), named_exprs, variables, fixed_vars, + external_funcs, expr_types); + break; + } + case numeric_constant: { + break; + } + case pyomo_unit: { + break; + } + case unary_abs: { + py::tuple args = expr.attr("_args_"); + for (py::handle arg : args) { + prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, + external_funcs, expr_types); + } + break; + } + default: { + if (expr_types.builtins.attr("hasattr")(expr, "is_constant").cast()) { + if (expr.attr("is_constant")().cast()) + break; + } + throw py::value_error("Unrecognized expression type: " + + expr_types.builtins.attr("str")(py::type::of(expr)) + .cast()); + break; + } + } +} + +py::tuple prep_for_repn(py::handle expr, PyomoExprTypes &expr_types) { + py::dict named_exprs; + py::dict variables; + py::dict fixed_vars; + py::dict external_funcs; + + prep_for_repn_helper(expr, named_exprs, variables, fixed_vars, external_funcs, + expr_types); + + py::list named_expr_list = named_exprs.attr("values")(); + py::list variable_list = variables.attr("values")(); + py::list fixed_var_list = fixed_vars.attr("values")(); + py::list external_func_list = external_funcs.attr("values")(); + + py::tuple res = py::make_tuple(named_expr_list, variable_list, fixed_var_list, + external_func_list); + return res; +} + +int build_expression_tree(py::handle pyomo_expr, + std::shared_ptr appsi_expr, py::handle var_map, + py::handle param_map, PyomoExprTypes &expr_types) { + int num_nodes = 0; + + if (expr_types.expr_type_map[py::type::of(pyomo_expr)].cast() == + named_expr) + return build_expression_tree(pyomo_expr.attr("expr"), appsi_expr, var_map, + param_map, expr_types); + + if (appsi_expr->is_leaf()) { + ; + } else if (appsi_expr->is_binary_operator()) { + num_nodes += 1; + std::shared_ptr oper = + std::dynamic_pointer_cast(appsi_expr); + py::list pyomo_args = pyomo_expr.attr("args"); + oper->operand1 = appsi_operator_from_pyomo_expr(pyomo_args[0], var_map, + param_map, expr_types); + oper->operand2 = appsi_operator_from_pyomo_expr(pyomo_args[1], var_map, + param_map, expr_types); + num_nodes += build_expression_tree(pyomo_args[0], oper->operand1, var_map, + param_map, expr_types); + num_nodes += build_expression_tree(pyomo_args[1], oper->operand2, var_map, + param_map, expr_types); + } else if (appsi_expr->is_unary_operator()) { + num_nodes += 1; + std::shared_ptr oper = + std::dynamic_pointer_cast(appsi_expr); + py::list pyomo_args = pyomo_expr.attr("args"); + oper->operand = appsi_operator_from_pyomo_expr(pyomo_args[0], var_map, + param_map, expr_types); + num_nodes += build_expression_tree(pyomo_args[0], oper->operand, var_map, + param_map, expr_types); + } else if (appsi_expr->is_sum_operator()) { + num_nodes += 1; + std::shared_ptr oper = + std::dynamic_pointer_cast(appsi_expr); + py::list pyomo_args = pyomo_expr.attr("args"); + for (unsigned int arg_ndx = 0; arg_ndx < oper->nargs; ++arg_ndx) { + oper->operands[arg_ndx] = appsi_operator_from_pyomo_expr( + pyomo_args[arg_ndx], var_map, param_map, expr_types); + num_nodes += + build_expression_tree(pyomo_args[arg_ndx], oper->operands[arg_ndx], + var_map, param_map, expr_types); + } + } else if (appsi_expr->is_linear_operator()) { + num_nodes += 1; + std::shared_ptr oper = + std::dynamic_pointer_cast(appsi_expr); + oper->constant = appsi_expr_from_pyomo_expr(pyomo_expr.attr("constant"), + var_map, param_map, expr_types); + py::list pyomo_vars = pyomo_expr.attr("linear_vars"); + py::list pyomo_coefs = pyomo_expr.attr("linear_coefs"); + for (unsigned int arg_ndx = 0; arg_ndx < oper->nterms; ++arg_ndx) { + oper->variables[arg_ndx] = var_map[expr_types.id(pyomo_vars[arg_ndx])] + .cast>(); + oper->coefficients[arg_ndx] = appsi_expr_from_pyomo_expr( + pyomo_coefs[arg_ndx], var_map, param_map, expr_types); + } + } else if (appsi_expr->is_external_operator()) { + num_nodes += 1; + std::shared_ptr oper = + std::dynamic_pointer_cast(appsi_expr); + py::list pyomo_args = pyomo_expr.attr("args"); + for (unsigned int arg_ndx = 0; arg_ndx < oper->nargs; ++arg_ndx) { + oper->operands[arg_ndx] = appsi_operator_from_pyomo_expr( + pyomo_args[arg_ndx], var_map, param_map, expr_types); + num_nodes += + build_expression_tree(pyomo_args[arg_ndx], oper->operands[arg_ndx], + var_map, param_map, expr_types); + } + } else { + throw py::value_error( + "Unrecognized expression type: " + + expr_types.builtins.attr("str")(py::type::of(pyomo_expr)) + .cast()); + } + return num_nodes; +} + +std::shared_ptr +appsi_expr_from_pyomo_expr(py::handle expr, py::handle var_map, + py::handle param_map, PyomoExprTypes &expr_types) { + std::shared_ptr node = + appsi_operator_from_pyomo_expr(expr, var_map, param_map, expr_types); + int num_nodes = + build_expression_tree(expr, node, var_map, param_map, expr_types); + if (num_nodes == 0) { + return std::dynamic_pointer_cast(node); + } else { + std::shared_ptr res = std::make_shared(num_nodes); + node->fill_expression(res->operators, num_nodes); + return res; + } +} + +std::vector> +appsi_exprs_from_pyomo_exprs(py::list expr_list, py::dict var_map, + py::dict param_map) { + PyomoExprTypes expr_types = PyomoExprTypes(); + int num_exprs = expr_types.builtins.attr("len")(expr_list).cast(); + std::vector> res(num_exprs); + + int ndx = 0; + for (py::handle expr : expr_list) { + res[ndx] = appsi_expr_from_pyomo_expr(expr, var_map, param_map, expr_types); + ndx += 1; + } + return res; +} + +void process_pyomo_vars(PyomoExprTypes &expr_types, py::list pyomo_vars, + py::dict var_map, py::dict param_map, + py::dict var_attrs, py::dict rev_var_map, + py::bool_ _set_name, py::handle symbol_map, + py::handle labeler, py::bool_ _update) { + py::tuple v_attrs; + std::shared_ptr cv; + py::handle v_lb; + py::handle v_ub; + py::handle v_val; + py::tuple domain_interval; + py::handle interval_lb; + py::handle interval_ub; + py::handle interval_step; + bool v_fixed; + bool set_name = _set_name.cast(); + bool update = _update.cast(); + double domain_step; + + for (py::handle v : pyomo_vars) { + v_attrs = var_attrs[expr_types.id(v)]; + v_lb = v_attrs[1]; + v_ub = v_attrs[2]; + v_fixed = v_attrs[3].cast(); + domain_interval = v_attrs[4]; + v_val = v_attrs[5]; + + interval_lb = domain_interval[0]; + interval_ub = domain_interval[1]; + interval_step = domain_interval[2]; + domain_step = interval_step.cast(); + + if (update) { + cv = var_map[expr_types.id(v)].cast>(); + } else { + cv = std::make_shared(); + } + + if (!(v_lb.is(py::none()))) { + cv->lb = appsi_expr_from_pyomo_expr(v_lb, var_map, param_map, expr_types); + } else { + cv->lb = std::make_shared(-inf); + } + if (!(v_ub.is(py::none()))) { + cv->ub = appsi_expr_from_pyomo_expr(v_ub, var_map, param_map, expr_types); + } else { + cv->ub = std::make_shared(inf); + } + + if (!(v_val.is(py::none()))) { + cv->value = v_val.cast(); + } + + if (v_fixed) { + cv->fixed = true; + } else { + cv->fixed = false; + } + + if (set_name && !update) { + cv->name = symbol_map.attr("getSymbol")(v, labeler).cast(); + } + + if (interval_lb.is(py::none())) + cv->domain_lb = -inf; + else + cv->domain_lb = interval_lb.cast(); + if (interval_ub.is(py::none())) + cv->domain_ub = inf; + else + cv->domain_ub = interval_ub.cast(); + if (domain_step == 0) + cv->domain = continuous; + else if (domain_step == 1) { + if ((cv->domain_lb == 0) && (cv->domain_ub == 1)) + cv->domain = binary; + else + cv->domain = integers; + } else + throw py::value_error("Unrecognized domain step"); + + if (!update) { + var_map[expr_types.id(v)] = py::cast(cv); + rev_var_map[py::cast(cv)] = v; + } + } +} diff --git a/pyomo/contrib/appsi/cmodel/src/expression.hpp b/pyomo/contrib/appsi/cmodel/src/expression.hpp index 9a991102a90..e91ca0af3b3 100644 --- a/pyomo/contrib/appsi/cmodel/src/expression.hpp +++ b/pyomo/contrib/appsi/cmodel/src/expression.hpp @@ -1,788 +1,800 @@ -#ifndef EXPRESSION_HEADER -#define EXPRESSION_HEADER - -#include "interval.hpp" -#include - -class Node; -class ExpressionBase; -class Leaf; -class Var; -class Constant; -class Param; -class Expression; -class Operator; -class BinaryOperator; -class UnaryOperator; -class LinearOperator; -class SumOperator; -class MultiplyOperator; -class DivideOperator; -class PowerOperator; -class NegationOperator; -class ExpOperator; -class LogOperator; -class AbsOperator; -class ExternalOperator; -class PyomoExprTypes; - -extern double inf; - -class Node : public std::enable_shared_from_this { -public: - Node() = default; - virtual ~Node() = default; - virtual bool is_variable_type() { return false; } - virtual bool is_param_type() { return false; } - virtual bool is_expression_type() { return false; } - virtual bool is_operator_type() { return false; } - virtual bool is_constant_type() { return false; } - virtual bool is_leaf() { return false; } - virtual bool is_binary_operator() { return false; } - virtual bool is_unary_operator() { return false; } - virtual bool is_linear_operator() { return false; } - virtual bool is_sum_operator() { return false; } - virtual bool is_multiply_operator() { return false; } - virtual bool is_divide_operator() { return false; } - virtual bool is_power_operator() { return false; } - virtual bool is_negation_operator() { return false; } - virtual bool is_exp_operator() { return false; } - virtual bool is_log_operator() { return false; } - virtual bool is_abs_operator() { return false; } - virtual bool is_sqrt_operator() { return false; } - virtual bool is_external_operator() { return false; } - virtual double get_value_from_array(double *) = 0; - virtual int get_degree_from_array(int *) = 0; - virtual std::string get_string_from_array(std::string *) = 0; - virtual void fill_prefix_notation_stack( - std::shared_ptr>> stack) = 0; - virtual void write_nl_string(std::ofstream &) = 0; - virtual void fill_expression(std::shared_ptr *oper_array, - int &oper_ndx) = 0; - virtual double get_lb_from_array(double *lbs) = 0; - virtual double get_ub_from_array(double *ubs) = 0; - virtual void - set_bounds_in_array(double new_lb, double new_ub, double *lbs, double *ubs, - double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) = 0; -}; - -class ExpressionBase : public Node { -public: - ExpressionBase() = default; - virtual double evaluate() = 0; - virtual std::string __str__() = 0; - virtual std::shared_ptr>> - identify_variables() = 0; - virtual std::shared_ptr>> - identify_external_operators() = 0; - virtual std::shared_ptr>> - get_prefix_notation() = 0; - std::shared_ptr shared_from_this() { - return std::static_pointer_cast(Node::shared_from_this()); - } - void fill_prefix_notation_stack( - std::shared_ptr>> stack) override { - ; - } -}; - -class Leaf : public ExpressionBase { -public: - Leaf() = default; - Leaf(double value) : value(value) {} - virtual ~Leaf() = default; - double value = 0.0; - bool is_leaf() override; - double evaluate() override; - double get_value_from_array(double *) override; - std::string get_string_from_array(std::string *) override; - std::shared_ptr>> - get_prefix_notation() override; - void fill_expression(std::shared_ptr *oper_array, - int &oper_ndx) override; - double get_lb_from_array(double *lbs) override; - double get_ub_from_array(double *ubs) override; - void - set_bounds_in_array(double new_lb, double new_ub, double *lbs, double *ubs, - double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class Constant : public Leaf { -public: - Constant() = default; - Constant(double value) : Leaf(value) {} - bool is_constant_type() override; - std::string __str__() override; - int get_degree_from_array(int *) override; - std::shared_ptr>> - identify_variables() override; - std::shared_ptr>> - identify_external_operators() override; - void write_nl_string(std::ofstream &) override; -}; - -enum Domain { continuous, binary, integers }; - -class Var : public Leaf { -public: - Var() = default; - Var(double val) : Leaf(val) {} - Var(std::string _name) : name(_name) {} - Var(std::string _name, double val) : Leaf(val), name(_name) {} - std::string name = "v"; - std::string __str__() override; - std::shared_ptr lb; - std::shared_ptr ub; - int index = -1; - bool fixed = false; - double domain_lb = -inf; - double domain_ub = inf; - Domain domain = continuous; - bool is_variable_type() override; - int get_degree_from_array(int *) override; - std::shared_ptr>> - identify_variables() override; - std::shared_ptr>> - identify_external_operators() override; - void write_nl_string(std::ofstream &) override; - std::shared_ptr shared_from_this() { - return std::static_pointer_cast(Node::shared_from_this()); - } - double get_lb(); - double get_ub(); - Domain get_domain(); - double get_lb_from_array(double *lbs) override; - double get_ub_from_array(double *ubs) override; - void - set_bounds_in_array(double new_lb, double new_ub, double *lbs, double *ubs, - double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class Param : public Leaf { -public: - Param() = default; - Param(double val) : Leaf(val) {} - Param(std::string _name) : name(_name) {} - Param(std::string _name, double val) : Leaf(val), name(_name) {} - std::string name = "p"; - std::string __str__() override; - bool is_param_type() override; - int get_degree_from_array(int *) override; - std::shared_ptr>> - identify_variables() override; - std::shared_ptr>> - identify_external_operators() override; - void write_nl_string(std::ofstream &) override; -}; - -class Expression : public ExpressionBase { -public: - Expression(int _n_operators) : ExpressionBase() { - operators = new std::shared_ptr[_n_operators]; - n_operators = _n_operators; - } - ~Expression() { delete[] operators; } - std::string __str__() override; - bool is_expression_type() override; - double evaluate() override; - double get_value_from_array(double *) override; - int get_degree_from_array(int *) override; - std::shared_ptr>> - identify_variables() override; - std::shared_ptr>> - identify_external_operators() override; - std::string get_string_from_array(std::string *) override; - std::shared_ptr>> - get_prefix_notation() override; - void write_nl_string(std::ofstream &) override; - std::vector> get_operators(); - std::shared_ptr *operators; - unsigned int n_operators; - void fill_expression(std::shared_ptr *oper_array, - int &oper_ndx) override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, double integer_tol); - void propagate_bounds_backward(double *lbs, double *ubs, - double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars); - double get_lb_from_array(double *lbs) override; - double get_ub_from_array(double *ubs) override; - void - set_bounds_in_array(double new_lb, double new_ub, double *lbs, double *ubs, - double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class Operator : public Node { -public: - Operator() = default; - int index = 0; - virtual void evaluate(double *values) = 0; - virtual void propagate_degree_forward(int *degrees, double *values) = 0; - virtual void - identify_variables(std::set> &, - std::shared_ptr>>) = 0; - std::shared_ptr shared_from_this() { - return std::static_pointer_cast(Node::shared_from_this()); - } - bool is_operator_type() override; - double get_value_from_array(double *) override; - int get_degree_from_array(int *) override; - std::string get_string_from_array(std::string *) override; - virtual void print(std::string *) = 0; - virtual std::string name() = 0; - virtual void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol); - virtual void - propagate_bounds_backward(double *lbs, double *ubs, double feasibility_tol, - double integer_tol, double improvement_tol, - std::set> &improved_vars); - double get_lb_from_array(double *lbs) override; - double get_ub_from_array(double *ubs) override; - void - set_bounds_in_array(double new_lb, double new_ub, double *lbs, double *ubs, - double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class BinaryOperator : public Operator { -public: - BinaryOperator() = default; - virtual ~BinaryOperator() = default; - void identify_variables( - std::set> &, - std::shared_ptr>>) override; - std::shared_ptr operand1; - std::shared_ptr operand2; - void fill_prefix_notation_stack( - std::shared_ptr>> stack) override; - bool is_binary_operator() override; - void fill_expression(std::shared_ptr *oper_array, - int &oper_ndx) override; -}; - -class UnaryOperator : public Operator { -public: - UnaryOperator() = default; - virtual ~UnaryOperator() = default; - void identify_variables( - std::set> &, - std::shared_ptr>>) override; - std::shared_ptr operand; - void fill_prefix_notation_stack( - std::shared_ptr>> stack) override; - bool is_unary_operator() override; - void propagate_degree_forward(int *degrees, double *values) override; - void fill_expression(std::shared_ptr *oper_array, - int &oper_ndx) override; -}; - -class LinearOperator : public Operator { -public: - LinearOperator(int _nterms) { - variables = new std::shared_ptr[_nterms]; - coefficients = new std::shared_ptr[_nterms]; - nterms = _nterms; - } - ~LinearOperator() { - delete[] variables; - delete[] coefficients; - } - void identify_variables( - std::set> &, - std::shared_ptr>>) override; - std::shared_ptr *variables; - std::shared_ptr *coefficients; - std::shared_ptr constant = std::make_shared(0); - void evaluate(double *values) override; - void propagate_degree_forward(int *degrees, double *values) override; - void print(std::string *) override; - std::string name() override { return "LinearOperator"; }; - void write_nl_string(std::ofstream &) override; - void fill_prefix_notation_stack( - std::shared_ptr>> stack) override; - bool is_linear_operator() override; - unsigned int nterms; - void fill_expression(std::shared_ptr *oper_array, - int &oper_ndx) override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class SumOperator : public Operator { -public: - SumOperator(int _nargs) { - operands = new std::shared_ptr[_nargs]; - nargs = _nargs; - } - ~SumOperator() { delete[] operands; } - void identify_variables( - std::set> &, - std::shared_ptr>>) override; - void evaluate(double *values) override; - void propagate_degree_forward(int *degrees, double *values) override; - void print(std::string *) override; - std::string name() override { return "SumOperator"; }; - void write_nl_string(std::ofstream &) override; - void fill_prefix_notation_stack( - std::shared_ptr>> stack) override; - bool is_sum_operator() override; - std::shared_ptr *operands; - unsigned int nargs; - void fill_expression(std::shared_ptr *oper_array, - int &oper_ndx) override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class MultiplyOperator : public BinaryOperator { -public: - MultiplyOperator() = default; - void evaluate(double *values) override; - void propagate_degree_forward(int *degrees, double *values) override; - void print(std::string *) override; - std::string name() override { return "MultiplyOperator"; }; - void write_nl_string(std::ofstream &) override; - bool is_multiply_operator() override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class ExternalOperator : public Operator { -public: - ExternalOperator(int _nargs) { - operands = new std::shared_ptr[_nargs]; - nargs = _nargs; - } - ~ExternalOperator() { delete[] operands; } - void evaluate(double *values) override; - void propagate_degree_forward(int *degrees, double *values) override; - void print(std::string *) override; - std::string name() override { return "ExternalOperator"; }; - void write_nl_string(std::ofstream &) override; - void fill_prefix_notation_stack( - std::shared_ptr>> stack) override; - void identify_variables( - std::set> &, - std::shared_ptr>>) override; - bool is_external_operator() override; - std::string function_name; - int external_function_index = -1; - std::shared_ptr *operands; - unsigned int nargs; - void fill_expression(std::shared_ptr *oper_array, - int &oper_ndx) override; -}; - -class DivideOperator : public BinaryOperator { -public: - DivideOperator() = default; - void evaluate(double *values) override; - void propagate_degree_forward(int *degrees, double *values) override; - void print(std::string *) override; - std::string name() override { return "DivideOperator"; }; - void write_nl_string(std::ofstream &) override; - bool is_divide_operator() override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class PowerOperator : public BinaryOperator { -public: - PowerOperator() = default; - void evaluate(double *values) override; - void propagate_degree_forward(int *degrees, double *values) override; - void print(std::string *) override; - std::string name() override { return "PowerOperator"; }; - void write_nl_string(std::ofstream &) override; - bool is_power_operator() override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class NegationOperator : public UnaryOperator { -public: - NegationOperator() = default; - void evaluate(double *values) override; - void propagate_degree_forward(int *degrees, double *values) override; - void print(std::string *) override; - std::string name() override { return "NegationOperator"; }; - void write_nl_string(std::ofstream &) override; - bool is_negation_operator() override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class ExpOperator : public UnaryOperator { -public: - ExpOperator() = default; - void evaluate(double *values) override; - void print(std::string *) override; - std::string name() override { return "ExpOperator"; }; - void write_nl_string(std::ofstream &) override; - bool is_exp_operator() override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class LogOperator : public UnaryOperator { -public: - LogOperator() = default; - void evaluate(double *values) override; - void print(std::string *) override; - std::string name() override { return "LogOperator"; }; - void write_nl_string(std::ofstream &) override; - bool is_log_operator() override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class AbsOperator : public UnaryOperator { -public: - AbsOperator() = default; - void evaluate(double *values) override; - void print(std::string *) override; - std::string name() override { return "AbsOperator"; }; - void write_nl_string(std::ofstream &) override; - bool is_abs_operator() override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class SqrtOperator : public UnaryOperator { -public: - SqrtOperator() = default; - void evaluate(double *values) override; - void print(std::string *) override; - std::string name() override { return "SqrtOperator"; }; - void write_nl_string(std::ofstream &) override; - bool is_sqrt_operator() override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class Log10Operator : public UnaryOperator { -public: - Log10Operator() = default; - void evaluate(double *values) override; - void print(std::string *) override; - std::string name() override { return "Log10Operator"; }; - void write_nl_string(std::ofstream &) override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class SinOperator : public UnaryOperator { -public: - SinOperator() = default; - void evaluate(double *values) override; - void print(std::string *) override; - std::string name() override { return "SinOperator"; }; - void write_nl_string(std::ofstream &) override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class CosOperator : public UnaryOperator { -public: - CosOperator() = default; - void evaluate(double *values) override; - void print(std::string *) override; - std::string name() override { return "CosOperator"; }; - void write_nl_string(std::ofstream &) override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class TanOperator : public UnaryOperator { -public: - TanOperator() = default; - void evaluate(double *values) override; - void print(std::string *) override; - std::string name() override { return "TanOperator"; }; - void write_nl_string(std::ofstream &) override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class AsinOperator : public UnaryOperator { -public: - AsinOperator() = default; - void evaluate(double *values) override; - void print(std::string *) override; - std::string name() override { return "AsinOperator"; }; - void write_nl_string(std::ofstream &) override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class AcosOperator : public UnaryOperator { -public: - AcosOperator() = default; - void evaluate(double *values) override; - void print(std::string *) override; - std::string name() override { return "AcosOperator"; }; - void write_nl_string(std::ofstream &) override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class AtanOperator : public UnaryOperator { -public: - AtanOperator() = default; - void evaluate(double *values) override; - void print(std::string *) override; - std::string name() override { return "AtanOperator"; }; - void write_nl_string(std::ofstream &) override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -enum ExprType { - py_float = 0, - var = 1, - param = 2, - product = 3, - sum = 4, - negation = 5, - external_func = 6, - power = 7, - division = 8, - unary_func = 9, - linear = 10, - named_expr = 11, - numeric_constant = 12, - pyomo_unit = 13, - unary_abs = 14 -}; - -class PyomoExprTypes { -public: - PyomoExprTypes() { - expr_type_map[int_] = py_float; - expr_type_map[float_] = py_float; - expr_type_map[np_int16] = py_float; - expr_type_map[np_int32] = py_float; - expr_type_map[np_int64] = py_float; - expr_type_map[np_longlong] = py_float; - expr_type_map[np_uint16] = py_float; - expr_type_map[np_uint32] = py_float; - expr_type_map[np_uint64] = py_float; - expr_type_map[np_ulonglong] = py_float; - expr_type_map[np_float16] = py_float; - expr_type_map[np_float32] = py_float; - expr_type_map[np_float64] = py_float; - expr_type_map[ScalarVar] = var; - expr_type_map[_GeneralVarData] = var; - expr_type_map[AutoLinkedBinaryVar] = var; - expr_type_map[ScalarParam] = param; - expr_type_map[_ParamData] = param; - expr_type_map[MonomialTermExpression] = product; - expr_type_map[ProductExpression] = product; - expr_type_map[NPV_ProductExpression] = product; - expr_type_map[SumExpression] = sum; - expr_type_map[NPV_SumExpression] = sum; - expr_type_map[NegationExpression] = negation; - expr_type_map[NPV_NegationExpression] = negation; - expr_type_map[ExternalFunctionExpression] = external_func; - expr_type_map[NPV_ExternalFunctionExpression] = external_func; - expr_type_map[PowExpression] = power; - expr_type_map[NPV_PowExpression] = power; - expr_type_map[DivisionExpression] = division; - expr_type_map[NPV_DivisionExpression] = division; - expr_type_map[UnaryFunctionExpression] = unary_func; - expr_type_map[NPV_UnaryFunctionExpression] = unary_func; - expr_type_map[LinearExpression] = linear; - expr_type_map[_GeneralExpressionData] = named_expr; - expr_type_map[ScalarExpression] = named_expr; - expr_type_map[Integral] = named_expr; - expr_type_map[ScalarIntegral] = named_expr; - expr_type_map[NumericConstant] = numeric_constant; - expr_type_map[_PyomoUnit] = pyomo_unit; - expr_type_map[AbsExpression] = unary_abs; - expr_type_map[NPV_AbsExpression] = unary_abs; - } - ~PyomoExprTypes() = default; - py::int_ ione = 1; - py::float_ fone = 1.0; - py::type int_ = py::type::of(ione); - py::type float_ = py::type::of(fone); - py::object np = py::module_::import("numpy"); - py::type np_int16 = np.attr("int16"); - py::type np_int32 = np.attr("int32"); - py::type np_int64 = np.attr("int64"); - py::type np_longlong = np.attr("longlong"); - py::type np_uint16 = np.attr("uint16"); - py::type np_uint32 = np.attr("uint32"); - py::type np_uint64 = np.attr("uint64"); - py::type np_ulonglong = np.attr("ulonglong"); - py::type np_float16 = np.attr("float16"); - py::type np_float32 = np.attr("float32"); - py::type np_float64 = np.attr("float64"); - py::object ScalarParam = - py::module_::import("pyomo.core.base.param").attr("ScalarParam"); - py::object _ParamData = - py::module_::import("pyomo.core.base.param").attr("_ParamData"); - py::object ScalarVar = - py::module_::import("pyomo.core.base.var").attr("ScalarVar"); - py::object _GeneralVarData = - py::module_::import("pyomo.core.base.var").attr("_GeneralVarData"); - py::object AutoLinkedBinaryVar = - py::module_::import("pyomo.gdp.disjunct").attr("AutoLinkedBinaryVar"); - py::object numeric_expr = py::module_::import("pyomo.core.expr.numeric_expr"); - py::object NegationExpression = numeric_expr.attr("NegationExpression"); - py::object NPV_NegationExpression = - numeric_expr.attr("NPV_NegationExpression"); - py::object ExternalFunctionExpression = - numeric_expr.attr("ExternalFunctionExpression"); - py::object NPV_ExternalFunctionExpression = - numeric_expr.attr("NPV_ExternalFunctionExpression"); - py::object PowExpression = numeric_expr.attr("PowExpression"); - py::object NPV_PowExpression = numeric_expr.attr("NPV_PowExpression"); - py::object ProductExpression = numeric_expr.attr("ProductExpression"); - py::object NPV_ProductExpression = numeric_expr.attr("NPV_ProductExpression"); - py::object MonomialTermExpression = - numeric_expr.attr("MonomialTermExpression"); - py::object DivisionExpression = numeric_expr.attr("DivisionExpression"); - py::object NPV_DivisionExpression = - numeric_expr.attr("NPV_DivisionExpression"); - py::object SumExpression = numeric_expr.attr("SumExpression"); - py::object NPV_SumExpression = numeric_expr.attr("NPV_SumExpression"); - py::object UnaryFunctionExpression = - numeric_expr.attr("UnaryFunctionExpression"); - py::object AbsExpression = numeric_expr.attr("AbsExpression"); - py::object NPV_AbsExpression = numeric_expr.attr("NPV_AbsExpression"); - py::object NPV_UnaryFunctionExpression = - numeric_expr.attr("NPV_UnaryFunctionExpression"); - py::object LinearExpression = numeric_expr.attr("LinearExpression"); - py::object NumericConstant = - py::module_::import("pyomo.core.expr.numvalue").attr("NumericConstant"); - py::object expr_module = py::module_::import("pyomo.core.base.expression"); - py::object _GeneralExpressionData = - expr_module.attr("_GeneralExpressionData"); - py::object ScalarExpression = expr_module.attr("ScalarExpression"); - py::object ScalarIntegral = - py::module_::import("pyomo.dae.integral").attr("ScalarIntegral"); - py::object Integral = - py::module_::import("pyomo.dae.integral").attr("Integral"); - py::object _PyomoUnit = - py::module_::import("pyomo.core.base.units_container").attr("_PyomoUnit"); - py::object builtins = py::module_::import("builtins"); - py::object id = builtins.attr("id"); - py::object len = builtins.attr("len"); - py::dict expr_type_map; -}; - -std::vector> create_vars(int n_vars); -std::vector> create_params(int n_params); -std::vector> create_constants(int n_constants); -std::shared_ptr -appsi_expr_from_pyomo_expr(py::handle expr, py::handle var_map, - py::handle param_map, PyomoExprTypes &expr_types); -std::vector> -appsi_exprs_from_pyomo_exprs(py::list expr_list, py::dict var_map, - py::dict param_map); -py::tuple prep_for_repn(py::handle expr, PyomoExprTypes &expr_types); - -void process_pyomo_vars(PyomoExprTypes &expr_types, py::list pyomo_vars, - py::dict var_map, py::dict param_map, - py::dict var_attrs, py::dict rev_var_map, - py::bool_ _set_name, py::handle symbol_map, - py::handle labeler, py::bool_ _update); - -#endif +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2024 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + +#ifndef EXPRESSION_HEADER +#define EXPRESSION_HEADER + +#include "interval.hpp" +#include + +class Node; +class ExpressionBase; +class Leaf; +class Var; +class Constant; +class Param; +class Expression; +class Operator; +class BinaryOperator; +class UnaryOperator; +class LinearOperator; +class SumOperator; +class MultiplyOperator; +class DivideOperator; +class PowerOperator; +class NegationOperator; +class ExpOperator; +class LogOperator; +class AbsOperator; +class ExternalOperator; +class PyomoExprTypes; + +extern double inf; + +class Node : public std::enable_shared_from_this { +public: + Node() = default; + virtual ~Node() = default; + virtual bool is_variable_type() { return false; } + virtual bool is_param_type() { return false; } + virtual bool is_expression_type() { return false; } + virtual bool is_operator_type() { return false; } + virtual bool is_constant_type() { return false; } + virtual bool is_leaf() { return false; } + virtual bool is_binary_operator() { return false; } + virtual bool is_unary_operator() { return false; } + virtual bool is_linear_operator() { return false; } + virtual bool is_sum_operator() { return false; } + virtual bool is_multiply_operator() { return false; } + virtual bool is_divide_operator() { return false; } + virtual bool is_power_operator() { return false; } + virtual bool is_negation_operator() { return false; } + virtual bool is_exp_operator() { return false; } + virtual bool is_log_operator() { return false; } + virtual bool is_abs_operator() { return false; } + virtual bool is_sqrt_operator() { return false; } + virtual bool is_external_operator() { return false; } + virtual double get_value_from_array(double *) = 0; + virtual int get_degree_from_array(int *) = 0; + virtual std::string get_string_from_array(std::string *) = 0; + virtual void fill_prefix_notation_stack( + std::shared_ptr>> stack) = 0; + virtual void write_nl_string(std::ofstream &) = 0; + virtual void fill_expression(std::shared_ptr *oper_array, + int &oper_ndx) = 0; + virtual double get_lb_from_array(double *lbs) = 0; + virtual double get_ub_from_array(double *ubs) = 0; + virtual void + set_bounds_in_array(double new_lb, double new_ub, double *lbs, double *ubs, + double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) = 0; +}; + +class ExpressionBase : public Node { +public: + ExpressionBase() = default; + virtual double evaluate() = 0; + virtual std::string __str__() = 0; + virtual std::shared_ptr>> + identify_variables() = 0; + virtual std::shared_ptr>> + identify_external_operators() = 0; + virtual std::shared_ptr>> + get_prefix_notation() = 0; + std::shared_ptr shared_from_this() { + return std::static_pointer_cast(Node::shared_from_this()); + } + void fill_prefix_notation_stack( + std::shared_ptr>> stack) override { + ; + } +}; + +class Leaf : public ExpressionBase { +public: + Leaf() = default; + Leaf(double value) : value(value) {} + virtual ~Leaf() = default; + double value = 0.0; + bool is_leaf() override; + double evaluate() override; + double get_value_from_array(double *) override; + std::string get_string_from_array(std::string *) override; + std::shared_ptr>> + get_prefix_notation() override; + void fill_expression(std::shared_ptr *oper_array, + int &oper_ndx) override; + double get_lb_from_array(double *lbs) override; + double get_ub_from_array(double *ubs) override; + void + set_bounds_in_array(double new_lb, double new_ub, double *lbs, double *ubs, + double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class Constant : public Leaf { +public: + Constant() = default; + Constant(double value) : Leaf(value) {} + bool is_constant_type() override; + std::string __str__() override; + int get_degree_from_array(int *) override; + std::shared_ptr>> + identify_variables() override; + std::shared_ptr>> + identify_external_operators() override; + void write_nl_string(std::ofstream &) override; +}; + +enum Domain { continuous, binary, integers }; + +class Var : public Leaf { +public: + Var() = default; + Var(double val) : Leaf(val) {} + Var(std::string _name) : name(_name) {} + Var(std::string _name, double val) : Leaf(val), name(_name) {} + std::string name = "v"; + std::string __str__() override; + std::shared_ptr lb; + std::shared_ptr ub; + int index = -1; + bool fixed = false; + double domain_lb = -inf; + double domain_ub = inf; + Domain domain = continuous; + bool is_variable_type() override; + int get_degree_from_array(int *) override; + std::shared_ptr>> + identify_variables() override; + std::shared_ptr>> + identify_external_operators() override; + void write_nl_string(std::ofstream &) override; + std::shared_ptr shared_from_this() { + return std::static_pointer_cast(Node::shared_from_this()); + } + double get_lb(); + double get_ub(); + Domain get_domain(); + double get_lb_from_array(double *lbs) override; + double get_ub_from_array(double *ubs) override; + void + set_bounds_in_array(double new_lb, double new_ub, double *lbs, double *ubs, + double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class Param : public Leaf { +public: + Param() = default; + Param(double val) : Leaf(val) {} + Param(std::string _name) : name(_name) {} + Param(std::string _name, double val) : Leaf(val), name(_name) {} + std::string name = "p"; + std::string __str__() override; + bool is_param_type() override; + int get_degree_from_array(int *) override; + std::shared_ptr>> + identify_variables() override; + std::shared_ptr>> + identify_external_operators() override; + void write_nl_string(std::ofstream &) override; +}; + +class Expression : public ExpressionBase { +public: + Expression(int _n_operators) : ExpressionBase() { + operators = new std::shared_ptr[_n_operators]; + n_operators = _n_operators; + } + ~Expression() { delete[] operators; } + std::string __str__() override; + bool is_expression_type() override; + double evaluate() override; + double get_value_from_array(double *) override; + int get_degree_from_array(int *) override; + std::shared_ptr>> + identify_variables() override; + std::shared_ptr>> + identify_external_operators() override; + std::string get_string_from_array(std::string *) override; + std::shared_ptr>> + get_prefix_notation() override; + void write_nl_string(std::ofstream &) override; + std::vector> get_operators(); + std::shared_ptr *operators; + unsigned int n_operators; + void fill_expression(std::shared_ptr *oper_array, + int &oper_ndx) override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, double integer_tol); + void propagate_bounds_backward(double *lbs, double *ubs, + double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars); + double get_lb_from_array(double *lbs) override; + double get_ub_from_array(double *ubs) override; + void + set_bounds_in_array(double new_lb, double new_ub, double *lbs, double *ubs, + double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class Operator : public Node { +public: + Operator() = default; + int index = 0; + virtual void evaluate(double *values) = 0; + virtual void propagate_degree_forward(int *degrees, double *values) = 0; + virtual void + identify_variables(std::set> &, + std::shared_ptr>>) = 0; + std::shared_ptr shared_from_this() { + return std::static_pointer_cast(Node::shared_from_this()); + } + bool is_operator_type() override; + double get_value_from_array(double *) override; + int get_degree_from_array(int *) override; + std::string get_string_from_array(std::string *) override; + virtual void print(std::string *) = 0; + virtual std::string name() = 0; + virtual void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol); + virtual void + propagate_bounds_backward(double *lbs, double *ubs, double feasibility_tol, + double integer_tol, double improvement_tol, + std::set> &improved_vars); + double get_lb_from_array(double *lbs) override; + double get_ub_from_array(double *ubs) override; + void + set_bounds_in_array(double new_lb, double new_ub, double *lbs, double *ubs, + double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class BinaryOperator : public Operator { +public: + BinaryOperator() = default; + virtual ~BinaryOperator() = default; + void identify_variables( + std::set> &, + std::shared_ptr>>) override; + std::shared_ptr operand1; + std::shared_ptr operand2; + void fill_prefix_notation_stack( + std::shared_ptr>> stack) override; + bool is_binary_operator() override; + void fill_expression(std::shared_ptr *oper_array, + int &oper_ndx) override; +}; + +class UnaryOperator : public Operator { +public: + UnaryOperator() = default; + virtual ~UnaryOperator() = default; + void identify_variables( + std::set> &, + std::shared_ptr>>) override; + std::shared_ptr operand; + void fill_prefix_notation_stack( + std::shared_ptr>> stack) override; + bool is_unary_operator() override; + void propagate_degree_forward(int *degrees, double *values) override; + void fill_expression(std::shared_ptr *oper_array, + int &oper_ndx) override; +}; + +class LinearOperator : public Operator { +public: + LinearOperator(int _nterms) { + variables = new std::shared_ptr[_nterms]; + coefficients = new std::shared_ptr[_nterms]; + nterms = _nterms; + } + ~LinearOperator() { + delete[] variables; + delete[] coefficients; + } + void identify_variables( + std::set> &, + std::shared_ptr>>) override; + std::shared_ptr *variables; + std::shared_ptr *coefficients; + std::shared_ptr constant = std::make_shared(0); + void evaluate(double *values) override; + void propagate_degree_forward(int *degrees, double *values) override; + void print(std::string *) override; + std::string name() override { return "LinearOperator"; }; + void write_nl_string(std::ofstream &) override; + void fill_prefix_notation_stack( + std::shared_ptr>> stack) override; + bool is_linear_operator() override; + unsigned int nterms; + void fill_expression(std::shared_ptr *oper_array, + int &oper_ndx) override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class SumOperator : public Operator { +public: + SumOperator(int _nargs) { + operands = new std::shared_ptr[_nargs]; + nargs = _nargs; + } + ~SumOperator() { delete[] operands; } + void identify_variables( + std::set> &, + std::shared_ptr>>) override; + void evaluate(double *values) override; + void propagate_degree_forward(int *degrees, double *values) override; + void print(std::string *) override; + std::string name() override { return "SumOperator"; }; + void write_nl_string(std::ofstream &) override; + void fill_prefix_notation_stack( + std::shared_ptr>> stack) override; + bool is_sum_operator() override; + std::shared_ptr *operands; + unsigned int nargs; + void fill_expression(std::shared_ptr *oper_array, + int &oper_ndx) override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class MultiplyOperator : public BinaryOperator { +public: + MultiplyOperator() = default; + void evaluate(double *values) override; + void propagate_degree_forward(int *degrees, double *values) override; + void print(std::string *) override; + std::string name() override { return "MultiplyOperator"; }; + void write_nl_string(std::ofstream &) override; + bool is_multiply_operator() override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class ExternalOperator : public Operator { +public: + ExternalOperator(int _nargs) { + operands = new std::shared_ptr[_nargs]; + nargs = _nargs; + } + ~ExternalOperator() { delete[] operands; } + void evaluate(double *values) override; + void propagate_degree_forward(int *degrees, double *values) override; + void print(std::string *) override; + std::string name() override { return "ExternalOperator"; }; + void write_nl_string(std::ofstream &) override; + void fill_prefix_notation_stack( + std::shared_ptr>> stack) override; + void identify_variables( + std::set> &, + std::shared_ptr>>) override; + bool is_external_operator() override; + std::string function_name; + int external_function_index = -1; + std::shared_ptr *operands; + unsigned int nargs; + void fill_expression(std::shared_ptr *oper_array, + int &oper_ndx) override; +}; + +class DivideOperator : public BinaryOperator { +public: + DivideOperator() = default; + void evaluate(double *values) override; + void propagate_degree_forward(int *degrees, double *values) override; + void print(std::string *) override; + std::string name() override { return "DivideOperator"; }; + void write_nl_string(std::ofstream &) override; + bool is_divide_operator() override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class PowerOperator : public BinaryOperator { +public: + PowerOperator() = default; + void evaluate(double *values) override; + void propagate_degree_forward(int *degrees, double *values) override; + void print(std::string *) override; + std::string name() override { return "PowerOperator"; }; + void write_nl_string(std::ofstream &) override; + bool is_power_operator() override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class NegationOperator : public UnaryOperator { +public: + NegationOperator() = default; + void evaluate(double *values) override; + void propagate_degree_forward(int *degrees, double *values) override; + void print(std::string *) override; + std::string name() override { return "NegationOperator"; }; + void write_nl_string(std::ofstream &) override; + bool is_negation_operator() override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class ExpOperator : public UnaryOperator { +public: + ExpOperator() = default; + void evaluate(double *values) override; + void print(std::string *) override; + std::string name() override { return "ExpOperator"; }; + void write_nl_string(std::ofstream &) override; + bool is_exp_operator() override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class LogOperator : public UnaryOperator { +public: + LogOperator() = default; + void evaluate(double *values) override; + void print(std::string *) override; + std::string name() override { return "LogOperator"; }; + void write_nl_string(std::ofstream &) override; + bool is_log_operator() override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class AbsOperator : public UnaryOperator { +public: + AbsOperator() = default; + void evaluate(double *values) override; + void print(std::string *) override; + std::string name() override { return "AbsOperator"; }; + void write_nl_string(std::ofstream &) override; + bool is_abs_operator() override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class SqrtOperator : public UnaryOperator { +public: + SqrtOperator() = default; + void evaluate(double *values) override; + void print(std::string *) override; + std::string name() override { return "SqrtOperator"; }; + void write_nl_string(std::ofstream &) override; + bool is_sqrt_operator() override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class Log10Operator : public UnaryOperator { +public: + Log10Operator() = default; + void evaluate(double *values) override; + void print(std::string *) override; + std::string name() override { return "Log10Operator"; }; + void write_nl_string(std::ofstream &) override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class SinOperator : public UnaryOperator { +public: + SinOperator() = default; + void evaluate(double *values) override; + void print(std::string *) override; + std::string name() override { return "SinOperator"; }; + void write_nl_string(std::ofstream &) override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class CosOperator : public UnaryOperator { +public: + CosOperator() = default; + void evaluate(double *values) override; + void print(std::string *) override; + std::string name() override { return "CosOperator"; }; + void write_nl_string(std::ofstream &) override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class TanOperator : public UnaryOperator { +public: + TanOperator() = default; + void evaluate(double *values) override; + void print(std::string *) override; + std::string name() override { return "TanOperator"; }; + void write_nl_string(std::ofstream &) override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class AsinOperator : public UnaryOperator { +public: + AsinOperator() = default; + void evaluate(double *values) override; + void print(std::string *) override; + std::string name() override { return "AsinOperator"; }; + void write_nl_string(std::ofstream &) override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class AcosOperator : public UnaryOperator { +public: + AcosOperator() = default; + void evaluate(double *values) override; + void print(std::string *) override; + std::string name() override { return "AcosOperator"; }; + void write_nl_string(std::ofstream &) override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class AtanOperator : public UnaryOperator { +public: + AtanOperator() = default; + void evaluate(double *values) override; + void print(std::string *) override; + std::string name() override { return "AtanOperator"; }; + void write_nl_string(std::ofstream &) override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +enum ExprType { + py_float = 0, + var = 1, + param = 2, + product = 3, + sum = 4, + negation = 5, + external_func = 6, + power = 7, + division = 8, + unary_func = 9, + linear = 10, + named_expr = 11, + numeric_constant = 12, + pyomo_unit = 13, + unary_abs = 14 +}; + +class PyomoExprTypes { +public: + PyomoExprTypes() { + expr_type_map[int_] = py_float; + expr_type_map[float_] = py_float; + expr_type_map[np_int16] = py_float; + expr_type_map[np_int32] = py_float; + expr_type_map[np_int64] = py_float; + expr_type_map[np_longlong] = py_float; + expr_type_map[np_uint16] = py_float; + expr_type_map[np_uint32] = py_float; + expr_type_map[np_uint64] = py_float; + expr_type_map[np_ulonglong] = py_float; + expr_type_map[np_float16] = py_float; + expr_type_map[np_float32] = py_float; + expr_type_map[np_float64] = py_float; + expr_type_map[ScalarVar] = var; + expr_type_map[VarData] = var; + expr_type_map[AutoLinkedBinaryVar] = var; + expr_type_map[ScalarParam] = param; + expr_type_map[ParamData] = param; + expr_type_map[MonomialTermExpression] = product; + expr_type_map[ProductExpression] = product; + expr_type_map[NPV_ProductExpression] = product; + expr_type_map[SumExpression] = sum; + expr_type_map[NPV_SumExpression] = sum; + expr_type_map[NegationExpression] = negation; + expr_type_map[NPV_NegationExpression] = negation; + expr_type_map[ExternalFunctionExpression] = external_func; + expr_type_map[NPV_ExternalFunctionExpression] = external_func; + expr_type_map[PowExpression] = power; + expr_type_map[NPV_PowExpression] = power; + expr_type_map[DivisionExpression] = division; + expr_type_map[NPV_DivisionExpression] = division; + expr_type_map[UnaryFunctionExpression] = unary_func; + expr_type_map[NPV_UnaryFunctionExpression] = unary_func; + expr_type_map[LinearExpression] = linear; + expr_type_map[ExpressionData] = named_expr; + expr_type_map[ScalarExpression] = named_expr; + expr_type_map[Integral] = named_expr; + expr_type_map[ScalarIntegral] = named_expr; + expr_type_map[NumericConstant] = numeric_constant; + expr_type_map[_PyomoUnit] = pyomo_unit; + expr_type_map[AbsExpression] = unary_abs; + expr_type_map[NPV_AbsExpression] = unary_abs; + } + ~PyomoExprTypes() = default; + py::int_ ione = 1; + py::float_ fone = 1.0; + py::type int_ = py::type::of(ione); + py::type float_ = py::type::of(fone); + py::object np = py::module_::import("numpy"); + py::type np_int16 = np.attr("int16"); + py::type np_int32 = np.attr("int32"); + py::type np_int64 = np.attr("int64"); + py::type np_longlong = np.attr("longlong"); + py::type np_uint16 = np.attr("uint16"); + py::type np_uint32 = np.attr("uint32"); + py::type np_uint64 = np.attr("uint64"); + py::type np_ulonglong = np.attr("ulonglong"); + py::type np_float16 = np.attr("float16"); + py::type np_float32 = np.attr("float32"); + py::type np_float64 = np.attr("float64"); + py::object ScalarParam = + py::module_::import("pyomo.core.base.param").attr("ScalarParam"); + py::object ParamData = + py::module_::import("pyomo.core.base.param").attr("ParamData"); + py::object ScalarVar = + py::module_::import("pyomo.core.base.var").attr("ScalarVar"); + py::object VarData = + py::module_::import("pyomo.core.base.var").attr("VarData"); + py::object AutoLinkedBinaryVar = + py::module_::import("pyomo.gdp.disjunct").attr("AutoLinkedBinaryVar"); + py::object numeric_expr = py::module_::import("pyomo.core.expr.numeric_expr"); + py::object NegationExpression = numeric_expr.attr("NegationExpression"); + py::object NPV_NegationExpression = + numeric_expr.attr("NPV_NegationExpression"); + py::object ExternalFunctionExpression = + numeric_expr.attr("ExternalFunctionExpression"); + py::object NPV_ExternalFunctionExpression = + numeric_expr.attr("NPV_ExternalFunctionExpression"); + py::object PowExpression = numeric_expr.attr("PowExpression"); + py::object NPV_PowExpression = numeric_expr.attr("NPV_PowExpression"); + py::object ProductExpression = numeric_expr.attr("ProductExpression"); + py::object NPV_ProductExpression = numeric_expr.attr("NPV_ProductExpression"); + py::object MonomialTermExpression = + numeric_expr.attr("MonomialTermExpression"); + py::object DivisionExpression = numeric_expr.attr("DivisionExpression"); + py::object NPV_DivisionExpression = + numeric_expr.attr("NPV_DivisionExpression"); + py::object SumExpression = numeric_expr.attr("SumExpression"); + py::object NPV_SumExpression = numeric_expr.attr("NPV_SumExpression"); + py::object UnaryFunctionExpression = + numeric_expr.attr("UnaryFunctionExpression"); + py::object AbsExpression = numeric_expr.attr("AbsExpression"); + py::object NPV_AbsExpression = numeric_expr.attr("NPV_AbsExpression"); + py::object NPV_UnaryFunctionExpression = + numeric_expr.attr("NPV_UnaryFunctionExpression"); + py::object LinearExpression = numeric_expr.attr("LinearExpression"); + py::object NumericConstant = + py::module_::import("pyomo.core.expr.numvalue").attr("NumericConstant"); + py::object expr_module = py::module_::import("pyomo.core.base.expression"); + py::object ExpressionData = + expr_module.attr("ExpressionData"); + py::object ScalarExpression = expr_module.attr("ScalarExpression"); + py::object ScalarIntegral = + py::module_::import("pyomo.dae.integral").attr("ScalarIntegral"); + py::object Integral = + py::module_::import("pyomo.dae.integral").attr("Integral"); + py::object _PyomoUnit = + py::module_::import("pyomo.core.base.units_container").attr("_PyomoUnit"); + py::object builtins = py::module_::import("builtins"); + py::object id = builtins.attr("id"); + py::object len = builtins.attr("len"); + py::dict expr_type_map; +}; + +std::vector> create_vars(int n_vars); +std::vector> create_params(int n_params); +std::vector> create_constants(int n_constants); +std::shared_ptr +appsi_expr_from_pyomo_expr(py::handle expr, py::handle var_map, + py::handle param_map, PyomoExprTypes &expr_types); +std::vector> +appsi_exprs_from_pyomo_exprs(py::list expr_list, py::dict var_map, + py::dict param_map); +py::tuple prep_for_repn(py::handle expr, PyomoExprTypes &expr_types); + +void process_pyomo_vars(PyomoExprTypes &expr_types, py::list pyomo_vars, + py::dict var_map, py::dict param_map, + py::dict var_attrs, py::dict rev_var_map, + py::bool_ _set_name, py::handle symbol_map, + py::handle labeler, py::bool_ _update); + +#endif diff --git a/pyomo/contrib/appsi/cmodel/src/fbbt_model.cpp b/pyomo/contrib/appsi/cmodel/src/fbbt_model.cpp index 2e490659fab..bd8d7dbf854 100644 --- a/pyomo/contrib/appsi/cmodel/src/fbbt_model.cpp +++ b/pyomo/contrib/appsi/cmodel/src/fbbt_model.cpp @@ -1,3 +1,15 @@ +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2024 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + #include "fbbt_model.hpp" FBBTObjective::FBBTObjective(std::shared_ptr _expr) diff --git a/pyomo/contrib/appsi/cmodel/src/fbbt_model.hpp b/pyomo/contrib/appsi/cmodel/src/fbbt_model.hpp index 3d1c3a76caa..ca1980a797b 100644 --- a/pyomo/contrib/appsi/cmodel/src/fbbt_model.hpp +++ b/pyomo/contrib/appsi/cmodel/src/fbbt_model.hpp @@ -1,3 +1,15 @@ +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2024 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + #include "model_base.hpp" class FBBTConstraint; diff --git a/pyomo/contrib/appsi/cmodel/src/interval.cpp b/pyomo/contrib/appsi/cmodel/src/interval.cpp index f0a1aa2c2bb..1d9b3a6f82e 100644 --- a/pyomo/contrib/appsi/cmodel/src/interval.cpp +++ b/pyomo/contrib/appsi/cmodel/src/interval.cpp @@ -1,3 +1,15 @@ +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2024 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + #include "interval.hpp" bool _is_inf(double x) { diff --git a/pyomo/contrib/appsi/cmodel/src/interval.hpp b/pyomo/contrib/appsi/cmodel/src/interval.hpp index c35438887dd..a57f107f8db 100644 --- a/pyomo/contrib/appsi/cmodel/src/interval.hpp +++ b/pyomo/contrib/appsi/cmodel/src/interval.hpp @@ -1,3 +1,15 @@ +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2024 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + #ifndef INTERVAL_HEADER #define INTERVAL_HEADER diff --git a/pyomo/contrib/appsi/cmodel/src/lp_writer.cpp b/pyomo/contrib/appsi/cmodel/src/lp_writer.cpp index 1ce421b7c97..68baf2b8ae8 100644 --- a/pyomo/contrib/appsi/cmodel/src/lp_writer.cpp +++ b/pyomo/contrib/appsi/cmodel/src/lp_writer.cpp @@ -1,3 +1,15 @@ +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2024 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + #include "lp_writer.hpp" void write_expr(std::ofstream &f, std::shared_ptr obj, diff --git a/pyomo/contrib/appsi/cmodel/src/lp_writer.hpp b/pyomo/contrib/appsi/cmodel/src/lp_writer.hpp index ee4ad77500a..0b2e2882510 100644 --- a/pyomo/contrib/appsi/cmodel/src/lp_writer.hpp +++ b/pyomo/contrib/appsi/cmodel/src/lp_writer.hpp @@ -1,3 +1,15 @@ +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2024 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + #include "model_base.hpp" class LPBase; diff --git a/pyomo/contrib/appsi/cmodel/src/model_base.cpp b/pyomo/contrib/appsi/cmodel/src/model_base.cpp index ab0b25d8e0d..b0ae4013b32 100644 --- a/pyomo/contrib/appsi/cmodel/src/model_base.cpp +++ b/pyomo/contrib/appsi/cmodel/src/model_base.cpp @@ -1,3 +1,15 @@ +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2024 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + #include "model_base.hpp" bool constraint_sorter(std::shared_ptr c1, diff --git a/pyomo/contrib/appsi/cmodel/src/model_base.hpp b/pyomo/contrib/appsi/cmodel/src/model_base.hpp index bc61bc053de..a47f1d14a0b 100644 --- a/pyomo/contrib/appsi/cmodel/src/model_base.hpp +++ b/pyomo/contrib/appsi/cmodel/src/model_base.hpp @@ -1,3 +1,15 @@ +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2024 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + #ifndef MODEL_HEADER #define MODEL_HEADER diff --git a/pyomo/contrib/appsi/cmodel/src/nl_writer.cpp b/pyomo/contrib/appsi/cmodel/src/nl_writer.cpp index dc7004abc16..8de6cc74ab4 100644 --- a/pyomo/contrib/appsi/cmodel/src/nl_writer.cpp +++ b/pyomo/contrib/appsi/cmodel/src/nl_writer.cpp @@ -1,3 +1,15 @@ +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2024 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + #include "nl_writer.hpp" NLBase::NLBase( diff --git a/pyomo/contrib/appsi/cmodel/src/nl_writer.hpp b/pyomo/contrib/appsi/cmodel/src/nl_writer.hpp index 40e4c9b1222..b7439875301 100644 --- a/pyomo/contrib/appsi/cmodel/src/nl_writer.hpp +++ b/pyomo/contrib/appsi/cmodel/src/nl_writer.hpp @@ -1,3 +1,15 @@ +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2024 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + #include "model_base.hpp" class NLBase; diff --git a/pyomo/contrib/appsi/cmodel/tests/__init__.py b/pyomo/contrib/appsi/cmodel/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/appsi/cmodel/tests/__init__.py +++ b/pyomo/contrib/appsi/cmodel/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/appsi/cmodel/tests/test_import.py b/pyomo/contrib/appsi/cmodel/tests/test_import.py index f4647c216ba..76eda902ac0 100644 --- a/pyomo/contrib/appsi/cmodel/tests/test_import.py +++ b/pyomo/contrib/appsi/cmodel/tests/test_import.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.common import unittest from pyomo.common.fileutils import find_library, this_file_dir import os diff --git a/pyomo/contrib/appsi/examples/__init__.py b/pyomo/contrib/appsi/examples/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/appsi/examples/__init__.py +++ b/pyomo/contrib/appsi/examples/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/appsi/examples/getting_started.py b/pyomo/contrib/appsi/examples/getting_started.py index de22d28e0a4..6bc42d1d377 100644 --- a/pyomo/contrib/appsi/examples/getting_started.py +++ b/pyomo/contrib/appsi/examples/getting_started.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pe from pyomo.contrib import appsi from pyomo.common.timing import HierarchicalTimer diff --git a/pyomo/contrib/appsi/examples/tests/__init__.py b/pyomo/contrib/appsi/examples/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/appsi/examples/tests/__init__.py +++ b/pyomo/contrib/appsi/examples/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/appsi/examples/tests/test_examples.py b/pyomo/contrib/appsi/examples/tests/test_examples.py index d2c88224a7d..a7608d36b98 100644 --- a/pyomo/contrib/appsi/examples/tests/test_examples.py +++ b/pyomo/contrib/appsi/examples/tests/test_examples.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.contrib.appsi.examples import getting_started import pyomo.common.unittest as unittest import pyomo.environ as pe diff --git a/pyomo/contrib/appsi/fbbt.py b/pyomo/contrib/appsi/fbbt.py index 92a0e0c8cbc..8e0c74b00e9 100644 --- a/pyomo/contrib/appsi/fbbt.py +++ b/pyomo/contrib/appsi/fbbt.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.contrib.appsi.base import PersistentBase from pyomo.common.config import ( ConfigDict, @@ -7,12 +18,12 @@ ) from .cmodel import cmodel, cmodel_available from typing import List, Optional -from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.param import _ParamData -from pyomo.core.base.constraint import _GeneralConstraintData -from pyomo.core.base.sos import _SOSConstraintData -from pyomo.core.base.objective import _GeneralObjectiveData, minimize, maximize -from pyomo.core.base.block import _BlockData +from pyomo.core.base.var import VarData +from pyomo.core.base.param import ParamData +from pyomo.core.base.constraint import ConstraintData +from pyomo.core.base.sos import SOSConstraintData +from pyomo.core.base.objective import ObjectiveData, minimize, maximize +from pyomo.core.base.block import BlockData from pyomo.core.base import SymbolMap, TextLabeler from pyomo.common.errors import InfeasibleConstraintException @@ -110,7 +121,7 @@ def set_instance(self, model, symbolic_solver_labels: Optional[bool] = None): if self._objective is None: self.set_objective(None) - def _add_variables(self, variables: List[_GeneralVarData]): + def _add_variables(self, variables: List[VarData]): if self._symbolic_solver_labels: set_name = True symbol_map = self._symbol_map @@ -132,7 +143,7 @@ def _add_variables(self, variables: List[_GeneralVarData]): False, ) - def _add_params(self, params: List[_ParamData]): + def _add_params(self, params: List[ParamData]): cparams = cmodel.create_params(len(params)) for ndx, p in enumerate(params): cp = cparams[ndx] @@ -143,7 +154,7 @@ def _add_params(self, params: List[_ParamData]): cp = cparams[ndx] cp.name = self._symbol_map.getSymbol(p, self._param_labeler) - def _add_constraints(self, cons: List[_GeneralConstraintData]): + def _add_constraints(self, cons: List[ConstraintData]): cmodel.process_fbbt_constraints( self._cmodel, self._pyomo_expr_types, @@ -158,13 +169,13 @@ def _add_constraints(self, cons: List[_GeneralConstraintData]): for c, cc in self._con_map.items(): cc.name = self._symbol_map.getSymbol(c, self._con_labeler) - def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + def _add_sos_constraints(self, cons: List[SOSConstraintData]): if len(cons) != 0: raise NotImplementedError( 'IntervalTightener does not support SOS constraints' ) - def _remove_constraints(self, cons: List[_GeneralConstraintData]): + def _remove_constraints(self, cons: List[ConstraintData]): if self._symbolic_solver_labels: for c in cons: self._symbol_map.removeSymbol(c) @@ -173,13 +184,13 @@ def _remove_constraints(self, cons: List[_GeneralConstraintData]): self._cmodel.remove_constraint(cc) del self._rcon_map[cc] - def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + def _remove_sos_constraints(self, cons: List[SOSConstraintData]): if len(cons) != 0: raise NotImplementedError( 'IntervalTightener does not support SOS constraints' ) - def _remove_variables(self, variables: List[_GeneralVarData]): + def _remove_variables(self, variables: List[VarData]): if self._symbolic_solver_labels: for v in variables: self._symbol_map.removeSymbol(v) @@ -187,14 +198,14 @@ def _remove_variables(self, variables: List[_GeneralVarData]): cvar = self._var_map.pop(id(v)) del self._rvar_map[cvar] - def _remove_params(self, params: List[_ParamData]): + def _remove_params(self, params: List[ParamData]): if self._symbolic_solver_labels: for p in params: self._symbol_map.removeSymbol(p) for p in params: del self._param_map[id(p)] - def _update_variables(self, variables: List[_GeneralVarData]): + def _update_variables(self, variables: List[VarData]): cmodel.process_pyomo_vars( self._pyomo_expr_types, variables, @@ -213,13 +224,13 @@ def update_params(self): cp = self._param_map[p_id] cp.value = p.value - def set_objective(self, obj: _GeneralObjectiveData): + def set_objective(self, obj: ObjectiveData): if self._symbolic_solver_labels: if self._objective is not None: self._symbol_map.removeSymbol(self._objective) super().set_objective(obj) - def _set_objective(self, obj: _GeneralObjectiveData): + def _set_objective(self, obj: ObjectiveData): if obj is None: ce = cmodel.Constant(0) sense = 0 @@ -264,7 +275,7 @@ def _deactivate_satisfied_cons(self): c.deactivate() def perform_fbbt( - self, model: _BlockData, symbolic_solver_labels: Optional[bool] = None + self, model: BlockData, symbolic_solver_labels: Optional[bool] = None ): if model is not self._model: self.set_instance(model, symbolic_solver_labels=symbolic_solver_labels) @@ -293,7 +304,7 @@ def perform_fbbt( self._deactivate_satisfied_cons() return n_iter - def perform_fbbt_with_seed(self, model: _BlockData, seed_var: _GeneralVarData): + def perform_fbbt_with_seed(self, model: BlockData, seed_var: VarData): if model is not self._model: self.set_instance(model) else: diff --git a/pyomo/contrib/appsi/plugins.py b/pyomo/contrib/appsi/plugins.py index 5333158239e..3e1b639ce3b 100644 --- a/pyomo/contrib/appsi/plugins.py +++ b/pyomo/contrib/appsi/plugins.py @@ -1,23 +1,35 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.common.extensions import ExtensionBuilderFactory from .base import SolverFactory -from .solvers import Gurobi, Ipopt, Cbc, Cplex, Highs +from .solvers import Gurobi, Ipopt, Cbc, Cplex, Highs, MAiNGO from .build import AppsiBuilder def load(): ExtensionBuilderFactory.register('appsi')(AppsiBuilder) SolverFactory.register( - name='appsi_gurobi', doc='Automated persistent interface to Gurobi' + name='gurobi', doc='Automated persistent interface to Gurobi' )(Gurobi) + SolverFactory.register(name='cplex', doc='Automated persistent interface to Cplex')( + Cplex + ) + SolverFactory.register(name='ipopt', doc='Automated persistent interface to Ipopt')( + Ipopt + ) + SolverFactory.register(name='cbc', doc='Automated persistent interface to Cbc')(Cbc) + SolverFactory.register(name='highs', doc='Automated persistent interface to Highs')( + Highs + ) SolverFactory.register( - name='appsi_cplex', doc='Automated persistent interface to Cplex' - )(Cplex) - SolverFactory.register( - name='appsi_ipopt', doc='Automated persistent interface to Ipopt' - )(Ipopt) - SolverFactory.register( - name='appsi_cbc', doc='Automated persistent interface to Cbc' - )(Cbc) - SolverFactory.register( - name='appsi_highs', doc='Automated persistent interface to Highs' - )(Highs) + name='maingo', doc='Automated persistent interface to MAiNGO' + )(MAiNGO) diff --git a/pyomo/contrib/appsi/solvers/__init__.py b/pyomo/contrib/appsi/solvers/__init__.py index df58a0cb245..352571b98f8 100644 --- a/pyomo/contrib/appsi/solvers/__init__.py +++ b/pyomo/contrib/appsi/solvers/__init__.py @@ -1,5 +1,18 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from .gurobi import Gurobi, GurobiResults from .ipopt import Ipopt from .cbc import Cbc from .cplex import Cplex from .highs import Highs +from .wntr import Wntr, WntrResults +from .maingo import MAiNGO diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index a3aae2a9213..08833e747e2 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.common.tempfiles import TempfileManager from pyomo.common.fileutils import Executable from pyomo.contrib.appsi.base import ( @@ -15,11 +26,11 @@ import math from pyomo.common.collections import ComponentMap from typing import Optional, Sequence, NoReturn, List, Mapping -from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.constraint import _GeneralConstraintData -from pyomo.core.base.block import _BlockData -from pyomo.core.base.param import _ParamData -from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.core.base.var import VarData +from pyomo.core.base.constraint import ConstraintData +from pyomo.core.base.block import BlockData +from pyomo.core.base.param import ParamData +from pyomo.core.base.objective import ObjectiveData from pyomo.common.timing import HierarchicalTimer from pyomo.common.tee import TeeStream import sys @@ -153,34 +164,34 @@ def symbol_map(self): def set_instance(self, model): self._writer.set_instance(model) - def add_variables(self, variables: List[_GeneralVarData]): + def add_variables(self, variables: List[VarData]): self._writer.add_variables(variables) - def add_params(self, params: List[_ParamData]): + def add_params(self, params: List[ParamData]): self._writer.add_params(params) - def add_constraints(self, cons: List[_GeneralConstraintData]): + def add_constraints(self, cons: List[ConstraintData]): self._writer.add_constraints(cons) - def add_block(self, block: _BlockData): + def add_block(self, block: BlockData): self._writer.add_block(block) - def remove_variables(self, variables: List[_GeneralVarData]): + def remove_variables(self, variables: List[VarData]): self._writer.remove_variables(variables) - def remove_params(self, params: List[_ParamData]): + def remove_params(self, params: List[ParamData]): self._writer.remove_params(params) - def remove_constraints(self, cons: List[_GeneralConstraintData]): + def remove_constraints(self, cons: List[ConstraintData]): self._writer.remove_constraints(cons) - def remove_block(self, block: _BlockData): + def remove_block(self, block: BlockData): self._writer.remove_block(block) - def set_objective(self, obj: _GeneralObjectiveData): + def set_objective(self, obj: ObjectiveData): self._writer.set_objective(obj) - def update_variables(self, variables: List[_GeneralVarData]): + def update_variables(self, variables: List[VarData]): self._writer.update_variables(variables) def update_params(self): @@ -400,9 +411,11 @@ def _check_and_escape_options(): if cp.returncode != 0: if self.config.load_solution: raise RuntimeError( - 'A feasible solution was not found, so no solution can be loaded.' - 'Please set opt.config.load_solution=False and check ' - 'results.termination_condition and ' + 'A feasible solution was not found, so no solution can be loaded. ' + 'If using the appsi.solvers.Cbc interface, you can ' + 'set opt.config.load_solution=False. If using the environ.SolverFactory ' + 'interface, you can set opt.solve(model, load_solutions = False). ' + 'Then you can check results.termination_condition and ' 'results.best_feasible_objective before loading a solution.' ) results = Results() @@ -427,8 +440,8 @@ def _check_and_escape_options(): return results def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: if ( self._last_results_object is None or self._last_results_object.best_feasible_objective is None @@ -464,8 +477,8 @@ def get_duals(self, cons_to_load=None): return {c: self._dual_sol[c] for c in cons_to_load} def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: if ( self._last_results_object is None or self._last_results_object.termination_condition diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index f03bee6ecc5..10de981ce7d 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.common.tempfiles import TempfileManager from pyomo.contrib.appsi.base import ( PersistentSolver, @@ -11,11 +22,11 @@ import math from pyomo.common.collections import ComponentMap from typing import Optional, Sequence, NoReturn, List, Mapping, Dict -from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.constraint import _GeneralConstraintData -from pyomo.core.base.block import _BlockData -from pyomo.core.base.param import _ParamData -from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.core.base.var import VarData +from pyomo.core.base.constraint import ConstraintData +from pyomo.core.base.block import BlockData +from pyomo.core.base.param import ParamData +from pyomo.core.base.objective import ObjectiveData from pyomo.common.timing import HierarchicalTimer import sys import time @@ -168,34 +179,34 @@ def update_config(self): def set_instance(self, model): self._writer.set_instance(model) - def add_variables(self, variables: List[_GeneralVarData]): + def add_variables(self, variables: List[VarData]): self._writer.add_variables(variables) - def add_params(self, params: List[_ParamData]): + def add_params(self, params: List[ParamData]): self._writer.add_params(params) - def add_constraints(self, cons: List[_GeneralConstraintData]): + def add_constraints(self, cons: List[ConstraintData]): self._writer.add_constraints(cons) - def add_block(self, block: _BlockData): + def add_block(self, block: BlockData): self._writer.add_block(block) - def remove_variables(self, variables: List[_GeneralVarData]): + def remove_variables(self, variables: List[VarData]): self._writer.remove_variables(variables) - def remove_params(self, params: List[_ParamData]): + def remove_params(self, params: List[ParamData]): self._writer.remove_params(params) - def remove_constraints(self, cons: List[_GeneralConstraintData]): + def remove_constraints(self, cons: List[ConstraintData]): self._writer.remove_constraints(cons) - def remove_block(self, block: _BlockData): + def remove_block(self, block: BlockData): self._writer.remove_block(block) - def set_objective(self, obj: _GeneralObjectiveData): + def set_objective(self, obj: ObjectiveData): self._writer.set_objective(obj) - def update_variables(self, variables: List[_GeneralVarData]): + def update_variables(self, variables: List[VarData]): self._writer.update_variables(variables) def update_params(self): @@ -330,9 +341,11 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): if config.load_solution: if cpxprob.solution.get_solution_type() == cpxprob.solution.type.none: raise RuntimeError( - 'A feasible solution was not found, so no solution can be loades. ' - 'Please set opt.config.load_solution=False and check ' - 'results.termination_condition and ' + 'A feasible solution was not found, so no solution can be loaded. ' + 'If using the appsi.solvers.Cplex interface, you can ' + 'set opt.config.load_solution=False. If using the environ.SolverFactory ' + 'interface, you can set opt.solve(model, load_solutions = False). ' + 'Then you can check results.termination_condition and ' 'results.best_feasible_objective before loading a solution.' ) else: @@ -349,8 +362,8 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): return results def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: if ( self._cplex_model.solution.get_solution_type() == self._cplex_model.solution.type.none @@ -376,8 +389,8 @@ def get_primals( return res def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: if ( self._cplex_model.solution.get_solution_type() == self._cplex_model.solution.type.none @@ -427,8 +440,8 @@ def get_duals( return res def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: if ( self._cplex_model.solution.get_solution_type() == self._cplex_model.solution.type.none diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index a173c69abc6..2719ecc2a00 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from collections.abc import Iterable import logging import math @@ -12,10 +23,10 @@ from pyomo.common.config import ConfigValue, NonNegativeInt from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler -from pyomo.core.base.var import Var, _GeneralVarData -from pyomo.core.base.constraint import _GeneralConstraintData -from pyomo.core.base.sos import _SOSConstraintData -from pyomo.core.base.param import _ParamData +from pyomo.core.base.var import Var, VarData +from pyomo.core.base.constraint import ConstraintData +from pyomo.core.base.sos import SOSConstraintData +from pyomo.core.base.param import ParamData from pyomo.core.expr.numvalue import value, is_constant, is_fixed, native_numeric_types from pyomo.repn import generate_standard_repn from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression @@ -447,7 +458,7 @@ def _process_domain_and_bounds( return lb, ub, vtype - def _add_variables(self, variables: List[_GeneralVarData]): + def _add_variables(self, variables: List[VarData]): var_names = list() vtypes = list() lbs = list() @@ -478,7 +489,7 @@ def _add_variables(self, variables: List[_GeneralVarData]): self._vars_added_since_update.update(variables) self._needs_updated = True - def _add_params(self, params: List[_ParamData]): + def _add_params(self, params: List[ParamData]): pass def _reinit(self): @@ -568,7 +579,7 @@ def _get_expr_from_pyomo_expr(self, expr): mutable_quadratic_coefficients, ) - def _add_constraints(self, cons: List[_GeneralConstraintData]): + def _add_constraints(self, cons: List[ConstraintData]): for con in cons: conname = self._symbol_map.getSymbol(con, self._labeler) ( @@ -698,7 +709,7 @@ def _add_constraints(self, cons: List[_GeneralConstraintData]): self._constraints_added_since_update.update(cons) self._needs_updated = True - def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + def _add_sos_constraints(self, cons: List[SOSConstraintData]): for con in cons: conname = self._symbol_map.getSymbol(con, self._labeler) level = con.level @@ -724,7 +735,7 @@ def _add_sos_constraints(self, cons: List[_SOSConstraintData]): self._constraints_added_since_update.update(cons) self._needs_updated = True - def _remove_constraints(self, cons: List[_GeneralConstraintData]): + def _remove_constraints(self, cons: List[ConstraintData]): for con in cons: if con in self._constraints_added_since_update: self._update_gurobi_model() @@ -738,7 +749,7 @@ def _remove_constraints(self, cons: List[_GeneralConstraintData]): self._mutable_quadratic_helpers.pop(con, None) self._needs_updated = True - def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + def _remove_sos_constraints(self, cons: List[SOSConstraintData]): for con in cons: if con in self._constraints_added_since_update: self._update_gurobi_model() @@ -748,7 +759,7 @@ def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): del self._pyomo_sos_to_solver_sos_map[con] self._needs_updated = True - def _remove_variables(self, variables: List[_GeneralVarData]): + def _remove_variables(self, variables: List[VarData]): for var in variables: v_id = id(var) if var in self._vars_added_since_update: @@ -760,10 +771,10 @@ def _remove_variables(self, variables: List[_GeneralVarData]): self._mutable_bounds.pop(v_id, None) self._needs_updated = True - def _remove_params(self, params: List[_ParamData]): + def _remove_params(self, params: List[ParamData]): pass - def _update_variables(self, variables: List[_GeneralVarData]): + def _update_variables(self, variables: List[VarData]): for var in variables: var_id = id(var) if var_id not in self._pyomo_var_to_solver_var_map: @@ -935,9 +946,11 @@ def _postsolve(self, timer: HierarchicalTimer): self.load_vars() else: raise RuntimeError( - 'A feasible solution was not found, so no solution can be loaded.' - 'Please set opt.config.load_solution=False and check ' - 'results.termination_condition and ' + 'A feasible solution was not found, so no solution can be loaded. ' + 'If using the appsi.solvers.Gurobi interface, you can ' + 'set opt.config.load_solution=False. If using the environ.SolverFactory ' + 'interface, you can set opt.solve(model, load_solutions = False). ' + 'Then you can check results.termination_condition and ' 'results.best_feasible_objective before loading a solution.' ) timer.stop('load solution') @@ -1182,7 +1195,7 @@ def set_linear_constraint_attr(self, con, attr, val): Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be modified. attr: str @@ -1208,7 +1221,7 @@ def set_var_attr(self, var, attr, val): Parameters ---------- - var: pyomo.core.base.var._GeneralVarData + var: pyomo.core.base.var.VarData The pyomo var for which the corresponding gurobi var attribute should be modified. attr: str @@ -1243,7 +1256,7 @@ def get_var_attr(self, var, attr): Parameters ---------- - var: pyomo.core.base.var._GeneralVarData + var: pyomo.core.base.var.VarData The pyomo var for which the corresponding gurobi var attribute should be retrieved. attr: str @@ -1259,7 +1272,7 @@ def get_linear_constraint_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be retrieved. attr: str @@ -1275,7 +1288,7 @@ def get_sos_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.sos._SOSConstraintData + con: pyomo.core.base.sos.SOSConstraintData The pyomo SOS constraint for which the corresponding gurobi SOS constraint attribute should be retrieved. attr: str @@ -1291,7 +1304,7 @@ def get_quadratic_constraint_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be retrieved. attr: str @@ -1412,7 +1425,7 @@ def cbCut(self, con): Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The cut to add """ if not con.active: @@ -1497,7 +1510,7 @@ def cbLazy(self, con): """ Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The lazy constraint to add """ if not con.active: diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 3d498f9388e..c948444839d 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import logging from typing import List, Dict, Optional from pyomo.common.collections import ComponentMap @@ -9,10 +20,10 @@ from pyomo.common.log import LogStream from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base import SymbolMap -from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.constraint import _GeneralConstraintData -from pyomo.core.base.sos import _SOSConstraintData -from pyomo.core.base.param import _ParamData +from pyomo.core.base.var import VarData +from pyomo.core.base.constraint import ConstraintData +from pyomo.core.base.sos import SOSConstraintData +from pyomo.core.base.param import ParamData from pyomo.core.expr.numvalue import value, is_constant from pyomo.repn import generate_standard_repn from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression @@ -165,11 +176,19 @@ def available(self): return self.Availability.NotFound def version(self): - version = ( - highspy.HIGHS_VERSION_MAJOR, - highspy.HIGHS_VERSION_MINOR, - highspy.HIGHS_VERSION_PATCH, - ) + try: + version = ( + highspy.HIGHS_VERSION_MAJOR, + highspy.HIGHS_VERSION_MINOR, + highspy.HIGHS_VERSION_PATCH, + ) + except AttributeError: + # Older versions of Highs do not have the above attributes + # and the solver version can only be obtained by making + # an instance of the solver class. + tmp = highspy.Highs() + version = (tmp.versionMajor(), tmp.versionMinor(), tmp.versionPatch()) + return version @property @@ -297,7 +316,7 @@ def _process_domain_and_bounds(self, var_id): return lb, ub, vtype - def _add_variables(self, variables: List[_GeneralVarData]): + def _add_variables(self, variables: List[VarData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() @@ -324,7 +343,7 @@ def _add_variables(self, variables: List[_GeneralVarData]): len(vtypes), np.array(indices), np.array(vtypes) ) - def _add_params(self, params: List[_ParamData]): + def _add_params(self, params: List[ParamData]): pass def _reinit(self): @@ -365,7 +384,7 @@ def set_instance(self, model): if self._objective is None: self.set_objective(None) - def _add_constraints(self, cons: List[_GeneralConstraintData]): + def _add_constraints(self, cons: List[ConstraintData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() @@ -445,13 +464,13 @@ def _add_constraints(self, cons: List[_GeneralConstraintData]): np.array(coef_values, dtype=np.double), ) - def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + def _add_sos_constraints(self, cons: List[SOSConstraintData]): if cons: raise NotImplementedError( 'Highs interface does not support SOS constraints' ) - def _remove_constraints(self, cons: List[_GeneralConstraintData]): + def _remove_constraints(self, cons: List[ConstraintData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() @@ -476,13 +495,13 @@ def _remove_constraints(self, cons: List[_GeneralConstraintData]): {v: k for k, v in self._pyomo_con_to_solver_con_map.items()} ) - def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + def _remove_sos_constraints(self, cons: List[SOSConstraintData]): if cons: raise NotImplementedError( 'Highs interface does not support SOS constraints' ) - def _remove_variables(self, variables: List[_GeneralVarData]): + def _remove_variables(self, variables: List[VarData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() @@ -504,10 +523,10 @@ def _remove_variables(self, variables: List[_GeneralVarData]): self._pyomo_var_to_solver_var_map.clear() self._pyomo_var_to_solver_var_map.update(new_var_map) - def _remove_params(self, params: List[_ParamData]): + def _remove_params(self, params: List[ParamData]): pass - def _update_variables(self, variables: List[_GeneralVarData]): + def _update_variables(self, variables: List[VarData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() @@ -669,9 +688,11 @@ def _postsolve(self, timer: HierarchicalTimer): self.load_vars() else: raise RuntimeError( - 'A feasible solution was not found, so no solution can be loaded.' - 'Please set opt.config.load_solution=False and check ' - 'results.termination_condition and ' + 'A feasible solution was not found, so no solution can be loaded. ' + 'If using the appsi.solvers.Highs interface, you can ' + 'set opt.config.load_solution=False. If using the environ.SolverFactory ' + 'interface, you can set opt.solve(model, load_solutions = False). ' + 'Then you can check results.termination_condition and ' 'results.best_feasible_objective before loading a solution.' ) timer.stop('load solution') diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index d38a836a2ac..76cd204e36d 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.common.tempfiles import TempfileManager from pyomo.common.fileutils import Executable from pyomo.contrib.appsi.base import ( @@ -17,11 +28,11 @@ from pyomo.core.expr.numvalue import value from pyomo.core.expr.visitor import replace_expressions from typing import Optional, Sequence, NoReturn, List, Mapping -from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.constraint import _GeneralConstraintData -from pyomo.core.base.block import _BlockData -from pyomo.core.base.param import _ParamData -from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.core.base.var import VarData +from pyomo.core.base.constraint import ConstraintData +from pyomo.core.base.block import BlockData +from pyomo.core.base.param import ParamData +from pyomo.core.base.objective import ObjectiveData from pyomo.common.timing import HierarchicalTimer from pyomo.common.tee import TeeStream import sys @@ -136,6 +147,7 @@ def __init__(self, only_child_vars=False): self._primal_sol = ComponentMap() self._reduced_costs = ComponentMap() self._last_results_object: Optional[Results] = None + self._version_timeout = 2 def available(self): if self.config.executable.path() is None: @@ -147,7 +159,7 @@ def available(self): def version(self): results = subprocess.run( [str(self.config.executable), '--version'], - timeout=1, + timeout=self._version_timeout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, @@ -216,34 +228,34 @@ def set_instance(self, model): self._writer.config.symbolic_solver_labels = self.config.symbolic_solver_labels self._writer.set_instance(model) - def add_variables(self, variables: List[_GeneralVarData]): + def add_variables(self, variables: List[VarData]): self._writer.add_variables(variables) - def add_params(self, params: List[_ParamData]): + def add_params(self, params: List[ParamData]): self._writer.add_params(params) - def add_constraints(self, cons: List[_GeneralConstraintData]): + def add_constraints(self, cons: List[ConstraintData]): self._writer.add_constraints(cons) - def add_block(self, block: _BlockData): + def add_block(self, block: BlockData): self._writer.add_block(block) - def remove_variables(self, variables: List[_GeneralVarData]): + def remove_variables(self, variables: List[VarData]): self._writer.remove_variables(variables) - def remove_params(self, params: List[_ParamData]): + def remove_params(self, params: List[ParamData]): self._writer.remove_params(params) - def remove_constraints(self, cons: List[_GeneralConstraintData]): + def remove_constraints(self, cons: List[ConstraintData]): self._writer.remove_constraints(cons) - def remove_block(self, block: _BlockData): + def remove_block(self, block: BlockData): self._writer.remove_block(block) - def set_objective(self, obj: _GeneralObjectiveData): + def set_objective(self, obj: ObjectiveData): self._writer.set_objective(obj) - def update_variables(self, variables: List[_GeneralVarData]): + def update_variables(self, variables: List[VarData]): self._writer.update_variables(variables) def update_params(self): @@ -410,9 +422,11 @@ def _parse_sol(self): results.best_feasible_objective = value(obj_expr_evaluated) elif self.config.load_solution: raise RuntimeError( - 'A feasible solution was not found, so no solution can be loaded.' - 'Please set opt.config.load_solution=False and check ' - 'results.termination_condition and ' + 'A feasible solution was not found, so no solution can be loaded. ' + 'If using the appsi.solvers.Ipopt interface, you can ' + 'set opt.config.load_solution=False. If using the environ.SolverFactory ' + 'interface, you can set opt.solve(model, load_solutions = False). ' + 'Then you can check results.termination_condition and ' 'results.best_feasible_objective before loading a solution.' ) @@ -500,8 +514,8 @@ def _apply_solver(self, timer: HierarchicalTimer): return results def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: if ( self._last_results_object is None or self._last_results_object.best_feasible_objective is None @@ -520,9 +534,7 @@ def get_primals( res[v] = self._primal_sol[v] return res - def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ): + def get_duals(self, cons_to_load: Optional[Sequence[ConstraintData]] = None): if ( self._last_results_object is None or self._last_results_object.termination_condition @@ -539,8 +551,8 @@ def get_duals( return {c: self._dual_sol[c] for c in cons_to_load} def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: if ( self._last_results_object is None or self._last_results_object.termination_condition diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py new file mode 100644 index 00000000000..e52130061f7 --- /dev/null +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -0,0 +1,568 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from collections import namedtuple +import logging +import math +import sys +from typing import Optional, List, Dict + +from pyomo.contrib.appsi.base import ( + PersistentSolver, + Results, + TerminationCondition, + MIPSolverConfig, + PersistentBase, + PersistentSolutionLoader, +) +from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available +from pyomo.common.collections import ComponentMap +from pyomo.common.config import ( + ConfigValue, + ConfigDict, + NonNegativeInt, + NonNegativeFloat, +) +from pyomo.common.dependencies import attempt_import +from pyomo.common.errors import PyomoException +from pyomo.common.log import LogStream +from pyomo.common.tee import capture_output, TeeStream +from pyomo.common.timing import HierarchicalTimer +from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler +from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.expression import ScalarExpression +from pyomo.core.base.param import _ParamData +from pyomo.core.base.sos import _SOSConstraintData +from pyomo.core.base.var import Var, ScalarVar, _GeneralVarData +import pyomo.core.expr.expr_common as common +import pyomo.core.expr as EXPR +from pyomo.core.expr.numvalue import ( + value, + is_constant, + is_fixed, + native_numeric_types, + native_types, + nonpyomo_leaf_types, +) +from pyomo.core.kernel.objective import minimize, maximize +from pyomo.core.staleflag import StaleFlagManager +from pyomo.repn.util import valid_expr_ctypes_minlp + + +def _import_SolverModel(): + try: + from . import maingo_solvermodel + except ImportError: + raise + return maingo_solvermodel + + +maingo_solvermodel, solvermodel_available = attempt_import( + "maingo_solvermodel", importer=_import_SolverModel +) + +MaingoVar = namedtuple("MaingoVar", "type name lb ub init") + +logger = logging.getLogger(__name__) + + +def _import_maingopy(): + try: + import maingopy + except ImportError: + MAiNGO._available = MAiNGO.Availability.NotFound + raise + return maingopy + + +maingopy, maingopy_available = attempt_import("maingopy", importer=_import_maingopy) + + +class MAiNGOConfig(MIPSolverConfig): + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super(MAiNGOConfig, self).__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + self.tolerances: ConfigDict = self.declare( + 'tolerances', ConfigDict(implicit=True) + ) + + self.tolerances.epsilonA: Optional[float] = self.tolerances.declare( + 'epsilonA', + ConfigValue( + domain=NonNegativeFloat, + default=1e-5, + description="Absolute optimality tolerance", + ), + ) + self.tolerances.epsilonR: Optional[float] = self.tolerances.declare( + 'epsilonR', + ConfigValue( + domain=NonNegativeFloat, + default=1e-5, + description="Relative optimality tolerance", + ), + ) + self.tolerances.deltaEq: Optional[float] = self.tolerances.declare( + 'deltaEq', + ConfigValue( + domain=NonNegativeFloat, default=1e-6, description="Equality tolerance" + ), + ) + + self.tolerances.deltaIneq: Optional[float] = self.tolerances.declare( + 'deltaIneq', + ConfigValue( + domain=NonNegativeFloat, + default=1e-6, + description="Inequality tolerance", + ), + ) + self.declare("logfile", ConfigValue(domain=str, default="")) + self.declare("solver_output_logger", ConfigValue(default=logger)) + self.declare( + "log_level", ConfigValue(domain=NonNegativeInt, default=logging.INFO) + ) + + +class MAiNGOSolutionLoader(PersistentSolutionLoader): + def load_vars(self, vars_to_load=None): + self._assert_solution_still_valid() + self._solver.load_vars(vars_to_load=vars_to_load) + + def get_primals(self, vars_to_load=None): + self._assert_solution_still_valid() + return self._solver.get_primals(vars_to_load=vars_to_load) + + +class MAiNGOResults(Results): + def __init__(self, solver): + super(MAiNGOResults, self).__init__() + self.wallclock_time = None + self.cpu_time = None + self.globally_optimal = None + self.solution_loader = MAiNGOSolutionLoader(solver=solver) + + +class MAiNGO(PersistentBase, PersistentSolver): + """ + Interface to MAiNGO + """ + + _available = None + + def __init__(self, only_child_vars=False): + super(MAiNGO, self).__init__(only_child_vars=only_child_vars) + self._config = MAiNGOConfig() + self._solver_options = dict() + self._solver_model = None + self._mymaingo = None + self._symbol_map = SymbolMap() + self._labeler = None + self._maingo_vars = [] + self._objective = None + self._cons = [] + self._pyomo_var_to_solver_var_id_map = dict() + self._last_results_object: Optional[MAiNGOResults] = None + + def available(self): + if not maingopy_available: + return self.Availability.NotFound + self._available = True + return self._available + + def version(self): + import pkg_resources + + version = pkg_resources.get_distribution('maingopy').version + + return tuple(int(k) for k in version.split('.')) + + @property + def config(self) -> MAiNGOConfig: + return self._config + + @config.setter + def config(self, val: MAiNGOConfig): + self._config = val + + @property + def maingo_options(self): + """ + A dictionary mapping solver options to values for those options. These + are solver specific. + + Returns + ------- + dict + A dictionary mapping solver options to values for those options + """ + return self._solver_options + + @maingo_options.setter + def maingo_options(self, val: Dict): + self._solver_options = val + + @property + def symbol_map(self): + return self._symbol_map + + def _solve(self, timer: HierarchicalTimer): + ostreams = [ + LogStream( + level=self.config.log_level, logger=self.config.solver_output_logger + ) + ] + if self.config.stream_solver: + ostreams.append(sys.stdout) + + with TeeStream(*ostreams) as t: + with capture_output(output=t.STDOUT, capture_fd=False): + config = self.config + options = self.maingo_options + + self._mymaingo = maingopy.MAiNGO(self._solver_model) + + self._mymaingo.set_option("loggingDestination", 2) + self._mymaingo.set_log_file_name(config.logfile) + self._mymaingo.set_option("epsilonA", config.tolerances.epsilonA) + self._mymaingo.set_option("epsilonR", config.tolerances.epsilonR) + self._mymaingo.set_option("deltaEq", config.tolerances.deltaEq) + self._mymaingo.set_option("deltaIneq", config.tolerances.deltaIneq) + + if config.time_limit is not None: + self._mymaingo.set_option("maxTime", config.time_limit) + if config.mip_gap is not None: + self._mymaingo.set_option("epsilonR", config.mip_gap) + for key, option in options.items(): + self._mymaingo.set_option(key, option) + + timer.start("MAiNGO solve") + self._mymaingo.solve() + timer.stop("MAiNGO solve") + + return self._postsolve(timer) + + def solve(self, model, timer: HierarchicalTimer = None): + StaleFlagManager.mark_all_as_stale() + + if self._last_results_object is not None: + self._last_results_object.solution_loader.invalidate() + if timer is None: + timer = HierarchicalTimer() + if model is not self._model: + timer.start("set_instance") + self.set_instance(model) + timer.stop("set_instance") + else: + timer.start("Update") + self.update(timer=timer) + timer.stop("Update") + res = self._solve(timer) + self._last_results_object = res + if self.config.report_timing: + logger.info("\n" + str(timer)) + return res + + def _process_domain_and_bounds(self, var): + _v, _lb, _ub, _fixed, _domain_interval, _value = self._vars[id(var)] + lb, ub, step = _domain_interval + + if _fixed: + lb = _value + ub = _value + else: + if lb is None and _lb is None: + logger.warning( + "No lower bound for variable " + + var.getname() + + " set. Using -1e10 instead. Please consider setting a valid lower bound." + ) + if ub is None and _ub is None: + logger.warning( + "No upper bound for variable " + + var.getname() + + " set. Using +1e10 instead. Please consider setting a valid upper bound." + ) + + if _lb is None: + _lb = -1e10 + if _ub is None: + _ub = 1e10 + if lb is None: + lb = -1e10 + if ub is None: + ub = 1e10 + + lb = max(value(_lb), lb) + ub = min(value(_ub), ub) + + if step == 0: + vtype = maingopy.VT_CONTINUOUS + elif step == 1: + if lb == 0 and ub == 1: + vtype = maingopy.VT_BINARY + else: + vtype = maingopy.VT_INTEGER + else: + raise ValueError( + f"Unrecognized domain step: {step} (should be either 0 or 1)" + ) + + return lb, ub, vtype + + def _add_variables(self, variables: List[_GeneralVarData]): + for var in variables: + varname = self._symbol_map.getSymbol(var, self._labeler) + lb, ub, vtype = self._process_domain_and_bounds(var) + self._maingo_vars.append( + MaingoVar(name=varname, type=vtype, lb=lb, ub=ub, init=var.value) + ) + self._pyomo_var_to_solver_var_id_map[id(var)] = len(self._maingo_vars) - 1 + + def _add_params(self, params: List[_ParamData]): + pass + + def _reinit(self): + saved_config = self.config + saved_options = self.maingo_options + saved_update_config = self.update_config + self.__init__(only_child_vars=self._only_child_vars) + self.config = saved_config + self.maingo_options = saved_options + self.update_config = saved_update_config + + def set_instance(self, model): + if self._last_results_object is not None: + self._last_results_object.solution_loader.invalidate() + if not self.available(): + c = self.__class__ + raise PyomoException( + f"Solver {c.__module__}.{c.__qualname__} is not available " + f"({self.available()})." + ) + self._reinit() + self._model = model + if self.use_extensions and cmodel_available: + self._expr_types = cmodel.PyomoExprTypes() + + if self.config.symbolic_solver_labels: + self._labeler = TextLabeler() + else: + self._labeler = NumericLabeler("x") + + self.add_block(model) + + self._solver_model = maingo_solvermodel.SolverModel( + var_list=self._maingo_vars, + con_list=self._cons, + objective=self._objective, + idmap=self._pyomo_var_to_solver_var_id_map, + logger=logger, + ) + + def _add_constraints(self, cons: List[_GeneralConstraintData]): + self._cons += cons + + def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + if len(cons) >= 1: + raise NotImplementedError( + "MAiNGO does not currently support SOS constraints." + ) + pass + + def _remove_constraints(self, cons: List[_GeneralConstraintData]): + for con in cons: + self._cons.remove(con) + + def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + if len(cons) >= 1: + raise NotImplementedError( + "MAiNGO does not currently support SOS constraints." + ) + pass + + def _remove_variables(self, variables: List[_GeneralVarData]): + removed_maingo_vars = [] + for var in variables: + varname = self._symbol_map.getSymbol(var, self._labeler) + del self._maingo_vars[self._pyomo_var_to_solver_var_id_map[id(var)]] + removed_maingo_vars += [self._pyomo_var_to_solver_var_id_map[id(var)]] + del self._pyomo_var_to_solver_var_id_map[id(var)] + + # Update _pyomo_var_to_solver_var_id_map to account for removed variables + for pyomo_var, maingo_var_id in self._pyomo_var_to_solver_var_id_map.items(): + num_removed = 0 + for removed_var in removed_maingo_vars: + if removed_var <= maingo_var_id: + num_removed += 1 + self._pyomo_var_to_solver_var_id_map[pyomo_var] = ( + maingo_var_id - num_removed + ) + + def _remove_params(self, params: List[_ParamData]): + pass + + def _update_variables(self, variables: List[_GeneralVarData]): + for var in variables: + if id(var) not in self._pyomo_var_to_solver_var_id_map: + raise ValueError( + 'The Var provided to update_var needs to be added first: {0}'.format( + var + ) + ) + lb, ub, vtype = self._process_domain_and_bounds(var) + self._maingo_vars[self._pyomo_var_to_solver_var_id_map[id(var)]] = ( + MaingoVar(name=var.name, type=vtype, lb=lb, ub=ub, init=var.value) + ) + + def update_params(self): + vars = [var[0] for var in self._vars.values()] + self._update_variables(vars) + + def _set_objective(self, obj): + + if not obj.sense in {minimize, maximize}: + raise ValueError("Objective sense is not recognized: {0}".format(obj.sense)) + self._objective = obj + + def _postsolve(self, timer: HierarchicalTimer): + config = self.config + + mprob = self._mymaingo + status = mprob.get_status() + results = MAiNGOResults(solver=self) + results.wallclock_time = mprob.get_wallclock_solution_time() + results.cpu_time = mprob.get_cpu_solution_time() + + if status in {maingopy.GLOBALLY_OPTIMAL, maingopy.FEASIBLE_POINT}: + results.termination_condition = TerminationCondition.optimal + results.globally_optimal = True + if status == maingopy.FEASIBLE_POINT: + results.globally_optimal = False + logger.warning( + "MAiNGO found a feasible solution but did not prove its global optimality." + ) + elif status == maingopy.INFEASIBLE: + results.termination_condition = TerminationCondition.infeasible + else: + results.termination_condition = TerminationCondition.unknown + + results.best_feasible_objective = None + results.best_objective_bound = None + if self._objective is not None: + try: + if self._objective.sense == maximize: + results.best_feasible_objective = -mprob.get_objective_value() + else: + results.best_feasible_objective = mprob.get_objective_value() + except: + results.best_feasible_objective = None + try: + if self._objective.sense == maximize: + results.best_objective_bound = -mprob.get_final_LBD() + else: + results.best_objective_bound = mprob.get_final_LBD() + except: + if self._objective.sense == maximize: + results.best_objective_bound = math.inf + else: + results.best_objective_bound = -math.inf + + if results.best_feasible_objective is not None and not math.isfinite( + results.best_feasible_objective + ): + results.best_feasible_objective = None + + timer.start("load solution") + if config.load_solution: + if results.termination_condition is TerminationCondition.optimal: + if not results.globally_optimal: + logger.warning( + "Loading a feasible but suboptimal solution. " + "Please set load_solution=False and check " + "results.termination_condition and " + "results.found_feasible_solution() before loading a solution." + ) + self.load_vars() + else: + raise RuntimeError( + "A feasible solution was not found, so no solution can be loaded." + "Please set opt.config.load_solution=False and check " + "results.termination_condition and " + "results.best_feasible_objective before loading a solution." + ) + timer.stop("load solution") + + return results + + def load_vars(self, vars_to_load=None): + for v, val in self.get_primals(vars_to_load=vars_to_load).items(): + v.set_value(val, skip_validation=True) + StaleFlagManager.mark_all_as_stale(delayed=True) + + def get_primals(self, vars_to_load=None): + if not self._mymaingo.get_status() in { + maingopy.GLOBALLY_OPTIMAL, + maingopy.FEASIBLE_POINT, + }: + raise RuntimeError( + "Solver does not currently have a valid solution." + "Please check the termination condition." + ) + + var_id_map = self._pyomo_var_to_solver_var_id_map + ref_vars = self._referenced_variables + if vars_to_load is None: + vars_to_load = var_id_map.keys() + else: + vars_to_load = [id(v) for v in vars_to_load] + + maingo_var_ids_to_load = [ + var_id_map[pyomo_var_id] for pyomo_var_id in vars_to_load + ] + + solution_point = self._mymaingo.get_solution_point() + vals = [solution_point[var_id] for var_id in maingo_var_ids_to_load] + + res = ComponentMap() + for var_id, val in zip(vars_to_load, vals): + using_cons, using_sos, using_obj = ref_vars[var_id] + if using_cons or using_sos or (using_obj is not None): + res[self._vars[var_id][0]] = val + return res + + def get_reduced_costs(self, vars_to_load=None): + raise ValueError("MAiNGO does not support returning Reduced Costs") + + def get_duals(self, cons_to_load=None): + raise ValueError("MAiNGO does not support returning Duals") + + def update(self, timer: HierarchicalTimer = None): + super(MAiNGO, self).update(timer=timer) + self._solver_model = maingo_solvermodel.SolverModel( + var_list=self._maingo_vars, + con_list=self._cons, + objective=self._objective, + idmap=self._pyomo_var_to_solver_var_id_map, + logger=logger, + ) diff --git a/pyomo/contrib/appsi/solvers/maingo_solvermodel.py b/pyomo/contrib/appsi/solvers/maingo_solvermodel.py new file mode 100644 index 00000000000..ca746c4a9b7 --- /dev/null +++ b/pyomo/contrib/appsi/solvers/maingo_solvermodel.py @@ -0,0 +1,291 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import math + +from pyomo.common.dependencies import attempt_import +from pyomo.core.base.var import ScalarVar +from pyomo.core.base.expression import ScalarExpression +import pyomo.core.expr.expr_common as common +import pyomo.core.expr as EXPR +from pyomo.core.expr.numvalue import ( + value, + is_constant, + is_fixed, + native_numeric_types, + native_types, + nonpyomo_leaf_types, +) +from pyomo.core.kernel.objective import minimize, maximize +from pyomo.repn.util import valid_expr_ctypes_minlp + + +def _import_maingopy(): + try: + import maingopy + except ImportError: + raise + return maingopy + + +maingopy, maingopy_available = attempt_import("maingopy", importer=_import_maingopy) + +_plusMinusOne = {1, -1} + +LEFT_TO_RIGHT = common.OperatorAssociativity.LEFT_TO_RIGHT +RIGHT_TO_LEFT = common.OperatorAssociativity.RIGHT_TO_LEFT + + +class ToMAiNGOVisitor(EXPR.ExpressionValueVisitor): + def __init__(self, variables, idmap): + super(ToMAiNGOVisitor, self).__init__() + self.variables = variables + self.idmap = idmap + self._pyomo_func_to_maingo_func = { + "log": maingopy.log, + "log10": ToMAiNGOVisitor.maingo_log10, + "sin": maingopy.sin, + "cos": maingopy.cos, + "tan": maingopy.tan, + "cosh": maingopy.cosh, + "sinh": maingopy.sinh, + "tanh": maingopy.tanh, + "asin": maingopy.asin, + "acos": maingopy.acos, + "atan": maingopy.atan, + "exp": maingopy.exp, + "sqrt": maingopy.sqrt, + "asinh": ToMAiNGOVisitor.maingo_asinh, + "acosh": ToMAiNGOVisitor.maingo_acosh, + "atanh": ToMAiNGOVisitor.maingo_atanh, + } + + @classmethod + def maingo_log10(cls, x): + return maingopy.log(x) / math.log(10) + + @classmethod + def maingo_asinh(cls, x): + return maingopy.log(x + maingopy.sqrt(maingopy.pow(x, 2) + 1)) + + @classmethod + def maingo_acosh(cls, x): + return maingopy.log(x + maingopy.sqrt(maingopy.pow(x, 2) - 1)) + + @classmethod + def maingo_atanh(cls, x): + return 0.5 * maingopy.log(x + 1) - 0.5 * maingopy.log(1 - x) + + def visit(self, node, values): + """Visit nodes that have been expanded""" + for i, val in enumerate(values): + arg = node._args_[i] + + if arg is None: + values[i] = "Undefined" + elif arg.__class__ in native_numeric_types: + pass + elif arg.__class__ in nonpyomo_leaf_types: + values[i] = val + else: + parens = False + if arg.is_expression_type() and node.PRECEDENCE is not None: + if arg.PRECEDENCE is None: + pass + elif node.PRECEDENCE < arg.PRECEDENCE: + parens = True + elif node.PRECEDENCE == arg.PRECEDENCE: + if i == 0: + parens = node.ASSOCIATIVITY != LEFT_TO_RIGHT + elif i == len(node._args_) - 1: + parens = node.ASSOCIATIVITY != RIGHT_TO_LEFT + else: + parens = True + if parens: + values[i] = val + + if node.__class__ in EXPR.NPV_expression_types: + return value(node) + + if node.__class__ in {EXPR.ProductExpression, EXPR.MonomialTermExpression}: + return values[0] * values[1] + + if node.__class__ in {EXPR.SumExpression}: + return sum(values) + + if node.__class__ in {EXPR.PowExpression}: + return maingopy.pow(values[0], values[1]) + + if node.__class__ in {EXPR.DivisionExpression}: + return values[0] / values[1] + + if node.__class__ in {EXPR.NegationExpression}: + return -values[0] + + if node.__class__ in {EXPR.AbsExpression}: + return maingopy.abs(values[0]) + + if node.__class__ in {EXPR.UnaryFunctionExpression}: + pyomo_func = node.getname() + maingo_func = self._pyomo_func_to_maingo_func[pyomo_func] + return maingo_func(values[0]) + + if node.__class__ in {ScalarExpression}: + return values[0] + + raise ValueError(f"Unknown function expression encountered: {node.getname()}") + + def visiting_potential_leaf(self, node): + """ + Visiting a potential leaf. + + Return True if the node is not expanded. + """ + if node.__class__ in native_types: + return True, node + + if node.is_expression_type(): + if node.__class__ is EXPR.MonomialTermExpression: + return True, self._monomial_to_maingo(node) + if node.__class__ is EXPR.LinearExpression: + return True, self._linear_to_maingo(node) + return False, None + + if node.is_component_type(): + if node.ctype not in valid_expr_ctypes_minlp: + # Make sure all components in active constraints + # are basic ctypes we know how to deal with. + raise RuntimeError( + "Unallowable component '%s' of type %s found in an active " + "constraint or objective.\nMAiNGO cannot export " + "expressions with this component type." + % (node.name, node.ctype.__name__) + ) + + if node.is_fixed(): + return True, node() + else: + assert node.is_variable_type() + maingo_var_id = self.idmap[id(node)] + maingo_var = self.variables[maingo_var_id] + return True, maingo_var + + def _monomial_to_maingo(self, node): + const, var = node.args + if const.__class__ not in native_types: + const = value(const) + if var.is_fixed(): + return const * var.value + if not const: + return 0 + maingo_var = self._var_to_maingo(var) + if const in _plusMinusOne: + if const < 0: + return -maingo_var + else: + return maingo_var + return const * maingo_var + + def _var_to_maingo(self, var): + maingo_var_id = self.idmap[id(var)] + maingo_var = self.variables[maingo_var_id] + return maingo_var + + def _linear_to_maingo(self, node): + values = [ + ( + self._monomial_to_maingo(arg) + if (arg.__class__ is EXPR.MonomialTermExpression) + else ( + value(arg) + if arg.__class__ in native_numeric_types + else ( + self._var_to_maingo(arg) + if arg.is_variable_type() + else value(arg) + ) + ) + ) + for arg in node.args + ] + return sum(values) + + +class SolverModel(maingopy.MAiNGOmodel): + def __init__(self, var_list, objective, con_list, idmap, logger): + maingopy.MAiNGOmodel.__init__(self) + self._var_list = var_list + self._con_list = con_list + self._objective = objective + self._idmap = idmap + self._logger = logger + self._no_objective = False + + if self._objective is None: + self._logger.warning("No objective given, setting a dummy objective of 1.") + self._no_objective = True + + def build_maingo_objective(self, obj, visitor): + if self._no_objective: + return visitor.variables[-1] + maingo_obj = visitor.dfs_postorder_stack(obj.expr) + if obj.sense == maximize: + return -1 * maingo_obj + return maingo_obj + + def build_maingo_constraints(self, cons, visitor): + eqs = [] + ineqs = [] + for con in cons: + if con.equality: + eqs += [visitor.dfs_postorder_stack(con.body - con.lower)] + elif con.has_ub() and con.has_lb(): + ineqs += [visitor.dfs_postorder_stack(con.body - con.upper)] + ineqs += [visitor.dfs_postorder_stack(con.lower - con.body)] + elif con.has_ub(): + ineqs += [visitor.dfs_postorder_stack(con.body - con.upper)] + elif con.has_lb(): + ineqs += [visitor.dfs_postorder_stack(con.lower - con.body)] + else: + raise ValueError( + "Constraint does not have a lower " + "or an upper bound: {0} \n".format(con) + ) + return eqs, ineqs + + def get_variables(self): + vars = [ + maingopy.OptimizationVariable( + maingopy.Bounds(var.lb, var.ub), var.type, var.name + ) + for var in self._var_list + ] + if self._no_objective: + vars += [maingopy.OptimizationVariable(maingopy.Bounds(1, 1), "dummy_obj")] + return vars + + def get_initial_point(self): + initial = [ + var.init if not var.init is None else (var.lb + var.ub) / 2.0 + for var in self._var_list + ] + if self._no_objective: + initial += [1] + return initial + + def evaluate(self, maingo_vars): + visitor = ToMAiNGOVisitor(maingo_vars, self._idmap) + result = maingopy.EvaluationContainer() + result.objective = self.build_maingo_objective(self._objective, visitor) + eqs, ineqs = self.build_maingo_constraints(self._con_list, visitor) + result.eq = eqs + result.ineq = ineqs + return result diff --git a/pyomo/contrib/appsi/solvers/tests/__init__.py b/pyomo/contrib/appsi/solvers/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/appsi/solvers/tests/__init__.py +++ b/pyomo/contrib/appsi/solvers/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py index b032f5c827e..2f674a2eb6a 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.common.errors import PyomoException import pyomo.common.unittest as unittest import pyomo.environ as pe diff --git a/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py index 6451db18087..b26f45ff2cc 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import subprocess import sys diff --git a/pyomo/contrib/appsi/solvers/tests/test_ipopt_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_ipopt_persistent.py index 6b86deaa535..8e6473a6b01 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_ipopt_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_ipopt_persistent.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pe import pyomo.common.unittest as unittest from pyomo.contrib.appsi.cmodel import cmodel_available diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 33f6877aaf8..67088297cf4 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pe from pyomo.common.dependencies import attempt_import import pyomo.common.unittest as unittest @@ -6,7 +17,7 @@ parameterized = parameterized.parameterized from pyomo.contrib.appsi.base import TerminationCondition, Results, PersistentSolver from pyomo.contrib.appsi.cmodel import cmodel_available -from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Cplex, Cbc, Highs +from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Cplex, Cbc, Highs, MAiNGO from typing import Type from pyomo.core.expr.numeric_expr import LinearExpression import os @@ -25,11 +36,23 @@ ('cplex', Cplex), ('cbc', Cbc), ('highs', Highs), + ('maingo', MAiNGO), +] +mip_solvers = [ + ('gurobi', Gurobi), + ('cplex', Cplex), + ('cbc', Cbc), + ('highs', Highs), + ('maingo', MAiNGO), +] +nlp_solvers = [('ipopt', Ipopt), ('maingo', MAiNGO)] +qcp_solvers = [ + ('gurobi', Gurobi), + ('ipopt', Ipopt), + ('cplex', Cplex), + ('maingo', MAiNGO), ] -mip_solvers = [('gurobi', Gurobi), ('cplex', Cplex), ('cbc', Cbc), ('highs', Highs)] -nlp_solvers = [('ipopt', Ipopt)] -qcp_solvers = [('gurobi', Gurobi), ('ipopt', Ipopt), ('cplex', Cplex)] -miqcqp_solvers = [('gurobi', Gurobi), ('cplex', Cplex)] +miqcqp_solvers = [('gurobi', Gurobi), ('cplex', Cplex), ('maingo', MAiNGO)] only_child_vars_options = [True, False] @@ -161,14 +184,16 @@ def test_range_constraint( res = opt.solve(m) self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, -1) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c], 1) + if opt_class != MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c], 1) m.obj.sense = pe.maximize res = opt.solve(m) self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 1) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c], 1) + if opt_class != MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c], 1) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_reduced_costs( @@ -185,9 +210,10 @@ def test_reduced_costs( self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, -1) self.assertAlmostEqual(m.y.value, -2) - rc = opt.get_reduced_costs() - self.assertAlmostEqual(rc[m.x], 3) - self.assertAlmostEqual(rc[m.y], 4) + if opt_class != MAiNGO: + rc = opt.get_reduced_costs() + self.assertAlmostEqual(rc[m.x], 3) + self.assertAlmostEqual(rc[m.y], 4) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_reduced_costs2( @@ -202,14 +228,16 @@ def test_reduced_costs2( res = opt.solve(m) self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, -1) - rc = opt.get_reduced_costs() - self.assertAlmostEqual(rc[m.x], 1) + if opt_class != MAiNGO: + rc = opt.get_reduced_costs() + self.assertAlmostEqual(rc[m.x], 1) m.obj.sense = pe.maximize res = opt.solve(m) self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 1) - rc = opt.get_reduced_costs() - self.assertAlmostEqual(rc[m.x], 1) + if opt_class != MAiNGO: + rc = opt.get_reduced_costs() + self.assertAlmostEqual(rc[m.x], 1) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_param_changes( @@ -241,9 +269,10 @@ def test_param_changes( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) - self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) + if opt_class != MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_immutable_param( @@ -279,9 +308,10 @@ def test_immutable_param( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) - self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) + if opt_class != MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_equality( @@ -313,9 +343,10 @@ def test_equality( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) - self.assertAlmostEqual(duals[m.c2], -a1 / (a2 - a1)) + if opt_class != MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], -a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_linear_expression( @@ -383,9 +414,10 @@ def test_no_objective( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertEqual(res.best_feasible_objective, None) self.assertEqual(res.best_objective_bound, None) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c1], 0) - self.assertAlmostEqual(duals[m.c2], 0) + if opt_class != MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c1], 0) + self.assertAlmostEqual(duals[m.c2], 0) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_add_remove_cons( @@ -412,9 +444,10 @@ def test_add_remove_cons( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) - self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) + if opt_class != MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) m.c3 = pe.Constraint(expr=m.y >= a3 * m.x + b3) res = opt.solve(m) @@ -423,10 +456,11 @@ def test_add_remove_cons( self.assertAlmostEqual(m.y.value, a1 * (b3 - b1) / (a1 - a3) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a3 - a1))) - self.assertAlmostEqual(duals[m.c2], 0) - self.assertAlmostEqual(duals[m.c3], a1 / (a3 - a1)) + if opt_class != MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a3 - a1))) + self.assertAlmostEqual(duals[m.c2], 0) + self.assertAlmostEqual(duals[m.c3], a1 / (a3 - a1)) del m.c3 res = opt.solve(m) @@ -435,9 +469,10 @@ def test_add_remove_cons( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) - self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) + if opt_class != MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_results_infeasible( @@ -476,14 +511,15 @@ def test_results_infeasible( RuntimeError, '.*does not currently have a valid solution.*' ): res.solution_loader.load_vars() - with self.assertRaisesRegex( - RuntimeError, '.*does not currently have valid duals.*' - ): - res.solution_loader.get_duals() - with self.assertRaisesRegex( - RuntimeError, '.*does not currently have valid reduced costs.*' - ): - res.solution_loader.get_reduced_costs() + if opt_class != MAiNGO: + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have valid duals.*' + ): + res.solution_loader.get_duals() + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have valid reduced costs.*' + ): + res.solution_loader.get_reduced_costs() @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_duals(self, name: str, opt_class: Type[PersistentSolver], only_child_vars): @@ -500,13 +536,14 @@ def test_duals(self, name: str, opt_class: Type[PersistentSolver], only_child_va res = opt.solve(m) self.assertAlmostEqual(m.x.value, 1) self.assertAlmostEqual(m.y.value, 1) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c1], 0.5) - self.assertAlmostEqual(duals[m.c2], 0.5) + if opt_class != MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c1], 0.5) + self.assertAlmostEqual(duals[m.c2], 0.5) - duals = opt.get_duals(cons_to_load=[m.c1]) - self.assertAlmostEqual(duals[m.c1], 0.5) - self.assertNotIn(m.c2, duals) + duals = opt.get_duals(cons_to_load=[m.c1]) + self.assertAlmostEqual(duals[m.c1], 0.5) + self.assertNotIn(m.c2, duals) @parameterized.expand(input=_load_tests(qcp_solvers, only_child_vars_options)) def test_mutable_quadratic_coefficient( @@ -661,7 +698,7 @@ def test_fixed_vars_4( ): opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = True - if not opt.available(): + if not opt.available() or opt_class == MAiNGO: raise unittest.SkipTest m = pe.ConcreteModel() m.x = pe.Var() @@ -754,17 +791,19 @@ def test_mutable_param_with_range( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1, 6) self.assertAlmostEqual(res.best_feasible_objective, m.y.value, 6) self.assertTrue(res.best_objective_bound <= m.y.value + 1e-12) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) - self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) + if opt_class != MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) + self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) else: self.assertAlmostEqual(m.x.value, (c2 - c1) / (a1 - a2), 6) self.assertAlmostEqual(m.y.value, a1 * (c2 - c1) / (a1 - a2) + c1, 6) self.assertAlmostEqual(res.best_feasible_objective, m.y.value, 6) self.assertTrue(res.best_objective_bound >= m.y.value - 1e-12) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) - self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) + if opt_class != MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) + self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_add_and_remove_vars( @@ -826,13 +865,13 @@ def test_exp(self, name: str, opt_class: Type[PersistentSolver], only_child_vars m.obj = pe.Objective(expr=m.x**2 + m.y**2) m.c1 = pe.Constraint(expr=m.y >= pe.exp(m.x)) res = opt.solve(m) - self.assertAlmostEqual(m.x.value, -0.42630274815985264) - self.assertAlmostEqual(m.y.value, 0.6529186341994245) + self.assertAlmostEqual(m.x.value, -0.42630274815985264, 6) + self.assertAlmostEqual(m.y.value, 0.6529186341994245, 6) @parameterized.expand(input=_load_tests(nlp_solvers, only_child_vars_options)) def test_log(self, name: str, opt_class: Type[PersistentSolver], only_child_vars): opt = opt_class(only_child_vars=only_child_vars) - if not opt.available(): + if not opt.available() or opt_class == MAiNGO: raise unittest.SkipTest m = pe.ConcreteModel() m.x = pe.Var(initialize=1) @@ -907,6 +946,27 @@ def test_bounds_with_params( res = opt.solve(m) self.assertAlmostEqual(m.y.value, 3) + @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) + def test_bounds_with_immutable_params( + self, name: str, opt_class: Type[PersistentSolver], only_child_vars + ): + # this test is for issue #2574 + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.p = pe.Param(mutable=False, initialize=1) + m.q = pe.Param([1, 2], mutable=False, initialize=10) + m.y = pe.Var() + m.y.setlb(m.p) + m.y.setub(m.q[1]) + m.obj = pe.Objective(expr=m.y) + res = opt.solve(m) + self.assertAlmostEqual(m.y.value, 1) + m.y.setlb(m.q[2]) + res = opt.solve(m) + self.assertAlmostEqual(m.y.value, 10) + @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_solution_loader( self, name: str, opt_class: Type[PersistentSolver], only_child_vars @@ -941,31 +1001,32 @@ def test_solution_loader( self.assertNotIn(m.x, primals) self.assertIn(m.y, primals) self.assertAlmostEqual(primals[m.y], 1) - reduced_costs = res.solution_loader.get_reduced_costs() - self.assertIn(m.x, reduced_costs) - self.assertIn(m.y, reduced_costs) - self.assertAlmostEqual(reduced_costs[m.x], 1) - self.assertAlmostEqual(reduced_costs[m.y], 0) - reduced_costs = res.solution_loader.get_reduced_costs([m.y]) - self.assertNotIn(m.x, reduced_costs) - self.assertIn(m.y, reduced_costs) - self.assertAlmostEqual(reduced_costs[m.y], 0) - duals = res.solution_loader.get_duals() - self.assertIn(m.c1, duals) - self.assertIn(m.c2, duals) - self.assertAlmostEqual(duals[m.c1], 1) - self.assertAlmostEqual(duals[m.c2], 0) - duals = res.solution_loader.get_duals([m.c1]) - self.assertNotIn(m.c2, duals) - self.assertIn(m.c1, duals) - self.assertAlmostEqual(duals[m.c1], 1) + if opt_class != MAiNGO: + reduced_costs = res.solution_loader.get_reduced_costs() + self.assertIn(m.x, reduced_costs) + self.assertIn(m.y, reduced_costs) + self.assertAlmostEqual(reduced_costs[m.x], 1) + self.assertAlmostEqual(reduced_costs[m.y], 0) + reduced_costs = res.solution_loader.get_reduced_costs([m.y]) + self.assertNotIn(m.x, reduced_costs) + self.assertIn(m.y, reduced_costs) + self.assertAlmostEqual(reduced_costs[m.y], 0) + duals = res.solution_loader.get_duals() + self.assertIn(m.c1, duals) + self.assertIn(m.c2, duals) + self.assertAlmostEqual(duals[m.c1], 1) + self.assertAlmostEqual(duals[m.c2], 0) + duals = res.solution_loader.get_duals([m.c1]) + self.assertNotIn(m.c2, duals) + self.assertIn(m.c1, duals) + self.assertAlmostEqual(duals[m.c1], 1) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_time_limit( self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) - if not opt.available(): + if not opt.available() or opt_class == MAiNGO: raise unittest.SkipTest from sys import platform @@ -1046,13 +1107,14 @@ def test_objective_changes( m.obj.sense = pe.maximize opt.config.load_solution = False res = opt.solve(m) - self.assertIn( - res.termination_condition, - { - TerminationCondition.unbounded, - TerminationCondition.infeasibleOrUnbounded, - }, - ) + if opt_class != MAiNGO: + self.assertIn( + res.termination_condition, + { + TerminationCondition.unbounded, + TerminationCondition.infeasibleOrUnbounded, + }, + ) m.obj.sense = pe.minimize opt.config.load_solution = True m.obj = pe.Objective(expr=m.x * m.y) @@ -1149,19 +1211,19 @@ def test_fixed_binaries( m.c = pe.Constraint(expr=m.y >= m.x) m.x.fix(0) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0) + self.assertAlmostEqual(res.best_feasible_objective, 0, 5) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1, 5) opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = False m.x.fix(0) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0) + self.assertAlmostEqual(res.best_feasible_objective, 0, 5) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1, 5) @parameterized.expand(input=_load_tests(mip_solvers, only_child_vars_options)) def test_with_gdp( @@ -1185,16 +1247,16 @@ def test_with_gdp( pe.TransformationFactory("gdp.bigm").apply_to(m) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) - self.assertAlmostEqual(m.x.value, 0) - self.assertAlmostEqual(m.y.value, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1, 6) + self.assertAlmostEqual(m.x.value, 0, 6) + self.assertAlmostEqual(m.y.value, 1, 6) opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.use_extensions = True res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) - self.assertAlmostEqual(m.x.value, 0) - self.assertAlmostEqual(m.y.value, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1, 6) + self.assertAlmostEqual(m.x.value, 0, 6) + self.assertAlmostEqual(m.y.value, 1, 6) @parameterized.expand(input=all_solvers) def test_variables_elsewhere(self, name: str, opt_class: Type[PersistentSolver]): @@ -1327,7 +1389,8 @@ def test_param_updates(self, name: str, opt_class: Type[PersistentSolver]): m.obj = pe.Objective(expr=m.y) m.c1 = pe.Constraint(expr=(0, m.y - m.a1 * m.x - m.b1, None)) m.c2 = pe.Constraint(expr=(None, -m.y + m.a2 * m.x + m.b2, 0)) - m.dual = pe.Suffix(direction=pe.Suffix.IMPORT) + if opt_class != MAiNGO: + m.dual = pe.Suffix(direction=pe.Suffix.IMPORT) params_to_test = [(1, -1, 2, 1), (1, -2, 2, 1), (1, -1, 3, 1)] for a1, a2, b1, b2 in params_to_test: @@ -1339,8 +1402,9 @@ def test_param_updates(self, name: str, opt_class: Type[PersistentSolver]): pe.assert_optimal_termination(res) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - self.assertAlmostEqual(m.dual[m.c1], (1 + a1 / (a2 - a1))) - self.assertAlmostEqual(m.dual[m.c2], a1 / (a2 - a1)) + if opt_class != MAiNGO: + self.assertAlmostEqual(m.dual[m.c1], (1 + a1 / (a2 - a1))) + self.assertAlmostEqual(m.dual[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=all_solvers) def test_load_solutions(self, name: str, opt_class: Type[PersistentSolver]): @@ -1351,11 +1415,14 @@ def test_load_solutions(self, name: str, opt_class: Type[PersistentSolver]): m.x = pe.Var() m.obj = pe.Objective(expr=m.x) m.c = pe.Constraint(expr=(-1, m.x, 1)) - m.dual = pe.Suffix(direction=pe.Suffix.IMPORT) + if opt_class != MAiNGO: + m.dual = pe.Suffix(direction=pe.Suffix.IMPORT) res = opt.solve(m, load_solutions=False) pe.assert_optimal_termination(res) self.assertIsNone(m.x.value) - self.assertNotIn(m.c, m.dual) + if opt_class != MAiNGO: + self.assertNotIn(m.c, m.dual) m.solutions.load_from(res) self.assertAlmostEqual(m.x.value, -1) - self.assertAlmostEqual(m.dual[m.c], 1) + if opt_class != MAiNGO: + self.assertAlmostEqual(m.dual[m.c], 1) diff --git a/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py new file mode 100644 index 00000000000..6fb25bfb529 --- /dev/null +++ b/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py @@ -0,0 +1,193 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.environ as pe +import pyomo.common.unittest as unittest +from pyomo.contrib.appsi.base import TerminationCondition, Results, PersistentSolver +from pyomo.contrib.appsi.solvers.wntr import Wntr, wntr_available +import math + + +_default_wntr_options = dict(TOL=1e-8) + + +@unittest.skipUnless(wntr_available, 'wntr is not available') +class TestWntrPersistent(unittest.TestCase): + def test_param_updates(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.p = pe.Param(initialize=1, mutable=True) + m.c = pe.Constraint(expr=m.x == m.p) + opt = Wntr() + opt.wntr_options.update(_default_wntr_options) + res = opt.solve(m) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(m.x.value, 1) + + m.p.value = 2 + res = opt.solve(m) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(m.x.value, 2) + + def test_remove_add_constraint(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.c1 = pe.Constraint(expr=m.y == (m.x - 1) ** 2) + m.c2 = pe.Constraint(expr=m.y == pe.exp(m.x)) + opt = Wntr() + opt.config.symbolic_solver_labels = True + opt.wntr_options.update(_default_wntr_options) + res = opt.solve(m) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 1) + + del m.c2 + m.c2 = pe.Constraint(expr=m.y == pe.log(m.x)) + m.x.value = 0.5 + m.y.value = 0.5 + res = opt.solve(m) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(m.x.value, 1) + self.assertAlmostEqual(m.y.value, 0) + + def test_fixed_var(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.c1 = pe.Constraint(expr=m.y == (m.x - 1) ** 2) + m.x.fix(0.5) + opt = Wntr() + opt.wntr_options.update(_default_wntr_options) + res = opt.solve(m) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(m.x.value, 0.5) + self.assertAlmostEqual(m.y.value, 0.25) + + m.x.unfix() + m.c2 = pe.Constraint(expr=m.y == pe.exp(m.x)) + res = opt.solve(m) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 1) + + m.x.fix(0.5) + del m.c2 + res = opt.solve(m) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(m.x.value, 0.5) + self.assertAlmostEqual(m.y.value, 0.25) + + def test_remove_variables_params(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var() + m.z.fix(0) + m.px = pe.Param(mutable=True, initialize=1) + m.py = pe.Param(mutable=True, initialize=1) + m.c1 = pe.Constraint(expr=m.x == m.px) + m.c2 = pe.Constraint(expr=m.y == m.py) + opt = Wntr() + opt.wntr_options.update(_default_wntr_options) + res = opt.solve(m) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(m.x.value, 1) + self.assertAlmostEqual(m.y.value, 1) + self.assertAlmostEqual(m.z.value, 0) + + del m.c2 + del m.y + del m.py + m.z.value = 2 + m.px.value = 2 + res = opt.solve(m) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(m.x.value, 2) + self.assertAlmostEqual(m.z.value, 2) + + del m.z + m.px.value = 3 + res = opt.solve(m) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(m.x.value, 3) + + def test_get_primals(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.c1 = pe.Constraint(expr=m.y == (m.x - 1) ** 2) + m.c2 = pe.Constraint(expr=m.y == pe.exp(m.x)) + opt = Wntr() + opt.config.load_solution = False + opt.wntr_options.update(_default_wntr_options) + res = opt.solve(m) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(m.x.value, None) + self.assertAlmostEqual(m.y.value, None) + primals = opt.get_primals() + self.assertAlmostEqual(primals[m.x], 0) + self.assertAlmostEqual(primals[m.y], 1) + + def test_operators(self): + m = pe.ConcreteModel() + m.x = pe.Var(initialize=1) + m.c1 = pe.Constraint(expr=2 / m.x == 1) + opt = Wntr() + opt.wntr_options.update(_default_wntr_options) + res = opt.solve(m) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(m.x.value, 2) + + del m.c1 + m.x.value = 0 + m.c1 = pe.Constraint(expr=pe.sin(m.x) == math.sin(math.pi / 4)) + res = opt.solve(m) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(m.x.value, math.pi / 4) + + del m.c1 + m.c1 = pe.Constraint(expr=pe.cos(m.x) == 0) + res = opt.solve(m) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(m.x.value, math.pi / 2) + + del m.c1 + m.c1 = pe.Constraint(expr=pe.tan(m.x) == 1) + m.x.value = 0 + res = opt.solve(m) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(m.x.value, math.pi / 4) + + del m.c1 + m.c1 = pe.Constraint(expr=pe.asin(m.x) == math.asin(0.5)) + res = opt.solve(m) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(m.x.value, 0.5) + + del m.c1 + m.c1 = pe.Constraint(expr=pe.acos(m.x) == math.acos(0.6)) + res = opt.solve(m) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(m.x.value, 0.6) + + del m.c1 + m.c1 = pe.Constraint(expr=pe.atan(m.x) == math.atan(0.5)) + res = opt.solve(m) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(m.x.value, 0.5) + + del m.c1 + m.c1 = pe.Constraint(expr=pe.sqrt(m.x) == math.sqrt(0.6)) + res = opt.solve(m) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(m.x.value, 0.6) diff --git a/pyomo/contrib/appsi/solvers/wntr.py b/pyomo/contrib/appsi/solvers/wntr.py new file mode 100644 index 00000000000..0a66cc640e5 --- /dev/null +++ b/pyomo/contrib/appsi/solvers/wntr.py @@ -0,0 +1,529 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.contrib.appsi.base import ( + PersistentBase, + PersistentSolver, + SolverConfig, + Results, + TerminationCondition, + PersistentSolutionLoader, +) +from pyomo.core.expr.numeric_expr import ( + ProductExpression, + DivisionExpression, + PowExpression, + SumExpression, + MonomialTermExpression, + NegationExpression, + UnaryFunctionExpression, + LinearExpression, + AbsExpression, + NPV_ProductExpression, + NPV_DivisionExpression, + NPV_PowExpression, + NPV_SumExpression, + NPV_NegationExpression, + NPV_UnaryFunctionExpression, + NPV_AbsExpression, +) +from pyomo.common.errors import PyomoException +from pyomo.common.collections import ComponentMap +from pyomo.core.expr.numvalue import native_numeric_types +from typing import Dict, Optional, List +from pyomo.core.base.block import BlockData +from pyomo.core.base.var import VarData +from pyomo.core.base.param import ParamData +from pyomo.core.base.constraint import ConstraintData +from pyomo.common.timing import HierarchicalTimer +from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler +from pyomo.common.dependencies import attempt_import +from pyomo.core.staleflag import StaleFlagManager +from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available + +wntr, wntr_available = attempt_import('wntr') +import logging +import time +import sys +from pyomo.core.expr.visitor import ExpressionValueVisitor + + +logger = logging.getLogger(__name__) + + +class WntrConfig(SolverConfig): + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + +class WntrResults(Results): + def __init__(self, solver): + super().__init__() + self.wallclock_time = None + self.solution_loader = PersistentSolutionLoader(solver=solver) + + +class Wntr(PersistentBase, PersistentSolver): + def __init__(self, only_child_vars=True): + super().__init__(only_child_vars=only_child_vars) + self._config = WntrConfig() + self._solver_options = dict() + self._solver_model = None + self._symbol_map = SymbolMap() + self._labeler = None + self._pyomo_var_to_solver_var_map = dict() + self._pyomo_con_to_solver_con_map = dict() + self._pyomo_param_to_solver_param_map = dict() + self._needs_updated = True + self._last_results_object: Optional[WntrResults] = None + self._pyomo_to_wntr_visitor = PyomoToWntrVisitor( + self._pyomo_var_to_solver_var_map, self._pyomo_param_to_solver_param_map + ) + + def available(self): + if wntr_available: + return self.Availability.FullLicense + else: + return self.Availability.NotFound + + def version(self): + return tuple(int(i) for i in wntr.__version__.split('.')) + + @property + def config(self) -> WntrConfig: + return self._config + + @config.setter + def config(self, val: WntrConfig): + self._config = val + + @property + def wntr_options(self): + return self._solver_options + + @wntr_options.setter + def wntr_options(self, val: Dict): + self._solver_options = val + + @property + def symbol_map(self): + return self._symbol_map + + def _solve(self, timer: HierarchicalTimer): + options = dict() + if self.config.time_limit is not None: + options['TIME_LIMIT'] = self.config.time_limit + options.update(self.wntr_options) + opt = wntr.sim.solvers.NewtonSolver(options) + + if self.config.stream_solver: + ostream = sys.stdout + else: + ostream = None + + t0 = time.time() + if self._needs_updated: + timer.start('set_structure') + self._solver_model.set_structure() + timer.stop('set_structure') + self._needs_updated = False + timer.start('newton solve') + status, msg, num_iter = opt.solve(self._solver_model, ostream) + timer.stop('newton solve') + tf = time.time() + + results = WntrResults(self) + results.wallclock_time = tf - t0 + if status == wntr.sim.solvers.SolverStatus.converged: + results.termination_condition = TerminationCondition.optimal + else: + results.termination_condition = TerminationCondition.error + results.best_feasible_objective = None + results.best_objective_bound = None + + if self.config.load_solution: + if status == wntr.sim.solvers.SolverStatus.converged: + timer.start('load solution') + self.load_vars() + timer.stop('load solution') + else: + raise RuntimeError( + 'A feasible solution was not found, so no solution can be loaded. ' + 'If using the appsi.solvers.Wntr interface, you can ' + 'set opt.config.load_solution=False. If using the environ.SolverFactory ' + 'interface, you can set opt.solve(model, load_solutions = False). ' + 'Then you can check results.termination_condition and ' + 'results.best_feasible_objective before loading a solution.' + ) + return results + + def solve(self, model: BlockData, timer: HierarchicalTimer = None) -> Results: + StaleFlagManager.mark_all_as_stale() + if self._last_results_object is not None: + self._last_results_object.solution_loader.invalidate() + if timer is None: + timer = HierarchicalTimer() + if model is not self._model: + timer.start('set_instance') + self.set_instance(model) + timer.stop('set_instance') + else: + timer.start('update') + self.update(timer=timer) + timer.start('initial values') + for v_id, solver_v in self._pyomo_var_to_solver_var_map.items(): + pyomo_v = self._vars[v_id][0] + val = pyomo_v.value + if val is not None: + solver_v.value = val + timer.stop('initial values') + timer.stop('update') + res = self._solve(timer) + self._last_results_object = res + if self.config.report_timing: + logger.info('\n' + str(timer)) + return res + + def _reinit(self): + saved_config = self.config + saved_options = self.wntr_options + saved_update_config = self.update_config + self.__init__(only_child_vars=self._only_child_vars) + self.config = saved_config + self.wntr_options = saved_options + self.update_config = saved_update_config + + def set_instance(self, model): + if self._last_results_object is not None: + self._last_results_object.solution_loader.invalidate() + if not self.available(): + c = self.__class__ + raise PyomoException( + f'Solver {c.__module__}.{c.__qualname__} is not available ' + f'({self.available()}).' + ) + self._reinit() + self._model = model + if self.use_extensions and cmodel_available: + self._expr_types = cmodel.PyomoExprTypes() + + if self.config.symbolic_solver_labels: + self._labeler = TextLabeler() + else: + self._labeler = NumericLabeler('x') + + self._solver_model = wntr.sim.aml.aml.Model() + self._solver_model._wntr_fixed_var_params = wntr.sim.aml.aml.ParamDict() + self._solver_model._wntr_fixed_var_cons = wntr.sim.aml.aml.ConstraintDict() + + self.add_block(model) + + def _add_variables(self, variables: List[VarData]): + aml = wntr.sim.aml.aml + for var in variables: + varname = self._symbol_map.getSymbol(var, self._labeler) + _v, _lb, _ub, _fixed, _domain_interval, _value = self._vars[id(var)] + lb, ub, step = _domain_interval + if ( + _lb is not None + or _ub is not None + or lb is not None + or ub is not None + or step != 0 + ): + raise ValueError( + f"WNTR's newton solver only supports continuous variables without bounds: {var.name}" + ) + if _value is None: + _value = 0 + wntr_var = aml.Var(_value) + setattr(self._solver_model, varname, wntr_var) + self._pyomo_var_to_solver_var_map[id(var)] = wntr_var + if _fixed: + self._solver_model._wntr_fixed_var_params[id(var)] = param = aml.Param( + _value + ) + wntr_expr = wntr_var - param + self._solver_model._wntr_fixed_var_cons[id(var)] = aml.Constraint( + wntr_expr + ) + self._needs_updated = True + + def _add_params(self, params: List[ParamData]): + aml = wntr.sim.aml.aml + for p in params: + pname = self._symbol_map.getSymbol(p, self._labeler) + wntr_p = aml.Param(p.value) + setattr(self._solver_model, pname, wntr_p) + self._pyomo_param_to_solver_param_map[id(p)] = wntr_p + + def _add_constraints(self, cons: List[ConstraintData]): + aml = wntr.sim.aml.aml + for con in cons: + if not con.equality: + raise ValueError( + f"WNTR's newtwon solver only supports equality constraints: {con.name}" + ) + conname = self._symbol_map.getSymbol(con, self._labeler) + wntr_expr = self._pyomo_to_wntr_visitor.dfs_postorder_stack( + con.body - con.upper + ) + wntr_con = aml.Constraint(wntr_expr) + setattr(self._solver_model, conname, wntr_con) + self._pyomo_con_to_solver_con_map[con] = wntr_con + self._needs_updated = True + + def _remove_constraints(self, cons: List[ConstraintData]): + for con in cons: + solver_con = self._pyomo_con_to_solver_con_map[con] + delattr(self._solver_model, solver_con.name) + self._symbol_map.removeSymbol(con) + del self._pyomo_con_to_solver_con_map[con] + self._needs_updated = True + + def _remove_variables(self, variables: List[VarData]): + for var in variables: + v_id = id(var) + solver_var = self._pyomo_var_to_solver_var_map[v_id] + delattr(self._solver_model, solver_var.name) + self._symbol_map.removeSymbol(var) + del self._pyomo_var_to_solver_var_map[v_id] + if v_id in self._solver_model._wntr_fixed_var_params: + del self._solver_model._wntr_fixed_var_params[v_id] + del self._solver_model._wntr_fixed_var_cons[v_id] + self._needs_updated = True + + def _remove_params(self, params: List[ParamData]): + for p in params: + p_id = id(p) + solver_param = self._pyomo_param_to_solver_param_map[p_id] + delattr(self._solver_model, solver_param.name) + self._symbol_map.removeSymbol(p) + del self._pyomo_param_to_solver_param_map[p_id] + + def _update_variables(self, variables: List[VarData]): + aml = wntr.sim.aml.aml + for var in variables: + v_id = id(var) + solver_var = self._pyomo_var_to_solver_var_map[v_id] + _v, _lb, _ub, _fixed, _domain_interval, _value = self._vars[v_id] + lb, ub, step = _domain_interval + if ( + _lb is not None + or _ub is not None + or lb is not None + or ub is not None + or step != 0 + ): + raise ValueError( + f"WNTR's newton solver only supports continuous variables without bounds: {var.name}" + ) + if _value is None: + _value = 0 + solver_var.value = _value + if _fixed: + if v_id not in self._solver_model._wntr_fixed_var_params: + self._solver_model._wntr_fixed_var_params[v_id] = param = aml.Param( + _value + ) + wntr_expr = solver_var - param + self._solver_model._wntr_fixed_var_cons[v_id] = aml.Constraint( + wntr_expr + ) + self._needs_updated = True + else: + self._solver_model._wntr_fixed_var_params[v_id].value = _value + else: + if v_id in self._solver_model._wntr_fixed_var_params: + del self._solver_model._wntr_fixed_var_params[v_id] + del self._solver_model._wntr_fixed_var_cons[v_id] + self._needs_updated = True + + def update_params(self): + for p_id, solver_p in self._pyomo_param_to_solver_param_map.items(): + p = self._params[p_id] + solver_p.value = p.value + + def _set_objective(self, obj): + raise NotImplementedError( + f"WNTR's newton solver can only solve square problems" + ) + + def load_vars(self, vars_to_load=None): + if vars_to_load is None: + vars_to_load = [i[0] for i in self._vars.values()] + for v in vars_to_load: + v_id = id(v) + solver_v = self._pyomo_var_to_solver_var_map[v_id] + v.value = solver_v.value + + def get_primals(self, vars_to_load=None): + if vars_to_load is None: + vars_to_load = [i[0] for i in self._vars.values()] + res = ComponentMap() + for v in vars_to_load: + v_id = id(v) + solver_v = self._pyomo_var_to_solver_var_map[v_id] + res[v] = solver_v.value + return res + + def _add_sos_constraints(self, cons): + if len(cons) > 0: + raise NotImplementedError( + f"WNTR's newton solver does not support SOS constraints" + ) + + def _remove_sos_constraints(self, cons): + if len(cons) > 0: + raise NotImplementedError( + f"WNTR's newton solver does not support SOS constraints" + ) + + +def _handle_product_expression(node, values): + arg1, arg2 = values + return arg1 * arg2 + + +def _handle_sum_expression(node, values): + return sum(values) + + +def _handle_division_expression(node, values): + arg1, arg2 = values + return arg1 / arg2 + + +def _handle_pow_expression(node, values): + arg1, arg2 = values + return arg1**arg2 + + +def _handle_negation_expression(node, values): + return -values[0] + + +def _handle_exp_expression(node, values): + return wntr.sim.aml.exp(values[0]) + + +def _handle_log_expression(node, values): + return wntr.sim.aml.log(values[0]) + + +def _handle_sin_expression(node, values): + return wntr.sim.aml.sin(values[0]) + + +def _handle_cos_expression(node, values): + return wntr.sim.aml.cos(values[0]) + + +def _handle_tan_expression(node, values): + return wntr.sim.aml.tan(values[0]) + + +def _handle_asin_expression(node, values): + return wntr.sim.aml.asin(values[0]) + + +def _handle_acos_expression(node, values): + return wntr.sim.aml.acos(values[0]) + + +def _handle_atan_expression(node, values): + return wntr.sim.aml.atan(values[0]) + + +def _handle_sqrt_expression(node, values): + return (values[0]) ** 0.5 + + +def _handle_abs_expression(node, values): + return wntr.sim.aml.abs(values[0]) + + +_unary_handler_map = dict() +_unary_handler_map['exp'] = _handle_exp_expression +_unary_handler_map['log'] = _handle_log_expression +_unary_handler_map['sin'] = _handle_sin_expression +_unary_handler_map['cos'] = _handle_cos_expression +_unary_handler_map['tan'] = _handle_tan_expression +_unary_handler_map['asin'] = _handle_asin_expression +_unary_handler_map['acos'] = _handle_acos_expression +_unary_handler_map['atan'] = _handle_atan_expression +_unary_handler_map['sqrt'] = _handle_sqrt_expression +_unary_handler_map['abs'] = _handle_abs_expression + + +def _handle_unary_function_expression(node, values): + if node.getname() in _unary_handler_map: + return _unary_handler_map[node.getname()](node, values) + else: + raise NotImplementedError( + f'Unrecognized unary function expression: {node.getname()}' + ) + + +_handler_map = dict() +_handler_map[ProductExpression] = _handle_product_expression +_handler_map[DivisionExpression] = _handle_division_expression +_handler_map[PowExpression] = _handle_pow_expression +_handler_map[SumExpression] = _handle_sum_expression +_handler_map[MonomialTermExpression] = _handle_product_expression +_handler_map[NegationExpression] = _handle_negation_expression +_handler_map[UnaryFunctionExpression] = _handle_unary_function_expression +_handler_map[LinearExpression] = _handle_sum_expression +_handler_map[AbsExpression] = _handle_abs_expression +_handler_map[NPV_ProductExpression] = _handle_product_expression +_handler_map[NPV_DivisionExpression] = _handle_division_expression +_handler_map[NPV_PowExpression] = _handle_pow_expression +_handler_map[NPV_SumExpression] = _handle_sum_expression +_handler_map[NPV_NegationExpression] = _handle_negation_expression +_handler_map[NPV_UnaryFunctionExpression] = _handle_unary_function_expression +_handler_map[NPV_AbsExpression] = _handle_abs_expression + + +class PyomoToWntrVisitor(ExpressionValueVisitor): + def __init__(self, var_map, param_map): + self.var_map = var_map + self.param_map = param_map + + def visit(self, node, values): + if node.__class__ in _handler_map: + return _handler_map[node.__class__](node, values) + else: + raise NotImplementedError(f'Unrecognized expression type: {node.__class__}') + + def visiting_potential_leaf(self, node): + if node.__class__ in native_numeric_types: + return True, node + + if node.is_variable_type(): + return True, self.var_map[id(node)] + + if node.is_parameter_type(): + return True, self.param_map[id(node)] + + return False, None diff --git a/pyomo/contrib/appsi/tests/__init__.py b/pyomo/contrib/appsi/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/appsi/tests/__init__.py +++ b/pyomo/contrib/appsi/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/appsi/tests/test_base.py b/pyomo/contrib/appsi/tests/test_base.py index 0d67ca4d01a..e537cc0f219 100644 --- a/pyomo/contrib/appsi/tests/test_base.py +++ b/pyomo/contrib/appsi/tests/test_base.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.common import unittest from pyomo.contrib import appsi import pyomo.environ as pe diff --git a/pyomo/contrib/appsi/tests/test_fbbt.py b/pyomo/contrib/appsi/tests/test_fbbt.py index f92960769cf..97af611c572 100644 --- a/pyomo/contrib/appsi/tests/test_fbbt.py +++ b/pyomo/contrib/appsi/tests/test_fbbt.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.common import unittest import pyomo.environ as pyo from pyomo.contrib import appsi @@ -140,3 +151,16 @@ def test_named_exprs(self): for x in m.x.values(): self.assertAlmostEqual(x.lb, 0) self.assertAlmostEqual(x.ub, 0) + + def test_named_exprs_nest(self): + # test for issue #3184 + m = pe.ConcreteModel() + m.x = pe.Var() + m.e = pe.Expression(expr=m.x + 1) + m.f = pe.Expression(expr=m.e) + m.c = pe.Constraint(expr=(0, m.f, 0)) + it = appsi.fbbt.IntervalTightener() + it.perform_fbbt(m) + for x in m.x.values(): + self.assertAlmostEqual(x.lb, -1) + self.assertAlmostEqual(x.ub, -1) diff --git a/pyomo/contrib/appsi/tests/test_interval.py b/pyomo/contrib/appsi/tests/test_interval.py index 7963cc31665..2184f69621a 100644 --- a/pyomo/contrib/appsi/tests/test_interval.py +++ b/pyomo/contrib/appsi/tests/test_interval.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available import pyomo.common.unittest as unittest import math diff --git a/pyomo/contrib/appsi/utils/__init__.py b/pyomo/contrib/appsi/utils/__init__.py index f665736fd4a..e1278431835 100644 --- a/pyomo/contrib/appsi/utils/__init__.py +++ b/pyomo/contrib/appsi/utils/__init__.py @@ -1,2 +1,13 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from .get_objective import get_objective from .collect_vars_and_named_exprs import collect_vars_and_named_exprs diff --git a/pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py b/pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py index 9027080f08c..4e117b04094 100644 --- a/pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py +++ b/pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core.expr.visitor import ExpressionValueVisitor, nonpyomo_leaf_types import pyomo.core.expr as EXPR diff --git a/pyomo/contrib/appsi/utils/get_objective.py b/pyomo/contrib/appsi/utils/get_objective.py index 30dd911f9c8..110c0188d16 100644 --- a/pyomo/contrib/appsi/utils/get_objective.py +++ b/pyomo/contrib/appsi/utils/get_objective.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core.base.objective import Objective diff --git a/pyomo/contrib/appsi/utils/tests/__init__.py b/pyomo/contrib/appsi/utils/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/appsi/utils/tests/__init__.py +++ b/pyomo/contrib/appsi/utils/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/appsi/utils/tests/test_collect_vars_and_named_exprs.py b/pyomo/contrib/appsi/utils/tests/test_collect_vars_and_named_exprs.py index 4c2a167a017..62f98728850 100644 --- a/pyomo/contrib/appsi/utils/tests/test_collect_vars_and_named_exprs.py +++ b/pyomo/contrib/appsi/utils/tests/test_collect_vars_and_named_exprs.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.common import unittest import pyomo.environ as pe from pyomo.contrib.appsi.utils import collect_vars_and_named_exprs diff --git a/pyomo/contrib/appsi/writers/__init__.py b/pyomo/contrib/appsi/writers/__init__.py index eeadfa73d03..18f90e8aa96 100644 --- a/pyomo/contrib/appsi/writers/__init__.py +++ b/pyomo/contrib/appsi/writers/__init__.py @@ -1,2 +1,13 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from .nl_writer import NLWriter from .lp_writer import LPWriter diff --git a/pyomo/contrib/appsi/writers/config.py b/pyomo/contrib/appsi/writers/config.py index 7a7faadaabe..32d45325e96 100644 --- a/pyomo/contrib/appsi/writers/config.py +++ b/pyomo/contrib/appsi/writers/config.py @@ -1,3 +1,15 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + class WriterConfig(object): def __init__(self): self.symbolic_solver_labels = False diff --git a/pyomo/contrib/appsi/writers/lp_writer.py b/pyomo/contrib/appsi/writers/lp_writer.py index 8a76fa5f9eb..788dfde7892 100644 --- a/pyomo/contrib/appsi/writers/lp_writer.py +++ b/pyomo/contrib/appsi/writers/lp_writer.py @@ -1,10 +1,21 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from typing import List -from pyomo.core.base.param import _ParamData -from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.constraint import _GeneralConstraintData -from pyomo.core.base.objective import _GeneralObjectiveData -from pyomo.core.base.sos import _SOSConstraintData -from pyomo.core.base.block import _BlockData +from pyomo.core.base.param import ParamData +from pyomo.core.base.var import VarData +from pyomo.core.base.constraint import ConstraintData +from pyomo.core.base.objective import ObjectiveData +from pyomo.core.base.sos import SOSConstraintData +from pyomo.core.base.block import BlockData from pyomo.repn.standard_repn import generate_standard_repn from pyomo.core.expr.numvalue import value from pyomo.contrib.appsi.base import PersistentBase @@ -66,7 +77,7 @@ def set_instance(self, model): if self._objective is None: self.set_objective(None) - def _add_variables(self, variables: List[_GeneralVarData]): + def _add_variables(self, variables: List[VarData]): cmodel.process_pyomo_vars( self._expr_types, variables, @@ -80,7 +91,7 @@ def _add_variables(self, variables: List[_GeneralVarData]): False, ) - def _add_params(self, params: List[_ParamData]): + def _add_params(self, params: List[ParamData]): cparams = cmodel.create_params(len(params)) for ndx, p in enumerate(params): cp = cparams[ndx] @@ -88,36 +99,36 @@ def _add_params(self, params: List[_ParamData]): cp.value = p.value self._pyomo_param_to_solver_param_map[id(p)] = cp - def _add_constraints(self, cons: List[_GeneralConstraintData]): + def _add_constraints(self, cons: List[ConstraintData]): cmodel.process_lp_constraints(cons, self) - def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + def _add_sos_constraints(self, cons: List[SOSConstraintData]): if len(cons) != 0: raise NotImplementedError('LP writer does not yet support SOS constraints') - def _remove_constraints(self, cons: List[_GeneralConstraintData]): + def _remove_constraints(self, cons: List[ConstraintData]): for c in cons: cc = self._pyomo_con_to_solver_con_map.pop(c) self._writer.remove_constraint(cc) self._symbol_map.removeSymbol(c) del self._solver_con_to_pyomo_con_map[cc] - def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + def _remove_sos_constraints(self, cons: List[SOSConstraintData]): if len(cons) != 0: raise NotImplementedError('LP writer does not yet support SOS constraints') - def _remove_variables(self, variables: List[_GeneralVarData]): + def _remove_variables(self, variables: List[VarData]): for v in variables: cvar = self._pyomo_var_to_solver_var_map.pop(id(v)) del self._solver_var_to_pyomo_var_map[cvar] self._symbol_map.removeSymbol(v) - def _remove_params(self, params: List[_ParamData]): + def _remove_params(self, params: List[ParamData]): for p in params: del self._pyomo_param_to_solver_param_map[id(p)] self._symbol_map.removeSymbol(p) - def _update_variables(self, variables: List[_GeneralVarData]): + def _update_variables(self, variables: List[VarData]): cmodel.process_pyomo_vars( self._expr_types, variables, @@ -136,7 +147,7 @@ def update_params(self): cp = self._pyomo_param_to_solver_param_map[p_id] cp.value = p.value - def _set_objective(self, obj: _GeneralObjectiveData): + def _set_objective(self, obj: ObjectiveData): cobj = cmodel.process_lp_objective( self._expr_types, obj, @@ -156,7 +167,7 @@ def _set_objective(self, obj: _GeneralObjectiveData): cobj.name = cname self._writer.objective = cobj - def write(self, model: _BlockData, filename: str, timer: HierarchicalTimer = None): + def write(self, model: BlockData, filename: str, timer: HierarchicalTimer = None): if timer is None: timer = HierarchicalTimer() if model is not self._model: diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index 9c739fd6ebb..27cdca004cb 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -1,10 +1,21 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from typing import List -from pyomo.core.base.param import _ParamData -from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.constraint import _GeneralConstraintData -from pyomo.core.base.objective import _GeneralObjectiveData -from pyomo.core.base.sos import _SOSConstraintData -from pyomo.core.base.block import _BlockData +from pyomo.core.base.param import ParamData +from pyomo.core.base.var import VarData +from pyomo.core.base.constraint import ConstraintData +from pyomo.core.base.objective import ObjectiveData +from pyomo.core.base.sos import SOSConstraintData +from pyomo.core.base.block import BlockData from pyomo.repn.standard_repn import generate_standard_repn from pyomo.core.expr.numvalue import value from pyomo.contrib.appsi.base import PersistentBase @@ -67,7 +78,7 @@ def set_instance(self, model): self.set_objective(None) self._set_pyomo_amplfunc_env() - def _add_variables(self, variables: List[_GeneralVarData]): + def _add_variables(self, variables: List[VarData]): if self.config.symbolic_solver_labels: set_name = True symbol_map = self._symbol_map @@ -89,7 +100,7 @@ def _add_variables(self, variables: List[_GeneralVarData]): False, ) - def _add_params(self, params: List[_ParamData]): + def _add_params(self, params: List[ParamData]): cparams = cmodel.create_params(len(params)) for ndx, p in enumerate(params): cp = cparams[ndx] @@ -100,7 +111,7 @@ def _add_params(self, params: List[_ParamData]): cp = cparams[ndx] cp.name = self._symbol_map.getSymbol(p, self._param_labeler) - def _add_constraints(self, cons: List[_GeneralConstraintData]): + def _add_constraints(self, cons: List[ConstraintData]): cmodel.process_nl_constraints( self._writer, self._expr_types, @@ -115,11 +126,11 @@ def _add_constraints(self, cons: List[_GeneralConstraintData]): for c, cc in self._pyomo_con_to_solver_con_map.items(): cc.name = self._symbol_map.getSymbol(c, self._con_labeler) - def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + def _add_sos_constraints(self, cons: List[SOSConstraintData]): if len(cons) != 0: raise NotImplementedError('NL writer does not support SOS constraints') - def _remove_constraints(self, cons: List[_GeneralConstraintData]): + def _remove_constraints(self, cons: List[ConstraintData]): if self.config.symbolic_solver_labels: for c in cons: self._symbol_map.removeSymbol(c) @@ -129,11 +140,11 @@ def _remove_constraints(self, cons: List[_GeneralConstraintData]): self._writer.remove_constraint(cc) del self._solver_con_to_pyomo_con_map[cc] - def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + def _remove_sos_constraints(self, cons: List[SOSConstraintData]): if len(cons) != 0: raise NotImplementedError('NL writer does not support SOS constraints') - def _remove_variables(self, variables: List[_GeneralVarData]): + def _remove_variables(self, variables: List[VarData]): if self.config.symbolic_solver_labels: for v in variables: self._symbol_map.removeSymbol(v) @@ -142,7 +153,7 @@ def _remove_variables(self, variables: List[_GeneralVarData]): cvar = self._pyomo_var_to_solver_var_map.pop(id(v)) del self._solver_var_to_pyomo_var_map[cvar] - def _remove_params(self, params: List[_ParamData]): + def _remove_params(self, params: List[ParamData]): if self.config.symbolic_solver_labels: for p in params: self._symbol_map.removeSymbol(p) @@ -150,7 +161,7 @@ def _remove_params(self, params: List[_ParamData]): for p in params: del self._pyomo_param_to_solver_param_map[id(p)] - def _update_variables(self, variables: List[_GeneralVarData]): + def _update_variables(self, variables: List[VarData]): cmodel.process_pyomo_vars( self._expr_types, variables, @@ -169,7 +180,7 @@ def update_params(self): cp = self._pyomo_param_to_solver_param_map[p_id] cp.value = p.value - def _set_objective(self, obj: _GeneralObjectiveData): + def _set_objective(self, obj: ObjectiveData): if obj is None: const = cmodel.Constant(0) lin_vars = list() @@ -221,7 +232,7 @@ def _set_objective(self, obj: _GeneralObjectiveData): cobj.sense = sense self._writer.objective = cobj - def write(self, model: _BlockData, filename: str, timer: HierarchicalTimer = None): + def write(self, model: BlockData, filename: str, timer: HierarchicalTimer = None): if timer is None: timer = HierarchicalTimer() if model is not self._model: diff --git a/pyomo/contrib/appsi/writers/tests/__init__.py b/pyomo/contrib/appsi/writers/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/appsi/writers/tests/__init__.py +++ b/pyomo/contrib/appsi/writers/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/appsi/writers/tests/test_nl_writer.py b/pyomo/contrib/appsi/writers/tests/test_nl_writer.py index 3b61a5901c3..c6005afceb2 100644 --- a/pyomo/contrib/appsi/writers/tests/test_nl_writer.py +++ b/pyomo/contrib/appsi/writers/tests/test_nl_writer.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.common.unittest as unittest from pyomo.common.tempfiles import TempfileManager import pyomo.environ as pe diff --git a/pyomo/contrib/benders/__init__.py b/pyomo/contrib/benders/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/benders/__init__.py +++ b/pyomo/contrib/benders/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/benders/benders_cuts.py b/pyomo/contrib/benders/benders_cuts.py index 5eb2e91cc82..0653be55986 100644 --- a/pyomo/contrib/benders/benders_cuts.py +++ b/pyomo/contrib/benders/benders_cuts.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,7 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from pyomo.core.base.block import _BlockData, declare_custom_block +from pyomo.core.base.block import BlockData, declare_custom_block import pyomo.environ as pyo from pyomo.solvers.plugins.solvers.persistent_solver import PersistentSolver from pyomo.core.expr.visitor import identify_variables @@ -166,13 +166,13 @@ def _setup_subproblem(b, root_vars, relax_subproblem_cons): @declare_custom_block(name='BendersCutGenerator') -class BendersCutGeneratorData(_BlockData): +class BendersCutGeneratorData(BlockData): def __init__(self, component): if not mpi4py_available: raise ImportError('BendersCutGenerator requires mpi4py.') if not numpy_available: raise ImportError('BendersCutGenerator requires numpy.') - _BlockData.__init__(self, component) + BlockData.__init__(self, component) self.num_subproblems_by_rank = 0 # np.zeros(self.comm.Get_size()) self.subproblems = list() @@ -335,7 +335,6 @@ def generate_cut(self): subproblem_solver.remove_constraint(c) subproblem_solver.remove_constraint(subproblem.fix_eta) del subproblem.fix_complicating_vars - del subproblem.fix_complicating_vars_index del subproblem.fix_eta total_num_subproblems = self.global_num_subproblems() diff --git a/pyomo/contrib/benders/examples/__init__.py b/pyomo/contrib/benders/examples/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/benders/examples/__init__.py +++ b/pyomo/contrib/benders/examples/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/benders/examples/farmer.py b/pyomo/contrib/benders/examples/farmer.py index bf5d40e112c..47cdb3511a3 100644 --- a/pyomo/contrib/benders/examples/farmer.py +++ b/pyomo/contrib/benders/examples/farmer.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/benders/examples/grothey_ex.py b/pyomo/contrib/benders/examples/grothey_ex.py index 66457fa7293..27d37cac124 100644 --- a/pyomo/contrib/benders/examples/grothey_ex.py +++ b/pyomo/contrib/benders/examples/grothey_ex.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/benders/tests/__init__.py b/pyomo/contrib/benders/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/benders/tests/__init__.py +++ b/pyomo/contrib/benders/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/benders/tests/test_benders.py b/pyomo/contrib/benders/tests/test_benders.py index 26a2a0b7910..d985f886c10 100644 --- a/pyomo/contrib/benders/tests/test_benders.py +++ b/pyomo/contrib/benders/tests/test_benders.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -10,35 +10,24 @@ # ___________________________________________________________________________ import pyomo.common.unittest as unittest -from pyomo.contrib.benders.benders_cuts import BendersCutGenerator import pyomo.environ as pyo -try: - import mpi4py - - mpi4py_available = True -except: - mpi4py_available = False -try: - import numpy as np - - numpy_available = True -except: - numpy_available = False - +from pyomo.common.dependencies import mpi4py_available, numpy_available +from pyomo.contrib.benders.benders_cuts import BendersCutGenerator -ipopt_opt = pyo.SolverFactory('ipopt') -ipopt_available = ipopt_opt.available(exception_flag=False) +ipopt_available = pyo.SolverFactory('ipopt').available(exception_flag=False) -cplex_opt = pyo.SolverFactory('cplex_direct') -cplex_available = cplex_opt.available(exception_flag=False) +for mip_name in ('cplex_direct', 'gurobi_direct', 'gurobi', 'cplex', 'glpk', 'cbc'): + mip_available = pyo.SolverFactory(mip_name).available(exception_flag=False) + if mip_available: + break @unittest.pytest.mark.mpi class MPITestBenders(unittest.TestCase): @unittest.skipIf(not mpi4py_available, 'mpi4py is not available.') @unittest.skipIf(not numpy_available, 'numpy is not available.') - @unittest.skipIf(not cplex_available, 'cplex is not available.') + @unittest.skipIf(not mip_available, 'MIP solver is not available.') def test_farmer(self): class Farmer(object): def __init__(self): @@ -200,9 +189,9 @@ def EnforceQuotas_rule(m, i): subproblem_fn=create_subproblem, subproblem_fn_kwargs=subproblem_fn_kwargs, root_eta=m.eta[s], - subproblem_solver='cplex_direct', + subproblem_solver=mip_name, ) - opt = pyo.SolverFactory('cplex_direct') + opt = pyo.SolverFactory(mip_name) for i in range(30): res = opt.solve(m, tee=False) @@ -261,7 +250,7 @@ def create_subproblem(root): @unittest.skipIf(not mpi4py_available, 'mpi4py is not available.') @unittest.skipIf(not numpy_available, 'numpy is not available.') - @unittest.skipIf(not cplex_available, 'cplex is not available.') + @unittest.skipIf(not mip_available, 'MIP solver is not available.') def test_four_scen_farmer(self): class FourScenFarmer(object): def __init__(self): @@ -430,9 +419,9 @@ def EnforceQuotas_rule(m, i): subproblem_fn=create_subproblem, subproblem_fn_kwargs=subproblem_fn_kwargs, root_eta=m.eta[s], - subproblem_solver='cplex_direct', + subproblem_solver=mip_name, ) - opt = pyo.SolverFactory('cplex_direct') + opt = pyo.SolverFactory(mip_name) for i in range(30): res = opt.solve(m, tee=False) diff --git a/pyomo/contrib/community_detection/__init__.py b/pyomo/contrib/community_detection/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/community_detection/__init__.py +++ b/pyomo/contrib/community_detection/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/community_detection/community_graph.py b/pyomo/contrib/community_detection/community_graph.py index d4aa7e0b973..889940b5996 100644 --- a/pyomo/contrib/community_detection/community_graph.py +++ b/pyomo/contrib/community_detection/community_graph.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Model Graph Generator Code""" from pyomo.common.dependencies import networkx as nx @@ -116,9 +127,9 @@ def generate_model_graph( ] # Update constraint_variable_map - constraint_variable_map[ - numbered_constraint - ] = numbered_variables_in_constraint_equation + constraint_variable_map[numbered_constraint] = ( + numbered_variables_in_constraint_equation + ) # Create a list of all the edges that need to be created based on the variables in this constraint equation edges_between_nodes = [ @@ -145,9 +156,9 @@ def generate_model_graph( ] # Update constraint_variable_map - constraint_variable_map[ - numbered_objective - ] = numbered_variables_in_objective + constraint_variable_map[numbered_objective] = ( + numbered_variables_in_objective + ) # Create a list of all the edges that need to be created based on the variables in the objective function edges_between_nodes = [ diff --git a/pyomo/contrib/community_detection/detection.py b/pyomo/contrib/community_detection/detection.py index 5751f54e9c1..0e2c3912e06 100644 --- a/pyomo/contrib/community_detection/detection.py +++ b/pyomo/contrib/community_detection/detection.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ Main module for community detection integration with Pyomo models. @@ -7,6 +18,7 @@ Original implementation developed by Rahul Joglekar in the Grossmann research group. """ + from logging import getLogger from pyomo.common.dependencies import attempt_import @@ -19,7 +31,7 @@ Objective, ConstraintList, ) -from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.core.base.objective import ObjectiveData from pyomo.core.expr.visitor import replace_expressions, identify_variables from pyomo.contrib.community_detection.community_graph import generate_model_graph from pyomo.common.dependencies import networkx as nx @@ -453,16 +465,14 @@ def visualize_model_graph( if type_of_graph != self.type_of_community_map: # Use the generate_model_graph function to create a NetworkX graph of the given model (along with # number_component_map and constraint_variable_map, which will be used to help with drawing the graph) - ( - model_graph, - number_component_map, - constraint_variable_map, - ) = generate_model_graph( - self.model, - type_of_graph=type_of_graph, - with_objective=self.with_objective, - weighted_graph=self.weighted_graph, - use_only_active_components=self.use_only_active_components, + (model_graph, number_component_map, constraint_variable_map) = ( + generate_model_graph( + self.model, + type_of_graph=type_of_graph, + with_objective=self.with_objective, + weighted_graph=self.weighted_graph, + use_only_active_components=self.use_only_active_components, + ) ) else: # This is the case where, as mentioned above, we can use the networkX graph that was made to create @@ -726,13 +736,10 @@ def generate_structured_model(self): variable_in_new_model = structured_model.find_component( new_variable ) - blocked_variable_map[ - variable_in_stored_constraint - ] = blocked_variable_map.get( - variable_in_stored_constraint, [] - ) + [ - variable_in_new_model - ] + blocked_variable_map[variable_in_stored_constraint] = ( + blocked_variable_map.get(variable_in_stored_constraint, []) + + [variable_in_new_model] + ) # Update replace_variables_in_expression_map accordingly replace_variables_in_expression_map[ @@ -743,7 +750,7 @@ def generate_structured_model(self): # Check to see whether 'stored_constraint' is actually an objective (since constraints and objectives # grouped together) if self.with_objective and isinstance( - stored_constraint, (_GeneralObjectiveData, Objective) + stored_constraint, (ObjectiveData, Objective) ): # If the constraint is actually an objective, we add it to the block as an objective new_objective = Objective( @@ -802,11 +809,10 @@ def generate_structured_model(self): variable_in_new_model = structured_model.find_component( new_variable ) - blocked_variable_map[ - variable_in_objective - ] = blocked_variable_map.get(variable_in_objective, []) + [ - variable_in_new_model - ] + blocked_variable_map[variable_in_objective] = ( + blocked_variable_map.get(variable_in_objective, []) + + [variable_in_new_model] + ) # Update the dictionary that we will use to replace the variables replace_variables_in_expression_map[ diff --git a/pyomo/contrib/community_detection/event_log.py b/pyomo/contrib/community_detection/event_log.py index 30e28257de8..767ff0f50f5 100644 --- a/pyomo/contrib/community_detection/event_log.py +++ b/pyomo/contrib/community_detection/event_log.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ Logger function for community_graph.py """ from logging import getLogger diff --git a/pyomo/contrib/community_detection/plugins.py b/pyomo/contrib/community_detection/plugins.py index 0cdc95ad02a..229b7255a27 100644 --- a/pyomo/contrib/community_detection/plugins.py +++ b/pyomo/contrib/community_detection/plugins.py @@ -1,2 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + def load(): import pyomo.contrib.community_detection.detection diff --git a/pyomo/contrib/community_detection/tests/__init__.py b/pyomo/contrib/community_detection/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/community_detection/tests/__init__.py +++ b/pyomo/contrib/community_detection/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/community_detection/tests/test_detection.py b/pyomo/contrib/community_detection/tests/test_detection.py index 724388f9ab6..6a43ea1b61a 100644 --- a/pyomo/contrib/community_detection/tests/test_detection.py +++ b/pyomo/contrib/community_detection/tests/test_detection.py @@ -4,7 +4,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -12,7 +12,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from __future__ import division import logging diff --git a/pyomo/contrib/cp/__init__.py b/pyomo/contrib/cp/__init__.py index c51160bf931..d206fe95251 100644 --- a/pyomo/contrib/cp/__init__.py +++ b/pyomo/contrib/cp/__init__.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.contrib.cp.interval_var import ( IntervalVar, IntervalVarStartTime, @@ -6,6 +17,19 @@ IntervalVarPresence, ) from pyomo.contrib.cp.repn.docplex_writer import DocplexWriter, CPOptimizerSolver +from pyomo.contrib.cp.sequence_var import SequenceVar +from pyomo.contrib.cp.scheduling_expr.sequence_expressions import ( + no_overlap, + first_in_sequence, + last_in_sequence, + before_in_sequence, + predecessor_to, +) +from pyomo.contrib.cp.scheduling_expr.scheduling_logic import ( + alternative, + spans, + synchronize, +) from pyomo.contrib.cp.scheduling_expr.step_function_expressions import ( AlwaysIn, Step, diff --git a/pyomo/contrib/cp/interval_var.py b/pyomo/contrib/cp/interval_var.py index 911d9ba50ba..dec5af74d9f 100644 --- a/pyomo/contrib/cp/interval_var.py +++ b/pyomo/contrib/cp/interval_var.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -11,6 +11,7 @@ from pyomo.common.collections import ComponentSet from pyomo.common.pyomo_typing import overload +from pyomo.contrib.cp.scheduling_expr.scheduling_logic import SpanExpression from pyomo.contrib.cp.scheduling_expr.precedence_expressions import ( BeforeExpression, AtExpression, @@ -18,12 +19,13 @@ from pyomo.core import Integers, value from pyomo.core.base import Any, ScalarVar, ScalarBooleanVar -from pyomo.core.base.block import _BlockData, Block +from pyomo.core.base.block import BlockData, Block from pyomo.core.base.component import ModelComponentFactory from pyomo.core.base.global_set import UnindexedComponent_index from pyomo.core.base.indexed_component import IndexedComponent, UnindexedComponent_set from pyomo.core.base.initializer import BoundInitializer, Initializer from pyomo.core.expr import GetItemExpression +from pyomo.core.expr.logical_expr import _flattened class IntervalVarTimePoint(ScalarVar): @@ -49,7 +51,7 @@ class IntervalVarStartTime(IntervalVarTimePoint): """This class defines a single variable denoting a start time point of an IntervalVar""" - def __init__(self): + def __init__(self, *args, **kwd): super().__init__(domain=Integers, ctype=IntervalVarStartTime) @@ -57,7 +59,7 @@ class IntervalVarEndTime(IntervalVarTimePoint): """This class defines a single variable denoting an end time point of an IntervalVar""" - def __init__(self): + def __init__(self, *args, **kwd): super().__init__(domain=Integers, ctype=IntervalVarEndTime) @@ -67,7 +69,7 @@ class IntervalVarLength(ScalarVar): __slots__ = () - def __init__(self): + def __init__(self, *args, **kwd): super().__init__(domain=Integers, ctype=IntervalVarLength) def get_associated_interval_var(self): @@ -80,21 +82,23 @@ class IntervalVarPresence(ScalarBooleanVar): __slots__ = () - def __init__(self): + def __init__(self, *args, **kwd): + # TODO: adding args and kwd above made Reference work, but we + # probably shouldn't just swallow them, right? super().__init__(ctype=IntervalVarPresence) def get_associated_interval_var(self): return self.parent_block() -class IntervalVarData(_BlockData): +class IntervalVarData(BlockData): """This class defines the abstract interface for a single interval variable.""" # We will put our four variables on this, and everything else is off limits. _Block_reserved_words = Any def __init__(self, component=None): - _BlockData.__init__(self, component) + BlockData.__init__(self, component) with self._declare_reserved_components(): self.is_present = IntervalVarPresence() @@ -122,6 +126,9 @@ def optional(self, val): else: self.is_present.fix(True) + def spans(self, *args): + return SpanExpression([self] + list(_flattened(args))) + @ModelComponentFactory.register("Interval variables for scheduling.") class IntervalVar(Block): @@ -161,8 +168,7 @@ def __init__( optional=False, name=None, doc=None - ): - ... + ): ... def __init__(self, *args, **kwargs): _start_arg = kwargs.pop('start', None) diff --git a/pyomo/contrib/cp/plugins.py b/pyomo/contrib/cp/plugins.py index 445599daab0..b0f7c84eb65 100644 --- a/pyomo/contrib/cp/plugins.py +++ b/pyomo/contrib/cp/plugins.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/cp/repn/__init__.py b/pyomo/contrib/cp/repn/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/cp/repn/__init__.py +++ b/pyomo/contrib/cp/repn/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 51c3f66140e..6a0eb7749a8 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -30,10 +30,27 @@ IntervalVarData, IndexedIntervalVar, ) +from pyomo.contrib.cp.sequence_var import ( + SequenceVar, + ScalarSequenceVar, + SequenceVarData, +) +from pyomo.contrib.cp.scheduling_expr.scheduling_logic import ( + AlternativeExpression, + SpanExpression, + SynchronizeExpression, +) from pyomo.contrib.cp.scheduling_expr.precedence_expressions import ( BeforeExpression, AtExpression, ) +from pyomo.contrib.cp.scheduling_expr.sequence_expressions import ( + NoOverlapExpression, + FirstInSequenceExpression, + LastInSequenceExpression, + BeforeInSequenceExpression, + PredecessorToExpression, +) from pyomo.contrib.cp.scheduling_expr.step_function_expressions import ( AlwaysIn, StepAt, @@ -60,16 +77,17 @@ ) from pyomo.core.base.boolean_var import ( ScalarBooleanVar, - _GeneralBooleanVarData, + BooleanVarData, IndexedBooleanVar, ) -from pyomo.core.base.expression import ScalarExpression, _GeneralExpressionData -from pyomo.core.base.param import IndexedParam, ScalarParam -from pyomo.core.base.var import ScalarVar, _GeneralVarData, IndexedVar +from pyomo.core.base.expression import ScalarExpression, ExpressionData +from pyomo.core.base.param import IndexedParam, ScalarParam, ParamData +from pyomo.core.base.var import ScalarVar, VarData, IndexedVar import pyomo.core.expr as EXPR from pyomo.core.expr.visitor import StreamBasedExpressionVisitor, identify_variables from pyomo.core.base import Set, RangeSet from pyomo.core.base.set import SetProduct +from pyomo.repn.util import ExitNodeDispatcher from pyomo.opt import WriterFactory, SolverFactory, TerminationCondition, SolverResults ### FIXME: Remove the following as soon as non-active components no @@ -449,6 +467,7 @@ def _create_docplex_interval_var(visitor, interval_var): nm = interval_var.name if visitor.symbolic_solver_labels else None cpx_interval_var = cp.interval_var(name=nm) visitor.var_map[id(interval_var)] = cpx_interval_var + visitor.pyomo_to_docplex[interval_var] = cpx_interval_var # Figure out if it exists if interval_var.is_present.fixed and not interval_var.is_present.value: @@ -491,6 +510,19 @@ def _create_docplex_interval_var(visitor, interval_var): return cpx_interval_var +def _create_docplex_sequence_var(visitor, sequence_var): + nm = sequence_var.name if visitor.symbolic_solver_labels else None + + cpx_seq_var = cp.sequence_var( + name=nm, + vars=[ + _get_docplex_interval_var(visitor, v) for v in sequence_var.interval_vars + ], + ) + visitor.var_map[id(sequence_var)] = cpx_seq_var + return cpx_seq_var + + def _get_docplex_interval_var(visitor, interval_var): # We might already have the interval_var and just need to retrieve it if id(interval_var) in visitor.var_map: @@ -501,6 +533,25 @@ def _get_docplex_interval_var(visitor, interval_var): return cpx_interval_var +def _get_docplex_sequence_var(visitor, sequence_var): + if id(sequence_var) in visitor.var_map: + cpx_seq_var = visitor.var_map[id(sequence_var)] + else: + cpx_seq_var = _create_docplex_sequence_var(visitor, sequence_var) + visitor.cpx.add(cpx_seq_var) + return cpx_seq_var + + +def _before_sequence_var(visitor, child): + _id = id(child) + if _id not in visitor.var_map: + cpx_seq_var = _get_docplex_sequence_var(visitor, child) + visitor.var_map[_id] = cpx_seq_var + visitor.pyomo_to_docplex[child] = cpx_seq_var + + return False, (_GENERAL, visitor.var_map[_id]) + + def _before_interval_var(visitor, child): _id = id(child) if _id not in visitor.var_map: @@ -564,22 +615,22 @@ def _before_interval_var_presence(visitor, child): def _handle_step_at_node(visitor, node): - return cp.step_at(node._time, node._height) + return False, (_GENERAL, cp.step_at(node._time, node._height)) def _handle_step_at_start_node(visitor, node): cpx_var = _get_docplex_interval_var(visitor, node._time) - return cp.step_at_start(cpx_var, node._height) + return False, (_GENERAL, cp.step_at_start(cpx_var, node._height)) def _handle_step_at_end_node(visitor, node): cpx_var = _get_docplex_interval_var(visitor, node._time) - return cp.step_at_end(cpx_var, node._height) + return False, (_GENERAL, cp.step_at_end(cpx_var, node._height)) def _handle_pulse_node(visitor, node): cpx_var = _get_docplex_interval_var(visitor, node._interval_var) - return cp.pulse(cpx_var, node._height) + return False, (_GENERAL, cp.pulse(cpx_var, node._height)) def _handle_negated_step_function_node(visitor, node): @@ -590,9 +641,9 @@ def _handle_cumulative_function(visitor, node): expr = 0 for arg in node.args: if arg.__class__ is NegatedStepFunction: - expr -= _handle_negated_step_function_node(visitor, arg) + expr -= _handle_negated_step_function_node(visitor, arg)[1][1] else: - expr += _step_function_handles[arg.__class__](visitor, arg) + expr += _step_function_handles[arg.__class__](visitor, arg)[1][1] return False, (_GENERAL, expr) @@ -658,7 +709,7 @@ def _handle_monomial_expr(visitor, node, arg1, arg2): # simplifications (necessary in part for the unit tests) if arg2[1].__class__ in EXPR.native_types: return _GENERAL, arg1[1] * arg2[1] - elif arg1[1] == 1: + elif arg1[1].__class__ in EXPR.native_types and arg1[1] == 1: return arg2 return (_GENERAL, cp.times(_get_int_valued_expr(arg1), _get_int_valued_expr(arg2))) @@ -805,6 +856,14 @@ def _handle_at_least_node(visitor, node, *args): ) +def _handle_all_diff_node(visitor, node, *args): + return (_GENERAL, cp.all_diff(_get_int_valued_expr(arg) for arg in args)) + + +def _handle_count_if_node(visitor, node, *args): + return (_GENERAL, cp.count((_get_bool_valued_expr(arg) for arg in args), 1)) + + ## CallExpression handllers @@ -902,46 +961,91 @@ def _handle_always_in_node(visitor, node, cumul_func, lb, ub, start, end): ) +def _handle_no_overlap_expression_node(visitor, node, seq_var): + return _GENERAL, cp.no_overlap(seq_var[1]) + + +def _handle_first_in_sequence_expression_node(visitor, node, interval_var, seq_var): + return _GENERAL, cp.first(seq_var[1], interval_var[1]) + + +def _handle_last_in_sequence_expression_node(visitor, node, interval_var, seq_var): + return _GENERAL, cp.last(seq_var[1], interval_var[1]) + + +def _handle_before_in_sequence_expression_node( + visitor, node, before_var, after_var, seq_var +): + return _GENERAL, cp.before(seq_var[1], before_var[1], after_var[1]) + + +def _handle_predecessor_to_expression_node( + visitor, node, before_var, after_var, seq_var +): + return _GENERAL, cp.previous(seq_var[1], before_var[1], after_var[1]) + + +def _handle_span_expression_node(visitor, node, *args): + return _GENERAL, cp.span(args[0][1], [arg[1] for arg in args[1:]]) + + +def _handle_alternative_expression_node(visitor, node, *args): + return _GENERAL, cp.alternative(args[0][1], [arg[1] for arg in args[1:]]) + + +def _handle_synchronize_expression_node(visitor, node, *args): + return _GENERAL, cp.synchronize(args[0][1], [arg[1] for arg in args[1:]]) + + +_operator_handles = { + EXPR.GetItemExpression: _handle_getitem, + EXPR.GetAttrExpression: _handle_getattr, + EXPR.CallExpression: _handle_call, + EXPR.NegationExpression: _handle_negation_node, + EXPR.ProductExpression: _handle_product_node, + EXPR.DivisionExpression: _handle_division_node, + EXPR.PowExpression: _handle_pow_node, + EXPR.AbsExpression: _handle_abs_node, + EXPR.MonomialTermExpression: _handle_monomial_expr, + EXPR.SumExpression: _handle_sum_node, + EXPR.MinExpression: _handle_min_node, + EXPR.MaxExpression: _handle_max_node, + EXPR.NotExpression: _handle_not_node, + EXPR.EquivalenceExpression: _handle_equivalence_node, + EXPR.ImplicationExpression: _handle_implication_node, + EXPR.AndExpression: _handle_and_node, + EXPR.OrExpression: _handle_or_node, + EXPR.XorExpression: _handle_xor_node, + EXPR.ExactlyExpression: _handle_exactly_node, + EXPR.AtMostExpression: _handle_at_most_node, + EXPR.AtLeastExpression: _handle_at_least_node, + EXPR.AllDifferentExpression: _handle_all_diff_node, + EXPR.CountIfExpression: _handle_count_if_node, + EXPR.EqualityExpression: _handle_equality_node, + EXPR.NotEqualExpression: _handle_not_equal_node, + EXPR.InequalityExpression: _handle_inequality_node, + EXPR.RangedExpression: _handle_ranged_inequality_node, + BeforeExpression: _handle_before_expression_node, + AtExpression: _handle_at_expression_node, + AlwaysIn: _handle_always_in_node, + ExpressionData: _handle_named_expression_node, + ScalarExpression: _handle_named_expression_node, + NoOverlapExpression: _handle_no_overlap_expression_node, + FirstInSequenceExpression: _handle_first_in_sequence_expression_node, + LastInSequenceExpression: _handle_last_in_sequence_expression_node, + BeforeInSequenceExpression: _handle_before_in_sequence_expression_node, + PredecessorToExpression: _handle_predecessor_to_expression_node, + SpanExpression: _handle_span_expression_node, + AlternativeExpression: _handle_alternative_expression_node, + SynchronizeExpression: _handle_synchronize_expression_node, +} + + class LogicalToDoCplex(StreamBasedExpressionVisitor): - _operator_handles = { - EXPR.GetItemExpression: _handle_getitem, - EXPR.Structural_GetItemExpression: _handle_getitem, - EXPR.Numeric_GetItemExpression: _handle_getitem, - EXPR.Boolean_GetItemExpression: _handle_getitem, - EXPR.GetAttrExpression: _handle_getattr, - EXPR.Structural_GetAttrExpression: _handle_getattr, - EXPR.Numeric_GetAttrExpression: _handle_getattr, - EXPR.Boolean_GetAttrExpression: _handle_getattr, - EXPR.CallExpression: _handle_call, - EXPR.NegationExpression: _handle_negation_node, - EXPR.ProductExpression: _handle_product_node, - EXPR.DivisionExpression: _handle_division_node, - EXPR.PowExpression: _handle_pow_node, - EXPR.AbsExpression: _handle_abs_node, - EXPR.MonomialTermExpression: _handle_monomial_expr, - EXPR.SumExpression: _handle_sum_node, - EXPR.LinearExpression: _handle_sum_node, - EXPR.MinExpression: _handle_min_node, - EXPR.MaxExpression: _handle_max_node, - EXPR.NotExpression: _handle_not_node, - EXPR.EquivalenceExpression: _handle_equivalence_node, - EXPR.ImplicationExpression: _handle_implication_node, - EXPR.AndExpression: _handle_and_node, - EXPR.OrExpression: _handle_or_node, - EXPR.XorExpression: _handle_xor_node, - EXPR.ExactlyExpression: _handle_exactly_node, - EXPR.AtMostExpression: _handle_at_most_node, - EXPR.AtLeastExpression: _handle_at_least_node, - EXPR.EqualityExpression: _handle_equality_node, - EXPR.NotEqualExpression: _handle_not_equal_node, - EXPR.InequalityExpression: _handle_inequality_node, - EXPR.RangedExpression: _handle_ranged_inequality_node, - BeforeExpression: _handle_before_expression_node, - AtExpression: _handle_at_expression_node, - AlwaysIn: _handle_always_in_node, - _GeneralExpressionData: _handle_named_expression_node, - ScalarExpression: _handle_named_expression_node, - } + exit_node_dispatcher = ExitNodeDispatcher(_operator_handles) + # NOTE: Because of indirection, we can encounter indexed Params and Vars in + # expressions + _var_handles = { IntervalVarStartTime: _before_interval_var_start_time, IntervalVarEndTime: _before_interval_var_end_time, @@ -950,16 +1054,19 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): ScalarIntervalVar: _before_interval_var, IntervalVarData: _before_interval_var, IndexedIntervalVar: _before_indexed_interval_var, + ScalarSequenceVar: _before_sequence_var, + SequenceVarData: _before_sequence_var, ScalarVar: _before_var, - _GeneralVarData: _before_var, + VarData: _before_var, IndexedVar: _before_indexed_var, ScalarBooleanVar: _before_boolean_var, - _GeneralBooleanVarData: _before_boolean_var, + BooleanVarData: _before_boolean_var, IndexedBooleanVar: _before_indexed_boolean_var, - _GeneralExpressionData: _before_named_expression, + ExpressionData: _before_named_expression, ScalarExpression: _before_named_expression, - IndexedParam: _before_indexed_param, # Because of indirection + IndexedParam: _before_indexed_param, ScalarParam: _before_param, + ParamData: _before_param, } def __init__(self, cpx_model, symbolic_solver_labels=False): @@ -993,7 +1100,7 @@ def beforeChild(self, node, child, child_idx): return True, None def exitNode(self, node, data): - return self._operator_handles[node.__class__](self, node, *data) + return self.exit_node_dispatcher[node.__class__](self, node, *data) finalizeResult = None @@ -1005,6 +1112,9 @@ def collect_valid_components(model, active=True, sort=None, valid=set(), targets unrecognized = {} components = {k: [] for k in targets} for obj in model.component_data_objects(active=True, descend_into=True, sort=sort): + # HACK around #3045 + if not hasattr(obj, 'ctype'): + continue ctype = obj.ctype if ctype in components: components[ctype].append(obj) @@ -1055,7 +1165,13 @@ def write(self, model, **options): RangeSet, Port, }, - targets={Objective, Constraint, LogicalConstraint, IntervalVar}, + targets={ + Objective, + Constraint, + LogicalConstraint, + IntervalVar, + SequenceVar, + }, ) if unknown: raise ValueError( @@ -1284,6 +1400,10 @@ def solve(self, model, **kwds): ) else: sol = sol.get_value() + if py_var.ctype is SequenceVar: + # They don't actually have values--the IntervalVars will get + # set. + continue if py_var.ctype is IntervalVar: if len(sol) == 0: # The interval_var is absent diff --git a/pyomo/contrib/cp/scheduling_expr/__init__.py b/pyomo/contrib/cp/scheduling_expr/__init__.py index 8b137891791..a4a626013c4 100644 --- a/pyomo/contrib/cp/scheduling_expr/__init__.py +++ b/pyomo/contrib/cp/scheduling_expr/__init__.py @@ -1 +1,10 @@ - +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/cp/scheduling_expr/precedence_expressions.py b/pyomo/contrib/cp/scheduling_expr/precedence_expressions.py index 5340583a216..675d9efb0a0 100644 --- a/pyomo/contrib/cp/scheduling_expr/precedence_expressions.py +++ b/pyomo/contrib/cp/scheduling_expr/precedence_expressions.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -21,13 +21,13 @@ def delay(self): return self._args_[2] def _to_string_impl(self, values, relation): - delay = int(values[2]) - if delay == 0: + delay = values[2] + if delay == '0': first = values[0] - elif delay > 0: - first = "%s + %s" % (values[0], delay) + elif delay[0] in '-+': + first = "%s %s %s" % (values[0], delay[0], delay[1:]) else: - first = "%s - %s" % (values[0], abs(delay)) + first = "%s + %s" % (values[0], delay) return "%s %s %s" % (first, relation, values[1]) diff --git a/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py b/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py new file mode 100644 index 00000000000..e5695b57c5c --- /dev/null +++ b/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py @@ -0,0 +1,72 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + +from pyomo.core.expr.logical_expr import NaryBooleanExpression, _flattened + + +class SpanExpression(NaryBooleanExpression): + """ + Expression over IntervalVars representing that the first arg spans all the + following args in the schedule. The first arg is absent if and only if all + the others are absent. + + args: + args (tuple): Child nodes, of type IntervalVar + """ + + def _to_string(self, values, verbose, smap): + return "%s.spans(%s)" % (values[0], ", ".join(values[1:])) + + +class AlternativeExpression(NaryBooleanExpression): + """ + Expression over IntervalVars representing that if the first arg is present, + then exactly one of the following args must be present. The first arg is + absent if and only if all the others are absent. + """ + + # [ESJ 4/4/24]: docplex takes an optional 'cardinality' argument with this + # too--it generalized to "exactly n" of the intervals have to exist, + # basically. It would be nice to include this eventually, but this is + # probably fine for now. + + def _to_string(self, values, verbose, smap): + return "alternative(%s, [%s])" % (values[0], ", ".join(values[1:])) + + +class SynchronizeExpression(NaryBooleanExpression): + """ + Expression over IntervalVars synchronizing the first argument with all of the + following arguments. That is, if the first argument is present, the remaining + arguments start and end at the same time as it. + """ + + def _to_string(self, values, verbose, smap): + return "synchronize(%s, [%s])" % (values[0], ", ".join(values[1:])) + + +def spans(*args): + """Creates a new SpanExpression""" + + return SpanExpression(list(_flattened(args))) + + +def alternative(*args): + """Creates a new AlternativeExpression""" + + return AlternativeExpression(list(_flattened(args))) + + +def synchronize(*args): + """Creates a new SynchronizeExpression""" + + return SynchronizeExpression(list(_flattened(args))) diff --git a/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py b/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py new file mode 100644 index 00000000000..3ba799074de --- /dev/null +++ b/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py @@ -0,0 +1,173 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.core.expr.logical_expr import BooleanExpression + + +class NoOverlapExpression(BooleanExpression): + """ + Expression representing that none of the IntervalVars in a SequenceVar overlap + (if they are scheduled) + + args: + args (tuple): Child node of type SequenceVar + """ + + def nargs(self): + return 1 + + def _to_string(self, values, verbose, smap): + return "no_overlap(%s)" % values[0] + + +class FirstInSequenceExpression(BooleanExpression): + """ + Expression representing that the specified IntervalVar is the first in the + sequence specified by SequenceVar (if it is scheduled) + + args: + args (tuple): Child nodes, the first of type IntervalVar, the second of type + SequenceVar + """ + + def nargs(self): + return 2 + + def _to_string(self, values, verbose, smap): + return "first_in(%s, %s)" % (values[0], values[1]) + + +class LastInSequenceExpression(BooleanExpression): + """ + Expression representing that the specified IntervalVar is the last in the + sequence specified by SequenceVar (if it is scheduled) + + args: + args (tuple): Child nodes, the first of type IntervalVar, the second of type + SequenceVar + """ + + def nargs(self): + return 2 + + def _to_string(self, values, verbose, smap): + return "last_in(%s, %s)" % (values[0], values[1]) + + +class BeforeInSequenceExpression(BooleanExpression): + """ + Expression representing that one IntervalVar occurs before another in the + sequence specified by the given SequenceVar (if both are scheduled) + + args: + args (tuple): Child nodes, the IntervalVar that must be before, the + IntervalVar that must be after, and the SequenceVar + """ + + def nargs(self): + return 3 + + def _to_string(self, values, verbose, smap): + return "before_in(%s, %s, %s)" % (values[0], values[1], values[2]) + + +class PredecessorToExpression(BooleanExpression): + """ + Expression representing that one IntervalVar is a direct predecessor to another + in the sequence specified by the given SequenceVar (if both are scheduled) + + args: + args (tuple): Child nodes, the predecessor IntervalVar, the successor + IntervalVar, and the SequenceVar + """ + + def nargs(self): + return 3 + + def _to_string(self, values, verbose, smap): + return "predecessor_to(%s, %s, %s)" % (values[0], values[1], values[2]) + + +def no_overlap(sequence_var): + """ + Creates a new NoOverlapExpression + + Requires that none of the scheduled intervals in the SequenceVar overlap each other + + args: + sequence_var: A SequenceVar + """ + return NoOverlapExpression((sequence_var,)) + + +def first_in_sequence(interval_var, sequence_var): + """ + Creates a new FirstInSequenceExpression + + Requires that 'interval_var' be the first in the sequence specified by + 'sequence_var' if it is scheduled + + args: + interval_var (IntervalVar): The activity that should be scheduled first + if it is scheduled at all + sequence_var (SequenceVar): The sequence of activities + """ + return FirstInSequenceExpression((interval_var, sequence_var)) + + +def last_in_sequence(interval_var, sequence_var): + """ + Creates a new LastInSequenceExpression + + Requires that 'interval_var' be the last in the sequence specified by + 'sequence_var' if it is scheduled + + args: + interval_var (IntervalVar): The activity that should be scheduled last + if it is scheduled at all + sequence_var (SequenceVar): The sequence of activities + """ + + return LastInSequenceExpression((interval_var, sequence_var)) + + +def before_in_sequence(before_var, after_var, sequence_var): + """ + Creates a new BeforeInSequenceExpression + + Requires that 'before_var' be scheduled to start before 'after_var' in the + sequence specified bv 'sequence_var', if both are scheduled + + args: + before_var (IntervalVar): The activity that should be scheduled earlier in + the sequence + after_var (IntervalVar): The activity that should be scheduled later in the + sequence + sequence_var (SequenceVar): The sequence of activities + """ + return BeforeInSequenceExpression((before_var, after_var, sequence_var)) + + +def predecessor_to(before_var, after_var, sequence_var): + """ + Creates a new PredecessorToExpression + + Requires that 'before_var' be a direct predecessor to 'after_var' in the + sequence specified by 'sequence_var', if both are scheduled + + args: + before_var (IntervalVar): The activity that should be scheduled as the + predecessor + after_var (IntervalVar): The activity that should be scheduled as the + successor + sequence_var (SequenceVar): The sequence of activities + """ + return PredecessorToExpression((before_var, after_var, sequence_var)) diff --git a/pyomo/contrib/cp/scheduling_expr/step_function_expressions.py b/pyomo/contrib/cp/scheduling_expr/step_function_expressions.py index b4f8fbb4977..129dff66b48 100644 --- a/pyomo/contrib/cp/scheduling_expr/step_function_expressions.py +++ b/pyomo/contrib/cp/scheduling_expr/step_function_expressions.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -15,7 +15,6 @@ IntervalVarStartTime, IntervalVarEndTime, ) -from pyomo.core.base.component import Component from pyomo.core.expr.base import ExpressionBase from pyomo.core.expr.logical_expr import BooleanExpression diff --git a/pyomo/contrib/cp/sequence_var.py b/pyomo/contrib/cp/sequence_var.py new file mode 100644 index 00000000000..cb42f445dc3 --- /dev/null +++ b/pyomo/contrib/cp/sequence_var.py @@ -0,0 +1,151 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import logging + +from pyomo.common.log import is_debug_set +from pyomo.common.modeling import NOTSET +from pyomo.contrib.cp import IntervalVar +from pyomo.core import ModelComponentFactory +from pyomo.core.base.component import ActiveComponentData +from pyomo.core.base.global_set import UnindexedComponent_index +from pyomo.core.base.indexed_component import ActiveIndexedComponent +from pyomo.core.base.initializer import Initializer + +import sys +from weakref import ref as weakref_ref + +logger = logging.getLogger(__name__) + + +class SequenceVarData(ActiveComponentData): + """This class defines the abstract interface for a single sequence variable.""" + + __slots__ = ('interval_vars',) + + def __init__(self, component=None): + # in-lining ActiveComponentData and ComponentData constructors, as is + # traditional: + self._component = weakref_ref(component) if (component is not None) else None + self._index = NOTSET + self._active = True + + # This thing is really just an ordered set of interval vars that we can + # write constraints over. + self.interval_vars = [] + + def set_value(self, expr): + # We'll demand expr be a list for now--it needs to be ordered so this + # doesn't seem like too much to ask + if not hasattr(expr, '__iter__'): + raise ValueError( + "'expr' for SequenceVar must be a list of IntervalVars. " + "Encountered type '%s' constructing '%s'" % (type(expr), self.name) + ) + for v in expr: + if not hasattr(v, 'ctype') or v.ctype is not IntervalVar: + raise ValueError( + "The SequenceVar 'expr' argument must be a list of " + "IntervalVars. The 'expr' for SequenceVar '%s' included " + "an object of type '%s'" % (self.name, type(v)) + ) + self.interval_vars.append(v) + + +@ModelComponentFactory.register("Sequences of IntervalVars") +class SequenceVar(ActiveIndexedComponent): + _ComponentDataClass = SequenceVarData + + def __new__(cls, *args, **kwds): + if cls != SequenceVar: + return super(SequenceVar, cls).__new__(cls) + if args == (): + return ScalarSequenceVar.__new__(ScalarSequenceVar) + else: + return IndexedSequenceVar.__new__(IndexedSequenceVar) + + def __init__(self, *args, **kwargs): + self._init_rule = Initializer(kwargs.pop('rule', None)) + self._init_expr = kwargs.pop('expr', None) + kwargs.setdefault('ctype', SequenceVar) + super(SequenceVar, self).__init__(*args, **kwargs) + + if self._init_expr is not None and self._init_rule is not None: + raise ValueError( + "Cannot specify both rule= and expr= for SequenceVar %s" % (self.name,) + ) + + def _getitem_when_not_present(self, index): + if index is None and not self.is_indexed(): + obj = self._data[index] = self + else: + obj = self._data[index] = self._ComponentDataClass(component=self) + parent = self.parent_block() + obj._index = index + + if self._init_rule is not None: + obj.set_value(self._init_rule(parent, index)) + if self._init_expr is not None: + obj.set_value(self._init_expr) + + return obj + + def construct(self, data=None): + """ + Construct the SequenceVarData objects for this SequenceVar + """ + if self._constructed: + return + self._constructed = True + + if is_debug_set(logger): + logger.debug("Constructing SequenceVar %s" % self.name) + + # Initialize index in case we hit the exception below + index = None + try: + if not self.is_indexed(): + self._getitem_when_not_present(None) + if self._init_rule is not None: + for index in self.index_set(): + self._getitem_when_not_present(index) + except Exception: + err = sys.exc_info()[1] + logger.error( + "Rule failed when initializing sequence variable for " + "SequenceVar %s with index %s:\n%s: %s" + % (self.name, str(index), type(err).__name__, err) + ) + raise + + def _pprint(self): + """Print component information.""" + headers = [ + ("Size", len(self)), + ("Index", self._index_set if self.is_indexed() else None), + ] + return ( + headers, + self._data.items(), + ("IntervalVars",), + lambda k, v: ['[' + ', '.join(iv.name for iv in v.interval_vars) + ']'], + ) + + +class ScalarSequenceVar(SequenceVarData, SequenceVar): + def __init__(self, *args, **kwds): + SequenceVarData.__init__(self, component=self) + SequenceVar.__init__(self, *args, **kwds) + self._index = UnindexedComponent_index + + +class IndexedSequenceVar(SequenceVar): + pass diff --git a/pyomo/contrib/cp/tests/__init__.py b/pyomo/contrib/cp/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/cp/tests/__init__.py +++ b/pyomo/contrib/cp/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/cp/tests/test_docplex_walker.py b/pyomo/contrib/cp/tests/test_docplex_walker.py index 97bc538c827..f7abb3d2b3c 100644 --- a/pyomo/contrib/cp/tests/test_docplex_walker.py +++ b/pyomo/contrib/cp/tests/test_docplex_walker.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -11,17 +11,28 @@ import pyomo.common.unittest as unittest -from pyomo.contrib.cp import IntervalVar -from pyomo.contrib.cp.scheduling_expr.step_function_expressions import ( - AlwaysIn, - Step, - Pulse, +from pyomo.contrib.cp import ( + IntervalVar, + SequenceVar, + no_overlap, + first_in_sequence, + last_in_sequence, + alternative, + synchronize, ) +from pyomo.contrib.cp.scheduling_expr.step_function_expressions import Step, Pulse from pyomo.contrib.cp.repn.docplex_writer import docplex_available, LogicalToDoCplex from pyomo.core.base.range import NumericRange from pyomo.core.expr.numeric_expr import MinExpression, MaxExpression -from pyomo.core.expr.logical_expr import equivalent, exactly, atleast, atmost +from pyomo.core.expr.logical_expr import ( + equivalent, + exactly, + atleast, + atmost, + all_different, + count_if, +) from pyomo.core.expr.relational_expr import NotEqualExpression from pyomo.environ import ( @@ -39,8 +50,6 @@ Integers, inequality, Expression, - Reals, - Set, Param, ) @@ -91,6 +100,10 @@ def test_write_addition(self): expr[1].equals(cpx_x + cp.start_of(cpx_i) + cp.length_of(cpx_i2)) ) + self.assertIs(visitor.pyomo_to_docplex[m.x], cpx_x) + self.assertIs(visitor.pyomo_to_docplex[m.i], cpx_i) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], cpx_i2) + def test_write_subtraction(self): m = self.get_model() m.a.domain = Binary @@ -106,6 +119,9 @@ def test_write_subtraction(self): self.assertTrue(expr[1].equals(x + (-1 * a1))) + self.assertIs(visitor.pyomo_to_docplex[m.x], x) + self.assertIs(visitor.pyomo_to_docplex[m.a[1]], a1) + def test_write_product(self): m = self.get_model() m.a.domain = PositiveIntegers @@ -121,6 +137,9 @@ def test_write_product(self): self.assertTrue(expr[1].equals(x * (a1 + 1))) + self.assertIs(visitor.pyomo_to_docplex[m.x], x) + self.assertIs(visitor.pyomo_to_docplex[m.a[1]], a1) + def test_write_floating_point_division(self): m = self.get_model() m.a.domain = NonNegativeIntegers @@ -136,6 +155,9 @@ def test_write_floating_point_division(self): self.assertTrue(expr[1].equals(x / (a1 + 1))) + self.assertIs(visitor.pyomo_to_docplex[m.x], x) + self.assertIs(visitor.pyomo_to_docplex[m.a[1]], a1) + def test_write_power_expression(self): m = self.get_model() m.c = Constraint(expr=m.x**2 <= 3) @@ -147,6 +169,8 @@ def test_write_power_expression(self): # .equals checks the equality of two expressions in docplex. self.assertTrue(expr[1].equals(cpx_x**2)) + self.assertIs(visitor.pyomo_to_docplex[m.x], cpx_x) + def test_write_absolute_value_expression(self): m = self.get_model() m.a.domain = NegativeIntegers @@ -160,6 +184,8 @@ def test_write_absolute_value_expression(self): self.assertTrue(expr[1].equals(cp.abs(a1) + 1)) + self.assertIs(visitor.pyomo_to_docplex[m.a[1]], a1) + def test_write_min_expression(self): m = self.get_model() m.a.domain = NonPositiveIntegers @@ -171,6 +197,7 @@ def test_write_min_expression(self): for i in m.I: self.assertIn(id(m.a[i]), visitor.var_map) a[i] = visitor.var_map[id(m.a[i])] + self.assertIs(visitor.pyomo_to_docplex[m.a[i]], a[i]) self.assertTrue(expr[1].equals(cp.min(a[i] for i in m.I))) @@ -185,6 +212,7 @@ def test_write_max_expression(self): for i in m.I: self.assertIn(id(m.a[i]), visitor.var_map) a[i] = visitor.var_map[id(m.a[i])] + self.assertIs(visitor.pyomo_to_docplex[m.a[i]], a[i]) self.assertTrue(expr[1].equals(cp.max(a[i] for i in m.I))) @@ -202,6 +230,35 @@ def test_expression_with_mutable_param(self): self.assertTrue(expr[1].equals(4 * x)) + def test_monomial_expressions(self): + m = ConcreteModel() + m.x = Var(domain=Integers, bounds=(1, 4)) + m.p = Param(initialize=4, mutable=True) + + visitor = self.get_visitor() + + const_expr = 3 * m.x + nested_expr = (1 / m.p) * m.x + pow_expr = (m.p ** (0.5)) * m.x + + e = m.x * 4 + expr = visitor.walk_expression((e, e, 0)) + self.assertIn(id(m.x), visitor.var_map) + x = visitor.var_map[id(m.x)] + self.assertTrue(expr[1].equals(4 * x)) + + e = 1.0 * m.x + expr = visitor.walk_expression((e, e, 0)) + self.assertTrue(expr[1].equals(x)) + + e = (1 / m.p) * m.x + expr = visitor.walk_expression((e, e, 0)) + self.assertTrue(expr[1].equals(cp.float_div(1, 4) * x)) + + e = (m.p ** (0.5)) * m.x + expr = visitor.walk_expression((e, e, 0)) + self.assertTrue(expr[1].equals(cp.power(4, 0.5) * x)) + @unittest.skipIf(not docplex_available, "docplex is not available") class TestCPExpressionWalker_LogicalExpressions(CommonTest): @@ -219,6 +276,14 @@ def test_write_logical_and(self): self.assertTrue(expr[1].equals(cp.logical_and(b, b2b))) + # ESJ: This is ludicrous, but I don't know how to get the args of a CP + # expression, so testing that we were correct in the pyomo to docplex + # map by checking that we can build an expression that is the same as b + # (because b is actually "b == 1" since docplex doesn't believe in + # Booleans) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + self.assertTrue(b2b.equals(visitor.pyomo_to_docplex[m.b2['b']] == 1)) + def test_write_logical_or(self): m = self.get_model() m.c = LogicalConstraint(expr=m.b.lor(m.i.is_present)) @@ -232,6 +297,9 @@ def test_write_logical_or(self): self.assertTrue(expr[1].equals(cp.logical_or(b, cp.presence_of(i)))) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + def test_write_xor(self): m = self.get_model() m.c = LogicalConstraint(expr=m.b.xor(m.i2[2].start_time >= 5)) @@ -249,6 +317,9 @@ def test_write_xor(self): expr[1].equals(cp.count([b, cp.less_or_equal(5, cp.start_of(i22))], 1) == 1) ) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + def test_write_logical_not(self): m = self.get_model() m.c = LogicalConstraint(expr=~m.b2['a']) @@ -260,6 +331,8 @@ def test_write_logical_not(self): self.assertTrue(expr[1].equals(cp.logical_not(b2a))) + self.assertTrue(b2a.equals(visitor.pyomo_to_docplex[m.b2['a']] == 1)) + def test_equivalence(self): m = self.get_model() m.c = LogicalConstraint(expr=equivalent(~m.b2['a'], m.b)) @@ -273,18 +346,8 @@ def test_equivalence(self): self.assertTrue(expr[1].equals(cp.equal(cp.logical_not(b2a), b))) - def test_implication(self): - m = self.get_model() - m.c = LogicalConstraint(expr=m.b2['a'].implies(~m.b)) - visitor = self.get_visitor() - expr = visitor.walk_expression((m.c.expr, m.c, 0)) - - self.assertIn(id(m.b), visitor.var_map) - self.assertIn(id(m.b2['a']), visitor.var_map) - b = visitor.var_map[id(m.b)] - b2a = visitor.var_map[id(m.b2['a'])] - - self.assertTrue(expr[1].equals(cp.if_then(b2a, cp.logical_not(b)))) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + self.assertTrue(b2a.equals(visitor.pyomo_to_docplex[m.b2['a']] == 1)) def test_equality(self): m = self.get_model() @@ -301,6 +364,9 @@ def test_equality(self): self.assertTrue(expr[1].equals(cp.if_then(b, cp.equal(a3, 4)))) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + self.assertIs(visitor.pyomo_to_docplex[m.a[3]], a3) + def test_inequality(self): m = self.get_model() m.a.domain = Integers @@ -318,6 +384,10 @@ def test_inequality(self): self.assertTrue(expr[1].equals(cp.if_then(b, cp.less_or_equal(a4, a3)))) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + self.assertIs(visitor.pyomo_to_docplex[m.a[3]], a3) + self.assertIs(visitor.pyomo_to_docplex[m.a[4]], a4) + def test_ranged_inequality(self): m = self.get_model() m.a.domain = Integers @@ -348,6 +418,10 @@ def test_not_equal(self): self.assertTrue(expr[1].equals(cp.if_then(b, a3 != a4))) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + self.assertIs(visitor.pyomo_to_docplex[m.a[3]], a3) + self.assertIs(visitor.pyomo_to_docplex[m.a[4]], a4) + def test_exactly_expression(self): m = self.get_model() m.a.domain = Integers @@ -360,6 +434,7 @@ def test_exactly_expression(self): for i in m.I: self.assertIn(id(m.a[i]), visitor.var_map) a[i] = visitor.var_map[id(m.a[i])] + self.assertIs(visitor.pyomo_to_docplex[m.a[i]], a[i]) self.assertTrue( expr[1].equals(cp.equal(cp.count([a[i] == 4 for i in m.I], 1), 3)) @@ -377,6 +452,7 @@ def test_atleast_expression(self): for i in m.I: self.assertIn(id(m.a[i]), visitor.var_map) a[i] = visitor.var_map[id(m.a[i])] + self.assertIs(visitor.pyomo_to_docplex[m.a[i]], a[i]) self.assertTrue( expr[1].equals( @@ -396,11 +472,46 @@ def test_atmost_expression(self): for i in m.I: self.assertIn(id(m.a[i]), visitor.var_map) a[i] = visitor.var_map[id(m.a[i])] + self.assertIs(visitor.pyomo_to_docplex[m.a[i]], a[i]) self.assertTrue( expr[1].equals(cp.less_or_equal(cp.count([a[i] == 4 for i in m.I], 1), 3)) ) + def test_all_diff_expression(self): + m = self.get_model() + m.a.domain = Integers + m.a.bounds = (11, 20) + m.c = LogicalConstraint(expr=all_different(m.a)) + + visitor = self.get_visitor() + expr = visitor.walk_expression((m.c.body, m.c, 0)) + + a = {} + for i in m.I: + self.assertIn(id(m.a[i]), visitor.var_map) + a[i] = visitor.var_map[id(m.a[i])] + self.assertIs(visitor.pyomo_to_docplex[m.a[i]], a[i]) + + self.assertTrue(expr[1].equals(cp.all_diff(a[i] for i in m.I))) + + def test_count_if_expression(self): + m = self.get_model() + m.a.domain = Integers + m.a.bounds = (11, 20) + m.c = Constraint(expr=count_if(m.a[i] == i for i in m.I) == 5) + + visitor = self.get_visitor() + expr = visitor.walk_expression((m.c.expr, m.c, 0)) + + a = {} + for i in m.I: + self.assertIn(id(m.a[i]), visitor.var_map) + a[i] = visitor.var_map[id(m.a[i])] + self.assertIs(visitor.pyomo_to_docplex[m.a[i]], a[i]) + + self.assertTrue(expr[1].equals(cp.count((a[i] == i for i in m.I), 1) == 5)) + def test_interval_var_is_present(self): m = self.get_model() m.a.domain = Integers @@ -416,6 +527,9 @@ def test_interval_var_is_present(self): self.assertTrue(expr[1].equals(cp.if_then(cp.presence_of(i), a1 == 5))) + self.assertIs(visitor.pyomo_to_docplex[m.a[1]], a1) + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + def test_interval_var_is_present_indirection(self): m = self.get_model() m.a.domain = Integers @@ -449,6 +563,11 @@ def test_interval_var_is_present_indirection(self): ) ) + self.assertIs(visitor.pyomo_to_docplex[m.a[1]], a1) + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + def test_is_present_indirection_and_length(self): m = self.get_model() m.y = Var(domain=Integers, bounds=[1, 2]) @@ -483,6 +602,10 @@ def test_is_present_indirection_and_length(self): ) ) + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + def test_handle_getattr_lor(self): m = self.get_model() m.y = Var(domain=Integers, bounds=(1, 2)) @@ -514,6 +637,11 @@ def test_handle_getattr_lor(self): ) ) + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + def test_handle_getattr_xor(self): m = self.get_model() m.y = Var(domain=Integers, bounds=(1, 2)) @@ -552,6 +680,11 @@ def test_handle_getattr_xor(self): ) ) + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + def test_handle_getattr_equivalent_to(self): m = self.get_model() m.y = Var(domain=Integers, bounds=(1, 2)) @@ -583,6 +716,11 @@ def test_handle_getattr_equivalent_to(self): ) ) + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + def test_logical_or_on_indirection(self): m = ConcreteModel() m.b = BooleanVar([2, 3, 4, 5]) @@ -612,6 +750,11 @@ def test_logical_or_on_indirection(self): ) ) + self.assertIs(visitor.pyomo_to_docplex[m.x], x) + self.assertTrue(b3.equals(visitor.pyomo_to_docplex[m.b[3]] == 1)) + self.assertTrue(b4.equals(visitor.pyomo_to_docplex[m.b[4]] == 1)) + self.assertTrue(b5.equals(visitor.pyomo_to_docplex[m.b[5]] == 1)) + def test_logical_xor_on_indirection(self): m = ConcreteModel() m.b = BooleanVar([2, 3, 4, 5]) @@ -646,6 +789,10 @@ def test_logical_xor_on_indirection(self): ) ) + self.assertIs(visitor.pyomo_to_docplex[m.x], x) + self.assertTrue(b3.equals(visitor.pyomo_to_docplex[m.b[3]] == 1)) + self.assertTrue(b5.equals(visitor.pyomo_to_docplex[m.b[5]] == 1)) + def test_using_precedence_expr_as_boolean_expr(self): m = self.get_model() e = m.b.implies(m.i2[2].start_time.before(m.i2[1].start_time)) @@ -665,6 +812,10 @@ def test_using_precedence_expr_as_boolean_expr(self): expr[1].equals(cp.if_then(b, cp.start_of(i22) + 0 <= cp.start_of(i21))) ) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + def test_using_precedence_expr_as_boolean_expr_positive_delay(self): m = self.get_model() e = m.b.implies(m.i2[2].start_time.before(m.i2[1].start_time, delay=4)) @@ -684,6 +835,10 @@ def test_using_precedence_expr_as_boolean_expr_positive_delay(self): expr[1].equals(cp.if_then(b, cp.start_of(i22) + 4 <= cp.start_of(i21))) ) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + def test_using_precedence_expr_as_boolean_expr_negative_delay(self): m = self.get_model() e = m.b.implies(m.i2[2].start_time.at(m.i2[1].start_time, delay=-3)) @@ -703,6 +858,10 @@ def test_using_precedence_expr_as_boolean_expr_negative_delay(self): expr[1].equals(cp.if_then(b, cp.start_of(i22) + (-3) == cp.start_of(i21))) ) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + @unittest.skipIf(not docplex_available, "docplex is not available") class TestCPExpressionWalker_IntervalVars(CommonTest): @@ -716,6 +875,7 @@ def test_interval_var_fixed_presences_correct(self): i = visitor.var_map[id(m.i)] # Check that docplex knows it's optional self.assertTrue(i.is_optional()) + self.assertIs(visitor.pyomo_to_docplex[m.i], i) # Now fix it to absent m.i.is_present.fix(False) @@ -726,8 +886,10 @@ def test_interval_var_fixed_presences_correct(self): self.assertIn(id(m.i2[1]), visitor.var_map) i21 = visitor.var_map[id(m.i2[1])] + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) self.assertIn(id(m.i), visitor.var_map) i = visitor.var_map[id(m.i)] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) # Check that we passed on the presence info to docplex self.assertTrue(i.is_absent()) @@ -746,6 +908,7 @@ def test_interval_var_fixed_length(self): self.assertIn(id(m.i), visitor.var_map) i = visitor.var_map[id(m.i)] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) self.assertTrue(i.is_optional()) self.assertEqual(i.get_length(), (4, 4)) @@ -763,12 +926,85 @@ def test_interval_var_fixed_start_and_end(self): self.assertIn(id(m.i), visitor.var_map) i = visitor.var_map[id(m.i)] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) self.assertFalse(i.is_optional()) self.assertEqual(i.get_start(), (3, 3)) self.assertEqual(i.get_end(), (6, 6)) +@unittest.skipIf(not docplex_available, "docplex is not available") +class TestCPExpressionWalker_SequenceVars(CommonTest): + def get_model(self): + m = super().get_model() + m.seq = SequenceVar(expr=[m.i, m.i2[1], m.i2[2]]) + + return m + + def check_scalar_sequence_var(self, m, visitor): + self.assertIn(id(m.seq), visitor.var_map) + seq = visitor.var_map[id(m.seq)] + self.assertIs(visitor.pyomo_to_docplex[m.seq], seq) + + i = visitor.var_map[id(m.i)] + i21 = visitor.var_map[id(m.i2[1])] + i22 = visitor.var_map[id(m.i2[2])] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + + ivs = seq.get_interval_variables() + self.assertEqual(len(ivs), 3) + self.assertIs(ivs[0], i) + self.assertIs(ivs[1], i21) + self.assertIs(ivs[2], i22) + + return seq, i, i21, i22 + + def test_scalar_sequence_var(self): + m = self.get_model() + + visitor = self.get_visitor() + expr = visitor.walk_expression((m.seq, m.seq, 0)) + self.check_scalar_sequence_var(m, visitor) + + def test_no_overlap(self): + m = self.get_model() + e = no_overlap(m.seq) + visitor = self.get_visitor() + expr = visitor.walk_expression((e, e, 0)) + + seq, i, i21, i22 = self.check_scalar_sequence_var(m, visitor) + self.assertTrue(expr[1].equals(cp.no_overlap(seq))) + + def test_first_in_sequence(self): + m = self.get_model() + e = first_in_sequence(m.i2[1], m.seq) + visitor = self.get_visitor() + expr = visitor.walk_expression((e, e, 0)) + + seq, i, i21, i22 = self.check_scalar_sequence_var(m, visitor) + self.assertTrue(expr[1].equals(cp.first(seq, i21))) + + def test_before_in_sequence(self): + m = self.get_model() + e = last_in_sequence(m.i, m.seq) + visitor = self.get_visitor() + expr = visitor.walk_expression((e, e, 0)) + + seq, i, i21, i22 = self.check_scalar_sequence_var(m, visitor) + self.assertTrue(expr[1].equals(cp.last(seq, i))) + + def test_last_in_sequence(self): + m = self.get_model() + e = last_in_sequence(m.i2[1], m.seq) + visitor = self.get_visitor() + expr = visitor.walk_expression((e, e, 0)) + + seq, i, i21, i22 = self.check_scalar_sequence_var(m, visitor) + self.assertTrue(expr[1].equals(cp.last(seq, i21))) + + @unittest.skipIf(not docplex_available, "docplex is not available") class TestCPExpressionWalker_PrecedenceExpressions(CommonTest): def test_start_before_start(self): @@ -782,6 +1018,8 @@ def test_start_before_start(self): i = visitor.var_map[id(m.i)] i21 = visitor.var_map[id(m.i2[1])] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) self.assertTrue(expr[1].equals(cp.start_before_start(i, i21, 0))) @@ -796,6 +1034,8 @@ def test_start_before_end(self): i = visitor.var_map[id(m.i)] i21 = visitor.var_map[id(m.i2[1])] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) self.assertTrue(expr[1].equals(cp.start_before_end(i, i21, 3))) @@ -810,6 +1050,8 @@ def test_end_before_start(self): i = visitor.var_map[id(m.i)] i21 = visitor.var_map[id(m.i2[1])] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) self.assertTrue(expr[1].equals(cp.end_before_start(i, i21, -2))) @@ -824,6 +1066,8 @@ def test_end_before_end(self): i = visitor.var_map[id(m.i)] i21 = visitor.var_map[id(m.i2[1])] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) self.assertTrue(expr[1].equals(cp.end_before_end(i, i21, 6))) @@ -838,6 +1082,8 @@ def test_start_at_start(self): i = visitor.var_map[id(m.i)] i21 = visitor.var_map[id(m.i2[1])] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) self.assertTrue(expr[1].equals(cp.start_at_start(i, i21, 0))) @@ -852,6 +1098,8 @@ def test_start_at_end(self): i = visitor.var_map[id(m.i)] i21 = visitor.var_map[id(m.i2[1])] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) self.assertTrue(expr[1].equals(cp.start_at_end(i, i21, 3))) @@ -866,6 +1114,8 @@ def test_end_at_start(self): i = visitor.var_map[id(m.i)] i21 = visitor.var_map[id(m.i2[1])] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) self.assertTrue(expr[1].equals(cp.end_at_start(i, i21, -2))) @@ -880,6 +1130,8 @@ def test_end_at_end(self): i = visitor.var_map[id(m.i)] i21 = visitor.var_map[id(m.i2[1])] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) self.assertTrue(expr[1].equals(cp.end_at_end(i, i21, 6))) @@ -904,6 +1156,10 @@ def test_indirection_before_constraint(self): i21 = visitor.var_map[id(m.i2[1])] i22 = visitor.var_map[id(m.i2[2])] i = visitor.var_map[id(m.i)] + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertIs(visitor.pyomo_to_docplex[m.i], i) self.assertTrue( expr[1].equals( @@ -930,6 +1186,10 @@ def test_indirection_after_constraint(self): i21 = visitor.var_map[id(m.i2[1])] i22 = visitor.var_map[id(m.i2[2])] i = visitor.var_map[id(m.i)] + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertIs(visitor.pyomo_to_docplex[m.i], i) self.assertTrue( expr[1].equals( @@ -957,6 +1217,10 @@ def test_indirection_at_constraint(self): i21 = visitor.var_map[id(m.i2[1])] i22 = visitor.var_map[id(m.i2[2])] i = visitor.var_map[id(m.i)] + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertIs(visitor.pyomo_to_docplex[m.i], i) self.assertTrue( expr[1].equals( @@ -984,6 +1248,10 @@ def test_before_indirection_constraint(self): i21 = visitor.var_map[id(m.i2[1])] i22 = visitor.var_map[id(m.i2[2])] i = visitor.var_map[id(m.i)] + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertIs(visitor.pyomo_to_docplex[m.i], i) self.assertTrue( expr[1].equals( @@ -1009,6 +1277,10 @@ def test_after_indirection_constraint(self): i21 = visitor.var_map[id(m.i2[1])] i22 = visitor.var_map[id(m.i2[2])] i = visitor.var_map[id(m.i)] + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertIs(visitor.pyomo_to_docplex[m.i], i) self.assertTrue( expr[1].equals( @@ -1034,6 +1306,10 @@ def test_at_indirection_constraint(self): i21 = visitor.var_map[id(m.i2[1])] i22 = visitor.var_map[id(m.i2[2])] i = visitor.var_map[id(m.i)] + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertIs(visitor.pyomo_to_docplex[m.i], i) self.assertTrue( expr[1].equals( @@ -1068,6 +1344,13 @@ def test_double_indirection_before_constraint(self): i33 = visitor.var_map[id(m.i3[1, 3])] i34 = visitor.var_map[id(m.i3[1, 4])] i35 = visitor.var_map[id(m.i3[1, 5])] + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.x], x) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertIs(visitor.pyomo_to_docplex[m.i3[1, 3]], i33) + self.assertIs(visitor.pyomo_to_docplex[m.i3[1, 4]], i34) + self.assertIs(visitor.pyomo_to_docplex[m.i3[1, 5]], i35) self.assertTrue( expr[1].equals( @@ -1105,6 +1388,13 @@ def test_double_indirection_after_constraint(self): i33 = visitor.var_map[id(m.i3[1, 3])] i34 = visitor.var_map[id(m.i3[1, 4])] i35 = visitor.var_map[id(m.i3[1, 5])] + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.x], x) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertIs(visitor.pyomo_to_docplex[m.i3[1, 3]], i33) + self.assertIs(visitor.pyomo_to_docplex[m.i3[1, 4]], i34) + self.assertIs(visitor.pyomo_to_docplex[m.i3[1, 5]], i35) self.assertTrue( expr[1].equals( @@ -1140,6 +1430,13 @@ def test_double_indirection_at_constraint(self): i33 = visitor.var_map[id(m.i3[1, 3])] i34 = visitor.var_map[id(m.i3[1, 4])] i35 = visitor.var_map[id(m.i3[1, 5])] + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.x], x) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertIs(visitor.pyomo_to_docplex[m.i3[1, 3]], i33) + self.assertIs(visitor.pyomo_to_docplex[m.i3[1, 4]], i34) + self.assertIs(visitor.pyomo_to_docplex[m.i3[1, 5]], i35) self.assertTrue( expr[1].equals( @@ -1187,10 +1484,91 @@ def param_rule(m, i): self.assertIn(id(m.a), visitor.var_map) x = visitor.var_map[id(m.x)] a = visitor.var_map[id(m.a)] + self.assertIs(visitor.pyomo_to_docplex[m.x], x) + self.assertIs(visitor.pyomo_to_docplex[m.a], a) self.assertTrue(expr[1].equals(cp.element([2, 4, 6], 0 + 1 * (x - 1) // 2) / a)) +@unittest.skipIf(not docplex_available, "docplex is not available") +class TestCPExpressionWalker_HierarchicalScheduling(CommonTest): + def get_model(self): + m = ConcreteModel() + + def start_rule(m, i): + return 2 * i + + def length_rule(m, i): + return i + + m.iv = IntervalVar( + [1, 2, 3], start=start_rule, length=length_rule, optional=True + ) + m.whole_enchilada = IntervalVar() + + return m + + def test_spans(self): + m = self.get_model() + e = m.whole_enchilada.spans(m.iv[i] for i in [1, 2, 3]) + + visitor = self.get_visitor() + expr = visitor.walk_expression((e, e, 0)) + + self.assertIn(id(m.whole_enchilada), visitor.var_map) + whole_enchilada = visitor.var_map[id(m.whole_enchilada)] + self.assertIs(visitor.pyomo_to_docplex[m.whole_enchilada], whole_enchilada) + + iv = {} + for i in [1, 2, 3]: + self.assertIn(id(m.iv[i]), visitor.var_map) + iv[i] = visitor.var_map[id(m.iv[i])] + + self.assertTrue( + expr[1].equals(cp.span(whole_enchilada, [iv[i] for i in [1, 2, 3]])) + ) + + def test_alternative(self): + m = self.get_model() + e = alternative(m.whole_enchilada, [m.iv[i] for i in [1, 2, 3]]) + + visitor = self.get_visitor() + expr = visitor.walk_expression((e, e, 0)) + + self.assertIn(id(m.whole_enchilada), visitor.var_map) + whole_enchilada = visitor.var_map[id(m.whole_enchilada)] + self.assertIs(visitor.pyomo_to_docplex[m.whole_enchilada], whole_enchilada) + + iv = {} + for i in [1, 2, 3]: + self.assertIn(id(m.iv[i]), visitor.var_map) + iv[i] = visitor.var_map[id(m.iv[i])] + + self.assertTrue( + expr[1].equals(cp.alternative(whole_enchilada, [iv[i] for i in [1, 2, 3]])) + ) + + def test_synchronize(self): + m = self.get_model() + e = synchronize(m.whole_enchilada, [m.iv[i] for i in [1, 2, 3]]) + + visitor = self.get_visitor() + expr = visitor.walk_expression((e, e, 0)) + + self.assertIn(id(m.whole_enchilada), visitor.var_map) + whole_enchilada = visitor.var_map[id(m.whole_enchilada)] + self.assertIs(visitor.pyomo_to_docplex[m.whole_enchilada], whole_enchilada) + + iv = {} + for i in [1, 2, 3]: + self.assertIn(id(m.iv[i]), visitor.var_map) + iv[i] = visitor.var_map[id(m.iv[i])] + + self.assertTrue( + expr[1].equals(cp.synchronize(whole_enchilada, [iv[i] for i in [1, 2, 3]])) + ) + + @unittest.skipIf(not docplex_available, "docplex is not available") class TestCPExpressionWalker_CumulFuncExpressions(CommonTest): def test_always_in(self): @@ -1212,6 +1590,9 @@ def test_always_in(self): i = visitor.var_map[id(m.i)] i21 = visitor.var_map[id(m.i2[1])] i22 = visitor.var_map[id(m.i2[2])] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) self.assertTrue( expr[1].equals( @@ -1227,6 +1608,24 @@ def test_always_in(self): ) ) + def test_always_in_single_pulse(self): + # This is a bit silly as you can tell whether or not it is feasible + # structurally, but there's no reason it couldn't happen. + m = self.get_model() + f = Pulse((m.i, 3)) + m.c = LogicalConstraint(expr=f.within((0, 3), (0, 10))) + visitor = self.get_visitor() + expr = visitor.walk_expression((m.c.expr, m.c, 0)) + + self.assertIn(id(m.i), visitor.var_map) + + i = visitor.var_map[id(m.i)] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + + self.assertTrue( + expr[1].equals(cp.always_in(cp.pulse(i, 3), interval=(0, 10), min=0, max=3)) + ) + @unittest.skipIf(not docplex_available, "docplex is not available") class TestCPExpressionWalker_NamedExpressions(CommonTest): @@ -1240,6 +1639,7 @@ def test_named_expression(self): self.assertIn(id(m.x), visitor.var_map) x = visitor.var_map[id(m.x)] + self.assertIs(visitor.pyomo_to_docplex[m.x], x) self.assertTrue(expr[1].equals(x**2 + 7)) @@ -1253,6 +1653,7 @@ def test_repeated_named_expression(self): self.assertIn(id(m.x), visitor.var_map) x = visitor.var_map[id(m.x)] + self.assertIs(visitor.pyomo_to_docplex[m.x], x) self.assertTrue(expr[1].equals(x**2 + 7 + (-1) * (8 * (x**2 + 7)))) @@ -1283,6 +1684,7 @@ def test_fixed_integer_var(self): self.assertIn(id(m.a[2]), visitor.var_map) a2 = visitor.var_map[id(m.a[2])] + self.assertIs(visitor.pyomo_to_docplex[m.a[2]], a2) self.assertTrue(expr[1].equals(3 + a2)) @@ -1297,6 +1699,7 @@ def test_fixed_boolean_var(self): self.assertIn(id(m.b2['b']), visitor.var_map) b2b = visitor.var_map[id(m.b2['b'])] + self.assertTrue(b2b.equals(visitor.pyomo_to_docplex[m.b2['b']] == 1)) self.assertTrue(expr[1].equals(cp.logical_or(False, cp.logical_and(True, b2b)))) @@ -1310,13 +1713,16 @@ def test_indirection_single_index(self): self.assertIn(id(m.x), visitor.var_map) x = visitor.var_map[id(m.x)] + self.assertIs(visitor.pyomo_to_docplex[m.x], x) a = [] # only need indices 6, 7, and 8 from a, since that's what x is capable # of selecting. for idx in [6, 7, 8]: v = m.a[idx] self.assertIn(id(v), visitor.var_map) - a.append(visitor.var_map[id(v)]) + cpx_v = visitor.var_map[id(v)] + self.assertIs(visitor.pyomo_to_docplex[v], cpx_v) + a.append(cpx_v) # since x is between 6 and 8, we subtract 6 from it for it to be the # right index self.assertTrue(expr[1].equals(cp.element(a, 0 + 1 * (x - 6) // 1))) @@ -1334,8 +1740,10 @@ def test_indirection_multi_index_second_constant(self): for i in [6, 7, 8]: self.assertIn(id(m.z[i, 3]), visitor.var_map) z[i, 3] = visitor.var_map[id(m.z[i, 3])] + self.assertIs(visitor.pyomo_to_docplex[m.z[i, 3]], z[i, 3]) self.assertIn(id(m.x), visitor.var_map) x = visitor.var_map[id(m.x)] + self.assertIs(visitor.pyomo_to_docplex[m.x], x) self.assertTrue( expr[1].equals( @@ -1356,8 +1764,11 @@ def test_indirection_multi_index_first_constant(self): for i in [6, 7, 8]: self.assertIn(id(m.z[3, i]), visitor.var_map) z[3, i] = visitor.var_map[id(m.z[3, i])] + self.assertIs(visitor.pyomo_to_docplex[m.z[3, i]], z[3, i]) + self.assertIn(id(m.x), visitor.var_map) x = visitor.var_map[id(m.x)] + self.assertIs(visitor.pyomo_to_docplex[m.x], x) self.assertTrue( expr[1].equals( @@ -1379,8 +1790,11 @@ def test_indirection_multi_index_neither_constant_same_var(self): for j in [6, 7, 8]: self.assertIn(id(m.z[i, j]), visitor.var_map) z[i, j] = visitor.var_map[id(m.z[i, j])] + self.assertIs(visitor.pyomo_to_docplex[m.z[i, j]], z[i, j]) + self.assertIn(id(m.x), visitor.var_map) x = visitor.var_map[id(m.x)] + self.assertIs(visitor.pyomo_to_docplex[m.x], x) self.assertTrue( expr[1].equals( @@ -1404,12 +1818,17 @@ def test_indirection_multi_index_neither_constant_diff_vars(self): z = {} for i in [6, 7, 8]: for j in [1, 3, 5]: - self.assertIn(id(m.z[i, 3]), visitor.var_map) + self.assertIn(id(m.z[i, j]), visitor.var_map) z[i, j] = visitor.var_map[id(m.z[i, j])] + self.assertIs(visitor.pyomo_to_docplex[m.z[i, j]], z[i, j]) + self.assertIn(id(m.x), visitor.var_map) x = visitor.var_map[id(m.x)] + self.assertIs(visitor.pyomo_to_docplex[m.x], x) + self.assertIn(id(m.y), visitor.var_map) y = visitor.var_map[id(m.y)] + self.assertIs(visitor.pyomo_to_docplex[m.y], y) self.assertTrue( expr[1].equals( @@ -1434,10 +1853,14 @@ def test_indirection_expression_index(self): for i in range(1, 8): self.assertIn(id(m.a[i]), visitor.var_map) a[i] = visitor.var_map[id(m.a[i])] + self.assertIs(visitor.pyomo_to_docplex[m.a[i]], a[i]) + self.assertIn(id(m.x), visitor.var_map) x = visitor.var_map[id(m.x)] + self.assertIs(visitor.pyomo_to_docplex[m.x], x) self.assertIn(id(m.y), visitor.var_map) y = visitor.var_map[id(m.y)] + self.assertIs(visitor.pyomo_to_docplex[m.y], y) self.assertTrue( expr[1].equals( diff --git a/pyomo/contrib/cp/tests/test_docplex_writer.py b/pyomo/contrib/cp/tests/test_docplex_writer.py index d569ef2e696..4f6039993c3 100644 --- a/pyomo/contrib/cp/tests/test_docplex_writer.py +++ b/pyomo/contrib/cp/tests/test_docplex_writer.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -12,13 +12,25 @@ import pyomo.common.unittest as unittest from pyomo.common.fileutils import Executable -from pyomo.contrib.cp import IntervalVar, Pulse, Step, AlwaysIn +from pyomo.contrib.cp import ( + IntervalVar, + SequenceVar, + Pulse, + Step, + AlwaysIn, + first_in_sequence, + predecessor_to, + no_overlap, +) from pyomo.contrib.cp.repn.docplex_writer import LogicalToDoCplex from pyomo.environ import ( + all_different, + count_if, ConcreteModel, Set, Var, Integers, + Param, LogicalConstraint, implies, value, @@ -254,3 +266,159 @@ def x_bounds(m, i): self.assertEqual(results.problem.sense, minimize) self.assertEqual(results.problem.lower_bound, 6) self.assertEqual(results.problem.upper_bound, 6) + + def test_matching_problem(self): + m = ConcreteModel() + + m.People = Set(initialize=['P1', 'P2', 'P3', 'P4', 'P5', 'P6', 'P7']) + m.Languages = Set(initialize=['English', 'Spanish', 'Hindi', 'Swedish']) + # People have integer names because we don't have categorical vars yet. + m.Names = Set(initialize=range(len(m.People))) + + m.Observed = Param( + m.Names, + m.Names, + m.Languages, + initialize={ + (0, 1, 'English'): 1, + (1, 0, 'English'): 1, + (0, 2, 'English'): 1, + (2, 0, 'English'): 1, + (0, 3, 'English'): 1, + (3, 0, 'English'): 1, + (0, 4, 'English'): 1, + (4, 0, 'English'): 1, + (0, 5, 'English'): 1, + (5, 0, 'English'): 1, + (0, 6, 'English'): 1, + (6, 0, 'English'): 1, + (1, 2, 'Spanish'): 1, + (2, 1, 'Spanish'): 1, + (1, 5, 'Hindi'): 1, + (5, 1, 'Hindi'): 1, + (1, 6, 'Hindi'): 1, + (6, 1, 'Hindi'): 1, + (2, 3, 'Swedish'): 1, + (3, 2, 'Swedish'): 1, + (3, 4, 'English'): 1, + (4, 3, 'English'): 1, + }, + default=0, + mutable=True, + ) # TODO: shouldn't need to + # be mutable, but waiting + # on #3045 + + m.Expected = Param( + m.People, + m.People, + m.Languages, + initialize={ + ('P1', 'P2', 'English'): 1, + ('P2', 'P1', 'English'): 1, + ('P1', 'P3', 'English'): 1, + ('P3', 'P1', 'English'): 1, + ('P1', 'P4', 'English'): 1, + ('P4', 'P1', 'English'): 1, + ('P1', 'P5', 'English'): 1, + ('P5', 'P1', 'English'): 1, + ('P1', 'P6', 'English'): 1, + ('P6', 'P1', 'English'): 1, + ('P1', 'P7', 'English'): 1, + ('P7', 'P1', 'English'): 1, + ('P2', 'P3', 'Spanish'): 1, + ('P3', 'P2', 'Spanish'): 1, + ('P2', 'P6', 'Hindi'): 1, + ('P6', 'P2', 'Hindi'): 1, + ('P2', 'P7', 'Hindi'): 1, + ('P7', 'P2', 'Hindi'): 1, + ('P3', 'P4', 'Swedish'): 1, + ('P4', 'P3', 'Swedish'): 1, + ('P4', 'P5', 'English'): 1, + ('P5', 'P4', 'English'): 1, + }, + default=0, + mutable=True, + ) # TODO: shouldn't need to be mutable, but + # waiting on #3045 + + m.person_name = Var(m.People, bounds=(0, max(m.Names)), domain=Integers) + + m.one_to_one = LogicalConstraint( + expr=all_different(m.person_name[person] for person in m.People) + ) + + m.obj = Objective( + expr=count_if( + m.Observed[m.person_name[p1], m.person_name[p2], l] + == m.Expected[p1, p2, l] + for p1 in m.People + for p2 in m.People + for l in m.Languages + ), + sense=maximize, + ) + + results = SolverFactory('cp_optimizer').solve(m) + + # we can get one of two perfect matches: + perfect = 7 * 7 * 4 + self.assertEqual(results.problem.lower_bound, perfect) + self.assertEqual(results.problem.upper_bound, perfect) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.optimal + ) + self.assertEqual(value(m.obj), perfect) + self.assertEqual(value(m.person_name['P1']), 0) + self.assertEqual(value(m.person_name['P2']), 1) + self.assertEqual(value(m.person_name['P3']), 2) + self.assertEqual(value(m.person_name['P4']), 3) + self.assertEqual(value(m.person_name['P5']), 4) + # We can't distinguish P6 and P7, so they could each have either of + # names 5 and 6 + self.assertTrue( + value(m.person_name['P6']) == 5 or value(m.person_name['P6']) == 6 + ) + self.assertTrue( + value(m.person_name['P7']) == 5 or value(m.person_name['P7']) == 6 + ) + + m.person_name['P6'].fix(5) + m.person_name['P7'].fix(6) + + results = SolverFactory('cp_optimizer').solve(m) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.optimal + ) + self.assertEqual(value(m.obj), perfect) + + m.person_name['P6'].fix(6) + m.person_name['P7'].fix(5) + + results = SolverFactory('cp_optimizer').solve(m) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.optimal + ) + self.assertEqual(value(m.obj), perfect) + + def test_scheduling_with_sequence_vars(self): + m = ConcreteModel() + m.Steps = Set(initialize=[1, 2, 3]) + + def length_rule(m, j): + return 2 * j + + m.i = IntervalVar(m.Steps, start=(0, 12), end=(0, 12), length=length_rule) + m.seq = SequenceVar(expr=[m.i[j] for j in m.Steps]) + m.first = LogicalConstraint(expr=first_in_sequence(m.i[1], m.seq)) + m.seq_order1 = LogicalConstraint(expr=predecessor_to(m.i[1], m.i[2], m.seq)) + m.seq_order2 = LogicalConstraint(expr=predecessor_to(m.i[2], m.i[3], m.seq)) + m.no_ovlerpa = LogicalConstraint(expr=no_overlap(m.seq)) + + results = SolverFactory('cp_optimizer').solve(m) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.feasible + ) + self.assertEqual(value(m.i[1].start_time), 0) + self.assertEqual(value(m.i[2].start_time), 2) + self.assertEqual(value(m.i[3].start_time), 6) diff --git a/pyomo/contrib/cp/tests/test_interval_var.py b/pyomo/contrib/cp/tests/test_interval_var.py index edbf889fcda..e44ba00210d 100644 --- a/pyomo/contrib/cp/tests/test_interval_var.py +++ b/pyomo/contrib/cp/tests/test_interval_var.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -17,7 +17,7 @@ IntervalVarPresence, ) from pyomo.core.expr import GetItemExpression, GetAttrExpression -from pyomo.environ import ConcreteModel, Integers, Set, value, Var +from pyomo.environ import ConcreteModel, Integers, Reference, Set, value, Var class TestScalarIntervalVar(unittest.TestCase): @@ -217,5 +217,24 @@ def test_index_by_expr(self): self.assertIs(thing2.args[0], thing1) self.assertEqual(thing2.args[1], 'start_time') - # TODO: But this is where it dies. expr1 = m.act[m.i, 2].start_time.before(m.act[m.i**2, 1].end_time) + + def test_reference(self): + m = ConcreteModel() + m.act = IntervalVar([1, 2], end=[0, 10], optional=True) + + thing = Reference(m.act[:].is_present) + self.assertIs(thing[1], m.act[1].is_present) + self.assertIs(thing[2], m.act[2].is_present) + + thing = Reference(m.act[:].start_time) + self.assertIs(thing[1], m.act[1].start_time) + self.assertIs(thing[2], m.act[2].start_time) + + thing = Reference(m.act[:].end_time) + self.assertIs(thing[1], m.act[1].end_time) + self.assertIs(thing[2], m.act[2].end_time) + + thing = Reference(m.act[:].length) + self.assertIs(thing[1], m.act[1].length) + self.assertIs(thing[2], m.act[2].length) diff --git a/pyomo/contrib/cp/tests/test_logical_to_disjunctive.py b/pyomo/contrib/cp/tests/test_logical_to_disjunctive.py index c6733f34f83..3f66aa57726 100755 --- a/pyomo/contrib/cp/tests/test_logical_to_disjunctive.py +++ b/pyomo/contrib/cp/tests/test_logical_to_disjunctive.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/cp/tests/test_precedence_constraints.py b/pyomo/contrib/cp/tests/test_precedence_constraints.py index 461dabf564c..3faf054f241 100644 --- a/pyomo/contrib/cp/tests/test_precedence_constraints.py +++ b/pyomo/contrib/cp/tests/test_precedence_constraints.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -15,7 +15,7 @@ BeforeExpression, AtExpression, ) -from pyomo.environ import ConcreteModel, LogicalConstraint +from pyomo.environ import ConcreteModel, LogicalConstraint, Param class TestPrecedenceRelationships(unittest.TestCase): @@ -173,3 +173,17 @@ def test_end_after_end(self): self.assertEqual(m.c.expr.delay, 0) self.assertEqual(str(m.c.expr), "b.end_time <= a.end_time") + + def test_end_before_start_param_delay(self): + m = self.get_model() + m.PrepTime = Param(initialize=5) + m.c = LogicalConstraint( + expr=m.a.end_time.before(m.b.start_time, delay=m.PrepTime) + ) + self.assertIsInstance(m.c.expr, BeforeExpression) + self.assertEqual(len(m.c.expr.args), 3) + self.assertIs(m.c.expr.args[0], m.a.end_time) + self.assertIs(m.c.expr.args[1], m.b.start_time) + self.assertIs(m.c.expr.delay, m.PrepTime) + + self.assertEqual(str(m.c.expr), "a.end_time + PrepTime <= b.start_time") diff --git a/pyomo/contrib/cp/tests/test_sequence_expressions.py b/pyomo/contrib/cp/tests/test_sequence_expressions.py new file mode 100644 index 00000000000..c7cf94f23d5 --- /dev/null +++ b/pyomo/contrib/cp/tests/test_sequence_expressions.py @@ -0,0 +1,175 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest +from pyomo.contrib.cp.interval_var import IntervalVar +from pyomo.contrib.cp.scheduling_expr.scheduling_logic import ( + AlternativeExpression, + SpanExpression, + SynchronizeExpression, + alternative, + spans, + synchronize, +) +from pyomo.contrib.cp.scheduling_expr.sequence_expressions import ( + NoOverlapExpression, + FirstInSequenceExpression, + LastInSequenceExpression, + BeforeInSequenceExpression, + PredecessorToExpression, + no_overlap, + predecessor_to, + before_in_sequence, + first_in_sequence, + last_in_sequence, +) +from pyomo.contrib.cp.sequence_var import SequenceVar +from pyomo.environ import ConcreteModel, LogicalConstraint, Set + + +class TestSequenceVarExpressions(unittest.TestCase): + def get_model(self): + m = ConcreteModel() + m.S = Set(initialize=range(3)) + m.i = IntervalVar(m.S, start=(0, 5)) + m.seq = SequenceVar(expr=[m.i[j] for j in m.S]) + + return m + + def test_no_overlap(self): + m = self.get_model() + m.c = LogicalConstraint(expr=no_overlap(m.seq)) + e = m.c.expr + + self.assertIsInstance(e, NoOverlapExpression) + self.assertEqual(e.nargs(), 1) + self.assertEqual(len(e.args), 1) + self.assertIs(e.args[0], m.seq) + + self.assertEqual(str(e), "no_overlap(seq)") + + def test_first_in_sequence(self): + m = self.get_model() + m.c = LogicalConstraint(expr=first_in_sequence(m.i[2], m.seq)) + e = m.c.expr + + self.assertIsInstance(e, FirstInSequenceExpression) + self.assertEqual(e.nargs(), 2) + self.assertEqual(len(e.args), 2) + self.assertIs(e.args[0], m.i[2]) + self.assertIs(e.args[1], m.seq) + + self.assertEqual(str(e), "first_in(i[2], seq)") + + def test_last_in_sequence(self): + m = self.get_model() + m.c = LogicalConstraint(expr=last_in_sequence(m.i[0], m.seq)) + e = m.c.expr + + self.assertIsInstance(e, LastInSequenceExpression) + self.assertEqual(e.nargs(), 2) + self.assertEqual(len(e.args), 2) + self.assertIs(e.args[0], m.i[0]) + self.assertIs(e.args[1], m.seq) + + self.assertEqual(str(e), "last_in(i[0], seq)") + + def test_before_in_sequence(self): + m = self.get_model() + m.c = LogicalConstraint(expr=before_in_sequence(m.i[1], m.i[0], m.seq)) + e = m.c.expr + + self.assertIsInstance(e, BeforeInSequenceExpression) + self.assertEqual(e.nargs(), 3) + self.assertEqual(len(e.args), 3) + self.assertIs(e.args[0], m.i[1]) + self.assertIs(e.args[1], m.i[0]) + self.assertIs(e.args[2], m.seq) + + self.assertEqual(str(e), "before_in(i[1], i[0], seq)") + + def test_predecessor_in_sequence(self): + m = self.get_model() + m.c = LogicalConstraint(expr=predecessor_to(m.i[0], m.i[1], m.seq)) + e = m.c.expr + + self.assertIsInstance(e, PredecessorToExpression) + self.assertEqual(e.nargs(), 3) + self.assertEqual(len(e.args), 3) + self.assertIs(e.args[0], m.i[0]) + self.assertIs(e.args[1], m.i[1]) + self.assertIs(e.args[2], m.seq) + + self.assertEqual(str(e), "predecessor_to(i[0], i[1], seq)") + + +class TestHierarchicalSchedulingExpressions(unittest.TestCase): + def make_model(self): + m = ConcreteModel() + + def start_rule(m, i): + return 2 * i + + def length_rule(m, i): + return i + + m.iv = IntervalVar( + [1, 2, 3], start=start_rule, length=length_rule, optional=True + ) + m.whole_enchilada = IntervalVar() + + return m + + def check_span_expression(self, m, e): + self.assertIsInstance(e, SpanExpression) + self.assertEqual(e.nargs(), 4) + self.assertEqual(len(e.args), 4) + self.assertIs(e.args[0], m.whole_enchilada) + for i in [1, 2, 3]: + self.assertIs(e.args[i], m.iv[i]) + + self.assertEqual(str(e), "whole_enchilada.spans(iv[1], iv[2], iv[3])") + + def test_spans(self): + m = self.make_model() + e = spans(m.whole_enchilada, [m.iv[i] for i in [1, 2, 3]]) + self.check_span_expression(m, e) + + def test_spans_method(self): + m = self.make_model() + e = m.whole_enchilada.spans(m.iv[i] for i in [1, 2, 3]) + self.check_span_expression(m, e) + + def test_alternative(self): + m = self.make_model() + e = alternative(m.whole_enchilada, [m.iv[i] for i in [1, 2, 3]]) + + self.assertIsInstance(e, AlternativeExpression) + self.assertEqual(e.nargs(), 4) + self.assertEqual(len(e.args), 4) + self.assertIs(e.args[0], m.whole_enchilada) + for i in [1, 2, 3]: + self.assertIs(e.args[i], m.iv[i]) + + self.assertEqual(str(e), "alternative(whole_enchilada, [iv[1], iv[2], iv[3]])") + + def test_synchronize(self): + m = self.make_model() + e = synchronize(m.whole_enchilada, [m.iv[i] for i in [1, 2, 3]]) + + self.assertIsInstance(e, SynchronizeExpression) + self.assertEqual(e.nargs(), 4) + self.assertEqual(len(e.args), 4) + self.assertIs(e.args[0], m.whole_enchilada) + for i in [1, 2, 3]: + self.assertIs(e.args[i], m.iv[i]) + + self.assertEqual(str(e), "synchronize(whole_enchilada, [iv[1], iv[2], iv[3]])") diff --git a/pyomo/contrib/cp/tests/test_sequence_var.py b/pyomo/contrib/cp/tests/test_sequence_var.py new file mode 100644 index 00000000000..c1e205c6326 --- /dev/null +++ b/pyomo/contrib/cp/tests/test_sequence_var.py @@ -0,0 +1,158 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from io import StringIO +import pyomo.common.unittest as unittest +from pyomo.contrib.cp.interval_var import IntervalVar +from pyomo.contrib.cp.sequence_var import SequenceVar, IndexedSequenceVar +from pyomo.environ import ConcreteModel, Set + + +class TestScalarSequenceVar(unittest.TestCase): + def test_initialize_with_no_data(self): + m = ConcreteModel() + m.i = SequenceVar() + + self.assertIsInstance(m.i, SequenceVar) + self.assertIsInstance(m.i.interval_vars, list) + self.assertEqual(len(m.i.interval_vars), 0) + + m.iv1 = IntervalVar() + m.iv2 = IntervalVar() + m.i.set_value(expr=[m.iv1, m.iv2]) + + self.assertIsInstance(m.i.interval_vars, list) + self.assertEqual(len(m.i.interval_vars), 2) + self.assertIs(m.i.interval_vars[0], m.iv1) + self.assertIs(m.i.interval_vars[1], m.iv2) + + def get_model(self): + m = ConcreteModel() + m.S = Set(initialize=range(3)) + m.i = IntervalVar(m.S, start=(0, 5)) + m.seq = SequenceVar(expr=[m.i[j] for j in m.S]) + + return m + + def test_initialize_with_expr(self): + m = self.get_model() + self.assertEqual(len(m.seq.interval_vars), 3) + for j in m.S: + self.assertIs(m.seq.interval_vars[j], m.i[j]) + + def test_pprint(self): + m = self.get_model() + buf = StringIO() + m.seq.pprint(ostream=buf) + self.assertEqual( + buf.getvalue().strip(), + """ +seq : Size=1, Index=None + Key : IntervalVars + None : [i[0], i[1], i[2]] + """.strip(), + ) + + def test_interval_vars_not_a_list(self): + m = self.get_model() + + with self.assertRaisesRegex( + ValueError, + "'expr' for SequenceVar must be a list of IntervalVars. " + "Encountered type '' constructing 'seq2'", + ): + m.seq2 = SequenceVar(expr=1) + + def test_interval_vars_list_includes_things_that_are_not_interval_vars(self): + m = self.get_model() + + with self.assertRaisesRegex( + ValueError, + "The SequenceVar 'expr' argument must be a list of " + "IntervalVars. The 'expr' for SequenceVar 'seq2' included " + "an object of type ''", + ): + m.seq2 = SequenceVar(expr=m.i) + + +class TestIndexedSequenceVar(unittest.TestCase): + def test_initialize_with_not_data(self): + m = ConcreteModel() + m.i = SequenceVar([1, 2]) + + self.assertIsInstance(m.i, IndexedSequenceVar) + for j in [1, 2]: + self.assertIsInstance(m.i[j].interval_vars, list) + self.assertEqual(len(m.i[j].interval_vars), 0) + + m.iv = IntervalVar() + m.iv2 = IntervalVar([0, 1]) + m.i[2] = [m.iv] + [m.iv2[i] for i in [0, 1]] + + self.assertEqual(len(m.i[2].interval_vars), 3) + self.assertEqual(len(m.i[1].interval_vars), 0) + self.assertIs(m.i[2].interval_vars[0], m.iv) + for i in [0, 1]: + self.assertIs(m.i[2].interval_vars[i + 1], m.iv2[i]) + + def make_model(self): + m = ConcreteModel() + m.alphabetic = Set(initialize=['a', 'b']) + m.numeric = Set(initialize=[1, 2]) + m.i = IntervalVar(m.alphabetic, m.numeric) + + def the_rule(m, j): + return [m.i[j, k] for k in m.numeric] + + m.seq = SequenceVar(m.alphabetic, rule=the_rule) + + return m + + def test_initialize_with_rule(self): + m = self.make_model() + + self.assertIsInstance(m.seq, IndexedSequenceVar) + self.assertEqual(len(m.seq), 2) + for j in m.alphabetic: + self.assertTrue(j in m.seq) + self.assertEqual(len(m.seq[j].interval_vars), 2) + for k in m.numeric: + self.assertIs(m.seq[j].interval_vars[k - 1], m.i[j, k]) + + def test_pprint(self): + m = self.make_model() + m.seq.pprint() + + buf = StringIO() + m.seq.pprint(ostream=buf) + self.assertEqual( + buf.getvalue().strip(), + """ +seq : Size=2, Index=alphabetic + Key : IntervalVars + a : [i[a,1], i[a,2]] + b : [i[b,1], i[b,2]]""".strip(), + ) + + def test_multidimensional_index(self): + m = self.make_model() + + @m.SequenceVar(m.alphabetic, m.numeric) + def s(m, i, j): + return [m.i[i, j]] + + self.assertIsInstance(m.s, IndexedSequenceVar) + self.assertEqual(len(m.s), 4) + for i in m.alphabetic: + for j in m.numeric: + self.assertTrue((i, j) in m.s) + self.assertEqual(len(m.s[i, j].interval_vars), 1) + self.assertIs(m.s[i, j].interval_vars[0], m.i[i, j]) diff --git a/pyomo/contrib/cp/tests/test_step_function_expressions.py b/pyomo/contrib/cp/tests/test_step_function_expressions.py index 7212cc870d5..a7b30c1d4e6 100644 --- a/pyomo/contrib/cp/tests/test_step_function_expressions.py +++ b/pyomo/contrib/cp/tests/test_step_function_expressions.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/cp/transform/__init__.py b/pyomo/contrib/cp/transform/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/cp/transform/__init__.py +++ b/pyomo/contrib/cp/transform/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/cp/transform/logical_to_disjunctive_program.py b/pyomo/contrib/cp/transform/logical_to_disjunctive_program.py index cd7681d4d87..7c5ef8d13c0 100644 --- a/pyomo/contrib/cp/transform/logical_to_disjunctive_program.py +++ b/pyomo/contrib/cp/transform/logical_to_disjunctive_program.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -12,7 +12,6 @@ from pyomo.contrib.cp.transform.logical_to_disjunctive_walker import ( LogicalToDisjunctiveVisitor, ) -from pyomo.common.collections import ComponentMap from pyomo.common.modeling import unique_component_name from pyomo.common.config import ConfigDict, ConfigValue @@ -26,7 +25,7 @@ Transformation, NonNegativeIntegers, ) -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.base import SortComponents from pyomo.core.util import target_list from pyomo.gdp import Disjunct, Disjunction @@ -73,7 +72,7 @@ def _apply_to(self, model, **kwds): transBlocks = {} visitor = LogicalToDisjunctiveVisitor() for t in targets: - if t.ctype is Block or isinstance(t, _BlockData): + if t.ctype is Block or isinstance(t, BlockData): self._transform_block(t, model, visitor, transBlocks) elif t.ctype is LogicalConstraint: if t.is_indexed(): diff --git a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py index 624629d326d..09894b47090 100644 --- a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py +++ b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,14 +9,10 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import collections - from pyomo.common.collections import ComponentMap from pyomo.common.errors import MouseTrap from pyomo.core.expr.expr_common import ExpressionType from pyomo.core.expr.visitor import StreamBasedExpressionVisitor -from pyomo.core.expr.numeric_expr import NumericExpression -from pyomo.core.expr.relational_expr import RelationalExpression import pyomo.core.expr as EXPR from pyomo.core.base import ( Binary, @@ -27,9 +23,9 @@ value, ) import pyomo.core.base.boolean_var as BV -from pyomo.core.base.expression import ScalarExpression, _GeneralExpressionData -from pyomo.core.base.param import ScalarParam, _ParamData -from pyomo.core.base.var import ScalarVar, _GeneralVarData +from pyomo.core.base.expression import ScalarExpression, ExpressionData +from pyomo.core.base.param import ScalarParam, ParamData +from pyomo.core.base.var import ScalarVar, VarData from pyomo.gdp.disjunct import AutoLinkedBooleanVar, Disjunct, Disjunction @@ -209,15 +205,15 @@ def _dispatch_atmost(visitor, node, *args): _before_child_dispatcher = {} _before_child_dispatcher[BV.ScalarBooleanVar] = _dispatch_boolean_var -_before_child_dispatcher[BV._GeneralBooleanVarData] = _dispatch_boolean_var +_before_child_dispatcher[BV.BooleanVarData] = _dispatch_boolean_var _before_child_dispatcher[AutoLinkedBooleanVar] = _dispatch_boolean_var -_before_child_dispatcher[_ParamData] = _dispatch_param +_before_child_dispatcher[ParamData] = _dispatch_param _before_child_dispatcher[ScalarParam] = _dispatch_param # for the moment, these are all just so we can get good error messages when we # don't handle them: _before_child_dispatcher[ScalarVar] = _dispatch_var -_before_child_dispatcher[_GeneralVarData] = _dispatch_var -_before_child_dispatcher[_GeneralExpressionData] = _dispatch_expression +_before_child_dispatcher[VarData] = _dispatch_var +_before_child_dispatcher[ExpressionData] = _dispatch_expression _before_child_dispatcher[ScalarExpression] = _dispatch_expression diff --git a/pyomo/contrib/doe/__init__.py b/pyomo/contrib/doe/__init__.py index e38b5dce1d9..e45aa3b44a3 100644 --- a/pyomo/contrib/doe/__init__.py +++ b/pyomo/contrib/doe/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index b451c431f21..0fc3e8770fe 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -38,6 +38,9 @@ from pyomo.contrib.sensitivity_toolbox.sens import get_dsdp from pyomo.contrib.doe.scenario import ScenarioGenerator, FiniteDifferenceStep from pyomo.contrib.doe.result import FisherResults, GridSearchResult +import collections.abc + +import inspect class CalculationMode(Enum): @@ -101,6 +104,8 @@ def __init__( """ # parameters + if not isinstance(param_init, collections.abc.Mapping): + raise ValueError("param_init should be a dictionary.") self.param = param_init # design variable name self.design_name = design_vars.variable_names @@ -226,6 +231,7 @@ def stochastic_program( # FIM = Jacobian.T@Jacobian, the FIM is scaled by squared value the Jacobian is scaled self.fim_scale_constant_value = self.scale_constant_value**2 + # Start timer sp_timer = TicTocTimer() sp_timer.tic(msg=None) @@ -236,14 +242,17 @@ def stochastic_program( m, analysis_square = self._compute_stochastic_program(m, optimize_opt) if self.optimize: + # If set to optimize, solve the optimization problem (with degrees of freedom) analysis_optimize = self._optimize_stochastic_program(m) dT = sp_timer.toc(msg=None) - self.logger.info("elapsed time: %0.1f" % dT) + self.logger.info("elapsed time: %0.1f seconds" % dT) + # Return both square problem and optimization problem results return analysis_square, analysis_optimize else: dT = sp_timer.toc(msg=None) - self.logger.info("elapsed time: %0.1f" % dT) + self.logger.info("elapsed time: %0.1f seconds" % dT) + # Return only square problem results return analysis_square def _compute_stochastic_program(self, m, optimize_option): @@ -387,7 +396,7 @@ def compute_FIM( FIM_analysis = self._direct_kaug() dT = square_timer.toc(msg=None) - self.logger.info("elapsed time: %0.1f" % dT) + self.logger.info("elapsed time: %0.1f seconds" % dT) return FIM_analysis @@ -587,12 +596,37 @@ def _create_block(self): # Set for block/scenarios mod.scenario = pyo.Set(initialize=self.scenario_data.scenario_indices) + # Determine if create_model takes theta as an optional input + pass_theta_to_initialize = ( + 'theta' in inspect.getfullargspec(self.create_model).args + ) + # Allow user to self-define complex design variables self.create_model(mod=mod, model_option=ModelOptionLib.stage1) + # Fix parameter values in the copy of the stage1 model (if they exist) + for par in self.param: + cuid = pyo.ComponentUID(par) + var = cuid.find_component_on(mod) + if var is not None: + # Fix the parameter value + # Otherwise, the parameter does not exist on the stage 1 model + var.fix(self.param[par]) + def block_build(b, s): # create block scenarios - self.create_model(mod=b, model_option=ModelOptionLib.stage2) + # idea: check if create_model takes theta as an optional input, if so, pass parameter values to create_model + + if pass_theta_to_initialize: + # Grab the values of theta for this scenario/block + theta_initialize = self.scenario_data.scenario[s] + # Add model on block with theta values + self.create_model( + mod=b, model_option=ModelOptionLib.stage2, theta=theta_initialize + ) + else: + # Otherwise add model on block without theta values + self.create_model(mod=b, model_option=ModelOptionLib.stage2) # fix parameter values to perturbed values for par in self.param: @@ -603,7 +637,7 @@ def block_build(b, s): mod.block = pyo.Block(mod.scenario, rule=block_build) # discretize the model - if self.discretize_model: + if self.discretize_model is not None: mod = self.discretize_model(mod) # force design variables in blocks to be equal to global design values @@ -775,7 +809,7 @@ def run_grid_search( # update the controlled value of certain time points for certain design variables for i, names in enumerate(design_dimension_names): # if the element is a list, all design variables in this list share the same values - if type(names) is list or type(names) is tuple: + if isinstance(names, collections.abc.Sequence): for n in names: design_iter[n] = list(design_set_iter)[i] else: @@ -879,11 +913,36 @@ def identity_matrix(m, i, j): else: return 0 + ### Initialize the Jacobian if provided by the user + + # If the user provides an initial Jacobian, convert it to a dictionary + if self.jac_initial is not None: + dict_jac_initialize = {} + for i, bu in enumerate(model.regression_parameters): + for j, un in enumerate(model.measured_variables): + if isinstance(self.jac_initial, dict): + # Jacobian is a dictionary of arrays or lists where the key is the regression parameter name + dict_jac_initialize[(bu, un)] = self.jac_initial[bu][j] + elif isinstance(self.jac_initial, np.ndarray): + # Jacobian is a numpy array, rows are regression parameters, columns are measured variables + dict_jac_initialize[(bu, un)] = self.jac_initial[i][j] + + # Initialize the Jacobian matrix + def initialize_jac(m, i, j): + # If provided by the user, use the values now stored in the dictionary + if self.jac_initial is not None: + return dict_jac_initialize[(i, j)] + # Otherwise initialize to 0.1 (which is an arbitrary non-zero value) + else: + return 0.1 + model.sensitivity_jacobian = pyo.Var( - model.regression_parameters, model.measured_variables, initialize=0.1 + model.regression_parameters, + model.measured_variables, + initialize=initialize_jac, ) - if self.fim_initial: + if self.fim_initial is not None: dict_fim_initialize = {} for i, bu in enumerate(model.regression_parameters): for j, un in enumerate(model.regression_parameters): @@ -892,7 +951,7 @@ def identity_matrix(m, i, j): def initialize_fim(m, j, d): return dict_fim_initialize[(j, d)] - if self.fim_initial: + if self.fim_initial is not None: model.fim = pyo.Var( model.regression_parameters, model.regression_parameters, @@ -1011,6 +1070,32 @@ def fim_rule(m, p, q): return model def _add_objective(self, m): + + ### Initialize the Cholesky decomposition matrix + if self.Cholesky_option: + + # Assemble the FIM matrix + fim = np.zeros((len(self.param), len(self.param))) + for i, bu in enumerate(m.regression_parameters): + for j, un in enumerate(m.regression_parameters): + fim[i][j] = m.fim[bu, un].value + + # Calculate the eigenvalues of the FIM matrix + eig = np.linalg.eigvals(fim) + + # If the smallest eigenvalue is (practically) negative, add a diagonal matrix to make it positive definite + small_number = 1e-10 + if min(eig) < small_number: + fim = fim + np.eye(len(self.param)) * (small_number - min(eig)) + + # Compute the Cholesky decomposition of the FIM matrix + L = np.linalg.cholesky(fim) + + # Initialize the Cholesky matrix + for i, c in enumerate(m.regression_parameters): + for j, d in enumerate(m.regression_parameters): + m.L_ele[c, d].value = L[i, j] + def cholesky_imp(m, c, d): """ Calculate Cholesky L matrix using algebraic constraints @@ -1101,14 +1186,20 @@ def _fix_design(self, m, design_val, fix_opt=True, optimize_option=None): m: model """ for name in self.design_name: + # Loop over design variables + # Get Pyomo variable object cuid = pyo.ComponentUID(name) var = cuid.find_component_on(m) if fix_opt: + # If fix_opt is True, fix the design variable var.fix(design_val[name]) else: + # Otherwise check optimize_option if optimize_option is None: + # If optimize_option is None, unfix all design variables var.unfix() else: + # Otherwise, unfix only the design variables listed in optimize_option with value True if optimize_option[name]: var.unfix() return m @@ -1124,7 +1215,7 @@ def _get_default_ipopt_solver(self): def _solve_doe(self, m, fix=False, opt_option=None): """Solve DOE model. If it's a square problem, fix design variable and solve. - Else, fix design variable and solve square problem firstly, then unfix them and solve the optimization problem + Else, fix design variable and solve square problem first, then unfix them and solve the optimization problem Parameters ---------- @@ -1138,7 +1229,10 @@ def _solve_doe(self, m, fix=False, opt_option=None): ------- solver_results: solver results """ - ### Solve square problem + # if fix = False, solve the optimization problem + # if fix = True, solve the square problem + + # either fix or unfix the design variables mod = self._fix_design( m, self.design_values, fix_opt=fix, optimize_option=opt_option ) diff --git a/pyomo/contrib/doe/examples/__init__.py b/pyomo/contrib/doe/examples/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/doe/examples/__init__.py +++ b/pyomo/contrib/doe/examples/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/doe/examples/reactor_compute_FIM.py b/pyomo/contrib/doe/examples/reactor_compute_FIM.py index c004ad36f00..108f5bd16a0 100644 --- a/pyomo/contrib/doe/examples/reactor_compute_FIM.py +++ b/pyomo/contrib/doe/examples/reactor_compute_FIM.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/doe/examples/reactor_grid_search.py b/pyomo/contrib/doe/examples/reactor_grid_search.py index a4516c36451..1f5aae77f85 100644 --- a/pyomo/contrib/doe/examples/reactor_grid_search.py +++ b/pyomo/contrib/doe/examples/reactor_grid_search.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/doe/examples/reactor_kinetics.py b/pyomo/contrib/doe/examples/reactor_kinetics.py index 57d06e146c5..ed2175085f2 100644 --- a/pyomo/contrib/doe/examples/reactor_kinetics.py +++ b/pyomo/contrib/doe/examples/reactor_kinetics.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/doe/examples/reactor_optimize_doe.py b/pyomo/contrib/doe/examples/reactor_optimize_doe.py index 56ea1ffeac3..f7b4a74c891 100644 --- a/pyomo/contrib/doe/examples/reactor_optimize_doe.py +++ b/pyomo/contrib/doe/examples/reactor_optimize_doe.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/doe/measurements.py b/pyomo/contrib/doe/measurements.py index 75fd4f7c485..fd3962f7888 100644 --- a/pyomo/contrib/doe/measurements.py +++ b/pyomo/contrib/doe/measurements.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -26,6 +26,8 @@ # ___________________________________________________________________________ import itertools +import collections.abc +from pyomo.common.numeric_types import native_numeric_types class VariablesWithIndices: @@ -93,18 +95,18 @@ def add_variables( upper_bounds, ) - if values: + if values is not None: # this dictionary keys are special set, values are its value self.variable_names_value.update(zip(added_names, values)) # if a scalar (int or float) is given, set it as the lower bound for all variables - if lower_bounds: - if type(lower_bounds) in [int, float]: + if lower_bounds is not None: + if type(lower_bounds) in native_numeric_types: lower_bounds = [lower_bounds] * len(added_names) self.lower_bounds.update(zip(added_names, lower_bounds)) - if upper_bounds: - if type(upper_bounds) in [int, float]: + if upper_bounds is not None: + if type(upper_bounds) in native_numeric_types: upper_bounds = [upper_bounds] * len(added_names) self.upper_bounds.update(zip(added_names, upper_bounds)) @@ -171,26 +173,26 @@ def _check_valid_input( """ Check if the measurement information provided are valid to use. """ - assert type(var_name) is str, "var_name should be a string." + assert isinstance(var_name, str), "var_name should be a string." if time_index_position not in indices: raise ValueError("time index cannot be found in indices.") # if given a list, check if bounds have the same length with flattened variable - if values and len(values) != len_indices: + if values is not None and len(values) != len_indices: raise ValueError("Values is of different length with indices.") if ( - lower_bounds - and type(lower_bounds) == list - and len(lower_bounds) != len_indices + lower_bounds is not None # ensure not None + and isinstance(lower_bounds, collections.abc.Sequence) # ensure list-like + and len(lower_bounds) != len_indices # ensure same length ): raise ValueError("Lowerbounds is of different length with indices.") if ( - upper_bounds - and type(upper_bounds) == list - and len(upper_bounds) != len_indices + upper_bounds is not None # ensure None + and isinstance(upper_bounds, collections.abc.Sequence) # ensure list-like + and len(upper_bounds) != len_indices # ensure same length ): raise ValueError("Upperbounds is of different length with indices.") diff --git a/pyomo/contrib/doe/result.py b/pyomo/contrib/doe/result.py index 65ded38a63b..1593214c30a 100644 --- a/pyomo/contrib/doe/result.py +++ b/pyomo/contrib/doe/result.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/doe/scenario.py b/pyomo/contrib/doe/scenario.py index eff9c883e0b..6c6f5ef7d1b 100644 --- a/pyomo/contrib/doe/scenario.py +++ b/pyomo/contrib/doe/scenario.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/doe/tests/__init__.py b/pyomo/contrib/doe/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/doe/tests/__init__.py +++ b/pyomo/contrib/doe/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/doe/tests/test_example.py b/pyomo/contrib/doe/tests/test_example.py index 0f143e03677..b59014a8110 100644 --- a/pyomo/contrib/doe/tests/test_example.py +++ b/pyomo/contrib/doe/tests/test_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/doe/tests/test_fim_doe.py b/pyomo/contrib/doe/tests/test_fim_doe.py index 42b463162b2..31d250f0d10 100644 --- a/pyomo/contrib/doe/tests/test_fim_doe.py +++ b/pyomo/contrib/doe/tests/test_fim_doe.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/doe/tests/test_reactor_example.py b/pyomo/contrib/doe/tests/test_reactor_example.py index 86c914ec4e0..daf2ee89194 100644 --- a/pyomo/contrib/doe/tests/test_reactor_example.py +++ b/pyomo/contrib/doe/tests/test_reactor_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/example/__init__.py b/pyomo/contrib/example/__init__.py index 7f2d08a0292..c70b50e84de 100644 --- a/pyomo/contrib/example/__init__.py +++ b/pyomo/contrib/example/__init__.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # # import symbols and sub-packages # diff --git a/pyomo/contrib/example/bar.py b/pyomo/contrib/example/bar.py index 295540d3318..22e5c3997e9 100644 --- a/pyomo/contrib/example/bar.py +++ b/pyomo/contrib/example/bar.py @@ -1 +1,12 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + b = "1" diff --git a/pyomo/contrib/example/foo.py b/pyomo/contrib/example/foo.py index 1337a530cbc..f879bc70722 100644 --- a/pyomo/contrib/example/foo.py +++ b/pyomo/contrib/example/foo.py @@ -1 +1,12 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + a = 1 diff --git a/pyomo/contrib/example/plugins/__init__.py b/pyomo/contrib/example/plugins/__init__.py index dc71adec9dc..179098bc18e 100644 --- a/pyomo/contrib/example/plugins/__init__.py +++ b/pyomo/contrib/example/plugins/__init__.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # Define a 'load()' function, which simply imports # sub-packages that define plugin classes. diff --git a/pyomo/contrib/example/plugins/ex_plugin.py b/pyomo/contrib/example/plugins/ex_plugin.py index 504605205f4..7ee4c414ccf 100644 --- a/pyomo/contrib/example/plugins/ex_plugin.py +++ b/pyomo/contrib/example/plugins/ex_plugin.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core.base import Transformation, TransformationFactory diff --git a/pyomo/contrib/example/tests/__init__.py b/pyomo/contrib/example/tests/__init__.py index 5a1047f74ae..9c45a6ef8b6 100644 --- a/pyomo/contrib/example/tests/__init__.py +++ b/pyomo/contrib/example/tests/__init__.py @@ -1 +1,12 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # Tests for pyomo.contrib.example diff --git a/pyomo/contrib/example/tests/test_example.py b/pyomo/contrib/example/tests/test_example.py index c38de1b914f..55394f5d0c1 100644 --- a/pyomo/contrib/example/tests/test_example.py +++ b/pyomo/contrib/example/tests/test_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/fbbt/__init__.py b/pyomo/contrib/fbbt/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/fbbt/__init__.py +++ b/pyomo/contrib/fbbt/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/fbbt/expression_bounds_walker.py b/pyomo/contrib/fbbt/expression_bounds_walker.py new file mode 100644 index 00000000000..cb287d54df5 --- /dev/null +++ b/pyomo/contrib/fbbt/expression_bounds_walker.py @@ -0,0 +1,344 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import logging +from math import pi +from pyomo.common.collections import ComponentMap +from pyomo.contrib.fbbt.interval import ( + BoolFlag, + eq, + ineq, + ranged, + if_, + add, + acos, + asin, + atan, + cos, + div, + exp, + interval_abs, + log, + log10, + mul, + power, + sin, + sub, + tan, +) +from pyomo.core.base.expression import Expression +from pyomo.core.expr.numeric_expr import ( + NumericExpression, + NegationExpression, + ProductExpression, + DivisionExpression, + PowExpression, + AbsExpression, + UnaryFunctionExpression, + MonomialTermExpression, + LinearExpression, + SumExpression, + ExternalFunctionExpression, + Expr_ifExpression, +) +from pyomo.core.expr.logical_expr import BooleanExpression +from pyomo.core.expr.relational_expr import ( + EqualityExpression, + InequalityExpression, + RangedExpression, +) +from pyomo.core.expr.numvalue import native_numeric_types, native_types, value +from pyomo.core.expr.visitor import StreamBasedExpressionVisitor +from pyomo.repn.util import BeforeChildDispatcher, ExitNodeDispatcher + +inf = float('inf') +logger = logging.getLogger(__name__) + + +class ExpressionBoundsBeforeChildDispatcher(BeforeChildDispatcher): + __slots__ = () + + def __init__(self): + self[ExternalFunctionExpression] = self._before_external_function + + @staticmethod + def _before_external_function(visitor, child): + # [ESJ 10/6/23]: If external functions ever implement callbacks to help with + # this then this should use them + return False, (-inf, inf) + + @staticmethod + def _before_native_numeric(visitor, child): + return False, (child, child) + + @staticmethod + def _before_native_logical(visitor, child): + return False, (BoolFlag(child), BoolFlag(child)) + + @staticmethod + def _before_var(visitor, child): + leaf_bounds = visitor.leaf_bounds + if child in leaf_bounds: + pass + elif child.is_fixed() and visitor.use_fixed_var_values_as_bounds: + val = child.value + try: + ans = visitor._before_child_handlers[val.__class__](visitor, val) + except ValueError: + raise ValueError( + "Var '%s' is fixed to None. This value cannot be used to " + "calculate bounds." % child.name + ) from None + leaf_bounds[child] = ans[1] + return ans + else: + lb = child.lb + ub = child.ub + if lb is None: + lb = -inf + if ub is None: + ub = inf + leaf_bounds[child] = (lb, ub) + return False, leaf_bounds[child] + + @staticmethod + def _before_named_expression(visitor, child): + leaf_bounds = visitor.leaf_bounds + if child in leaf_bounds: + return False, leaf_bounds[child] + else: + return True, None + + @staticmethod + def _before_param(visitor, child): + val = child.value + return visitor._before_child_handlers[val.__class__](visitor, val) + + @staticmethod + def _before_string(visitor, child): + raise ValueError( + f"{child!r} ({type(child).__name__}) is not a valid numeric type. " + f"Cannot compute bounds on expression." + ) + + @staticmethod + def _before_invalid(visitor, child): + raise ValueError( + f"{child!r} ({type(child).__name__}) is not a valid numeric type. " + f"Cannot compute bounds on expression." + ) + + @staticmethod + def _before_complex(visitor, child): + raise ValueError( + f"Cannot compute bounds on expressions containing " + f"complex numbers. Encountered when processing {child}" + ) + + @staticmethod + def _before_npv(visitor, child): + val = value(child) + return visitor._before_child_handlers[val.__class__](visitor, val) + + +def _handle_ProductExpression(visitor, node, arg1, arg2): + if arg1 is arg2: + return power(*arg1, 2, 2, feasibility_tol=visitor.feasibility_tol) + return mul(*arg1, *arg2) + + +def _handle_SumExpression(visitor, node, *args): + bnds = (0, 0) + for arg in args: + bnds = add(*bnds, *arg) + return bnds + + +def _handle_DivisionExpression(visitor, node, arg1, arg2): + return div(*arg1, *arg2, feasibility_tol=visitor.feasibility_tol) + + +def _handle_PowExpression(visitor, node, arg1, arg2): + return power(*arg1, *arg2, feasibility_tol=visitor.feasibility_tol) + + +def _handle_NegationExpression(visitor, node, arg): + return sub(0, 0, *arg) + + +def _handle_exp(visitor, node, arg): + return exp(*arg) + + +def _handle_log(visitor, node, arg): + return log(*arg) + + +def _handle_log10(visitor, node, arg): + return log10(*arg) + + +def _handle_sin(visitor, node, arg): + return sin(*arg) + + +def _handle_cos(visitor, node, arg): + return cos(*arg) + + +def _handle_tan(visitor, node, arg): + return tan(*arg) + + +def _handle_asin(visitor, node, arg): + return asin(*arg, -pi / 2, pi / 2, visitor.feasibility_tol) + + +def _handle_acos(visitor, node, arg): + return acos(*arg, 0, pi, visitor.feasibility_tol) + + +def _handle_atan(visitor, node, arg): + return atan(*arg, -pi / 2, pi / 2) + + +def _handle_sqrt(visitor, node, arg): + return power(*arg, 0.5, 0.5, feasibility_tol=visitor.feasibility_tol) + + +def _handle_AbsExpression(visitor, node, arg): + return interval_abs(*arg) + + +def _handle_UnaryFunctionExpression(visitor, node, arg): + return _unary_function_dispatcher[node.getname()](visitor, node, arg) + + +def _handle_named_expression(visitor, node, arg): + visitor.leaf_bounds[node] = arg + return arg + + +def _handle_unknowable_bounds(visitor, node, arg): + return -inf, inf + + +def _handle_equality(visitor, node, arg1, arg2): + return eq(*arg1, *arg2) + + +def _handle_inequality(visitor, node, arg1, arg2): + return ineq(*arg1, *arg2) + + +def _handle_ranged(visitor, node, arg1, arg2, arg3): + return ranged(*arg1, *arg2, *arg3) + + +def _handle_expr_if(visitor, node, arg1, arg2, arg3): + return if_(*arg1, *arg2, *arg3) + + +_unary_function_dispatcher = { + 'exp': _handle_exp, + 'log': _handle_log, + 'log10': _handle_log10, + 'sin': _handle_sin, + 'cos': _handle_cos, + 'tan': _handle_tan, + 'asin': _handle_asin, + 'acos': _handle_acos, + 'atan': _handle_atan, + 'sqrt': _handle_sqrt, +} + + +class ExpressionBoundsExitNodeDispatcher(ExitNodeDispatcher): + def unexpected_expression_type(self, visitor, node, *args): + if isinstance(node, NumericExpression): + ans = -inf, inf + elif isinstance(node, BooleanExpression): + ans = BoolFlag(False), BoolFlag(True) + else: + super().unexpected_expression_type(visitor, node, *args) + logger.warning( + f"Unexpected expression node type '{type(node).__name__}' " + f"found while walking expression tree; returning {ans} " + "for the expression bounds." + ) + return ans + + +class ExpressionBoundsVisitor(StreamBasedExpressionVisitor): + """ + Walker to calculate bounds on an expression, from leaf to root, with + caching of terminal node bounds (Vars and Expressions) + + NOTE: If anything changes on the model (e.g., Var bounds, fixing, mutable + Param values, etc), then you need to either create a new instance of this + walker, or clear self.leaf_bounds! + + Parameters + ---------- + leaf_bounds: ComponentMap in which to cache bounds at leaves of the expression + tree + feasibility_tol: float, feasibility tolerance for interval arithmetic + calculations + use_fixed_var_values_as_bounds: bool, whether or not to use the values of + fixed Vars as the upper and lower bounds for those Vars or to instead + ignore fixed status and use the bounds. Set to 'True' if you do not + anticipate the fixed status of Variables to change for the duration that + the computed bounds should be valid. + """ + + _before_child_handlers = ExpressionBoundsBeforeChildDispatcher() + _operator_dispatcher = ExpressionBoundsExitNodeDispatcher( + { + ProductExpression: _handle_ProductExpression, + DivisionExpression: _handle_DivisionExpression, + PowExpression: _handle_PowExpression, + AbsExpression: _handle_AbsExpression, + SumExpression: _handle_SumExpression, + MonomialTermExpression: _handle_ProductExpression, + NegationExpression: _handle_NegationExpression, + UnaryFunctionExpression: _handle_UnaryFunctionExpression, + LinearExpression: _handle_SumExpression, + Expression: _handle_named_expression, + ExternalFunctionExpression: _handle_unknowable_bounds, + EqualityExpression: _handle_equality, + InequalityExpression: _handle_inequality, + RangedExpression: _handle_ranged, + Expr_ifExpression: _handle_expr_if, + } + ) + + def __init__( + self, + leaf_bounds=None, + feasibility_tol=1e-8, + use_fixed_var_values_as_bounds=False, + ): + super().__init__() + self.leaf_bounds = leaf_bounds if leaf_bounds is not None else ComponentMap() + self.feasibility_tol = feasibility_tol + self.use_fixed_var_values_as_bounds = use_fixed_var_values_as_bounds + + def initializeWalker(self, expr): + walk, result = self.beforeChild(None, expr, 0) + if not walk: + return False, result + return True, expr + + def beforeChild(self, node, child, child_idx): + return self._before_child_handlers[child.__class__](self, child) + + def exitNode(self, node, data): + return self._operator_dispatcher[node.__class__](self, node, *data) diff --git a/pyomo/contrib/fbbt/fbbt.py b/pyomo/contrib/fbbt/fbbt.py index 5c486488540..1507c4a3cc5 100644 --- a/pyomo/contrib/fbbt/fbbt.py +++ b/pyomo/contrib/fbbt/fbbt.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,22 +9,29 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from collections import defaultdict from pyomo.common.collections import ComponentMap, ComponentSet +from pyomo.contrib.fbbt.expression_bounds_walker import ExpressionBoundsVisitor import pyomo.core.expr.numeric_expr as numeric_expr -from pyomo.core.expr.visitor import ExpressionValueVisitor, identify_variables +from pyomo.core.expr.visitor import ( + ExpressionValueVisitor, + identify_variables, + StreamBasedExpressionVisitor, +) from pyomo.core.expr.numvalue import nonpyomo_leaf_types, value from pyomo.core.expr.numvalue import is_fixed import pyomo.contrib.fbbt.interval as interval import math from pyomo.core.base.block import Block from pyomo.core.base.constraint import Constraint +from pyomo.core.base.expression import ExpressionData, ScalarExpression +from pyomo.core.base.objective import ObjectiveData, ScalarObjective from pyomo.core.base.var import Var from pyomo.gdp import Disjunct -from pyomo.core.base.expression import _GeneralExpressionData, ScalarExpression import logging from pyomo.common.errors import InfeasibleConstraintException, PyomoException from pyomo.common.config import ( - ConfigBlock, + ConfigDict, ConfigValue, In, NonNegativeFloat, @@ -73,377 +80,274 @@ class FBBTException(PyomoException): pass -def _prop_bnds_leaf_to_root_ProductExpression(node, bnds_dict, feasibility_tol): +def _prop_bnds_leaf_to_root_ProductExpression(visitor, node, arg1, arg2): """ Parameters ---------- + visitor: _FBBTVisitorLeafToRoot node: pyomo.core.expr.numeric_expr.ProductExpression - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + arg1: First arg in product expression + arg2: Second arg in product expression """ - assert len(node.args) == 2 - arg1, arg2 = node.args - lb1, ub1 = bnds_dict[arg1] - lb2, ub2 = bnds_dict[arg2] + bnds_dict = visitor.bnds_dict if arg1 is arg2: - bnds_dict[node] = interval.power(lb1, ub1, 2, 2, feasibility_tol) + bnds_dict[node] = interval.power( + *bnds_dict[arg1], 2, 2, visitor.feasibility_tol + ) else: - bnds_dict[node] = interval.mul(lb1, ub1, lb2, ub2) + bnds_dict[node] = interval.mul(*bnds_dict[arg1], *bnds_dict[arg2]) -def _prop_bnds_leaf_to_root_SumExpression(node, bnds_dict, feasibility_tol): +def _prop_bnds_leaf_to_root_SumExpression(visitor, node, *args): """ Parameters ---------- + visitor: _FBBTVisitorLeafToRoot node: pyomo.core.expr.numeric_expr.SumExpression - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + args: summands in SumExpression """ + bnds_dict = visitor.bnds_dict bnds = (0, 0) - for arg in node.args: + for arg in args: bnds = interval.add(*bnds, *bnds_dict[arg]) bnds_dict[node] = bnds -def _prop_bnds_leaf_to_root_DivisionExpression(node, bnds_dict, feasibility_tol): +def _prop_bnds_leaf_to_root_DivisionExpression(visitor, node, arg1, arg2): """ Parameters ---------- + visitor: _FBBTVisitorLeafToRoot node: pyomo.core.expr.numeric_expr.DivisionExpression - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + arg1: dividend + arg2: divisor """ - assert len(node.args) == 2 - arg1, arg2 = node.args - lb1, ub1 = bnds_dict[arg1] - lb2, ub2 = bnds_dict[arg2] - bnds_dict[node] = interval.div(lb1, ub1, lb2, ub2, feasibility_tol=feasibility_tol) + bnds_dict = visitor.bnds_dict + bnds_dict[node] = interval.div( + *bnds_dict[arg1], *bnds_dict[arg2], feasibility_tol=visitor.feasibility_tol + ) -def _prop_bnds_leaf_to_root_PowExpression(node, bnds_dict, feasibility_tol): +def _prop_bnds_leaf_to_root_PowExpression(visitor, node, arg1, arg2): """ Parameters ---------- + visitor: _FBBTVisitorLeafToRoot node: pyomo.core.expr.numeric_expr.PowExpression - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + arg1: base + arg2: exponent """ - assert len(node.args) == 2 - arg1, arg2 = node.args - lb1, ub1 = bnds_dict[arg1] - lb2, ub2 = bnds_dict[arg2] + bnds_dict = visitor.bnds_dict bnds_dict[node] = interval.power( - lb1, ub1, lb2, ub2, feasibility_tol=feasibility_tol + *bnds_dict[arg1], *bnds_dict[arg2], feasibility_tol=visitor.feasibility_tol ) -def _prop_bnds_leaf_to_root_NegationExpression(node, bnds_dict, feasibility_tol): +def _prop_bnds_leaf_to_root_NegationExpression(visitor, node, arg): """ Parameters ---------- - node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + visitor: _FBBTVisitorLeafToRoot + node: pyomo.core.expr.numeric_expr.NegationExpression + arg: NegationExpression arg """ - assert len(node.args) == 1 - arg = node.args[0] - lb1, ub1 = bnds_dict[arg] - bnds_dict[node] = interval.sub(0, 0, lb1, ub1) + bnds_dict = visitor.bnds_dict + bnds_dict[node] = interval.sub(0, 0, *bnds_dict[arg]) -def _prop_bnds_leaf_to_root_exp(node, bnds_dict, feasibility_tol): +def _prop_bnds_leaf_to_root_exp(visitor, node, arg): """ Parameters ---------- + visitor: _FBBTVisitorLeafToRoot node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + arg: UnaryFunctionExpression arg """ - assert len(node.args) == 1 - arg = node.args[0] - lb1, ub1 = bnds_dict[arg] - bnds_dict[node] = interval.exp(lb1, ub1) + bnds_dict = visitor.bnds_dict + bnds_dict[node] = interval.exp(*bnds_dict[arg]) -def _prop_bnds_leaf_to_root_log(node, bnds_dict, feasibility_tol): +def _prop_bnds_leaf_to_root_log(visitor, node, arg): """ Parameters ---------- + visitor: _FBBTVisitorLeafToRoot node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + arg: UnaryFunctionExpression arg """ - assert len(node.args) == 1 - arg = node.args[0] - lb1, ub1 = bnds_dict[arg] - bnds_dict[node] = interval.log(lb1, ub1) + bnds_dict = visitor.bnds_dict + bnds_dict[node] = interval.log(*bnds_dict[arg]) -def _prop_bnds_leaf_to_root_log10(node, bnds_dict, feasibility_tol): +def _prop_bnds_leaf_to_root_log10(visitor, node, arg): """ Parameters ---------- + visitor: _FBBTVisitorLeafToRoot node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + arg: UnaryFunctionExpression arg """ - assert len(node.args) == 1 - arg = node.args[0] - lb1, ub1 = bnds_dict[arg] - bnds_dict[node] = interval.log10(lb1, ub1) + bnds_dict = visitor.bnds_dict + bnds_dict[node] = interval.log10(*bnds_dict[arg]) -def _prop_bnds_leaf_to_root_sin(node, bnds_dict, feasibility_tol): +def _prop_bnds_leaf_to_root_sin(visitor, node, arg): """ Parameters ---------- + visitor: _FBBTVisitorLeafToRoot node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + arg: UnaryFunctionExpression arg """ - assert len(node.args) == 1 - arg = node.args[0] - lb1, ub1 = bnds_dict[arg] - bnds_dict[node] = interval.sin(lb1, ub1) + bnds_dict = visitor.bnds_dict + bnds_dict[node] = interval.sin(*bnds_dict[arg]) -def _prop_bnds_leaf_to_root_cos(node, bnds_dict, feasibility_tol): +def _prop_bnds_leaf_to_root_cos(visitor, node, arg): """ Parameters ---------- + visitor: _FBBTVisitorLeafToRoot node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + arg: UnaryFunctionExpression arg """ - assert len(node.args) == 1 - arg = node.args[0] - lb1, ub1 = bnds_dict[arg] - bnds_dict[node] = interval.cos(lb1, ub1) + bnds_dict = visitor.bnds_dict + bnds_dict[node] = interval.cos(*bnds_dict[arg]) -def _prop_bnds_leaf_to_root_tan(node, bnds_dict, feasibility_tol): +def _prop_bnds_leaf_to_root_tan(visitor, node, arg): """ Parameters ---------- + visitor: _FBBTVisitorLeafToRoot node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + arg: UnaryFunctionExpression arg """ - assert len(node.args) == 1 - arg = node.args[0] - lb1, ub1 = bnds_dict[arg] - bnds_dict[node] = interval.tan(lb1, ub1) + bnds_dict = visitor.bnds_dict + bnds_dict[node] = interval.tan(*bnds_dict[arg]) -def _prop_bnds_leaf_to_root_asin(node, bnds_dict, feasibility_tol): +def _prop_bnds_leaf_to_root_asin(visitor, node, arg): """ Parameters ---------- + visitor: _FBBTVisitorLeafToRoot node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + arg: UnaryFunctionExpression arg """ - assert len(node.args) == 1 - arg = node.args[0] - lb1, ub1 = bnds_dict[arg] + bnds_dict = visitor.bnds_dict bnds_dict[node] = interval.asin( - lb1, ub1, -interval.inf, interval.inf, feasibility_tol + *bnds_dict[arg], -interval.inf, interval.inf, visitor.feasibility_tol ) -def _prop_bnds_leaf_to_root_acos(node, bnds_dict, feasibility_tol): +def _prop_bnds_leaf_to_root_acos(visitor, node, arg): """ Parameters ---------- + visitor: _FBBTVisitorLeafToRoot node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + arg: UnaryFunctionExpression arg """ - assert len(node.args) == 1 - arg = node.args[0] - lb1, ub1 = bnds_dict[arg] + bnds_dict = visitor.bnds_dict bnds_dict[node] = interval.acos( - lb1, ub1, -interval.inf, interval.inf, feasibility_tol + *bnds_dict[arg], -interval.inf, interval.inf, visitor.feasibility_tol ) -def _prop_bnds_leaf_to_root_atan(node, bnds_dict, feasibility_tol): +def _prop_bnds_leaf_to_root_atan(visitor, node, arg): """ Parameters ---------- + visitor: _FBBTVisitorLeafToRoot node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). """ - assert len(node.args) == 1 - arg = node.args[0] - lb1, ub1 = bnds_dict[arg] - bnds_dict[node] = interval.atan(lb1, ub1, -interval.inf, interval.inf) + bnds_dict = visitor.bnds_dict + bnds_dict[node] = interval.atan(*bnds_dict[arg], -interval.inf, interval.inf) -def _prop_bnds_leaf_to_root_sqrt(node, bnds_dict, feasibility_tol): +def _prop_bnds_leaf_to_root_sqrt(visitor, node, arg): """ Parameters ---------- + visitor: _FBBTVisitorLeafToRoot node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + arg: UnaryFunctionExpression arg """ - assert len(node.args) == 1 - arg = node.args[0] - lb1, ub1 = bnds_dict[arg] + bnds_dict = visitor.bnds_dict bnds_dict[node] = interval.power( - lb1, ub1, 0.5, 0.5, feasibility_tol=feasibility_tol + *bnds_dict[arg], 0.5, 0.5, feasibility_tol=visitor.feasibility_tol ) -def _prop_bnds_leaf_to_root_abs(node, bnds_dict, feasibility_tol): - assert len(node.args) == 1 - arg = node.args[0] - lb1, ub1 = bnds_dict[arg] - bnds_dict[node] = interval.interval_abs(lb1, ub1) +def _prop_bnds_leaf_to_root_abs(visitor, node, arg): + bnds_dict = visitor.bnds_dict + bnds_dict[node] = interval.interval_abs(*bnds_dict[arg]) -_unary_leaf_to_root_map = dict() -_unary_leaf_to_root_map['exp'] = _prop_bnds_leaf_to_root_exp -_unary_leaf_to_root_map['log'] = _prop_bnds_leaf_to_root_log -_unary_leaf_to_root_map['log10'] = _prop_bnds_leaf_to_root_log10 -_unary_leaf_to_root_map['sin'] = _prop_bnds_leaf_to_root_sin -_unary_leaf_to_root_map['cos'] = _prop_bnds_leaf_to_root_cos -_unary_leaf_to_root_map['tan'] = _prop_bnds_leaf_to_root_tan -_unary_leaf_to_root_map['asin'] = _prop_bnds_leaf_to_root_asin -_unary_leaf_to_root_map['acos'] = _prop_bnds_leaf_to_root_acos -_unary_leaf_to_root_map['atan'] = _prop_bnds_leaf_to_root_atan -_unary_leaf_to_root_map['sqrt'] = _prop_bnds_leaf_to_root_sqrt -_unary_leaf_to_root_map['abs'] = _prop_bnds_leaf_to_root_abs +def _prop_no_bounds(visitor, node, *args): + visitor.bnds_dict[node] = (-interval.inf, interval.inf) -def _prop_bnds_leaf_to_root_UnaryFunctionExpression(node, bnds_dict, feasibility_tol): +_unary_leaf_to_root_map = defaultdict( + lambda: _prop_no_bounds, + { + 'exp': _prop_bnds_leaf_to_root_exp, + 'log': _prop_bnds_leaf_to_root_log, + 'log10': _prop_bnds_leaf_to_root_log10, + 'sin': _prop_bnds_leaf_to_root_sin, + 'cos': _prop_bnds_leaf_to_root_cos, + 'tan': _prop_bnds_leaf_to_root_tan, + 'asin': _prop_bnds_leaf_to_root_asin, + 'acos': _prop_bnds_leaf_to_root_acos, + 'atan': _prop_bnds_leaf_to_root_atan, + 'sqrt': _prop_bnds_leaf_to_root_sqrt, + 'abs': _prop_bnds_leaf_to_root_abs, + }, +) + + +def _prop_bnds_leaf_to_root_UnaryFunctionExpression(visitor, node, arg): """ Parameters ---------- + visitor: _FBBTVisitorLeafToRoot node: pyomo.core.expr.numeric_expr.UnaryFunctionExpression - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + arg: UnaryFunctionExpression arg """ - if node.getname() in _unary_leaf_to_root_map: - _unary_leaf_to_root_map[node.getname()](node, bnds_dict, feasibility_tol) - else: - bnds_dict[node] = (-interval.inf, interval.inf) + _unary_leaf_to_root_map[node.getname()](visitor, node, arg) -def _prop_bnds_leaf_to_root_GeneralExpression(node, bnds_dict, feasibility_tol): +def _prop_bnds_leaf_to_root_NamedExpression(visitor, node, expr): """ Propagate bounds from children to parent Parameters ---------- - node: pyomo.core.base.expression._GeneralExpressionData - bnds_dict: ComponentMap - feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + visitor: _FBBTVisitorLeafToRoot + node: pyomo.core.base.expression.NamedExpressionData + expr: NamedExpressionData arg """ - (expr,) = node.args + bnds_dict = visitor.bnds_dict + if node in bnds_dict: + return + if expr.__class__ in native_types: expr_lb = expr_ub = expr else: @@ -451,39 +355,24 @@ def _prop_bnds_leaf_to_root_GeneralExpression(node, bnds_dict, feasibility_tol): bnds_dict[node] = (expr_lb, expr_ub) -_prop_bnds_leaf_to_root_map = dict() -_prop_bnds_leaf_to_root_map[ - numeric_expr.ProductExpression -] = _prop_bnds_leaf_to_root_ProductExpression -_prop_bnds_leaf_to_root_map[ - numeric_expr.DivisionExpression -] = _prop_bnds_leaf_to_root_DivisionExpression -_prop_bnds_leaf_to_root_map[ - numeric_expr.PowExpression -] = _prop_bnds_leaf_to_root_PowExpression -_prop_bnds_leaf_to_root_map[ - numeric_expr.SumExpression -] = _prop_bnds_leaf_to_root_SumExpression -_prop_bnds_leaf_to_root_map[ - numeric_expr.MonomialTermExpression -] = _prop_bnds_leaf_to_root_ProductExpression -_prop_bnds_leaf_to_root_map[ - numeric_expr.NegationExpression -] = _prop_bnds_leaf_to_root_NegationExpression -_prop_bnds_leaf_to_root_map[ - numeric_expr.UnaryFunctionExpression -] = _prop_bnds_leaf_to_root_UnaryFunctionExpression -_prop_bnds_leaf_to_root_map[ - numeric_expr.LinearExpression -] = _prop_bnds_leaf_to_root_SumExpression -_prop_bnds_leaf_to_root_map[numeric_expr.AbsExpression] = _prop_bnds_leaf_to_root_abs - -_prop_bnds_leaf_to_root_map[ - _GeneralExpressionData -] = _prop_bnds_leaf_to_root_GeneralExpression -_prop_bnds_leaf_to_root_map[ - ScalarExpression -] = _prop_bnds_leaf_to_root_GeneralExpression +_prop_bnds_leaf_to_root_map = defaultdict( + lambda: _prop_no_bounds, + { + numeric_expr.ProductExpression: _prop_bnds_leaf_to_root_ProductExpression, + numeric_expr.DivisionExpression: _prop_bnds_leaf_to_root_DivisionExpression, + numeric_expr.PowExpression: _prop_bnds_leaf_to_root_PowExpression, + numeric_expr.SumExpression: _prop_bnds_leaf_to_root_SumExpression, + numeric_expr.MonomialTermExpression: _prop_bnds_leaf_to_root_ProductExpression, + numeric_expr.NegationExpression: _prop_bnds_leaf_to_root_NegationExpression, + numeric_expr.UnaryFunctionExpression: _prop_bnds_leaf_to_root_UnaryFunctionExpression, + numeric_expr.LinearExpression: _prop_bnds_leaf_to_root_SumExpression, + numeric_expr.AbsExpression: _prop_bnds_leaf_to_root_abs, + ExpressionData: _prop_bnds_leaf_to_root_NamedExpression, + ScalarExpression: _prop_bnds_leaf_to_root_NamedExpression, + ObjectiveData: _prop_bnds_leaf_to_root_NamedExpression, + ScalarObjective: _prop_bnds_leaf_to_root_NamedExpression, + }, +) def _prop_bnds_root_to_leaf_ProductExpression(node, bnds_dict, feasibility_tol): @@ -1012,13 +901,13 @@ def _prop_bnds_root_to_leaf_UnaryFunctionExpression(node, bnds_dict, feasibility ) -def _prop_bnds_root_to_leaf_GeneralExpression(node, bnds_dict, feasibility_tol): +def _prop_bnds_root_to_leaf_NamedExpression(node, bnds_dict, feasibility_tol): """ Propagate bounds from parent to children. Parameters ---------- - node: pyomo.core.base.expression._GeneralExpressionData + node: pyomo.core.base.expression.NamedExpressionData bnds_dict: ComponentMap feasibility_tol: float If the bounds computed on the body of a constraint violate the bounds of the constraint by more than @@ -1033,46 +922,44 @@ def _prop_bnds_root_to_leaf_GeneralExpression(node, bnds_dict, feasibility_tol): _prop_bnds_root_to_leaf_map = dict() -_prop_bnds_root_to_leaf_map[ - numeric_expr.ProductExpression -] = _prop_bnds_root_to_leaf_ProductExpression -_prop_bnds_root_to_leaf_map[ - numeric_expr.DivisionExpression -] = _prop_bnds_root_to_leaf_DivisionExpression -_prop_bnds_root_to_leaf_map[ - numeric_expr.PowExpression -] = _prop_bnds_root_to_leaf_PowExpression -_prop_bnds_root_to_leaf_map[ - numeric_expr.SumExpression -] = _prop_bnds_root_to_leaf_SumExpression -_prop_bnds_root_to_leaf_map[ - numeric_expr.MonomialTermExpression -] = _prop_bnds_root_to_leaf_ProductExpression -_prop_bnds_root_to_leaf_map[ - numeric_expr.NegationExpression -] = _prop_bnds_root_to_leaf_NegationExpression -_prop_bnds_root_to_leaf_map[ - numeric_expr.UnaryFunctionExpression -] = _prop_bnds_root_to_leaf_UnaryFunctionExpression -_prop_bnds_root_to_leaf_map[ - numeric_expr.LinearExpression -] = _prop_bnds_root_to_leaf_SumExpression +_prop_bnds_root_to_leaf_map[numeric_expr.ProductExpression] = ( + _prop_bnds_root_to_leaf_ProductExpression +) +_prop_bnds_root_to_leaf_map[numeric_expr.DivisionExpression] = ( + _prop_bnds_root_to_leaf_DivisionExpression +) +_prop_bnds_root_to_leaf_map[numeric_expr.PowExpression] = ( + _prop_bnds_root_to_leaf_PowExpression +) +_prop_bnds_root_to_leaf_map[numeric_expr.SumExpression] = ( + _prop_bnds_root_to_leaf_SumExpression +) +_prop_bnds_root_to_leaf_map[numeric_expr.MonomialTermExpression] = ( + _prop_bnds_root_to_leaf_ProductExpression +) +_prop_bnds_root_to_leaf_map[numeric_expr.NegationExpression] = ( + _prop_bnds_root_to_leaf_NegationExpression +) +_prop_bnds_root_to_leaf_map[numeric_expr.UnaryFunctionExpression] = ( + _prop_bnds_root_to_leaf_UnaryFunctionExpression +) +_prop_bnds_root_to_leaf_map[numeric_expr.LinearExpression] = ( + _prop_bnds_root_to_leaf_SumExpression +) _prop_bnds_root_to_leaf_map[numeric_expr.AbsExpression] = _prop_bnds_root_to_leaf_abs -_prop_bnds_root_to_leaf_map[ - _GeneralExpressionData -] = _prop_bnds_root_to_leaf_GeneralExpression -_prop_bnds_root_to_leaf_map[ - ScalarExpression -] = _prop_bnds_root_to_leaf_GeneralExpression +_prop_bnds_root_to_leaf_map[ExpressionData] = _prop_bnds_root_to_leaf_NamedExpression +_prop_bnds_root_to_leaf_map[ScalarExpression] = _prop_bnds_root_to_leaf_NamedExpression +_prop_bnds_root_to_leaf_map[ObjectiveData] = _prop_bnds_root_to_leaf_NamedExpression +_prop_bnds_root_to_leaf_map[ScalarObjective] = _prop_bnds_root_to_leaf_NamedExpression def _check_and_reset_bounds(var, lb, ub): """ This function ensures that lb is not less than var.lb and that ub is not greater than var.ub. """ - orig_lb = value(var.lb) - orig_ub = value(var.ub) + orig_lb = var.lb + orig_ub = var.ub if orig_lb is None: orig_lb = -interval.inf if orig_ub is None: @@ -1084,7 +971,77 @@ def _check_and_reset_bounds(var, lb, ub): return lb, ub -class _FBBTVisitorLeafToRoot(ExpressionValueVisitor): +def _before_constant(visitor, child): + if child in visitor.bnds_dict: + pass + else: + visitor.bnds_dict[child] = (child, child) + return False, None + + +def _before_var(visitor, child): + if child in visitor.bnds_dict: + return False, None + elif child.is_fixed() and not visitor.ignore_fixed: + lb = value(child.value) + ub = lb + else: + lb = child.lb + ub = child.ub + if lb is None: + lb = -interval.inf + if ub is None: + ub = interval.inf + if lb - visitor.feasibility_tol > ub: + raise InfeasibleConstraintException( + 'Variable has a lower bound that is larger than its ' + 'upper bound: {0}'.format(str(child)) + ) + visitor.bnds_dict[child] = (lb, ub) + return False, None + + +def _before_NPV(visitor, child): + if child in visitor.bnds_dict: + return False, None + val = value(child) + visitor.bnds_dict[child] = (val, val) + return False, None + + +def _before_other(visitor, child): + return True, None + + +def _before_external_function(visitor, child): + # TODO: provide some mechanism for users to provide interval + # arithmetic callback functions for general external + # functions + visitor.bnds_dict[child] = (-interval.inf, interval.inf) + return False, None + + +def _register_new_before_child_handler(visitor, child): + handlers = _before_child_handlers + child_type = child.__class__ + if child.is_variable_type(): + handlers[child_type] = _before_var + elif not child.is_potentially_variable(): + handlers[child_type] = _before_NPV + else: + handlers[child_type] = _before_other + return handlers[child_type](visitor, child) + + +_before_child_handlers = defaultdict(lambda: _register_new_before_child_handler) +_before_child_handlers[numeric_expr.ExternalFunctionExpression] = ( + _before_external_function +) +for _type in nonpyomo_leaf_types: + _before_child_handlers[_type] = _before_constant + + +class _FBBTVisitorLeafToRoot(StreamBasedExpressionVisitor): """ This walker propagates bounds from the variables to each node in the expression tree (all the way to the root node). @@ -1099,68 +1056,30 @@ def __init__( bnds_dict: ComponentMap integer_tol: float feasibility_tol: float - If the bounds computed on the body of a constraint violate the bounds of the constraint by more than - feasibility_tol, then the constraint is considered infeasible and an exception is raised. This tolerance - is also used when performing certain interval arithmetic operations to ensure that none of the feasible - region is removed due to floating point arithmetic and to prevent math domain errors (a larger value - is more conservative). + If the bounds computed on the body of a constraint violate the bounds of + the constraint by more than feasibility_tol, then the constraint is + considered infeasible and an exception is raised. This tolerance is also + used when performing certain interval arithmetic operations to ensure that + none of the feasible region is removed due to floating point arithmetic and + to prevent math domain errors (a larger value is more conservative). """ + super().__init__() self.bnds_dict = bnds_dict self.integer_tol = integer_tol self.feasibility_tol = feasibility_tol self.ignore_fixed = ignore_fixed - def visit(self, node, values): - if node.__class__ in _prop_bnds_leaf_to_root_map: - _prop_bnds_leaf_to_root_map[node.__class__]( - node, self.bnds_dict, self.feasibility_tol - ) - else: - self.bnds_dict[node] = (-interval.inf, interval.inf) - return None + def initializeWalker(self, expr): + walk, result = self.beforeChild(None, expr, 0) + if not walk: + return False, result + return True, expr - def visiting_potential_leaf(self, node): - if node.__class__ in nonpyomo_leaf_types: - self.bnds_dict[node] = (node, node) - return True, None + def beforeChild(self, node, child, child_idx): + return _before_child_handlers[child.__class__](self, child) - if node.is_variable_type(): - if node in self.bnds_dict: - return True, None - if node.is_fixed() and not self.ignore_fixed: - lb = value(node.value) - ub = lb - else: - lb = value(node.lb) - ub = value(node.ub) - if lb is None: - lb = -interval.inf - if ub is None: - ub = interval.inf - if lb - self.feasibility_tol > ub: - raise InfeasibleConstraintException( - 'Variable has a lower bound that is larger than its upper bound: {0}'.format( - str(node) - ) - ) - self.bnds_dict[node] = (lb, ub) - return True, None - - if not node.is_potentially_variable(): - # NPV nodes are effectively constant leaves. Evaluate it - # and return the value. - val = value(node) - self.bnds_dict[node] = (val, val) - return True, None - - if node.__class__ is numeric_expr.ExternalFunctionExpression: - # TODO: provide some mechanism for users to provide interval - # arithmetic callback functions for general external - # functions - self.bnds_dict[node] = (-interval.inf, interval.inf) - return True, None - - return False, None + def exitNode(self, node, data): + _prop_bnds_leaf_to_root_map[node.__class__](self, node, *node.args) class _FBBTVisitorRootToLeaf(ExpressionValueVisitor): @@ -1313,7 +1232,7 @@ def _fbbt_con(con, config): ---------- con: pyomo.core.base.constraint.Constraint constraint on which to perform fbbt - config: ConfigBlock + config: ConfigDict see documentation for fbbt Returns @@ -1331,7 +1250,7 @@ def _fbbt_con(con, config): # a walker to propagate bounds from the variables to the root visitorA = _FBBTVisitorLeafToRoot(bnds_dict, feasibility_tol=config.feasibility_tol) - visitorA.dfs_postorder_stack(con.body) + visitorA.walk_expression(con.body) # Now we need to replace the bounds in bnds_dict for the root # node with the bounds on the constraint (if those bounds are @@ -1398,7 +1317,7 @@ def _fbbt_block(m, config): Parameters ---------- m: pyomo.core.base.block.Block or pyomo.core.base.PyomoModel.ConcreteModel - config: ConfigBlock + config: ConfigDict See the docs for fbbt Returns @@ -1421,11 +1340,11 @@ def _fbbt_block(m, config): if v.lb is None: var_lbs[v] = -interval.inf else: - var_lbs[v] = value(v.lb) + var_lbs[v] = v.lb if v.ub is None: var_ubs[v] = interval.inf else: - var_ubs[v] = value(v.ub) + var_ubs[v] = v.ub var_to_con_map[v].append(c) n_cons += 1 @@ -1529,7 +1448,7 @@ def fbbt( A ComponentMap mapping from variables a tuple containing the lower and upper bounds, respectively, computed from FBBT. """ - config = ConfigBlock() + config = ConfigDict() dsc_config = ConfigValue( default=deactivate_satisfied_constraints, domain=In({True, False}) ) @@ -1569,21 +1488,23 @@ def fbbt( def compute_bounds_on_expr(expr, ignore_fixed=False): """ - Compute bounds on an expression based on the bounds on the variables in the expression. + Compute bounds on an expression based on the bounds on the variables in + the expression. Parameters ---------- expr: pyomo.core.expr.numeric_expr.NumericExpression + ignore_fixed: bool, treats fixed Vars as constants if False, else treats + them as Vars Returns ------- lb: float ub: float """ - bnds_dict = ComponentMap() - visitor = _FBBTVisitorLeafToRoot(bnds_dict, ignore_fixed=ignore_fixed) - visitor.dfs_postorder_stack(expr) - lb, ub = bnds_dict[expr] + lb, ub = ExpressionBoundsVisitor( + use_fixed_var_values_as_bounds=not ignore_fixed + ).walk_expression(expr) if lb == -interval.inf: lb = None if ub == interval.inf: diff --git a/pyomo/contrib/fbbt/interval.py b/pyomo/contrib/fbbt/interval.py index aca6531c8df..a12d1a4529f 100644 --- a/pyomo/contrib/fbbt/interval.py +++ b/pyomo/contrib/fbbt/interval.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -17,6 +17,119 @@ inf = float('inf') +class _bool_flag(object): + def __init__(self, val): + self._val = val + + def __bool__(self): + return self._val + + def _op(self, *others): + raise ValueError( + f"{self._val!r} ({type(self._val).__name__}) is not a valid numeric type. " + f"Cannot compute bounds on expression." + ) + + def __repr__(self): + return repr(self._val) + + __float__ = _op + __int__ = _op + __abs__ = _op + __neg__ = _op + __add__ = _op + __sub__ = _op + __mul__ = _op + __div__ = _op + __pow__ = _op + __radd__ = _op + __rsub__ = _op + __rmul__ = _op + __rdiv__ = _op + __rpow__ = _op + + +_true = _bool_flag(True) +_false = _bool_flag(False) + + +def BoolFlag(val): + return _true if val else _false + + +def ineq(xl, xu, yl, yu): + """Compute the "bounds" on an InequalityExpression + + Note this is *not* performing interval arithmetic: we are + calculating the "bounds" on a RelationalExpression (whose domain is + {True, False}). Therefore we are determining if `x` can be less + than `y`, `x` can not be less than `y`, or both. + + """ + ans = [] + if yl < xu: + ans.append(_false) + if xl <= yu: + ans.append(_true) + assert ans + if len(ans) == 1: + ans.append(ans[0]) + return tuple(ans) + + +def eq(xl, xu, yl, yu): + """Compute the "bounds" on an EqualityExpression + + Note this is *not* performing interval arithmetic: we are + calculating the "bounds" on a RelationalExpression (whose domain is + {True, False}). Therefore we are determining if `x` can be equal to + `y`, `x` can not be equal to `y`, or both. + + """ + ans = [] + if xl != xu or yl != yu or xl != yl: + ans.append(_false) + if xl <= yu and yl <= xu: + ans.append(_true) + assert ans + if len(ans) == 1: + ans.append(ans[0]) + return tuple(ans) + + +def ranged(xl, xu, yl, yu, zl, zu): + """Compute the "bounds" on a RangedExpression + + Note this is *not* performing interval arithmetic: we are + calculating the "bounds" on a RelationalExpression (whose domain is + {True, False}). Therefore we are determining if `y` can be between + `z` and `z`, `y` can be outside the range `x` and `z`, or both. + + """ + lb = ineq(xl, xu, yl, yu) + ub = ineq(yl, yu, zl, zu) + ans = [] + if not lb[0] or not ub[0]: + ans.append(_false) + if lb[1] and ub[1]: + ans.append(_true) + if len(ans) == 1: + ans.append(ans[0]) + return tuple(ans) + + +def if_(il, iu, tl, tu, fl, fu): + l = [] + u = [] + if iu: + l.append(tl) + u.append(tu) + if not il: + l.append(fl) + u.append(fu) + return min(l), max(u) + + def add(xl, xu, yl, yu): return xl + yl, xu + yu @@ -26,23 +139,31 @@ def sub(xl, xu, yl, yu): def mul(xl, xu, yl, yu): - options = [xl * yl, xl * yu, xu * yl, xu * yu] - if any(math.isnan(i) for i in options): - lb = -inf - ub = inf - else: - lb = min(options) - ub = max(options) + lb = inf + ub = -inf + for i in (xl * yl, xu * yu, xu * yl, xl * yu): + if i < lb: + lb = i + if i > ub: + ub = i + if i != i: # math.isnan(i) + return (-inf, inf) return lb, ub def inv(xl, xu, feasibility_tol): - """ - The case where xl is very slightly positive but should be very slightly negative (or xu is very slightly negative - but should be very slightly positive) should not be an issue. Suppose xu is 2 and xl is 1e-15 but should be -1e-15. - The bounds obtained from this function will be [0.5, 1e15] or [0.5, inf), depending on the value of - feasibility_tol. The true bounds are (-inf, -1e15] U [0.5, inf), where U is union. The exclusion of (-inf, -1e15] - should be acceptable. Additionally, it very important to return a non-negative interval when xl is non-negative. + """Compute the inverse of an interval + + The case where xl is very slightly positive but should be very + slightly negative (or xu is very slightly negative but should be + very slightly positive) should not be an issue. Suppose xu is 2 and + xl is 1e-15 but should be -1e-15. The bounds obtained from this + function will be [0.5, 1e15] or [0.5, inf), depending on the value + of feasibility_tol. The true bounds are (-inf, -1e15] U [0.5, inf), + where U is union. The exclusion of (-inf, -1e15] should be + acceptable. Additionally, it very important to return a non-negative + interval when xl is non-negative. + """ if xu - xl <= -feasibility_tol: raise InfeasibleConstraintException( @@ -79,8 +200,7 @@ def inv(xl, xu, feasibility_tol): def div(xl, xu, yl, yu, feasibility_tol): - lb, ub = mul(xl, xu, *inv(yl, yu, feasibility_tol)) - return lb, ub + return mul(xl, xu, *inv(yl, yu, feasibility_tol)) def power(xl, xu, yl, yu, feasibility_tol): @@ -88,9 +208,8 @@ def power(xl, xu, yl, yu, feasibility_tol): Compute bounds on x**y. """ if xl > 0: - """ - If x is always positive, things are simple. We only need to worry about the sign of y. - """ + # If x is always positive, things are simple. We only need to + # worry about the sign of y. if yl < 0 < yu: lb = min(xu**yl, xl**yu) ub = max(xl**yl, xu**yu) @@ -146,7 +265,7 @@ def power(xl, xu, yl, yu, feasibility_tol): else: lb = xl**y ub = xu**y - else: + else: # xu is positive if y < 0: if y % 2 == 0: lb = min(xl**y, xu**y) @@ -154,8 +273,9 @@ def power(xl, xu, yl, yu, feasibility_tol): else: lb = -inf ub = inf - else: + else: # exponent is nonnegative if y % 2 == 0: + # xl is negative and xu is positive, so lb is 0 lb = 0 ub = max(xl**y, xu**y) else: @@ -179,14 +299,15 @@ def power(xl, xu, yl, yu, feasibility_tol): def _inverse_power1(zl, zu, yl, yu, orig_xl, orig_xu, feasibility_tol): - """ - z = x**y => compute bounds on x. + """z = x**y => compute bounds on x. First, start by computing bounds on x with x = exp(ln(z) / y) - However, if y is an integer, then x can be negative, so there are several special cases. See the docs below. + However, if y is an integer, then x can be negative, so there are + several special cases. See the docs below. + """ xl, xu = log(zl, zu) xl, xu = div(xl, xu, yl, yu, feasibility_tol) @@ -197,22 +318,31 @@ def _inverse_power1(zl, zu, yl, yu, orig_xl, orig_xu, feasibility_tol): y = yl if y == 0: # Anything to the power of 0 is 1, so if y is 0, then x can be anything - # (assuming zl <= 1 <= zu, which is enforced when traversing the tree in the other direction) + # (assuming zl <= 1 <= zu, which is enforced when traversing + # the tree in the other direction) xl = -inf xu = inf elif y % 2 == 0: - """ - if y is even, then there are two primary cases (note that it is much easier to walk through these - while looking at plots): + """if y is even, then there are two primary cases (note that it is much + easier to walk through these while looking at plots): + case 1: y is positive - x**y is convex, positive, and symmetric. The bounds on x depend on the lower bound of z. If zl <= 0, - then xl should simply be -xu. However, if zl > 0, then we may be able to say something better. For - example, if the original lower bound on x is positive, then we can keep xl computed from - x = exp(ln(z) / y). Furthermore, if the original lower bound on x is larger than -xl computed from - x = exp(ln(z) / y), then we can still keep the xl computed from x = exp(ln(z) / y). Similar logic - applies to the upper bound of x. + + x**y is convex, positive, and symmetric. The bounds on x + depend on the lower bound of z. If zl <= 0, then xl + should simply be -xu. However, if zl > 0, then we may be + able to say something better. For example, if the + original lower bound on x is positive, then we can keep + xl computed from x = exp(ln(z) / y). Furthermore, if the + original lower bound on x is larger than -xl computed + from x = exp(ln(z) / y), then we can still keep the xl + computed from x = exp(ln(z) / y). Similar logic applies + to the upper bound of x. + case 2: y is negative + The ideas are similar to case 1. + """ if zu + feasibility_tol < 0: raise InfeasibleConstraintException( @@ -260,16 +390,25 @@ def _inverse_power1(zl, zu, yl, yu, orig_xl, orig_xu, feasibility_tol): xl = _xl xu = _xu else: # y % 2 == 1 - """ - y is odd. + """y is odd. + Case 1: y is positive - x**y is monotonically increasing. If y is positive, then we can can compute the bounds on x using - x = z**(1/y) and the signs on xl and xu depend on the signs of zl and zu. + + x**y is monotonically increasing. If y is positive, then + we can can compute the bounds on x using x = z**(1/y) + and the signs on xl and xu depend on the signs of zl and + zu. + Case 2: y is negative - Again, this is easier to visualize with a plot. x**y approaches zero when x approaches -inf or inf. - Thus, if zl < 0 < zu, then no bounds can be inferred for x. If z is positive (zl >=0 ) then we can - use the bounds computed from x = exp(ln(z) / y). If z is negative (zu <= 0), then we live in the - bottom left quadrant, xl depends on zu, and xu depends on zl. + + Again, this is easier to visualize with a plot. x**y + approaches zero when x approaches -inf or inf. Thus, if + zl < 0 < zu, then no bounds can be inferred for x. If z + is positive (zl >=0 ) then we can use the bounds + computed from x = exp(ln(z) / y). If z is negative (zu + <= 0), then we live in the bottom left quadrant, xl + depends on zu, and xu depends on zl. + """ if y > 0: xl = abs(zl) ** (1.0 / y) @@ -296,12 +435,13 @@ def _inverse_power1(zl, zu, yl, yu, orig_xl, orig_xu, feasibility_tol): def _inverse_power2(zl, zu, xl, xu, feasiblity_tol): - """ - z = x**y => compute bounds on y + """z = x**y => compute bounds on y y = ln(z) / ln(x) - This function assumes the exponent can be fractional, so x must be positive. This method should not be called - if the exponent is an integer. + This function assumes the exponent can be fractional, so x must be + positive. This method should not be called if the exponent is an + integer. + """ if xu <= 0: raise IntervalException( @@ -320,7 +460,7 @@ def _inverse_power2(zl, zu, xl, xu, feasiblity_tol): def interval_abs(xl, xu): abs_xl = abs(xl) abs_xu = abs(xu) - if xl <= 0 <= xu: + if xl <= 0 and 0 <= xu: res_lb = 0 res_ub = max(abs_xl, abs_xu) else: @@ -389,10 +529,12 @@ def sin(xl, xu): ub: float """ - # if there is a minimum between xl and xu, then the lower bound is -1. Minimums occur at 2*pi*n - pi/2 - # find the minimum value of i such that 2*pi*i - pi/2 >= xl. Then round i up. If 2*pi*i - pi/2 is still less - # than or equal to xu, then there is a minimum between xl and xu. Thus the lb is -1. Otherwise, the minimum - # occurs at either xl or xu + # if there is a minimum between xl and xu, then the lower bound is + # -1. Minimums occur at 2*pi*n - pi/2 find the minimum value of i + # such that 2*pi*i - pi/2 >= xl. Then round i up. If 2*pi*i - pi/2 + # is still less than or equal to xu, then there is a minimum between + # xl and xu. Thus the lb is -1. Otherwise, the minimum occurs at + # either xl or xu if xl <= -inf or xu >= inf: return -1, 1 pi = math.pi @@ -404,7 +546,8 @@ def sin(xl, xu): else: lb = min(math.sin(xl), math.sin(xu)) - # if there is a maximum between xl and xu, then the upper bound is 1. Maximums occur at 2*pi*n + pi/2 + # if there is a maximum between xl and xu, then the upper bound is + # 1. Maximums occur at 2*pi*n + pi/2 i = (xu - pi / 2) / (2 * pi) i = math.floor(i) x_at_max = 2 * pi * i + pi / 2 @@ -430,10 +573,12 @@ def cos(xl, xu): ub: float """ - # if there is a minimum between xl and xu, then the lower bound is -1. Minimums occur at 2*pi*n - pi - # find the minimum value of i such that 2*pi*i - pi >= xl. Then round i up. If 2*pi*i - pi/2 is still less - # than or equal to xu, then there is a minimum between xl and xu. Thus the lb is -1. Otherwise, the minimum - # occurs at either xl or xu + # if there is a minimum between xl and xu, then the lower bound is + # -1. Minimums occur at 2*pi*n - pi find the minimum value of i such + # that 2*pi*i - pi >= xl. Then round i up. If 2*pi*i - pi/2 is still + # less than or equal to xu, then there is a minimum between xl and + # xu. Thus the lb is -1. Otherwise, the minimum occurs at either xl + # or xu if xl <= -inf or xu >= inf: return -1, 1 pi = math.pi @@ -445,7 +590,8 @@ def cos(xl, xu): else: lb = min(math.cos(xl), math.cos(xu)) - # if there is a maximum between xl and xu, then the upper bound is 1. Maximums occur at 2*pi*n + # if there is a maximum between xl and xu, then the upper bound is + # 1. Maximums occur at 2*pi*n i = (xu) / (2 * pi) i = math.floor(i) x_at_max = 2 * pi * i @@ -471,10 +617,12 @@ def tan(xl, xu): ub: float """ - # tan goes to -inf and inf at every pi*i + pi/2 (integer i). If one of these values is between xl and xu, then - # the lb is -inf and the ub is inf. Otherwise the minimum occurs at xl and the maximum occurs at xu. - # find the minimum value of i such that pi*i + pi/2 >= xl. Then round i up. If pi*i + pi/2 is still less - # than or equal to xu, then there is an undefined point between xl and xu. + # tan goes to -inf and inf at every pi*i + pi/2 (integer i). If one + # of these values is between xl and xu, then the lb is -inf and the + # ub is inf. Otherwise the minimum occurs at xl and the maximum + # occurs at xu. find the minimum value of i such that pi*i + pi/2 + # >= xl. Then round i up. If pi*i + pi/2 is still less than or equal + # to xu, then there is an undefined point between xl and xu. if xl <= -inf or xu >= inf: return -inf, inf pi = math.pi @@ -518,12 +666,12 @@ def asin(xl, xu, yl, yu, feasibility_tol): if yl <= -inf: lb = yl elif xl <= math.sin(yl) <= xu: - # if sin(yl) >= xl then yl satisfies the bounds on x, and the lower bound of y cannot be improved + # if sin(yl) >= xl then yl satisfies the bounds on x, and the + # lower bound of y cannot be improved lb = yl elif math.sin(yl) < xl: - """ - we can only push yl up from its current value to the next lowest value such that xl = sin(y). In other words, - we need to + """we can only push yl up from its current value to the next lowest + value such that xl = sin(y). In other words, we need to min y s.t. @@ -531,19 +679,21 @@ def asin(xl, xu, yl, yu, feasibility_tol): y >= yl globally. + """ - # first find the next minimum of x = sin(y). Minimums occur at y = 2*pi*n - pi/2 for integer n. + # first find the next minimum of x = sin(y). Minimums occur at y + # = 2*pi*n - pi/2 for integer n. i = (yl + pi / 2) / (2 * pi) i1 = math.floor(i) i2 = math.ceil(i) i1 = 2 * pi * i1 - pi / 2 i2 = 2 * pi * i2 - pi / 2 - # now find the next value of y such that xl = sin(y). This can be computed by a distance from the minimum (i). + # now find the next value of y such that xl = sin(y). This can + # be computed by a distance from the minimum (i). y_tmp = math.asin(xl) # this will give me a value between -pi/2 and pi/2 - dist = y_tmp - ( - -pi / 2 - ) # this is the distance between the minimum of the sin function and a value that - # satisfies xl = sin(y) + dist = y_tmp - (-pi / 2) + # this is the distance between the minimum of the sin function + # and a value that satisfies xl = sin(y) lb1 = i1 + dist lb2 = i2 + dist if lb1 >= yl - feasibility_tol: @@ -631,12 +781,12 @@ def acos(xl, xu, yl, yu, feasibility_tol): if yl <= -inf: lb = yl elif xl <= math.cos(yl) <= xu: - # if xl <= cos(yl) <= xu then yl satisfies the bounds on x, and the lower bound of y cannot be improved + # if xl <= cos(yl) <= xu then yl satisfies the bounds on x, and + # the lower bound of y cannot be improved lb = yl elif math.cos(yl) < xl: - """ - we can only push yl up from its current value to the next lowest value such that xl = cos(y). In other words, - we need to + """we can only push yl up from its current value to the next lowest + value such that xl = cos(y). In other words, we need to min y s.t. @@ -644,19 +794,21 @@ def acos(xl, xu, yl, yu, feasibility_tol): y >= yl globally. + """ - # first find the next minimum of x = cos(y). Minimums occur at y = 2*pi*n - pi for integer n. + # first find the next minimum of x = cos(y). Minimums occur at y + # = 2*pi*n - pi for integer n. i = (yl + pi) / (2 * pi) i1 = math.floor(i) i2 = math.ceil(i) i1 = 2 * pi * i1 - pi i2 = 2 * pi * i2 - pi - # now find the next value of y such that xl = cos(y). This can be computed by a distance from the minimum (i). + # now find the next value of y such that xl = cos(y). This can + # be computed by a distance from the minimum (i). y_tmp = math.acos(xl) # this will give me a value between 0 and pi - dist = ( - pi - y_tmp - ) # this is the distance between the minimum of the sin function and a value that - # satisfies xl = sin(y) + dist = pi - y_tmp + # this is the distance between the minimum of the sin function + # and a value that satisfies xl = sin(y) lb1 = i1 + dist lb2 = i2 + dist if lb1 >= yl - feasibility_tol: diff --git a/pyomo/contrib/fbbt/tests/__init__.py b/pyomo/contrib/fbbt/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/fbbt/tests/__init__.py +++ b/pyomo/contrib/fbbt/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/fbbt/tests/test_expression_bounds_walker.py b/pyomo/contrib/fbbt/tests/test_expression_bounds_walker.py new file mode 100644 index 00000000000..5d27a2e4087 --- /dev/null +++ b/pyomo/contrib/fbbt/tests/test_expression_bounds_walker.py @@ -0,0 +1,415 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import math +import pyomo.common.unittest as unittest + +from pyomo.environ import ( + exp, + log, + log10, + sin, + cos, + tan, + asin, + acos, + atan, + sqrt, + inequality, + Expr_if, + Any, + ConcreteModel, + Expression, + Param, + Var, +) + +from pyomo.common.errors import DeveloperError +from pyomo.common.log import LoggingIntercept +from pyomo.contrib.fbbt.expression_bounds_walker import ExpressionBoundsVisitor, inf +from pyomo.contrib.fbbt.interval import _true, _false +from pyomo.core.expr import ExpressionBase, NumericExpression, BooleanExpression + + +class TestExpressionBoundsWalker(unittest.TestCase): + def make_model(self): + m = ConcreteModel() + m.x = Var(bounds=(-2, 4)) + m.y = Var(bounds=(3, 5)) + m.z = Var(bounds=(0.5, 0.75)) + return m + + def test_sum_bounds(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(m.x + m.y) + self.assertEqual(lb, 1) + self.assertEqual(ub, 9) + + self.assertEqual(len(visitor.leaf_bounds), 2) + self.assertIn(m.x, visitor.leaf_bounds) + self.assertIn(m.y, visitor.leaf_bounds) + self.assertEqual(visitor.leaf_bounds[m.x], (-2, 4)) + self.assertEqual(visitor.leaf_bounds[m.y], (3, 5)) + + def test_fixed_var(self): + m = self.make_model() + m.x.fix(3) + + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(m.x + m.y) + self.assertEqual(lb, 1) + self.assertEqual(ub, 9) + + self.assertEqual(len(visitor.leaf_bounds), 2) + self.assertIn(m.x, visitor.leaf_bounds) + self.assertIn(m.y, visitor.leaf_bounds) + self.assertEqual(visitor.leaf_bounds[m.x], (-2, 4)) + self.assertEqual(visitor.leaf_bounds[m.y], (3, 5)) + + def test_fixed_var_value_used_for_bounds(self): + m = self.make_model() + m.x.fix(3) + + visitor = ExpressionBoundsVisitor(use_fixed_var_values_as_bounds=True) + lb, ub = visitor.walk_expression(m.x + m.y) + self.assertEqual(lb, 6) + self.assertEqual(ub, 8) + + self.assertEqual(len(visitor.leaf_bounds), 2) + self.assertIn(m.x, visitor.leaf_bounds) + self.assertIn(m.y, visitor.leaf_bounds) + self.assertEqual(visitor.leaf_bounds[m.x], (3, 3)) + self.assertEqual(visitor.leaf_bounds[m.y], (3, 5)) + + def test_product_bounds(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(m.x * m.y) + self.assertEqual(lb, -10) + self.assertEqual(ub, 20) + + def test_division_bounds(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(m.x / m.y) + self.assertAlmostEqual(lb, -2 / 3) + self.assertAlmostEqual(ub, 4 / 3) + + def test_power_bounds(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(m.y**m.x) + self.assertEqual(lb, 5 ** (-2)) + self.assertEqual(ub, 5**4) + + def test_sums_of_squares_bounds(self): + m = ConcreteModel() + m.x = Var([1, 2], bounds=(-2, 6)) + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(m.x[1] * m.x[1] + m.x[2] * m.x[2]) + self.assertEqual(lb, 0) + self.assertEqual(ub, 72) + + def test_negation_bounds(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(-(m.y + 3 * m.x)) + self.assertEqual(lb, -17) + self.assertEqual(ub, 3) + + def test_exp_bounds(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(exp(m.y)) + self.assertAlmostEqual(lb, math.e**3) + self.assertAlmostEqual(ub, math.e**5) + + def test_log_bounds(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(log(m.y)) + self.assertAlmostEqual(lb, log(3)) + self.assertAlmostEqual(ub, log(5)) + + def test_log10_bounds(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(log10(m.y)) + self.assertAlmostEqual(lb, log10(3)) + self.assertAlmostEqual(ub, log10(5)) + + def test_sin_bounds(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(sin(m.y)) + self.assertAlmostEqual(lb, -1) # reaches -1 at 3*pi/2 \approx 4.712 + self.assertAlmostEqual(ub, sin(3)) # it's positive here + + def test_cos_bounds(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(cos(m.y)) + self.assertAlmostEqual(lb, -1) # reaches -1 at pi + self.assertAlmostEqual(ub, cos(5)) # it's positive here + + def test_tan_bounds(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(tan(m.y)) + self.assertEqual(lb, -float('inf')) + self.assertEqual(ub, float('inf')) + + def test_asin_bounds(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(asin(m.z)) + self.assertAlmostEqual(lb, asin(0.5)) + self.assertAlmostEqual(ub, asin(0.75)) + + def test_acos_bounds(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(acos(m.z)) + self.assertAlmostEqual(lb, acos(0.75)) + self.assertAlmostEqual(ub, acos(0.5)) + + def test_atan_bounds(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(atan(m.z)) + self.assertAlmostEqual(lb, atan(0.5)) + self.assertAlmostEqual(ub, atan(0.75)) + + def test_sqrt_bounds(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(sqrt(m.y)) + self.assertAlmostEqual(lb, sqrt(3)) + self.assertAlmostEqual(ub, sqrt(5)) + + def test_abs_bounds(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(abs(m.x)) + self.assertEqual(lb, 0) + self.assertEqual(ub, 4) + + def test_leaf_bounds_cached(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(m.x - m.y) + self.assertEqual(lb, -7) + self.assertEqual(ub, 1) + + self.assertIn(m.x, visitor.leaf_bounds) + self.assertEqual(visitor.leaf_bounds[m.x], m.x.bounds) + self.assertIn(m.y, visitor.leaf_bounds) + self.assertEqual(visitor.leaf_bounds[m.y], m.y.bounds) + + # This should exercise the code that uses the cache. + lb, ub = visitor.walk_expression(m.x**2 + 3) + self.assertEqual(lb, 3) + self.assertEqual(ub, 19) + + def test_var_fixed_to_None(self): + m = self.make_model() + m.x.fix(None) + + visitor = ExpressionBoundsVisitor(use_fixed_var_values_as_bounds=True) + with self.assertRaisesRegex( + ValueError, + "Var 'x' is fixed to None. This value cannot be " + "used to calculate bounds.", + ): + lb, ub = visitor.walk_expression(m.x - m.y) + + def test_var_with_no_lb(self): + m = self.make_model() + m.x.setlb(None) + + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(m.x - m.y) + self.assertEqual(lb, -float('inf')) + self.assertEqual(ub, 1) + + def test_var_with_no_ub(self): + m = self.make_model() + m.y.setub(None) + + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(m.x - m.y) + self.assertEqual(lb, -float('inf')) + self.assertEqual(ub, 1) + + def test_param(self): + m = self.make_model() + m.p = Param(initialize=6) + + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(m.p**m.y) + self.assertEqual(lb, 6**3) + self.assertEqual(ub, 6**5) + + def test_mutable_param(self): + m = self.make_model() + m.p = Param(initialize=6, mutable=True) + + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(m.p**m.y) + self.assertEqual(lb, 6**3) + self.assertEqual(ub, 6**5) + + def test_named_expression(self): + m = self.make_model() + m.e = Expression(expr=sqrt(m.x**2 + m.y**2)) + visitor = ExpressionBoundsVisitor() + + lb, ub = visitor.walk_expression(m.e + 4) + self.assertEqual(lb, 7) + self.assertAlmostEqual(ub, sqrt(41) + 4) + + self.assertIn(m.e, visitor.leaf_bounds) + self.assertEqual(visitor.leaf_bounds[m.e][0], 3) + self.assertAlmostEqual(visitor.leaf_bounds[m.e][1], sqrt(41)) + + # exercise the using of the cached bounds + lb, ub = visitor.walk_expression(m.e) + self.assertEqual(lb, 3) + self.assertAlmostEqual(ub, sqrt(41)) + + def test_npv_expression(self): + m = self.make_model() + m.p = Param(initialize=4, mutable=True) + visitor = ExpressionBoundsVisitor() + lb, ub = visitor.walk_expression(1 / m.p) + self.assertEqual(lb, 0.25) + self.assertEqual(ub, 0.25) + + def test_invalid_numeric_type(self): + m = self.make_model() + m.p = Param(initialize=True, mutable=True, domain=Any) + visitor = ExpressionBoundsVisitor() + with self.assertRaisesRegex( + ValueError, + r"True \(bool\) is not a valid numeric type. " + r"Cannot compute bounds on expression.", + ): + lb, ub = visitor.walk_expression(m.p + m.y) + + m.p.set_value(None) + with self.assertRaisesRegex( + ValueError, + r"None \(NoneType\) is not a valid numeric type. " + r"Cannot compute bounds on expression.", + ): + lb, ub = visitor.walk_expression(m.p + m.y) + + def test_invalid_string(self): + m = self.make_model() + m.p = Param(initialize='True', domain=Any) + visitor = ExpressionBoundsVisitor() + with self.assertRaisesRegex( + ValueError, + r"'True' \(str\) is not a valid numeric type. " + r"Cannot compute bounds on expression.", + ): + lb, ub = visitor.walk_expression(m.p + m.y) + + def test_invalid_complex(self): + m = self.make_model() + m.p = Param(initialize=complex(4, 5), domain=Any) + visitor = ExpressionBoundsVisitor() + with self.assertRaisesRegex( + ValueError, + r"Cannot compute bounds on expressions containing " + r"complex numbers. Encountered when processing \(4\+5j\)", + ): + lb, ub = visitor.walk_expression(m.p + m.y) + + def test_inequality(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + self.assertEqual(visitor.walk_expression(m.z <= m.y), (_true, _true)) + self.assertEqual(visitor.walk_expression(m.y <= m.z), (_false, _false)) + self.assertEqual(visitor.walk_expression(m.y <= m.x), (_false, _true)) + + def test_equality(self): + m = self.make_model() + m.p = Param(initialize=5) + visitor = ExpressionBoundsVisitor() + self.assertEqual(visitor.walk_expression(m.y == m.z), (_false, _false)) + self.assertEqual(visitor.walk_expression(m.y == m.x), (_false, _true)) + self.assertEqual(visitor.walk_expression(m.p == m.p), (_true, _true)) + + def test_ranged(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + self.assertEqual( + visitor.walk_expression(inequality(m.z, m.y, 5)), (_true, _true) + ) + self.assertEqual( + visitor.walk_expression(inequality(m.y, m.z, m.y)), (_false, _false) + ) + self.assertEqual( + visitor.walk_expression(inequality(m.y, m.x, m.y)), (_false, _true) + ) + + def test_expr_if(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + self.assertEqual( + visitor.walk_expression(Expr_if(IF=m.z <= m.y, THEN=m.z, ELSE=m.y)), + m.z.bounds, + ) + self.assertEqual( + visitor.walk_expression(Expr_if(IF=m.z >= m.y, THEN=m.z, ELSE=m.y)), + m.y.bounds, + ) + self.assertEqual( + visitor.walk_expression(Expr_if(IF=m.y <= m.x, THEN=m.y, ELSE=m.x)), (-2, 5) + ) + + def test_unknown_classes(self): + class UnknownNumeric(NumericExpression): + pass + + class UnknownLogic(BooleanExpression): + def nargs(self): + return 0 + + class UnknownOther(ExpressionBase): + @property + def args(self): + return () + + def nargs(self): + return 0 + + visitor = ExpressionBoundsVisitor() + with LoggingIntercept() as LOG: + self.assertEqual(visitor.walk_expression(UnknownNumeric(())), (-inf, inf)) + self.assertEqual( + LOG.getvalue(), + "Unexpected expression node type 'UnknownNumeric' found while walking " + "expression tree; returning (-inf, inf) for the expression bounds.\n", + ) + with LoggingIntercept() as LOG: + self.assertEqual(visitor.walk_expression(UnknownLogic(())), (_false, _true)) + self.assertEqual( + LOG.getvalue(), + "Unexpected expression node type 'UnknownLogic' found while walking " + "expression tree; returning (False, True) for the expression bounds.\n", + ) + with self.assertRaisesRegex( + DeveloperError, "Unexpected expression node type 'UnknownOther' found" + ): + visitor.walk_expression(UnknownOther()) diff --git a/pyomo/contrib/fbbt/tests/test_fbbt.py b/pyomo/contrib/fbbt/tests/test_fbbt.py index 5e8d656eeab..f7d08d11215 100644 --- a/pyomo/contrib/fbbt/tests/test_fbbt.py +++ b/pyomo/contrib/fbbt/tests/test_fbbt.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/fbbt/tests/test_interval.py b/pyomo/contrib/fbbt/tests/test_interval.py index 59c62be4e84..1e42162a35e 100644 --- a/pyomo/contrib/fbbt/tests/test_interval.py +++ b/pyomo/contrib/fbbt/tests/test_interval.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import math import pyomo.common.unittest as unittest from pyomo.common.dependencies import numpy as np, numpy_available diff --git a/pyomo/contrib/fme/__init__.py b/pyomo/contrib/fme/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/fme/__init__.py +++ b/pyomo/contrib/fme/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/fme/fourier_motzkin_elimination.py b/pyomo/contrib/fme/fourier_motzkin_elimination.py index 18aa157545e..4636450c58e 100644 --- a/pyomo/contrib/fme/fourier_motzkin_elimination.py +++ b/pyomo/contrib/fme/fourier_motzkin_elimination.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -23,7 +23,7 @@ value, ConstraintList, ) -from pyomo.core.base import TransformationFactory, _VarData +from pyomo.core.base import TransformationFactory, VarData from pyomo.core.plugins.transform.hierarchy import Transformation from pyomo.common.config import ConfigBlock, ConfigValue, NonNegativeFloat from pyomo.common.modeling import unique_component_name @@ -58,7 +58,7 @@ def _check_var_bounds_filter(constraint): def vars_to_eliminate_list(x): - if isinstance(x, (Var, _VarData)): + if isinstance(x, (Var, VarData)): if not x.is_indexed(): return ComponentSet([x]) ans = ComponentSet() diff --git a/pyomo/contrib/fme/plugins.py b/pyomo/contrib/fme/plugins.py index 324dd583d0f..b8278ccbb27 100644 --- a/pyomo/contrib/fme/plugins.py +++ b/pyomo/contrib/fme/plugins.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/fme/tests/__init__.py b/pyomo/contrib/fme/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/fme/tests/__init__.py +++ b/pyomo/contrib/fme/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py b/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py index 11c008acf82..dc721488f74 100644 --- a/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py +++ b/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -435,7 +435,7 @@ def check_hull_projected_constraints(self, m, constraints, indices): self.assertIs(body.linear_vars[2], m.startup.binary_indicator_var) self.assertEqual(body.linear_coefs[2], 2) - # 1 <= time1_disjuncts[0].ind_var + time_1.disjuncts[1].ind_var + # 1 <= time1_disjuncts[0].ind_var + time1_disjuncts[1].ind_var cons = constraints[indices[7]] self.assertEqual(cons.lower, 1) self.assertIsNone(cons.upper) @@ -548,12 +548,12 @@ def test_project_disaggregated_vars(self): # we of course get tremendous amounts of garbage, but we make sure that # what should be here is: self.check_hull_projected_constraints( - m, constraints, [23, 19, 8, 10, 54, 67, 35, 3, 4, 1, 2] + m, constraints, [16, 12, 69, 71, 47, 60, 28, 1, 2, 3, 4] ) # and when we filter, it's still there. constraints = filtered._pyomo_contrib_fme_transformation.projected_constraints self.check_hull_projected_constraints( - filtered, constraints, [10, 8, 5, 6, 15, 19, 11, 3, 4, 1, 2] + filtered, constraints, [8, 6, 20, 21, 13, 17, 9, 1, 2, 3, 4] ) @unittest.skipIf(not 'glpk' in solvers, 'glpk not available') @@ -570,7 +570,7 @@ def test_post_processing(self): # They should be the same as the above, but now these are *all* the # constraints self.check_hull_projected_constraints( - m, constraints, [10, 8, 5, 6, 15, 19, 11, 3, 4, 1, 2] + m, constraints, [8, 6, 20, 21, 13, 17, 9, 1, 2, 3, 4] ) # and check that we didn't change the model diff --git a/pyomo/contrib/gdp_bounds/__init__.py b/pyomo/contrib/gdp_bounds/__init__.py index 3a02f9e5f8e..ac71890cf7c 100644 --- a/pyomo/contrib/gdp_bounds/__init__.py +++ b/pyomo/contrib/gdp_bounds/__init__.py @@ -1 +1,12 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.contrib.gdp_bounds.plugins diff --git a/pyomo/contrib/gdp_bounds/compute_bounds.py b/pyomo/contrib/gdp_bounds/compute_bounds.py index f4f046e79df..3c04e4e1af7 100644 --- a/pyomo/contrib/gdp_bounds/compute_bounds.py +++ b/pyomo/contrib/gdp_bounds/compute_bounds.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdp_bounds/info.py b/pyomo/contrib/gdp_bounds/info.py index bad76e0f2f7..e65df2bfab0 100644 --- a/pyomo/contrib/gdp_bounds/info.py +++ b/pyomo/contrib/gdp_bounds/info.py @@ -1,4 +1,16 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Provides functions for retrieving disjunctive variable bound information stored on a model.""" + from pyomo.common.collections import ComponentMap from pyomo.core import value @@ -23,10 +35,10 @@ def disjunctive_bound(var, scope): """Compute the disjunctive bounds for a variable in a given scope. Args: - var (_VarData): Variable for which to compute bound + var (VarData): Variable for which to compute bound scope (Component): The scope in which to compute the bound. If not a - _DisjunctData, it will walk up the tree and use the scope of the - most immediate enclosing _DisjunctData. + DisjunctData, it will walk up the tree and use the scope of the + most immediate enclosing DisjunctData. Returns: numeric: the tighter of either the disjunctive lower bound, the diff --git a/pyomo/contrib/gdp_bounds/plugins.py b/pyomo/contrib/gdp_bounds/plugins.py index 1ebe44378f0..016a1fc7b13 100644 --- a/pyomo/contrib/gdp_bounds/plugins.py +++ b/pyomo/contrib/gdp_bounds/plugins.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdp_bounds/tests/__init__.py b/pyomo/contrib/gdp_bounds/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/gdp_bounds/tests/__init__.py +++ b/pyomo/contrib/gdp_bounds/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/gdp_bounds/tests/test_gdp_bounds.py b/pyomo/contrib/gdp_bounds/tests/test_gdp_bounds.py index a5f7780f043..0c8eae2c43b 100644 --- a/pyomo/contrib/gdp_bounds/tests/test_gdp_bounds.py +++ b/pyomo/contrib/gdp_bounds/tests/test_gdp_bounds.py @@ -1,4 +1,16 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Tests explicit bound to variable bound transformation module.""" + import pyomo.common.unittest as unittest from pyomo.contrib.gdp_bounds.info import disjunctive_lb, disjunctive_ub from pyomo.environ import ( diff --git a/pyomo/contrib/gdpopt/GDPopt.py b/pyomo/contrib/gdpopt/GDPopt.py index 3d45fa504cb..f0ff6d690d6 100644 --- a/pyomo/contrib/gdpopt/GDPopt.py +++ b/pyomo/contrib/gdpopt/GDPopt.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/__init__.py b/pyomo/contrib/gdpopt/__init__.py index 307fbc1594c..a84b8385ad3 100644 --- a/pyomo/contrib/gdpopt/__init__.py +++ b/pyomo/contrib/gdpopt/__init__.py @@ -1 +1,12 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + __version__ = (22, 5, 13) # Note: date-based version number diff --git a/pyomo/contrib/gdpopt/algorithm_base_class.py b/pyomo/contrib/gdpopt/algorithm_base_class.py index 5bf41148700..c5929ad4a88 100644 --- a/pyomo/contrib/gdpopt/algorithm_base_class.py +++ b/pyomo/contrib/gdpopt/algorithm_base_class.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/branch_and_bound.py b/pyomo/contrib/gdpopt/branch_and_bound.py index f69a92efe16..36b81c881be 100644 --- a/pyomo/contrib/gdpopt/branch_and_bound.py +++ b/pyomo/contrib/gdpopt/branch_and_bound.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -179,13 +179,13 @@ def _solve_gdp(self, model, config): # TODO might be worthwhile to log number of nonlinear # constraints in each disjunction for later branching # purposes - root_util_blk.disjunct_to_nonlinear_constraints[ - disjunct - ] = nonlinear_constraints_in_disjunct + root_util_blk.disjunct_to_nonlinear_constraints[disjunct] = ( + nonlinear_constraints_in_disjunct + ) - root_util_blk.disjunction_to_unfixed_disjuncts[ - disjunction - ] = unfixed_disjuncts + root_util_blk.disjunction_to_unfixed_disjuncts[disjunction] = ( + unfixed_disjuncts + ) pass # Add the BigM suffix if it does not already exist. Used later during @@ -230,12 +230,12 @@ def _solve_gdp(self, model, config): no_feasible_soln = float('inf') self.LB = ( node_data.obj_lb - if solve_data.objective_sense == minimize + if self.objective_sense == minimize else -no_feasible_soln ) self.UB = ( no_feasible_soln - if solve_data.objective_sense == minimize + if self.objective_sense == minimize else -node_data.obj_lb ) config.logger.info( diff --git a/pyomo/contrib/gdpopt/config_options.py b/pyomo/contrib/gdpopt/config_options.py index 386826b844c..467c4a6ec32 100644 --- a/pyomo/contrib/gdpopt/config_options.py +++ b/pyomo/contrib/gdpopt/config_options.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/create_oa_subproblems.py b/pyomo/contrib/gdpopt/create_oa_subproblems.py index 12266866dbc..690fe1f15f1 100644 --- a/pyomo/contrib/gdpopt/create_oa_subproblems.py +++ b/pyomo/contrib/gdpopt/create_oa_subproblems.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/cut_generation.py b/pyomo/contrib/gdpopt/cut_generation.py index 36a826a4f83..742a2cde395 100644 --- a/pyomo/contrib/gdpopt/cut_generation.py +++ b/pyomo/contrib/gdpopt/cut_generation.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/discrete_problem_initialize.py b/pyomo/contrib/gdpopt/discrete_problem_initialize.py index 3dc18132c5b..81c339b94a2 100644 --- a/pyomo/contrib/gdpopt/discrete_problem_initialize.py +++ b/pyomo/contrib/gdpopt/discrete_problem_initialize.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/enumerate.py b/pyomo/contrib/gdpopt/enumerate.py index 45ecc8864f9..6c25d0088f4 100644 --- a/pyomo/contrib/gdpopt/enumerate.py +++ b/pyomo/contrib/gdpopt/enumerate.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/gloa.py b/pyomo/contrib/gdpopt/gloa.py index ba8ed2fe234..212da057e05 100644 --- a/pyomo/contrib/gdpopt/gloa.py +++ b/pyomo/contrib/gdpopt/gloa.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -89,10 +89,9 @@ def _solve_gdp(self, original_model, config): # constraints will be added by the transformation to a MIP, so these are # all we'll ever need. add_global_constraint_list(self.original_util_block) - ( - discrete_problem_util_block, - subproblem_util_block, - ) = _get_discrete_problem_and_subproblem(self, config) + (discrete_problem_util_block, subproblem_util_block) = ( + _get_discrete_problem_and_subproblem(self, config) + ) discrete = discrete_problem_util_block.parent_block() subproblem = subproblem_util_block.parent_block() discrete_obj = next( diff --git a/pyomo/contrib/gdpopt/loa.py b/pyomo/contrib/gdpopt/loa.py index 6a9889065bf..354b61ae940 100644 --- a/pyomo/contrib/gdpopt/loa.py +++ b/pyomo/contrib/gdpopt/loa.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -99,10 +99,9 @@ def _solve_gdp(self, original_model, config): # We'll need these to get dual info after solving subproblems add_constraint_list(self.original_util_block) - ( - discrete_problem_util_block, - subproblem_util_block, - ) = _get_discrete_problem_and_subproblem(self, config) + (discrete_problem_util_block, subproblem_util_block) = ( + _get_discrete_problem_and_subproblem(self, config) + ) discrete = discrete_problem_util_block.parent_block() subproblem = subproblem_util_block.parent_block() diff --git a/pyomo/contrib/gdpopt/nlp_initialization.py b/pyomo/contrib/gdpopt/nlp_initialization.py index fc083c095da..dbc33eb20be 100644 --- a/pyomo/contrib/gdpopt/nlp_initialization.py +++ b/pyomo/contrib/gdpopt/nlp_initialization.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/oa_algorithm_utils.py b/pyomo/contrib/gdpopt/oa_algorithm_utils.py index 9aba59e4527..ce4012d8800 100644 --- a/pyomo/contrib/gdpopt/oa_algorithm_utils.py +++ b/pyomo/contrib/gdpopt/oa_algorithm_utils.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/plugins.py b/pyomo/contrib/gdpopt/plugins.py index 3ebad88a626..d0068d25993 100644 --- a/pyomo/contrib/gdpopt/plugins.py +++ b/pyomo/contrib/gdpopt/plugins.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -16,3 +16,4 @@ def load(): import pyomo.contrib.gdpopt.branch_and_bound import pyomo.contrib.gdpopt.loa import pyomo.contrib.gdpopt.ric + import pyomo.contrib.gdpopt.enumerate diff --git a/pyomo/contrib/gdpopt/ric.py b/pyomo/contrib/gdpopt/ric.py index f3eb83b79a9..2aa1aaf8c67 100644 --- a/pyomo/contrib/gdpopt/ric.py +++ b/pyomo/contrib/gdpopt/ric.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -62,10 +62,9 @@ def solve(self, model, **kwds): def _solve_gdp(self, original_model, config): logger = config.logger - ( - discrete_problem_util_block, - subproblem_util_block, - ) = _get_discrete_problem_and_subproblem(self, config) + (discrete_problem_util_block, subproblem_util_block) = ( + _get_discrete_problem_and_subproblem(self, config) + ) discrete_problem = discrete_problem_util_block.parent_block() subproblem = subproblem_util_block.parent_block() discrete_problem_obj = next( diff --git a/pyomo/contrib/gdpopt/solve_discrete_problem.py b/pyomo/contrib/gdpopt/solve_discrete_problem.py index 3de66fbaca0..54218edc50a 100644 --- a/pyomo/contrib/gdpopt/solve_discrete_problem.py +++ b/pyomo/contrib/gdpopt/solve_discrete_problem.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/solve_subproblem.py b/pyomo/contrib/gdpopt/solve_subproblem.py index bd9b85c0cef..e3980c3c784 100644 --- a/pyomo/contrib/gdpopt/solve_subproblem.py +++ b/pyomo/contrib/gdpopt/solve_subproblem.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/tests/__init__.py b/pyomo/contrib/gdpopt/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/gdpopt/tests/__init__.py +++ b/pyomo/contrib/gdpopt/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/gdpopt/tests/common_tests.py b/pyomo/contrib/gdpopt/tests/common_tests.py index 5a363430381..88a2642704a 100644 --- a/pyomo/contrib/gdpopt/tests/common_tests.py +++ b/pyomo/contrib/gdpopt/tests/common_tests.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/tests/test_LBB.py b/pyomo/contrib/gdpopt/tests/test_LBB.py index 7d25767020e..8a553398fa6 100644 --- a/pyomo/contrib/gdpopt/tests/test_LBB.py +++ b/pyomo/contrib/gdpopt/tests/test_LBB.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -59,6 +59,7 @@ def test_infeasible_GDP(self): self.assertIsNone(m.d.disjuncts[0].indicator_var.value) self.assertIsNone(m.d.disjuncts[1].indicator_var.value) + @unittest.skipUnless(z3_available, "Z3 SAT solver is not available") def test_infeasible_GDP_check_sat(self): """Test for infeasible GDP with check_sat option True.""" m = ConcreteModel() diff --git a/pyomo/contrib/gdpopt/tests/test_enumerate.py b/pyomo/contrib/gdpopt/tests/test_enumerate.py index 606dd172064..8798557ddc9 100644 --- a/pyomo/contrib/gdpopt/tests/test_enumerate.py +++ b/pyomo/contrib/gdpopt/tests/test_enumerate.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/tests/test_gdpopt.py b/pyomo/contrib/gdpopt/tests/test_gdpopt.py index 1d5559a9b33..873bafabc76 100644 --- a/pyomo/contrib/gdpopt/tests/test_gdpopt.py +++ b/pyomo/contrib/gdpopt/tests/test_gdpopt.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -22,7 +22,6 @@ from pyomo.common.collections import Bunch from pyomo.common.config import ConfigDict, ConfigValue from pyomo.common.fileutils import import_file, PYOMO_ROOT_DIR -from pyomo.contrib.appsi.solvers.gurobi import Gurobi from pyomo.contrib.gdpopt.create_oa_subproblems import ( add_util_block, add_disjunct_list, @@ -767,6 +766,9 @@ def test_time_limit(self): results.solver.termination_condition, TerminationCondition.maxTimeLimit ) + @unittest.skipUnless( + license_available, "No BARON license--8PP logical problem exceeds demo size" + ) def test_LOA_8PP_logical_default_init(self): """Test logic-based outer approximation with 8PP.""" exfile = import_file(join(exdir, 'eight_process', 'eight_proc_logical.py')) @@ -870,6 +872,9 @@ def test_LOA_8PP_maxBinary(self): ) ct.check_8PP_solution(self, eight_process, results) + @unittest.skipUnless( + license_available, "No BARON license--8PP logical problem exceeds demo size" + ) def test_LOA_8PP_logical_maxBinary(self): """Test logic-based OA with max_binary initialization.""" exfile = import_file(join(exdir, 'eight_process', 'eight_proc_logical.py')) @@ -1050,7 +1055,11 @@ def assert_correct_disjuncts_active( self.assertTrue(fabs(value(eight_process.profit.expr) - 68) <= 1e-2) - @unittest.skipUnless(Gurobi().available(), "APPSI Gurobi solver is not available") + @unittest.skipUnless( + SolverFactory('appsi_gurobi').available(exception_flag=False) + and SolverFactory('appsi_gurobi').license_is_valid(), + "Legacy APPSI Gurobi solver is not available", + ) def test_auto_persistent_solver(self): exfile = import_file(join(exdir, 'eight_process', 'eight_proc_model.py')) m = exfile.build_eight_process_flowsheet() @@ -1126,6 +1135,9 @@ def test_RIC_8PP_default_init(self): ) ct.check_8PP_solution(self, eight_process, results) + @unittest.skipUnless( + license_available, "No BARON license--8PP logical problem exceeds demo size" + ) def test_RIC_8PP_logical_default_init(self): """Test logic-based outer approximation with 8PP.""" exfile = import_file(join(exdir, 'eight_process', 'eight_proc_logical.py')) diff --git a/pyomo/contrib/gdpopt/util.py b/pyomo/contrib/gdpopt/util.py index f288f9e2647..babe0245d57 100644 --- a/pyomo/contrib/gdpopt/util.py +++ b/pyomo/contrib/gdpopt/util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -553,6 +553,13 @@ def _add_bigm_constraint_to_transformed_model(m, constraint, block): # making a Reference to the ComponentData so that it will look like an # indexed component for now. If I redesign bigm at some point, then this # could be prettier. - bigm._transform_constraint(Reference(constraint), parent_disjunct, None, [], []) + bigm._transform_constraint( + Reference(constraint), + parent_disjunct, + None, + [], + [], + 1 - parent_disjunct.binary_indicator_var, + ) # Now get rid of it because this is a class attribute! del bigm._config diff --git a/pyomo/contrib/gjh/GJH.py b/pyomo/contrib/gjh/GJH.py index df9dfebf477..dc7c8de89c1 100644 --- a/pyomo/contrib/gjh/GJH.py +++ b/pyomo/contrib/gjh/GJH.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gjh/__init__.py b/pyomo/contrib/gjh/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/gjh/__init__.py +++ b/pyomo/contrib/gjh/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gjh/getGJH.py b/pyomo/contrib/gjh/getGJH.py index 112de054745..2d503c71438 100644 --- a/pyomo/contrib/gjh/getGJH.py +++ b/pyomo/contrib/gjh/getGJH.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gjh/plugins.py b/pyomo/contrib/gjh/plugins.py index 4af2f38becd..f072f7b2c38 100644 --- a/pyomo/contrib/gjh/plugins.py +++ b/pyomo/contrib/gjh/plugins.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/iis/__init__.py b/pyomo/contrib/iis/__init__.py index eb9f60b8928..961ac576d42 100644 --- a/pyomo/contrib/iis/__init__.py +++ b/pyomo/contrib/iis/__init__.py @@ -1 +1,13 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.contrib.iis.iis import write_iis +from pyomo.contrib.iis.mis import compute_infeasibility_explanation diff --git a/pyomo/contrib/iis/iis.py b/pyomo/contrib/iis/iis.py index bd192d04eb3..1ffd6cb0bd3 100644 --- a/pyomo/contrib/iis/iis.py +++ b/pyomo/contrib/iis/iis.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ This module contains functions for computing an irreducible infeasible set for a Pyomo MILP or LP using a specified commercial solver, one of CPLEX, diff --git a/pyomo/contrib/iis/mis.py b/pyomo/contrib/iis/mis.py new file mode 100644 index 00000000000..6b6cca8e29c --- /dev/null +++ b/pyomo/contrib/iis/mis.py @@ -0,0 +1,377 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ +""" +WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, National Renewable Energy Laboratory, and National Energy Technology Laboratory (subject to receipt of any required approvals from the U.S. Dept. of Energy). All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + + Neither the name of the University of California, Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, National Renewable Energy Laboratory, National Energy Technology Laboratory, U.S. Dept. of Energy nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +You are under no obligation whatsoever to provide any bug fixes, patches, or upgrades to the features, functionality or performance of the source code ("Enhancements") to anyone; however, if you choose to make your Enhancements available either publicly, or directly to Lawrence Berkeley National Laboratory, without imposing a separate written license agreement for such Enhancements, then you hereby grant the following license: a non-exclusive, royalty-free perpetual license to install, use, modify, prepare derivative works, incorporate into other computer software, distribute, and sublicense such enhancements or derivative works thereof, in binary and source code form. +""" +""" +Minimal Intractable System (MIS) finder +Originally written by Ben Knueven as part of the WaterTAP project: + https://github.com/watertap-org/watertap +That's why this file has the watertap copyright notice. + +copied by DLW 18Feb2024 and edited + +See: https://www.sce.carleton.ca/faculty/chinneck/docs/CPAIOR07InfeasibilityTutorial.pdf +""" + +import logging +import pyomo.environ as pyo + +from pyomo.core.plugins.transform.add_slack_vars import AddSlackVariables + +from pyomo.core.plugins.transform.hierarchy import IsomorphicTransformation + +from pyomo.common.modeling import unique_component_name +from pyomo.common.collections import ComponentMap, ComponentSet + +from pyomo.opt import WriterFactory + +logger = logging.getLogger("pyomo.contrib.iis") +logger.setLevel(logging.INFO) + + +class _VariableBoundsAsConstraints(IsomorphicTransformation): + """Replace all variables bounds and domain information with constraints. + + Leaves fixed Vars untouched (for now) + """ + + def _apply_to(self, instance, **kwds): + + bound_constr_block_name = unique_component_name(instance, "_variable_bounds") + instance.add_component(bound_constr_block_name, pyo.Block()) + bound_constr_block = instance.component(bound_constr_block_name) + + for v in instance.component_data_objects(pyo.Var, descend_into=True): + if v.fixed: + continue + lb, ub = v.bounds + if lb is None and ub is None: + continue + var_name = v.getname(fully_qualified=True) + if lb is not None: + con_name = "lb_for_" + var_name + con = pyo.Constraint(expr=(lb, v, None)) + bound_constr_block.add_component(con_name, con) + if ub is not None: + con_name = "ub_for_" + var_name + con = pyo.Constraint(expr=(None, v, ub)) + bound_constr_block.add_component(con_name, con) + + # now we deactivate the variable bounds / domain + v.domain = pyo.Reals + v.setlb(None) + v.setub(None) + + +def compute_infeasibility_explanation( + model, solver, tee=False, tolerance=1e-8, logger=logger +): + """ + This function attempts to determine why a given model is infeasible. It deploys + two main algorithms: + + 1. Successfully relaxes the constraints of the problem, and reports to the user + some sets of constraints and variable bounds, which when relaxed, creates a + feasible model. + 2. Uses the information collected from (1) to attempt to compute a Minimal + Infeasible System (MIS), which is a set of constraints and variable bounds + which appear to be in conflict with each other. It is minimal in the sense + that removing any single constraint or variable bound would result in a + feasible subsystem. + + Args + ---- + model: A pyomo block + solver: A pyomo solver object or a string for SolverFactory + tee (optional): Display intermediate solves conducted (False) + tolerance (optional): The feasibility tolerance to use when declaring a + constraint feasible (1e-08) + logger:logging.Logger + A logger for messages. Uses pyomo.contrib.mis logger by default. + + """ + # Suggested enhancement: It might be useful to return sets of names for each set of relaxed components, as well as the final minimal infeasible system + + # hold the original harmless + modified_model = model.clone() + + if solver is None: + raise ValueError("A solver must be supplied") + elif isinstance(solver, str): + solver = pyo.SolverFactory(solver) + else: + # assume we have a solver + assert solver.available() + + # first, cache the values we get + _value_cache = ComponentMap() + for v in model.component_data_objects(pyo.Var, descend_into=True): + _value_cache[v] = v.value + + # finding proper reference + if model.parent_block() is None: + common_name = "" + else: + common_name = model.name + "." + + _modified_model_var_to_original_model_var = ComponentMap() + _modified_model_value_cache = ComponentMap() + + for v in model.component_data_objects(pyo.Var, descend_into=True): + modified_model_var = modified_model.find_component(v.name[len(common_name) :]) + + _modified_model_var_to_original_model_var[modified_model_var] = v + _modified_model_value_cache[modified_model_var] = _value_cache[v] + modified_model_var.set_value(_value_cache[v], skip_validation=True) + + # TODO: For WT / IDAES models, we should probably be more + # selective in *what* we elasticize. E.g., it probably + # does not make sense to elasticize property calculations + # and maybe certain other equality constraints calculating + # values. Maybe we shouldn't elasticize *any* equality + # constraints. + # For example, elasticizing the calculation of mass fraction + # makes absolutely no sense and will just be noise for the + # modeler to sift through. We could try to sort the constraints + # such that we look for those with linear coefficients `1` on + # some term and leave those be. + # Alternatively, we could apply this tool to a version of the + # model that has as many as possible of these constraints + # "substituted out". + # move the variable bounds to the constraints + _VariableBoundsAsConstraints().apply_to(modified_model) + + AddSlackVariables().apply_to(modified_model) + slack_block = modified_model._core_add_slack_variables + + for v in slack_block.component_data_objects(pyo.Var): + v.fix(0) + # start with variable bounds -- these are the easiest to interpret + for c in modified_model._variable_bounds.component_data_objects( + pyo.Constraint, descend_into=True + ): + plus = slack_block.component(f"_slack_plus_{c.name}") + minus = slack_block.component(f"_slack_minus_{c.name}") + assert not (plus is None and minus is None) + if plus is not None: + plus.unfix() + if minus is not None: + minus.unfix() + + # TODO: Elasticizing too much at once seems to cause Ipopt trouble. + # After an initial sweep, we should just fix one elastic variable + # and put everything else on a stack of "constraints to elasticize". + # We elasticize one constraint at a time and fix one constraint at a time. + # After fixing an elastic variable, we elasticize a single constraint it + # appears in and put the remaining constraints on the stack. If the resulting problem + # is feasible, we keep going "down the tree". If the resulting problem is + # infeasible or cannot be solved, we elasticize a single constraint from + # the top of the stack. + # The algorithm stops when the stack is empty and the subproblem is infeasible. + # Along the way, any time the current problem is infeasible we can check to + # see if the current set of constraints in the filter is as a collection of + # infeasible constraints -- to terminate early. + # However, while more stable, this is much more computationally intensive. + # So, we leave the implementation simpler for now and consider this as + # a potential extension if this tool sometimes cannot report a good answer. + # Phase 1 -- build the initial set of constraints, or prove feasibility + msg = "" + fixed_slacks = ComponentSet() + elastic_filter = ComponentSet() + + def _constraint_loop(relaxed_things, msg): + if msg == "": + msg += f"Model {model.name} may be infeasible. A feasible solution was found with only the following {relaxed_things} relaxed:\n" + else: + msg += f"Another feasible solution was found with only the following {relaxed_things} relaxed:\n" + while True: + + def _constraint_generator(): + elastic_filter_size_initial = len(elastic_filter) + for v in slack_block.component_data_objects(pyo.Var): + if v.value > tolerance: + constr = _get_constraint(modified_model, v) + yield constr, v.value + v.fix(0) + fixed_slacks.add(v) + elastic_filter.add(constr) + if len(elastic_filter) == elastic_filter_size_initial: + raise Exception(f"Found model {model.name} to be feasible!") + + msg = _get_results_with_value(_constraint_generator(), msg) + for var, val in _modified_model_value_cache.items(): + var.set_value(val, skip_validation=True) + results = solver.solve(modified_model, tee=tee) + if pyo.check_optimal_termination(results): + msg += f"Another feasible solution was found with only the following {relaxed_things} relaxed:\n" + else: + break + return msg + + results = solver.solve(modified_model, tee=tee) + if pyo.check_optimal_termination(results): + msg = _constraint_loop("variable bounds", msg) + + # next, try relaxing the inequality constraints + for v in slack_block.component_data_objects(pyo.Var): + c = _get_constraint(modified_model, v) + if c.equality: + # equality constraint + continue + if v not in fixed_slacks: + v.unfix() + + results = solver.solve(modified_model, tee=tee) + if pyo.check_optimal_termination(results): + msg = _constraint_loop("inequality constraints and/or variable bounds", msg) + + for v in slack_block.component_data_objects(pyo.Var): + if v not in fixed_slacks: + v.unfix() + + results = solver.solve(modified_model, tee=tee) + if pyo.check_optimal_termination(results): + msg = _constraint_loop( + "inequality constraints, equality constraints, and/or variable bounds", msg + ) + + if len(elastic_filter) == 0: + # load the feasible solution into the original model + for modified_model_var, v in _modified_model_var_to_original_model_var.items(): + v.set_value(modified_model_var.value, skip_validation=True) + results = solver.solve(model, tee=tee) + if pyo.check_optimal_termination(results): + logger.info(f"A feasible solution was found!") + else: + logger.info( + f"Could not find a feasible solution with violated constraints or bounds. This model is likely unstable" + ) + + # Phase 2 -- deletion filter + # remove slacks by fixing them to 0 + for v in slack_block.component_data_objects(pyo.Var): + v.fix(0) + for o in modified_model.component_data_objects(pyo.Objective, descend_into=True): + o.deactivate() + + # mark all constraints not in the filter as inactive + for c in modified_model.component_data_objects(pyo.Constraint): + if c in elastic_filter: + continue + else: + c.deactivate() + + try: + results = solver.solve(modified_model, tee=tee) + except: + results = None + + if pyo.check_optimal_termination(results): + msg += "Could not determine Minimal Intractable System\n" + else: + deletion_filter = [] + guards = [] + for constr in elastic_filter: + constr.deactivate() + for var, val in _modified_model_value_cache.items(): + var.set_value(val, skip_validation=True) + math_failure = False + try: + results = solver.solve(modified_model, tee=tee) + except: + math_failure = True + + if math_failure: + constr.activate() + guards.append(constr) + elif pyo.check_optimal_termination(results): + constr.activate() + deletion_filter.append(constr) + else: # still infeasible without this constraint + pass + + msg += "Computed Minimal Intractable System (MIS)!\n" + msg += "Constraints / bounds in MIS:\n" + msg = _get_results(deletion_filter, msg) + msg += "Constraints / bounds in guards for stability:" + msg = _get_results(guards, msg) + + logger.info(msg) + + +def _get_results_with_value(constr_value_generator, msg=None): + # note that "lb_for_" and "ub_for_" are 7 characters long + if msg is None: + msg = "" + for c, value in constr_value_generator: + c_name = c.name + if "_variable_bounds" in c_name: + name = c.local_name + if "lb" in name: + msg += f"\tlb of var {name[7:]} by {value}\n" + elif "ub" in name: + msg += f"\tub of var {name[7:]} by {value}\n" + else: + raise RuntimeError("unrecognized var name") + else: + msg += f"\tconstraint: {c_name} by {value}\n" + return msg + + +def _get_results(constr_generator, msg=None): + # note that "lb_for_" and "ub_for_" are 7 characters long + if msg is None: + msg = "" + for c in constr_generator: + c_name = c.name + if "_variable_bounds" in c_name: + name = c.local_name + if "lb" in name: + msg += f"\tlb of var {name[7:]}\n" + elif "ub" in name: + msg += f"\tub of var {name[7:]}\n" + else: + raise RuntimeError("unrecognized var name") + else: + msg += f"\tconstraint: {c_name}\n" + return msg + + +def _get_constraint(modified_model, v): + if "_slack_plus_" in v.name: + constr = modified_model.find_component(v.local_name[len("_slack_plus_") :]) + if constr is None: + raise RuntimeError( + f"Bad constraint name {v.local_name[len('_slack_plus_'):]}" + ) + return constr + elif "_slack_minus_" in v.name: + constr = modified_model.find_component(v.local_name[len("_slack_minus_") :]) + if constr is None: + raise RuntimeError( + f"Bad constraint name {v.local_name[len('_slack_minus_'):]}" + ) + return constr + else: + raise RuntimeError(f"Bad var name {v.name}") diff --git a/pyomo/contrib/iis/tests/__init__.py b/pyomo/contrib/iis/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/iis/tests/__init__.py +++ b/pyomo/contrib/iis/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/iis/tests/test_iis.py b/pyomo/contrib/iis/tests/test_iis.py index b1b675d5081..cf7b5613a3a 100644 --- a/pyomo/contrib/iis/tests/test_iis.py +++ b/pyomo/contrib/iis/tests/test_iis.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.common.unittest as unittest import pyomo.environ as pyo from pyomo.contrib.iis import write_iis diff --git a/pyomo/contrib/iis/tests/test_mis.py b/pyomo/contrib/iis/tests/test_mis.py new file mode 100644 index 00000000000..bbdb2367016 --- /dev/null +++ b/pyomo/contrib/iis/tests/test_mis.py @@ -0,0 +1,125 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest +import pyomo.environ as pyo +import pyomo.contrib.iis.mis as mis +from pyomo.contrib.iis.mis import _get_constraint +from pyomo.common.tempfiles import TempfileManager + +import logging +import os + + +def _get_infeasible_model(): + m = pyo.ConcreteModel("trivial4test") + m.x = pyo.Var(within=pyo.Binary) + m.y = pyo.Var(within=pyo.NonNegativeReals) + + m.c1 = pyo.Constraint(expr=m.y <= 100.0 * m.x) + m.c2 = pyo.Constraint(expr=m.y <= -100.0 * m.x) + m.c3 = pyo.Constraint(expr=m.x >= 0.5) + + m.o = pyo.Objective(expr=-m.y) + + return m + + +def _get_feasible_model(): + m = pyo.ConcreteModel("Trivial Feasible Quad") + m.x = pyo.Var([1, 2], bounds=(0, 1)) + m.y = pyo.Var(bounds=(0, 1)) + m.c = pyo.Constraint(expr=m.x[1] * m.x[2] >= -1) + m.d = pyo.Constraint(expr=m.x[1] + m.y >= 1) + + return m + + +class TestMIS(unittest.TestCase): + @unittest.skipUnless( + pyo.SolverFactory("ipopt").available(exception_flag=False), + "ipopt not available", + ) + def test_write_mis_ipopt(self): + _test_mis("ipopt") + + def test__get_constraint_errors(self): + # A not-completely-cynical way to get the coverage up. + m = _get_infeasible_model() # not modified + fct = _get_constraint + + m.foo_slack_plus_ = pyo.Var() + self.assertRaises(RuntimeError, fct, m, m.foo_slack_plus_) + m.foo_slack_minus_ = pyo.Var() + self.assertRaises(RuntimeError, fct, m, m.foo_slack_minus_) + m.foo_bar = pyo.Var() + self.assertRaises(RuntimeError, fct, m, m.foo_bar) + + def test_feasible_model(self): + m = _get_feasible_model() + opt = pyo.SolverFactory("ipopt") + self.assertRaises(Exception, mis.compute_infeasibility_explanation, m, opt) + + +def _check_output(file_name): + # pretty simple check for now + with open(file_name, "r+") as file1: + lines = file1.readlines() + trigger = "Constraints / bounds in MIS:" + nugget = "lb of var y" + live = False # (long i) + found_nugget = False + for line in lines: + if trigger in line: + live = True + if live: + if nugget in line: + found_nugget = True + if not found_nugget: + raise RuntimeError(f"Did not find '{nugget}' after '{trigger}' in output") + else: + pass + + +def _test_mis(solver_name): + m = _get_infeasible_model() + opt = pyo.SolverFactory(solver_name) + + # This test seems to fail on Windows as it unlinks the tempfile, so live with it + # On a Windows machine, we will not use a temp dir and just try to delete the log file + if os.name == "nt": + file_name = f"_test_mis_{solver_name}.log" + logger = logging.getLogger(f"test_mis_{solver_name}") + logger.setLevel(logging.INFO) + fh = logging.FileHandler(file_name) + fh.setLevel(logging.DEBUG) + logger.addHandler(fh) + + mis.compute_infeasibility_explanation(m, opt, logger=logger) + _check_output(file_name) + # os.remove(file_name) cannot remove it on Windows. Still in use. + + else: # not windows + with TempfileManager.new_context() as tmpmgr: + tmp_path = tmpmgr.mkdtemp() + file_name = os.path.join(tmp_path, f"_test_mis_{solver_name}.log") + logger = logging.getLogger(f"test_mis_{solver_name}") + logger.setLevel(logging.INFO) + fh = logging.FileHandler(file_name) + fh.setLevel(logging.DEBUG) + logger.addHandler(fh) + + mis.compute_infeasibility_explanation(m, opt, logger=logger) + _check_output(file_name) + + +if __name__ == "__main__": + unittest.main() diff --git a/pyomo/contrib/iis/tests/trivial_mis.py b/pyomo/contrib/iis/tests/trivial_mis.py new file mode 100644 index 00000000000..4cf0dd7a357 --- /dev/null +++ b/pyomo/contrib/iis/tests/trivial_mis.py @@ -0,0 +1,24 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ +import pyomo.environ as pyo + +m = pyo.ConcreteModel("Trivial Quad") +m.x = pyo.Var([1, 2], bounds=(0, 1)) +m.y = pyo.Var(bounds=(0, 1)) +m.c = pyo.Constraint(expr=m.x[1] * m.x[2] == -1) +m.d = pyo.Constraint(expr=m.x[1] + m.y >= 1) + +from pyomo.contrib.iis.mis import compute_infeasibility_explanation + +# Note: this particular little problem is quadratic +# As of 18Feb2024 DLW is not sure the explanation code works with solvers other than ipopt +ipopt = pyo.SolverFactory("ipopt") +compute_infeasibility_explanation(m, solver=ipopt) diff --git a/pyomo/contrib/incidence_analysis/__init__.py b/pyomo/contrib/incidence_analysis/__init__.py index ee078690f2f..8942d09b6b9 100644 --- a/pyomo/contrib/incidence_analysis/__init__.py +++ b/pyomo/contrib/incidence_analysis/__init__.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from .triangularize import block_triangularize from .matching import maximum_matching from .interface import IncidenceGraphInterface, get_bipartite_incidence_graph diff --git a/pyomo/contrib/incidence_analysis/common/__init__.py b/pyomo/contrib/incidence_analysis/common/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/incidence_analysis/common/__init__.py +++ b/pyomo/contrib/incidence_analysis/common/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/incidence_analysis/common/dulmage_mendelsohn.py b/pyomo/contrib/incidence_analysis/common/dulmage_mendelsohn.py index 95b6cd7134f..5bc724fafc1 100644 --- a/pyomo/contrib/incidence_analysis/common/dulmage_mendelsohn.py +++ b/pyomo/contrib/incidence_analysis/common/dulmage_mendelsohn.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -90,7 +90,7 @@ def dulmage_mendelsohn(bg, top_nodes=None, matching=None): _filter.update(b_unmatched) _filter.update(b_matched_with_reachable) t_other = [t for t in top_nodes if t not in _filter] - b_other = [b for b in bot_nodes if b not in _filter] + b_other = [matching[t] for t in t_other] return ( (t_unmatched, t_reachable, t_matched_with_reachable, t_other), diff --git a/pyomo/contrib/incidence_analysis/common/tests/__init__.py b/pyomo/contrib/incidence_analysis/common/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/incidence_analysis/common/tests/__init__.py +++ b/pyomo/contrib/incidence_analysis/common/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/incidence_analysis/common/tests/test_dulmage_mendelsohn.py b/pyomo/contrib/incidence_analysis/common/tests/test_dulmage_mendelsohn.py index 1675fc7420a..b17ae9b1dfc 100644 --- a/pyomo/contrib/incidence_analysis/common/tests/test_dulmage_mendelsohn.py +++ b/pyomo/contrib/incidence_analysis/common/tests/test_dulmage_mendelsohn.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/incidence_analysis/config.py b/pyomo/contrib/incidence_analysis/config.py index 56841617cac..2a7734ba433 100644 --- a/pyomo/contrib/incidence_analysis/config.py +++ b/pyomo/contrib/incidence_analysis/config.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -13,6 +13,9 @@ import enum from pyomo.common.config import ConfigDict, ConfigValue, InEnum +from pyomo.common.modeling import NOTSET +from pyomo.repn.plugins.nl_writer import AMPLRepnVisitor, text_nl_template +from pyomo.repn.util import FileDeterminism, FileDeterminism_to_SortComponents class IncidenceMethod(enum.Enum): @@ -24,6 +27,21 @@ class IncidenceMethod(enum.Enum): standard_repn = 1 """Use ``pyomo.repn.standard_repn.generate_standard_repn``""" + standard_repn_compute_values = 2 + """Use ``pyomo.repn.standard_repn.generate_standard_repn`` with + ``compute_values=True`` + """ + + ampl_repn = 3 + """Use ``pyomo.repn.plugins.nl_writer.AMPLRepnVisitor``""" + + +class IncidenceOrder(enum.Enum): + + dulmage_mendelsohn_upper = 0 + + dulmage_mendelsohn_lower = 1 + _include_fixed = ConfigValue( default=False, @@ -39,11 +57,10 @@ class IncidenceMethod(enum.Enum): _linear_only = ConfigValue( default=False, domain=bool, - description="Identify variables that participate linearly", + description="Identify only variables that participate linearly", doc=( "Flag indicating whether only variables that participate linearly should" - " be included. Note that these are included even if they participate" - " nonlinearly as well." + " be included." ), ) @@ -55,16 +72,33 @@ class IncidenceMethod(enum.Enum): ) +def _amplrepnvisitor_validator(visitor): + if not isinstance(visitor, AMPLRepnVisitor): + raise TypeError( + "'visitor' config argument should be an instance of AMPLRepnVisitor" + ) + return visitor + + +_ampl_repn_visitor = ConfigValue( + default=None, + domain=_amplrepnvisitor_validator, + description="Visitor used to generate AMPLRepn of each constraint", +) + + IncidenceConfig = ConfigDict() """Options for incidence graph generation - ``include_fixed`` -- Flag indicating whether fixed variables should be included in the incidence graph - ``linear_only`` -- Flag indicating whether only variables that participate linearly - should be included. Note that these are included even if they participate - nonlinearly as well + should be included. - ``method`` -- Method used to identify incident variables. Must be a value of the ``IncidenceMethod`` enum. +- ``_ampl_repn_visitor`` -- Expression visitor used to generate ``AMPLRepn`` of each + constraint. Must be an instance of ``AMPLRepnVisitor``. *This option is constructed + automatically when needed and should not be set by users!* """ @@ -76,3 +110,46 @@ class IncidenceMethod(enum.Enum): IncidenceConfig.declare("method", _method) + + +IncidenceConfig.declare("_ampl_repn_visitor", _ampl_repn_visitor) + + +def get_config_from_kwds(**kwds): + """Get an instance of IncidenceConfig from provided keyword arguments. + + If the ``method`` argument is ``IncidenceMethod.ampl_repn`` and no + ``AMPLRepnVisitor`` has been provided, a new ``AMPLRepnVisitor`` is + constructed. This function should generally be used by callers such + as ``IncidenceGraphInterface`` to ensure that a visitor is created then + re-used when calling ``get_incident_variables`` in a loop. + + """ + if ( + kwds.get("method", None) is IncidenceMethod.ampl_repn + and kwds.get("_ampl_repn_visitor", None) is None + ): + subexpression_cache = {} + subexpression_order = [] + external_functions = {} + var_map = {} + used_named_expressions = set() + symbolic_solver_labels = False + # TODO: Explore potential performance benefit of exporting defined variables. + # This likely only shows up if we can preserve the subexpression cache across + # multiple constraint expressions. + export_defined_variables = False + sorter = FileDeterminism_to_SortComponents(FileDeterminism.ORDERED) + amplvisitor = AMPLRepnVisitor( + text_nl_template, + subexpression_cache, + subexpression_order, + external_functions, + var_map, + used_named_expressions, + symbolic_solver_labels, + export_defined_variables, + sorter, + ) + kwds["_ampl_repn_visitor"] = amplvisitor + return IncidenceConfig(kwds) diff --git a/pyomo/contrib/incidence_analysis/connected.py b/pyomo/contrib/incidence_analysis/connected.py index 2dcf31c0fe0..28d4bdee73f 100644 --- a/pyomo/contrib/incidence_analysis/connected.py +++ b/pyomo/contrib/incidence_analysis/connected.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/incidence_analysis/dulmage_mendelsohn.py b/pyomo/contrib/incidence_analysis/dulmage_mendelsohn.py index a3af0d1e6c9..3a6d06a809c 100644 --- a/pyomo/contrib/incidence_analysis/dulmage_mendelsohn.py +++ b/pyomo/contrib/incidence_analysis/dulmage_mendelsohn.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -70,6 +70,33 @@ def dulmage_mendelsohn(matrix_or_graph, top_nodes=None, matching=None): - **overconstrained** - The columns matched with *possibly* unmatched rows (unmatched and overconstrained rows) + While the Dulmage-Mendelsohn decomposition does not specify an order within + any of these subsets, the order returned by this function preserves the + maximum matching that is used to compute the decomposition. That is, zipping + "corresponding" row and column subsets yields pairs in this maximum matching. + For example: + + .. doctest:: + :hide: + :skipif: not (networkx_available and scipy_available) + + >>> # Hidden code block to make the following example runnable + >>> import scipy.sparse as sps + >>> from pyomo.contrib.incidence_analysis.dulmage_mendelsohn import dulmage_mendelsohn + >>> matrix = sps.identity(3) + + .. doctest:: + :skipif: not (networkx_available and scipy_available) + + >>> row_dmpartition, col_dmpartition = dulmage_mendelsohn(matrix) + >>> rdmp = row_dmpartition + >>> cdmp = col_dmpartition + >>> matching = list(zip( + ... rdmp.underconstrained + rdmp.square + rdmp.overconstrained, + ... cdmp.underconstrained + cdmp.square + cdmp.overconstrained, + ... )) + >>> # matching is a valid maximum matching of rows and columns of the matrix! + Parameters ---------- matrix_or_graph: ``scipy.sparse.coo_matrix`` or ``networkx.Graph`` @@ -133,7 +160,7 @@ def dulmage_mendelsohn(matrix_or_graph, top_nodes=None, matching=None): partition = ( row_partition, - tuple([n - M for n in subset] for subset in col_partition) + tuple([n - M for n in subset] for subset in col_partition), # Column nodes have values in [M, M+N-1]. Apply the offset # to get values corresponding to indices in user's matrix. ) diff --git a/pyomo/contrib/incidence_analysis/incidence.py b/pyomo/contrib/incidence_analysis/incidence.py index e68268094a6..96cbf77c47d 100644 --- a/pyomo/contrib/incidence_analysis/incidence.py +++ b/pyomo/contrib/incidence_analysis/incidence.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -17,7 +17,11 @@ from pyomo.core.expr.numvalue import value as pyo_value from pyomo.repn import generate_standard_repn from pyomo.util.subsystems import TemporarySubsystemManager -from pyomo.contrib.incidence_analysis.config import IncidenceMethod, IncidenceConfig +from pyomo.repn.plugins.nl_writer import AMPLRepn +from pyomo.contrib.incidence_analysis.config import ( + IncidenceMethod, + get_config_from_kwds, +) # @@ -29,7 +33,9 @@ def _get_incident_via_identify_variables(expr, include_fixed): return list(identify_variables(expr, include_fixed=include_fixed)) -def _get_incident_via_standard_repn(expr, include_fixed, linear_only): +def _get_incident_via_standard_repn( + expr, include_fixed, linear_only, compute_values=False +): if include_fixed: to_unfix = [ var for var in identify_variables(expr, include_fixed=True) if var.fixed @@ -39,7 +45,9 @@ def _get_incident_via_standard_repn(expr, include_fixed, linear_only): context = nullcontext() with context: - repn = generate_standard_repn(expr, compute_values=False, quadratic=False) + repn = generate_standard_repn( + expr, compute_values=compute_values, quadratic=False + ) linear_vars = [] # Check coefficients to make sure we don't include linear variables with @@ -59,7 +67,7 @@ def _get_incident_via_standard_repn(expr, include_fixed, linear_only): linear_vars.append(var) if linear_only: nl_var_id_set = set(id(var) for var in repn.nonlinear_vars) - return [var for var in repn.linear_vars if id(var) not in nl_var_id_set] + return [var for var in linear_vars if id(var) not in nl_var_id_set] else: # Combine linear and nonlinear variables and filter out duplicates. Note # that quadratic=False, so we don't need to include repn.quadratic_vars. @@ -74,6 +82,36 @@ def _get_incident_via_standard_repn(expr, include_fixed, linear_only): return unique_variables +def _get_incident_via_ampl_repn(expr, linear_only, visitor): + var_map = visitor.var_map + orig_activevisitor = AMPLRepn.ActiveVisitor + AMPLRepn.ActiveVisitor = visitor + try: + repn = visitor.walk_expression((expr, None, 0, 1.0)) + finally: + AMPLRepn.ActiveVisitor = orig_activevisitor + + nonlinear_var_ids = [] if repn.nonlinear is None else repn.nonlinear[1] + nonlinear_var_id_set = set() + unique_nonlinear_var_ids = [] + for v_id in nonlinear_var_ids: + if v_id not in nonlinear_var_id_set: + nonlinear_var_id_set.add(v_id) + unique_nonlinear_var_ids.append(v_id) + + nonlinear_vars = [var_map[v_id] for v_id in unique_nonlinear_var_ids] + linear_only_vars = [ + var_map[v_id] + for v_id, coef in repn.linear.items() + if coef != 0.0 and v_id not in nonlinear_var_id_set + ] + if linear_only: + return linear_only_vars + else: + variables = linear_only_vars + nonlinear_vars + return variables + + def get_incident_variables(expr, **kwds): """Get variables that participate in an expression @@ -112,21 +150,38 @@ def get_incident_variables(expr, **kwds): ['x[1]', 'x[2]'] """ - config = IncidenceConfig(kwds) + config = get_config_from_kwds(**kwds) method = config.method include_fixed = config.include_fixed linear_only = config.linear_only + amplrepnvisitor = config._ampl_repn_visitor + + # Check compatibility of arguments if linear_only and method is IncidenceMethod.identify_variables: raise RuntimeError( "linear_only=True is not supported when using identify_variables" ) + if include_fixed and method is IncidenceMethod.ampl_repn: + raise RuntimeError("include_fixed=True is not supported when using ampl_repn") + if method is IncidenceMethod.ampl_repn and amplrepnvisitor is None: + # Developer error, this should never happen! + raise RuntimeError("_ampl_repn_visitor must be provided when using ampl_repn") + + # Dispatch to correct method if method is IncidenceMethod.identify_variables: return _get_incident_via_identify_variables(expr, include_fixed) elif method is IncidenceMethod.standard_repn: - return _get_incident_via_standard_repn(expr, include_fixed, linear_only) + return _get_incident_via_standard_repn( + expr, include_fixed, linear_only, compute_values=False + ) + elif method is IncidenceMethod.standard_repn_compute_values: + return _get_incident_via_standard_repn( + expr, include_fixed, linear_only, compute_values=True + ) + elif method is IncidenceMethod.ampl_repn: + return _get_incident_via_ampl_repn(expr, linear_only, amplrepnvisitor) else: raise ValueError( f"Unrecognized value {method} for the method used to identify incident" - f" variables. Valid options are {IncidenceMethod.identify_variables}" - f" and {IncidenceMethod.standard_repn}." + f" variables. See the IncidenceMethod enum for valid methods." ) diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index f74a68b4422..73d9722eb7e 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -15,7 +15,7 @@ import enum import textwrap -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.base.var import Var from pyomo.core.base.constraint import Constraint from pyomo.core.base.objective import Objective @@ -28,8 +28,8 @@ scipy as sp, plotly, ) -from pyomo.common.deprecation import deprecated -from pyomo.contrib.incidence_analysis.config import IncidenceConfig +from pyomo.common.deprecation import deprecated, deprecation_warning +from pyomo.contrib.incidence_analysis.config import get_config_from_kwds from pyomo.contrib.incidence_analysis.matching import maximum_matching from pyomo.contrib.incidence_analysis.connected import get_independent_submatrices from pyomo.contrib.incidence_analysis.triangularize import ( @@ -47,7 +47,7 @@ from pyomo.contrib.pynumero.asl import AmplInterface pyomo_nlp, pyomo_nlp_available = attempt_import( - 'pyomo.contrib.pynumero.interfaces.pyomo_nlp' + "pyomo.contrib.pynumero.interfaces.pyomo_nlp" ) asl_available = pyomo_nlp_available & AmplInterface.available() @@ -62,7 +62,7 @@ def _check_unindexed(complist): def get_incidence_graph(variables, constraints, **kwds): - config = IncidenceConfig(kwds) + config = get_config_from_kwds(**kwds) return get_bipartite_incidence_graph(variables, constraints, **config) @@ -91,7 +91,9 @@ def get_bipartite_incidence_graph(variables, constraints, **kwds): ``networkx.Graph`` """ - config = IncidenceConfig(kwds) + # Note that this ConfigDict contains the visitor that we will re-use + # when constructing constraints. + config = get_config_from_kwds(**kwds) _check_unindexed(variables + constraints) N = len(variables) M = len(constraints) @@ -134,36 +136,34 @@ def extract_bipartite_subgraph(graph, nodes0, nodes1): in the original graph. """ - subgraph = nx.Graph() - sub_M = len(nodes0) - sub_N = len(nodes1) - subgraph.add_nodes_from(range(sub_M), bipartite=0) - subgraph.add_nodes_from(range(sub_M, sub_M + sub_N), bipartite=1) - + subgraph = graph.subgraph(nodes0 + nodes1) + # TODO: Any error checking that nodes are valid bipartition? + for node in nodes0: + bipartite = graph.nodes[node]["bipartite"] + if bipartite != 0: + raise RuntimeError( + "Invalid bipartite sets. Node {node} in set 0 has" + " bipartite={bipartite}" + ) + for node in nodes1: + bipartite = graph.nodes[node]["bipartite"] + if bipartite != 1: + raise RuntimeError( + "Invalid bipartite sets. Node {node} in set 1 has" + " bipartite={bipartite}" + ) old_new_map = {} for i, node in enumerate(nodes0 + nodes1): if node in old_new_map: raise RuntimeError("Node %s provided more than once.") old_new_map[node] = i - - for node1, node2 in graph.edges(): - if node1 in old_new_map and node2 in old_new_map: - new_node_1 = old_new_map[node1] - new_node_2 = old_new_map[node2] - if ( - subgraph.nodes[new_node_1]["bipartite"] - == subgraph.nodes[new_node_2]["bipartite"] - ): - raise RuntimeError( - "Subgraph is not bipartite. Found an edge between nodes" - " %s and %s (in the original graph)." % (node1, node2) - ) - subgraph.add_edge(new_node_1, new_node_2) - return subgraph + relabeled_subgraph = nx.relabel_nodes(subgraph, old_new_map) + return relabeled_subgraph def _generate_variables_in_constraints(constraints, **kwds): - config = IncidenceConfig(kwds) + # Note: We construct a visitor here + config = get_config_from_kwds(**kwds) known_vars = ComponentSet() for con in constraints: for var in get_incident_variables(con.body, **config): @@ -191,7 +191,7 @@ def get_structural_incidence_matrix(variables, constraints, **kwds): Entries are 1.0. """ - config = IncidenceConfig(kwds) + config = get_config_from_kwds(**kwds) _check_unindexed(variables + constraints) N, M = len(variables), len(constraints) var_idx_map = ComponentMap((v, i) for i, v in enumerate(variables)) @@ -266,7 +266,6 @@ class IncidenceGraphInterface(object): ``evaluate_jacobian_eq`` method instead of ``evaluate_jacobian`` rather than checking constraint expression types. - """ def __init__(self, model=None, active=True, include_inequality=True, **kwds): @@ -275,12 +274,12 @@ def __init__(self, model=None, active=True, include_inequality=True, **kwds): # to cache the incidence graph for fast analysis later on. # WARNING: This cache will become invalid if the user alters their # model. - self._config = IncidenceConfig(kwds) + self._config = get_config_from_kwds(**kwds) if model is None: self._incidence_graph = None self._variables = None self._constraints = None - elif isinstance(model, _BlockData): + elif isinstance(model, BlockData): self._constraints = [ con for con in model.component_data_objects(Constraint, active=active) @@ -330,10 +329,26 @@ def __init__(self, model=None, active=True, include_inequality=True, **kwds): incidence_matrix = nlp.evaluate_jacobian_eq() nxb = nx.algorithms.bipartite self._incidence_graph = nxb.from_biadjacency_matrix(incidence_matrix) + elif isinstance(model, tuple): + # model is a tuple of (nx.Graph, list[pyo.Var], list[pyo.Constraint]) + # We could potentially accept a tuple (variables, constraints). + # TODO: Disallow kwargs if this type of "model" is provided? + nx_graph, variables, constraints = model + self._variables = list(variables) + self._constraints = list(constraints) + self._var_index_map = ComponentMap( + (var, i) for i, var in enumerate(self._variables) + ) + self._con_index_map = ComponentMap( + (con, i) for i, con in enumerate(self._constraints) + ) + # For now, don't check any properties of this graph. We could check + # for a bipartition that matches the variable and constraint lists. + self._incidence_graph = nx_graph else: raise TypeError( "Unsupported type for incidence graph. Expected PyomoNLP" - " or _BlockData but got %s." % type(model) + " or BlockData but got %s." % type(model) ) @property @@ -438,11 +453,29 @@ def _validate_input(self, variables, constraints): raise ValueError("Neither variables nor a model have been provided.") else: variables = self.variables + elif self._incidence_graph is not None: + # If variables were provided and an incidence graph is cached, + # make sure the provided variables exist in the graph. + for var in variables: + if var not in self._var_index_map: + raise KeyError( + f"Variable {var} does not exist in the cached" + " incidence graph." + ) if constraints is None: if self._incidence_graph is None: raise ValueError("Neither constraints nor a model have been provided.") else: constraints = self.constraints + elif self._incidence_graph is not None: + # If constraints were provided and an incidence graph is cached, + # make sure the provided constraints exist in the graph. + for con in constraints: + if con not in self._con_index_map: + raise KeyError( + f"Constraint {con} does not exist in the cached" + " incidence graph." + ) _check_unindexed(variables + constraints) return variables, constraints @@ -464,6 +497,25 @@ def _extract_subgraph(self, variables, constraints): ) return subgraph + def subgraph(self, variables, constraints): + """Extract a subgraph defined by the provided variables and constraints + + Underlying data structures are copied, and constraints are not reinspected + for incidence variables (the edges from this incidence graph are used). + + Returns + ------- + ``IncidenceGraphInterface`` + A new incidence graph containing only the specified variables and + constraints, and the edges between pairs thereof. + + """ + nx_subgraph = self._extract_subgraph(variables, constraints) + subgraph = IncidenceGraphInterface( + (nx_subgraph, variables, constraints), **self._config + ) + return subgraph + @property def incidence_matrix(self): """The structural incidence matrix of variables and constraints. @@ -743,6 +795,36 @@ def dulmage_mendelsohn(self, variables=None, constraints=None): - **unmatched** - Constraints that were not matched in a particular maximum cardinality matching + While the Dulmage-Mendelsohn decomposition does not specify an order + within any of these subsets, the order returned by this function + preserves the maximum matching that is used to compute the decomposition. + That is, zipping "corresponding" variable and constraint subsets yields + pairs in this maximum matching. For example: + + .. doctest:: + :hide: + :skipif: not (networkx_available and scipy_available) + + >>> # Hidden code block creating a dummy model so the following doctest runs + >>> import pyomo.environ as pyo + >>> from pyomo.contrib.incidence_analysis import IncidenceGraphInterface + >>> model = pyo.ConcreteModel() + >>> model.x = pyo.Var([1,2,3]) + >>> model.eq = pyo.Constraint(expr=sum(m.x[:]) == 1) + + .. doctest:: + :skipif: not (networkx_available and scipy_available) + + >>> igraph = IncidenceGraphInterface(model) + >>> var_dmpartition, con_dmpartition = igraph.dulmage_mendelsohn() + >>> vdmp = var_dmpartition + >>> cdmp = con_dmpartition + >>> matching = list(zip( + ... vdmp.underconstrained + vdmp.square + vdmp.overconstrained, + ... cdmp.underconstrained + cdmp.square + cdmp.overconstrained, + ... )) + >>> # matching is a valid maximum matching of variables and constraints! + Returns ------- var_partition: ``ColPartition`` named tuple @@ -790,7 +872,7 @@ def dulmage_mendelsohn(self, variables=None, constraints=None): # Hopefully this does not get too confusing... return var_partition, con_partition - def remove_nodes(self, nodes, constraints=None): + def remove_nodes(self, variables=None, constraints=None): """Removes the specified variables and constraints (columns and rows) from the cached incidence matrix. @@ -802,35 +884,76 @@ def remove_nodes(self, nodes, constraints=None): Parameters ---------- - nodes: list - VarData or ConData objects whose columns or rows will be - removed from the incidence matrix. + variables: list + VarData objects whose nodes will be removed from the incidence graph constraints: list - VarData or ConData objects whose columns or rows will be - removed from the incidence matrix. + ConData objects whose nodes will be removed from the incidence graph + + .. note:: + + **Deprecation in Pyomo v6.7.2** + + The pre-6.7.2 implementation of ``remove_nodes`` allowed variables and + constraints to remove to be specified in a single list. This made + error checking difficult, and indeed, if invalid components were + provided, we carried on silently instead of throwing an error or + warning. As part of a fix to raise an error if an invalid component + (one that is not part of the incidence graph) is provided, we now require + variables and constraints to be specified separately. """ if constraints is None: constraints = [] + if variables is None: + variables = [] if self._incidence_graph is None: raise RuntimeError( "Attempting to remove variables and constraints from cached " "incidence matrix,\nbut no incidence matrix has been cached." ) - to_exclude = ComponentSet(nodes) - to_exclude.update(constraints) - vars_to_include = [v for v in self.variables if v not in to_exclude] - cons_to_include = [c for c in self.constraints if c not in to_exclude] + + vars_to_validate = [] + cons_to_validate = [] + depr_msg = ( + "In IncidenceGraphInterface.remove_nodes, passing variables and" + " constraints in the same list is deprecated. Please separate your" + " variables and constraints and pass them in the order variables," + " constraints." + ) + if any(var in self._con_index_map for var in variables) or any( + con in self._var_index_map for con in constraints + ): + deprecation_warning(depr_msg, version="6.7.2") + # If we received variables/constraints in the same list, sort them. + # Any unrecognized objects will be caught by _validate_input. + for var in variables: + if var in self._con_index_map: + cons_to_validate.append(var) + else: + vars_to_validate.append(var) + for con in constraints: + if con in self._var_index_map: + vars_to_validate.append(con) + else: + cons_to_validate.append(con) + + variables, constraints = self._validate_input( + vars_to_validate, cons_to_validate + ) + v_exclude = ComponentSet(variables) + c_exclude = ComponentSet(constraints) + vars_to_include = [v for v in self.variables if v not in v_exclude] + cons_to_include = [c for c in self.constraints if c not in c_exclude] incidence_graph = self._extract_subgraph(vars_to_include, cons_to_include) # update attributes self._variables = vars_to_include self._constraints = cons_to_include self._incidence_graph = incidence_graph self._var_index_map = ComponentMap( - (var, i) for i, var in enumerate(self.variables) + (var, i) for i, var in enumerate(vars_to_include) ) self._con_index_map = ComponentMap( - (con, i) for i, con in enumerate(self._constraints) + (con, i) for i, con in enumerate(cons_to_include) ) def plot(self, variables=None, constraints=None, title=None, show=True): @@ -856,9 +979,9 @@ def plot(self, variables=None, constraints=None, title=None, show=True): edge_trace = plotly.graph_objects.Scatter( x=edge_x, y=edge_y, - line=dict(width=0.5, color='#888'), - hoverinfo='none', - mode='lines', + line=dict(width=0.5, color="#888"), + hoverinfo="none", + mode="lines", ) node_x = [] @@ -872,28 +995,28 @@ def plot(self, variables=None, constraints=None, title=None, show=True): if node < M: # According to convention, we are a constraint node c = constraints[node] - node_color.append('red') - body_text = '
'.join( + node_color.append("red") + body_text = "
".join( textwrap.wrap(str(c.body), width=120, subsequent_indent=" ") ) node_text.append( - f'{str(c)}
lb: {str(c.lower)}
body: {body_text}
' - f'ub: {str(c.upper)}
active: {str(c.active)}' + f"{str(c)}
lb: {str(c.lower)}
body: {body_text}
" + f"ub: {str(c.upper)}
active: {str(c.active)}" ) else: # According to convention, we are a variable node v = variables[node - M] - node_color.append('blue') + node_color.append("blue") node_text.append( - f'{str(v)}
lb: {str(v.lb)}
ub: {str(v.ub)}
' - f'value: {str(v.value)}
domain: {str(v.domain)}
' - f'fixed: {str(v.is_fixed())}' + f"{str(v)}
lb: {str(v.lb)}
ub: {str(v.ub)}
" + f"value: {str(v.value)}
domain: {str(v.domain)}
" + f"fixed: {str(v.is_fixed())}" ) node_trace = plotly.graph_objects.Scatter( x=node_x, y=node_y, - mode='markers', - hoverinfo='text', + mode="markers", + hoverinfo="text", text=node_text, marker=dict(color=node_color, size=10), ) @@ -902,3 +1025,32 @@ def plot(self, variables=None, constraints=None, title=None, show=True): fig.update_layout(title=dict(text=title)) if show: fig.show() + + def add_edge(self, variable, constraint): + """Adds an edge between variable and constraint in the incidence graph + + Parameters + ---------- + variable: VarData + A variable in the graph + constraint: ConstraintData + A constraint in the graph + """ + if self._incidence_graph is None: + raise RuntimeError( + "Attempting to add edge in an incidence graph from cached " + "incidence graph,\nbut no incidence graph has been cached." + ) + + if variable not in self._var_index_map: + raise RuntimeError("%s is not a variable in the incidence graph" % variable) + + if constraint not in self._con_index_map: + raise RuntimeError( + "%s is not a constraint in the incidence graph" % constraint + ) + + var_id = self._var_index_map[variable] + len(self._con_index_map) + con_id = self._con_index_map[constraint] + + self._incidence_graph.add_edge(var_id, con_id) diff --git a/pyomo/contrib/incidence_analysis/matching.py b/pyomo/contrib/incidence_analysis/matching.py index 14b3cd5b18d..e37b35cd973 100644 --- a/pyomo/contrib/incidence_analysis/matching.py +++ b/pyomo/contrib/incidence_analysis/matching.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/incidence_analysis/scc_solver.py b/pyomo/contrib/incidence_analysis/scc_solver.py index d7620278fd3..378647c190c 100644 --- a/pyomo/contrib/incidence_analysis/scc_solver.py +++ b/pyomo/contrib/incidence_analysis/scc_solver.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -18,15 +18,16 @@ IncidenceGraphInterface, _generate_variables_in_constraints, ) +from pyomo.contrib.incidence_analysis.config import IncidenceMethod _log = logging.getLogger(__name__) def generate_strongly_connected_components( - constraints, variables=None, include_fixed=False + constraints, variables=None, include_fixed=False, igraph=None ): - """Yield in order ``_BlockData`` that each contain the variables and + """Yield in order ``BlockData`` that each contain the variables and constraints of a single diagonal block in a block lower triangularization of the incidence matrix of constraints and variables @@ -41,13 +42,16 @@ def generate_strongly_connected_components( variables: List of Pyomo variable data objects Variables that may participate in strongly connected components. If not provided, all variables in the constraints will be used. - include_fixed: Bool + include_fixed: Bool, optional Indicates whether fixed variables will be included when identifying variables in constraints. + igraph: IncidenceGraphInterface, optional + Incidence graph containing (at least) the provided constraints + and variables. Yields ------ - Tuple of ``_BlockData``, list-of-variables + Tuple of ``BlockData``, list-of-variables Blocks containing the variables and constraints of every strongly connected component, in a topological order. The variables are the "input variables" for that block. @@ -55,11 +59,17 @@ def generate_strongly_connected_components( """ if variables is None: variables = list( - _generate_variables_in_constraints(constraints, include_fixed=include_fixed) + _generate_variables_in_constraints( + constraints, + include_fixed=include_fixed, + method=IncidenceMethod.ampl_repn, + ) ) assert len(variables) == len(constraints) - igraph = IncidenceGraphInterface() + if igraph is None: + igraph = IncidenceGraphInterface() + var_blocks, con_blocks = igraph.block_triangularize( variables=variables, constraints=constraints ) @@ -73,7 +83,7 @@ def generate_strongly_connected_components( def solve_strongly_connected_components( - block, solver=None, solve_kwds=None, calc_var_kwds=None + block, *, solver=None, solve_kwds=None, use_calc_var=True, calc_var_kwds=None ): """Solve a square system of variables and equality constraints by solving strongly connected components individually. @@ -98,6 +108,9 @@ def solve_strongly_connected_components( a solve method. solve_kwds: Dictionary Keyword arguments for the solver's solve method + use_calc_var: Bool + Whether to use ``calculate_variable_from_constraint`` for one-by-one + square system solves calc_var_kwds: Dictionary Keyword arguments for calculate_variable_from_constraint @@ -112,23 +125,28 @@ def solve_strongly_connected_components( calc_var_kwds = {} igraph = IncidenceGraphInterface( - block, active=True, include_fixed=False, include_inequality=False + block, + active=True, + include_fixed=False, + include_inequality=False, + method=IncidenceMethod.ampl_repn, ) constraints = igraph.constraints variables = igraph.variables res_list = [] log_blocks = _log.isEnabledFor(logging.DEBUG) - for scc, inputs in generate_strongly_connected_components(constraints, variables): - with TemporarySubsystemManager(to_fix=inputs): + for scc, inputs in generate_strongly_connected_components( + constraints, variables, igraph=igraph + ): + with TemporarySubsystemManager(to_fix=inputs, remove_bounds_on_fix=True): N = len(scc.vars) - if N == 1: + if N == 1 and use_calc_var: if log_blocks: _log.debug(f"Solving 1x1 block: {scc.cons[0].name}.") results = calculate_variable_from_constraint( scc.vars[0], scc.cons[0], **calc_var_kwds ) - res_list.append(results) else: if solver is None: var_names = [var.name for var in scc.vars.values()][:10] @@ -142,5 +160,5 @@ def solve_strongly_connected_components( if log_blocks: _log.debug(f"Solving {N}x{N} block.") results = solver.solve(scc, **solve_kwds) - res_list.append(results) + res_list.append(results) return res_list diff --git a/pyomo/contrib/incidence_analysis/tests/__init__.py b/pyomo/contrib/incidence_analysis/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/incidence_analysis/tests/__init__.py +++ b/pyomo/contrib/incidence_analysis/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/incidence_analysis/tests/models_for_testing.py b/pyomo/contrib/incidence_analysis/tests/models_for_testing.py index 98d61201619..6040e80e068 100644 --- a/pyomo/contrib/incidence_analysis/tests/models_for_testing.py +++ b/pyomo/contrib/incidence_analysis/tests/models_for_testing.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/incidence_analysis/tests/test_connected.py b/pyomo/contrib/incidence_analysis/tests/test_connected.py index a937a5029a1..421231d3dd0 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_connected.py +++ b/pyomo/contrib/incidence_analysis/tests/test_connected.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/incidence_analysis/tests/test_dulmage_mendelsohn.py b/pyomo/contrib/incidence_analysis/tests/test_dulmage_mendelsohn.py index 4aae9abc2c6..6195d6afca7 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_dulmage_mendelsohn.py +++ b/pyomo/contrib/incidence_analysis/tests/test_dulmage_mendelsohn.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -120,6 +120,27 @@ def test_rectangular_system(self): potentially_unmatched_set = set(range(len(variables))) self.assertEqual(set(potentially_unmatched), potentially_unmatched_set) + def test_recover_matching(self): + N_model = 4 + m = make_gas_expansion_model(N_model) + variables = list(m.component_data_objects(pyo.Var)) + constraints = list(m.component_data_objects(pyo.Constraint)) + imat = get_structural_incidence_matrix(variables, constraints) + rdmp, cdmp = dulmage_mendelsohn(imat) + rmatch = rdmp.underconstrained + rdmp.square + rdmp.overconstrained + cmatch = cdmp.underconstrained + cdmp.square + cdmp.overconstrained + matching = list(zip(rmatch, cmatch)) + rmatch = [r for (r, c) in matching] + cmatch = [c for (r, c) in matching] + # Assert that the matched rows and columns contain no duplicates + self.assertEqual(len(set(rmatch)), len(rmatch)) + self.assertEqual(len(set(cmatch)), len(cmatch)) + entry_set = set(zip(imat.row, imat.col)) + for i, j in matching: + # Assert that each pair in the matching is a valid entry + # in the matrix + self.assertIn((i, j), entry_set) + @unittest.skipUnless(networkx_available, "networkx is not available.") @unittest.skipUnless(scipy_available, "scipy is not available.") diff --git a/pyomo/contrib/incidence_analysis/tests/test_incidence.py b/pyomo/contrib/incidence_analysis/tests/test_incidence.py index 3b0b6a997aa..832fbbfb10c 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_incidence.py +++ b/pyomo/contrib/incidence_analysis/tests/test_incidence.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -56,44 +56,56 @@ def test_basic_incidence(self): def test_incidence_with_fixed_variable(self): m = pyo.ConcreteModel() - m.x = pyo.Var([1, 2, 3]) + m.x = pyo.Var([1, 2, 3], initialize=1.0) expr = m.x[1] + m.x[1] * m.x[2] + m.x[1] * pyo.exp(m.x[3]) m.x[2].fix() variables = self._get_incident_variables(expr) var_set = ComponentSet(variables) self.assertEqual(var_set, ComponentSet([m.x[1], m.x[3]])) - def test_incidence_with_mutable_parameter(self): + def test_incidence_with_named_expression(self): m = pyo.ConcreteModel() m.x = pyo.Var([1, 2, 3]) - m.p = pyo.Param(mutable=True, initialize=None) - expr = m.x[1] + m.p * m.x[1] * m.x[2] + m.x[1] * pyo.exp(m.x[3]) + m.subexpr = pyo.Expression(pyo.Integers) + m.subexpr[1] = m.x[1] * pyo.exp(m.x[3]) + expr = m.x[1] + m.x[1] * m.x[2] + m.subexpr[1] variables = self._get_incident_variables(expr) self.assertEqual(ComponentSet(variables), ComponentSet(m.x[:])) -class TestIncidenceStandardRepn(unittest.TestCase, _TestIncidence): - def _get_incident_variables(self, expr, **kwds): - method = IncidenceMethod.standard_repn - return get_incident_variables(expr, method=method, **kwds) +class _TestIncidenceLinearOnly(object): + """Tests for methods that support linear_only""" - def test_assumed_standard_repn_behavior(self): + def _get_incident_variables(self, expr): + raise NotImplementedError( + "_TestIncidenceLinearOnly should not be used directly" + ) + + def test_linear_only(self): m = pyo.ConcreteModel() - m.x = pyo.Var([1, 2]) - m.p = pyo.Param(initialize=0.0) + m.x = pyo.Var([1, 2, 3]) - # We rely on variables with constant coefficients of zero not appearing - # in the standard repn (as opposed to appearing with explicit - # coefficients of zero). - expr = m.x[1] + 0 * m.x[2] - repn = generate_standard_repn(expr) - self.assertEqual(len(repn.linear_vars), 1) - self.assertIs(repn.linear_vars[0], m.x[1]) + expr = 2 * m.x[1] + 4 * m.x[2] * m.x[1] - m.x[1] * pyo.exp(m.x[3]) + variables = self._get_incident_variables(expr, linear_only=True) + self.assertEqual(len(variables), 0) - expr = m.p * m.x[1] + m.x[2] - repn = generate_standard_repn(expr) - self.assertEqual(len(repn.linear_vars), 1) - self.assertIs(repn.linear_vars[0], m.x[2]) + expr = 2 * m.x[1] + 2 * m.x[2] * m.x[3] + 3 * m.x[2] + variables = self._get_incident_variables(expr, linear_only=True) + self.assertEqual(ComponentSet(variables), ComponentSet([m.x[1]])) + + m.x[3].fix(2.5) + expr = 2 * m.x[1] + 2 * m.x[2] * m.x[3] + 3 * m.x[2] + variables = self._get_incident_variables(expr, linear_only=True) + self.assertEqual(ComponentSet(variables), ComponentSet([m.x[1], m.x[2]])) + + +class _TestIncidenceLinearCancellation(object): + """Tests for methods that perform linear cancellation""" + + def _get_incident_variables(self, expr): + raise NotImplementedError( + "_TestIncidenceLinearCancellation should not be used directly" + ) def test_zero_coef(self): m = pyo.ConcreteModel() @@ -113,23 +125,6 @@ def test_variable_minus_itself(self): var_set = ComponentSet(variables) self.assertEqual(var_set, ComponentSet([m.x[2], m.x[3]])) - def test_linear_only(self): - m = pyo.ConcreteModel() - m.x = pyo.Var([1, 2, 3]) - - expr = 2 * m.x[1] + 4 * m.x[2] * m.x[1] - m.x[1] * pyo.exp(m.x[3]) - variables = self._get_incident_variables(expr, linear_only=True) - self.assertEqual(len(variables), 0) - - expr = 2 * m.x[1] + 2 * m.x[2] * m.x[3] + 3 * m.x[2] - variables = self._get_incident_variables(expr, linear_only=True) - self.assertEqual(ComponentSet(variables), ComponentSet([m.x[1]])) - - m.x[3].fix(2.5) - expr = 2 * m.x[1] + 2 * m.x[2] * m.x[3] + 3 * m.x[2] - variables = self._get_incident_variables(expr, linear_only=True) - self.assertEqual(ComponentSet(variables), ComponentSet([m.x[1], m.x[2]])) - def test_fixed_zero_linear_coefficient(self): m = pyo.ConcreteModel() m.x = pyo.Var([1, 2, 3]) @@ -148,6 +143,49 @@ def test_fixed_zero_linear_coefficient(self): variables = self._get_incident_variables(expr) self.assertEqual(ComponentSet(variables), ComponentSet([m.x[1], m.x[2]])) + # NOTE: This test assumes that all methods that support linear cancellation + # accept a linear_only argument. If this changes, this test will need to be + # moved. + def test_fixed_zero_coefficient_linear_only(self): + m = pyo.ConcreteModel() + m.x = pyo.Var([1, 2, 3]) + expr = m.x[1] * m.x[2] + 2 * m.x[3] + m.x[2].fix(0) + variables = get_incident_variables( + expr, method=IncidenceMethod.standard_repn, linear_only=True + ) + self.assertEqual(len(variables), 1) + self.assertIs(variables[0], m.x[3]) + + +class TestIncidenceStandardRepn( + unittest.TestCase, + _TestIncidence, + _TestIncidenceLinearOnly, + _TestIncidenceLinearCancellation, +): + def _get_incident_variables(self, expr, **kwds): + method = IncidenceMethod.standard_repn + return get_incident_variables(expr, method=method, **kwds) + + def test_assumed_standard_repn_behavior(self): + m = pyo.ConcreteModel() + m.x = pyo.Var([1, 2]) + m.p = pyo.Param(initialize=0.0) + + # We rely on variables with constant coefficients of zero not appearing + # in the standard repn (as opposed to appearing with explicit + # coefficients of zero). + expr = m.x[1] + 0 * m.x[2] + repn = generate_standard_repn(expr) + self.assertEqual(len(repn.linear_vars), 1) + self.assertIs(repn.linear_vars[0], m.x[1]) + + expr = m.p * m.x[1] + m.x[2] + repn = generate_standard_repn(expr) + self.assertEqual(len(repn.linear_vars), 1) + self.assertIs(repn.linear_vars[0], m.x[2]) + def test_fixed_none_linear_coefficient(self): m = pyo.ConcreteModel() m.x = pyo.Var([1, 2, 3]) @@ -157,6 +195,14 @@ def test_fixed_none_linear_coefficient(self): variables = self._get_incident_variables(expr) self.assertEqual(ComponentSet(variables), ComponentSet([m.x[1], m.x[2]])) + def test_incidence_with_mutable_parameter(self): + m = pyo.ConcreteModel() + m.x = pyo.Var([1, 2, 3]) + m.p = pyo.Param(mutable=True, initialize=None) + expr = m.x[1] + m.p * m.x[1] * m.x[2] + m.x[1] * pyo.exp(m.x[3]) + variables = self._get_incident_variables(expr) + self.assertEqual(ComponentSet(variables), ComponentSet(m.x[:])) + class TestIncidenceIdentifyVariables(unittest.TestCase, _TestIncidence): def _get_incident_variables(self, expr, **kwds): @@ -181,6 +227,36 @@ def test_variable_minus_itself(self): var_set = ComponentSet(variables) self.assertEqual(var_set, ComponentSet(m.x[:])) + def test_incidence_with_mutable_parameter(self): + m = pyo.ConcreteModel() + m.x = pyo.Var([1, 2, 3]) + m.p = pyo.Param(mutable=True, initialize=None) + expr = m.x[1] + m.p * m.x[1] * m.x[2] + m.x[1] * pyo.exp(m.x[3]) + variables = self._get_incident_variables(expr) + self.assertEqual(ComponentSet(variables), ComponentSet(m.x[:])) + + +class TestIncidenceAmplRepn( + unittest.TestCase, + _TestIncidence, + _TestIncidenceLinearOnly, + _TestIncidenceLinearCancellation, +): + def _get_incident_variables(self, expr, **kwds): + method = IncidenceMethod.ampl_repn + return get_incident_variables(expr, method=method, **kwds) + + +class TestIncidenceStandardRepnComputeValues( + unittest.TestCase, + _TestIncidence, + _TestIncidenceLinearOnly, + _TestIncidenceLinearCancellation, +): + def _get_incident_variables(self, expr, **kwds): + method = IncidenceMethod.standard_repn_compute_values + return get_incident_variables(expr, method=method, **kwds) + if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/incidence_analysis/tests/test_interface.py b/pyomo/contrib/incidence_analysis/tests/test_interface.py index 75bac643790..9957e78168b 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_interface.py +++ b/pyomo/contrib/incidence_analysis/tests/test_interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -634,17 +634,15 @@ def test_exception(self): nlp = PyomoNLP(model) igraph = IncidenceGraphInterface(nlp) - with self.assertRaises(RuntimeError) as exc: + with self.assertRaisesRegex(KeyError, "does not exist"): variables = [model.P] constraints = [model.ideal_gas] igraph.maximum_matching(variables, constraints) - self.assertIn('must be unindexed', str(exc.exception)) - with self.assertRaises(RuntimeError) as exc: + with self.assertRaisesRegex(KeyError, "does not exist"): variables = [model.P] constraints = [model.ideal_gas] igraph.block_triangularize(variables, constraints) - self.assertIn('must be unindexed', str(exc.exception)) @unittest.skipUnless(networkx_available, "networkx is not available.") @@ -885,17 +883,15 @@ def test_exception(self): model = make_gas_expansion_model() igraph = IncidenceGraphInterface(model) - with self.assertRaises(RuntimeError) as exc: + with self.assertRaisesRegex(KeyError, "does not exist"): variables = [model.P] constraints = [model.ideal_gas] igraph.maximum_matching(variables, constraints) - self.assertIn('must be unindexed', str(exc.exception)) - with self.assertRaises(RuntimeError) as exc: + with self.assertRaisesRegex(KeyError, "does not exist"): variables = [model.P] constraints = [model.ideal_gas] igraph.block_triangularize(variables, constraints) - self.assertIn('must be unindexed', str(exc.exception)) @unittest.skipUnless(scipy_available, "scipy is not available.") def test_remove(self): @@ -923,7 +919,7 @@ def test_remove(self): # Say we know that these variables and constraints should # be matched... vars_to_remove = [model.F[0], model.F[2]] - cons_to_remove = (model.mbal[1], model.mbal[2]) + cons_to_remove = [model.mbal[1], model.mbal[2]] igraph.remove_nodes(vars_to_remove, cons_to_remove) variable_set = ComponentSet(igraph.variables) self.assertNotIn(model.F[0], variable_set) @@ -1309,7 +1305,7 @@ def test_remove(self): # matrix. vars_to_remove = [m.flow_comp[1]] cons_to_remove = [m.flow_eqn[1]] - igraph.remove_nodes(vars_to_remove + cons_to_remove) + igraph.remove_nodes(vars_to_remove, cons_to_remove) var_dmp, con_dmp = igraph.dulmage_mendelsohn() var_con_set = ComponentSet(igraph.variables + igraph.constraints) underconstrained_set = ComponentSet( @@ -1323,6 +1319,22 @@ def test_remove(self): self.assertEqual(N_new, N - len(cons_to_remove)) self.assertEqual(M_new, M - len(vars_to_remove)) + def test_recover_matching_from_dulmage_mendelsohn(self): + m = make_degenerate_solid_phase_model() + igraph = IncidenceGraphInterface(m) + vdmp, cdmp = igraph.dulmage_mendelsohn() + vmatch = vdmp.underconstrained + vdmp.square + vdmp.overconstrained + cmatch = cdmp.underconstrained + cdmp.square + cdmp.overconstrained + # Assert no duplicates in matched variables and constraints + self.assertEqual(len(ComponentSet(vmatch)), len(vmatch)) + self.assertEqual(len(ComponentSet(cmatch)), len(cmatch)) + matching = list(zip(vmatch, cmatch)) + # Assert each matched pair contains a variable that participates + # in the constraint. + for var, con in matching: + var_in_con = ComponentSet(igraph.get_adjacent_to(con)) + self.assertIn(var, var_in_con) + @unittest.skipUnless(networkx_available, "networkx is not available.") class TestConnectedComponents(unittest.TestCase): @@ -1444,6 +1456,42 @@ def test_remove_no_matrix(self): with self.assertRaisesRegex(RuntimeError, "no incidence matrix"): igraph.remove_nodes([m.v1]) + def test_remove_bad_node(self): + m = pyo.ConcreteModel() + m.x = pyo.Var([1, 2, 3]) + m.eq = pyo.Constraint(pyo.PositiveIntegers) + m.eq[1] = m.x[1] * m.x[2] == m.x[3] + m.eq[2] = m.x[1] + 2 * m.x[2] == 3 * m.x[3] + igraph = IncidenceGraphInterface(m) + with self.assertRaisesRegex(KeyError, "does not exist"): + # Suppose we think something like this should work. We should get + # an error, and not silently do nothing. + igraph.remove_nodes([m.x], [m.eq[1]]) + + with self.assertRaisesRegex(KeyError, "does not exist"): + igraph.remove_nodes(None, [m.eq]) + + with self.assertRaisesRegex(KeyError, "does not exist"): + igraph.remove_nodes([[m.x[1], m.x[2]], [m.eq[1]]]) + + def test_remove_varcon_samelist_deprecated(self): + m = pyo.ConcreteModel() + m.x = pyo.Var([1, 2, 3]) + m.eq = pyo.Constraint(pyo.PositiveIntegers) + m.eq[1] = m.x[1] * m.x[2] == m.x[3] + m.eq[2] = m.x[1] + 2 * m.x[2] == 3 * m.x[3] + + igraph = IncidenceGraphInterface(m) + # This raises a deprecation warning. When the deprecated functionality + # is removed, this will fail, and this test should be updated accordingly. + igraph.remove_nodes([m.eq[1], m.x[1]]) + self.assertEqual(len(igraph.variables), 2) + self.assertEqual(len(igraph.constraints), 1) + + igraph.remove_nodes([], [m.eq[2], m.x[2]]) + self.assertEqual(len(igraph.variables), 1) + self.assertEqual(len(igraph.constraints), 0) + @unittest.skipUnless(networkx_available, "networkx is not available.") @unittest.skipUnless(scipy_available, "scipy is not available.") @@ -1637,11 +1685,11 @@ def test_extract_exceptions(self): sg_cons = [0, 2, 5] sg_vars = [i + len(constraints) for i in [2, 3]] - msg = "Subgraph is not bipartite" + msg = "Invalid bipartite sets." with self.assertRaisesRegex(RuntimeError, msg): subgraph = extract_bipartite_subgraph(graph, sg_cons, sg_vars) - sg_cons = [0, 2, 5] + sg_cons = [0, 2, 0] sg_vars = [i + len(constraints) for i in [2, 0, 3]] msg = "provided more than once" with self.assertRaisesRegex(RuntimeError, msg): @@ -1729,7 +1777,7 @@ def test_plot(self): m.c2 = pyo.Constraint(expr=m.z >= m.x) m.y.fix() igraph = IncidenceGraphInterface(m, include_inequality=True, include_fixed=True) - igraph.plot(title='test plot', show=False) + igraph.plot(title="test plot", show=False) def test_zero_coeff(self): m = pyo.ConcreteModel() @@ -1775,6 +1823,91 @@ def test_linear_only(self): self.assertIs(matching[m.eq2], m.x[2]) self.assertIs(matching[m.eq3], m.x[3]) + def test_add_edge(self): + m = pyo.ConcreteModel() + m.x = pyo.Var([1, 2, 3, 4]) + m.eq1 = pyo.Constraint(expr=m.x[1] ** 2 + m.x[2] ** 2 + m.x[3] ** 2 == 1) + m.eq2 = pyo.Constraint(expr=m.x[2] + pyo.sqrt(m.x[1]) + pyo.exp(m.x[3]) == 1) + m.eq3 = pyo.Constraint(expr=m.x[3] + m.x[2] + m.x[4] == 1) + m.eq4 = pyo.Constraint(expr=m.x[1] + m.x[2] ** 2 == 5) + + igraph = IncidenceGraphInterface(m, linear_only=False) + n_edges_original = igraph.n_edges + + # Test edge is added between previously unconnected nodes + igraph.add_edge(m.x[1], m.eq3) + n_edges_new = igraph.n_edges + assert ComponentSet(igraph.get_adjacent_to(m.eq3)) == ComponentSet(m.x[:]) + self.assertEqual(n_edges_original + 1, n_edges_new) + + # Test no edge is added if there exists a previous edge between nodes + igraph.add_edge(m.x[2], m.eq3) + n_edges2 = igraph.n_edges + self.assertEqual(n_edges_new, n_edges2) + + def test_add_edge_linear_igraph(self): + m = pyo.ConcreteModel() + m.x = pyo.Var([1, 2, 3, 4]) + m.eq1 = pyo.Constraint(expr=m.x[1] + m.x[3] == 1) + m.eq2 = pyo.Constraint(expr=m.x[2] + pyo.sqrt(m.x[1]) + pyo.exp(m.x[3]) == 1) + m.eq3 = pyo.Constraint(expr=m.x[4] ** 2 + m.x[1] ** 3 + m.x[2] ** 2 == 1) + + # Make sure error is raised when a variable is not in the igraph + igraph = IncidenceGraphInterface(m, linear_only=True) + + msg = "is not a variable in the incidence graph" + with self.assertRaisesRegex(RuntimeError, msg): + igraph.add_edge(m.x[4], m.eq2) + + def test_var_elim(self): + m = pyo.ConcreteModel() + m.x = pyo.Var([1, 2, 3, 4]) + m.eq1 = pyo.Constraint(expr=m.x[1] ** 2 + m.x[2] ** 2 + m.x[3] ** 2 == 1) + m.eq2 = pyo.Constraint(expr=pyo.sqrt(m.x[1]) + pyo.exp(m.x[3]) == 1) + m.eq3 = pyo.Constraint(expr=m.x[3] + m.x[2] + m.x[4] == 1) + m.eq4 = pyo.Constraint(expr=m.x[1] == 5 * m.x[2]) + + igraph = IncidenceGraphInterface(m) + # Eliminate x[1] using eq4 + for adj_con in igraph.get_adjacent_to(m.x[1]): + for adj_var in igraph.get_adjacent_to(m.eq4): + igraph.add_edge(adj_var, adj_con) + igraph.remove_nodes([m.x[1]], [m.eq4]) + + assert ComponentSet(igraph.variables) == ComponentSet([m.x[2], m.x[3], m.x[4]]) + assert ComponentSet(igraph.constraints) == ComponentSet([m.eq1, m.eq2, m.eq3]) + self.assertEqual(7, igraph.n_edges) + + assert m.x[2] in ComponentSet(igraph.get_adjacent_to(m.eq1)) + assert m.x[2] in ComponentSet(igraph.get_adjacent_to(m.eq2)) + + def test_subgraph(self): + m = pyo.ConcreteModel() + m.I = pyo.Set(initialize=[1, 2, 3, 4]) + m.v = pyo.Var(m.I, bounds=(0, None)) + m.eq1 = pyo.Constraint(expr=m.v[1] ** 2 + m.v[2] ** 2 == 1.0) + m.eq2 = pyo.Constraint(expr=m.v[1] + 2.0 == m.v[3]) + m.ineq1 = pyo.Constraint(expr=m.v[2] - m.v[3] ** 0.5 + m.v[4] ** 2 <= 1.0) + m.ineq2 = pyo.Constraint(expr=m.v[2] * m.v[4] >= 1.0) + m.ineq3 = pyo.Constraint(expr=m.v[1] >= m.v[4] ** 4) + m.obj = pyo.Objective(expr=-m.v[1] - m.v[2] + m.v[3] ** 2 + m.v[4] ** 2) + igraph = IncidenceGraphInterface(m) + eq_igraph = igraph.subgraph(igraph.variables, [m.eq1, m.eq2]) + for i in range(len(igraph.variables)): + self.assertIs(igraph.variables[i], eq_igraph.variables[i]) + self.assertEqual( + ComponentSet(eq_igraph.constraints), ComponentSet([m.eq1, m.eq2]) + ) + + subgraph = eq_igraph.subgraph([m.v[1], m.v[3]], [m.eq1, m.eq2]) + self.assertEqual( + ComponentSet(subgraph.get_adjacent_to(m.eq2)), + ComponentSet([m.v[1], m.v[3]]), + ) + self.assertEqual( + ComponentSet(subgraph.get_adjacent_to(m.eq1)), ComponentSet([m.v[1]]) + ) + @unittest.skipUnless(networkx_available, "networkx is not available.") class TestIndexedBlock(unittest.TestCase): @@ -1787,7 +1920,7 @@ def test_block_data_obj(self): self.assertEqual(len(var_dmp.unmatched), 1) self.assertEqual(len(con_dmp.unmatched), 1) - msg = "Unsupported type.*_BlockData" + msg = "Unsupported type.*BlockData" with self.assertRaisesRegex(TypeError, msg): igraph = IncidenceGraphInterface(m.block) diff --git a/pyomo/contrib/incidence_analysis/tests/test_matching.py b/pyomo/contrib/incidence_analysis/tests/test_matching.py index b5550b3b84c..2327439f0a2 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_matching.py +++ b/pyomo/contrib/incidence_analysis/tests/test_matching.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/incidence_analysis/tests/test_scc_solver.py b/pyomo/contrib/incidence_analysis/tests/test_scc_solver.py index 6efe52a7d80..b75f93e4a12 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_scc_solver.py +++ b/pyomo/contrib/incidence_analysis/tests/test_scc_solver.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/incidence_analysis/tests/test_triangularize.py b/pyomo/contrib/incidence_analysis/tests/test_triangularize.py index 76ba4403310..22548a15998 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_triangularize.py +++ b/pyomo/contrib/incidence_analysis/tests/test_triangularize.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/incidence_analysis/tests/test_visualize.py b/pyomo/contrib/incidence_analysis/tests/test_visualize.py new file mode 100644 index 00000000000..7c5538b671f --- /dev/null +++ b/pyomo/contrib/incidence_analysis/tests/test_visualize.py @@ -0,0 +1,47 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest +from pyomo.common.dependencies import ( + matplotlib, + matplotlib_available, + scipy_available, + networkx_available, +) +from pyomo.contrib.incidence_analysis.visualize import spy_dulmage_mendelsohn +from pyomo.contrib.incidence_analysis.tests.models_for_testing import ( + make_gas_expansion_model, + make_dynamic_model, + make_degenerate_solid_phase_model, +) + + +@unittest.skipUnless(matplotlib_available, "Matplotlib is not available") +@unittest.skipUnless(scipy_available, "SciPy is not available") +@unittest.skipUnless(networkx_available, "NetworkX is not available") +class TestSpy(unittest.TestCase): + def test_spy_dulmage_mendelsohn(self): + models = [ + make_gas_expansion_model(), + make_dynamic_model(), + make_degenerate_solid_phase_model(), + ] + for m in models: + fig, ax = spy_dulmage_mendelsohn(m) + # Note that this is a weak test. We just test that we can call the + # plot method, it doesn't raise an error, and gives us back the + # types we expect. We don't attempt to validate the resulting plot. + self.assertTrue(isinstance(fig, matplotlib.pyplot.Figure)) + self.assertTrue(isinstance(ax, matplotlib.pyplot.Axes)) + + +if __name__ == "__main__": + unittest.main() diff --git a/pyomo/contrib/incidence_analysis/triangularize.py b/pyomo/contrib/incidence_analysis/triangularize.py index ac6680a367e..6af251b1ec6 100644 --- a/pyomo/contrib/incidence_analysis/triangularize.py +++ b/pyomo/contrib/incidence_analysis/triangularize.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/incidence_analysis/util.py b/pyomo/contrib/incidence_analysis/util.py index a127161d33d..8b6572eb900 100644 --- a/pyomo/contrib/incidence_analysis/util.py +++ b/pyomo/contrib/incidence_analysis/util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/incidence_analysis/visualize.py b/pyomo/contrib/incidence_analysis/visualize.py new file mode 100644 index 00000000000..af1bdbbb918 --- /dev/null +++ b/pyomo/contrib/incidence_analysis/visualize.py @@ -0,0 +1,219 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ +"""Module for visualizing results of incidence graph or matrix analysis + +""" +from pyomo.contrib.incidence_analysis.config import IncidenceOrder +from pyomo.contrib.incidence_analysis.interface import ( + IncidenceGraphInterface, + get_structural_incidence_matrix, +) +from pyomo.common.dependencies import matplotlib + + +def _partition_variables_and_constraints( + model, order=IncidenceOrder.dulmage_mendelsohn_upper, **kwds +): + """Partition variables and constraints in an incidence graph""" + igraph = IncidenceGraphInterface(model, **kwds) + vdmp, cdmp = igraph.dulmage_mendelsohn() + + ucv = vdmp.unmatched + vdmp.underconstrained + ucc = cdmp.underconstrained + + ocv = vdmp.overconstrained + occ = cdmp.overconstrained + cdmp.unmatched + + ucvblocks, uccblocks = igraph.get_connected_components( + variables=ucv, constraints=ucc + ) + ocvblocks, occblocks = igraph.get_connected_components( + variables=ocv, constraints=occ + ) + wcvblocks, wccblocks = igraph.block_triangularize( + variables=vdmp.square, constraints=cdmp.square + ) + # By default, we block-*lower* triangularize. By default, however, we want + # the Dulmage-Mendelsohn decomposition to be block-*upper* triangular. + wcvblocks.reverse() + wccblocks.reverse() + vpartition = [ucvblocks, wcvblocks, ocvblocks] + cpartition = [uccblocks, wccblocks, occblocks] + + if order == IncidenceOrder.dulmage_mendelsohn_lower: + # If a block-lower triangular matrix was requested, we need to reverse + # both the inner and outer partitions + vpartition.reverse() + cpartition.reverse() + for vb in vpartition: + vb.reverse() + for cb in cpartition: + cb.reverse() + + return vpartition, cpartition + + +def _get_rectangle_around_coords(ij1, ij2, linewidth=2, linestyle="-"): + i1, j1 = ij1 + i2, j2 = ij2 + buffer = 0.5 + ll_corner = (min(i1, i2) - buffer, min(j1, j2) - buffer) + width = abs(i1 - i2) + 2 * buffer + height = abs(j1 - j2) + 2 * buffer + rect = matplotlib.patches.Rectangle( + ll_corner, + width, + height, + clip_on=False, + fill=False, + edgecolor="orange", + linewidth=linewidth, + linestyle=linestyle, + ) + return rect + + +def spy_dulmage_mendelsohn( + model, + *, + incidence_kwds=None, + order=IncidenceOrder.dulmage_mendelsohn_upper, + highlight_coarse=True, + highlight_fine=True, + skip_wellconstrained=False, + ax=None, + linewidth=2, + spy_kwds=None, +): + """Plot sparsity structure in Dulmage-Mendelsohn order on Matplotlib axes + + This is a wrapper around the Matplotlib ``Axes.spy`` method for plotting + an incidence matrix in Dulmage-Mendelsohn order, with coarse and/or fine + partitions highlighted. The coarse partition refers to the under-constrained, + over-constrained, and well-constrained subsystems, while the fine partition + refers to block diagonal or block triangular partitions of the former + subsystems. + + Parameters + ---------- + + model: ``ConcreteModel`` + Input model to plot sparsity structure of + + incidence_kwds: dict, optional + Config options for ``IncidenceGraphInterface`` + + order: ``IncidenceOrder``, optional + Order in which to plot sparsity structure. Default is + ``IncidenceOrder.dulmage_mendelsohn_upper`` for a block-upper triangular + matrix. Set to ``IncidenceOrder.dulmage_mendelsohn_lower`` for a + block-lower triangular matrix. + + highlight_coarse: bool, optional + Whether to draw a rectangle around the coarse partition. Default True + + highlight_fine: bool, optional + Whether to draw a rectangle around the fine partition. Default True + + skip_wellconstrained: bool, optional + Whether to skip highlighting the well-constrained subsystem of the + coarse partition. Default False + + ax: ``matplotlib.pyplot.Axes``, optional + Axes object on which to plot. If not provided, new figure + and axes are created. + + linewidth: int, optional + Line width of for rectangle used to highlight. Default 2 + + spy_kwds: dict, optional + Keyword arguments for ``Axes.spy`` + + Returns + ------- + + fig: ``matplotlib.pyplot.Figure`` or ``None`` + Figure on which the sparsity structure is plotted. ``None`` if axes + are provided + + ax: ``matplotlib.pyplot.Axes`` + Axes on which the sparsity structure is plotted + + """ + plt = matplotlib.pyplot + if incidence_kwds is None: + incidence_kwds = {} + if spy_kwds is None: + spy_kwds = {} + + vpart, cpart = _partition_variables_and_constraints(model, order=order) + vpart_fine = sum(vpart, []) + cpart_fine = sum(cpart, []) + vorder = sum(vpart_fine, []) + corder = sum(cpart_fine, []) + + imat = get_structural_incidence_matrix(vorder, corder) + nvar = len(vorder) + ncon = len(corder) + + if ax is None: + fig, ax = plt.subplots() + else: + fig = None + + markersize = spy_kwds.pop("markersize", None) + if markersize is None: + # At 10000 vars/cons, we want markersize=0.2 + # At 20 vars/cons, we want markersize=10 + # We assume we want a linear relationship between 1/nvar + # and the markersize. + markersize = (10.0 - 0.2) / (1 / 20 - 1 / 10000) * ( + 1 / max(nvar, ncon) - 1 / 10000 + ) + 0.2 + + ax.spy(imat, markersize=markersize, **spy_kwds) + ax.tick_params(length=0) + if highlight_coarse: + start = (0, 0) + for i, (vblocks, cblocks) in enumerate(zip(vpart, cpart)): + # Get the total number of variables/constraints in this part + # of the coarse partition + nv = sum(len(vb) for vb in vblocks) + nc = sum(len(cb) for cb in cblocks) + stop = (start[0] + nv - 1, start[1] + nc - 1) + if not (i == 1 and skip_wellconstrained) and nv > 0 and nc > 0: + # Regardless of whether we are plotting in upper or lower + # triangular order, the well-constrained subsystem is at + # position 1 + # + # The get-rectangle function doesn't look good if we give it + # an "empty region" to box. + ax.add_patch( + _get_rectangle_around_coords(start, stop, linewidth=linewidth) + ) + start = (stop[0] + 1, stop[1] + 1) + + if highlight_fine: + # Use dashed lines to distinguish inner from outer partitions + # if we are highlighting both + linestyle = "--" if highlight_coarse else "-" + start = (0, 0) + for vb, cb in zip(vpart_fine, cpart_fine): + stop = (start[0] + len(vb) - 1, start[1] + len(cb) - 1) + # Note that the subset's we're boxing here can't be empty. + ax.add_patch( + _get_rectangle_around_coords( + start, stop, linestyle=linestyle, linewidth=linewidth + ) + ) + start = (stop[0] + 1, stop[1] + 1) + + return fig, ax diff --git a/pyomo/contrib/interior_point/__init__.py b/pyomo/contrib/interior_point/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/interior_point/__init__.py +++ b/pyomo/contrib/interior_point/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/interior_point/examples/__init__.py b/pyomo/contrib/interior_point/examples/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/interior_point/examples/__init__.py +++ b/pyomo/contrib/interior_point/examples/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/interior_point/examples/ex1.py b/pyomo/contrib/interior_point/examples/ex1.py index d9931e1daa8..f6d8f14ac0a 100644 --- a/pyomo/contrib/interior_point/examples/ex1.py +++ b/pyomo/contrib/interior_point/examples/ex1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/interior_point/interface.py b/pyomo/contrib/interior_point/interface.py index 38d91be5566..93b83f385ba 100644 --- a/pyomo/contrib/interior_point/interface.py +++ b/pyomo/contrib/interior_point/interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -258,10 +258,9 @@ def __init__(self, pyomo_model): # set the init_duals_primals_lb/ub from ipopt_zL_out, ipopt_zU_out if available # need to compress them as well and initialize the duals_primals_lb/ub - ( - self._init_duals_primals_lb, - self._init_duals_primals_ub, - ) = self._get_full_duals_primals_bounds() + (self._init_duals_primals_lb, self._init_duals_primals_ub) = ( + self._get_full_duals_primals_bounds() + ) self._init_duals_primals_lb[np.isneginf(self._nlp.primals_lb())] = 0 self._init_duals_primals_ub[np.isinf(self._nlp.primals_ub())] = 0 self._duals_primals_lb = self._init_duals_primals_lb.copy() diff --git a/pyomo/contrib/interior_point/interior_point.py b/pyomo/contrib/interior_point/interior_point.py index 00d26ddef03..502de338fdc 100644 --- a/pyomo/contrib/interior_point/interior_point.py +++ b/pyomo/contrib/interior_point/interior_point.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/interior_point/inverse_reduced_hessian.py b/pyomo/contrib/interior_point/inverse_reduced_hessian.py index 6144a4afeb8..ac3c6a98463 100644 --- a/pyomo/contrib/interior_point/inverse_reduced_hessian.py +++ b/pyomo/contrib/interior_point/inverse_reduced_hessian.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/interior_point/linalg/__init__.py b/pyomo/contrib/interior_point/linalg/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/interior_point/linalg/__init__.py +++ b/pyomo/contrib/interior_point/linalg/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/interior_point/linalg/base_linear_solver_interface.py b/pyomo/contrib/interior_point/linalg/base_linear_solver_interface.py index 722a5c55e8d..c3304fd1395 100644 --- a/pyomo/contrib/interior_point/linalg/base_linear_solver_interface.py +++ b/pyomo/contrib/interior_point/linalg/base_linear_solver_interface.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.contrib.pynumero.linalg.base import DirectLinearSolverInterface from abc import ABCMeta, abstractmethod import logging diff --git a/pyomo/contrib/interior_point/linalg/ma27_interface.py b/pyomo/contrib/interior_point/linalg/ma27_interface.py index 7bb98b0b6fd..7604bd432bb 100644 --- a/pyomo/contrib/interior_point/linalg/ma27_interface.py +++ b/pyomo/contrib/interior_point/linalg/ma27_interface.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from .base_linear_solver_interface import IPLinearSolverInterface from pyomo.contrib.pynumero.linalg.base import LinearSolverStatus, LinearSolverResults from pyomo.contrib.pynumero.linalg.ma27_interface import MA27 diff --git a/pyomo/contrib/interior_point/linalg/mumps_interface.py b/pyomo/contrib/interior_point/linalg/mumps_interface.py index 98f0ef03210..c7480e2b6d0 100644 --- a/pyomo/contrib/interior_point/linalg/mumps_interface.py +++ b/pyomo/contrib/interior_point/linalg/mumps_interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/interior_point/linalg/scipy_interface.py b/pyomo/contrib/interior_point/linalg/scipy_interface.py index b7b7923bad4..d0f773fcb81 100644 --- a/pyomo/contrib/interior_point/linalg/scipy_interface.py +++ b/pyomo/contrib/interior_point/linalg/scipy_interface.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from .base_linear_solver_interface import IPLinearSolverInterface from pyomo.contrib.pynumero.linalg.base import LinearSolverResults from scipy.linalg import eigvals diff --git a/pyomo/contrib/interior_point/linalg/tests/__init__.py b/pyomo/contrib/interior_point/linalg/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/interior_point/linalg/tests/__init__.py +++ b/pyomo/contrib/interior_point/linalg/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/interior_point/linalg/tests/test_linear_solvers.py b/pyomo/contrib/interior_point/linalg/tests/test_linear_solvers.py index 35863aa7cf7..93071a5f215 100644 --- a/pyomo/contrib/interior_point/linalg/tests/test_linear_solvers.py +++ b/pyomo/contrib/interior_point/linalg/tests/test_linear_solvers.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.common.unittest as unittest from pyomo.common.dependencies import attempt_import diff --git a/pyomo/contrib/interior_point/linalg/tests/test_realloc.py b/pyomo/contrib/interior_point/linalg/tests/test_realloc.py index 0b2e449e349..3a53d0e7db9 100644 --- a/pyomo/contrib/interior_point/linalg/tests/test_realloc.py +++ b/pyomo/contrib/interior_point/linalg/tests/test_realloc.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.common.unittest as unittest from pyomo.common.dependencies import attempt_import @@ -44,6 +55,13 @@ def test_reallocate_memory_mumps(self): predicted = linear_solver.get_infog(16) + # We predict that factorization will take 2 MB + self.assertEqual(predicted, 2) + + # Explicitly set maximum memory to less than the predicted + # requirement. + linear_solver.set_icntl(23, 1) + res = linear_solver.do_numeric_factorization(matrix, raise_on_error=False) self.assertEqual(res.status, LinearSolverStatus.not_enough_memory) diff --git a/pyomo/contrib/interior_point/tests/__init__.py b/pyomo/contrib/interior_point/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/interior_point/tests/__init__.py +++ b/pyomo/contrib/interior_point/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/interior_point/tests/test_interior_point.py b/pyomo/contrib/interior_point/tests/test_interior_point.py index bff80934d20..a05408abe1e 100644 --- a/pyomo/contrib/interior_point/tests/test_interior_point.py +++ b/pyomo/contrib/interior_point/tests/test_interior_point.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/interior_point/tests/test_inverse_reduced_hessian.py b/pyomo/contrib/interior_point/tests/test_inverse_reduced_hessian.py index 67657dfce47..61f5e90e3cf 100644 --- a/pyomo/contrib/interior_point/tests/test_inverse_reduced_hessian.py +++ b/pyomo/contrib/interior_point/tests/test_inverse_reduced_hessian.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/interior_point/tests/test_realloc.py b/pyomo/contrib/interior_point/tests/test_realloc.py index 9789c7d3ac0..b7a5d00e488 100644 --- a/pyomo/contrib/interior_point/tests/test_realloc.py +++ b/pyomo/contrib/interior_point/tests/test_realloc.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.common.unittest as unittest import pyomo.environ as pe from pyomo.core.base import ConcreteModel, Var, Constraint, Objective @@ -67,12 +78,22 @@ def test_mumps(self): res = linear_solver.do_symbolic_factorization(kkt) predicted = linear_solver.get_infog(16) + linear_solver.set_icntl(23, 8) + self._test_ip_with_reallocation(linear_solver, interface) + # In Mumps 5.6.2 (and likely previous versions), ICNTL(23)=0 + # corresponds to "use default increase factor over prediction". actual = linear_solver.get_icntl(23) + percent_increase = linear_solver.get_icntl(14) + increase_factor = 1.0 + percent_increase / 100.0 + + if actual == 0: + actual = increase_factor * predicted - self.assertTrue(predicted == 12 or predicted == 11) + # As of Mumps 5.6.2, predicted == 9, which is lower than the + # default actual of 10.8 + # self.assertTrue(predicted == 12 or predicted == 11) self.assertTrue(actual > predicted) - # self.assertEqual(actual, 14) # NOTE: This test will break if Mumps (or your Mumps version) # gets more conservative at estimating memory requirement, # or if the numeric factorization gets more efficient. diff --git a/pyomo/contrib/interior_point/tests/test_reg.py b/pyomo/contrib/interior_point/tests/test_reg.py index b37d9532428..a7fc686545b 100644 --- a/pyomo/contrib/interior_point/tests/test_reg.py +++ b/pyomo/contrib/interior_point/tests/test_reg.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/latex_printer/Readme.md b/pyomo/contrib/latex_printer/Readme.md new file mode 100644 index 00000000000..9b9febf9644 --- /dev/null +++ b/pyomo/contrib/latex_printer/Readme.md @@ -0,0 +1,37 @@ +# Pyomo LaTeX Printer + +This is a prototype latex printer for Pyomo models. DISCLAIMER: The API for the LaTeX printer is not finalized and may have a future breaking change. Use at your own risk. + +## Usage + +```python +import pyomo.environ as pyo +from pyomo.contrib.latex_printer import latex_printer + +m = pyo.ConcreteModel(name = 'basicFormulation') +m.x = pyo.Var() +m.y = pyo.Var() +m.z = pyo.Var() +m.c = pyo.Param(initialize=1.0, mutable=True) +m.objective = pyo.Objective( expr = m.x + m.y + m.z ) +m.constraint_1 = pyo.Constraint(expr = m.x**2 + m.y**2.0 - m.z**2.0 <= m.c ) + +pstr = latex_printer(m) +``` + + +## Acknowledgement + +Pyomo: Python Optimization Modeling Objects +Copyright (c) 2008-2023 +National Technology and Engineering Solutions of Sandia, LLC +Under the terms of Contract DE-NA0003525 with National Technology and +Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +rights in this software. + +Development of this module was conducted as part of the Institute for +the Design of Advanced Energy Systems (IDAES) with support through the +Simulation-Based Engineering, Crosscutting Research Program within the +U.S. Department of Energy’s Office of Fossil Energy and Carbon Management. + +This software is distributed under the 3-clause BSD License. \ No newline at end of file diff --git a/pyomo/contrib/latex_printer/__init__.py b/pyomo/contrib/latex_printer/__init__.py new file mode 100644 index 00000000000..02eaa636a36 --- /dev/null +++ b/pyomo/contrib/latex_printer/__init__.py @@ -0,0 +1,22 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +# Recommended just to build all of the appropriate things +import pyomo.environ + +# Remove one layer of .latex_printer +# import statement is now: +# from pyomo.contrib.latex_printer import latex_printer +try: + from pyomo.contrib.latex_printer.latex_printer import latex_printer +except: + pass + # in this case, the dependencies are not installed, nothing will work diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py new file mode 100644 index 00000000000..cf286472a66 --- /dev/null +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -0,0 +1,1356 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import math +import copy +import re +import io +import pyomo.environ as pyo +from pyomo.core.expr.visitor import StreamBasedExpressionVisitor +from pyomo.core.expr import ( + NegationExpression, + ProductExpression, + DivisionExpression, + PowExpression, + AbsExpression, + UnaryFunctionExpression, + MonomialTermExpression, + LinearExpression, + SumExpression, + EqualityExpression, + InequalityExpression, + RangedExpression, + Expr_ifExpression, + ExternalFunctionExpression, +) + +from pyomo.core.expr.visitor import identify_components +from pyomo.core.expr.base import ExpressionBase +from pyomo.core.base.expression import ScalarExpression, ExpressionData +from pyomo.core.base.objective import ScalarObjective, ObjectiveData +import pyomo.core.kernel as kernel +from pyomo.core.expr.template_expr import ( + GetItemExpression, + GetAttrExpression, + TemplateSumExpression, + IndexTemplate, + Numeric_GetItemExpression, + templatize_constraint, + resolve_template, + templatize_rule, +) +from pyomo.core.base.var import ScalarVar, VarData, IndexedVar +from pyomo.core.base.param import ParamData, ScalarParam, IndexedParam +from pyomo.core.base.set import SetData, SetOperator +from pyomo.core.base.constraint import ScalarConstraint, IndexedConstraint +from pyomo.common.collections.component_map import ComponentMap +from pyomo.common.collections.component_set import ComponentSet +from pyomo.core.expr.template_expr import ( + NPV_Numeric_GetItemExpression, + NPV_Structural_GetItemExpression, + Numeric_GetAttrExpression, +) +from pyomo.core.expr.numeric_expr import NPV_SumExpression, NPV_DivisionExpression +from pyomo.core.base.block import IndexedBlock + +from pyomo.core.base.external import _PythonCallbackFunctionID +from pyomo.core.base.enums import SortComponents + +from pyomo.core.base.block import BlockData + +from pyomo.repn.util import ExprType + +from pyomo.common import DeveloperError + +_CONSTANT = ExprType.CONSTANT +_MONOMIAL = ExprType.MONOMIAL +_GENERAL = ExprType.GENERAL + +from pyomo.common.errors import InfeasibleConstraintException + +from pyomo.common.dependencies import numpy as np, numpy_available + + +set_operator_map = { + '|': r' \cup ', + '&': r' \cap ', + '*': r' \times ', + '-': r' \setminus ', + '^': r' \triangle ', +} + +latex_reals = r'\mathds{R}' +latex_integers = r'\mathds{Z}' + +domainMap = { + 'Reals': latex_reals, + 'PositiveReals': latex_reals + '_{> 0}', + 'NonPositiveReals': latex_reals + '_{\\leq 0}', + 'NegativeReals': latex_reals + '_{< 0}', + 'NonNegativeReals': latex_reals + '_{\\geq 0}', + 'Integers': latex_integers, + 'PositiveIntegers': latex_integers + '_{> 0}', + 'NonPositiveIntegers': latex_integers + '_{\\leq 0}', + 'NegativeIntegers': latex_integers + '_{< 0}', + 'NonNegativeIntegers': latex_integers + '_{\\geq 0}', + 'Boolean': '\\left\\{ \\text{True} , \\text{False} \\right \\}', + 'Binary': '\\left\\{ 0 , 1 \\right \\}', + # 'Any': None, + # 'AnyWithNone': None, + 'EmptySet': '\\varnothing', + 'UnitInterval': latex_reals, + 'PercentFraction': latex_reals, + # 'RealInterval' : None , + # 'IntegerInterval' : None , +} + + +def decoder(num, base): + if int(num) != abs(num): + # Requiring an integer is nice, but not strictly necessary; + # the algorithm works for floating point + raise ValueError("num should be a nonnegative integer") + if int(base) != abs(base) or not base: + raise ValueError("base should be a positive integer") + ans = [] + while 1: + ans.append(num % base) + num //= base + if not num: + return list(reversed(ans)) + + +def indexCorrector(ixs, base): + for i in range(0, len(ixs)): + ix = ixs[i] + if i + 1 < len(ixs): + if ixs[i + 1] == 0: + ixs[i] -= 1 + ixs[i + 1] = base + if ixs[i] == 0: + ixs = indexCorrector(ixs, base) + return ixs + + +def alphabetStringGenerator(num): + alphabet = ['.', 'i', 'j', 'k', 'm', 'n', 'p', 'q', 'r'] + + ixs = decoder(num + 1, len(alphabet) - 1) + pstr = '' + ixs = indexCorrector(ixs, len(alphabet) - 1) + for i in range(0, len(ixs)): + ix = ixs[i] + pstr += alphabet[ix] + pstr = pstr.replace('.', '') + return pstr + + +def templatize_expression(expr): + expr, indices = templatize_rule(expr.parent_block(), expr._rule, expr.index_set()) + return (expr, indices) + + +def templatize_passthrough(con): + return (con, []) + + +def precedenceChecker(node, arg1, arg2=None): + childPrecedence = [] + for a in node.args: + if hasattr(a, 'PRECEDENCE'): + if a.PRECEDENCE is None: + childPrecedence.append(-1) + else: + childPrecedence.append(a.PRECEDENCE) + else: + childPrecedence.append(-1) + + if hasattr(node, 'PRECEDENCE'): + precedence = node.PRECEDENCE + else: + # Should never hit this + raise DeveloperError( + 'This error should never be thrown, node does not have a precedence. Report to developers' + ) + + if childPrecedence[0] > precedence: + arg1 = ' \\left( ' + arg1 + ' \\right) ' + + if arg2 is not None: + if childPrecedence[1] > precedence: + arg2 = ' \\left( ' + arg2 + ' \\right) ' + + return arg1, arg2 + + +def handle_negation_node(visitor, node, arg1): + arg1, tsh = precedenceChecker(node, arg1) + return '-' + arg1 + + +def handle_product_node(visitor, node, arg1, arg2): + arg1, arg2 = precedenceChecker(node, arg1, arg2) + return ' '.join([arg1, arg2]) + + +def handle_pow_node(visitor, node, arg1, arg2): + arg1, arg2 = precedenceChecker(node, arg1, arg2) + return "%s^{%s}" % (arg1, arg2) + + +def handle_division_node(visitor, node, arg1, arg2): + return '\\frac{%s}{%s}' % (arg1, arg2) + + +def handle_abs_node(visitor, node, arg1): + return ' \\left| ' + arg1 + ' \\right| ' + + +def handle_unary_node(visitor, node, arg1): + fcn_handle = node.getname() + if fcn_handle == 'log10': + fcn_handle = 'log_{10}' + + if fcn_handle == 'sqrt': + return '\\sqrt { ' + arg1 + ' }' + else: + return '\\' + fcn_handle + ' \\left( ' + arg1 + ' \\right) ' + + +def handle_equality_node(visitor, node, arg1, arg2): + return arg1 + ' = ' + arg2 + + +def handle_inequality_node(visitor, node, arg1, arg2): + return arg1 + ' \\leq ' + arg2 + + +def handle_var_node(visitor, node): + return visitor.variableMap[node] + + +def handle_num_node(visitor, node): + if isinstance(node, float): + if node.is_integer(): + node = int(node) + return str(node) + + +def handle_sumExpression_node(visitor, node, *args): + rstr = args[0] + for i in range(1, len(args)): + if args[i][0] == '-': + rstr += ' - ' + args[i][1:] + else: + rstr += ' + ' + args[i] + return rstr + + +def handle_monomialTermExpression_node(visitor, node, arg1, arg2): + if arg1 == '1': + return arg2 + elif arg1 == '-1': + return '-' + arg2 + else: + return arg1 + ' ' + arg2 + + +def handle_named_expression_node(visitor, node, arg1): + # needed to preserve consistency with the exitNode function call + # prevents the need to type check in the exitNode function + return arg1 + + +def handle_ranged_inequality_node(visitor, node, arg1, arg2, arg3): + return arg1 + ' \\leq ' + arg2 + ' \\leq ' + arg3 + + +def handle_exprif_node(visitor, node, arg1, arg2, arg3): + return 'f_{\\text{exprIf}}(' + arg1 + ',' + arg2 + ',' + arg3 + ')' + + ## Could be handled in the future using cases or similar + + ## Raises not implemented error + # raise NotImplementedError('Expr_if objects not supported by the Latex Printer') + + ## Puts cases in a bracketed matrix + # pstr = '' + # pstr += '\\begin{Bmatrix} ' + # pstr += arg2 + ' , & ' + arg1 + '\\\\ ' + # pstr += arg3 + ' , & \\text{otherwise}' + '\\\\ ' + # pstr += '\\end{Bmatrix}' + # return pstr + + +def handle_external_function_node(visitor, node, *args): + pstr = '' + visitor.externalFunctionCounter += 1 + pstr += 'f\\_' + str(visitor.externalFunctionCounter) + '(' + for i in range(0, len(args) - 1): + pstr += args[i] + if i <= len(args) - 3: + pstr += ',' + else: + pstr += ')' + return pstr + + +def handle_functionID_node(visitor, node, *args): + # seems to just be a placeholder empty wrapper object + return '' + + +def handle_indexTemplate_node(visitor, node, *args): + if node._set in visitor.setMap: + # already detected set, do nothing + pass + else: + visitor.setMap[node._set] = 'SET%d' % (len(visitor.setMap) + 1) + + return '__I_PLACEHOLDER_8675309_GROUP_%s_%s_%s__' % ( + node._group, + node._id, + visitor.setMap[node._set], + ) + + +def handle_numericGetItemExpression_node(visitor, node, *args): + joinedName = args[0] + + pstr = '' + pstr += joinedName + '_{' + for i in range(1, len(args)): + pstr += args[i] + if i <= len(args) - 2: + pstr += ',' + else: + pstr += '}' + return pstr + + +def handle_templateSumExpression_node(visitor, node, *args): + pstr = '' + for i in range(0, len(node._iters)): + pstr += '\\sum_{__S_PLACEHOLDER_8675309_GROUP_%s_%s_%s__} ' % ( + node._iters[i][0]._group, + ','.join(str(it._id) for it in node._iters[i]), + visitor.setMap[node._iters[i][0]._set], + ) + + pstr += args[0] + + return pstr + + +def handle_param_node(visitor, node): + return visitor.parameterMap[node] + + +def handle_str_node(visitor, node): + return "\\mathtt{'" + node.replace('_', '\\_') + "'}" + + +def handle_npv_structuralGetItemExpression_node(visitor, node, *args): + joinedName = args[0] + + pstr = '' + pstr += joinedName + '[' + for i in range(1, len(args)): + pstr += args[i] + if i <= len(args) - 2: + pstr += ',' + else: + pstr += ']' + return pstr + + +def handle_indexedBlock_node(visitor, node, *args): + return str(node) + + +def handle_numericGetAttrExpression_node(visitor, node, *args): + return args[0] + '.' + args[1] + + +class _LatexVisitor(StreamBasedExpressionVisitor): + def __init__(self): + super().__init__() + self.externalFunctionCounter = 0 + + self._operator_handles = { + ScalarVar: handle_var_node, + int: handle_num_node, + float: handle_num_node, + NegationExpression: handle_negation_node, + ProductExpression: handle_product_node, + DivisionExpression: handle_division_node, + PowExpression: handle_pow_node, + AbsExpression: handle_abs_node, + UnaryFunctionExpression: handle_unary_node, + Expr_ifExpression: handle_exprif_node, + EqualityExpression: handle_equality_node, + InequalityExpression: handle_inequality_node, + RangedExpression: handle_ranged_inequality_node, + ExpressionData: handle_named_expression_node, + ScalarExpression: handle_named_expression_node, + kernel.expression.expression: handle_named_expression_node, + kernel.expression.noclone: handle_named_expression_node, + ObjectiveData: handle_named_expression_node, + VarData: handle_var_node, + ScalarObjective: handle_named_expression_node, + kernel.objective.objective: handle_named_expression_node, + ExternalFunctionExpression: handle_external_function_node, + _PythonCallbackFunctionID: handle_functionID_node, + LinearExpression: handle_sumExpression_node, + SumExpression: handle_sumExpression_node, + MonomialTermExpression: handle_monomialTermExpression_node, + IndexedVar: handle_var_node, + IndexTemplate: handle_indexTemplate_node, + Numeric_GetItemExpression: handle_numericGetItemExpression_node, + TemplateSumExpression: handle_templateSumExpression_node, + ScalarParam: handle_param_node, + ParamData: handle_param_node, + IndexedParam: handle_param_node, + NPV_Numeric_GetItemExpression: handle_numericGetItemExpression_node, + IndexedBlock: handle_indexedBlock_node, + NPV_Structural_GetItemExpression: handle_npv_structuralGetItemExpression_node, + str: handle_str_node, + Numeric_GetAttrExpression: handle_numericGetAttrExpression_node, + NPV_SumExpression: handle_sumExpression_node, + NPV_DivisionExpression: handle_division_node, + } + if numpy_available: + self._operator_handles[np.float64] = handle_num_node + + def exitNode(self, node, data): + try: + return self._operator_handles[node.__class__](self, node, *data) + except: + raise DeveloperError( + 'Latex printer encountered an error when processing type %s, contact the developers' + % (node.__class__) + ) + + +def analyze_variable(vr): + domainName = vr.domain.name + varBounds = vr.bounds + lowerBoundValue = varBounds[0] + upperBoundValue = varBounds[1] + + if domainName in ['Reals', 'Integers']: + if lowerBoundValue is not None: + lowerBound = str(lowerBoundValue) + ' \\leq ' + else: + lowerBound = '' + + if upperBoundValue is not None: + upperBound = ' \\leq ' + str(upperBoundValue) + else: + upperBound = '' + + elif domainName in ['PositiveReals', 'PositiveIntegers']: + if lowerBoundValue > 0: + lowerBound = str(lowerBoundValue) + ' \\leq ' + else: + lowerBound = ' 0 < ' + + if upperBoundValue is not None: + if upperBoundValue <= 0: + raise InfeasibleConstraintException( + 'Formulation is infeasible due to bounds on variable %s' % (vr.name) + ) + else: + upperBound = ' \\leq ' + str(upperBoundValue) + else: + upperBound = '' + + elif domainName in ['NonPositiveReals', 'NonPositiveIntegers']: + if lowerBoundValue is not None: + if lowerBoundValue > 0: + raise InfeasibleConstraintException( + 'Formulation is infeasible due to bounds on variable %s' % (vr.name) + ) + elif lowerBoundValue == 0: + lowerBound = ' 0 = ' + else: + lowerBound = str(lowerBoundValue) + ' \\leq ' + else: + lowerBound = '' + + if upperBoundValue >= 0: + upperBound = ' \\leq 0 ' + else: + upperBound = ' \\leq ' + str(upperBoundValue) + + elif domainName in ['NegativeReals', 'NegativeIntegers']: + if lowerBoundValue is not None: + if lowerBoundValue >= 0: + raise InfeasibleConstraintException( + 'Formulation is infeasible due to bounds on variable %s' % (vr.name) + ) + else: + lowerBound = str(lowerBoundValue) + ' \\leq ' + else: + lowerBound = '' + + if upperBoundValue >= 0: + upperBound = ' < 0 ' + else: + upperBound = ' \\leq ' + str(upperBoundValue) + + elif domainName in ['NonNegativeReals', 'NonNegativeIntegers']: + if lowerBoundValue > 0: + lowerBound = str(lowerBoundValue) + ' \\leq ' + else: + lowerBound = ' 0 \\leq ' + + if upperBoundValue is not None: + if upperBoundValue < 0: + raise InfeasibleConstraintException( + 'Formulation is infeasible due to bounds on variable %s' % (vr.name) + ) + elif upperBoundValue == 0: + upperBound = ' = 0 ' + else: + upperBound = ' \\leq ' + str(upperBoundValue) + else: + upperBound = '' + + elif domainName in ['Boolean', 'Binary', 'Any', 'AnyWithNone', 'EmptySet']: + lowerBound = '' + upperBound = '' + + elif domainName in ['UnitInterval', 'PercentFraction']: + if lowerBoundValue > 1: + raise InfeasibleConstraintException( + 'Formulation is infeasible due to bounds on variable %s' % (vr.name) + ) + elif lowerBoundValue == 1: + lowerBound = ' = 1 ' + elif lowerBoundValue > 0: + lowerBound = str(lowerBoundValue) + ' \\leq ' + else: + lowerBound = ' 0 \\leq ' + + if upperBoundValue < 0: + raise InfeasibleConstraintException( + 'Formulation is infeasible due to bounds on variable %s' % (vr.name) + ) + elif upperBoundValue == 0: + upperBound = ' = 0 ' + elif upperBoundValue < 1: + upperBound = ' \\leq ' + str(upperBoundValue) + else: + upperBound = ' \\leq 1 ' + + else: + raise NotImplementedError( + 'Invalid domain encountered, will be supported in a future update' + ) + + varBoundData = { + 'variable': vr, + 'lowerBound': lowerBound, + 'upperBound': upperBound, + 'domainName': domainName, + 'domainLatex': domainMap[domainName], + } + + return varBoundData + + +def multiple_replace(pstr, rep_dict): + pattern = re.compile("|".join(rep_dict.keys()), flags=re.DOTALL) + return pattern.sub(lambda x: rep_dict[x.group(0)], pstr) + + +def latex_printer( + pyomo_component, + latex_component_map=None, + ostream=None, + use_equation_environment=False, + explicit_set_summation=False, + throw_templatization_error=False, +): + """This function produces a string that can be rendered as LaTeX + + Prints a Pyomo component (Block, Model, Objective, Constraint, or Expression) to a LaTeX compatible string + + Parameters + ---------- + pyomo_component: BlockData or Model or Objective or Constraint or Expression + The Pyomo component to be printed + + latex_component_map: pyomo.common.collections.component_map.ComponentMap + A map keyed by Pyomo component, values become the LaTeX representation in + the printer + + ostream: io.TextIOWrapper or io.StringIO or str + The object to print the LaTeX string to. Can be an open file object, + string I/O object, or a string for a filename to write to + + use_equation_environment: bool + If False, the equation/aligned construction is used to create a single + LaTeX equation. If True, then the align environment is used in LaTeX and + each constraint and objective will be given an individual equation number + + explicit_set_summation: bool + If False, all sums will be done over 'index in set' or similar. If True, + sums will be done over 'i=1' to 'N' or similar if the set is a continuous + set + + throw_templatization_error: bool + Option to throw an error on templatization failure rather than + printing each constraint individually, useful for very large models + + + Returns + ------- + str + A LaTeX string of the pyomo_component + + """ + + # Various setup things + + # is Single implies Objective, constraint, or expression + # these objects require a slight modification of behavior + # isSingle==False means a model or block + + use_short_descriptors = True + + # Cody's backdoor because he got outvoted + if latex_component_map is not None: + if 'use_short_descriptors' in latex_component_map: + if latex_component_map['use_short_descriptors'] == False: + use_short_descriptors = False + + if latex_component_map is None: + latex_component_map = ComponentMap() + existing_components = ComponentSet() + else: + existing_components = ComponentSet(latex_component_map) + + isSingle = False + + if isinstance(pyomo_component, pyo.Objective): + objectives = [pyomo_component] + constraints = [] + expressions = [] + templatize_fcn = templatize_constraint + use_equation_environment = True + isSingle = True + + elif isinstance(pyomo_component, pyo.Constraint): + objectives = [] + constraints = [pyomo_component] + expressions = [] + templatize_fcn = templatize_constraint + use_equation_environment = True + isSingle = True + + elif isinstance(pyomo_component, pyo.Expression): + objectives = [] + constraints = [] + expressions = [pyomo_component] + templatize_fcn = templatize_expression + use_equation_environment = True + isSingle = True + + elif isinstance(pyomo_component, (ExpressionBase, pyo.Var)): + objectives = [] + constraints = [] + expressions = [pyomo_component] + templatize_fcn = templatize_passthrough + use_equation_environment = True + isSingle = True + + elif isinstance(pyomo_component, BlockData): + objectives = [ + obj + for obj in pyomo_component.component_data_objects( + pyo.Objective, + descend_into=True, + active=True, + sort=SortComponents.deterministic, + ) + ] + constraints = [ + con + for con in pyomo_component.component_objects( + pyo.Constraint, + descend_into=True, + active=True, + sort=SortComponents.deterministic, + ) + ] + expressions = [] + templatize_fcn = templatize_constraint + + else: + raise ValueError( + "Invalid type %s passed into the latex printer" + % (str(type(pyomo_component))) + ) + + if isSingle: + temp_comp, temp_indexes = templatize_fcn(pyomo_component) + variableList = [] + for v in identify_components(temp_comp, [ScalarVar, VarData, IndexedVar]): + if isinstance(v, VarData): + v_write = v.parent_component() + if v_write not in ComponentSet(variableList): + variableList.append(v_write) + else: + if v not in ComponentSet(variableList): + variableList.append(v) + + parameterList = [] + for p in identify_components(temp_comp, [ScalarParam, ParamData, IndexedParam]): + if isinstance(p, ParamData): + p_write = p.parent_component() + if p_write not in ComponentSet(parameterList): + parameterList.append(p_write) + else: + if p not in ComponentSet(parameterList): + parameterList.append(p) + + # Will grab the sets as the expression is walked + setList = [] + + else: + variableList = [ + vr + for vr in pyomo_component.component_objects( + pyo.Var, + descend_into=True, + active=True, + sort=SortComponents.deterministic, + ) + ] + + parameterList = [ + pm + for pm in pyomo_component.component_objects( + pyo.Param, + descend_into=True, + active=True, + sort=SortComponents.deterministic, + ) + ] + + setList = [ + st + for st in pyomo_component.component_objects( + pyo.Set, + descend_into=True, + active=True, + sort=SortComponents.deterministic, + ) + ] + + descriptorDict = {} + if use_short_descriptors: + descriptorDict['minimize'] = '\\min' + descriptorDict['maximize'] = '\\max' + descriptorDict['subject to'] = '\\text{s.t.}' + descriptorDict['with bounds'] = '\\text{w.b.}' + else: + descriptorDict['minimize'] = '\\text{minimize}' + descriptorDict['maximize'] = '\\text{maximize}' + descriptorDict['subject to'] = '\\text{subject to}' + descriptorDict['with bounds'] = '\\text{with bounds}' + + # In the case where just a single expression is passed, add this to the constraint list for printing + constraints = constraints + expressions + + # Declare a visitor/walker + visitor = _LatexVisitor() + + variableMap = ComponentMap() + vrIdx = 0 + for vr in variableList: + vrIdx += 1 + if isinstance(vr, ScalarVar): + variableMap[vr] = 'x_' + str(vrIdx) + '_' + elif isinstance(vr, IndexedVar): + variableMap[vr] = 'x_' + str(vrIdx) + '_' + for sd in vr.index_set().data(): + vrIdx += 1 + variableMap[vr[sd]] = 'x_' + str(vrIdx) + '_' + else: + raise DeveloperError( + 'Variable is not a variable. Should not happen. Contact developers' + ) + visitor.variableMap = variableMap + + parameterMap = ComponentMap() + pmIdx = 0 + for vr in parameterList: + pmIdx += 1 + if isinstance(vr, ScalarParam): + parameterMap[vr] = 'p_' + str(pmIdx) + '_' + elif isinstance(vr, IndexedParam): + parameterMap[vr] = 'p_' + str(pmIdx) + '_' + for sd in vr.index_set().data(): + pmIdx += 1 + parameterMap[vr[sd]] = 'p_' + str(pmIdx) + '_' + else: + raise DeveloperError( + 'Parameter is not a parameter. Should not happen. Contact developers' + ) + visitor.parameterMap = parameterMap + + setMap = ComponentMap() + for i in range(0, len(setList)): + st = setList[i] + setMap[st] = 'SET' + str(i + 1) + visitor.setMap = setMap + + # starts building the output string + pstr = '' + if not use_equation_environment: + pstr += '\\begin{align} \n' + tbSpc = 4 + trailingAligner = '& ' + else: + pstr += '\\begin{equation} \n' + if not isSingle: + pstr += ' \\begin{aligned} \n' + tbSpc = 8 + else: + tbSpc = 4 + trailingAligner = '' + + # Iterate over the objectives and print + for obj in objectives: + try: + obj_template, obj_indices = templatize_fcn(obj) + except: + if throw_templatization_error: + raise RuntimeError( + "An objective named '%s' has been constructed that cannot be templatized" + % (obj.__str__()) + ) + else: + obj_template = obj + + if obj.sense == pyo.minimize: # or == 1 + pstr += ' ' * tbSpc + '& %s \n' % (descriptorDict['minimize']) + else: + pstr += ' ' * tbSpc + '& %s \n' % (descriptorDict['maximize']) + + pstr += ' ' * tbSpc + '& & %s %s' % ( + visitor.walk_expression(obj_template), + trailingAligner, + ) + if not use_equation_environment: + pstr += '\\label{obj:' + pyomo_component.name + '_' + obj.name + '} ' + if not isSingle: + pstr += '\\\\ \n' + else: + pstr += '\n' + + # Iterate over the constraints + if len(constraints) > 0: + # only print this if printing a full formulation + if not isSingle: + pstr += ' ' * tbSpc + '& %s \n' % (descriptorDict['subject to']) + + # first constraint needs different alignment because of the 'subject to': + # & minimize & & [Objective] + # & subject to & & [Constraint 1] + # & & & [Constraint 2] + # & & & [Constraint N] + + # The double '& &' renders better for some reason + + for i, con in enumerate(constraints): + if not isSingle: + if i == 0: + algn = '& &' + else: + algn = '&&&' + else: + algn = '' + + if not isSingle: + tail = '\\\\ \n' + else: + tail = '\n' + + # grab the constraint and templatize + try: + con_template, indices = templatize_fcn(con) + con_template_list = [con_template] + except: + if throw_templatization_error: + raise RuntimeError( + "A constraint named '%s' has been constructed that cannot be templatized" + % (con.__str__()) + ) + else: + con_template_list = [c.expr for c in con.values()] + indices = [] + + for con_template in con_template_list: + # Walk the constraint + conLine = ( + ' ' * tbSpc + + algn + + ' %s %s' + % (visitor.walk_expression(con_template), trailingAligner) + ) + + # setMap = visitor.setMap + # Multiple constraints are generated using a set + if len(indices) > 0: + conLine += ' \\qquad \\forall' + + _bygroups = {} + for idx in indices: + _bygroups.setdefault(idx._group, []).append(idx) + for _group, idxs in _bygroups.items(): + if idxs[0]._set in visitor.setMap: + # already detected set, do nothing + pass + else: + visitor.setMap[idxs[0]._set] = 'SET%d' % ( + len(visitor.setMap) + 1 + ) + + idxTag = ','.join( + '__I_PLACEHOLDER_8675309_GROUP_%s_%s_%s__' + % (idx._group, idx._id, visitor.setMap[idx._set]) + for idx in idxs + ) + + setTag = '__S_PLACEHOLDER_8675309_GROUP_%s_%s_%s__' % ( + indices[0]._group, + ','.join(str(it._id) for it in idxs), + visitor.setMap[indices[0]._set], + ) + + conLine += ' %s \\in %s ' % (idxTag, setTag) + pstr += conLine + + # Add labels as needed + if not use_equation_environment: + pstr += ( + '\\label{con:' + pyomo_component.name + '_' + con.name + '} ' + ) + + pstr += tail + + # Print bounds and sets + if not isSingle: + varBoundData = [] + for i in range(0, len(variableList)): + vr = variableList[i] + if isinstance(vr, ScalarVar): + varBoundDataEntry = analyze_variable(vr) + varBoundData.append(varBoundDataEntry) + elif isinstance(vr, IndexedVar): + varBoundData_indexedVar = [] + setData = vr.index_set().data() + for sd in setData: + varBoundDataEntry = analyze_variable(vr[sd]) + varBoundData_indexedVar.append(varBoundDataEntry) + globIndexedVariables = True + for j in range(0, len(varBoundData_indexedVar) - 1): + chks = [] + chks.append( + varBoundData_indexedVar[j]['lowerBound'] + == varBoundData_indexedVar[j + 1]['lowerBound'] + ) + chks.append( + varBoundData_indexedVar[j]['upperBound'] + == varBoundData_indexedVar[j + 1]['upperBound'] + ) + chks.append( + varBoundData_indexedVar[j]['domainName'] + == varBoundData_indexedVar[j + 1]['domainName'] + ) + if not all(chks): + globIndexedVariables = False + break + if globIndexedVariables: + varBoundData.append( + { + 'variable': vr, + 'lowerBound': varBoundData_indexedVar[0]['lowerBound'], + 'upperBound': varBoundData_indexedVar[0]['upperBound'], + 'domainName': varBoundData_indexedVar[0]['domainName'], + 'domainLatex': varBoundData_indexedVar[0]['domainLatex'], + } + ) + else: + varBoundData += varBoundData_indexedVar + else: + raise DeveloperError( + 'Variable is not a variable. Should not happen. Contact developers' + ) + + # print the accumulated data to the string + bstr = '' + appendBoundString = False + useThreeAlgn = False + for i, vbd in enumerate(varBoundData): + if ( + vbd['lowerBound'] == '' + and vbd['upperBound'] == '' + and vbd['domainName'] == 'Reals' + ): + # unbounded all real, do not print + if i == len(varBoundData) - 1: + bstr = bstr[0:-4] + else: + if not useThreeAlgn: + algn = '& &' + useThreeAlgn = True + else: + algn = '&&&' + + if use_equation_environment: + conLabel = '' + else: + conLabel = ( + ' \\label{con:' + + pyomo_component.name + + '_' + + variableMap[vbd['variable']] + + '_bound' + + '} ' + ) + + appendBoundString = True + coreString = ( + vbd['lowerBound'] + + variableMap[vbd['variable']] + + vbd['upperBound'] + + ' ' + + trailingAligner + + '\\qquad \\in ' + + vbd['domainLatex'] + + conLabel + ) + bstr += ' ' * tbSpc + algn + ' %s' % (coreString) + if i <= len(varBoundData) - 2: + bstr += '\\\\ \n' + else: + bstr += '\n' + + if appendBoundString: + pstr += ' ' * tbSpc + '& %s \n' % (descriptorDict['with bounds']) + pstr += bstr + '\n' + else: + pstr = pstr[0:-4] + '\n' + + # close off the print string + if not use_equation_environment: + pstr += '\\end{align} \n' + else: + if not isSingle: + pstr += ' \\end{aligned} \n' + pstr += ' \\label{%s} \n' % (pyomo_component.name) + pstr += '\\end{equation} \n' + + setMap = visitor.setMap + setMap_inverse = {vl: ky for ky, vl in setMap.items()} + + def generate_set_name(st, lcm): + if st in lcm: + return lcm[st][0] + if st.parent_block().component(st.name) is st: + return st.name.replace('_', r'\_') + if isinstance(st, SetOperator): + return set_operator_map[st._operator.strip()].join( + generate_set_name(s, lcm) for s in st.subsets(False) + ) + else: + return str(st).replace('_', r'\_').replace('{', r'\{').replace('}', r'\}') + + # Handling the iterator indices + defaultSetLatexNames = ComponentMap() + for ky in setMap: + defaultSetLatexNames[ky] = generate_set_name(ky, latex_component_map) + + latexLines = pstr.split('\n') + for jj in range(0, len(latexLines)): + groupMap = {} + uniqueSets = [] + ln = latexLines[jj] + # only modify if there is a placeholder in the line + if "PLACEHOLDER_8675309_GROUP_" in ln: + splitLatex = ln.split('__') + # Find the unique combinations of group numbers and set names + for word in splitLatex: + if "PLACEHOLDER_8675309_GROUP_" in word: + ifo = word.split("PLACEHOLDER_8675309_GROUP_")[1] + gpNum, idNum, stName = ifo.split('_') + if gpNum not in groupMap: + groupMap[gpNum] = [stName] + if stName not in ComponentSet(uniqueSets): + uniqueSets.append(stName) + + # Determine if the set is continuous + setInfo = dict( + zip( + uniqueSets, + [{'continuous': False} for i in range(0, len(uniqueSets))], + ) + ) + + for ky, vl in setInfo.items(): + ix = int(ky[3:]) - 1 + setInfo[ky]['setObject'] = setMap_inverse[ky] # setList[ix] + setInfo[ky]['setRegEx'] = ( + r'__S_PLACEHOLDER_8675309_GROUP_([0-9]+)_([0-9,]+)_%s__' % (ky,) + ) + # setInfo[ky]['idxRegEx'] = r'__I_PLACEHOLDER_8675309_GROUP_[0-9*]_%s__'%(ky) + + if explicit_set_summation: + for ky, vl in setInfo.items(): + st = vl['setObject'] + stData = st.data() + stCont = True + for ii in range(0, len(stData)): + if ii + stData[0] != stData[ii]: + stCont = False + break + setInfo[ky]['continuous'] = stCont + + # replace the sets + for ky, vl in setInfo.items(): + # if the set is continuous and the flag has been set + if explicit_set_summation and setInfo[ky]['continuous']: + st = setInfo[ky]['setObject'] + stData = st.data() + bgn = stData[0] + ed = stData[-1] + + replacement = ( + r'sum_{ __I_PLACEHOLDER_8675309_GROUP_\1_\2_%s__ = %d }^{%d}' + % (ky, bgn, ed) + ) + ln = re.sub( + 'sum_{' + setInfo[ky]['setRegEx'] + '}', replacement, ln + ) + else: + # if the set is not continuous or the flag has not been set + for _grp, _id in re.findall( + 'sum_{' + setInfo[ky]['setRegEx'] + '}', ln + ): + set_placeholder = '__S_PLACEHOLDER_8675309_GROUP_%s_%s_%s__' % ( + _grp, + _id, + ky, + ) + i_placeholder = ','.join( + '__I_PLACEHOLDER_8675309_GROUP_%s_%s_%s__' % (_grp, _, ky) + for _ in _id.split(',') + ) + replacement = r'sum_{ %s \in %s }' % ( + i_placeholder, + set_placeholder, + ) + ln = ln.replace('sum_{' + set_placeholder + '}', replacement) + + replacement = repr(defaultSetLatexNames[setInfo[ky]['setObject']])[1:-1] + ln = re.sub(setInfo[ky]['setRegEx'], replacement, ln) + + # groupNumbers = re.findall(r'__I_PLACEHOLDER_8675309_GROUP_([0-9*])_SET[0-9]*__',ln) + setNumbers = re.findall( + r'__I_PLACEHOLDER_8675309_GROUP_[0-9]+_[0-9]+_SET([0-9]+)__', ln + ) + groupIdSetTuples = re.findall( + r'__I_PLACEHOLDER_8675309_GROUP_([0-9]+)_([0-9]+)_SET([0-9]+)__', ln + ) + + groupInfo = {} + for vl in setNumbers: + groupInfo['SET' + vl] = { + 'setObject': setInfo['SET' + vl]['setObject'], + 'indices': [], + } + + for _gp, _id, _set in groupIdSetTuples: + if (_gp, _id) not in groupInfo['SET' + _set]['indices']: + groupInfo['SET' + _set]['indices'].append((_gp, _id)) + + def get_index_names(st, lcm): + if st in lcm: + return lcm[st][1] + elif isinstance(st, SetOperator): + return sum( + (get_index_names(s, lcm) for s in st.subsets(False)), start=[] + ) + elif st.dimen is not None: + return [None] * st.dimen + else: + return [Ellipsis] + + indexCounter = 0 + for ky, vl in groupInfo.items(): + indexNames = get_index_names(vl['setObject'], latex_component_map) + nonNone = list(filter(None, indexNames)) + if nonNone: + if len(nonNone) < len(vl['indices']): + raise ValueError( + 'Insufficient number of indices provided to the ' + 'overwrite dictionary for set %s (expected %s, but got %s)' + % (vl['setObject'].name, len(vl['indices']), indexNames) + ) + else: + indexNames = [] + for i in vl['indices']: + indexNames.append(alphabetStringGenerator(indexCounter)) + indexCounter += 1 + for i in range(0, len(vl['indices'])): + ln = ln.replace( + '__I_PLACEHOLDER_8675309_GROUP_%s_%s_%s__' + % (*vl['indices'][i], ky), + indexNames[i], + ) + latexLines[jj] = ln + + pstr = '\n'.join(latexLines) + + new_variableMap = ComponentMap() + for i, vr in enumerate(variableList): + if isinstance(vr, ScalarVar): + new_variableMap[vr] = vr.name + elif isinstance(vr, IndexedVar): + new_variableMap[vr] = vr.name + for sd in vr.index_set().data(): + sdString = str(sd) + if sdString[0] == '(': + sdString = sdString[1:] + if sdString[-1] == ')': + sdString = sdString[0:-1] + new_variableMap[vr[sd]] = vr[sd].name + else: + raise DeveloperError( + 'Variable is not a variable. Should not happen. Contact developers' + ) + + new_parameterMap = ComponentMap() + for i, pm in enumerate(parameterList): + pm = parameterList[i] + if isinstance(pm, ScalarParam): + new_parameterMap[pm] = pm.name + elif isinstance(pm, IndexedParam): + new_parameterMap[pm] = pm.name + for sd in pm.index_set().data(): + sdString = str(sd) + if sdString[0] == '(': + sdString = sdString[1:] + if sdString[-1] == ')': + sdString = sdString[0:-1] + new_parameterMap[pm[sd]] = str(pm[sd]) # .name + else: + raise DeveloperError( + 'Parameter is not a parameter. Should not happen. Contact developers' + ) + + for ky, vl in new_variableMap.items(): + if ky not in latex_component_map: + latex_component_map[ky] = vl + for ky, vl in new_parameterMap.items(): + if ky not in latex_component_map: + latex_component_map[ky] = vl + + rep_dict = {} + for ky in reversed(list(latex_component_map)): + if isinstance(ky, (pyo.Var, VarData)): + overwrite_value = latex_component_map[ky] + if ky not in existing_components: + overwrite_value = overwrite_value.replace('_', '\\_') + rep_dict[variableMap[ky]] = overwrite_value + elif isinstance(ky, (pyo.Param, ParamData)): + overwrite_value = latex_component_map[ky] + if ky not in existing_components: + overwrite_value = overwrite_value.replace('_', '\\_') + rep_dict[parameterMap[ky]] = overwrite_value + elif isinstance(ky, SetData): + # already handled + pass + elif isinstance(ky, (float, int)): + # happens when immutable parameters are used, do nothing + pass + else: + raise ValueError( + 'The latex_component_map object has a key of invalid type: %s' + % (str(ky)) + ) + + label_rep_dict = copy.deepcopy(rep_dict) + for ky, vl in label_rep_dict.items(): + label_rep_dict[ky] = vl.replace('{', '').replace('}', '').replace('\\', '') + + splitLines = pstr.split('\n') + for i in range(0, len(splitLines)): + if use_equation_environment: + splitLines[i] = multiple_replace(splitLines[i], rep_dict) + else: + if '\\label{' in splitLines[i]: + epr, lbl = splitLines[i].split('\\label{') + epr = multiple_replace(epr, rep_dict) + # rep_dict[ky] = vl.replace('_', '\\_') + lbl = multiple_replace(lbl, label_rep_dict) + splitLines[i] = epr + '\\label{' + lbl + + pstr = '\n'.join(splitLines) + + pattern = r'_{([^{]*)}_{([^{]*)}' + replacement = r'_{\1_{\2}}' + pstr = re.sub(pattern, replacement, pstr) + + pattern = r'_(.)_{([^}]*)}' + replacement = r'_{\1_{\2}}' + pstr = re.sub(pattern, replacement, pstr) + + splitLines = pstr.split('\n') + finalLines = [] + for sl in splitLines: + if sl != '': + finalLines.append(sl) + + pstr = '\n'.join(finalLines) + + if ostream is not None: + fstr = '' + fstr += '\\documentclass{article} \n' + fstr += '\\usepackage{amsmath} \n' + fstr += '\\usepackage{amssymb} \n' + fstr += '\\usepackage{dsfont} \n' + fstr += '\\usepackage[paperheight=11in, paperwidth=8.5in, left=1in, right=1in, top=1in, bottom=1in]{geometry} \n' + fstr += '\\allowdisplaybreaks \n' + fstr += '\\begin{document} \n' + fstr += '\\normalsize \n' + fstr += pstr + '\n' + fstr += '\\end{document} \n' + + # optional write to output file + if isinstance(ostream, (io.TextIOWrapper, io.StringIO)): + ostream.write(fstr) + elif isinstance(ostream, str): + f = open(ostream, 'w') + f.write(fstr) + f.close() + else: + raise ValueError( + 'Invalid type %s encountered when parsing the ostream. Must be a StringIO, FileIO, or valid filename string' + ) + + # return the latex string + return pstr diff --git a/pyomo/contrib/latex_printer/tests/__init__.py b/pyomo/contrib/latex_printer/tests/__init__.py new file mode 100644 index 00000000000..a4a626013c4 --- /dev/null +++ b/pyomo/contrib/latex_printer/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/latex_printer/tests/test_latex_printer.py b/pyomo/contrib/latex_printer/tests/test_latex_printer.py new file mode 100644 index 00000000000..b0ada97a5fe --- /dev/null +++ b/pyomo/contrib/latex_printer/tests/test_latex_printer.py @@ -0,0 +1,837 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import io +from textwrap import dedent + +import pyomo.common.unittest as unittest +import pyomo.core.tests.examples.pmedian_concrete as pmedian_concrete +import pyomo.environ as pyo + +from pyomo.contrib.latex_printer import latex_printer +from pyomo.common.tempfiles import TempfileManager +from pyomo.common.collections.component_map import ComponentMap +from pyomo.environ import ( + Reals, + PositiveReals, + NonPositiveReals, + NegativeReals, + NonNegativeReals, + Integers, + PositiveIntegers, + NonPositiveIntegers, + NegativeIntegers, + NonNegativeIntegers, + Boolean, + Binary, + Any, + # AnyWithNone, + EmptySet, + UnitInterval, + PercentFraction, + # RealInterval, + # IntegerInterval, +) + + +def generate_model(): + import pyomo.environ as pyo + from pyomo.core.expr import Expr_if + from pyomo.core.base import ExternalFunction + + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var() + m.y = pyo.Var() + m.z = pyo.Var() + m.objective_1 = pyo.Objective(expr=m.x + m.y + m.z) + m.constraint_1 = pyo.Constraint( + expr=m.x**2 + m.y**-2.0 - m.x * m.y * m.z + 1 == 2.0 + ) + m.constraint_2 = pyo.Constraint(expr=abs(m.x / m.z**-2) * (m.x + m.y) <= 2.0) + m.constraint_3 = pyo.Constraint(expr=pyo.sqrt(m.x / m.z**-2) <= 2.0) + m.constraint_4 = pyo.Constraint(expr=(1, m.x, 2)) + m.constraint_5 = pyo.Constraint(expr=Expr_if(m.x <= 1.0, m.z, m.y) <= 1.0) + + def blackbox(a, b): + return sin(a - b) + + m.bb = ExternalFunction(blackbox) + m.constraint_6 = pyo.Constraint(expr=m.x + m.bb(m.x, m.y) == 2) + + m.I = pyo.Set(initialize=[1, 2, 3, 4, 5]) + m.J = pyo.Set(initialize=[1, 2, 3]) + m.K = pyo.Set(initialize=[1, 3, 5]) + m.u = pyo.Var(m.I * m.I) + m.v = pyo.Var(m.I) + m.w = pyo.Var(m.J) + m.p = pyo.Var(m.K) + + m.express = pyo.Expression(expr=m.x**2 + m.y**2) + + def ruleMaker(m, j): + return (m.x + m.y) * sum(m.v[i] + m.u[i, j] ** 2 for i in m.I) <= 0 + + m.constraint_7 = pyo.Constraint(m.I, rule=ruleMaker) + + def ruleMaker(m): + return sum(m.p[k] for k in m.K) == 1 + + m.constraint_8 = pyo.Constraint(rule=ruleMaker) + + def ruleMaker(m): + return (m.x + m.y) * sum(m.w[j] for j in m.J) + + m.objective_2 = pyo.Objective(rule=ruleMaker) + + m.objective_3 = pyo.Objective(expr=m.x + m.y + m.z, sense=-1) + + return m + + +def generate_simple_model(): + import pyomo.environ as pyo + + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var() + m.y = pyo.Var() + m.objective_1 = pyo.Objective(expr=m.x + m.y) + m.constraint_1 = pyo.Constraint(expr=m.x**2 + m.y**2.0 <= 1.0) + m.constraint_2 = pyo.Constraint(expr=m.x >= 0.0) + + m.I = pyo.Set(initialize=[1, 2, 3, 4, 5]) + m.J = pyo.Set(initialize=[1, 2, 3]) + m.K = pyo.Set(initialize=[1, 3, 5]) + m.u = pyo.Var(m.I * m.I) + m.v = pyo.Var(m.I) + m.w = pyo.Var(m.J) + m.p = pyo.Var(m.K) + + def ruleMaker(m, j): + return (m.x + m.y) * sum(m.v[i] + m.u[i, j] ** 2 for i in m.I) <= 0 + + m.constraint_7 = pyo.Constraint(m.I, rule=ruleMaker) + + def ruleMaker(m): + return sum(m.p[k] for k in m.K) == 1 + + m.constraint_8 = pyo.Constraint(rule=ruleMaker) + + return m + + +def generate_simple_model_2(): + import pyomo.environ as pyo + + m = pyo.ConcreteModel(name='basicFormulation') + m.x_dot = pyo.Var() + m.x_bar = pyo.Var() + m.x_star = pyo.Var() + m.x_hat = pyo.Var() + m.x_hat_1 = pyo.Var() + m.y_sub1_sub2_sub3 = pyo.Var() + m.objective_1 = pyo.Objective(expr=m.y_sub1_sub2_sub3) + m.constraint_1 = pyo.Constraint( + expr=(m.x_dot + m.x_bar + m.x_star + m.x_hat + m.x_hat_1) ** 2 + <= m.y_sub1_sub2_sub3 + ) + m.constraint_2 = pyo.Constraint( + expr=(m.x_dot + m.x_bar) ** -(m.x_star + m.x_hat) <= m.y_sub1_sub2_sub3 + ) + m.constraint_3 = pyo.Constraint( + expr=-(m.x_dot + m.x_bar) + -(m.x_star + m.x_hat) <= m.y_sub1_sub2_sub3 + ) + + return m + + +class TestLatexPrinter(unittest.TestCase): + def test_latexPrinter_simpleDocTests(self): + # Ex 1 ----------------------- + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var() + m.y = pyo.Var() + pstr = latex_printer(m.x + m.y) + bstr = dedent( + r""" + \begin{equation} + x + y + \end{equation} + """ + ) + self.assertEqual('\n' + pstr + '\n', bstr) + + # Ex 2 ----------------------- + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var() + m.y = pyo.Var() + m.expression_1 = pyo.Expression(expr=m.x**2 + m.y**2) + pstr = latex_printer(m.expression_1) + bstr = dedent( + r""" + \begin{equation} + x^{2} + y^{2} + \end{equation} + """ + ) + self.assertEqual('\n' + pstr + '\n', bstr) + + # Ex 3 ----------------------- + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var() + m.y = pyo.Var() + m.constraint_1 = pyo.Constraint(expr=m.x**2 + m.y**2 <= 1.0) + pstr = latex_printer(m.constraint_1) + bstr = dedent( + r""" + \begin{equation} + x^{2} + y^{2} \leq 1 + \end{equation} + """ + ) + self.assertEqual('\n' + pstr + '\n', bstr) + + # Ex 4 ----------------------- + m = pyo.ConcreteModel(name='basicFormulation') + m.I = pyo.Set(initialize=[1, 2, 3, 4, 5]) + m.v = pyo.Var(m.I) + + def ruleMaker(m): + return sum(m.v[i] for i in m.I) <= 0 + + m.constraint = pyo.Constraint(rule=ruleMaker) + pstr = latex_printer(m.constraint) + bstr = dedent( + r""" + \begin{equation} + \sum_{ i \in I } v_{i} \leq 0 + \end{equation} + """ + ) + self.assertEqual('\n' + pstr + '\n', bstr) + + # Ex 5 ----------------------- + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var() + m.y = pyo.Var() + m.z = pyo.Var() + m.c = pyo.Param(initialize=1.0, mutable=True) + m.objective = pyo.Objective(expr=m.x + m.y + m.z) + m.constraint_1 = pyo.Constraint(expr=m.x**2 + m.y**2.0 - m.z**2.0 <= m.c) + pstr = latex_printer(m) + bstr = dedent( + r""" + \begin{align} + & \min + & & x + y + z & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} + y^{2} - z^{2} \leq c & \label{con:basicFormulation_constraint_1} + \end{align} + """ + ) + self.assertEqual('\n' + pstr + '\n', bstr) + + # Ex 6 ----------------------- + m = pyo.ConcreteModel(name='basicFormulation') + m.I = pyo.Set(initialize=[1, 2, 3, 4, 5]) + m.v = pyo.Var(m.I) + + def ruleMaker(m): + return sum(m.v[i] for i in m.I) <= 0 + + m.constraint = pyo.Constraint(rule=ruleMaker) + lcm = ComponentMap() + lcm[m.v] = 'x' + lcm[m.I] = ['\\mathcal{A}', ['j', 'k']] + pstr = latex_printer(m.constraint, latex_component_map=lcm) + bstr = dedent( + r""" + \begin{equation} + \sum_{ j \in \mathcal{A} } x_{j} \leq 0 + \end{equation} + """ + ) + self.assertEqual('\n' + pstr + '\n', bstr) + + def test_latexPrinter_objective(self): + m = generate_model() + pstr = latex_printer(m.objective_1) + bstr = dedent( + r""" + \begin{equation} + & \min + & & x + y + z + \end{equation} + """ + ) + self.assertEqual('\n' + pstr + '\n', bstr) + + pstr = latex_printer(m.objective_3) + bstr = dedent( + r""" + \begin{equation} + & \max + & & x + y + z + \end{equation} + """ + ) + self.assertEqual('\n' + pstr + '\n', bstr) + + def test_latexPrinter_constraint(self): + m = generate_model() + pstr = latex_printer(m.constraint_1) + + bstr = dedent( + r""" + \begin{equation} + x^{2} + y^{-2} - x y z + 1 = 2 + \end{equation} + """ + ) + + self.assertEqual('\n' + pstr + '\n', bstr) + + def test_latexPrinter_expression(self): + m = generate_model() + + m.express = pyo.Expression(expr=m.x + m.y) + + pstr = latex_printer(m.express) + + bstr = dedent( + r""" + \begin{equation} + x + y + \end{equation} + """ + ) + + self.assertEqual('\n' + pstr + '\n', bstr) + + def test_latexPrinter_simpleExpression(self): + m = generate_model() + + pstr = latex_printer(m.x - m.y) + bstr = dedent( + r""" + \begin{equation} + x - y + \end{equation} + """ + ) + self.assertEqual('\n' + pstr + '\n', bstr) + + pstr = latex_printer(m.x - 2 * m.y) + bstr = dedent( + r""" + \begin{equation} + x - 2 y + \end{equation} + """ + ) + self.assertEqual('\n' + pstr + '\n', bstr) + + def test_latexPrinter_unary(self): + m = generate_model() + + pstr = latex_printer(m.constraint_2) + bstr = dedent( + r""" + \begin{equation} + \left| \frac{x}{z^{-2}} \right| \left( x + y \right) \leq 2 + \end{equation} + """ + ) + self.assertEqual('\n' + pstr + '\n', bstr) + + pstr = latex_printer(pyo.Constraint(expr=pyo.sin(m.x) == 1)) + bstr = dedent( + r""" + \begin{equation} + \sin \left( x \right) = 1 + \end{equation} + """ + ) + self.assertEqual('\n' + pstr + '\n', bstr) + + pstr = latex_printer(pyo.Constraint(expr=pyo.log10(m.x) == 1)) + bstr = dedent( + r""" + \begin{equation} + \log_{10} \left( x \right) = 1 + \end{equation} + """ + ) + self.assertEqual('\n' + pstr + '\n', bstr) + + pstr = latex_printer(pyo.Constraint(expr=pyo.sqrt(m.x) == 1)) + bstr = dedent( + r""" + \begin{equation} + \sqrt { x } = 1 + \end{equation} + """ + ) + self.assertEqual('\n' + pstr + '\n', bstr) + + def test_latexPrinter_rangedConstraint(self): + m = generate_model() + + pstr = latex_printer(m.constraint_4) + bstr = dedent( + r""" + \begin{equation} + 1 \leq x \leq 2 + \end{equation} + """ + ) + self.assertEqual('\n' + pstr + '\n', bstr) + + def test_latexPrinter_exprIf(self): + m = generate_model() + + pstr = latex_printer(m.constraint_5) + bstr = dedent( + r""" + \begin{equation} + f_{\text{exprIf}}(x \leq 1,z,y) \leq 1 + \end{equation} + """ + ) + self.assertEqual('\n' + pstr + '\n', bstr) + + def test_latexPrinter_blackBox(self): + m = generate_model() + + pstr = latex_printer(m.constraint_6) + bstr = dedent( + r""" + \begin{equation} + x + f\_1(x,y) = 2 + \end{equation} + """ + ) + self.assertEqual('\n' + pstr + '\n', bstr) + + def test_latexPrinter_iteratedConstraints(self): + m = generate_model() + + pstr = latex_printer(m.constraint_7) + bstr = dedent( + r""" + \begin{equation} + \left( x + y \right) \sum_{ i \in I } v_{i} + u_{i,j}^{2} \leq 0 \qquad \forall j \in I + \end{equation} + """ + ) + self.assertEqual('\n' + pstr + '\n', bstr) + + pstr = latex_printer(m.constraint_8) + bstr = dedent( + r""" + \begin{equation} + \sum_{ i \in K } p_{i} = 1 + \end{equation} + """ + ) + self.assertEqual('\n' + pstr + '\n', bstr) + + def test_latexPrinter_fileWriter(self): + m = generate_simple_model() + + with TempfileManager.new_context() as tempfile: + fd, fname = tempfile.mkstemp() + pstr = latex_printer(m, ostream=fname) + + f = open(fname) + bstr = f.read() + f.close() + + bstr_split = bstr.split('\n') + bstr_stripped = bstr_split[8:-2] + bstr = '\n'.join(bstr_stripped) + '\n' + + self.assertEqual(pstr + '\n', bstr) + + def test_latexPrinter_inputError(self): + self.assertRaises( + ValueError, latex_printer, **{'pyomo_component': 'errorString'} + ) + + def test_latexPrinter_fileWriter(self): + m = generate_simple_model() + + with TempfileManager.new_context() as tempfile: + fd, fname = tempfile.mkstemp() + pstr = latex_printer(m, ostream=fname) + + f = open(fname) + bstr = f.read() + f.close() + + bstr_split = bstr.split('\n') + bstr_stripped = bstr_split[8:-2] + bstr = '\n'.join(bstr_stripped) + '\n' + + self.assertEqual(pstr + '\n', bstr) + + self.assertRaises( + ValueError, latex_printer, **{'pyomo_component': m, 'ostream': 2.0} + ) + + def test_latexPrinter_overwriteError(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.I = pyo.Set(initialize=[1, 2, 3, 4, 5]) + m.v = pyo.Var(m.I) + + def ruleMaker(m): + return sum(m.v[i] for i in m.I) <= 0 + + m.constraint = pyo.Constraint(rule=ruleMaker) + lcm = ComponentMap() + lcm[m.v] = 'x' + lcm[m.I] = ['\\mathcal{A}', ['j', 'k']] + lcm['err'] = 1.0 + + self.assertRaises( + ValueError, + latex_printer, + **{'pyomo_component': m.constraint, 'latex_component_map': lcm} + ) + + def test_latexPrinter_indexedParam(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.I = pyo.Set(initialize=[1, 2, 3, 4, 5]) + m.x = pyo.Var(m.I * m.I) + m.c = pyo.Param(m.I * m.I, initialize=1.0, mutable=True) + + def ruleMaker_1(m): + return sum(m.c[i, j] * m.x[i, j] for i in m.I for j in m.I) + + def ruleMaker_2(m): + return sum(m.x[i, j] ** 2 for i in m.I for j in m.I) <= 1 + + m.objective = pyo.Objective(rule=ruleMaker_1) + m.constraint_1 = pyo.Constraint(rule=ruleMaker_2) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & \sum_{ i \in I } \sum_{ j \in I } c_{i,j} x_{i,j} & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & \sum_{ i \in I } \sum_{ j \in I } x_{i,j}^{2} \leq 1 & \label{con:basicFormulation_constraint_1} + \end{align} + """ + ) + + self.assertEqual('\n' + pstr + '\n', bstr) + + lcm = ComponentMap() + lcm[m.I] = ['\\mathcal{A}', ['j']] + self.assertRaises( + ValueError, + latex_printer, + **{'pyomo_component': m, 'latex_component_map': lcm} + ) + + def test_latexPrinter_involvedModel(self): + m = generate_model() + pstr = latex_printer(m) + print(pstr) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x + y + z & \label{obj:basicFormulation_objective_1} \\ + & \min + & & \left( x + y \right) \sum_{ i \in J } w_{i} & \label{obj:basicFormulation_objective_2} \\ + & \max + & & x + y + z & \label{obj:basicFormulation_objective_3} \\ + & \text{s.t.} + & & x^{2} + y^{-2} - x y z + 1 = 2 & \label{con:basicFormulation_constraint_1} \\ + &&& \left| \frac{x}{z^{-2}} \right| \left( x + y \right) \leq 2 & \label{con:basicFormulation_constraint_2} \\ + &&& \sqrt { \frac{x}{z^{-2}} } \leq 2 & \label{con:basicFormulation_constraint_3} \\ + &&& 1 \leq x \leq 2 & \label{con:basicFormulation_constraint_4} \\ + &&& f_{\text{exprIf}}(x \leq 1,z,y) \leq 1 & \label{con:basicFormulation_constraint_5} \\ + &&& x + f\_1(x,y) = 2 & \label{con:basicFormulation_constraint_6} \\ + &&& \left( x + y \right) \sum_{ i \in I } v_{i} + u_{i,j}^{2} \leq 0 & \qquad \forall j \in I \label{con:basicFormulation_constraint_7} \\ + &&& \sum_{ i \in K } p_{i} = 1 & \label{con:basicFormulation_constraint_8} + \end{align} + """ + ) + + self.assertEqual('\n' + pstr + '\n', bstr) + + def test_latexPrinter_continuousSet(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.I = pyo.Set(initialize=[1, 2, 3, 4, 5]) + m.v = pyo.Var(m.I) + + def ruleMaker(m): + return sum(m.v[i] for i in m.I) <= 0 + + m.constraint = pyo.Constraint(rule=ruleMaker) + pstr = latex_printer(m.constraint, explicit_set_summation=True) + + bstr = dedent( + r""" + \begin{equation} + \sum_{ i = 1 }^{5} v_{i} \leq 0 + \end{equation} + """ + ) + self.assertEqual('\n' + pstr + '\n', bstr) + + def test_latexPrinter_notContinuousSet(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.I = pyo.Set(initialize=[1, 3, 4, 5]) + m.v = pyo.Var(m.I) + + def ruleMaker(m): + return sum(m.v[i] for i in m.I) <= 0 + + m.constraint = pyo.Constraint(rule=ruleMaker) + pstr = latex_printer(m.constraint, explicit_set_summation=True) + + bstr = dedent( + r""" + \begin{equation} + \sum_{ i \in I } v_{i} \leq 0 + \end{equation} + """ + ) + self.assertEqual('\n' + pstr + '\n', bstr) + + def test_latexPrinter_autoIndex(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.I = pyo.Set(initialize=[1, 2, 3, 4, 5]) + m.v = pyo.Var(m.I) + + def ruleMaker(m): + return sum(m.v[i] for i in m.I) <= 0 + + m.constraint = pyo.Constraint(rule=ruleMaker) + lcm = ComponentMap() + lcm[m.v] = 'x' + lcm[m.I] = ['\\mathcal{A}', []] + pstr = latex_printer(m.constraint, latex_component_map=lcm) + bstr = dedent( + r""" + \begin{equation} + \sum_{ i \in \mathcal{A} } x_{i} \leq 0 + \end{equation} + """ + ) + self.assertEqual('\n' + pstr + '\n', bstr) + + def test_latexPrinter_equationEnvironment(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var() + m.y = pyo.Var() + m.z = pyo.Var() + m.c = pyo.Param(initialize=1.0, mutable=True) + m.objective = pyo.Objective(expr=m.x + m.y + m.z) + m.constraint_1 = pyo.Constraint(expr=m.x**2 + m.y**2.0 - m.z**2.0 <= m.c) + pstr = latex_printer(m, use_equation_environment=True) + + bstr = dedent( + r""" + \begin{equation} + \begin{aligned} + & \min + & & x + y + z \\ + & \text{s.t.} + & & x^{2} + y^{2} - z^{2} \leq c + \end{aligned} + \label{basicFormulation} + \end{equation} + """ + ) + self.assertEqual('\n' + pstr + '\n', bstr) + + def test_latexPrinter_manyVariablesWithDomains(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Integers, bounds=(-10, 10)) + m.y = pyo.Var(domain=Binary, bounds=(-10, 10)) + m.z = pyo.Var(domain=PositiveReals, bounds=(-10, 10)) + m.u = pyo.Var(domain=NonNegativeIntegers, bounds=(-10, 10)) + m.v = pyo.Var(domain=NegativeReals, bounds=(-10, 10)) + m.w = pyo.Var(domain=PercentFraction, bounds=(-10, 10)) + m.objective = pyo.Objective(expr=m.x + m.y + m.z + m.u + m.v + m.w) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x + y + z + u + v + w & \label{obj:basicFormulation_objective} \\ + & \text{w.b.} + & & -10 \leq x \leq 10 & \qquad \in \mathds{Z} \label{con:basicFormulation_x_bound} \\ + &&& y & \qquad \in \left\{ 0 , 1 \right \} \label{con:basicFormulation_y_bound} \\ + &&& 0 < z \leq 10 & \qquad \in \mathds{R}_{> 0} \label{con:basicFormulation_z_bound} \\ + &&& 0 \leq u \leq 10 & \qquad \in \mathds{Z}_{\geq 0} \label{con:basicFormulation_u_bound} \\ + &&& -10 \leq v < 0 & \qquad \in \mathds{R}_{< 0} \label{con:basicFormulation_v_bound} \\ + &&& 0 \leq w \leq 1 & \qquad \in \mathds{R} \label{con:basicFormulation_w_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_manyVariablesWithDomains_eqn(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Integers, bounds=(-10, 10)) + m.y = pyo.Var(domain=Binary, bounds=(-10, 10)) + m.z = pyo.Var(domain=PositiveReals, bounds=(-10, 10)) + m.u = pyo.Var(domain=NonNegativeIntegers, bounds=(-10, 10)) + m.v = pyo.Var(domain=NegativeReals, bounds=(-10, 10)) + m.w = pyo.Var(domain=PercentFraction, bounds=(-10, 10)) + m.objective = pyo.Objective(expr=m.x + m.y + m.z + m.u + m.v + m.w) + pstr = latex_printer(m, use_equation_environment=True) + + bstr = dedent( + r""" + \begin{equation} + \begin{aligned} + & \min + & & x + y + z + u + v + w \\ + & \text{w.b.} + & & -10 \leq x \leq 10 \qquad \in \mathds{Z}\\ + &&& y \qquad \in \left\{ 0 , 1 \right \}\\ + &&& 0 < z \leq 10 \qquad \in \mathds{R}_{> 0}\\ + &&& 0 \leq u \leq 10 \qquad \in \mathds{Z}_{\geq 0}\\ + &&& -10 \leq v < 0 \qquad \in \mathds{R}_{< 0}\\ + &&& 0 \leq w \leq 1 \qquad \in \mathds{R} + \end{aligned} + \label{basicFormulation} + \end{equation} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_indexedParamSingle(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.I = pyo.Set(initialize=[1, 2, 3, 4, 5]) + m.x = pyo.Var(m.I * m.I) + m.c = pyo.Param(m.I * m.I, initialize=1.0, mutable=True) + + def ruleMaker_1(m): + return sum(m.c[i, j] * m.x[i, j] for i in m.I for j in m.I) + + def ruleMaker_2(m): + return sum(m.c[i, j] * m.x[i, j] ** 2 for i in m.I for j in m.I) <= 1 + + m.objective = pyo.Objective(rule=ruleMaker_1) + m.constraint_1 = pyo.Constraint(rule=ruleMaker_2) + pstr = latex_printer(m.constraint_1) + print(pstr) + + bstr = dedent( + r""" + \begin{equation} + \sum_{ i \in I } \sum_{ j \in I } c_{i,j} x_{i,j}^{2} \leq 1 + \end{equation} + """ + ) + + self.assertEqual('\n' + pstr + '\n', bstr) + + def test_latexPrinter_throwTemplatizeError(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.I = pyo.Set(initialize=[1, 2, 3, 4, 5]) + m.x = pyo.Var(m.I, bounds=[-10, 10]) + m.c = pyo.Param(m.I, initialize=1.0, mutable=True) + + def ruleMaker_1(m): + return sum(m.c[i] * m.x[i] for i in m.I) + + def ruleMaker_2(m, i): + if i >= 2: + return m.x[i] <= 1 + else: + return pyo.Constraint.Skip + + m.objective = pyo.Objective(rule=ruleMaker_1) + m.constraint_1 = pyo.Constraint(m.I, rule=ruleMaker_2) + self.assertRaises( + RuntimeError, + latex_printer, + **{'pyomo_component': m, 'throw_templatization_error': True} + ) + pstr = latex_printer(m) + bstr = dedent( + r""" + \begin{align} + & \min + & & \sum_{ i \in I } c_{i} x_{i} & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x[2] \leq 1 & \label{con:basicFormulation_constraint_1} \\ + & & x[3] \leq 1 & \label{con:basicFormulation_constraint_1} \\ + & & x[4] \leq 1 & \label{con:basicFormulation_constraint_1} \\ + & & x[5] \leq 1 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & -10 \leq x \leq 10 & \qquad \in \mathds{R} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual('\n' + pstr + '\n', bstr) + + def test_latexPrinter_pmedian_verbose(self): + m = pmedian_concrete.create_model() + self.assertEqual( + latex_printer(m).strip(), + r""" +\begin{align} + & \min + & & \sum_{ i \in Locations } \sum_{ j \in Customers } cost_{i,j} serve\_customer\_from\_location_{i,j} & \label{obj:M1_obj} \\ + & \text{s.t.} + & & \sum_{ i \in Locations } serve\_customer\_from\_location_{i,j} = 1 & \qquad \forall j \in Customers \label{con:M1_single_x} \\ + &&& serve\_customer\_from\_location_{i,j} \leq select\_location_{i} & \qquad \forall i,j \in Locations \times Customers \label{con:M1_bound_y} \\ + &&& \sum_{ i \in Locations } select\_location_{i} = P & \label{con:M1_num_facilities} \\ + & \text{w.b.} + & & 0.0 \leq serve\_customer\_from\_location \leq 1.0 & \qquad \in \mathds{R} \label{con:M1_serve_customer_from_location_bound} \\ + &&& select\_location & \qquad \in \left\{ 0 , 1 \right \} \label{con:M1_select_location_bound} +\end{align} + """.strip(), + ) + + def test_latexPrinter_pmedian_concise(self): + m = pmedian_concrete.create_model() + lcm = ComponentMap() + lcm[m.Locations] = ['L', ['n']] + lcm[m.Customers] = ['C', ['m']] + lcm[m.cost] = 'd' + lcm[m.serve_customer_from_location] = 'x' + lcm[m.select_location] = 'y' + self.assertEqual( + latex_printer(m, latex_component_map=lcm).strip(), + r""" +\begin{align} + & \min + & & \sum_{ n \in L } \sum_{ m \in C } d_{n,m} x_{n,m} & \label{obj:M1_obj} \\ + & \text{s.t.} + & & \sum_{ n \in L } x_{n,m} = 1 & \qquad \forall m \in C \label{con:M1_single_x} \\ + &&& x_{n,m} \leq y_{n} & \qquad \forall n,m \in L \times C \label{con:M1_bound_y} \\ + &&& \sum_{ n \in L } y_{n} = P & \label{con:M1_num_facilities} \\ + & \text{w.b.} + & & 0.0 \leq x \leq 1.0 & \qquad \in \mathds{R} \label{con:M1_x_bound} \\ + &&& y & \qquad \in \left\{ 0 , 1 \right \} \label{con:M1_y_bound} +\end{align} + """.strip(), + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/pyomo/contrib/latex_printer/tests/test_latex_printer_vartypes.py b/pyomo/contrib/latex_printer/tests/test_latex_printer_vartypes.py new file mode 100644 index 00000000000..dc3a415618b --- /dev/null +++ b/pyomo/contrib/latex_printer/tests/test_latex_printer_vartypes.py @@ -0,0 +1,3294 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2023 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest +from pyomo.contrib.latex_printer import latex_printer +import pyomo.environ as pyo +from textwrap import dedent +from pyomo.common.tempfiles import TempfileManager +from pyomo.common.collections.component_map import ComponentMap + +from pyomo.environ import ( + Reals, + PositiveReals, + NonPositiveReals, + NegativeReals, + NonNegativeReals, + Integers, + PositiveIntegers, + NonPositiveIntegers, + NegativeIntegers, + NonNegativeIntegers, + Boolean, + Binary, + Any, + # AnyWithNone, + EmptySet, + UnitInterval, + PercentFraction, + # RealInterval, + # IntegerInterval, +) + +from pyomo.common.errors import InfeasibleConstraintException + + +class TestLatexPrinterVariableTypes(unittest.TestCase): + def test_latexPrinter_variableType_Reals_1(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Reals) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Reals_2(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Reals, bounds=(None, None)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Reals_3(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Reals, bounds=(-10, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & -10 \leq x \leq 10 & \qquad \in \mathds{R} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Reals_4(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Reals, bounds=(-10, 0)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & -10 \leq x \leq 0 & \qquad \in \mathds{R} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Reals_5(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Reals, bounds=(-10, -2)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & -10 \leq x \leq -2 & \qquad \in \mathds{R} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Reals_6(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Reals, bounds=(0, 0)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x \leq 0 & \qquad \in \mathds{R} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Reals_7(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Reals, bounds=(0, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x \leq 10 & \qquad \in \mathds{R} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Reals_8(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Reals, bounds=(2, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 2 \leq x \leq 10 & \qquad \in \mathds{R} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Reals_9(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Reals, bounds=(0, 1)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x \leq 1 & \qquad \in \mathds{R} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Reals_10(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Reals, bounds=(1, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 1 \leq x \leq 10 & \qquad \in \mathds{R} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Reals_11(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Reals, bounds=(0.25, 0.75)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0.25 \leq x \leq 0.75 & \qquad \in \mathds{R} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_PositiveReals_1(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=PositiveReals) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 < x & \qquad \in \mathds{R}_{> 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_PositiveReals_2(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=PositiveReals, bounds=(None, None)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 < x & \qquad \in \mathds{R}_{> 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_PositiveReals_3(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=PositiveReals, bounds=(-10, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 < x \leq 10 & \qquad \in \mathds{R}_{> 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_PositiveReals_4(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=PositiveReals, bounds=(-10, 0)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + self.assertRaises( + InfeasibleConstraintException, latex_printer, **{'pyomo_component': m} + ) + + def test_latexPrinter_variableType_PositiveReals_5(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=PositiveReals, bounds=(-10, -2)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + self.assertRaises( + InfeasibleConstraintException, latex_printer, **{'pyomo_component': m} + ) + + def test_latexPrinter_variableType_PositiveReals_6(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=PositiveReals, bounds=(0, 0)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + self.assertRaises( + InfeasibleConstraintException, latex_printer, **{'pyomo_component': m} + ) + + def test_latexPrinter_variableType_PositiveReals_7(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=PositiveReals, bounds=(0, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 < x \leq 10 & \qquad \in \mathds{R}_{> 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_PositiveReals_8(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=PositiveReals, bounds=(2, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 2 \leq x \leq 10 & \qquad \in \mathds{R}_{> 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_PositiveReals_9(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=PositiveReals, bounds=(0, 1)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 < x \leq 1 & \qquad \in \mathds{R}_{> 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_PositiveReals_10(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=PositiveReals, bounds=(1, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 1 \leq x \leq 10 & \qquad \in \mathds{R}_{> 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_PositiveReals_11(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=PositiveReals, bounds=(0.25, 0.75)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0.25 \leq x \leq 0.75 & \qquad \in \mathds{R}_{> 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonPositiveReals_1(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonPositiveReals) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x \leq 0 & \qquad \in \mathds{R}_{\leq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonPositiveReals_2(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonPositiveReals, bounds=(None, None)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x \leq 0 & \qquad \in \mathds{R}_{\leq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonPositiveReals_3(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonPositiveReals, bounds=(-10, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & -10 \leq x \leq 0 & \qquad \in \mathds{R}_{\leq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonPositiveReals_4(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonPositiveReals, bounds=(-10, 0)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & -10 \leq x \leq 0 & \qquad \in \mathds{R}_{\leq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonPositiveReals_5(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonPositiveReals, bounds=(-10, -2)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & -10 \leq x \leq -2 & \qquad \in \mathds{R}_{\leq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonPositiveReals_6(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonPositiveReals, bounds=(0, 0)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 = x \leq 0 & \qquad \in \mathds{R}_{\leq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonPositiveReals_7(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonPositiveReals, bounds=(0, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 = x \leq 0 & \qquad \in \mathds{R}_{\leq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonPositiveReals_8(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonPositiveReals, bounds=(2, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + self.assertRaises( + InfeasibleConstraintException, latex_printer, **{'pyomo_component': m} + ) + + def test_latexPrinter_variableType_NonPositiveReals_9(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonPositiveReals, bounds=(0, 1)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 = x \leq 0 & \qquad \in \mathds{R}_{\leq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonPositiveReals_10(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonPositiveReals, bounds=(1, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + self.assertRaises( + InfeasibleConstraintException, latex_printer, **{'pyomo_component': m} + ) + + def test_latexPrinter_variableType_NonPositiveReals_11(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonPositiveReals, bounds=(0.25, 0.75)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + self.assertRaises( + InfeasibleConstraintException, latex_printer, **{'pyomo_component': m} + ) + + def test_latexPrinter_variableType_NegativeReals_1(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NegativeReals) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x < 0 & \qquad \in \mathds{R}_{< 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NegativeReals_2(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NegativeReals, bounds=(None, None)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x < 0 & \qquad \in \mathds{R}_{< 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NegativeReals_3(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NegativeReals, bounds=(-10, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & -10 \leq x < 0 & \qquad \in \mathds{R}_{< 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NegativeReals_4(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NegativeReals, bounds=(-10, 0)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & -10 \leq x < 0 & \qquad \in \mathds{R}_{< 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NegativeReals_5(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NegativeReals, bounds=(-10, -2)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & -10 \leq x \leq -2 & \qquad \in \mathds{R}_{< 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NegativeReals_6(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NegativeReals, bounds=(0, 0)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + self.assertRaises( + InfeasibleConstraintException, latex_printer, **{'pyomo_component': m} + ) + + def test_latexPrinter_variableType_NegativeReals_7(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NegativeReals, bounds=(0, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + self.assertRaises( + InfeasibleConstraintException, latex_printer, **{'pyomo_component': m} + ) + + def test_latexPrinter_variableType_NegativeReals_8(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NegativeReals, bounds=(2, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + self.assertRaises( + InfeasibleConstraintException, latex_printer, **{'pyomo_component': m} + ) + + def test_latexPrinter_variableType_NegativeReals_9(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NegativeReals, bounds=(0, 1)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + self.assertRaises( + InfeasibleConstraintException, latex_printer, **{'pyomo_component': m} + ) + + def test_latexPrinter_variableType_NegativeReals_10(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NegativeReals, bounds=(1, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + self.assertRaises( + InfeasibleConstraintException, latex_printer, **{'pyomo_component': m} + ) + + def test_latexPrinter_variableType_NegativeReals_11(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NegativeReals, bounds=(0.25, 0.75)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + self.assertRaises( + InfeasibleConstraintException, latex_printer, **{'pyomo_component': m} + ) + + def test_latexPrinter_variableType_NonNegativeReals_1(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonNegativeReals) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x & \qquad \in \mathds{R}_{\geq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonNegativeReals_2(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonNegativeReals, bounds=(None, None)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x & \qquad \in \mathds{R}_{\geq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonNegativeReals_3(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonNegativeReals, bounds=(-10, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x \leq 10 & \qquad \in \mathds{R}_{\geq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonNegativeReals_4(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonNegativeReals, bounds=(-10, 0)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x = 0 & \qquad \in \mathds{R}_{\geq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonNegativeReals_5(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonNegativeReals, bounds=(-10, -2)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + self.assertRaises( + InfeasibleConstraintException, latex_printer, **{'pyomo_component': m} + ) + + def test_latexPrinter_variableType_NonNegativeReals_6(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonNegativeReals, bounds=(0, 0)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x = 0 & \qquad \in \mathds{R}_{\geq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonNegativeReals_7(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonNegativeReals, bounds=(0, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x \leq 10 & \qquad \in \mathds{R}_{\geq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonNegativeReals_8(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonNegativeReals, bounds=(2, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 2 \leq x \leq 10 & \qquad \in \mathds{R}_{\geq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonNegativeReals_9(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonNegativeReals, bounds=(0, 1)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x \leq 1 & \qquad \in \mathds{R}_{\geq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonNegativeReals_10(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonNegativeReals, bounds=(1, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 1 \leq x \leq 10 & \qquad \in \mathds{R}_{\geq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonNegativeReals_11(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonNegativeReals, bounds=(0.25, 0.75)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0.25 \leq x \leq 0.75 & \qquad \in \mathds{R}_{\geq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Integers_1(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Integers) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \mathds{Z} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Integers_2(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Integers, bounds=(None, None)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \mathds{Z} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Integers_3(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Integers, bounds=(-10, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & -10 \leq x \leq 10 & \qquad \in \mathds{Z} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Integers_4(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Integers, bounds=(-10, 0)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & -10 \leq x \leq 0 & \qquad \in \mathds{Z} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Integers_5(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Integers, bounds=(-10, -2)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & -10 \leq x \leq -2 & \qquad \in \mathds{Z} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Integers_6(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Integers, bounds=(0, 0)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x \leq 0 & \qquad \in \mathds{Z} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Integers_7(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Integers, bounds=(0, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x \leq 10 & \qquad \in \mathds{Z} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Integers_8(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Integers, bounds=(2, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 2 \leq x \leq 10 & \qquad \in \mathds{Z} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Integers_9(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Integers, bounds=(0, 1)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x \leq 1 & \qquad \in \mathds{Z} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Integers_10(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Integers, bounds=(1, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 1 \leq x \leq 10 & \qquad \in \mathds{Z} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Integers_11(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Integers, bounds=(0.25, 0.75)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0.25 \leq x \leq 0.75 & \qquad \in \mathds{Z} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_PositiveIntegers_1(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=PositiveIntegers) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 1 \leq x & \qquad \in \mathds{Z}_{> 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_PositiveIntegers_2(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=PositiveIntegers, bounds=(None, None)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 1 \leq x & \qquad \in \mathds{Z}_{> 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_PositiveIntegers_3(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=PositiveIntegers, bounds=(-10, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 1 \leq x \leq 10 & \qquad \in \mathds{Z}_{> 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_PositiveIntegers_4(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=PositiveIntegers, bounds=(-10, 0)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + self.assertRaises( + InfeasibleConstraintException, latex_printer, **{'pyomo_component': m} + ) + + def test_latexPrinter_variableType_PositiveIntegers_5(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=PositiveIntegers, bounds=(-10, -2)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + self.assertRaises( + InfeasibleConstraintException, latex_printer, **{'pyomo_component': m} + ) + + def test_latexPrinter_variableType_PositiveIntegers_6(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=PositiveIntegers, bounds=(0, 0)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + self.assertRaises( + InfeasibleConstraintException, latex_printer, **{'pyomo_component': m} + ) + + def test_latexPrinter_variableType_PositiveIntegers_7(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=PositiveIntegers, bounds=(0, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 1 \leq x \leq 10 & \qquad \in \mathds{Z}_{> 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_PositiveIntegers_8(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=PositiveIntegers, bounds=(2, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 2 \leq x \leq 10 & \qquad \in \mathds{Z}_{> 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_PositiveIntegers_9(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=PositiveIntegers, bounds=(0, 1)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 1 \leq x \leq 1 & \qquad \in \mathds{Z}_{> 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_PositiveIntegers_10(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=PositiveIntegers, bounds=(1, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 1 \leq x \leq 10 & \qquad \in \mathds{Z}_{> 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_PositiveIntegers_11(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=PositiveIntegers, bounds=(0.25, 0.75)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 1 \leq x \leq 0.75 & \qquad \in \mathds{Z}_{> 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonPositiveIntegers_1(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonPositiveIntegers) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x \leq 0 & \qquad \in \mathds{Z}_{\leq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonPositiveIntegers_2(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonPositiveIntegers, bounds=(None, None)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x \leq 0 & \qquad \in \mathds{Z}_{\leq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonPositiveIntegers_3(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonPositiveIntegers, bounds=(-10, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & -10 \leq x \leq 0 & \qquad \in \mathds{Z}_{\leq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonPositiveIntegers_4(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonPositiveIntegers, bounds=(-10, 0)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & -10 \leq x \leq 0 & \qquad \in \mathds{Z}_{\leq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonPositiveIntegers_5(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonPositiveIntegers, bounds=(-10, -2)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & -10 \leq x \leq -2 & \qquad \in \mathds{Z}_{\leq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonPositiveIntegers_6(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonPositiveIntegers, bounds=(0, 0)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 = x \leq 0 & \qquad \in \mathds{Z}_{\leq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonPositiveIntegers_7(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonPositiveIntegers, bounds=(0, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 = x \leq 0 & \qquad \in \mathds{Z}_{\leq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonPositiveIntegers_8(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonPositiveIntegers, bounds=(2, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + self.assertRaises( + InfeasibleConstraintException, latex_printer, **{'pyomo_component': m} + ) + + def test_latexPrinter_variableType_NonPositiveIntegers_9(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonPositiveIntegers, bounds=(0, 1)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 = x \leq 0 & \qquad \in \mathds{Z}_{\leq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonPositiveIntegers_10(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonPositiveIntegers, bounds=(1, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + self.assertRaises( + InfeasibleConstraintException, latex_printer, **{'pyomo_component': m} + ) + + def test_latexPrinter_variableType_NonPositiveIntegers_11(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonPositiveIntegers, bounds=(0.25, 0.75)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + self.assertRaises( + InfeasibleConstraintException, latex_printer, **{'pyomo_component': m} + ) + + def test_latexPrinter_variableType_NegativeIntegers_1(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NegativeIntegers) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x \leq -1 & \qquad \in \mathds{Z}_{< 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NegativeIntegers_2(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NegativeIntegers, bounds=(None, None)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x \leq -1 & \qquad \in \mathds{Z}_{< 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NegativeIntegers_3(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NegativeIntegers, bounds=(-10, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & -10 \leq x \leq -1 & \qquad \in \mathds{Z}_{< 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NegativeIntegers_4(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NegativeIntegers, bounds=(-10, 0)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & -10 \leq x \leq -1 & \qquad \in \mathds{Z}_{< 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NegativeIntegers_5(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NegativeIntegers, bounds=(-10, -2)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & -10 \leq x \leq -2 & \qquad \in \mathds{Z}_{< 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NegativeIntegers_6(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NegativeIntegers, bounds=(0, 0)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + self.assertRaises( + InfeasibleConstraintException, latex_printer, **{'pyomo_component': m} + ) + + def test_latexPrinter_variableType_NegativeIntegers_7(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NegativeIntegers, bounds=(0, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + self.assertRaises( + InfeasibleConstraintException, latex_printer, **{'pyomo_component': m} + ) + + def test_latexPrinter_variableType_NegativeIntegers_8(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NegativeIntegers, bounds=(2, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + self.assertRaises( + InfeasibleConstraintException, latex_printer, **{'pyomo_component': m} + ) + + def test_latexPrinter_variableType_NegativeIntegers_9(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NegativeIntegers, bounds=(0, 1)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + self.assertRaises( + InfeasibleConstraintException, latex_printer, **{'pyomo_component': m} + ) + + def test_latexPrinter_variableType_NegativeIntegers_10(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NegativeIntegers, bounds=(1, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + self.assertRaises( + InfeasibleConstraintException, latex_printer, **{'pyomo_component': m} + ) + + def test_latexPrinter_variableType_NegativeIntegers_11(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NegativeIntegers, bounds=(0.25, 0.75)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + self.assertRaises( + InfeasibleConstraintException, latex_printer, **{'pyomo_component': m} + ) + + def test_latexPrinter_variableType_NonNegativeIntegers_1(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonNegativeIntegers) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x & \qquad \in \mathds{Z}_{\geq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonNegativeIntegers_2(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonNegativeIntegers, bounds=(None, None)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x & \qquad \in \mathds{Z}_{\geq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonNegativeIntegers_3(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonNegativeIntegers, bounds=(-10, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x \leq 10 & \qquad \in \mathds{Z}_{\geq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonNegativeIntegers_4(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonNegativeIntegers, bounds=(-10, 0)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x = 0 & \qquad \in \mathds{Z}_{\geq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonNegativeIntegers_5(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonNegativeIntegers, bounds=(-10, -2)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + self.assertRaises( + InfeasibleConstraintException, latex_printer, **{'pyomo_component': m} + ) + + def test_latexPrinter_variableType_NonNegativeIntegers_6(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonNegativeIntegers, bounds=(0, 0)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x = 0 & \qquad \in \mathds{Z}_{\geq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonNegativeIntegers_7(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonNegativeIntegers, bounds=(0, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x \leq 10 & \qquad \in \mathds{Z}_{\geq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonNegativeIntegers_8(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonNegativeIntegers, bounds=(2, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 2 \leq x \leq 10 & \qquad \in \mathds{Z}_{\geq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonNegativeIntegers_9(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonNegativeIntegers, bounds=(0, 1)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x \leq 1 & \qquad \in \mathds{Z}_{\geq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonNegativeIntegers_10(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonNegativeIntegers, bounds=(1, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 1 \leq x \leq 10 & \qquad \in \mathds{Z}_{\geq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_NonNegativeIntegers_11(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=NonNegativeIntegers, bounds=(0.25, 0.75)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0.25 \leq x \leq 0.75 & \qquad \in \mathds{Z}_{\geq 0} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Boolean_1(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Boolean) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \left\{ \text{True} , \text{False} \right \} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Boolean_2(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Boolean, bounds=(None, None)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \left\{ \text{True} , \text{False} \right \} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Boolean_3(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Boolean, bounds=(-10, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \left\{ \text{True} , \text{False} \right \} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Boolean_4(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Boolean, bounds=(-10, 0)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \left\{ \text{True} , \text{False} \right \} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Boolean_5(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Boolean, bounds=(-10, -2)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \left\{ \text{True} , \text{False} \right \} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Boolean_6(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Boolean, bounds=(0, 0)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \left\{ \text{True} , \text{False} \right \} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Boolean_7(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Boolean, bounds=(0, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \left\{ \text{True} , \text{False} \right \} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Boolean_8(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Boolean, bounds=(2, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \left\{ \text{True} , \text{False} \right \} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Boolean_9(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Boolean, bounds=(0, 1)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \left\{ \text{True} , \text{False} \right \} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Boolean_10(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Boolean, bounds=(1, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \left\{ \text{True} , \text{False} \right \} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Boolean_11(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Boolean, bounds=(0.25, 0.75)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \left\{ \text{True} , \text{False} \right \} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Binary_1(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Binary) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \left\{ 0 , 1 \right \} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Binary_2(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Binary, bounds=(None, None)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \left\{ 0 , 1 \right \} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Binary_3(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Binary, bounds=(-10, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \left\{ 0 , 1 \right \} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Binary_4(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Binary, bounds=(-10, 0)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \left\{ 0 , 1 \right \} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Binary_5(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Binary, bounds=(-10, -2)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \left\{ 0 , 1 \right \} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Binary_6(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Binary, bounds=(0, 0)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \left\{ 0 , 1 \right \} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Binary_7(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Binary, bounds=(0, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \left\{ 0 , 1 \right \} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Binary_8(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Binary, bounds=(2, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \left\{ 0 , 1 \right \} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Binary_9(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Binary, bounds=(0, 1)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \left\{ 0 , 1 \right \} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Binary_10(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Binary, bounds=(1, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \left\{ 0 , 1 \right \} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_Binary_11(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=Binary, bounds=(0.25, 0.75)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \left\{ 0 , 1 \right \} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_EmptySet_1(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=EmptySet) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \varnothing \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_EmptySet_2(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=EmptySet, bounds=(None, None)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \varnothing \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_EmptySet_3(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=EmptySet, bounds=(-10, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \varnothing \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_EmptySet_4(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=EmptySet, bounds=(-10, 0)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \varnothing \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_EmptySet_5(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=EmptySet, bounds=(-10, -2)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \varnothing \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_EmptySet_6(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=EmptySet, bounds=(0, 0)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \varnothing \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_EmptySet_7(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=EmptySet, bounds=(0, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \varnothing \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_EmptySet_8(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=EmptySet, bounds=(2, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \varnothing \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_EmptySet_9(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=EmptySet, bounds=(0, 1)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \varnothing \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_EmptySet_10(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=EmptySet, bounds=(1, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \varnothing \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_EmptySet_11(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=EmptySet, bounds=(0.25, 0.75)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & x & \qquad \in \varnothing \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_UnitInterval_1(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=UnitInterval) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x \leq 1 & \qquad \in \mathds{R} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_UnitInterval_2(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=UnitInterval, bounds=(None, None)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x \leq 1 & \qquad \in \mathds{R} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_UnitInterval_3(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=UnitInterval, bounds=(-10, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x \leq 1 & \qquad \in \mathds{R} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_UnitInterval_4(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=UnitInterval, bounds=(-10, 0)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x = 0 & \qquad \in \mathds{R} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_UnitInterval_5(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=UnitInterval, bounds=(-10, -2)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + self.assertRaises( + InfeasibleConstraintException, latex_printer, **{'pyomo_component': m} + ) + + def test_latexPrinter_variableType_UnitInterval_6(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=UnitInterval, bounds=(0, 0)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x = 0 & \qquad \in \mathds{R} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_UnitInterval_7(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=UnitInterval, bounds=(0, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x \leq 1 & \qquad \in \mathds{R} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_UnitInterval_8(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=UnitInterval, bounds=(2, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + self.assertRaises( + InfeasibleConstraintException, latex_printer, **{'pyomo_component': m} + ) + + def test_latexPrinter_variableType_UnitInterval_9(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=UnitInterval, bounds=(0, 1)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x \leq 1 & \qquad \in \mathds{R} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_UnitInterval_10(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=UnitInterval, bounds=(1, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & = 1 x \leq 1 & \qquad \in \mathds{R} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_UnitInterval_11(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=UnitInterval, bounds=(0.25, 0.75)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0.25 \leq x \leq 0.75 & \qquad \in \mathds{R} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_PercentFraction_1(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=PercentFraction) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x \leq 1 & \qquad \in \mathds{R} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_PercentFraction_2(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=PercentFraction, bounds=(None, None)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x \leq 1 & \qquad \in \mathds{R} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_PercentFraction_3(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=PercentFraction, bounds=(-10, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x \leq 1 & \qquad \in \mathds{R} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_PercentFraction_4(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=PercentFraction, bounds=(-10, 0)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x = 0 & \qquad \in \mathds{R} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_PercentFraction_5(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=PercentFraction, bounds=(-10, -2)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + self.assertRaises( + InfeasibleConstraintException, latex_printer, **{'pyomo_component': m} + ) + + def test_latexPrinter_variableType_PercentFraction_6(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=PercentFraction, bounds=(0, 0)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x = 0 & \qquad \in \mathds{R} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_PercentFraction_7(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=PercentFraction, bounds=(0, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x \leq 1 & \qquad \in \mathds{R} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_PercentFraction_8(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=PercentFraction, bounds=(2, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + self.assertRaises( + InfeasibleConstraintException, latex_printer, **{'pyomo_component': m} + ) + + def test_latexPrinter_variableType_PercentFraction_9(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=PercentFraction, bounds=(0, 1)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0 \leq x \leq 1 & \qquad \in \mathds{R} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_PercentFraction_10(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=PercentFraction, bounds=(1, 10)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & = 1 x \leq 1 & \qquad \in \mathds{R} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + def test_latexPrinter_variableType_PercentFraction_11(self): + m = pyo.ConcreteModel(name='basicFormulation') + m.x = pyo.Var(domain=PercentFraction, bounds=(0.25, 0.75)) + m.objective = pyo.Objective(expr=m.x) + m.constraint_1 = pyo.Constraint(expr=m.x**2 <= 5.0) + pstr = latex_printer(m) + + bstr = dedent( + r""" + \begin{align} + & \min + & & x & \label{obj:basicFormulation_objective} \\ + & \text{s.t.} + & & x^{2} \leq 5 & \label{con:basicFormulation_constraint_1} \\ + & \text{w.b.} + & & 0.25 \leq x \leq 0.75 & \qquad \in \mathds{R} \label{con:basicFormulation_x_bound} + \end{align} + """ + ) + + self.assertEqual("\n" + pstr + "\n", bstr) + + +if __name__ == '__main__': + unittest.main() diff --git a/pyomo/contrib/mcpp/__init__.py b/pyomo/contrib/mcpp/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/mcpp/__init__.py +++ b/pyomo/contrib/mcpp/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mcpp/build.py b/pyomo/contrib/mcpp/build.py index 95246e5278e..7e119caec9f 100644 --- a/pyomo/contrib/mcpp/build.py +++ b/pyomo/contrib/mcpp/build.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -64,8 +64,8 @@ def _generate_configuration(): def build_mcpp(): - import distutils.core - from distutils.command.build_ext import build_ext + from setuptools import Distribution + from setuptools.command.build_ext import build_ext class _BuildWithoutPlatformInfo(build_ext, object): # Python3.x puts platform information into the generated SO file @@ -87,7 +87,7 @@ def get_ext_filename(self, ext_name): print("\n**** Building MCPP library ****") package_config = _generate_configuration() package_config['cmdclass'] = {'build_ext': _BuildWithoutPlatformInfo} - dist = distutils.core.Distribution(package_config) + dist = Distribution(package_config) install_dir = os.path.join(envvar.PYOMO_CONFIG_DIR, 'lib') dist.get_command_obj('install_lib').install_dir = install_dir try: diff --git a/pyomo/contrib/mcpp/getMCPP.py b/pyomo/contrib/mcpp/getMCPP.py index caf9566df64..dbce611d1a0 100644 --- a/pyomo/contrib/mcpp/getMCPP.py +++ b/pyomo/contrib/mcpp/getMCPP.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mcpp/mcppInterface.cpp b/pyomo/contrib/mcpp/mcppInterface.cpp index 30491fde1b1..a1e74567896 100644 --- a/pyomo/contrib/mcpp/mcppInterface.cpp +++ b/pyomo/contrib/mcpp/mcppInterface.cpp @@ -1,7 +1,7 @@ /**___________________________________________________________________________ * * Pyomo: Python Optimization Modeling Objects - * Copyright (c) 2008-2022 + * Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC * Under the terms of Contract DE-NA0003525 with National Technology and * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mcpp/plugins.py b/pyomo/contrib/mcpp/plugins.py index eed8874b1e7..577feec7fe3 100644 --- a/pyomo/contrib/mcpp/plugins.py +++ b/pyomo/contrib/mcpp/plugins.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mcpp/pyomo_mcpp.py b/pyomo/contrib/mcpp/pyomo_mcpp.py index bfd4b80edc3..0ef0237681b 100644 --- a/pyomo/contrib/mcpp/pyomo_mcpp.py +++ b/pyomo/contrib/mcpp/pyomo_mcpp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -11,7 +11,7 @@ # Note: the self.mcpp.* functions are all C-style functions implemented # in the compiled MC++ wrapper library # Note: argument to pow must be an integer -from __future__ import division + import ctypes import logging @@ -20,7 +20,7 @@ from pyomo.common.fileutils import Library from pyomo.core import value, Expression from pyomo.core.base.block import SubclassOf -from pyomo.core.base.expression import _ExpressionData +from pyomo.core.base.expression import NamedExpressionData from pyomo.core.expr.numvalue import nonpyomo_leaf_types from pyomo.core.expr.numeric_expr import ( AbsExpression, @@ -307,7 +307,9 @@ def exitNode(self, node, data): ans = self.mcpp.newConstant(node) elif not node.is_expression_type(): ans = self.register_num(node) - elif type(node) in SubclassOf(Expression) or isinstance(node, _ExpressionData): + elif type(node) in SubclassOf(Expression) or isinstance( + node, NamedExpressionData + ): ans = data[0] else: raise RuntimeError("Unhandled expression type: %s" % (type(node))) @@ -383,7 +385,6 @@ def finalizeResult(self, node_result): class McCormick(object): - """ This class takes the constructed expression from MCPP_Visitor and allows for MC methods to be performed on pyomo expressions. diff --git a/pyomo/contrib/mcpp/test_mcpp.py b/pyomo/contrib/mcpp/test_mcpp.py index 9d8c670d470..1cfb46ce328 100644 --- a/pyomo/contrib/mcpp/test_mcpp.py +++ b/pyomo/contrib/mcpp/test_mcpp.py @@ -1,14 +1,14 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain # rights in this software. # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from __future__ import division + import logging from math import pi diff --git a/pyomo/contrib/mindtpy/MindtPy.py b/pyomo/contrib/mindtpy/MindtPy.py index 6eb27c4c649..7b41e0078a3 100644 --- a/pyomo/contrib/mindtpy/MindtPy.py +++ b/pyomo/contrib/mindtpy/MindtPy.py @@ -3,7 +3,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -50,6 +50,14 @@ - Add single-tree implementation. - Add support for cplex_persistent solver. - Fix bug in OA cut expression in cut_generation.py. + +24.1.11 changes: +- fix gurobi single tree termination check bug +- fix Gurobi single tree cycle handling +- fix bug in feasibility pump method +- add special handling for infeasible relaxed NLP +- update the log format of infeasible fixed NLP subproblems +- create a new copy_var_list_values function """ from pyomo.contrib.mindtpy import __version__ diff --git a/pyomo/contrib/mindtpy/__init__.py b/pyomo/contrib/mindtpy/__init__.py index 8e2c2d9eaa4..652493b03a6 100644 --- a/pyomo/contrib/mindtpy/__init__.py +++ b/pyomo/contrib/mindtpy/__init__.py @@ -1 +1,12 @@ -__version__ = (0, 1, 0) +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +__version__ = (1, 0, 0) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index c930f613970..b8065a4c272 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -27,14 +27,7 @@ from operator import itemgetter from pyomo.common.errors import DeveloperError from pyomo.solvers.plugins.solvers.gurobi_direct import gurobipy -from pyomo.opt import ( - SolverFactory, - SolverResults, - ProblemSense, - SolutionStatus, - SolverStatus, -) -from pyomo.contrib.pynumero.interfaces.external_grey_box import ExternalGreyBoxBlock +from pyomo.opt import SolverFactory, SolverResults, SolutionStatus, SolverStatus from pyomo.core import ( minimize, maximize, @@ -80,11 +73,14 @@ set_solver_mipgap, set_solver_constraint_violation_tolerance, update_solver_timelimit, - copy_var_list_values + copy_var_list_values, ) single_tree, single_tree_available = attempt_import('pyomo.contrib.mindtpy.single_tree') tabu_list, tabu_list_available = attempt_import('pyomo.contrib.mindtpy.tabu_list') +egb, egb_available = attempt_import( + 'pyomo.contrib.pynumero.interfaces.external_grey_box' +) class _MindtPyAlgorithm(object): @@ -100,14 +96,14 @@ def __init__(self, **kwds): self.fixed_nlp = None # We store bounds, timing info, iteration count, incumbent, and the - # expression of the original (possibly nonlinear) objective function. + # Expression of the original (possibly nonlinear) objective function. self.results = SolverResults() self.timing = Bunch() self.curr_int_sol = [] self.should_terminate = False self.integer_list = [] - # dictionary {integer solution (list): cuts index (list)} - self.int_sol_2_cuts_ind = dict() + # Dictionary {integer solution (tuple): [cuts begin index, cuts end index] (list)} + self.integer_solution_to_cuts_index = dict() # Set up iteration counters self.nlp_iter = 0 @@ -123,6 +119,9 @@ def __init__(self, **kwds): self.log_formatter = ( ' {:>9} {:>15} {:>15g} {:>12g} {:>12g} {:>7.2%} {:>7.2f}' ) + self.termination_condition_log_formatter = ( + ' {:>9} {:>15} {:>15} {:>12g} {:>12g} {:>7.2%} {:>7.2f}' + ) self.fixed_nlp_log_formatter = ( '{:1}{:>9} {:>15} {:>15g} {:>12g} {:>12g} {:>7.2%} {:>7.2f}' ) @@ -146,6 +145,10 @@ def __init__(self, **kwds): self.last_iter_cuts = False # Store the OA cuts generated in the mip_start_process. self.mip_start_lazy_oa_cuts = [] + # Whether to load solutions in solve() function + self.mip_load_solutions = True + self.nlp_load_solutions = True + self.regularization_mip_load_solutions = True # Support use as a context manager under current solver API def __enter__(self): @@ -295,7 +298,7 @@ def model_is_valid(self): results = self.mip_opt.solve( self.original_model, tee=config.mip_solver_tee, - load_solutions=config.load_solutions, + load_solutions=self.mip_load_solutions, **config.mip_solver_args, ) if len(results.solution) > 0: @@ -329,11 +332,14 @@ def build_ordered_component_lists(self, model): ctype=Constraint, active=True, descend_into=(Block) ) ) - util_block.grey_box_list = list( - model.component_data_objects( - ctype=ExternalGreyBoxBlock, active=True, descend_into=(Block) + if egb_available: + util_block.grey_box_list = list( + model.component_data_objects( + ctype=egb.ExternalGreyBoxBlock, active=True, descend_into=(Block) + ) ) - ) + else: + util_block.grey_box_list = [] util_block.linear_constraint_list = list( c for c in util_block.constraint_list @@ -361,13 +367,20 @@ def build_ordered_component_lists(self, model): # We use component_data_objects rather than list(var_set) in order to # preserve a deterministic ordering. - util_block.variable_list = list( - v - for v in model.component_data_objects( - ctype=Var, descend_into=(Block, ExternalGreyBoxBlock) + if egb_available: + util_block.variable_list = list( + v + for v in model.component_data_objects( + ctype=Var, descend_into=(Block, egb.ExternalGreyBoxBlock) + ) + if v in var_set + ) + else: + util_block.variable_list = list( + v + for v in model.component_data_objects(ctype=Var, descend_into=(Block)) + if v in var_set ) - if v in var_set - ) util_block.discrete_variable_list = list( v for v in util_block.variable_list if v in var_set and v.is_integer() ) @@ -502,9 +515,9 @@ def get_primal_integral(self): return primal_integral def get_integral_info(self): - ''' + """ Obtain primal integral, dual integral and primal dual gap integral. - ''' + """ self.primal_integral = self.get_primal_integral() self.dual_integral = self.get_dual_integral() self.primal_dual_gap_integral = self.primal_integral + self.dual_integral @@ -616,9 +629,7 @@ def process_objective(self, update_var_con_list=True): raise ValueError('Model has multiple active objectives.') else: main_obj = active_objectives[0] - self.results.problem.sense = ( - ProblemSense.minimize if main_obj.sense == 1 else ProblemSense.maximize - ) + self.results.problem.sense = main_obj.sense self.objective_sense = main_obj.sense # Move the objective to the constraints if it is nonlinear or move_objective is True. @@ -788,7 +799,7 @@ def MindtPy_initialization(self): try: self.curr_int_sol = get_integer_solution(self.working_model) except TypeError as e: - config.logger.error(e) + config.logger.error(e, exc_info=True) raise ValueError( 'The initial integer combination is not provided or not complete. ' 'Please provide the complete integer combination or use other initialization strategy.' @@ -796,7 +807,10 @@ def MindtPy_initialization(self): self.integer_list.append(self.curr_int_sol) fixed_nlp, fixed_nlp_result = self.solve_subproblem() self.handle_nlp_subproblem_tc(fixed_nlp, fixed_nlp_result) - self.int_sol_2_cuts_ind[self.curr_int_sol] = list(range(1, len(self.mip.MindtPy_utils.cuts.oa_cuts) + 1)) + self.integer_solution_to_cuts_index[self.curr_int_sol] = [ + 1, + len(self.mip.MindtPy_utils.cuts.oa_cuts), + ] elif config.init_strategy == 'FP': self.init_rNLP() self.fp_loop() @@ -828,7 +842,7 @@ def init_rNLP(self, add_oa_cuts=True): results = self.nlp_opt.solve( self.rnlp, tee=config.nlp_solver_tee, - load_solutions=config.load_solutions, + load_solutions=self.nlp_load_solutions, **nlp_args, ) if len(results.solution) > 0: @@ -836,15 +850,21 @@ def init_rNLP(self, add_oa_cuts=True): subprob_terminate_cond = results.solver.termination_condition # Sometimes, the NLP solver might be trapped in a infeasible solution if the objective function is nonlinear and partition_obj_nonlinear_terms is True. If this happens, we will use the original objective function instead. - if subprob_terminate_cond == tc.infeasible and config.partition_obj_nonlinear_terms and self.rnlp.MindtPy_utils.objective_list[0].expr.polynomial_degree() not in self.mip_objective_polynomial_degree: + if ( + subprob_terminate_cond == tc.infeasible + and config.partition_obj_nonlinear_terms + and self.rnlp.MindtPy_utils.objective_list[0].expr.polynomial_degree() + not in self.mip_objective_polynomial_degree + ): config.logger.info( - 'Initial relaxed NLP problem is infeasible. This might be related to partition_obj_nonlinear_terms. Try to solve it again without partitioning nonlinear objective function.') + 'Initial relaxed NLP problem is infeasible. This might be related to partition_obj_nonlinear_terms. Trying to solve it again without partitioning nonlinear objective function.' + ) self.rnlp.MindtPy_utils.objective.deactivate() self.rnlp.MindtPy_utils.objective_list[0].activate() results = self.nlp_opt.solve( self.rnlp, tee=config.nlp_solver_tee, - load_solutions=config.load_solutions, + load_solutions=self.nlp_load_solutions, **nlp_args, ) if len(results.solution) > 0: @@ -891,14 +911,14 @@ def init_rNLP(self, add_oa_cuts=True): self.rnlp.MindtPy_utils.variable_list, self.mip.MindtPy_utils.variable_list, config, - ignore_integrality=True + ignore_integrality=True, ) if config.init_strategy == 'FP': copy_var_list_values( self.rnlp.MindtPy_utils.variable_list, self.working_model.MindtPy_utils.variable_list, config, - ignore_integrality=True + ignore_integrality=True, ) self.add_cuts( dual_values=dual_values, @@ -977,7 +997,7 @@ def init_max_binaries(self): results = self.mip_opt.solve( m, tee=config.mip_solver_tee, - load_solutions=config.load_solutions, + load_solutions=self.mip_load_solutions, **mip_args, ) if len(results.solution) > 0: @@ -1066,7 +1086,7 @@ def solve_subproblem(self): 0, c_geq * (rhs - value(c.body)) ) except (ValueError, OverflowError) as e: - config.logger.error(e) + config.logger.error(e, exc_info=True) self.fixed_nlp.tmp_duals[c] = None evaluation_error = True if evaluation_error: @@ -1083,8 +1103,9 @@ def solve_subproblem(self): tolerance=config.constraint_tolerance, ) except InfeasibleConstraintException as e: + config.logger.error(e, exc_info=True) config.logger.error( - str(e) + '\nInfeasibility detected in deactivate_trivial_constraints.' + 'Infeasibility detected in deactivate_trivial_constraints.' ) results = SolverResults() results.solver.termination_condition = tc.infeasible @@ -1097,7 +1118,7 @@ def solve_subproblem(self): results = self.nlp_opt.solve( self.fixed_nlp, tee=config.nlp_solver_tee, - load_solutions=config.load_solutions, + load_solutions=self.nlp_load_solutions, **nlp_args, ) if len(results.solution) > 0: @@ -1268,7 +1289,6 @@ def handle_subproblem_infeasible(self, fixed_nlp, cb_opt=None): # elif var.has_lb() and abs(value(var) - var.lb) < config.absolute_bound_tolerance: # fixed_nlp.ipopt_zU_out[var] = -1 - # config.logger.info('Solving feasibility problem') feas_subproblem, feas_subproblem_results = self.solve_feasibility_subproblem() # TODO: do we really need this? if self.should_terminate: @@ -1366,12 +1386,20 @@ def solve_feasibility_subproblem(self): update_solver_timelimit( self.feasibility_nlp_opt, config.nlp_solver, self.timing, config ) - TransformationFactory('contrib.deactivate_trivial_constraints').apply_to( - feas_subproblem, - tmp=True, - ignore_infeasible=False, - tolerance=config.constraint_tolerance, - ) + try: + TransformationFactory('contrib.deactivate_trivial_constraints').apply_to( + self.fixed_nlp, + tmp=True, + ignore_infeasible=False, + tolerance=config.constraint_tolerance, + ) + except InfeasibleConstraintException as e: + config.logger.error( + str(e) + '\nInfeasibility detected in deactivate_trivial_constraints.' + ) + results = SolverResults() + results.solver.termination_condition = tc.infeasible + return self.fixed_nlp, results with SuppressInfeasibleWarning(): try: with time_code(self.timing, 'feasibility subproblem'): @@ -1384,7 +1412,7 @@ def solve_feasibility_subproblem(self): if len(feas_soln.solution) > 0: feas_subproblem.solutions.load_from(feas_soln) except (ValueError, OverflowError) as e: - config.logger.error(e) + config.logger.error(e, exc_info=True) for nlp_var, orig_val in zip( MindtPy.variable_list, self.initial_var_values ): @@ -1525,9 +1553,8 @@ def fix_dual_bound(self, last_iter_cuts): try: self.dual_bound = self.stored_bound[self.primal_bound] except KeyError as e: - config.logger.error( - str(e) + '\nNo stored bound found. Bound fix failed.' - ) + config.logger.error(e, exc_info=True) + config.logger.error('No stored bound found. Bound fix failed.') else: config.logger.info( 'Solve the main problem without the last no_good cut to fix the bound.' @@ -1541,7 +1568,7 @@ def fix_dual_bound(self, last_iter_cuts): self.handle_nlp_subproblem_tc(fixed_nlp, fixed_nlp_result) MindtPy = self.mip.MindtPy_utils - # deactivate the integer cuts generated after the best solution was found. + # Deactivate the integer cuts generated after the best solution was found. self.deactivate_no_good_cuts_when_fixing_bound(MindtPy.cuts.no_good_cuts) if ( config.add_regularization is not None @@ -1558,7 +1585,7 @@ def fix_dual_bound(self, last_iter_cuts): main_mip_results = self.mip_opt.solve( self.mip, tee=config.mip_solver_tee, - load_solutions=config.load_solutions, + load_solutions=self.mip_load_solutions, **mip_args, ) if len(main_mip_results.solution) > 0: @@ -1646,14 +1673,14 @@ def solve_main(self): main_mip_results = self.mip_opt.solve( self.mip, tee=config.mip_solver_tee, - load_solutions=config.load_solutions, + load_solutions=self.mip_load_solutions, **mip_args, ) # update_attributes should be before load_from(main_mip_results), since load_from(main_mip_results) may fail. if len(main_mip_results.solution) > 0: self.mip.solutions.load_from(main_mip_results) except (ValueError, AttributeError, RuntimeError) as e: - config.logger.error(e) + config.logger.error(e, exc_info=True) if config.single_tree: config.logger.warning('Single tree terminate.') if get_main_elapsed_time(self.timing) >= config.time_limit: @@ -1667,7 +1694,7 @@ def solve_main(self): 'No integer solution is found, so the CPLEX solver will report an error status. ' ) # Value error will be raised if the MIP problem is unbounded and appsi solver is used when loading solutions. Although the problem is unbounded, a valid result is provided and we do not return None to let the algorithm continue. - if 'main_mip_results' in dir(): + if 'main_mip_results' in locals(): return self.mip, main_mip_results else: return None, None @@ -1702,14 +1729,12 @@ def solve_fp_main(self): config = self.config self.setup_fp_main() mip_args = self.set_up_mip_solver() - update_solver_timelimit( - self.mip_opt, config.mip_solver, self.timing, config - ) + update_solver_timelimit(self.mip_opt, config.mip_solver, self.timing, config) main_mip_results = self.mip_opt.solve( self.mip, tee=config.mip_solver_tee, - load_solutions=config.load_solutions, + load_solutions=self.mip_load_solutions, **mip_args, ) # update_attributes should be before load_from(main_mip_results), since load_from(main_mip_results) may fail. @@ -1752,7 +1777,7 @@ def solve_regularization_main(self): main_mip_results = self.regularization_mip_opt.solve( self.mip, tee=config.mip_solver_tee, - load_solutions=config.load_solutions, + load_solutions=self.regularization_mip_load_solutions, **dict(config.mip_solver_args), ) if len(main_mip_results.solution) > 0: @@ -1838,7 +1863,6 @@ def handle_main_optimal(self, main_mip, update_bound=True): f"Integer variable {var.name} not initialized. " "Setting it to its lower bound" ) - # nlp_var.bounds[0] var.set_value(var.lb, skip_validation=True) # warm start for the nlp subproblem copy_var_list_values( @@ -1904,11 +1928,6 @@ def handle_main_max_timelimit(self, main_mip, main_mip_results): """ # If we have found a valid feasible solution, we take that. If not, we can at least use the dual bound. MindtPy = main_mip.MindtPy_utils - self.config.logger.info( - 'Unable to optimize MILP main problem ' - 'within time limit. ' - 'Using current solver feasible solution.' - ) copy_var_list_values( main_mip.MindtPy_utils.variable_list, self.fixed_nlp.MindtPy_utils.variable_list, @@ -1917,10 +1936,10 @@ def handle_main_max_timelimit(self, main_mip, main_mip_results): ) self.update_suboptimal_dual_bound(main_mip_results) self.config.logger.info( - self.log_formatter.format( + self.termination_condition_log_formatter.format( self.mip_iter, 'MILP', - value(MindtPy.mip_obj.expr), + 'maxTimeLimit', self.primal_bound, self.dual_bound, self.rel_gap, @@ -1947,8 +1966,18 @@ def handle_main_unbounded(self, main_mip): # to the constraints, and deactivated for the linear main problem. config = self.config MindtPy = main_mip.MindtPy_utils + config.logger.info( + self.termination_condition_log_formatter.format( + self.mip_iter, + 'MILP', + 'Unbounded', + self.primal_bound, + self.dual_bound, + self.rel_gap, + get_main_elapsed_time(self.timing), + ) + ) config.logger.warning( - 'main MILP was unbounded. ' 'Resolving with arbitrary bound values of (-{0:.10g}, {0:.10g}) on the objective. ' 'You can change this bound with the option obj_bound.'.format( config.obj_bound @@ -1964,7 +1993,7 @@ def handle_main_unbounded(self, main_mip): main_mip_results = self.mip_opt.solve( main_mip, tee=config.mip_solver_tee, - load_solutions=config.load_solutions, + load_solutions=self.mip_load_solutions, **config.mip_solver_args, ) if len(main_mip_results.solution) > 0: @@ -2249,6 +2278,11 @@ def check_subsolver_validity(self): raise ValueError(self.config.mip_solver + ' is not available.') if not self.mip_opt.license_is_valid(): raise ValueError(self.config.mip_solver + ' is not licensed.') + if self.config.mip_solver == "appsi_highs": + if self.mip_opt.version() < (1, 7, 0): + raise ValueError( + "MindtPy requires the use of HIGHS version 1.7.0 or higher for full compatibility." + ) if not self.nlp_opt.available(): raise ValueError(self.config.nlp_solver + ' is not available.') if not self.nlp_opt.license_is_valid(): @@ -2296,15 +2330,15 @@ def check_config(self): config.mip_solver = 'cplex_persistent' # related to https://github.com/Pyomo/pyomo/issues/2363 + if 'appsi' in config.mip_solver: + self.mip_load_solutions = False + if 'appsi' in config.nlp_solver: + self.nlp_load_solutions = False if ( - 'appsi' in config.mip_solver - or 'appsi' in config.nlp_solver - or ( - config.mip_regularization_solver is not None - and 'appsi' in config.mip_regularization_solver - ) + config.mip_regularization_solver is not None + and 'appsi' in config.mip_regularization_solver ): - config.load_solutions = False + self.regularization_mip_load_solutions = False ################################################################################################################################ # Feasibility Pump @@ -2357,8 +2391,9 @@ def solve_fp_subproblem(self): tolerance=config.constraint_tolerance, ) except InfeasibleConstraintException as e: + config.logger.error(e, exc_info=True) config.logger.error( - str(e) + '\nInfeasibility detected in deactivate_trivial_constraints.' + 'Infeasibility detected in deactivate_trivial_constraints.' ) results = SolverResults() results.solver.termination_condition = tc.infeasible @@ -2371,7 +2406,7 @@ def solve_fp_subproblem(self): results = self.nlp_opt.solve( fp_nlp, tee=config.nlp_solver_tee, - load_solutions=config.load_solutions, + load_solutions=self.nlp_load_solutions, **nlp_args, ) if len(results.solution) > 0: @@ -2391,7 +2426,7 @@ def handle_fp_subproblem_optimal(self, fp_nlp): fp_nlp.MindtPy_utils.variable_list, self.working_model.MindtPy_utils.variable_list, self.config, - ignore_integrality=True + ignore_integrality=True, ) add_orthogonality_cuts(self.working_model, self.mip, self.config) @@ -2576,7 +2611,7 @@ def fp_loop(self): self.working_model.MindtPy_utils.cuts.del_component('fp_orthogonality_cuts') def initialize_mip_problem(self): - '''Deactivate the nonlinear constraints to create the MIP problem.''' + """Deactivate the nonlinear constraints to create the MIP problem.""" # if single tree is activated, we need to add bounds for unbounded variables in nonlinear constraints to avoid unbounded main problem. config = self.config if config.single_tree: @@ -2609,7 +2644,7 @@ def initialize_mip_problem(self): # if config.use_baron_convexification: # self.fixed_nlp.baroncuts.deactivate() TransformationFactory('core.fix_integer_vars').apply_to(self.fixed_nlp) - initialize_feas_subproblem(self.fixed_nlp, config) + initialize_feas_subproblem(self.fixed_nlp, config.feasibility_norm) def initialize_subsolvers(self): """Initialize and set options for MIP and NLP subsolvers.""" @@ -2634,7 +2669,10 @@ def initialize_subsolvers(self): set_solver_mipgap(self.mip_opt, config.mip_solver, config) set_solver_constraint_violation_tolerance( - self.nlp_opt, config.nlp_solver, config + self.nlp_opt, + config.nlp_solver, + config, + warm_start=config.warm_start_fixed_nlp, ) set_solver_constraint_violation_tolerance( self.feasibility_nlp_opt, config.nlp_solver, config, warm_start=False @@ -2659,9 +2697,9 @@ def initialize_subsolvers(self): if config.mip_regularization_solver == 'gams': self.regularization_mip_opt.options['add_options'] = [] if config.regularization_mip_threads > 0: - self.regularization_mip_opt.options[ - 'threads' - ] = config.regularization_mip_threads + self.regularization_mip_opt.options['threads'] = ( + config.regularization_mip_threads + ) else: self.regularization_mip_opt.options['threads'] = config.threads @@ -2671,9 +2709,9 @@ def initialize_subsolvers(self): 'cplex_persistent', }: if config.solution_limit is not None: - self.regularization_mip_opt.options[ - 'mip_limits_solutions' - ] = config.solution_limit + self.regularization_mip_opt.options['mip_limits_solutions'] = ( + config.solution_limit + ) # We don't need to solve the regularization problem to optimality. # We will choose to perform aggressive node probing during presolve. self.regularization_mip_opt.options['mip_strategy_presolvenode'] = 3 @@ -2686,9 +2724,9 @@ def initialize_subsolvers(self): self.regularization_mip_opt.options['optimalitytarget'] = 3 elif config.mip_regularization_solver == 'gurobi': if config.solution_limit is not None: - self.regularization_mip_opt.options[ - 'SolutionLimit' - ] = config.solution_limit + self.regularization_mip_opt.options['SolutionLimit'] = ( + config.solution_limit + ) # Same reason as mip_strategy_presolvenode. self.regularization_mip_opt.options['Presolve'] = 2 @@ -2941,6 +2979,10 @@ def MindtPy_iteration_loop(self): skip_fixed=False, ) if self.curr_int_sol not in set(self.integer_list): + # Call the NLP pre-solve callback + with time_code(self.timing, 'Call before subproblem solve'): + config.call_before_subproblem_solve(self.fixed_nlp) + fixed_nlp, fixed_nlp_result = self.solve_subproblem() self.handle_nlp_subproblem_tc(fixed_nlp, fixed_nlp_result) @@ -2952,6 +2994,10 @@ def MindtPy_iteration_loop(self): # Solve NLP subproblem # The constraint linearization happens in the handlers if not config.solution_pool: + # Call the NLP pre-solve callback + with time_code(self.timing, 'Call before subproblem solve'): + config.call_before_subproblem_solve(self.fixed_nlp) + fixed_nlp, fixed_nlp_result = self.solve_subproblem() self.handle_nlp_subproblem_tc(fixed_nlp, fixed_nlp_result) @@ -2984,6 +3030,11 @@ def MindtPy_iteration_loop(self): continue else: self.integer_list.append(self.curr_int_sol) + + # Call the NLP pre-solve callback + with time_code(self.timing, 'Call before subproblem solve'): + config.call_before_subproblem_solve(self.fixed_nlp) + fixed_nlp, fixed_nlp_result = self.solve_subproblem() self.handle_nlp_subproblem_tc(fixed_nlp, fixed_nlp_result) @@ -2997,10 +3048,12 @@ def MindtPy_iteration_loop(self): # if add_no_good_cuts is True, the bound obtained in the last iteration is no reliable. # we correct it after the iteration. + # There is no need to fix the dual bound if no feasible solution has been found. if ( (config.add_no_good_cuts or config.use_tabu_list) and not self.should_terminate and config.add_regularization is None + and self.best_solution_found is not None ): self.fix_dual_bound(self.last_iter_cuts) config.logger.info( @@ -3038,10 +3091,9 @@ def add_regularization(self): # The main problem might be unbounded, regularization is activated only when a valid bound is provided. if self.dual_bound != self.dual_bound_progress[0]: with time_code(self.timing, 'regularization main'): - ( - regularization_main_mip, - regularization_main_mip_results, - ) = self.solve_regularization_main() + (regularization_main_mip, regularization_main_mip_results) = ( + self.solve_regularization_main() + ) self.handle_regularization_main_tc( regularization_main_mip, regularization_main_mip_results ) diff --git a/pyomo/contrib/mindtpy/config_options.py b/pyomo/contrib/mindtpy/config_options.py index b6389493fb5..d2863243ad8 100644 --- a/pyomo/contrib/mindtpy/config_options.py +++ b/pyomo/contrib/mindtpy/config_options.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- import logging from pyomo.common.config import ( @@ -312,6 +323,15 @@ def _add_common_configs(CONFIG): doc='Callback hook after a solution of the main problem.', ), ) + CONFIG.declare( + 'call_before_subproblem_solve', + ConfigValue( + default=_DoNothing(), + domain=None, + description='Function to be executed before every subproblem', + doc='Callback hook before a solution of the nonlinear subproblem.', + ), + ) CONFIG.declare( 'call_after_subproblem_solve', ConfigValue( @@ -502,12 +522,15 @@ def _add_common_configs(CONFIG): domain=bool, ), ) - CONFIG.declare("use_baron_convexification", ConfigValue( - default=False, - domain=bool, - description="use baron to provide the convex relations for nonconvex MINLPs.", - doc="use baron to provide the convex relations for nonconvex MINLPs." - )) + CONFIG.declare( + "use_baron_convexification", + ConfigValue( + default=False, + domain=bool, + description="use baron to provide the convex relations for nonconvex MINLPs.", + doc="use baron to provide the convex relations for nonconvex MINLPs.", + ), + ) def _add_subsolver_configs(CONFIG): @@ -552,7 +575,7 @@ def _add_subsolver_configs(CONFIG): 'cplex_persistent', 'appsi_cplex', 'appsi_gurobi', - # 'appsi_highs', TODO: feasibility pump now fails with appsi_highs #2951 + 'appsi_highs', ] ), description='MIP subsolver name', @@ -634,13 +657,21 @@ def _add_subsolver_configs(CONFIG): 'cplex_persistent', 'appsi_cplex', 'appsi_gurobi', - # 'appsi_highs', + 'appsi_highs', ] ), description='MIP subsolver for regularization problem', doc='Which MIP subsolver is going to be used for solving the regularization problem.', ), ) + CONFIG.declare( + 'warm_start_fixed_nlp', + ConfigValue( + default=True, + description='whether to warm start the fixed NLP subproblem.', + domain=bool, + ), + ) def _add_tolerance_configs(CONFIG): diff --git a/pyomo/contrib/mindtpy/cut_generation.py b/pyomo/contrib/mindtpy/cut_generation.py index 3fd7702efd2..6783f23574d 100644 --- a/pyomo/contrib/mindtpy/cut_generation.py +++ b/pyomo/contrib/mindtpy/cut_generation.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -113,7 +113,7 @@ def add_oa_cuts( constr.has_ub() and ( linearize_active - and abs(constr.uslack()) < config.zero_tolerance + and abs(constr.uslack()) < config.constraint_tolerance ) or (linearize_violated and constr.uslack() < 0) or (config.linearize_inactive and constr.uslack() > 0) @@ -151,7 +151,7 @@ def add_oa_cuts( constr.has_lb() and ( linearize_active - and abs(constr.lslack()) < config.zero_tolerance + and abs(constr.lslack()) < config.constraint_tolerance ) or (linearize_violated and constr.lslack() < 0) or (config.linearize_inactive and constr.lslack() > 0) @@ -200,6 +200,7 @@ def add_oa_cuts_for_grey_box( .evaluate_jacobian_outputs() .toarray() ) + # Enumerate over values works well now. However, it might be stable if the values() method changes. for index, output in enumerate(target_model_grey_box.outputs.values()): dual_value = jacobians_model.dual[jacobian_model_grey_box][ output.name.replace("outputs", "output_constraints") @@ -213,8 +214,8 @@ def add_oa_cuts_for_grey_box( target_model_grey_box.inputs.values() ) ) + - (output - value(output)) ) - - (output - value(output)) - (slack_var if config.add_slack else 0) <= 0 ) @@ -274,8 +275,9 @@ def add_ecp_cuts( try: upper_slack = constr.uslack() except (ValueError, OverflowError) as e: + config.logger.error(e, exc_info=True) config.logger.error( - str(e) + '\nConstraint {} has caused either a ' + 'Constraint {} has caused either a ' 'ValueError or OverflowError.' '\n'.format(constr) ) @@ -303,8 +305,9 @@ def add_ecp_cuts( try: lower_slack = constr.lslack() except (ValueError, OverflowError) as e: + config.logger.error(e, exc_info=True) config.logger.error( - str(e) + '\nConstraint {} has caused either a ' + 'Constraint {} has caused either a ' 'ValueError or OverflowError.' '\n'.format(constr) ) @@ -427,9 +430,9 @@ def add_affine_cuts(target_model, config, timing): try: mc_eqn = mc(constr.body) except MCPP_Error as e: + config.logger.error(e, exc_info=True) config.logger.error( - '\nSkipping constraint %s due to MCPP error %s' - % (constr.name, str(e)) + 'Skipping constraint %s due to MCPP error' % (constr.name) ) continue # skip to the next constraint diff --git a/pyomo/contrib/mindtpy/extended_cutting_plane.py b/pyomo/contrib/mindtpy/extended_cutting_plane.py index 446304b1361..7bb3ff783c9 100644 --- a/pyomo/contrib/mindtpy/extended_cutting_plane.py +++ b/pyomo/contrib/mindtpy/extended_cutting_plane.py @@ -3,7 +3,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -66,12 +66,6 @@ def MindtPy_iteration_loop(self): add_ecp_cuts(self.mip, self.jacobians, self.config, self.timing) - # if add_no_good_cuts is True, the bound obtained in the last iteration is no reliable. - # we correct it after the iteration. - if ( - self.config.add_no_good_cuts or self.config.use_tabu_list - ) and not self.should_terminate: - self.fix_dual_bound(self.last_iter_cuts) self.config.logger.info( ' ===============================================================================================' ) @@ -84,9 +78,12 @@ def check_config(self): super().check_config() def initialize_mip_problem(self): - '''Deactivate the nonlinear constraints to create the MIP problem.''' + """Deactivate the nonlinear constraints to create the MIP problem.""" super().initialize_mip_problem() - self.jacobians = calc_jacobians(self.mip, self.config) # preload jacobians + self.jacobians = calc_jacobians( + self.mip.MindtPy_utils.nonlinear_constraint_list, + self.config.differentiate_mode, + ) # preload jacobians self.mip.MindtPy_utils.cuts.ecp_cuts = ConstraintList( doc='Extended Cutting Planes' ) @@ -140,7 +137,7 @@ def all_nonlinear_constraint_satisfied(self): lower_slack = nlc.lslack() except (ValueError, OverflowError) as e: # Set lower_slack (upper_slack below) less than -config.ecp_tolerance in this case. - config.logger.error(e) + config.logger.error(e, exc_info=True) lower_slack = -10 * config.ecp_tolerance if lower_slack < -config.ecp_tolerance: config.logger.debug( @@ -153,7 +150,7 @@ def all_nonlinear_constraint_satisfied(self): try: upper_slack = nlc.uslack() except (ValueError, OverflowError) as e: - config.logger.error(e) + config.logger.error(e, exc_info=True) upper_slack = -10 * config.ecp_tolerance if upper_slack < -config.ecp_tolerance: config.logger.debug( diff --git a/pyomo/contrib/mindtpy/feasibility_pump.py b/pyomo/contrib/mindtpy/feasibility_pump.py index 990f56b8f93..5ee1260dd42 100644 --- a/pyomo/contrib/mindtpy/feasibility_pump.py +++ b/pyomo/contrib/mindtpy/feasibility_pump.py @@ -3,7 +3,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -44,9 +44,12 @@ def check_config(self): super().check_config() def initialize_mip_problem(self): - '''Deactivate the nonlinear constraints to create the MIP problem.''' + """Deactivate the nonlinear constraints to create the MIP problem.""" super().initialize_mip_problem() - self.jacobians = calc_jacobians(self.mip, self.config) # preload jacobians + self.jacobians = calc_jacobians( + self.mip.MindtPy_utils.nonlinear_constraint_list, + self.config.differentiate_mode, + ) # preload jacobians self.mip.MindtPy_utils.cuts.oa_cuts = ConstraintList( doc='Outer approximation cuts' ) diff --git a/pyomo/contrib/mindtpy/global_outer_approximation.py b/pyomo/contrib/mindtpy/global_outer_approximation.py index dfb7ef54630..c43409a8493 100644 --- a/pyomo/contrib/mindtpy/global_outer_approximation.py +++ b/pyomo/contrib/mindtpy/global_outer_approximation.py @@ -3,7 +3,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -67,7 +67,7 @@ def check_config(self): super().check_config() def initialize_mip_problem(self): - '''Deactivate the nonlinear constraints to create the MIP problem.''' + """Deactivate the nonlinear constraints to create the MIP problem.""" super().initialize_mip_problem() self.mip.MindtPy_utils.cuts.aff_cuts = ConstraintList(doc='Affine cuts') @@ -108,4 +108,5 @@ def deactivate_no_good_cuts_when_fixing_bound(self, no_good_cuts): if self.config.use_tabu_list: self.integer_list = self.integer_list[:valid_no_good_cuts_num] except KeyError as e: - self.config.logger.error(str(e) + '\nDeactivating no-good cuts failed.') + self.config.logger.error(e, exc_info=True) + self.config.logger.error('Deactivating no-good cuts failed.') diff --git a/pyomo/contrib/mindtpy/outer_approximation.py b/pyomo/contrib/mindtpy/outer_approximation.py index 6cf0b26cb37..ead5cadfeac 100644 --- a/pyomo/contrib/mindtpy/outer_approximation.py +++ b/pyomo/contrib/mindtpy/outer_approximation.py @@ -3,7 +3,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -94,9 +94,12 @@ def check_config(self): _MindtPyAlgorithm.check_config(self) def initialize_mip_problem(self): - '''Deactivate the nonlinear constraints to create the MIP problem.''' + """Deactivate the nonlinear constraints to create the MIP problem.""" super().initialize_mip_problem() - self.jacobians = calc_jacobians(self.mip, self.config) # preload jacobians + self.jacobians = calc_jacobians( + self.mip.MindtPy_utils.nonlinear_constraint_list, + self.config.differentiate_mode, + ) # preload jacobians self.mip.MindtPy_utils.cuts.oa_cuts = ConstraintList( doc='Outer approximation cuts' ) diff --git a/pyomo/contrib/mindtpy/plugins.py b/pyomo/contrib/mindtpy/plugins.py index f25706d086a..bf0ab0d1581 100644 --- a/pyomo/contrib/mindtpy/plugins.py +++ b/pyomo/contrib/mindtpy/plugins.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index 98365568ce6..98916a2a798 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -16,15 +16,15 @@ from pyomo.repn import generate_standard_repn import pyomo.core.expr as EXPR from math import copysign -from pyomo.contrib.mindtpy.util import get_integer_solution, copy_var_list_values -from pyomo.contrib.gdpopt.util import ( - get_main_elapsed_time, - time_code, +from pyomo.contrib.mindtpy.util import ( + get_integer_solution, + copy_var_list_values, + set_var_valid_value, ) +from pyomo.contrib.gdpopt.util import get_main_elapsed_time, time_code from pyomo.opt import TerminationCondition as tc from pyomo.core import minimize, value from pyomo.core.expr import identify_variables -import math cplex, cplex_available = attempt_import('cplex') @@ -35,16 +35,9 @@ class LazyOACallback_cplex( """Inherent class in CPLEX to call Lazy callback.""" def copy_lazy_var_list_values( - self, - opt, - from_list, - to_list, - config, - skip_stale=False, - skip_fixed=True, + self, opt, from_list, to_list, config, skip_stale=False, skip_fixed=True ): """This function copies variable values from one list to another. - Rounds to Binary/Integer if necessary. Sets to zero for NonNegativeReals if necessary. @@ -53,17 +46,15 @@ def copy_lazy_var_list_values( opt : SolverFactory The cplex_persistent solver. from_list : list - The variables that provides the values to copy from. + The variable list that provides the values to copy from. to_list : list - The variables that need to set value. + The variable list that needs to set value. config : ConfigBlock The specific configurations for MindtPy. skip_stale : bool, optional Whether to skip the stale variables, by default False. skip_fixed : bool, optional Whether to skip the fixed variables, by default True. - ignore_integrality : bool, optional - Whether to ignore the integrality of integer variables, by default False. """ for v_from, v_to in zip(from_list, to_list): if skip_stale and v_from.stale: @@ -71,43 +62,13 @@ def copy_lazy_var_list_values( if skip_fixed and v_to.is_fixed(): continue # Skip fixed variables. v_val = self.get_values(opt._pyomo_var_to_solver_var_map[v_from]) - rounded_val = int(round(v_val)) - # We don't want to trigger the reset of the global stale - # indicator, so we will set this variable to be "stale", - # knowing that set_value will switch it back to "not - # stale" - v_to.stale = True - # NOTE: PEP 2180 changes the var behavior so that domain - # / bounds violations no longer generate exceptions (and - # instead log warnings). This means that the following - # will always succeed and the ValueError should never be - # raised. - if v_val in v_to.domain \ - and not ((v_to.has_lb() and v_val < v_to.lb)) \ - and not ((v_to.has_ub() and v_val > v_to.ub)): - v_to.set_value(v_val) - # Snap the value to the bounds - # TODO: check the performance of - # v_to.lb - v_val <= config.variable_tolerance - elif ( - v_to.has_lb() - and v_val < v_to.lb - # and v_to.lb - v_val <= config.variable_tolerance - ): - v_to.set_value(v_to.lb) - elif ( - v_to.has_ub() - and v_val > v_to.ub - # and v_val - v_to.ub <= config.variable_tolerance - ): - v_to.set_value(v_to.ub) - # ... or the nearest integer - elif v_to.is_integer() and math.fabs(v_val - rounded_val) <= config.integer_tolerance: # and rounded_val in v_to.domain: - v_to.set_value(rounded_val) - elif abs(v_val) <= config.zero_tolerance and 0 in v_to.domain: - v_to.set_value(0) - else: - raise ValueError('copy_lazy_var_list_values failed.') + set_var_valid_value( + v_to, + v_val, + config.integer_tolerance, + config.zero_tolerance, + ignore_integrality=False, + ) def add_lazy_oa_cuts( self, @@ -201,7 +162,7 @@ def add_lazy_oa_cuts( constr.has_ub() and ( linearize_active - and abs(constr.uslack()) < config.zero_tolerance + and abs(constr.uslack()) < config.constraint_tolerance ) or (linearize_violated and constr.uslack() < 0) or (config.linearize_inactive and constr.uslack() > 0) @@ -240,7 +201,7 @@ def add_lazy_oa_cuts( constr.has_lb() and ( linearize_active - and abs(constr.lslack()) < config.zero_tolerance + and abs(constr.lslack()) < config.constraint_tolerance ) or (linearize_violated and constr.lslack() < 0) or (config.linearize_inactive and constr.lslack() > 0) @@ -308,12 +269,11 @@ def add_lazy_affine_cuts(self, mindtpy_solver, config, opt): try: mc_eqn = mc(constr.body) except MCPP_Error as e: + config.logger.error(e, exc_info=True) config.logger.debug( - 'Skipping constraint %s due to MCPP error %s' - % (constr.name, str(e)) + 'Skipping constraint %s due to MCPP error' % (constr.name) ) continue # skip to the next constraint - # TODO: check if the value of ccSlope and cvSlope is not Nan or inf. If so, we skip this. ccSlope = mc_eqn.subcc() cvSlope = mc_eqn.subcv() ccStart = mc_eqn.concave() @@ -628,10 +588,9 @@ def handle_lazy_subproblem_infeasible(self, fixed_nlp, mindtpy_solver, config, o dual_values = None config.logger.info('Solving feasibility problem') - ( - feas_subproblem, - feas_subproblem_results, - ) = mindtpy_solver.solve_feasibility_subproblem() + (feas_subproblem, feas_subproblem_results) = ( + mindtpy_solver.solve_feasibility_subproblem() + ) # In OA algorithm, OA cuts are generated based on the solution of the subproblem # We need to first copy the value of variables from the subproblem and then add cuts copy_var_list_values( @@ -705,6 +664,7 @@ def __call__(self): main_mip = self.main_mip mindtpy_solver = self.mindtpy_solver + # The lazy constraint callback may be invoked during MIP start processing. In that case get_solution_source returns mip_start_solution. # Reference: https://www.ibm.com/docs/en/icos/22.1.1?topic=SSSA5P_22.1.1/ilog.odms.cplex.help/refpythoncplex/html/cplex.callbacks.SolutionSource-class.htm # Another solution source is user_solution = 118, but it will not be encountered in LazyConstraintCallback. config.logger.debug( @@ -728,6 +688,7 @@ def __call__(self): mindtpy_solver.mip_start_lazy_oa_cuts = [] if mindtpy_solver.should_terminate: + # TODO: check the performance difference if we don't use self.abort() and let cplex terminate by itself. self.abort() return self.handle_lazy_main_feasible_solution(main_mip, mindtpy_solver, config, opt) @@ -745,9 +706,9 @@ def __call__(self): mindtpy_solver.mip, None, mindtpy_solver, config, opt ) except ValueError as e: + config.logger.error(e, exc_info=True) config.logger.error( - str(e) - + "\nUsually this error is caused by the MIP start solution causing a math domain error. " + "Usually this error is caused by the MIP start solution causing a math domain error. " "We will skip it." ) return @@ -783,6 +744,7 @@ def __call__(self): ) ) mindtpy_solver.results.solver.termination_condition = tc.optimal + # TODO: check the performance difference if we don't use self.abort() and let cplex terminate by itself. self.abort() return @@ -811,6 +773,9 @@ def __call__(self): mindtpy_solver.integer_list.append(mindtpy_solver.curr_int_sol) # solve subproblem + # Call the NLP pre-solve callback + with time_code(mindtpy_solver.timing, 'Call before subproblem solve'): + config.call_before_subproblem_solve(mindtpy_solver.fixed_nlp) # The constraint linearization happens in the handlers fixed_nlp, fixed_nlp_result = mindtpy_solver.solve_subproblem() # add oa cuts @@ -945,7 +910,10 @@ def LazyOACallback_gurobi(cb_m, cb_opt, cb_where, mindtpy_solver, config): # Your callback should be prepared to cut off solutions that violate any of your lazy constraints, including those that have already been added. Node solutions will usually respect previously added lazy constraints, but not always. # https://www.gurobi.com/documentation/current/refman/cs_cb_addlazy.html # If this happens, MindtPy will look for the index of corresponding cuts, instead of solving the fixed-NLP again. - for ind in mindtpy_solver.int_sol_2_cuts_ind[mindtpy_solver.curr_int_sol]: + begin_index, end_index = mindtpy_solver.integer_solution_to_cuts_index[ + mindtpy_solver.curr_int_sol + ] + for ind in range(begin_index, end_index + 1): cb_opt.cbLazy(mindtpy_solver.mip.MindtPy_utils.cuts.oa_cuts[ind]) return else: @@ -954,13 +922,18 @@ def LazyOACallback_gurobi(cb_m, cb_opt, cb_where, mindtpy_solver, config): cut_ind = len(mindtpy_solver.mip.MindtPy_utils.cuts.oa_cuts) # solve subproblem + # Call the NLP pre-solve callback + with time_code(mindtpy_solver.timing, 'Call before subproblem solve'): + config.call_before_subproblem_solve(mindtpy_solver.fixed_nlp) # The constraint linearization happens in the handlers fixed_nlp, fixed_nlp_result = mindtpy_solver.solve_subproblem() mindtpy_solver.handle_nlp_subproblem_tc(fixed_nlp, fixed_nlp_result, cb_opt) if config.strategy == 'OA': # store the cut index corresponding to current integer solution. - mindtpy_solver.int_sol_2_cuts_ind[mindtpy_solver.curr_int_sol] = list(range(cut_ind + 1, len(mindtpy_solver.mip.MindtPy_utils.cuts.oa_cuts) + 1)) + mindtpy_solver.integer_solution_to_cuts_index[ + mindtpy_solver.curr_int_sol + ] = [cut_ind + 1, len(mindtpy_solver.mip.MindtPy_utils.cuts.oa_cuts)] def handle_lazy_main_feasible_solution_gurobi(cb_m, cb_opt, mindtpy_solver, config): diff --git a/pyomo/contrib/mindtpy/tabu_list.py b/pyomo/contrib/mindtpy/tabu_list.py index 313bd6f6271..15c1d3b3a2b 100644 --- a/pyomo/contrib/mindtpy/tabu_list.py +++ b/pyomo/contrib/mindtpy/tabu_list.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/tests/MINLP2_simple.py b/pyomo/contrib/mindtpy/tests/MINLP2_simple.py index 10da243d332..f3fd51af79a 100644 --- a/pyomo/contrib/mindtpy/tests/MINLP2_simple.py +++ b/pyomo/contrib/mindtpy/tests/MINLP2_simple.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/tests/MINLP3_simple.py b/pyomo/contrib/mindtpy/tests/MINLP3_simple.py index f387b0e26a1..a17659e0c51 100644 --- a/pyomo/contrib/mindtpy/tests/MINLP3_simple.py +++ b/pyomo/contrib/mindtpy/tests/MINLP3_simple.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/tests/MINLP4_simple.py b/pyomo/contrib/mindtpy/tests/MINLP4_simple.py index 7b57c6b8f0d..44b6c7df543 100644 --- a/pyomo/contrib/mindtpy/tests/MINLP4_simple.py +++ b/pyomo/contrib/mindtpy/tests/MINLP4_simple.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """ Example 1 in Paper 'Using regularization and second order information in outer approximation for convex MINLP' diff --git a/pyomo/contrib/mindtpy/tests/MINLP5_simple.py b/pyomo/contrib/mindtpy/tests/MINLP5_simple.py index 5ab5f98b894..d5b04d0915c 100644 --- a/pyomo/contrib/mindtpy/tests/MINLP5_simple.py +++ b/pyomo/contrib/mindtpy/tests/MINLP5_simple.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """Example in paper 'Using regularization and second order information in outer approximation for convex MINLP' diff --git a/pyomo/contrib/mindtpy/tests/MINLP_simple.py b/pyomo/contrib/mindtpy/tests/MINLP_simple.py index 91976997c34..cde65536f43 100644 --- a/pyomo/contrib/mindtpy/tests/MINLP_simple.py +++ b/pyomo/contrib/mindtpy/tests/MINLP_simple.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -38,14 +38,10 @@ Block, ) from pyomo.common.collections import ComponentMap -from pyomo.contrib.mindtpy.tests.MINLP_simple_grey_box import GreyBoxModel -from pyomo.contrib.pynumero.interfaces.external_grey_box import ExternalGreyBoxBlock - - -def build_model_external(m): - ex_model = GreyBoxModel(initial={"X1": 0, "X2": 0, "Y1": 0, "Y2": 1, "Y3": 1}) - m.egb = ExternalGreyBoxBlock() - m.egb.set_external_model(ex_model) +from pyomo.contrib.mindtpy.tests.MINLP_simple_grey_box import ( + GreyBoxModel, + build_model_external, +) class SimpleMINLP(ConcreteModel): @@ -54,6 +50,10 @@ class SimpleMINLP(ConcreteModel): def __init__(self, grey_box=False, *args, **kwargs): """Create the problem.""" kwargs.setdefault('name', 'SimpleMINLP') + if grey_box and GreyBoxModel is None: + m = None + return + super(SimpleMINLP, self).__init__(*args, **kwargs) m = self diff --git a/pyomo/contrib/mindtpy/tests/MINLP_simple_grey_box.py b/pyomo/contrib/mindtpy/tests/MINLP_simple_grey_box.py index 186db3bb5a2..412067de0b5 100644 --- a/pyomo/contrib/mindtpy/tests/MINLP_simple_grey_box.py +++ b/pyomo/contrib/mindtpy/tests/MINLP_simple_grey_box.py @@ -1,138 +1,162 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.common.dependencies import numpy as np import pyomo.common.dependencies.scipy.sparse as scipy_sparse -from pyomo.contrib.pynumero.interfaces.external_grey_box import ExternalGreyBoxModel -from pyomo.contrib.pynumero.interfaces.external_grey_box import ExternalGreyBoxBlock - +from pyomo.common.dependencies import attempt_import + +egb, egb_available = attempt_import( + 'pyomo.contrib.pynumero.interfaces.external_grey_box' +) + +if egb_available: + + class GreyBoxModel(egb.ExternalGreyBoxModel): + """Greybox model to compute the example objective function.""" + + def __init__(self, initial, use_exact_derivatives=True, verbose=False): + """ + Parameters + + use_exact_derivatives: bool + If True, the exact derivatives are used. If False, the finite difference + approximation is used. + verbose: bool + If True, print information about the model. + """ + self._use_exact_derivatives = use_exact_derivatives + self.verbose = verbose + self.initial = initial + + # For use with exact Hessian + self._output_con_mult_values = np.zeros(1) + + if not use_exact_derivatives: + raise NotImplementedError( + "use_exact_derivatives == False not supported" + ) + + def input_names(self): + """Return the names of the inputs.""" + self.input_name_list = ["X1", "X2", "Y1", "Y2", "Y3"] + + return self.input_name_list + + def equality_constraint_names(self): + """Return the names of the equality constraints.""" + # no equality constraints + return [] + + def output_names(self): + """Return the names of the outputs.""" + return ['z'] + + def set_output_constraint_multipliers(self, output_con_multiplier_values): + """Set the values of the output constraint multipliers.""" + # because we only have one output constraint + assert len(output_con_multiplier_values) == 1 + np.copyto(self._output_con_mult_values, output_con_multiplier_values) + + def finalize_block_construction(self, pyomo_block): + """Finalize the construction of the ExternalGreyBoxBlock.""" + if self.initial is not None: + print("initialized") + pyomo_block.inputs["X1"].value = self.initial["X1"] + pyomo_block.inputs["X2"].value = self.initial["X2"] + pyomo_block.inputs["Y1"].value = self.initial["Y1"] + pyomo_block.inputs["Y2"].value = self.initial["Y2"] + pyomo_block.inputs["Y3"].value = self.initial["Y3"] + + else: + print("uninitialized") + for n in self.input_name_list: + pyomo_block.inputs[n].value = 1 + + pyomo_block.inputs["X1"].setub(4) + pyomo_block.inputs["X1"].setlb(0) + + pyomo_block.inputs["X2"].setub(4) + pyomo_block.inputs["X2"].setlb(0) + + pyomo_block.inputs["Y1"].setub(1) + pyomo_block.inputs["Y1"].setlb(0) + + pyomo_block.inputs["Y2"].setub(1) + pyomo_block.inputs["Y2"].setlb(0) + + pyomo_block.inputs["Y3"].setub(1) + pyomo_block.inputs["Y3"].setlb(0) + + def set_input_values(self, input_values): + """Set the values of the inputs.""" + self._input_values = list(input_values) + + def evaluate_equality_constraints(self): + """Evaluate the equality constraints.""" + return None + + def evaluate_outputs(self): + """Evaluate the output of the model.""" + # form matrix as a list of lists + # M = self._extract_and_assemble_fim() + x1 = self._input_values[0] + x2 = self._input_values[1] + y1 = self._input_values[2] + y2 = self._input_values[3] + y3 = self._input_values[4] + # z + z = x1**2 + x2**2 + y1 + 1.5 * y2 + 0.5 * y3 + + if self.verbose: + print("\n Consider inputs [x1,x2,y1,y2,y3] =\n", x1, x2, y1, y2, y3) + print(" z = ", z, "\n") + + return np.asarray([z], dtype=np.float64) + + def evaluate_jacobian_equality_constraints(self): + """Evaluate the Jacobian of the equality constraints.""" + return None -class GreyBoxModel(ExternalGreyBoxModel): - """Greybox model to compute the example OF.""" - - def __init__(self, initial, use_exact_derivatives=True, verbose=True): """ - Parameters + def _extract_and_assemble_fim(self): + M = np.zeros((self.n_parameters, self.n_parameters)) + for i in range(self.n_parameters): + for k in range(self.n_parameters): + M[i,k] = self._input_values[self.ele_to_order[(i,k)]] - use_exact_derivatives: bool - If True, the exact derivatives are used. If False, the finite difference - approximation is used. - verbose: bool - If True, print information about the model. + return M """ - self._use_exact_derivatives = use_exact_derivatives - self.verbose = verbose - self.initial = initial - - # For use with exact Hessian - self._output_con_mult_values = np.zeros(1) - - if not use_exact_derivatives: - raise NotImplementedError("use_exact_derivatives == False not supported") - - def input_names(self): - """Return the names of the inputs.""" - self.input_name_list = ["X1", "X2", "Y1", "Y2", "Y3"] - - return self.input_name_list - - def equality_constraint_names(self): - """Return the names of the equality constraints.""" - # no equality constraints - return [] - - def output_names(self): - """Return the names of the outputs.""" - return ['z'] - - def set_output_constraint_multipliers(self, output_con_multiplier_values): - """Set the values of the output constraint multipliers.""" - # because we only have one output constraint - assert len(output_con_multiplier_values) == 1 - np.copyto(self._output_con_mult_values, output_con_multiplier_values) - - def finalize_block_construction(self, pyomo_block): - """Finalize the construction of the ExternalGreyBoxBlock.""" - if self.initial is not None: - print("initialized") - pyomo_block.inputs["X1"].value = self.initial["X1"] - pyomo_block.inputs["X2"].value = self.initial["X2"] - pyomo_block.inputs["Y1"].value = self.initial["Y1"] - pyomo_block.inputs["Y2"].value = self.initial["Y2"] - pyomo_block.inputs["Y3"].value = self.initial["Y3"] - - else: - print("uninitialized") - for n in self.input_name_list: - pyomo_block.inputs[n].value = 1 - - pyomo_block.inputs["X1"].setub(4) - pyomo_block.inputs["X1"].setlb(0) - - pyomo_block.inputs["X2"].setub(4) - pyomo_block.inputs["X2"].setlb(0) - - pyomo_block.inputs["Y1"].setub(1) - pyomo_block.inputs["Y1"].setlb(0) - - pyomo_block.inputs["Y2"].setub(1) - pyomo_block.inputs["Y2"].setlb(0) - - pyomo_block.inputs["Y3"].setub(1) - pyomo_block.inputs["Y3"].setlb(0) - - def set_input_values(self, input_values): - """Set the values of the inputs.""" - self._input_values = list(input_values) - - def evaluate_equality_constraints(self): - """Evaluate the equality constraints.""" - # Not sure what this function should return with no equality constraints - return None - - def evaluate_outputs(self): - """Evaluate the output of the model.""" - # form matrix as a list of lists - # M = self._extract_and_assemble_fim() - x1 = self._input_values[0] - x2 = self._input_values[1] - y1 = self._input_values[2] - y2 = self._input_values[3] - y3 = self._input_values[4] - # z - z = x1**2 + x2**2 + y1 + 1.5 * y2 + 0.5 * y3 - - if self.verbose: - pass - # print("\n Consider inputs [x1,x2,y1,y2,y3] =\n",x1, x2, y1, y2, y3) - # print(" z = ",z,"\n") - - return np.asarray([z], dtype=np.float64) - - def evaluate_jacobian_equality_constraints(self): - """Evaluate the Jacobian of the equality constraints.""" - return None - - ''' - def _extract_and_assemble_fim(self): - M = np.zeros((self.n_parameters, self.n_parameters)) - for i in range(self.n_parameters): - for k in range(self.n_parameters): - M[i,k] = self._input_values[self.ele_to_order[(i,k)]] - - return M - ''' - - def evaluate_jacobian_outputs(self): - """Evaluate the Jacobian of the outputs.""" - if self._use_exact_derivatives: - # compute gradient of log determinant - row = np.zeros(5) # to store row index - col = np.zeros(5) # to store column index - data = np.zeros(5) # to store data - - row[0], col[0], data[0] = (0, 0, 2 * self._input_values[0]) # x1 - row[0], col[1], data[1] = (0, 1, 2 * self._input_values[1]) # x2 - row[0], col[2], data[2] = (0, 2, 1) # y1 - row[0], col[3], data[3] = (0, 3, 1.5) # y2 - row[0], col[4], data[4] = (0, 4, 0.5) # y3 - - # sparse matrix - return scipy_sparse.coo_matrix((data, (row, col)), shape=(1, 5)) + + def evaluate_jacobian_outputs(self): + """Evaluate the Jacobian of the outputs.""" + if self._use_exact_derivatives: + # compute gradient of log determinant + row = np.zeros(5) # to store row index + col = np.zeros(5) # to store column index + data = np.zeros(5) # to store data + + row[0], col[0], data[0] = (0, 0, 2 * self._input_values[0]) # x1 + row[0], col[1], data[1] = (0, 1, 2 * self._input_values[1]) # x2 + row[0], col[2], data[2] = (0, 2, 1) # y1 + row[0], col[3], data[3] = (0, 3, 1.5) # y2 + row[0], col[4], data[4] = (0, 4, 0.5) # y3 + + # sparse matrix + return scipy_sparse.coo_matrix((data, (row, col)), shape=(1, 5)) + + def build_model_external(m): + ex_model = GreyBoxModel(initial={"X1": 0, "X2": 0, "Y1": 0, "Y2": 1, "Y3": 1}) + m.egb = egb.ExternalGreyBoxBlock() + m.egb.set_external_model(ex_model) + +else: + GreyBoxModel = None + build_model_external = None diff --git a/pyomo/contrib/mindtpy/tests/__init__.py b/pyomo/contrib/mindtpy/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/mindtpy/tests/__init__.py +++ b/pyomo/contrib/mindtpy/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/mindtpy/tests/constraint_qualification_example.py b/pyomo/contrib/mindtpy/tests/constraint_qualification_example.py index 6038f9a74eb..c0849094300 100644 --- a/pyomo/contrib/mindtpy/tests/constraint_qualification_example.py +++ b/pyomo/contrib/mindtpy/tests/constraint_qualification_example.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """ Example of constraint qualification. diff --git a/pyomo/contrib/mindtpy/tests/eight_process_problem.py b/pyomo/contrib/mindtpy/tests/eight_process_problem.py index d3876a9dc44..ed9059ae4ae 100644 --- a/pyomo/contrib/mindtpy/tests/eight_process_problem.py +++ b/pyomo/contrib/mindtpy/tests/eight_process_problem.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """Re-implementation of eight-process problem. diff --git a/pyomo/contrib/mindtpy/tests/feasibility_pump1.py b/pyomo/contrib/mindtpy/tests/feasibility_pump1.py index e0a611c1ed2..fec750f9f12 100644 --- a/pyomo/contrib/mindtpy/tests/feasibility_pump1.py +++ b/pyomo/contrib/mindtpy/tests/feasibility_pump1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """Example 1 in paper 'A Feasibility Pump for mixed integer nonlinear programs' diff --git a/pyomo/contrib/mindtpy/tests/feasibility_pump2.py b/pyomo/contrib/mindtpy/tests/feasibility_pump2.py index 48b98dc5800..d739e4efbbe 100644 --- a/pyomo/contrib/mindtpy/tests/feasibility_pump2.py +++ b/pyomo/contrib/mindtpy/tests/feasibility_pump2.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """Example 2 in paper 'A Feasibility Pump for mixed integer nonlinear programs' diff --git a/pyomo/contrib/mindtpy/tests/from_proposal.py b/pyomo/contrib/mindtpy/tests/from_proposal.py index 6ddab15ee53..f29fbcd2cf7 100644 --- a/pyomo/contrib/mindtpy/tests/from_proposal.py +++ b/pyomo/contrib/mindtpy/tests/from_proposal.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """ See David Bernal PhD proposal example. diff --git a/pyomo/contrib/mindtpy/tests/nonconvex1.py b/pyomo/contrib/mindtpy/tests/nonconvex1.py index 94a4de29405..71b7e22af96 100644 --- a/pyomo/contrib/mindtpy/tests/nonconvex1.py +++ b/pyomo/contrib/mindtpy/tests/nonconvex1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """Problem A in paper 'Outer approximation algorithms for separable nonconvex mixed-integer nonlinear programs' diff --git a/pyomo/contrib/mindtpy/tests/nonconvex2.py b/pyomo/contrib/mindtpy/tests/nonconvex2.py index 525db1292c1..94c519ab0e1 100644 --- a/pyomo/contrib/mindtpy/tests/nonconvex2.py +++ b/pyomo/contrib/mindtpy/tests/nonconvex2.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """Problem B in paper 'Outer approximation algorithms for separable nonconvex mixed-integer nonlinear programs' diff --git a/pyomo/contrib/mindtpy/tests/nonconvex3.py b/pyomo/contrib/mindtpy/tests/nonconvex3.py index dbb88bb1fad..5b6a1de8d7d 100644 --- a/pyomo/contrib/mindtpy/tests/nonconvex3.py +++ b/pyomo/contrib/mindtpy/tests/nonconvex3.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """Problem C in paper 'Outer approximation algorithms for separable nonconvex mixed-integer nonlinear programs'. The problem in the paper has two optimal solution. Variable y4 and y6 are symmetric. Therefore, we remove variable y6 for simplification. @@ -40,9 +51,7 @@ def __init__(self, *args, **kwargs): m.objective = Objective(expr=7 * m.x1 + 10 * m.x2, sense=minimize) - m.c1 = Constraint( - expr=(m.x1**1.2) * (m.x2**1.7) - 7 * m.x1 - 9 * m.x2 <= -24 - ) + m.c1 = Constraint(expr=(m.x1**1.2) * (m.x2**1.7) - 7 * m.x1 - 9 * m.x2 <= -24) m.c2 = Constraint(expr=-m.x1 - 2 * m.x2 <= 5) m.c3 = Constraint(expr=-3 * m.x1 + m.x2 <= 1) m.c4 = Constraint(expr=4 * m.x1 - 3 * m.x2 <= 11) diff --git a/pyomo/contrib/mindtpy/tests/nonconvex4.py b/pyomo/contrib/mindtpy/tests/nonconvex4.py index c30fb9922a0..3b7f6660ddf 100644 --- a/pyomo/contrib/mindtpy/tests/nonconvex4.py +++ b/pyomo/contrib/mindtpy/tests/nonconvex4.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """Problem D in paper 'Outer approximation algorithms for separable nonconvex mixed-integer nonlinear programs' diff --git a/pyomo/contrib/mindtpy/tests/online_doc_example.py b/pyomo/contrib/mindtpy/tests/online_doc_example.py index d741455e7f7..17a758552c0 100644 --- a/pyomo/contrib/mindtpy/tests/online_doc_example.py +++ b/pyomo/contrib/mindtpy/tests/online_doc_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy.py b/pyomo/contrib/mindtpy/tests/test_mindtpy.py index e872eccc670..618967be00f 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -56,7 +56,12 @@ QCP_model._generate_model() extreme_model_list = [LP_model.model, QCP_model.model] -required_solvers = ('ipopt', 'glpk') +if SolverFactory('appsi_highs').available(exception_flag=False) and SolverFactory( + 'appsi_highs' +).version() >= (1, 7, 0): + required_solvers = ('ipopt', 'appsi_highs') +else: + required_solvers = ('ipopt', 'glpk') if all(SolverFactory(s).available(exception_flag=False) for s in required_solvers): subsolvers_available = True else: @@ -101,6 +106,30 @@ def test_OA_rNLP(self): ) self.check_optimal_solution(model) + def test_OA_callback(self): + """Test the outer approximation decomposition algorithm.""" + with SolverFactory('mindtpy') as opt: + + def callback(model): + model.Y[1].value = 0 + model.Y[2].value = 0 + model.Y[3].value = 0 + + model = SimpleMINLP2() + # The callback function will make the OA method cycling. + results = opt.solve( + model, + strategy='OA', + init_strategy='rNLP', + mip_solver=required_solvers[1], + nlp_solver=required_solvers[0], + call_before_subproblem_solve=callback, + ) + self.assertIs( + results.solver.termination_condition, TerminationCondition.feasible + ) + self.assertAlmostEqual(value(results.problem.lower_bound), 5, places=1) + def test_OA_extreme_model(self): """Test the outer approximation decomposition algorithm.""" with SolverFactory('mindtpy') as opt: @@ -327,6 +356,7 @@ def test_OA_APPSI_ipopt(self): value(model.objective.expr), model.optimal_value, places=1 ) + # CYIPOPT will raise WARNING (W1002) during loading solution. @unittest.skipUnless( SolverFactory('cyipopt').available(exception_flag=False), "APPSI_IPOPT not available.", diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py index b5bfbe62553..dda0f74147e 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """Tests for the MindtPy solver.""" import pyomo.common.unittest as unittest @@ -12,7 +23,13 @@ from pyomo.environ import SolverFactory, value from pyomo.opt import TerminationCondition -required_solvers = ('ipopt', 'glpk') +if SolverFactory('appsi_highs').available(exception_flag=False) and SolverFactory( + 'appsi_highs' +).version() >= (1, 7, 0): + required_solvers = ('ipopt', 'appsi_highs') +else: + required_solvers = ('ipopt', 'glpk') + if all(SolverFactory(s).available(exception_flag=False) for s in required_solvers): subsolvers_available = True else: diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py index 697a63d17c8..0baa361910e 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """Tests for the MindtPy solver.""" import pyomo.common.unittest as unittest @@ -17,8 +28,13 @@ from pyomo.contrib.mindtpy.tests.feasibility_pump1 import FeasPump1 from pyomo.contrib.mindtpy.tests.feasibility_pump2 import FeasPump2 -required_solvers = ('ipopt', 'cplex') -# TODO: 'appsi_highs' will fail here. +if SolverFactory('appsi_highs').available(exception_flag=False) and SolverFactory( + 'appsi_highs' +).version() >= (1, 7, 0): + required_solvers = ('ipopt', 'appsi_highs') +else: + required_solvers = ('ipopt', 'glpk') + if all(SolverFactory(s).available(exception_flag=False) for s in required_solvers): subsolvers_available = True else: @@ -69,6 +85,22 @@ def test_FP(self): log_infeasible_constraints(model) self.assertTrue(is_feasible(model, self.get_config(opt))) + def test_FP_L1_norm(self): + """Test the feasibility pump algorithm.""" + with SolverFactory('mindtpy') as opt: + for model in model_list: + model = model.clone() + results = opt.solve( + model, + strategy='FP', + mip_solver=required_solvers[1], + nlp_solver=required_solvers[0], + absolute_bound_tolerance=1e-5, + fp_main_norm='L1', + ) + log_infeasible_constraints(model) + self.assertTrue(is_feasible(model, self.get_config(opt))) + def test_FP_OA_8PP(self): """Test the FP-OA algorithm.""" with SolverFactory('mindtpy') as opt: diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_global.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_global.py index 0fa19b30d9c..07774805364 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_global.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_global.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """Tests for the MindtPy solver.""" import pyomo.common.unittest as unittest diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_global_lp_nlp.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_global_lp_nlp.py index 259cfe9dd7c..792bdb8d993 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_global_lp_nlp.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_global_lp_nlp.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """Tests for global LP/NLP in the MindtPy solver.""" import pyomo.common.unittest as unittest diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_grey_box.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_grey_box.py index d9ba683d198..e01558d48ef 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_grey_box.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_grey_box.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -12,19 +12,27 @@ """Tests for the MindtPy solver.""" from pyomo.core.expr.calculus.diff_with_sympy import differentiate_available import pyomo.common.unittest as unittest -from pyomo.contrib.mindtpy.tests.MINLP_simple import SimpleMINLP as SimpleMINLP from pyomo.environ import SolverFactory, value, maximize from pyomo.opt import TerminationCondition - +from pyomo.common.dependencies import numpy_available, scipy_available +from pyomo.contrib.mindtpy.tests.MINLP_simple import SimpleMINLP as SimpleMINLP model_list = [SimpleMINLP(grey_box=True)] -required_solvers = ('cyipopt', 'glpk') + +if SolverFactory('appsi_highs').available(exception_flag=False) and SolverFactory( + 'appsi_highs' +).version() >= (1, 7, 0): + required_solvers = ('cyipopt', 'appsi_highs') +else: + required_solvers = ('cyipopt', 'glpk') + if all(SolverFactory(s).available(exception_flag=False) for s in required_solvers): subsolvers_available = True else: subsolvers_available = False +@unittest.skipIf(model_list[0] is None, 'Unable to generate the Grey Box model.') @unittest.skipIf( not subsolvers_available, 'Required subsolvers %s are not available' % (required_solvers,), diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_lp_nlp.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_lp_nlp.py index 2662a0e6f56..97f73ece525 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_lp_nlp.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_lp_nlp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_regularization.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_regularization.py index 4c2ae4d1220..2e864a49578 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_regularization.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_regularization.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """Tests for the MindtPy solver.""" import pyomo.common.unittest as unittest diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_solution_pool.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_solution_pool.py index e8ad85ad9bc..a41f41d4d65 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_solution_pool.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_solution_pool.py @@ -1,4 +1,16 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Tests for solution pool in the MindtPy solver.""" + from pyomo.core.expr.calculus.diff_with_sympy import differentiate_available import pyomo.common.unittest as unittest from pyomo.contrib.mindtpy.tests.eight_process_problem import EightProcessFlowsheet diff --git a/pyomo/contrib/mindtpy/tests/unit_test.py b/pyomo/contrib/mindtpy/tests/unit_test.py new file mode 100644 index 00000000000..af6ffad282d --- /dev/null +++ b/pyomo/contrib/mindtpy/tests/unit_test.py @@ -0,0 +1,101 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest +from pyomo.contrib.mindtpy.util import set_var_valid_value + +from pyomo.environ import Var, Integers, ConcreteModel, Integers +from pyomo.contrib.mindtpy.algorithm_base_class import _MindtPyAlgorithm +from pyomo.contrib.mindtpy.config_options import _get_MindtPy_OA_config +from pyomo.contrib.mindtpy.tests.MINLP5_simple import SimpleMINLP5 +from pyomo.contrib.mindtpy.util import add_var_bound + + +class UnitTestMindtPy(unittest.TestCase): + def test_set_var_valid_value(self): + m = ConcreteModel() + m.x1 = Var(within=Integers, bounds=(-1, 4), initialize=0) + + set_var_valid_value( + m.x1, + var_val=5, + integer_tolerance=1e-6, + zero_tolerance=1e-6, + ignore_integrality=False, + ) + self.assertEqual(m.x1.value, 4) + + set_var_valid_value( + m.x1, + var_val=-2, + integer_tolerance=1e-6, + zero_tolerance=1e-6, + ignore_integrality=False, + ) + self.assertEqual(m.x1.value, -1) + + set_var_valid_value( + m.x1, + var_val=1.1, + integer_tolerance=1e-6, + zero_tolerance=1e-6, + ignore_integrality=True, + ) + self.assertEqual(m.x1.value, 1.1) + + set_var_valid_value( + m.x1, + var_val=2.00000001, + integer_tolerance=1e-6, + zero_tolerance=1e-6, + ignore_integrality=False, + ) + self.assertEqual(m.x1.value, 2) + + set_var_valid_value( + m.x1, + var_val=0.0000001, + integer_tolerance=1e-9, + zero_tolerance=1e-6, + ignore_integrality=False, + ) + self.assertEqual(m.x1.value, 0) + + def test_add_var_bound(self): + m = SimpleMINLP5().clone() + m.x.lb = None + m.x.ub = None + m.y.lb = None + m.y.ub = None + solver_object = _MindtPyAlgorithm() + solver_object.config = _get_MindtPy_OA_config() + solver_object.set_up_solve_data(m) + solver_object.create_utility_block(solver_object.working_model, 'MindtPy_utils') + add_var_bound(solver_object.working_model, solver_object.config) + self.assertEqual( + solver_object.working_model.x.lower, + -solver_object.config.continuous_var_bound - 1, + ) + self.assertEqual( + solver_object.working_model.x.upper, + solver_object.config.continuous_var_bound, + ) + self.assertEqual( + solver_object.working_model.y.lower, + -solver_object.config.integer_var_bound - 1, + ) + self.assertEqual( + solver_object.working_model.y.upper, solver_object.config.integer_var_bound + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index 7be524e79d4..4ff553e9907 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -23,13 +23,12 @@ RangeSet, ConstraintList, TransformationFactory, - value + value, ) from pyomo.repn import generate_standard_repn from pyomo.contrib.mcpp.pyomo_mcpp import mcpp_available, McCormick from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr import pyomo.core.expr as EXPR -from pyomo.opt import ProblemSense from pyomo.contrib.gdpopt.util import get_main_elapsed_time, time_code from pyomo.util.model_size import build_model_size_report from pyomo.common.dependencies import attempt_import @@ -41,27 +40,24 @@ numpy = attempt_import('numpy')[0] -def calc_jacobians(model, config): +def calc_jacobians(constraint_list, differentiate_mode): """Generates a map of jacobians for the variables in the model. This function generates a map of jacobians corresponding to the variables in the - model. + constraint list. Parameters ---------- - model : Pyomo model - Target model to calculate jacobian. - config : ConfigBlock - The specific configurations for MindtPy. + constraint_list : List + The list of constraints to calculate Jacobians. + differentiate_mode : String + The differentiate mode to calculate Jacobians. """ # Map nonlinear_constraint --> Map( # variable --> jacobian of constraint w.r.t. variable) jacobians = ComponentMap() - if config.differentiate_mode == 'reverse_symbolic': - mode = EXPR.differentiate.Modes.reverse_symbolic - elif config.differentiate_mode == 'sympy': - mode = EXPR.differentiate.Modes.sympy - for c in model.MindtPy_utils.nonlinear_constraint_list: + mode = EXPR.differentiate.Modes(differentiate_mode) + for c in constraint_list: vars_in_constr = list(EXPR.identify_variables(c.body)) jac_list = EXPR.differentiate(c.body, wrt_list=vars_in_constr, mode=mode) jacobians[c] = ComponentMap( @@ -70,7 +66,7 @@ def calc_jacobians(model, config): return jacobians -def initialize_feas_subproblem(m, config): +def initialize_feas_subproblem(m, feasibility_norm): """Adds feasibility slack variables according to config.feasibility_norm (given an infeasible problem). Defines the objective function of the feasibility subproblem. @@ -78,14 +74,14 @@ def initialize_feas_subproblem(m, config): ---------- m : Pyomo model The feasbility NLP subproblem. - config : ConfigBlock - The specific configurations for MindtPy. + feasibility_norm : String + The norm used to generate the objective function. """ MindtPy = m.MindtPy_utils # generate new constraints for i, constr in enumerate(MindtPy.nonlinear_constraint_list, 1): if constr.has_ub(): - if config.feasibility_norm in {'L1', 'L2'}: + if feasibility_norm in {'L1', 'L2'}: MindtPy.feas_opt.feas_constraints.add( constr.body - constr.upper <= MindtPy.feas_opt.slack_var[i] ) @@ -94,7 +90,7 @@ def initialize_feas_subproblem(m, config): constr.body - constr.upper <= MindtPy.feas_opt.slack_var ) if constr.has_lb(): - if config.feasibility_norm in {'L1', 'L2'}: + if feasibility_norm in {'L1', 'L2'}: MindtPy.feas_opt.feas_constraints.add( constr.body - constr.lower >= -MindtPy.feas_opt.slack_var[i] ) @@ -103,11 +99,11 @@ def initialize_feas_subproblem(m, config): constr.body - constr.lower >= -MindtPy.feas_opt.slack_var ) # Setup objective function for the feasibility subproblem. - if config.feasibility_norm == 'L1': + if feasibility_norm == 'L1': MindtPy.feas_obj = Objective( expr=sum(s for s in MindtPy.feas_opt.slack_var.values()), sense=minimize ) - elif config.feasibility_norm == 'L2': + elif feasibility_norm == 'L2': MindtPy.feas_obj = Objective( expr=sum(s * s for s in MindtPy.feas_opt.slack_var.values()), sense=minimize ) @@ -134,12 +130,12 @@ def add_var_bound(model, config): for var in EXPR.identify_variables(c.body): if var.has_lb() and var.has_ub(): continue - elif not var.has_lb(): + if not var.has_lb(): if var.is_integer(): var.setlb(-config.integer_var_bound - 1) else: var.setlb(-config.continuous_var_bound - 1) - elif not var.has_ub(): + if not var.has_ub(): if var.is_integer(): var.setub(config.integer_var_bound) else: @@ -345,9 +341,11 @@ def generate_lag_objective_function( with time_code(timing, 'PyomoNLP'): nlp = pyomo_nlp.PyomoNLP(temp_model) lam = [ - -temp_model.dual[constr] - if abs(temp_model.dual[constr]) > config.zero_tolerance - else 0 + ( + -temp_model.dual[constr] + if abs(temp_model.dual[constr]) > config.zero_tolerance + else 0 + ) for constr in nlp.get_pyomo_constraints() ] nlp.set_duals(lam) @@ -567,7 +565,9 @@ def set_solver_mipgap(opt, solver_name, config): opt.options['add_options'].append('option optcr=%s;' % config.mip_solver_mipgap) -def set_solver_constraint_violation_tolerance(opt, solver_name, config, warm_start=True): +def set_solver_constraint_violation_tolerance( + opt, solver_name, config, warm_start=True +): """Set constraint violation tolerance for solvers. Parameters @@ -580,11 +580,11 @@ def set_solver_constraint_violation_tolerance(opt, solver_name, config, warm_sta The specific configurations for MindtPy. """ if solver_name == 'baron': - opt.options['AbsConFeasTol'] = config.zero_tolerance + opt.options['AbsConFeasTol'] = config.constraint_tolerance elif solver_name in {'ipopt', 'appsi_ipopt'}: - opt.options['constr_viol_tol'] = config.zero_tolerance + opt.options['constr_viol_tol'] = config.constraint_tolerance elif solver_name == 'cyipopt': - opt.config.options['constr_viol_tol'] = config.zero_tolerance + opt.config.options['constr_viol_tol'] = config.constraint_tolerance elif solver_name == 'gams': if config.nlp_solver_args['solver'] in { 'ipopt', @@ -599,7 +599,7 @@ def set_solver_constraint_violation_tolerance(opt, solver_name, config, warm_sta ) if config.nlp_solver_args['solver'] in {'ipopt', 'ipopth'}: opt.options['add_options'].append( - 'constr_viol_tol ' + str(config.zero_tolerance) + 'constr_viol_tol ' + str(config.constraint_tolerance) ) if warm_start: pass @@ -614,15 +614,15 @@ def set_solver_constraint_violation_tolerance(opt, solver_name, config, warm_sta # ) elif config.nlp_solver_args['solver'] == 'conopt': opt.options['add_options'].append( - 'RTNWMA ' + str(config.zero_tolerance) + 'RTNWMA ' + str(config.constraint_tolerance) ) elif config.nlp_solver_args['solver'] == 'msnlp': opt.options['add_options'].append( - 'feasibility_tolerance ' + str(config.zero_tolerance) + 'feasibility_tolerance ' + str(config.constraint_tolerance) ) elif config.nlp_solver_args['solver'] == 'baron': opt.options['add_options'].append( - 'AbsConFeasTol ' + str(config.zero_tolerance) + 'AbsConFeasTol ' + str(config.constraint_tolerance) ) opt.options['add_options'].append('$offecho') @@ -692,35 +692,13 @@ def copy_var_list_values_from_solution_pool( elif config.mip_solver == 'gurobi_persistent': solver_model.setParam(gurobipy.GRB.Param.SolutionNumber, solution_name) var_val = var_map[v_from].Xn - # We don't want to trigger the reset of the global stale - # indicator, so we will set this variable to be "stale", - # knowing that set_value will switch it back to "not - # stale" - v_to.stale = True - rounded_val = int(round(var_val)) - # NOTE: PEP 2180 changes the var behavior so that domain / - # bounds violations no longer generate exceptions (and - # instead log warnings). This means that the following will - # always succeed and the ValueError should never be raised. - if var_val in v_to.domain \ - and not ((v_to.has_lb() and var_val < v_to.lb)) \ - and not ((v_to.has_ub() and var_val > v_to.ub)): - v_to.set_value(var_val, skip_validation=True) - elif v_to.has_lb() and var_val < v_to.lb: - v_to.set_value(v_to.lb) - elif v_to.has_ub() and var_val > v_to.ub: - v_to.set_value(v_to.ub) - # Check to see if this is just a tolerance issue - elif ignore_integrality and v_to.is_integer(): - v_to.set_value(var_val, skip_validation=True) - elif v_to.is_integer() and ( - abs(var_val - rounded_val) <= config.integer_tolerance - ): - v_to.set_value(rounded_val, skip_validation=True) - elif abs(var_val) <= config.zero_tolerance and 0 in v_to.domain: - v_to.set_value(0, skip_validation=True) - else: - raise ValueError("copy_var_list_values_from_solution_pool failed.") + set_var_valid_value( + v_to, + var_val, + config.integer_tolerance, + config.zero_tolerance, + ignore_integrality, + ) class GurobiPersistent4MindtPy(GurobiPersistent): @@ -745,25 +723,6 @@ def f(gurobi_model, where): return f -def set_up_logger(config): - """Set up the formatter and handler for logger. - - Parameters - ---------- - config : ConfigBlock - The specific configurations for MindtPy. - """ - config.logger.handlers.clear() - config.logger.propagate = False - ch = logging.StreamHandler() - ch.setLevel(config.logging_level) - # create formatter and add it to the handlers - formatter = logging.Formatter('%(message)s') - ch.setFormatter(formatter) - # add the handlers to logger - config.logger.addHandler(ch) - - def epigraph_reformulation(exp, slack_var_list, constraint_list, use_mcpp, sense): """Epigraph reformulation. @@ -968,12 +927,31 @@ def generate_norm_constraint(fp_nlp_model, mip_model, config): ): fp_nlp_model.norm_constraint.add(nlp_var - mip_var.value <= rhs) -def copy_var_list_values(from_list, to_list, config, - skip_stale=False, skip_fixed=True, - ignore_integrality=False): + +def copy_var_list_values( + from_list, + to_list, + config, + skip_stale=False, + skip_fixed=True, + ignore_integrality=False, +): """Copy variable values from one list to another. Rounds to Binary/Integer if necessary Sets to zero for NonNegativeReals if necessary + + from_list : list + The variables that provide the values to copy from. + to_list : list + The variables that need to set value. + config : ConfigBlock + The specific configurations for MindtPy. + skip_stale : bool, optional + Whether to skip the stale variables, by default False. + skip_fixed : bool, optional + Whether to skip the fixed variables, by default True. + ignore_integrality : bool, optional + Whether to ignore the integrality of integer variables, by default False. """ for v_from, v_to in zip(from_list, to_list): if skip_stale and v_from.stale: @@ -981,21 +959,68 @@ def copy_var_list_values(from_list, to_list, config, if skip_fixed and v_to.is_fixed(): continue # Skip fixed variables. var_val = value(v_from, exception=False) - rounded_val = int(round(var_val)) - if var_val in v_to.domain \ - and not ((v_to.has_lb() and var_val < v_to.lb)) \ - and not ((v_to.has_ub() and var_val > v_to.ub)): - v_to.set_value(value(v_from, exception=False)) - elif v_to.has_lb() and var_val < v_to.lb: - v_to.set_value(v_to.lb) - elif v_to.has_ub() and var_val > v_to.ub: - v_to.set_value(v_to.ub) - elif ignore_integrality and v_to.is_integer(): - v_to.set_value(value(v_from, exception=False), skip_validation=True) - elif v_to.is_integer() and (math.fabs(var_val - rounded_val) <= - config.integer_tolerance): - v_to.set_value(rounded_val) - elif abs(var_val) <= config.zero_tolerance and 0 in v_to.domain: - v_to.set_value(0) - else: - raise ValueError("copy_var_list_values failed.") + set_var_valid_value( + v_to, + var_val, + config.integer_tolerance, + config.zero_tolerance, + ignore_integrality, + ) + + +def set_var_valid_value( + var, var_val, integer_tolerance, zero_tolerance, ignore_integrality +): + """This function tries to set a valid value for variable with the given input. + Rounds to Binary/Integer if necessary. + Sets to zero for NonNegativeReals if necessary. + + Parameters + ---------- + var : Var + The variable that needs to set value. + var_val : float + The desired value to set for var. + integer_tolerance: float + Tolerance on integral values. + zero_tolerance: float + Tolerance on variable equal to zero. + ignore_integrality : bool, optional + Whether to ignore the integrality of integer variables, by default False. + + Raises + ------ + ValueError + Cannot successfully set the value to the variable. + """ + # NOTE: PEP 2180 changes the var behavior so that domain + # bounds violations no longer generate exceptions (and + # instead log warnings). This means that the set_value method + # will always succeed and the ValueError should never be raised. + + # We don't want to trigger the reset of the global stale + # indicator, so we will set this variable to be "stale", + # knowing that set_value will switch it back to "not stale". + var.stale = True + rounded_val = int(round(var_val)) + if ( + var_val in var.domain + and not ((var.has_lb() and var_val < var.lb)) + and not ((var.has_ub() and var_val > var.ub)) + ): + var.set_value(var_val) + elif var.has_lb() and var_val < var.lb: + var.set_value(var.lb) + elif var.has_ub() and var_val > var.ub: + var.set_value(var.ub) + elif ignore_integrality and var.is_integer(): + var.set_value(var_val, skip_validation=True) + elif var.is_integer() and (math.fabs(var_val - rounded_val) <= integer_tolerance): + var.set_value(rounded_val) + elif abs(var_val) <= zero_tolerance and 0 in var.domain: + var.set_value(0) + else: + raise ValueError( + "set_var_valid_value failed with variable {}, value = {} and rounded value = {}" + "".format(var.name, var_val, rounded_val) + ) diff --git a/pyomo/contrib/mpc/README.md b/pyomo/contrib/mpc/README.md new file mode 100644 index 00000000000..7e03163f703 --- /dev/null +++ b/pyomo/contrib/mpc/README.md @@ -0,0 +1,34 @@ +# Pyomo MPC + +Pyomo MPC is an extension for developing model predictive control simulations +using Pyomo models. Please see the +[documentation](https://pyomo.readthedocs.io/en/stable/contributed_packages/mpc/index.html) +for more detailed information. + +Pyomo MPC helps with, among other things, the following use cases: +- Transferring values between different points in time in a dynamic model +(e.g. to initialize a dynamic model to its initial conditions) +- Extracting or loading disturbances and inputs from or to models, and storing +these in model-agnostic, easily JSON-serializable data structures +- Constructing common modeling components, such as weighted-least-squares +tracking objective functions, piecewise-constant input constraints, or +terminal region constraints. + +## Citation + +If you use Pyomo MPC in your research, please cite the following paper, which +discusses the motivation for the Pyomo MPC data structures and the underlying +Pyomo features that make them possible. +```bibtex +@article{parker2023mpc, +title = {Model predictive control simulations with block-hierarchical differential-algebraic process models}, +journal = {Journal of Process Control}, +volume = {132}, +pages = {103113}, +year = {2023}, +issn = {0959-1524}, +doi = {https://doi.org/10.1016/j.jprocont.2023.103113}, +url = {https://www.sciencedirect.com/science/article/pii/S0959152423002007}, +author = {Robert B. Parker and Bethany L. Nicholson and John D. Siirola and Lorenz T. Biegler}, +} +``` diff --git a/pyomo/contrib/mpc/__init__.py b/pyomo/contrib/mpc/__init__.py index da977f365d2..2e1c51e154f 100644 --- a/pyomo/contrib/mpc/__init__.py +++ b/pyomo/contrib/mpc/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/data/__init__.py b/pyomo/contrib/mpc/data/__init__.py index 9061fda4bfd..6051f4ba3a2 100644 --- a/pyomo/contrib/mpc/data/__init__.py +++ b/pyomo/contrib/mpc/data/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/data/convert.py b/pyomo/contrib/mpc/data/convert.py index f1d35592a9f..10885370032 100644 --- a/pyomo/contrib/mpc/data/convert.py +++ b/pyomo/contrib/mpc/data/convert.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/data/dynamic_data_base.py b/pyomo/contrib/mpc/data/dynamic_data_base.py index c0223d2dcbe..5e567f060cf 100644 --- a/pyomo/contrib/mpc/data/dynamic_data_base.py +++ b/pyomo/contrib/mpc/data/dynamic_data_base.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/data/find_nearest_index.py b/pyomo/contrib/mpc/data/find_nearest_index.py index 0875bde63e9..c53a7a79841 100644 --- a/pyomo/contrib/mpc/data/find_nearest_index.py +++ b/pyomo/contrib/mpc/data/find_nearest_index.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/data/get_cuid.py b/pyomo/contrib/mpc/data/get_cuid.py index 1f229b35645..ef0df7ea679 100644 --- a/pyomo/contrib/mpc/data/get_cuid.py +++ b/pyomo/contrib/mpc/data/get_cuid.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -16,14 +16,13 @@ def get_indexed_cuid(var, sets=None, dereference=None, context=None): - """ - Attempts to convert the provided "var" object into a CUID with - with wildcards. + """Attempt to convert the provided "var" object into a CUID with wildcards Arguments --------- var: - Object to process + Object to process. May be a VarData, IndexedVar (reference or otherwise), + ComponentUID, slice, or string. sets: Tuple of sets Sets to use if slicing a vardata object dereference: None or int @@ -32,6 +31,11 @@ def get_indexed_cuid(var, sets=None, dereference=None, context=None): context: Block Block with respect to which slices and CUIDs will be generated + Returns + ------- + ``ComponentUID`` + ComponentUID corresponding to the provided ``var`` and sets + """ # TODO: Does this function have a good name? # Should this function be generalized beyond a single indexing set? diff --git a/pyomo/contrib/mpc/data/interval_data.py b/pyomo/contrib/mpc/data/interval_data.py index cdd3b0e37dc..54b7ca7e906 100644 --- a/pyomo/contrib/mpc/data/interval_data.py +++ b/pyomo/contrib/mpc/data/interval_data.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/data/scalar_data.py b/pyomo/contrib/mpc/data/scalar_data.py index 5426921ef06..b67384c8159 100644 --- a/pyomo/contrib/mpc/data/scalar_data.py +++ b/pyomo/contrib/mpc/data/scalar_data.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/data/series_data.py b/pyomo/contrib/mpc/data/series_data.py index d09ab8cae24..c812e76c9fc 100644 --- a/pyomo/contrib/mpc/data/series_data.py +++ b/pyomo/contrib/mpc/data/series_data.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/data/tests/__init__.py b/pyomo/contrib/mpc/data/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/mpc/data/tests/__init__.py +++ b/pyomo/contrib/mpc/data/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/mpc/data/tests/test_convert.py b/pyomo/contrib/mpc/data/tests/test_convert.py index 0f8a4623e20..dda3583cb00 100644 --- a/pyomo/contrib/mpc/data/tests/test_convert.py +++ b/pyomo/contrib/mpc/data/tests/test_convert.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/data/tests/test_find_nearest_index.py b/pyomo/contrib/mpc/data/tests/test_find_nearest_index.py index e90024ef108..8fb92e17534 100644 --- a/pyomo/contrib/mpc/data/tests/test_find_nearest_index.py +++ b/pyomo/contrib/mpc/data/tests/test_find_nearest_index.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/data/tests/test_get_cuid.py b/pyomo/contrib/mpc/data/tests/test_get_cuid.py index 30ba2b58b1b..66bfb613bcb 100644 --- a/pyomo/contrib/mpc/data/tests/test_get_cuid.py +++ b/pyomo/contrib/mpc/data/tests/test_get_cuid.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/data/tests/test_interval_data.py b/pyomo/contrib/mpc/data/tests/test_interval_data.py index 8afe3eb3021..b208c9066f9 100644 --- a/pyomo/contrib/mpc/data/tests/test_interval_data.py +++ b/pyomo/contrib/mpc/data/tests/test_interval_data.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/data/tests/test_scalar_data.py b/pyomo/contrib/mpc/data/tests/test_scalar_data.py index 110ed749bda..6522242e267 100644 --- a/pyomo/contrib/mpc/data/tests/test_scalar_data.py +++ b/pyomo/contrib/mpc/data/tests/test_scalar_data.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/data/tests/test_series_data.py b/pyomo/contrib/mpc/data/tests/test_series_data.py index e32559ac074..88b672279f2 100644 --- a/pyomo/contrib/mpc/data/tests/test_series_data.py +++ b/pyomo/contrib/mpc/data/tests/test_series_data.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/examples/__init__.py b/pyomo/contrib/mpc/examples/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/mpc/examples/__init__.py +++ b/pyomo/contrib/mpc/examples/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/mpc/examples/cstr/__init__.py b/pyomo/contrib/mpc/examples/cstr/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/mpc/examples/cstr/__init__.py +++ b/pyomo/contrib/mpc/examples/cstr/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/mpc/examples/cstr/model.py b/pyomo/contrib/mpc/examples/cstr/model.py index d794084f122..376e77186dd 100644 --- a/pyomo/contrib/mpc/examples/cstr/model.py +++ b/pyomo/contrib/mpc/examples/cstr/model.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/examples/cstr/run_mpc.py b/pyomo/contrib/mpc/examples/cstr/run_mpc.py index 86ae7e4e47b..588ed7d49fe 100644 --- a/pyomo/contrib/mpc/examples/cstr/run_mpc.py +++ b/pyomo/contrib/mpc/examples/cstr/run_mpc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/examples/cstr/run_openloop.py b/pyomo/contrib/mpc/examples/cstr/run_openloop.py index 36ddb990545..66fd0680a01 100644 --- a/pyomo/contrib/mpc/examples/cstr/run_openloop.py +++ b/pyomo/contrib/mpc/examples/cstr/run_openloop.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/examples/cstr/tests/__init__.py b/pyomo/contrib/mpc/examples/cstr/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/mpc/examples/cstr/tests/__init__.py +++ b/pyomo/contrib/mpc/examples/cstr/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/mpc/examples/cstr/tests/test_mpc.py b/pyomo/contrib/mpc/examples/cstr/tests/test_mpc.py index 741a1533da3..e808b8fc414 100644 --- a/pyomo/contrib/mpc/examples/cstr/tests/test_mpc.py +++ b/pyomo/contrib/mpc/examples/cstr/tests/test_mpc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/examples/cstr/tests/test_openloop.py b/pyomo/contrib/mpc/examples/cstr/tests/test_openloop.py index 218865ceabb..c21cb55233e 100644 --- a/pyomo/contrib/mpc/examples/cstr/tests/test_openloop.py +++ b/pyomo/contrib/mpc/examples/cstr/tests/test_openloop.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/interfaces/__init__.py b/pyomo/contrib/mpc/interfaces/__init__.py index 8e02003f99e..9b70a983e24 100644 --- a/pyomo/contrib/mpc/interfaces/__init__.py +++ b/pyomo/contrib/mpc/interfaces/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/interfaces/copy_values.py b/pyomo/contrib/mpc/interfaces/copy_values.py index 896656b230d..faf1594f114 100644 --- a/pyomo/contrib/mpc/interfaces/copy_values.py +++ b/pyomo/contrib/mpc/interfaces/copy_values.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/interfaces/load_data.py b/pyomo/contrib/mpc/interfaces/load_data.py index efa9515901e..b1851c3aa51 100644 --- a/pyomo/contrib/mpc/interfaces/load_data.py +++ b/pyomo/contrib/mpc/interfaces/load_data.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/interfaces/model_interface.py b/pyomo/contrib/mpc/interfaces/model_interface.py index 35f81af4a7a..9a30878c921 100644 --- a/pyomo/contrib/mpc/interfaces/model_interface.py +++ b/pyomo/contrib/mpc/interfaces/model_interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/interfaces/tests/__init__.py b/pyomo/contrib/mpc/interfaces/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/mpc/interfaces/tests/__init__.py +++ b/pyomo/contrib/mpc/interfaces/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/mpc/interfaces/tests/test_interface.py b/pyomo/contrib/mpc/interfaces/tests/test_interface.py index 65ffc7bb40a..e67e58bf900 100644 --- a/pyomo/contrib/mpc/interfaces/tests/test_interface.py +++ b/pyomo/contrib/mpc/interfaces/tests/test_interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/interfaces/tests/test_var_linker.py b/pyomo/contrib/mpc/interfaces/tests/test_var_linker.py index ceec9fada36..e169af686f3 100644 --- a/pyomo/contrib/mpc/interfaces/tests/test_var_linker.py +++ b/pyomo/contrib/mpc/interfaces/tests/test_var_linker.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/interfaces/var_linker.py b/pyomo/contrib/mpc/interfaces/var_linker.py index fd831c9a2c1..87831379204 100644 --- a/pyomo/contrib/mpc/interfaces/var_linker.py +++ b/pyomo/contrib/mpc/interfaces/var_linker.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/modeling/__init__.py b/pyomo/contrib/mpc/modeling/__init__.py index 0eb255a9f56..a174bafc944 100644 --- a/pyomo/contrib/mpc/modeling/__init__.py +++ b/pyomo/contrib/mpc/modeling/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/modeling/constraints.py b/pyomo/contrib/mpc/modeling/constraints.py index 6fb6a311afb..e6a1edf648b 100644 --- a/pyomo/contrib/mpc/modeling/constraints.py +++ b/pyomo/contrib/mpc/modeling/constraints.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/modeling/cost_expressions.py b/pyomo/contrib/mpc/modeling/cost_expressions.py index 65a376e42d2..aeb26705a38 100644 --- a/pyomo/contrib/mpc/modeling/cost_expressions.py +++ b/pyomo/contrib/mpc/modeling/cost_expressions.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/modeling/terminal.py b/pyomo/contrib/mpc/modeling/terminal.py index c25efca280a..d2118c7d92e 100644 --- a/pyomo/contrib/mpc/modeling/terminal.py +++ b/pyomo/contrib/mpc/modeling/terminal.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/modeling/tests/__init__.py b/pyomo/contrib/mpc/modeling/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/mpc/modeling/tests/__init__.py +++ b/pyomo/contrib/mpc/modeling/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/mpc/modeling/tests/test_cost_expressions.py b/pyomo/contrib/mpc/modeling/tests/test_cost_expressions.py index 5db390ffa47..67c474f7722 100644 --- a/pyomo/contrib/mpc/modeling/tests/test_cost_expressions.py +++ b/pyomo/contrib/mpc/modeling/tests/test_cost_expressions.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/modeling/tests/test_input_constraints.py b/pyomo/contrib/mpc/modeling/tests/test_input_constraints.py index e3ba3bf3760..be9edad37b9 100644 --- a/pyomo/contrib/mpc/modeling/tests/test_input_constraints.py +++ b/pyomo/contrib/mpc/modeling/tests/test_input_constraints.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/modeling/tests/test_terminal.py b/pyomo/contrib/mpc/modeling/tests/test_terminal.py index b835f0b1087..ef89fe24b57 100644 --- a/pyomo/contrib/mpc/modeling/tests/test_terminal.py +++ b/pyomo/contrib/mpc/modeling/tests/test_terminal.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/multistart/__init__.py b/pyomo/contrib/multistart/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/multistart/__init__.py +++ b/pyomo/contrib/multistart/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/multistart/high_conf_stop.py b/pyomo/contrib/multistart/high_conf_stop.py index e18467c1741..ce24d2dc1fc 100644 --- a/pyomo/contrib/multistart/high_conf_stop.py +++ b/pyomo/contrib/multistart/high_conf_stop.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Utility functions for the high confidence stopping rule. This stopping criterion operates by estimating the amount of missing optima, @@ -5,7 +16,6 @@ range, given some confidence. """ -from __future__ import division from collections import Counter from math import log, sqrt diff --git a/pyomo/contrib/multistart/multi.py b/pyomo/contrib/multistart/multi.py index a0e424d2c95..377ac8182e2 100644 --- a/pyomo/contrib/multistart/multi.py +++ b/pyomo/contrib/multistart/multi.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -10,8 +10,6 @@ # ___________________________________________________________________________ -from __future__ import division - import logging from pyomo.common.config import ( diff --git a/pyomo/contrib/multistart/plugins.py b/pyomo/contrib/multistart/plugins.py index 297b2f059cc..f094e2f58cc 100644 --- a/pyomo/contrib/multistart/plugins.py +++ b/pyomo/contrib/multistart/plugins.py @@ -1,2 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + def load(): import pyomo.contrib.multistart.multi diff --git a/pyomo/contrib/multistart/reinit.py b/pyomo/contrib/multistart/reinit.py index 214192df648..2b097bbc898 100644 --- a/pyomo/contrib/multistart/reinit.py +++ b/pyomo/contrib/multistart/reinit.py @@ -1,5 +1,15 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Helper functions for variable reinitialization.""" -from __future__ import division import logging import random diff --git a/pyomo/contrib/multistart/test_multi.py b/pyomo/contrib/multistart/test_multi.py index 16c8563ae9e..f8103eed3b8 100644 --- a/pyomo/contrib/multistart/test_multi.py +++ b/pyomo/contrib/multistart/test_multi.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import logging from itertools import product diff --git a/pyomo/contrib/parmest/__init__.py b/pyomo/contrib/parmest/__init__.py index d340885b3fd..e7d513dd95c 100644 --- a/pyomo/contrib/parmest/__init__.py +++ b/pyomo/contrib/parmest/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/examples/__init__.py b/pyomo/contrib/parmest/examples/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/parmest/examples/__init__.py +++ b/pyomo/contrib/parmest/examples/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/examples/reaction_kinetics/__init__.py b/pyomo/contrib/parmest/examples/reaction_kinetics/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/parmest/examples/reaction_kinetics/__init__.py +++ b/pyomo/contrib/parmest/examples/reaction_kinetics/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py b/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py index 719a930251c..5c8a0219946 100644 --- a/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py +++ b/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -18,6 +18,7 @@ Code provided by Paul Akula. ''' +import pyomo.environ as pyo from pyomo.environ import ( ConcreteModel, Param, @@ -32,6 +33,7 @@ value, ) import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.experiment import Experiment def simple_reaction_model(data): @@ -72,7 +74,62 @@ def total_cost_rule(m): return model +# For this experiment class, data is dictionary +class SimpleReactionExperiment(Experiment): + + def __init__(self, data): + self.data = data + self.model = None + + def create_model(self): + self.model = simple_reaction_model(self.data) + + def label_model(self): + + m = self.model + + m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.experiment_outputs.update( + [(m.x1, self.data['x1']), (m.x2, self.data['x2']), (m.y, self.data['y'])] + ) + + return m + + def get_labeled_model(self): + self.create_model() + m = self.label_model() + + return m + + +# k[2] fixed +class SimpleReactionExperimentK2Fixed(SimpleReactionExperiment): + + def label_model(self): + + m = super().label_model() + + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.k[1]]) + + return m + + +# k[2] variable +class SimpleReactionExperimentK2Variable(SimpleReactionExperiment): + + def label_model(self): + + m = super().label_model() + + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.k[1], m.k[2]]) + + return m + + def main(): + # Data from Table 5.2 in Y. Bard, "Nonlinear Parameter Estimation", (pg. 124) data = [ {'experiment': 1, 'x1': 0.1, 'x2': 100, 'y': 0.98}, @@ -92,21 +149,34 @@ def main(): {'experiment': 15, 'x1': 0.1, 'x2': 300, 'y': 0.006}, ] + # Create an experiment list with k[2] fixed + exp_list = [] + for i in range(len(data)): + exp_list.append(SimpleReactionExperimentK2Fixed(data[i])) + + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # exp0_model.pprint() + # ======================================================================= # Parameter estimation without covariance estimate # Only estimate the parameter k[1]. The parameter k[2] will remain fixed # at its initial value - theta_names = ['k[1]'] - pest = parmest.Estimator(simple_reaction_model, data, theta_names) + + pest = parmest.Estimator(exp_list) obj, theta = pest.theta_est() print(obj) print(theta) print() + # Create an experiment list with k[2] variable + exp_list = [] + for i in range(len(data)): + exp_list.append(SimpleReactionExperimentK2Variable(data[i])) + # ======================================================================= # Estimate both k1 and k2 and compute the covariance matrix - theta_names = ['k'] - pest = parmest.Estimator(simple_reaction_model, data, theta_names) + pest = parmest.Estimator(exp_list) n = 15 # total number of data points used in the objective (y in 15 scenarios) obj, theta, cov = pest.theta_est(calc_cov=True, cov_n=n) print(obj) diff --git a/pyomo/contrib/parmest/examples/reactor_design/__init__.py b/pyomo/contrib/parmest/examples/reactor_design/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/__init__.py +++ b/pyomo/contrib/parmest/examples/reactor_design/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/examples/reactor_design/bootstrap_example.py b/pyomo/contrib/parmest/examples/reactor_design/bootstrap_example.py index cf1b8a2de23..598fef32b60 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/bootstrap_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/bootstrap_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,35 +9,31 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import pandas as pd +from pyomo.common.dependencies import pandas as pd from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( - reactor_design_model, + ReactorDesignExperiment, ) def main(): - # Vars to estimate - theta_names = ['k1', 'k2', 'k3'] - # Data + # Read in data file_dirname = dirname(abspath(str(__file__))) - file_name = abspath(join(file_dirname, 'reactor_data.csv')) + file_name = abspath(join(file_dirname, "reactor_data.csv")) data = pd.read_csv(file_name) - # Sum of squared error function - def SSE(model, data): - expr = ( - (float(data['ca']) - model.ca) ** 2 - + (float(data['cb']) - model.cb) ** 2 - + (float(data['cc']) - model.cc) ** 2 - + (float(data['cd']) - model.cd) ** 2 - ) - return expr + # Create an experiment list + exp_list = [] + for i in range(data.shape[0]): + exp_list.append(ReactorDesignExperiment(data, i)) - # Create an instance of the parmest estimator - pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # exp0_model.pprint() + + pest = parmest.Estimator(exp_list, obj_function='SSE') # Parameter estimation obj, theta = pest.theta_est() @@ -46,13 +42,13 @@ def SSE(model, data): bootstrap_theta = pest.theta_est_bootstrap(50) # Plot results - parmest.graphics.pairwise_plot(bootstrap_theta, title='Bootstrap theta') + parmest.graphics.pairwise_plot(bootstrap_theta, title="Bootstrap theta") parmest.graphics.pairwise_plot( bootstrap_theta, theta, 0.8, - ['MVN', 'KDE', 'Rect'], - title='Bootstrap theta with confidence regions', + ["MVN", "KDE", "Rect"], + title="Bootstrap theta with confidence regions", ) diff --git a/pyomo/contrib/parmest/examples/reactor_design/confidence_region_example.py b/pyomo/contrib/parmest/examples/reactor_design/confidence_region_example.py new file mode 100644 index 00000000000..73129baf5cb --- /dev/null +++ b/pyomo/contrib/parmest/examples/reactor_design/confidence_region_example.py @@ -0,0 +1,51 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pandas as pd +from os.path import join, abspath, dirname +import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( + ReactorDesignExperiment, +) + + +def main(): + + # Read in data + file_dirname = dirname(abspath(str(__file__))) + file_name = abspath(join(file_dirname, "reactor_data.csv")) + data = pd.read_csv(file_name) + + # Create an experiment list + exp_list = [] + for i in range(data.shape[0]): + exp_list.append(ReactorDesignExperiment(data, i)) + + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # exp0_model.pprint() + + pest = parmest.Estimator(exp_list, obj_function='SSE') + + # Parameter estimation + obj, theta = pest.theta_est() + + # Bootstrapping + bootstrap_theta = pest.theta_est_bootstrap(10) + print(bootstrap_theta) + + # Confidence region test + CR = pest.confidence_region_test(bootstrap_theta, "MVN", [0.5, 0.75, 1.0]) + print(CR) + + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py index 811571e20ed..be08e727be9 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,25 +9,90 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import numpy as np -import pandas as pd +import pyomo.environ as pyo +from pyomo.common.dependencies import numpy as np, pandas as pd import pyomo.contrib.parmest.parmest as parmest from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( reactor_design_model, + ReactorDesignExperiment, ) np.random.seed(1234) -def reactor_design_model_for_datarec(data): - # Unfix inlet concentration for data rec - model = reactor_design_model(data) - model.caf.fixed = False +class ReactorDesignExperimentDataRec(ReactorDesignExperiment): - return model + def __init__(self, data, data_std, experiment_number): + + super().__init__(data, experiment_number) + self.data_std = data_std + + def create_model(self): + + self.model = m = reactor_design_model() + m.caf.fixed = False + + return m + + def label_model(self): + + m = self.model + + # experiment outputs + m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.experiment_outputs.update( + [ + (m.ca, self.data_i['ca']), + (m.cb, self.data_i['cb']), + (m.cc, self.data_i['cc']), + (m.cd, self.data_i['cd']), + ] + ) + + # experiment standard deviations + m.experiment_outputs_std = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.experiment_outputs_std.update( + [ + (m.ca, self.data_std['ca']), + (m.cb, self.data_std['cb']), + (m.cc, self.data_std['cc']), + (m.cd, self.data_std['cd']), + ] + ) + + # no unknowns (theta names) + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + + return m + + +class ReactorDesignExperimentPostDataRec(ReactorDesignExperiment): + + def __init__(self, data, data_std, experiment_number): + + super().__init__(data, experiment_number) + self.data_std = data_std + + def label_model(self): + + m = super().label_model() + + # add experiment standard deviations + m.experiment_outputs_std = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.experiment_outputs_std.update( + [ + (m.ca, self.data_std['ca']), + (m.cb, self.data_std['cb']), + (m.cc, self.data_std['cc']), + (m.cd, self.data_std['cd']), + ] + ) + + return m def generate_data(): + ### Generate data based on real sv, caf, ca, cb, cc, and cd sv_real = 1.05 caf_real = 10000 @@ -39,60 +104,66 @@ def generate_data(): data = pd.DataFrame() ndata = 200 # Normal distribution, mean = 3400, std = 500 - data['ca'] = 500 * np.random.randn(ndata) + 3400 + data["ca"] = 500 * np.random.randn(ndata) + 3400 # Random distribution between 500 and 1500 - data['cb'] = np.random.rand(ndata) * 1000 + 500 + data["cb"] = np.random.rand(ndata) * 1000 + 500 # Lognormal distribution - data['cc'] = np.random.lognormal(np.log(1600), 0.25, ndata) + data["cc"] = np.random.lognormal(np.log(1600), 0.25, ndata) # Triangular distribution between 1000 and 2000 - data['cd'] = np.random.triangular(1000, 1800, 3000, size=ndata) + data["cd"] = np.random.triangular(1000, 1800, 3000, size=ndata) - data['sv'] = sv_real - data['caf'] = caf_real + data["sv"] = sv_real + data["caf"] = caf_real return data def main(): + # Generate data data = generate_data() data_std = data.std() + # Create an experiment list + exp_list = [] + for i in range(data.shape[0]): + exp_list.append(ReactorDesignExperimentDataRec(data, data_std, i)) + # Define sum of squared error objective function for data rec - def SSE(model, data): - expr = ( - ((float(data['ca']) - model.ca) / float(data_std['ca'])) ** 2 - + ((float(data['cb']) - model.cb) / float(data_std['cb'])) ** 2 - + ((float(data['cc']) - model.cc) / float(data_std['cc'])) ** 2 - + ((float(data['cd']) - model.cd) / float(data_std['cd'])) ** 2 + def SSE_with_std(model): + expr = sum( + ((y - y_hat) / model.experiment_outputs_std[y]) ** 2 + for y, y_hat in model.experiment_outputs.items() ) return expr ### Data reconciliation - theta_names = [] # no variables to estimate, use initialized values - - pest = parmest.Estimator(reactor_design_model_for_datarec, data, theta_names, SSE) + pest = parmest.Estimator(exp_list, obj_function=SSE_with_std) - obj, theta, data_rec = pest.theta_est(return_values=['ca', 'cb', 'cc', 'cd', 'caf']) + obj, theta, data_rec = pest.theta_est(return_values=["ca", "cb", "cc", "cd", "caf"]) print(obj) print(theta) parmest.graphics.grouped_boxplot( - data[['ca', 'cb', 'cc', 'cd']], - data_rec[['ca', 'cb', 'cc', 'cd']], - group_names=['Data', 'Data Rec'], + data[["ca", "cb", "cc", "cd"]], + data_rec[["ca", "cb", "cc", "cd"]], + group_names=["Data", "Data Rec"], ) ### Parameter estimation using reconciled data - theta_names = ['k1', 'k2', 'k3'] - data_rec['sv'] = data['sv'] + data_rec["sv"] = data["sv"] + + # make a new list of experiments using reconciled data + exp_list = [] + for i in range(data_rec.shape[0]): + exp_list.append(ReactorDesignExperimentPostDataRec(data_rec, data_std, i)) - pest = parmest.Estimator(reactor_design_model, data_rec, theta_names, SSE) + pest = parmest.Estimator(exp_list, obj_function=SSE_with_std) obj, theta = pest.theta_est() print(obj) print(theta) - theta_real = {'k1': 5.0 / 6.0, 'k2': 5.0 / 3.0, 'k3': 1.0 / 6000.0} + theta_real = {"k1": 5.0 / 6.0, "k2": 5.0 / 3.0, "k3": 1.0 / 6000.0} print(theta_real) diff --git a/pyomo/contrib/parmest/examples/reactor_design/leaveNout_example.py b/pyomo/contrib/parmest/examples/reactor_design/leaveNout_example.py index 95af53e63d3..9560981ca5c 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/leaveNout_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/leaveNout_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,22 +9,19 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import numpy as np -import pandas as pd +from pyomo.common.dependencies import numpy as np, pandas as pd from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( - reactor_design_model, + ReactorDesignExperiment, ) def main(): - # Vars to estimate - theta_names = ['k1', 'k2', 'k3'] - # Data + # Read in data file_dirname = dirname(abspath(str(__file__))) - file_name = abspath(join(file_dirname, 'reactor_data.csv')) + file_name = abspath(join(file_dirname, "reactor_data.csv")) data = pd.read_csv(file_name) # Create more data for the example @@ -34,18 +31,16 @@ def main(): df_sample = data.sample(N, replace=True).reset_index(drop=True) data = df_sample + df_rand.dot(df_std) / 10 - # Sum of squared error function - def SSE(model, data): - expr = ( - (float(data['ca']) - model.ca) ** 2 - + (float(data['cb']) - model.cb) ** 2 - + (float(data['cc']) - model.cc) ** 2 - + (float(data['cd']) - model.cd) ** 2 - ) - return expr + # Create an experiment list + exp_list = [] + for i in range(data.shape[0]): + exp_list.append(ReactorDesignExperiment(data, i)) + + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # exp0_model.pprint() - # Create an instance of the parmest estimator - pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) + pest = parmest.Estimator(exp_list, obj_function='SSE') # Parameter estimation obj, theta = pest.theta_est() @@ -68,7 +63,7 @@ def SSE(model, data): lNo = 25 lNo_samples = 5 bootstrap_samples = 20 - dist = 'MVN' + dist = "MVN" alphas = [0.7, 0.8, 0.9] results = pest.leaveNout_bootstrap_test( @@ -84,8 +79,8 @@ def SSE(model, data): bootstrap_results, theta_est_N, alpha, - ['MVN'], - title='Alpha: ' + str(alpha) + ', ' + str(theta_est_N.loc[0, alpha]), + ["MVN"], + title="Alpha: " + str(alpha) + ", " + str(theta_est_N.loc[0, alpha]), ) # Extract the percent of points that are within the alpha region diff --git a/pyomo/contrib/parmest/examples/reactor_design/likelihood_ratio_example.py b/pyomo/contrib/parmest/examples/reactor_design/likelihood_ratio_example.py index 13a40774740..c2bff254077 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/likelihood_ratio_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/likelihood_ratio_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,37 +9,32 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import numpy as np -import pandas as pd +from pyomo.common.dependencies import numpy as np, pandas as pd from itertools import product from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( - reactor_design_model, + ReactorDesignExperiment, ) def main(): - # Vars to estimate - theta_names = ['k1', 'k2', 'k3'] - # Data + # Read in data file_dirname = dirname(abspath(str(__file__))) - file_name = abspath(join(file_dirname, 'reactor_data.csv')) + file_name = abspath(join(file_dirname, "reactor_data.csv")) data = pd.read_csv(file_name) - # Sum of squared error function - def SSE(model, data): - expr = ( - (float(data['ca']) - model.ca) ** 2 - + (float(data['cb']) - model.cb) ** 2 - + (float(data['cc']) - model.cc) ** 2 - + (float(data['cd']) - model.cd) ** 2 - ) - return expr + # Create an experiment list + exp_list = [] + for i in range(data.shape[0]): + exp_list.append(ReactorDesignExperiment(data, i)) - # Create an instance of the parmest estimator - pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # exp0_model.pprint() + + pest = parmest.Estimator(exp_list, obj_function='SSE') # Parameter estimation obj, theta = pest.theta_est() @@ -48,7 +43,7 @@ def SSE(model, data): k1 = [0.8, 0.85, 0.9] k2 = [1.6, 1.65, 1.7] k3 = [0.00016, 0.000165, 0.00017] - theta_vals = pd.DataFrame(list(product(k1, k2, k3)), columns=['k1', 'k2', 'k3']) + theta_vals = pd.DataFrame(list(product(k1, k2, k3)), columns=["k1", "k2", "k3"]) obj_at_theta = pest.objective_at_theta(theta_vals) # Run the likelihood ratio test @@ -56,7 +51,7 @@ def SSE(model, data): # Plot results parmest.graphics.pairwise_plot( - LR, theta, 0.9, title='LR results within 90% confidence region' + LR, theta, 0.9, title="LR results within 90% confidence region" ) diff --git a/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py b/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py index bc564cbdfd3..208981a784a 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,39 +9,83 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import pandas as pd +from pyomo.common.dependencies import pandas as pd from os.path import join, abspath, dirname +import pyomo.environ as pyo import pyomo.contrib.parmest.parmest as parmest from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( - reactor_design_model, + ReactorDesignExperiment, ) +class MultisensorReactorDesignExperiment(ReactorDesignExperiment): + + def finalize_model(self): + + m = self.model + + # Experiment inputs values + m.sv = self.data_i['sv'] + m.caf = self.data_i['caf'] + + # Experiment output values + m.ca = (self.data_i['ca1'] + self.data_i['ca2'] + self.data_i['ca3']) * (1 / 3) + m.cb = self.data_i['cb'] + m.cc = (self.data_i['cc1'] + self.data_i['cc2']) * (1 / 2) + m.cd = self.data_i['cd'] + + return m + + def label_model(self): + + m = self.model + + m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.experiment_outputs.update( + [ + (m.ca, [self.data_i['ca1'], self.data_i['ca2'], self.data_i['ca3']]), + (m.cb, [self.data_i['cb']]), + (m.cc, [self.data_i['cc1'], self.data_i['cc2']]), + (m.cd, [self.data_i['cd']]), + ] + ) + + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.unknown_parameters.update( + (k, pyo.ComponentUID(k)) for k in [m.k1, m.k2, m.k3] + ) + + return m + + def main(): # Parameter estimation using multisensor data - # Vars to estimate - theta_names = ['k1', 'k2', 'k3'] - - # Data, includes multiple sensors for ca and cc + # Read in data file_dirname = dirname(abspath(str(__file__))) - file_name = abspath(join(file_dirname, 'reactor_data_multisensor.csv')) + file_name = abspath(join(file_dirname, "reactor_data_multisensor.csv")) data = pd.read_csv(file_name) - # Sum of squared error function - def SSE_multisensor(model, data): - expr = ( - ((float(data['ca1']) - model.ca) ** 2) * (1 / 3) - + ((float(data['ca2']) - model.ca) ** 2) * (1 / 3) - + ((float(data['ca3']) - model.ca) ** 2) * (1 / 3) - + (float(data['cb']) - model.cb) ** 2 - + ((float(data['cc1']) - model.cc) ** 2) * (1 / 2) - + ((float(data['cc2']) - model.cc) ** 2) * (1 / 2) - + (float(data['cd']) - model.cd) ** 2 - ) + # Create an experiment list + exp_list = [] + for i in range(data.shape[0]): + exp_list.append(MultisensorReactorDesignExperiment(data, i)) + + # Define sum of squared error + def SSE_multisensor(model): + expr = 0 + for y, y_hat in model.experiment_outputs.items(): + num_outputs = len(y_hat) + for i in range(num_outputs): + expr += ((y - y_hat[i]) ** 2) * (1 / num_outputs) return expr - pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE_multisensor) + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # exp0_model.pprint() + # print(SSE_multisensor(exp0_model)) + + pest = parmest.Estimator(exp_list, obj_function=SSE_multisensor) obj, theta = pest.theta_est() print(obj) print(theta) diff --git a/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py b/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py index 334dfa264a4..a84a3fde5e7 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,50 +9,33 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import pandas as pd +from pyomo.common.dependencies import pandas as pd from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( - reactor_design_model, + ReactorDesignExperiment, ) def main(): - # Vars to estimate - theta_names = ['k1', 'k2', 'k3'] - # Data + # Read in data file_dirname = dirname(abspath(str(__file__))) - file_name = abspath(join(file_dirname, 'reactor_data.csv')) + file_name = abspath(join(file_dirname, "reactor_data.csv")) data = pd.read_csv(file_name) - # Sum of squared error function - def SSE(model, data): - expr = ( - (float(data['ca']) - model.ca) ** 2 - + (float(data['cb']) - model.cb) ** 2 - + (float(data['cc']) - model.cc) ** 2 - + (float(data['cd']) - model.cd) ** 2 - ) - return expr - - # Create an instance of the parmest estimator - pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) - - # Parameter estimation - obj, theta = pest.theta_est() - - # Assert statements compare parameter estimation (theta) to an expected value - k1_expected = 5.0 / 6.0 - k2_expected = 5.0 / 3.0 - k3_expected = 1.0 / 6000.0 - relative_error = abs(theta['k1'] - k1_expected) / k1_expected - assert relative_error < 0.05 - relative_error = abs(theta['k2'] - k2_expected) / k2_expected - assert relative_error < 0.05 - relative_error = abs(theta['k3'] - k3_expected) / k3_expected - assert relative_error < 0.05 - - -if __name__ == "__main__": - main() + # Create an experiment list + exp_list = [] + for i in range(data.shape[0]): + exp_list.append(ReactorDesignExperiment(data, i)) + + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # exp0_model.pprint() + + pest = parmest.Estimator(exp_list, obj_function='SSE') + + # Parameter estimation with covariance + obj, theta, cov = pest.theta_est(calc_cov=True, cov_n=17) + print(obj) + print(theta) diff --git a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py index f4cd6c8dbf5..a396c1ea721 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py +++ b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -12,47 +12,46 @@ Continuously stirred tank reactor model, based on pyomo/examples/doc/pyomobook/nonlinear-ch/react_design/ReactorDesign.py """ -import pandas as pd -from pyomo.environ import ( - ConcreteModel, - Param, - Var, - PositiveReals, - Objective, - Constraint, - maximize, - SolverFactory, -) - - -def reactor_design_model(data): + +from pyomo.common.dependencies import pandas as pd +import pyomo.environ as pyo +import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.experiment import Experiment + + +def reactor_design_model(): + # Create the concrete model - model = ConcreteModel() + model = pyo.ConcreteModel() # Rate constants - model.k1 = Param(initialize=5.0 / 6.0, within=PositiveReals, mutable=True) # min^-1 - model.k2 = Param(initialize=5.0 / 3.0, within=PositiveReals, mutable=True) # min^-1 - model.k3 = Param( - initialize=1.0 / 6000.0, within=PositiveReals, mutable=True + model.k1 = pyo.Param( + initialize=5.0 / 6.0, within=pyo.PositiveReals, mutable=True + ) # min^-1 + model.k2 = pyo.Param( + initialize=5.0 / 3.0, within=pyo.PositiveReals, mutable=True + ) # min^-1 + model.k3 = pyo.Param( + initialize=1.0 / 6000.0, within=pyo.PositiveReals, mutable=True ) # m^3/(gmol min) # Inlet concentration of A, gmol/m^3 - model.caf = Param(initialize=float(data['caf']), within=PositiveReals) + model.caf = pyo.Param(initialize=10000, within=pyo.PositiveReals, mutable=True) # Space velocity (flowrate/volume) - model.sv = Param(initialize=float(data['sv']), within=PositiveReals) + model.sv = pyo.Param(initialize=1.0, within=pyo.PositiveReals, mutable=True) # Outlet concentration of each component - model.ca = Var(initialize=5000.0, within=PositiveReals) - model.cb = Var(initialize=2000.0, within=PositiveReals) - model.cc = Var(initialize=2000.0, within=PositiveReals) - model.cd = Var(initialize=1000.0, within=PositiveReals) + model.ca = pyo.Var(initialize=5000.0, within=pyo.PositiveReals) + model.cb = pyo.Var(initialize=2000.0, within=pyo.PositiveReals) + model.cc = pyo.Var(initialize=2000.0, within=pyo.PositiveReals) + model.cd = pyo.Var(initialize=1000.0, within=pyo.PositiveReals) # Objective - model.obj = Objective(expr=model.cb, sense=maximize) + model.obj = pyo.Objective(expr=model.cb, sense=pyo.maximize) # Constraints - model.ca_bal = Constraint( + model.ca_bal = pyo.Constraint( expr=( 0 == model.sv * model.caf @@ -62,31 +61,99 @@ def reactor_design_model(data): ) ) - model.cb_bal = Constraint( + model.cb_bal = pyo.Constraint( expr=(0 == -model.sv * model.cb + model.k1 * model.ca - model.k2 * model.cb) ) - model.cc_bal = Constraint(expr=(0 == -model.sv * model.cc + model.k2 * model.cb)) + model.cc_bal = pyo.Constraint( + expr=(0 == -model.sv * model.cc + model.k2 * model.cb) + ) - model.cd_bal = Constraint( + model.cd_bal = pyo.Constraint( expr=(0 == -model.sv * model.cd + model.k3 * model.ca**2.0) ) return model +class ReactorDesignExperiment(Experiment): + + def __init__(self, data, experiment_number): + self.data = data + self.experiment_number = experiment_number + self.data_i = data.loc[experiment_number, :] + self.model = None + + def create_model(self): + self.model = m = reactor_design_model() + return m + + def finalize_model(self): + m = self.model + + # Experiment inputs values + m.sv = self.data_i['sv'] + m.caf = self.data_i['caf'] + + # Experiment output values + m.ca = self.data_i['ca'] + m.cb = self.data_i['cb'] + m.cc = self.data_i['cc'] + m.cd = self.data_i['cd'] + + return m + + def label_model(self): + m = self.model + + m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.experiment_outputs.update( + [ + (m.ca, self.data_i['ca']), + (m.cb, self.data_i['cb']), + (m.cc, self.data_i['cc']), + (m.cd, self.data_i['cd']), + ] + ) + + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.unknown_parameters.update( + (k, pyo.ComponentUID(k)) for k in [m.k1, m.k2, m.k3] + ) + + return m + + def get_labeled_model(self): + m = self.create_model() + m = self.finalize_model() + m = self.label_model() + + return m + + def main(): + # For a range of sv values, return ca, cb, cc, and cd results = [] sv_values = [1.0 + v * 0.05 for v in range(1, 20)] caf = 10000 for sv in sv_values: - model = reactor_design_model({'caf': caf, 'sv': sv}) - solver = SolverFactory('ipopt') + + # make model + model = reactor_design_model() + + # add caf, sv + model.caf = caf + model.sv = sv + + # solve model + solver = pyo.SolverFactory("ipopt") solver.solve(model) + + # save results results.append([sv, caf, model.ca(), model.cb(), model.cc(), model.cd()]) - results = pd.DataFrame(results, columns=['sv', 'caf', 'ca', 'cb', 'cc', 'cd']) + results = pd.DataFrame(results, columns=["sv", "caf", "ca", "cb", "cc", "cd"]) print(results) diff --git a/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py b/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py index da2ab1874c9..4eb191afd6d 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,43 +9,69 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import pandas as pd +from pyomo.common.dependencies import pandas as pd from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( - reactor_design_model, + ReactorDesignExperiment, ) -def main(): - # Parameter estimation using timeseries data +class TimeSeriesReactorDesignExperiment(ReactorDesignExperiment): + + def __init__(self, data, experiment_number): + self.data = data + self.experiment_number = experiment_number + data_i = data.loc[data['experiment'] == experiment_number, :] + self.data_i = data_i.reset_index() + self.model = None + + def finalize_model(self): + m = self.model + + # Experiment inputs values + m.sv = self.data_i['sv'].mean() + m.caf = self.data_i['caf'].mean() + + # Experiment output values + m.ca = self.data_i['ca'][0] + m.cb = self.data_i['cb'][0] + m.cc = self.data_i['cc'][0] + m.cd = self.data_i['cd'][0] + + return m - # Vars to estimate - theta_names = ['k1', 'k2', 'k3'] + +def main(): + # Parameter estimation using timeseries data, grouped by experiment number # Data, includes multiple sensors for ca and cc file_dirname = dirname(abspath(str(__file__))) file_name = abspath(join(file_dirname, 'reactor_data_timeseries.csv')) data = pd.read_csv(file_name) - # Group time series data into experiments, return the mean value for sv and caf - # Returns a list of dictionaries - data_ts = parmest.group_data(data, 'experiment', ['sv', 'caf']) + # Create an experiment list + exp_list = [] + for i in data['experiment'].unique(): + exp_list.append(TimeSeriesReactorDesignExperiment(data, i)) + + def SSE_timeseries(model): - def SSE_timeseries(model, data): expr = 0 - for val in data['ca']: - expr = expr + ((float(val) - model.ca) ** 2) * (1 / len(data['ca'])) - for val in data['cb']: - expr = expr + ((float(val) - model.cb) ** 2) * (1 / len(data['cb'])) - for val in data['cc']: - expr = expr + ((float(val) - model.cc) ** 2) * (1 / len(data['cc'])) - for val in data['cd']: - expr = expr + ((float(val) - model.cd) ** 2) * (1 / len(data['cd'])) + for y, y_hat in model.experiment_outputs.items(): + num_time_points = len(y_hat) + for i in range(num_time_points): + expr += ((y - y_hat[i]) ** 2) * (1 / num_time_points) + return expr - pest = parmest.Estimator(reactor_design_model, data_ts, theta_names, SSE_timeseries) + # View one model & SSE + # exp0_model = exp_list[0].get_labeled_model() + # exp0_model.pprint() + # print(SSE_timeseries(exp0_model)) + + pest = parmest.Estimator(exp_list, obj_function=SSE_timeseries) obj, theta = pest.theta_est() print(obj) print(theta) diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/__init__.py b/pyomo/contrib/parmest/examples/rooney_biegler/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/__init__.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py b/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py index f686bbd933d..944a01ac95e 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,16 +9,14 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import pandas as pd +from pyomo.common.dependencies import pandas as pd import pyomo.contrib.parmest.parmest as parmest from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( - rooney_biegler_model, + RooneyBieglerExperiment, ) def main(): - # Vars to estimate - theta_names = ['asymptote', 'rate_constant'] # Data data = pd.DataFrame( @@ -27,14 +25,24 @@ def main(): ) # Sum of squared error function - def SSE(model, data): - expr = sum( - (data.y[i] - model.response_function[data.hour[i]]) ** 2 for i in data.index - ) + def SSE(model): + expr = ( + model.experiment_outputs[model.y] + - model.response_function[model.experiment_outputs[model.hour]] + ) ** 2 return expr + # Create an experiment list + exp_list = [] + for i in range(data.shape[0]): + exp_list.append(RooneyBieglerExperiment(data.loc[i, :])) + + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # exp0_model.pprint() + # Create an instance of the parmest estimator - pest = parmest.Estimator(rooney_biegler_model, data, theta_names, SSE) + pest = parmest.Estimator(exp_list, obj_function=SSE) # Parameter estimation obj, theta = pest.theta_est() diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py b/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py index 5e54a33abda..54343993286 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,18 +9,15 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import numpy as np -import pandas as pd +from pyomo.common.dependencies import numpy as np, pandas as pd from itertools import product import pyomo.contrib.parmest.parmest as parmest from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( - rooney_biegler_model, + RooneyBieglerExperiment, ) def main(): - # Vars to estimate - theta_names = ['asymptote', 'rate_constant'] # Data data = pd.DataFrame( @@ -29,14 +26,24 @@ def main(): ) # Sum of squared error function - def SSE(model, data): - expr = sum( - (data.y[i] - model.response_function[data.hour[i]]) ** 2 for i in data.index - ) + def SSE(model): + expr = ( + model.experiment_outputs[model.y] + - model.response_function[model.experiment_outputs[model.hour]] + ) ** 2 return expr + # Create an experiment list + exp_list = [] + for i in range(data.shape[0]): + exp_list.append(RooneyBieglerExperiment(data.loc[i, :])) + + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # exp0_model.pprint() + # Create an instance of the parmest estimator - pest = parmest.Estimator(rooney_biegler_model, data, theta_names, SSE) + pest = parmest.Estimator(exp_list, obj_function=SSE) # Parameter estimation obj, theta = pest.theta_est() diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py b/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py index 9af33217fe4..3c9a93100bb 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,16 +9,14 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import pandas as pd +from pyomo.common.dependencies import pandas as pd import pyomo.contrib.parmest.parmest as parmest from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( - rooney_biegler_model, + RooneyBieglerExperiment, ) def main(): - # Vars to estimate - theta_names = ['asymptote', 'rate_constant'] # Data data = pd.DataFrame( @@ -27,14 +25,24 @@ def main(): ) # Sum of squared error function - def SSE(model, data): - expr = sum( - (data.y[i] - model.response_function[data.hour[i]]) ** 2 for i in data.index - ) + def SSE(model): + expr = ( + model.experiment_outputs[model.y] + - model.response_function[model.experiment_outputs[model.hour]] + ) ** 2 return expr + # Create an experiment list + exp_list = [] + for i in range(data.shape[0]): + exp_list.append(RooneyBieglerExperiment(data.loc[i, :])) + + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # exp0_model.pprint() + # Create an instance of the parmest estimator - pest = parmest.Estimator(rooney_biegler_model, data, theta_names, SSE) + pest = parmest.Estimator(exp_list, obj_function=SSE) # Parameter estimation and covariance n = 6 # total number of data points used in the objective (y in 6 scenarios) diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py index 5a0e1238e85..9625ab32ea3 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -15,8 +15,9 @@ 47(8), 1794-1804. """ -import pandas as pd +from pyomo.common.dependencies import pandas as pd import pyomo.environ as pyo +from pyomo.contrib.parmest.experiment import Experiment def rooney_biegler_model(data): @@ -25,6 +26,9 @@ def rooney_biegler_model(data): model.asymptote = pyo.Var(initialize=15) model.rate_constant = pyo.Var(initialize=0.5) + model.hour = pyo.Param(within=pyo.PositiveReals, mutable=True) + model.y = pyo.Param(within=pyo.PositiveReals, mutable=True) + def response_rule(m, h): expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) return expr @@ -41,6 +45,47 @@ def SSE_rule(m): return model +class RooneyBieglerExperiment(Experiment): + + def __init__(self, data): + self.data = data + self.model = None + + def create_model(self): + # rooney_biegler_model expects a dataframe + data_df = self.data.to_frame().transpose() + self.model = rooney_biegler_model(data_df) + + def label_model(self): + + m = self.model + + m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.experiment_outputs.update( + [(m.hour, self.data['hour']), (m.y, self.data['y'])] + ) + + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.unknown_parameters.update( + (k, pyo.ComponentUID(k)) for k in [m.asymptote, m.rate_constant] + ) + + def finalize_model(self): + + m = self.model + + # Experiment output values + m.hour = self.data['hour'] + m.y = self.data['y'] + + def get_labeled_model(self): + self.create_model() + self.label_model() + self.finalize_model() + + return self.model + + def main(): # These were taken from Table A1.4 in Bates and Watts (1988). data = pd.DataFrame( diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py index 2582e3fe928..dd82b50cf7a 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -15,8 +15,9 @@ 47(8), 1794-1804. """ -import pandas as pd +from pyomo.common.dependencies import pandas as pd import pyomo.environ as pyo +from pyomo.contrib.parmest.experiment import Experiment def rooney_biegler_model_with_constraint(data): @@ -24,6 +25,10 @@ def rooney_biegler_model_with_constraint(data): model.asymptote = pyo.Var(initialize=15) model.rate_constant = pyo.Var(initialize=0.5) + + model.hour = pyo.Param(within=pyo.PositiveReals, mutable=True) + model.y = pyo.Param(within=pyo.PositiveReals, mutable=True) + model.response_function = pyo.Var(data.hour, initialize=0.0) # changed from expression to constraint @@ -44,6 +49,47 @@ def SSE_rule(m): return model +class RooneyBieglerExperiment(Experiment): + + def __init__(self, data): + self.data = data + self.model = None + + def create_model(self): + # rooney_biegler_model_with_constraint expects a dataframe + data_df = self.data.to_frame().transpose() + self.model = rooney_biegler_model_with_constraint(data_df) + + def label_model(self): + + m = self.model + + m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.experiment_outputs.update( + [(m.hour, self.data['hour']), (m.y, self.data['y'])] + ) + + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.unknown_parameters.update( + (k, pyo.ComponentUID(k)) for k in [m.asymptote, m.rate_constant] + ) + + def finalize_model(self): + + m = self.model + + # Experiment output values + m.hour = self.data['hour'] + m.y = self.data['y'] + + def get_labeled_model(self): + self.create_model() + self.label_model() + self.finalize_model() + + return self.model + + def main(): # These were taken from Table A1.4 in Bates and Watts (1988). data = pd.DataFrame( diff --git a/pyomo/contrib/parmest/examples/semibatch/__init__.py b/pyomo/contrib/parmest/examples/semibatch/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/parmest/examples/semibatch/__init__.py +++ b/pyomo/contrib/parmest/examples/semibatch/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/examples/semibatch/obj_at_theta.csv b/pyomo/contrib/parmest/examples/semibatch/obj_at_theta.csv new file mode 100644 index 00000000000..79f03e07dcd --- /dev/null +++ b/pyomo/contrib/parmest/examples/semibatch/obj_at_theta.csv @@ -0,0 +1,1009 @@ +,k1,k2,E1,E2,obj +0,4,40,29000,38000,667.4023645794207 +1,4,40,29000,38500,665.8312183437167 +2,4,40,29000,39000,672.7539769993407 +3,4,40,29000,39500,684.9503752463216 +4,4,40,29000,40000,699.985589093255 +5,4,40,29000,40500,716.1241770970677 +6,4,40,29000,41000,732.2023201586336 +7,4,40,29000,41500,747.4931745925483 +8,4,40,29500,38000,907.4405527163311 +9,4,40,29500,38500,904.2229271927299 +10,4,40,29500,39000,907.6942345285257 +11,4,40,29500,39500,915.4570013614677 +12,4,40,29500,40000,925.65401444575 +13,4,40,29500,40500,936.9348578520337 +14,4,40,29500,41000,948.3759339765711 +15,4,40,29500,41500,959.386491783636 +16,4,40,30000,38000,1169.8685711377334 +17,4,40,30000,38500,1166.2211505723928 +18,4,40,30000,39000,1167.702295374574 +19,4,40,30000,39500,1172.5517020611685 +20,4,40,30000,40000,1179.3820406408263 +21,4,40,30000,40500,1187.1698633839655 +22,4,40,30000,41000,1195.2047840919602 +23,4,40,30000,41500,1203.0241101248102 +24,4,40,30500,38000,1445.9591944684807 +25,4,40,30500,38500,1442.6632745483 +26,4,40,30500,39000,1443.1982444457385 +27,4,40,30500,39500,1446.2833842279929 +28,4,40,30500,40000,1450.9012120934779 +29,4,40,30500,40500,1456.295140290636 +30,4,40,30500,41000,1461.9350767569827 +31,4,40,30500,41500,1467.4715014446226 +32,4,40,31000,38000,1726.8744994061449 +33,4,40,31000,38500,1724.2679845375048 +34,4,40,31000,39000,1724.4550886870552 +35,4,40,31000,39500,1726.5124587129135 +36,4,40,31000,40000,1729.7061680616455 +37,4,40,31000,40500,1733.48893482641 +38,4,40,31000,41000,1737.4753558920438 +39,4,40,31000,41500,1741.4093763605517 +40,4,40,31500,38000,2004.1978135112938 +41,4,40,31500,38500,2002.2807839860222 +42,4,40,31500,39000,2002.3676405166086 +43,4,40,31500,39500,2003.797808439923 +44,4,40,31500,40000,2006.048051591001 +45,4,40,31500,40500,2008.7281679153625 +46,4,40,31500,41000,2011.5626384878237 +47,4,40,31500,41500,2014.3675286347284 +48,4,80,29000,38000,845.8197358579285 +49,4,80,29000,38500,763.5039795545781 +50,4,80,29000,39000,709.8529964173656 +51,4,80,29000,39500,679.4215539491266 +52,4,80,29000,40000,666.4876088521157 +53,4,80,29000,40500,665.978271760966 +54,4,80,29000,41000,673.7240200504901 +55,4,80,29000,41500,686.4763909417914 +56,4,80,29500,38000,1042.519415429413 +57,4,80,29500,38500,982.8097210678039 +58,4,80,29500,39000,942.2990207573541 +59,4,80,29500,39500,917.9550916645245 +60,4,80,29500,40000,906.3116029967189 +61,4,80,29500,40500,904.0326666308792 +62,4,80,29500,41000,908.1964630052729 +63,4,80,29500,41500,916.4222043837499 +64,4,80,30000,38000,1271.1030403496538 +65,4,80,30000,38500,1227.7527550544085 +66,4,80,30000,39000,1197.433957624904 +67,4,80,30000,39500,1178.447676126182 +68,4,80,30000,40000,1168.645219243497 +69,4,80,30000,40500,1165.7995210546096 +70,4,80,30000,41000,1167.8586496250396 +71,4,80,30000,41500,1173.0949214020527 +72,4,80,30500,38000,1520.8220402652044 +73,4,80,30500,38500,1489.2563260709424 +74,4,80,30500,39000,1466.8099189128857 +75,4,80,30500,39500,1452.4352624958806 +76,4,80,30500,40000,1444.7074679423818 +77,4,80,30500,40500,1442.0820578624343 +78,4,80,30500,41000,1443.099006489627 +79,4,80,30500,41500,1446.5106517200784 +80,4,80,31000,38000,1781.149136032395 +81,4,80,31000,38500,1758.2414369536502 +82,4,80,31000,39000,1741.891639711003 +83,4,80,31000,39500,1731.358661496594 +84,4,80,31000,40000,1725.6231647999593 +85,4,80,31000,40500,1723.5757174297378 +86,4,80,31000,41000,1724.1680229486278 +87,4,80,31000,41500,1726.5050840601884 +88,4,80,31500,38000,2042.8335948845602 +89,4,80,31500,38500,2026.3067503042414 +90,4,80,31500,39000,2014.5720701940838 +91,4,80,31500,39500,2007.0463766643977 +92,4,80,31500,40000,2002.9647983728314 +93,4,80,31500,40500,2001.5163951989875 +94,4,80,31500,41000,2001.9474217001339 +95,4,80,31500,41500,2003.6204088755821 +96,4,120,29000,38000,1176.0713512305115 +97,4,120,29000,38500,1016.8213383282462 +98,4,120,29000,39000,886.0136231565133 +99,4,120,29000,39500,789.0101180066036 +100,4,120,29000,40000,724.5420056133441 +101,4,120,29000,40500,686.6877602625062 +102,4,120,29000,41000,668.8129085873959 +103,4,120,29000,41500,665.1167761036883 +104,4,120,29500,38000,1263.887274509128 +105,4,120,29500,38500,1155.6528408872423 +106,4,120,29500,39000,1066.393539894248 +107,4,120,29500,39500,998.9931006471243 +108,4,120,29500,40000,952.36314487701 +109,4,120,29500,40500,923.4000293372077 +110,4,120,29500,41000,908.407361383214 +111,4,120,29500,41500,903.8136176328255 +112,4,120,30000,38000,1421.1418235449091 +113,4,120,30000,38500,1347.114022652679 +114,4,120,30000,39000,1285.686103704643 +115,4,120,30000,39500,1238.2456448658272 +116,4,120,30000,40000,1204.3526810790904 +117,4,120,30000,40500,1182.4272879027071 +118,4,120,30000,41000,1170.3447810121902 +119,4,120,30000,41500,1165.8422968073423 +120,4,120,30500,38000,1625.5588911535713 +121,4,120,30500,38500,1573.5546642859429 +122,4,120,30500,39000,1530.1592840718379 +123,4,120,30500,39500,1496.2087139473604 +124,4,120,30500,40000,1471.525855239756 +125,4,120,30500,40500,1455.2084749904016 +126,4,120,30500,41000,1445.9160840082027 +127,4,120,30500,41500,1442.1255377330835 +128,4,120,31000,38000,1855.8467211183756 +129,4,120,31000,38500,1818.4368412235558 +130,4,120,31000,39000,1787.25956706785 +131,4,120,31000,39500,1762.8169908546402 +132,4,120,31000,40000,1744.9825741661596 +133,4,120,31000,40500,1733.136625016882 +134,4,120,31000,41000,1726.3352245899828 +135,4,120,31000,41500,1723.492199933745 +136,4,120,31500,38000,2096.6479813687533 +137,4,120,31500,38500,2069.3606691038876 +138,4,120,31500,39000,2046.792043575205 +139,4,120,31500,39500,2029.2128703900223 +140,4,120,31500,40000,2016.4664599897606 +141,4,120,31500,40500,2008.054814885348 +142,4,120,31500,41000,2003.2622557140814 +143,4,120,31500,41500,2001.289784483679 +144,7,40,29000,38000,149.32898706737052 +145,7,40,29000,38500,161.04814413969586 +146,7,40,29000,39000,187.87801343005242 +147,7,40,29000,39500,223.00789161520424 +148,7,40,29000,40000,261.66779887964003 +149,7,40,29000,40500,300.676316191238 +150,7,40,29000,41000,338.04021206995765 +151,7,40,29000,41500,372.6191631389286 +152,7,40,29500,38000,276.6495061185777 +153,7,40,29500,38500,282.1304583501965 +154,7,40,29500,39000,300.91417483065254 +155,7,40,29500,39500,327.24304394350395 +156,7,40,29500,40000,357.0561976596432 +157,7,40,29500,40500,387.61662064170207 +158,7,40,29500,41000,417.1836349752378 +159,7,40,29500,41500,444.73705844573243 +160,7,40,30000,38000,448.0380830353589 +161,7,40,30000,38500,448.8094536459122 +162,7,40,30000,39000,460.77530593327293 +163,7,40,30000,39500,479.342874472736 +164,7,40,30000,40000,501.20694459059405 +165,7,40,30000,40500,524.0971649678811 +166,7,40,30000,41000,546.539334134893 +167,7,40,30000,41500,567.6447156158981 +168,7,40,30500,38000,657.9909416906933 +169,7,40,30500,38500,655.7465129488842 +170,7,40,30500,39000,662.5420970804985 +171,7,40,30500,39500,674.8914651553109 +172,7,40,30500,40000,690.2111920703564 +173,7,40,30500,40500,706.6833639709198 +174,7,40,30500,41000,723.0994507096715 +175,7,40,30500,41500,738.7096013891406 +176,7,40,31000,38000,899.1769906655776 +177,7,40,31000,38500,895.4391505892945 +178,7,40,31000,39000,898.7695629120826 +179,7,40,31000,39500,906.603316771593 +180,7,40,31000,40000,916.9811481373996 +181,7,40,31000,40500,928.4913367709245 +182,7,40,31000,41000,940.1744934710283 +183,7,40,31000,41500,951.4199286075984 +184,7,40,31500,38000,1163.093373675207 +185,7,40,31500,38500,1159.0457727559028 +186,7,40,31500,39000,1160.3831770028223 +187,7,40,31500,39500,1165.2451698296604 +188,7,40,31500,40000,1172.1768190340001 +189,7,40,31500,40500,1180.1105659428963 +190,7,40,31500,41000,1188.3083929833688 +191,7,40,31500,41500,1196.29112579565 +192,7,80,29000,38000,514.0332369183081 +193,7,80,29000,38500,329.3645784712966 +194,7,80,29000,39000,215.73000998706416 +195,7,80,29000,39500,162.37338399591852 +196,7,80,29000,40000,149.8401793263549 +197,7,80,29000,40500,162.96125998112578 +198,7,80,29000,41000,191.173279165834 +199,7,80,29000,41500,227.2781971491003 +200,7,80,29500,38000,623.559246695578 +201,7,80,29500,38500,448.60620511421484 +202,7,80,29500,39000,344.21940687907573 +203,7,80,29500,39500,292.9758707105001 +204,7,80,29500,40000,277.07670134364804 +205,7,80,29500,40500,283.5158840045542 +206,7,80,29500,41000,303.33951582820265 +207,7,80,29500,41500,330.43357046741954 +208,7,80,30000,38000,732.5907387079073 +209,7,80,30000,38500,593.1926567994672 +210,7,80,30000,39000,508.5638538704666 +211,7,80,30000,39500,464.47881763522037 +212,7,80,30000,40000,448.0394620671692 +213,7,80,30000,40500,449.64309860415494 +214,7,80,30000,41000,462.4490598612332 +215,7,80,30000,41500,481.6323506247537 +216,7,80,30500,38000,871.1163930229344 +217,7,80,30500,38500,771.1320563649375 +218,7,80,30500,39000,707.8872660015606 +219,7,80,30500,39500,672.6612145133173 +220,7,80,30500,40000,657.4974157809264 +221,7,80,30500,40500,656.0835852491216 +222,7,80,30500,41000,663.6006958125331 +223,7,80,30500,41500,676.460675405631 +224,7,80,31000,38000,1053.1852617390061 +225,7,80,31000,38500,984.3647109805877 +226,7,80,31000,39000,938.6158531749268 +227,7,80,31000,39500,911.4268280093535 +228,7,80,31000,40000,898.333365348419 +229,7,80,31000,40500,895.3996527486954 +230,7,80,31000,41000,899.3556288533885 +231,7,80,31000,41500,907.6180684887955 +232,7,80,31500,38000,1274.2255948763498 +233,7,80,31500,38500,1226.5236809533717 +234,7,80,31500,39000,1193.4538731398666 +235,7,80,31500,39500,1172.8105398345213 +236,7,80,31500,40000,1162.0692230240734 +237,7,80,31500,40500,1158.7461521476607 +238,7,80,31500,41000,1160.6173577210805 +239,7,80,31500,41500,1165.840315694716 +240,7,120,29000,38000,1325.2409732290193 +241,7,120,29000,38500,900.8063148840154 +242,7,120,29000,39000,629.9300352098937 +243,7,120,29000,39500,413.81648033893424 +244,7,120,29000,40000,257.3116751690404 +245,7,120,29000,40500,177.89217179438947 +246,7,120,29000,41000,151.58366848473491 +247,7,120,29000,41500,157.56967437251706 +248,7,120,29500,38000,1211.2807882170853 +249,7,120,29500,38500,956.936161969002 +250,7,120,29500,39000,753.3050086992201 +251,7,120,29500,39500,528.2452647799327 +252,7,120,29500,40000,382.62610532894917 +253,7,120,29500,40500,308.44199089882375 +254,7,120,29500,41000,280.3893024671524 +255,7,120,29500,41500,280.4028092582749 +256,7,120,30000,38000,1266.5740351143413 +257,7,120,30000,38500,1084.3028700477778 +258,7,120,30000,39000,834.2392498526193 +259,7,120,30000,39500,650.7560171314304 +260,7,120,30000,40000,537.7846910878052 +261,7,120,30000,40500,477.3001078155485 +262,7,120,30000,41000,451.6865380286754 +263,7,120,30000,41500,448.14911508024613 +264,7,120,30500,38000,1319.6603196780936 +265,7,120,30500,38500,1102.3027489012372 +266,7,120,30500,39000,931.2523583659847 +267,7,120,30500,39500,807.0833484596384 +268,7,120,30500,40000,727.4852710400268 +269,7,120,30500,40500,682.1437030344305 +270,7,120,30500,41000,660.7859329989657 +271,7,120,30500,41500,655.6001132492668 +272,7,120,31000,38000,1330.5306924865326 +273,7,120,31000,38500,1195.9190861202942 +274,7,120,31000,39000,1086.0328080422887 +275,7,120,31000,39500,1005.4160637517409 +276,7,120,31000,40000,951.2021706290612 +277,7,120,31000,40500,918.1457644271304 +278,7,120,31000,41000,901.0511005554887 +279,7,120,31000,41500,895.4599964465793 +280,7,120,31500,38000,1447.8365822059013 +281,7,120,31500,38500,1362.3417347939844 +282,7,120,31500,39000,1292.382727215108 +283,7,120,31500,39500,1239.1826828976662 +284,7,120,31500,40000,1201.6474412465277 +285,7,120,31500,40500,1177.5235955796813 +286,7,120,31500,41000,1164.1761722345295 +287,7,120,31500,41500,1158.9997785002718 +288,10,40,29000,38000,33.437068437082054 +289,10,40,29000,38500,58.471249815534996 +290,10,40,29000,39000,101.41937628542912 +291,10,40,29000,39500,153.80690200519626 +292,10,40,29000,40000,209.66451461551316 +293,10,40,29000,40500,265.03070792175197 +294,10,40,29000,41000,317.46079310177566 +295,10,40,29000,41500,365.59950388342645 +296,10,40,29500,38000,70.26818405688635 +297,10,40,29500,38500,87.96463718548947 +298,10,40,29500,39000,122.58188233160993 +299,10,40,29500,39500,166.2478945807132 +300,10,40,29500,40000,213.48669617414316 +301,10,40,29500,40500,260.67953961944477 +302,10,40,29500,41000,305.5877041218316 +303,10,40,29500,41500,346.95612213021155 +304,10,40,30000,38000,153.67588703371362 +305,10,40,30000,38500,164.07504103479005 +306,10,40,30000,39000,190.0800160661499 +307,10,40,30000,39500,224.61382980242837 +308,10,40,30000,40000,262.79232847382445 +309,10,40,30000,40500,301.38687703450415 +310,10,40,30000,41000,338.38536686093164 +311,10,40,30000,41500,372.6399011703545 +312,10,40,30500,38000,284.2936286531718 +313,10,40,30500,38500,288.4690608277705 +314,10,40,30500,39000,306.44667517621144 +315,10,40,30500,39500,332.20122250191986 +316,10,40,30500,40000,361.5566690083291 +317,10,40,30500,40500,391.72755224929614 +318,10,40,30500,41000,420.95317535960476 +319,10,40,30500,41500,448.2049230608669 +320,10,40,31000,38000,459.03140021766137 +321,10,40,31000,38500,458.71477027519967 +322,10,40,31000,39000,469.9910751800656 +323,10,40,31000,39500,488.05850105225426 +324,10,40,31000,40000,509.5204701455629 +325,10,40,31000,40500,532.0674969691778 +326,10,40,31000,41000,554.2088430693509 +327,10,40,31000,41500,575.0485839499048 +328,10,40,31500,38000,672.2476845983564 +329,10,40,31500,38500,669.2240508488649 +330,10,40,31500,39000,675.4956226836405 +331,10,40,31500,39500,687.447764319295 +332,10,40,31500,40000,702.4395430742891 +333,10,40,31500,40500,718.6279487347668 +334,10,40,31500,41000,734.793684592168 +335,10,40,31500,41500,750.1821072409286 +336,10,80,29000,38000,387.7617282731497 +337,10,80,29000,38500,195.33642612593002 +338,10,80,29000,39000,82.7306931465102 +339,10,80,29000,39500,35.13436471793541 +340,10,80,29000,40000,33.521138659248706 +341,10,80,29000,40500,61.47395975053128 +342,10,80,29000,41000,106.71403229340167 +343,10,80,29000,41500,160.56068704487473 +344,10,80,29500,38000,459.63404601804103 +345,10,80,29500,38500,258.7453720995899 +346,10,80,29500,39000,135.96435731320256 +347,10,80,29500,39500,80.2685095017944 +348,10,80,29500,40000,70.86302366453106 +349,10,80,29500,40500,90.43203026480438 +350,10,80,29500,41000,126.7844695901737 +351,10,80,29500,41500,171.63682876805044 +352,10,80,30000,38000,564.1463320344325 +353,10,80,30000,38500,360.75718124523866 +354,10,80,30000,39000,231.70119191254307 +355,10,80,30000,39500,170.74752201483128 +356,10,80,30000,40000,154.7149036950422 +357,10,80,30000,40500,166.10596450541493 +358,10,80,30000,41000,193.3351721194443 +359,10,80,30000,41500,228.78394172417038 +360,10,80,30500,38000,689.6797223218513 +361,10,80,30500,38500,484.8023695265838 +362,10,80,30500,39000,363.5979340028588 +363,10,80,30500,39500,304.67857102688225 +364,10,80,30500,40000,285.29210000833734 +365,10,80,30500,40500,290.0135917456113 +366,10,80,30500,41000,308.8672169492536 +367,10,80,30500,41500,335.3210332569182 +368,10,80,31000,38000,789.946106942773 +369,10,80,31000,38500,625.7722360026959 +370,10,80,31000,39000,528.6063264942235 +371,10,80,31000,39500,478.6863763478618 +372,10,80,31000,40000,459.5026243189753 +373,10,80,31000,40500,459.6982093164963 +374,10,80,31000,41000,471.6790024321937 +375,10,80,31000,41500,490.3034492109124 +376,10,80,31500,38000,912.3540488244158 +377,10,80,31500,38500,798.2135101409633 +378,10,80,31500,39000,727.746684419146 +379,10,80,31500,39500,689.0119464356724 +380,10,80,31500,40000,672.0757202772029 +381,10,80,31500,40500,669.678339553036 +382,10,80,31500,41000,676.5761221409929 +383,10,80,31500,41500,688.9934449650118 +384,10,120,29000,38000,1155.1165164624408 +385,10,120,29000,38500,840.2641727088946 +386,10,120,29000,39000,506.9102636732852 +387,10,120,29000,39500,265.5278912452038 +388,10,120,29000,40000,116.39516513179322 +389,10,120,29000,40500,45.2088092745619 +390,10,120,29000,41000,30.22267557153353 +391,10,120,29000,41500,51.06063746392809 +392,10,120,29500,38000,1343.7868459826054 +393,10,120,29500,38500,977.9852373227346 +394,10,120,29500,39000,594.632756549817 +395,10,120,29500,39500,346.2478773329187 +396,10,120,29500,40000,180.23082247413407 +397,10,120,29500,40500,95.81649989178923 +398,10,120,29500,41000,71.0837801649128 +399,10,120,29500,41500,82.84289818279714 +400,10,120,30000,38000,1532.9333545384934 +401,10,120,30000,38500,1012.2223350568845 +402,10,120,30000,39000,688.4884716222766 +403,10,120,30000,39500,464.6206903113392 +404,10,120,30000,40000,283.5644748300334 +405,10,120,30000,40500,190.27593217865416 +406,10,120,30000,41000,158.0192279691727 +407,10,120,30000,41500,161.3611926772337 +408,10,120,30500,38000,1349.3785399811063 +409,10,120,30500,38500,1014.785480110738 +410,10,120,30500,39000,843.0316833766408 +411,10,120,30500,39500,589.4543896730125 +412,10,120,30500,40000,412.3358512291996 +413,10,120,30500,40500,324.11715620464133 +414,10,120,30500,41000,290.17588242984766 +415,10,120,30500,41500,287.56857384673356 +416,10,120,31000,38000,1328.0973931040146 +417,10,120,31000,38500,1216.5659656437845 +418,10,120,31000,39000,928.4831767181619 +419,10,120,31000,39500,700.3115484040329 +420,10,120,31000,40000,565.0876352458171 +421,10,120,31000,40500,494.44016026435037 +422,10,120,31000,41000,464.38005437182983 +423,10,120,31000,41500,458.7614573733091 +424,10,120,31500,38000,1473.1154650008834 +425,10,120,31500,38500,1195.943614951571 +426,10,120,31500,39000,990.2486604382486 +427,10,120,31500,39500,843.1390407497395 +428,10,120,31500,40000,751.2746391170706 +429,10,120,31500,40500,700.215375503209 +430,10,120,31500,41000,676.1585052687219 +431,10,120,31500,41500,669.5907920932743 +432,13,40,29000,38000,49.96352152045025 +433,13,40,29000,38500,83.75104994958261 +434,13,40,29000,39000,136.8176091795391 +435,13,40,29000,39500,199.91486685466407 +436,13,40,29000,40000,266.4367154860076 +437,13,40,29000,40500,331.97224579940524 +438,13,40,29000,41000,393.8001583706036 +439,13,40,29000,41500,450.42425363084493 +440,13,40,29500,38000,29.775721038786923 +441,13,40,29500,38500,57.37673742631121 +442,13,40,29500,39000,103.49161398239501 +443,13,40,29500,39500,159.3058253852367 +444,13,40,29500,40000,218.60083223764073 +445,13,40,29500,40500,277.2507278183831 +446,13,40,29500,41000,332.7141278886951 +447,13,40,29500,41500,383.58832292300576 +448,13,40,30000,38000,47.72263852005472 +449,13,40,30000,38500,68.07581028940402 +450,13,40,30000,39000,106.13974628945516 +451,13,40,30000,39500,153.58449949683063 +452,13,40,30000,40000,204.62393623358633 +453,13,40,30000,40500,255.44513025602419 +454,13,40,30000,41000,303.69954914051766 +455,13,40,30000,41500,348.0803709720354 +456,13,40,30500,38000,110.9331168284094 +457,13,40,30500,38500,123.63361262704746 +458,13,40,30500,39000,153.02654433825705 +459,13,40,30500,39500,191.40769947472756 +460,13,40,30500,40000,233.503841403055 +461,13,40,30500,40500,275.8557790922913 +462,13,40,30500,41000,316.32529882763697 +463,13,40,30500,41500,353.7060432094809 +464,13,40,31000,38000,221.90608823073939 +465,13,40,31000,38500,227.67026441593657 +466,13,40,31000,39000,248.62107049869064 +467,13,40,31000,39500,277.9507605389158 +468,13,40,31000,40000,311.0267471957685 +469,13,40,31000,40500,344.8024031161673 +470,13,40,31000,41000,377.3761144228052 +471,13,40,31000,41500,407.6529635071056 +472,13,40,31500,38000,378.8738382757093 +473,13,40,31500,38500,379.39748335944216 +474,13,40,31500,39000,393.01223361732553 +475,13,40,31500,39500,414.10238059122855 +476,13,40,31500,40000,438.8024282436204 +477,13,40,31500,40500,464.5348067190265 +478,13,40,31500,41000,489.6621039898805 +479,13,40,31500,41500,513.2163939332803 +480,13,80,29000,38000,364.387588581215 +481,13,80,29000,38500,184.2902007673634 +482,13,80,29000,39000,81.57192155036655 +483,13,80,29000,39500,42.54811210095659 +484,13,80,29000,40000,49.897338772663076 +485,13,80,29000,40500,87.84229516509882 +486,13,80,29000,41000,143.85451969447664 +487,13,80,29000,41500,208.71467984917848 +488,13,80,29500,38000,382.5794635435733 +489,13,80,29500,38500,188.38619353711718 +490,13,80,29500,39000,75.75749359688277 +491,13,80,29500,39500,29.27891251986562 +492,13,80,29500,40000,29.794874961934568 +493,13,80,29500,40500,60.654888662698205 +494,13,80,29500,41000,109.25801388824325 +495,13,80,29500,41500,166.6311093454692 +496,13,80,30000,38000,448.97795526074816 +497,13,80,30000,38500,238.44530107604737 +498,13,80,30000,39000,112.34545890264337 +499,13,80,30000,39500,56.125871791222835 +500,13,80,30000,40000,48.29987461781518 +501,13,80,30000,40500,70.7900626637678 +502,13,80,30000,41000,110.76865376691964 +503,13,80,30000,41500,159.50197316936024 +504,13,80,30500,38000,547.7818730461195 +505,13,80,30500,38500,332.92604070423494 +506,13,80,30500,39000,193.80760050280742 +507,13,80,30500,39500,128.3457644087917 +508,13,80,30500,40000,112.23915895822442 +509,13,80,30500,40500,125.96369396512564 +510,13,80,30500,41000,156.67918617660013 +511,13,80,30500,41500,196.05195109523765 +512,13,80,31000,38000,682.8591931963246 +513,13,80,31000,38500,457.56562267948556 +514,13,80,31000,39000,313.6380169123524 +515,13,80,31000,39500,245.13531819580908 +516,13,80,31000,40000,223.54473391202873 +517,13,80,31000,40500,229.60752111202834 +518,13,80,31000,41000,251.42377424735136 +519,13,80,31000,41500,281.48720903016886 +520,13,80,31500,38000,807.925638050234 +521,13,80,31500,38500,588.686585641994 +522,13,80,31500,39000,464.0488586698228 +523,13,80,31500,39500,402.69214492641095 +524,13,80,31500,40000,380.13626165363934 +525,13,80,31500,40500,380.8064948609387 +526,13,80,31500,41000,395.05186915919086 +527,13,80,31500,41500,416.70193045600774 +528,13,120,29000,38000,1068.8279454397398 +529,13,120,29000,38500,743.0012805963486 +530,13,120,29000,39000,451.2538301167544 +531,13,120,29000,39500,235.4154251166075 +532,13,120,29000,40000,104.73720814447498 +533,13,120,29000,40500,46.91983990671749 +534,13,120,29000,41000,42.81092192562316 +535,13,120,29000,41500,74.33530639171506 +536,13,120,29500,38000,1133.1178848710972 +537,13,120,29500,38500,824.0745323788527 +538,13,120,29500,39000,499.10867111401996 +539,13,120,29500,39500,256.1626809904186 +540,13,120,29500,40000,107.68599585294751 +541,13,120,29500,40500,38.18533662516749 +542,13,120,29500,41000,25.499608203619154 +543,13,120,29500,41500,49.283537699300375 +544,13,120,30000,38000,1292.409871290162 +545,13,120,30000,38500,994.669572829704 +546,13,120,30000,39000,598.9783697712826 +547,13,120,30000,39500,327.47348408537925 +548,13,120,30000,40000,156.82634841081907 +549,13,120,30000,40500,71.30833688875883 +550,13,120,30000,41000,47.72389750130817 +551,13,120,30000,41500,62.1982461882982 +552,13,120,30500,38000,1585.8797221278146 +553,13,120,30500,38500,1144.66688416451 +554,13,120,30500,39000,692.6651441690645 +555,13,120,30500,39500,441.98837639874046 +556,13,120,30500,40000,251.56311435857728 +557,13,120,30500,40500,149.79670413140468 +558,13,120,30500,41000,115.52645596043719 +559,13,120,30500,41500,120.44019473389324 +560,13,120,31000,38000,1702.7625866892163 +561,13,120,31000,38500,1071.7854750250656 +562,13,120,31000,39000,807.8943299034604 +563,13,120,31000,39500,588.672223513561 +564,13,120,31000,40000,376.44658358671404 +565,13,120,31000,40500,269.2159719426485 +566,13,120,31000,41000,229.41660529009877 +567,13,120,31000,41500,226.78274707181976 +568,13,120,31500,38000,1331.3523701291767 +569,13,120,31500,38500,1151.2055268669133 +570,13,120,31500,39000,1006.811285091974 +571,13,120,31500,39500,702.0053094629535 +572,13,120,31500,40000,515.9081891614829 +573,13,120,31500,40500,423.8652275555525 +574,13,120,31500,41000,386.4939696097151 +575,13,120,31500,41500,379.8118453367429 +576,16,40,29000,38000,106.1025746852808 +577,16,40,29000,38500,145.32590128581407 +578,16,40,29000,39000,204.74804378224422 +579,16,40,29000,39500,274.6339266648551 +580,16,40,29000,40000,347.9667393938497 +581,16,40,29000,40500,420.03753452490974 +582,16,40,29000,41000,487.9353932879741 +583,16,40,29000,41500,550.0623063219693 +584,16,40,29500,38000,54.65040870471303 +585,16,40,29500,38500,88.94089091627293 +586,16,40,29500,39000,142.72223808288405 +587,16,40,29500,39500,206.63598763907422 +588,16,40,29500,40000,273.99851593521134 +589,16,40,29500,40500,340.34861536649436 +590,16,40,29500,41000,402.935270882596 +591,16,40,29500,41500,460.2471155081633 +592,16,40,30000,38000,29.788548081995298 +593,16,40,30000,38500,57.96323252610644 +594,16,40,30000,39000,104.92815906834525 +595,16,40,30000,39500,161.71867032726158 +596,16,40,30000,40000,222.01677586338877 +597,16,40,30000,40500,281.6349465235367 +598,16,40,30000,41000,337.99683241119567 +599,16,40,30000,41500,389.68271710858414 +600,16,40,30500,38000,42.06569536892785 +601,16,40,30500,38500,62.95145274276575 +602,16,40,30500,39000,101.93860830594608 +603,16,40,30500,39500,150.47910837525734 +604,16,40,30500,40000,202.65388851823258 +605,16,40,30500,40500,254.5724108541227 +606,16,40,30500,41000,303.84403622726694 +607,16,40,30500,41500,349.1422884543064 +608,16,40,31000,38000,99.21707896667829 +609,16,40,31000,38500,112.24153596941301 +610,16,40,31000,39000,142.5186177618655 +611,16,40,31000,39500,182.02836955332134 +612,16,40,31000,40000,225.3201896575212 +613,16,40,31000,40500,268.83705389232614 +614,16,40,31000,41000,310.3895932135811 +615,16,40,31000,41500,348.7480165565453 +616,16,40,31500,38000,204.30418825821732 +617,16,40,31500,38500,210.0759235359138 +618,16,40,31500,39000,231.7643258544752 +619,16,40,31500,39500,262.1512494310348 +620,16,40,31500,40000,296.3864127264238 +621,16,40,31500,40500,331.30743171999035 +622,16,40,31500,41000,364.95322314895554 +623,16,40,31500,41500,396.20142191205844 +624,16,80,29000,38000,399.5975649320935 +625,16,80,29000,38500,225.6318269911425 +626,16,80,29000,39000,127.97354075513151 +627,16,80,29000,39500,93.73584101549991 +628,16,80,29000,40000,106.43084032022394 +629,16,80,29000,40500,150.51245762256931 +630,16,80,29000,41000,213.24213500046466 +631,16,80,29000,41500,285.0426423013882 +632,16,80,29500,38000,371.37706087096393 +633,16,80,29500,38500,189.77150413822454 +634,16,80,29500,39000,86.22375488959844 +635,16,80,29500,39500,46.98714814001572 +636,16,80,29500,40000,54.596900621760675 +637,16,80,29500,40500,93.12033833747024 +638,16,80,29500,41000,149.89341227947025 +639,16,80,29500,41500,215.5937000584367 +640,16,80,30000,38000,388.43657991253195 +641,16,80,30000,38500,190.77121362008674 +642,16,80,30000,39000,76.28535232335287 +643,16,80,30000,39500,29.152860363695716 +644,16,80,30000,40000,29.820972887404942 +645,16,80,30000,40500,61.320203047752464 +646,16,80,30000,41000,110.82086782062603 +647,16,80,30000,41500,169.197767615573 +648,16,80,30500,38000,458.8964339917103 +649,16,80,30500,38500,239.547928886725 +650,16,80,30500,39000,109.02338779317503 +651,16,80,30500,39500,50.888746196140914 +652,16,80,30500,40000,42.73606982375976 +653,16,80,30500,40500,65.75935122724029 +654,16,80,30500,41000,106.68884313872147 +655,16,80,30500,41500,156.54100549486617 +656,16,80,31000,38000,561.7385153195615 +657,16,80,31000,38500,335.5692026144635 +658,16,80,31000,39000,188.0383015831574 +659,16,80,31000,39500,118.2318539104416 +660,16,80,31000,40000,100.81000168801492 +661,16,80,31000,40500,114.72014539486217 +662,16,80,31000,41000,146.2992492326178 +663,16,80,31000,41500,186.8074429488408 +664,16,80,31500,38000,697.9937997454152 +665,16,80,31500,38500,466.42234442578484 +666,16,80,31500,39000,306.52125608515166 +667,16,80,31500,39500,230.54692639209762 +668,16,80,31500,40000,206.461121102699 +669,16,80,31500,40500,212.23429887269359 +670,16,80,31500,41000,234.70913795495554 +671,16,80,31500,41500,265.8143069252357 +672,16,120,29000,38000,1085.688903883652 +673,16,120,29000,38500,750.2887000017752 +674,16,120,29000,39000,469.92662852990964 +675,16,120,29000,39500,267.1560282754928 +676,16,120,29000,40000,146.06299930062625 +677,16,120,29000,40500,95.28836772053619 +678,16,120,29000,41000,97.41466545178946 +679,16,120,29000,41500,135.3804131941845 +680,16,120,29500,38000,1079.5576154477903 +681,16,120,29500,38500,751.2932384998761 +682,16,120,29500,39000,458.27083477307207 +683,16,120,29500,39500,240.9658024131812 +684,16,120,29500,40000,109.3801465044384 +685,16,120,29500,40500,51.274139057659724 +686,16,120,29500,41000,47.36446629605638 +687,16,120,29500,41500,79.42944320845996 +688,16,120,30000,38000,1139.3792936518537 +689,16,120,30000,38500,833.7979589668842 +690,16,120,30000,39000,507.805443202025 +691,16,120,30000,39500,259.93892964607977 +692,16,120,30000,40000,108.7341499557062 +693,16,120,30000,40500,38.152937143498605 +694,16,120,30000,41000,25.403985123518716 +695,16,120,30000,41500,49.72822589160786 +696,16,120,30500,38000,1285.0396277304772 +697,16,120,30500,38500,1025.254169031627 +698,16,120,30500,39000,622.5890550779666 +699,16,120,30500,39500,333.3353043756717 +700,16,120,30500,40000,155.70268128051293 +701,16,120,30500,40500,66.84125446522368 +702,16,120,30500,41000,42.25187049753978 +703,16,120,30500,41500,56.98314898830595 +704,16,120,31000,38000,1595.7993459811262 +705,16,120,31000,38500,1252.8886556470425 +706,16,120,31000,39000,731.4408383874198 +707,16,120,31000,39500,451.0090473423308 +708,16,120,31000,40000,251.5086563526081 +709,16,120,31000,40500,141.8915050063955 +710,16,120,31000,41000,104.67474675582574 +711,16,120,31000,41500,109.1609567535697 +712,16,120,31500,38000,1942.3896021770768 +713,16,120,31500,38500,1197.207050908449 +714,16,120,31500,39000,812.6818768064074 +715,16,120,31500,39500,611.45532452889 +716,16,120,31500,40000,380.63642711770643 +717,16,120,31500,40500,258.5514125337487 +718,16,120,31500,41000,213.48518421250665 +719,16,120,31500,41500,209.58134396574906 +720,19,40,29000,38000,169.3907733115706 +721,19,40,29000,38500,212.23331960093145 +722,19,40,29000,39000,275.9376503672959 +723,19,40,29000,39500,350.4301397081139 +724,19,40,29000,40000,428.40863665493924 +725,19,40,29000,40500,504.955113902399 +726,19,40,29000,41000,577.023450987656 +727,19,40,29000,41500,642.9410032211753 +728,19,40,29500,38000,102.40889356493292 +729,19,40,29500,38500,141.19036226103668 +730,19,40,29500,39000,200.19333708701748 +731,19,40,29500,39500,269.6750686488757 +732,19,40,29500,40000,342.6217886299377 +733,19,40,29500,40500,414.33044375626207 +734,19,40,29500,41000,481.89521316730713 +735,19,40,29500,41500,543.7211700546151 +736,19,40,30000,38000,51.95330426445395 +737,19,40,30000,38500,85.69656829127965 +738,19,40,30000,39000,138.98376466247876 +739,19,40,30000,39500,202.43251598105033 +740,19,40,30000,40000,269.3557903452929 +741,19,40,30000,40500,335.2960133312316 +742,19,40,30000,41000,397.50658847538665 +743,19,40,30000,41500,454.47903112410967 +744,19,40,30500,38000,28.864802790801026 +745,19,40,30500,38500,56.32899754732796 +746,19,40,30500,39000,102.69825523352162 +747,19,40,30500,39500,158.95118263535466 +748,19,40,30500,40000,218.75241957992617 +749,19,40,30500,40500,277.9122290233915 +750,19,40,30500,41000,333.8561815041273 +751,19,40,30500,41500,385.1662652901447 +752,19,40,31000,38000,43.72359701781447 +753,19,40,31000,38500,63.683967347844224 +754,19,40,31000,39000,101.95579433282329 +755,19,40,31000,39500,149.8826019475827 +756,19,40,31000,40000,201.50605279789198 +757,19,40,31000,40500,252.92391570754876 +758,19,40,31000,41000,301.7431453727685 +759,19,40,31000,41500,346.6368192781496 +760,19,40,31500,38000,104.05710998615942 +761,19,40,31500,38500,115.95783594434451 +762,19,40,31500,39000,145.42181873662554 +763,19,40,31500,39500,184.26373455825217 +764,19,40,31500,40000,226.97066340897095 +765,19,40,31500,40500,269.96403356902357 +766,19,40,31500,41000,311.04753558871505 +767,19,40,31500,41500,348.98866332680115 +768,19,80,29000,38000,453.1314944429312 +769,19,80,29000,38500,281.24067760117225 +770,19,80,29000,39000,185.83730378881882 +771,19,80,29000,39500,154.25726305915472 +772,19,80,29000,40000,170.2912737797755 +773,19,80,29000,40500,218.38979299191152 +774,19,80,29000,41000,285.604024444273 +775,19,80,29000,41500,362.0858325427657 +776,19,80,29500,38000,400.06299682217264 +777,19,80,29500,38500,224.41725666435008 +778,19,80,29500,39000,125.58476107530382 +779,19,80,29500,39500,90.55733834394478 +780,19,80,29500,40000,102.67519971027264 +781,19,80,29500,40500,146.27807815967392 +782,19,80,29500,41000,208.57372904155937 +783,19,80,29500,41500,279.9669583078214 +784,19,80,30000,38000,376.1594584816549 +785,19,80,30000,38500,191.30452808298463 +786,19,80,30000,39000,85.63116084217559 +787,19,80,30000,39500,45.10487847849711 +788,19,80,30000,40000,51.88389644342952 +789,19,80,30000,40500,89.78942817703852 +790,19,80,30000,41000,146.0393555385696 +791,19,80,30000,41500,211.26567367707352 +792,19,80,30500,38000,401.874315275947 +793,19,80,30500,38500,197.55305366608133 +794,19,80,30500,39000,79.00348967857379 +795,19,80,30500,39500,29.602719961568614 +796,19,80,30500,40000,28.980451378502487 +797,19,80,30500,40500,59.63541802023186 +798,19,80,30500,41000,108.48607655362268 +799,19,80,30500,41500,166.30589286399507 +800,19,80,31000,38000,484.930958445979 +801,19,80,31000,38500,254.27552635537404 +802,19,80,31000,39000,116.75543721560439 +803,19,80,31000,39500,54.77547840250418 +804,19,80,31000,40000,44.637472658824976 +805,19,80,31000,40500,66.50466903927668 +806,19,80,31000,41000,106.62737262508298 +807,19,80,31000,41500,155.8310688191254 +808,19,80,31500,38000,595.6094306603337 +809,19,80,31500,38500,359.60040819463063 +810,19,80,31500,39000,201.85328967228585 +811,19,80,31500,39500,126.24442464793601 +812,19,80,31500,40000,106.07388975142673 +813,19,80,31500,40500,118.52358345403363 +814,19,80,31500,41000,149.1597537162607 +815,19,80,31500,41500,188.94964975523197 +816,19,120,29000,38000,1133.9213841599772 +817,19,120,29000,38500,793.9759807804692 +818,19,120,29000,39000,516.5580425563733 +819,19,120,29000,39500,318.60172051726147 +820,19,120,29000,40000,201.662212274693 +821,19,120,29000,40500,154.47522945829064 +822,19,120,29000,41000,160.28049502033574 +823,19,120,29000,41500,202.35345983501588 +824,19,120,29500,38000,1091.6343400395158 +825,19,120,29500,38500,754.9332443184217 +826,19,120,29500,39000,472.1777992591152 +827,19,120,29500,39500,267.03951846894995 +828,19,120,29500,40000,144.25558152688114 +829,19,120,29500,40500,92.40384156679512 +830,19,120,29500,41000,93.81833253459942 +831,19,120,29500,41500,131.24753560710644 +832,19,120,30000,38000,1092.719296892266 +833,19,120,30000,38500,764.7065490850255 +834,19,120,30000,39000,467.2268758064373 +835,19,120,30000,39500,244.9367732985332 +836,19,120,30000,40000,110.00996333393202 +837,19,120,30000,40500,49.96381544207811 +838,19,120,30000,41000,44.9298739569088 +839,19,120,30000,41500,76.25447129089613 +840,19,120,30500,38000,1160.6160120981158 +841,19,120,30500,38500,865.5953188304933 +842,19,120,30500,39000,531.1657093741892 +843,19,120,30500,39500,271.98520008106277 +844,19,120,30500,40000,114.03616090967407 +845,19,120,30500,40500,39.74252227099571 +846,19,120,30500,41000,25.07176465285551 +847,19,120,30500,41500,48.298794094852724 +848,19,120,31000,38000,1304.8870694342509 +849,19,120,31000,38500,1089.6854636757826 +850,19,120,31000,39000,668.6632735260521 +851,19,120,31000,39500,356.7751012890747 +852,19,120,31000,40000,168.32491564142487 +853,19,120,31000,40500,72.82648063377391 +854,19,120,31000,41000,45.02326687759286 +855,19,120,31000,41500,58.13111530831655 +856,19,120,31500,38000,1645.2697164013964 +857,19,120,31500,38500,1373.859712069864 +858,19,120,31500,39000,787.3948673670299 +859,19,120,31500,39500,483.60546305948367 +860,19,120,31500,40000,273.4285373433001 +861,19,120,31500,40500,153.21079535396908 +862,19,120,31500,41000,111.21299419905313 +863,19,120,31500,41500,113.52006337929113 +864,22,40,29000,38000,229.2032513971666 +865,22,40,29000,38500,274.65023153674116 +866,22,40,29000,39000,341.4424739822062 +867,22,40,29000,39500,419.2624324130753 +868,22,40,29000,40000,500.6022690006133 +869,22,40,29000,40500,580.3923016374031 +870,22,40,29000,41000,655.4874207991389 +871,22,40,29000,41500,724.1595537770351 +872,22,40,29500,38000,155.45206306046595 +873,22,40,29500,38500,197.41588482427002 +874,22,40,29500,39000,260.1641484982308 +875,22,40,29500,39500,333.666918810689 +876,22,40,29500,40000,410.66541588422854 +877,22,40,29500,40500,486.276072112155 +878,22,40,29500,41000,557.4760464927683 +879,22,40,29500,41500,622.6057687448293 +880,22,40,30000,38000,90.70026588811803 +881,22,40,30000,38500,128.41239603755494 +882,22,40,30000,39000,186.27261386900233 +883,22,40,30000,39500,254.5802373859711 +884,22,40,30000,40000,326.3686182341553 +885,22,40,30000,40500,396.9735001502319 +886,22,40,30000,41000,463.5155278718613 +887,22,40,30000,41500,524.414569320113 +888,22,40,30500,38000,44.551475763397946 +889,22,40,30500,38500,76.95264448905411 +890,22,40,30500,39000,128.85898727872572 +891,22,40,30500,39500,190.91422001003792 +892,22,40,30500,40000,256.4755613806196 +893,22,40,30500,40500,321.125224208803 +894,22,40,30500,41000,382.14434919800453 +895,22,40,30500,41500,438.03974322333033 +896,22,40,31000,38000,28.101321546315717 +897,22,40,31000,38500,53.867829756398805 +898,22,40,31000,39000,98.57619184859544 +899,22,40,31000,39500,153.19473192134507 +900,22,40,31000,40000,211.4202434313414 +901,22,40,31000,40500,269.09905982026265 +902,22,40,31000,41000,323.68306330754416 +903,22,40,31000,41500,373.76836451736045 +904,22,40,31500,38000,51.648288279447364 +905,22,40,31500,38500,69.56074881661863 +906,22,40,31500,39000,105.91402675097291 +907,22,40,31500,39500,151.99456204656389 +908,22,40,31500,40000,201.85995274525234 +909,22,40,31500,40500,251.63807959916412 +910,22,40,31500,41000,298.9593498669657 +911,22,40,31500,41500,342.50888994628025 +912,22,80,29000,38000,507.5440336860194 +913,22,80,29000,38500,336.42019672232965 +914,22,80,29000,39000,242.21016116765423 +915,22,80,29000,39500,212.33396533224905 +916,22,80,29000,40000,230.67632355958136 +917,22,80,29000,40500,281.6224662955561 +918,22,80,29000,41000,352.0457411487133 +919,22,80,29000,41500,431.89288175778637 +920,22,80,29500,38000,443.2889283037078 +921,22,80,29500,38500,270.0648237630224 +922,22,80,29500,39000,173.57666711629645 +923,22,80,29500,39500,141.06258420240613 +924,22,80,29500,40000,156.18412870159142 +925,22,80,29500,40500,203.33105261575707 +926,22,80,29500,41000,269.5552387411201 +927,22,80,29500,41500,345.03801326123767 +928,22,80,30000,38000,395.34177505602497 +929,22,80,30000,38500,217.11094192826982 +930,22,80,30000,39000,116.38535634181476 +931,22,80,30000,39500,79.94742924888467 +932,22,80,30000,40000,90.84706550421288 +933,22,80,30000,40500,133.26308067939766 +934,22,80,30000,41000,194.36064414396228 +935,22,80,30000,41500,264.56059537656466 +936,22,80,30500,38000,382.0341866812038 +937,22,80,30500,38500,191.65621311671836 +938,22,80,30500,39000,82.3318677587146 +939,22,80,30500,39500,39.44606931321677 +940,22,80,30500,40000,44.476166488763134 +941,22,80,30500,40500,80.84561981845566 +942,22,80,30500,41000,135.62459431793735 +943,22,80,30500,41500,199.42208168600175 +944,22,80,31000,38000,425.5181957619983 +945,22,80,31000,38500,210.2667219741389 +946,22,80,31000,39000,84.97041062888985 +947,22,80,31000,39500,31.593073529038755 +948,22,80,31000,40000,28.407154164211214 +949,22,80,31000,40500,57.05446633976857 +950,22,80,31000,41000,104.10423883907688 +951,22,80,31000,41500,160.23135976433713 +952,22,80,31500,38000,527.5015417150911 +953,22,80,31500,38500,282.29650611769665 +954,22,80,31500,39000,134.62881845323489 +955,22,80,31500,39500,66.62736532046851 +956,22,80,31500,40000,52.9918858786988 +957,22,80,31500,40500,72.36913743145999 +958,22,80,31500,41000,110.38003828747726 +959,22,80,31500,41500,157.65470091455973 +960,22,120,29000,38000,1186.823326813257 +961,22,120,29000,38500,844.3317816964005 +962,22,120,29000,39000,567.7367986440256 +963,22,120,29000,39500,371.79782508970567 +964,22,120,29000,40000,256.9261857702517 +965,22,120,29000,40500,211.85466060592006 +966,22,120,29000,41000,220.09534855737033 +967,22,120,29000,41500,265.02731793490034 +968,22,120,29500,38000,1128.4568915685559 +969,22,120,29500,38500,787.7709648712951 +970,22,120,29500,39000,508.4832626962424 +971,22,120,29500,39500,308.52654841064975 +972,22,120,29500,40000,190.01030358402707 +973,22,120,29500,40500,141.62663282114926 +974,22,120,29500,41000,146.40704203984612 +975,22,120,29500,41500,187.48734389188584 +976,22,120,30000,38000,1094.7007205604846 +977,22,120,30000,38500,757.7313528729464 +978,22,120,30000,39000,471.282561364766 +979,22,120,30000,39500,262.0412520036699 +980,22,120,30000,40000,136.26956239282435 +981,22,120,30000,40500,82.4268827471484 +982,22,120,30000,41000,82.3695177584498 +983,22,120,30000,41500,118.51210034475737 +984,22,120,30500,38000,1111.0872182758205 +985,22,120,30500,38500,787.2204655558988 +986,22,120,30500,39000,481.85960605002055 +987,22,120,30500,39500,250.28740868446397 +988,22,120,30500,40000,109.21968920710272 +989,22,120,30500,40500,45.51600269221681 +990,22,120,30500,41000,38.172157811051115 +991,22,120,30500,41500,67.73748641348168 +992,22,120,31000,38000,1193.3958874354898 +993,22,120,31000,38500,923.0731791194576 +994,22,120,31000,39000,573.4457650536078 +995,22,120,31000,39500,294.2980811757103 +996,22,120,31000,40000,124.86249624679849 +997,22,120,31000,40500,43.948524347749846 +998,22,120,31000,41000,25.582084045731808 +999,22,120,31000,41500,46.36268252714472 +1000,22,120,31500,38000,1336.0993444856913 +1001,22,120,31500,38500,1194.893001664831 +1002,22,120,31500,39000,740.6584250286721 +1003,22,120,31500,39500,397.18127104230757 +1004,22,120,31500,40000,194.20390582893873 +1005,22,120,31500,40500,88.22588964369922 +1006,22,120,31500,41000,54.97797247760634 +1007,22,120,31500,41500,64.88195101638016 diff --git a/pyomo/contrib/parmest/examples/semibatch/parallel_example.py b/pyomo/contrib/parmest/examples/semibatch/parallel_example.py index ff1287811cf..d7cc497803e 100644 --- a/pyomo/contrib/parmest/examples/semibatch/parallel_example.py +++ b/pyomo/contrib/parmest/examples/semibatch/parallel_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -14,8 +14,7 @@ parallel and save results to files for later analysis and graphics. Example command: mpiexec -n 4 python parallel_example.py """ -import numpy as np -import pandas as pd +from pyomo.common.dependencies import numpy as np, pandas as pd from itertools import product from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest diff --git a/pyomo/contrib/parmest/examples/semibatch/parameter_estimation_example.py b/pyomo/contrib/parmest/examples/semibatch/parameter_estimation_example.py index fc4c9f5c675..7eafdd2b9c3 100644 --- a/pyomo/contrib/parmest/examples/semibatch/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/examples/semibatch/parameter_estimation_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -12,12 +12,10 @@ import json from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.semibatch.semibatch import generate_model +from pyomo.contrib.parmest.examples.semibatch.semibatch import SemiBatchExperiment def main(): - # Vars to estimate - theta_names = ['k1', 'k2', 'E1', 'E2'] # Data, list of dictionaries data = [] @@ -28,10 +26,19 @@ def main(): d = json.load(infile) data.append(d) + # Create an experiment list + exp_list = [] + for i in range(len(data)): + exp_list.append(SemiBatchExperiment(data[i])) + + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # exp0_model.pprint() + # Note, the model already includes a 'SecondStageCost' expression # for sum of squared error that will be used in parameter estimation - pest = parmest.Estimator(generate_model, data, theta_names) + pest = parmest.Estimator(exp_list) obj, theta = pest.theta_est() print(obj) diff --git a/pyomo/contrib/parmest/examples/semibatch/scenario_example.py b/pyomo/contrib/parmest/examples/semibatch/scenario_example.py index 071e53236c4..697cb9ac7a5 100644 --- a/pyomo/contrib/parmest/examples/semibatch/scenario_example.py +++ b/pyomo/contrib/parmest/examples/semibatch/scenario_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -12,13 +12,11 @@ import json from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.semibatch.semibatch import generate_model +from pyomo.contrib.parmest.examples.semibatch.semibatch import SemiBatchExperiment import pyomo.contrib.parmest.scenariocreator as sc def main(): - # Vars to estimate in parmest - theta_names = ['k1', 'k2', 'E1', 'E2'] # Data: list of dictionaries data = [] @@ -29,7 +27,16 @@ def main(): d = json.load(infile) data.append(d) - pest = parmest.Estimator(generate_model, data, theta_names) + # Create an experiment list + exp_list = [] + for i in range(len(data)): + exp_list.append(SemiBatchExperiment(data[i])) + + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # exp0_model.pprint() + + pest = parmest.Estimator(exp_list) scenmaker = sc.ScenarioCreator(pest, "ipopt") diff --git a/pyomo/contrib/parmest/examples/semibatch/semibatch.py b/pyomo/contrib/parmest/examples/semibatch/semibatch.py index 8cda262c019..b506d41d072 100644 --- a/pyomo/contrib/parmest/examples/semibatch/semibatch.py +++ b/pyomo/contrib/parmest/examples/semibatch/semibatch.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -29,16 +29,28 @@ SolverFactory, exp, minimize, + Suffix, + ComponentUID, ) from pyomo.dae import ContinuousSet, DerivativeVar +from pyomo.contrib.parmest.experiment import Experiment def generate_model(data): + # if data is a file name, then load file first + if isinstance(data, str): + file_name = data + try: + with open(file_name, "r") as infile: + data = json.load(infile) + except: + raise RuntimeError(f"Could not read {file_name} as json") + # unpack and fix the data - cameastemp = data['Ca_meas'] - cbmeastemp = data['Cb_meas'] - ccmeastemp = data['Cc_meas'] - trmeastemp = data['Tr_meas'] + cameastemp = data["Ca_meas"] + cbmeastemp = data["Cb_meas"] + ccmeastemp = data["Cc_meas"] + trmeastemp = data["Tr_meas"] cameas = {} cbmeas = {} @@ -79,9 +91,9 @@ def generate_model(data): m.Vc = Param(initialize=0.07) # m^3 m.rhow = Param(initialize=700.0) # kg/m^3 m.cpw = Param(initialize=3.1) # kJ/kg/K - m.Ca0 = Param(initialize=data['Ca0']) # kmol/m^3) - m.Cb0 = Param(initialize=data['Cb0']) # kmol/m^3) - m.Cc0 = Param(initialize=data['Cc0']) # kmol/m^3) + m.Ca0 = Param(initialize=data["Ca0"]) # kmol/m^3) + m.Cb0 = Param(initialize=data["Cb0"]) # kmol/m^3) + m.Cc0 = Param(initialize=data["Cc0"]) # kmol/m^3) m.Tr0 = Param(initialize=300.0) # K m.Vr0 = Param(initialize=1.0) # m^3 @@ -92,9 +104,9 @@ def generate_model(data): # def _initTc(m, t): if t < 10800: - return data['Tc1'] + return data["Tc1"] else: - return data['Tc2'] + return data["Tc2"] m.Tc = Param( m.time, initialize=_initTc, default=_initTc @@ -102,9 +114,9 @@ def _initTc(m, t): def _initFa(m, t): if t < 10800: - return data['Fa1'] + return data["Fa1"] else: - return data['Fa2'] + return data["Fa2"] m.Fa = Param( m.time, initialize=_initFa, default=_initFa @@ -230,7 +242,7 @@ def AllMeasurements(m): ) def MissingMeasurements(m): - if data['experiment'] == 1: + if data["experiment"] == 1: return sum( (m.Ca[t] - m.Ca_meas[t]) ** 2 + (m.Cb[t] - m.Cb_meas[t]) ** 2 @@ -238,7 +250,7 @@ def MissingMeasurements(m): + (m.Tr[t] - m.Tr_meas[t]) ** 2 for t in m.measT ) - elif data['experiment'] == 2: + elif data["experiment"] == 2: return sum((m.Tr[t] - m.Tr_meas[t]) ** 2 for t in m.measT) else: return sum( @@ -254,25 +266,54 @@ def total_cost_rule(model): m.Total_Cost_Objective = Objective(rule=total_cost_rule, sense=minimize) # Discretize model - disc = TransformationFactory('dae.collocation') + disc = TransformationFactory("dae.collocation") disc.apply_to(m, nfe=20, ncp=4) return m +class SemiBatchExperiment(Experiment): + + def __init__(self, data): + self.data = data + self.model = None + + def create_model(self): + self.model = generate_model(self.data) + + def label_model(self): + + m = self.model + + m.unknown_parameters = Suffix(direction=Suffix.LOCAL) + m.unknown_parameters.update( + (k, ComponentUID(k)) for k in [m.k1, m.k2, m.E1, m.E2] + ) + + def finalize_model(self): + pass + + def get_labeled_model(self): + self.create_model() + self.label_model() + self.finalize_model() + + return self.model + + def main(): # Data loaded from files file_dirname = dirname(abspath(str(__file__))) - file_name = abspath(join(file_dirname, 'exp2.out')) - with open(file_name, 'r') as infile: + file_name = abspath(join(file_dirname, "exp2.out")) + with open(file_name, "r") as infile: data = json.load(infile) - data['experiment'] = 2 + data["experiment"] = 2 model = generate_model(data) - solver = SolverFactory('ipopt') + solver = SolverFactory("ipopt") solver.solve(model) - print('k1 = ', model.k1()) - print('E1 = ', model.E1()) + print("k1 = ", model.k1()) + print("E1 = ", model.E1()) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/pyomo/contrib/parmest/experiment.py b/pyomo/contrib/parmest/experiment.py new file mode 100644 index 00000000000..4f797d6c89c --- /dev/null +++ b/pyomo/contrib/parmest/experiment.py @@ -0,0 +1,31 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + +class Experiment: + """ + The experiment class is a template for making experiment lists + to pass to parmest. + + An experiment is a Pyomo model "m" which is labeled + with additional suffixes: + * m.experiment_outputs which defines experiment outputs + * m.unknown_parameters which defines parameters to estimate + + The experiment class has one required method: + * get_labeled_model() which returns the labeled Pyomo model + """ + + def __init__(self, model=None): + self.model = model + + def get_labeled_model(self): + return self.model diff --git a/pyomo/contrib/parmest/graphics.py b/pyomo/contrib/parmest/graphics.py index f01622d2d17..c57bfb19696 100644 --- a/pyomo/contrib/parmest/graphics.py +++ b/pyomo/contrib/parmest/graphics.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -29,7 +29,7 @@ # (e.g. python 3.5) get released that are either broken not # compatible, resulting in a SyntaxError sns, seaborn_available = attempt_import( - 'seaborn', catch_exceptions=(ImportError, SyntaxError) + "seaborn", catch_exceptions=(ImportError, SyntaxError) ) imports_available = ( @@ -93,17 +93,17 @@ def _get_data_slice(xvar, yvar, columns, data, theta_star): temp[col] = temp[col] + data[col].std() data = pd.concat([data, temp], ignore_index=True) - data_slice['obj'] = scipy.interpolate.griddata( + data_slice["obj"] = scipy.interpolate.griddata( np.array(data[columns]), - np.array(data[['obj']]), + np.array(data[["obj"]]), np.array(data_slice[columns]), - method='linear', + method="linear", rescale=True, ) X = data_slice[xvar] Y = data_slice[yvar] - Z = data_slice['obj'] + Z = data_slice["obj"] return X, Y, Z @@ -152,7 +152,7 @@ def _add_scipy_dist_CI( data_slice.append(np.array([[theta_star[var]] * ncells] * ncells)) data_slice = np.dstack(tuple(data_slice)) - elif isinstance(dist, stats.kde.gaussian_kde): + elif isinstance(dist, stats.gaussian_kde): for var in theta_star.index: if var == xvar: data_slice.append(X.ravel()) @@ -178,11 +178,11 @@ def _add_obj_contour(x, y, color, columns, data, theta_star, label=None): X, Y, Z = _get_data_slice(xvar, yvar, columns, data, theta_star) triang = matplotlib.tri.Triangulation(X, Y) - cmap = plt.cm.get_cmap('Greys') + cmap = matplotlib.colormaps["Greys"] plt.tricontourf(triang, Z, cmap=cmap) except: - print('Objective contour plot for', xvar, yvar, 'slice failed') + print("Objective contour plot for", xvar, yvar, "slice failed") def _set_axis_limits(g, axis_limits, theta_vals, theta_star): @@ -277,7 +277,7 @@ def pairwise_plot( assert isinstance(theta_star, (type(None), dict, pd.Series, pd.DataFrame)) assert isinstance(alpha, (type(None), int, float)) assert isinstance(distributions, list) - assert set(distributions).issubset(set(['MVN', 'KDE', 'Rect'])) + assert set(distributions).issubset(set(["MVN", "KDE", "Rect"])) assert isinstance(axis_limits, (type(None), dict)) assert isinstance(title, (type(None), str)) assert isinstance(add_obj_contour, bool) @@ -307,7 +307,7 @@ def pairwise_plot( theta_names = [ col for col in theta_values.columns - if (col not in ['obj']) + if (col not in ["obj"]) and (not isinstance(col, float)) and (not isinstance(col, int)) ] @@ -335,7 +335,7 @@ def pairwise_plot( g.map_diag(sns.distplot, kde=False, hist=True, norm_hist=False) # Plot filled contours using all theta values based on obj - if 'obj' in theta_values.columns and add_obj_contour: + if "obj" in theta_values.columns and add_obj_contour: g.map_offdiag( _add_obj_contour, columns=theta_names, @@ -349,10 +349,10 @@ def pairwise_plot( matplotlib.lines.Line2D( [0], [0], - marker='o', - color='w', - label='thetas', - markerfacecolor='cadetblue', + marker="o", + color="w", + label="thetas", + markerfacecolor="cadetblue", markersize=5, ) ) @@ -360,23 +360,23 @@ def pairwise_plot( # Plot theta* if theta_star is not None: g.map_offdiag( - _add_scatter, color='k', columns=theta_names, theta_star=theta_star + _add_scatter, color="k", columns=theta_names, theta_star=theta_star ) legend_elements.append( matplotlib.lines.Line2D( [0], [0], - marker='o', - color='w', - label='theta*', - markerfacecolor='k', + marker="o", + color="w", + label="theta*", + markerfacecolor="k", markersize=6, ) ) # Plot confidence regions - colors = ['r', 'mediumblue', 'darkgray'] + colors = ["r", "mediumblue", "darkgray"] if (alpha is not None) and (len(distributions) > 0): if theta_star is None: print( @@ -388,7 +388,7 @@ def pairwise_plot( mvn_dist = None kde_dist = None for i, dist in enumerate(distributions): - if dist == 'Rect': + if dist == "Rect": lb, ub = fit_rect_dist(thetas, alpha) g.map_offdiag( _add_rectangle_CI, @@ -401,7 +401,7 @@ def pairwise_plot( matplotlib.lines.Line2D([0], [0], color=colors[i], lw=1, label=dist) ) - elif dist == 'MVN': + elif dist == "MVN": mvn_dist = fit_mvn_dist(thetas) Z = mvn_dist.pdf(thetas) score = stats.scoreatpercentile(Z, (1 - alpha) * 100) @@ -418,7 +418,7 @@ def pairwise_plot( matplotlib.lines.Line2D([0], [0], color=colors[i], lw=1, label=dist) ) - elif dist == 'KDE': + elif dist == "KDE": kde_dist = fit_kde_dist(thetas) Z = kde_dist.pdf(thetas.transpose()) score = stats.scoreatpercentile(Z, (1 - alpha) * 100) @@ -438,12 +438,12 @@ def pairwise_plot( _set_axis_limits(g, axis_limits, thetas, theta_star) for ax in g.axes.flatten(): - ax.ticklabel_format(style='sci', scilimits=(-2, 2), axis='both') + ax.ticklabel_format(style="sci", scilimits=(-2, 2), axis="both") if add_legend: xvar, yvar, loc = _get_variables(ax, theta_names) if loc == (len(theta_names) - 1, 0): - ax.legend(handles=legend_elements, loc='best', prop={'size': 8}) + ax.legend(handles=legend_elements, loc="best", prop={"size": 8}) if title: g.fig.subplots_adjust(top=0.9) g.fig.suptitle(title) @@ -474,7 +474,7 @@ def pairwise_plot( ax.tick_params(reset=True) if add_legend: - ax.legend(handles=legend_elements, loc='best', prop={'size': 8}) + ax.legend(handles=legend_elements, loc="best", prop={"size": 8}) plt.close(g.fig) @@ -563,15 +563,15 @@ def _get_grouped_data(data1, data2, normalize, group_names): # Combine data1 and data2 to create a grouped histogram data = pd.concat({group_names[0]: data1, group_names[1]: data2}) data.reset_index(level=0, inplace=True) - data.rename(columns={'level_0': 'set'}, inplace=True) + data.rename(columns={"level_0": "set"}, inplace=True) - data = data.melt(id_vars='set', value_vars=data1.columns, var_name='columns') + data = data.melt(id_vars="set", value_vars=data1.columns, var_name="columns") return data def grouped_boxplot( - data1, data2, normalize=False, group_names=['data1', 'data2'], filename=None + data1, data2, normalize=False, group_names=["data1", "data2"], filename=None ): """ Plot a grouped boxplot to compare two datasets @@ -600,11 +600,11 @@ def grouped_boxplot( data = _get_grouped_data(data1, data2, normalize, group_names) plt.figure() - sns.boxplot(data=data, hue='set', y='value', x='columns', order=data1.columns) + sns.boxplot(data=data, hue="set", y="value", x="columns", order=data1.columns) - plt.gca().legend().set_title('') - plt.gca().set_xlabel('') - plt.gca().set_ylabel('') + plt.gca().legend().set_title("") + plt.gca().set_xlabel("") + plt.gca().set_ylabel("") if filename is None: plt.show() @@ -614,7 +614,7 @@ def grouped_boxplot( def grouped_violinplot( - data1, data2, normalize=False, group_names=['data1', 'data2'], filename=None + data1, data2, normalize=False, group_names=["data1", "data2"], filename=None ): """ Plot a grouped violinplot to compare two datasets @@ -644,12 +644,12 @@ def grouped_violinplot( plt.figure() sns.violinplot( - data=data, hue='set', y='value', x='columns', order=data1.columns, split=True + data=data, hue="set", y="value", x="columns", order=data1.columns, split=True ) - plt.gca().legend().set_title('') - plt.gca().set_xlabel('') - plt.gca().set_ylabel('') + plt.gca().legend().set_title("") + plt.gca().set_xlabel("") + plt.gca().set_ylabel("") if filename is None: plt.show() diff --git a/pyomo/contrib/parmest/ipopt_solver_wrapper.py b/pyomo/contrib/parmest/ipopt_solver_wrapper.py index a6d5e0506fb..75c470a4b81 100644 --- a/pyomo/contrib/parmest/ipopt_solver_wrapper.py +++ b/pyomo/contrib/parmest/ipopt_solver_wrapper.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index cbdc9179f35..41e7792570b 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -11,9 +11,9 @@ #### Using mpi-sppy instead of PySP; May 2020 #### Adding option for "local" EF starting Sept 2020 #### Wrapping mpi-sppy functionality and local option Jan 2021, Feb 2021 +#### Redesign with Experiment class Dec 2023 # TODO: move use_mpisppy to a Pyomo configuration option -# # False implies always use the EF that is local to parmest use_mpisppy = True # Use it if we can but use local if not. if use_mpisppy: @@ -42,7 +42,9 @@ import logging import types import json +from collections.abc import Callable from itertools import combinations +from functools import singledispatchmethod from pyomo.common.dependencies import ( attempt_import, @@ -63,6 +65,9 @@ import pyomo.contrib.parmest.graphics as graphics from pyomo.dae import ContinuousSet +from pyomo.common.deprecation import deprecated +from pyomo.common.deprecation import deprecation_warning + parmest_available = numpy_available & pandas_available & scipy_available inverse_reduced_hessian, inverse_reduced_hessian_available = attempt_import( @@ -209,12 +214,12 @@ def _experiment_instance_creation_callback( thetavals = outer_cb_data["ThetaVals"] # dlw august 2018: see mea code for more general theta - for vstr in thetavals: - theta_cuid = ComponentUID(vstr) + for name, val in thetavals.items(): + theta_cuid = ComponentUID(name) theta_object = theta_cuid.find_component_on(instance) - if thetavals[vstr] is not None: + if val is not None: # print("Fixing",vstr,"at",str(thetavals[vstr])) - theta_object.fix(thetavals[vstr]) + theta_object.fix(val) else: # print("Freeing",vstr) theta_object.unfix() @@ -222,90 +227,12 @@ def _experiment_instance_creation_callback( return instance -# ============================================= -def _treemaker(scenlist): - """ - Makes a scenario tree (avoids dependence on daps) - - Parameters - ---------- - scenlist (list of `int`): experiment (i.e. scenario) numbers - - Returns - ------- - a `ConcreteModel` that is the scenario tree - """ - - num_scenarios = len(scenlist) - m = scenario_tree.tree_structure_model.CreateAbstractScenarioTreeModel() - m = m.create_instance() - m.Stages.add('Stage1') - m.Stages.add('Stage2') - m.Nodes.add('RootNode') - for i in scenlist: - m.Nodes.add('LeafNode_Experiment' + str(i)) - m.Scenarios.add('Experiment' + str(i)) - m.NodeStage['RootNode'] = 'Stage1' - m.ConditionalProbability['RootNode'] = 1.0 - for node in m.Nodes: - if node != 'RootNode': - m.NodeStage[node] = 'Stage2' - m.Children['RootNode'].add(node) - m.Children[node].clear() - m.ConditionalProbability[node] = 1.0 / num_scenarios - m.ScenarioLeafNode[node.replace('LeafNode_', '')] = node - - return m - - -def group_data(data, groupby_column_name, use_mean=None): - """ - Group data by scenario - - Parameters - ---------- - data: DataFrame - Data - groupby_column_name: strings - Name of data column which contains scenario numbers - use_mean: list of column names or None, optional - Name of data columns which should be reduced to a single value per - scenario by taking the mean - - Returns - ---------- - grouped_data: list of dictionaries - Grouped data - """ - if use_mean is None: - use_mean_list = [] - else: - use_mean_list = use_mean - - grouped_data = [] - for exp_num, group in data.groupby(data[groupby_column_name]): - d = {} - for col in group.columns: - if col in use_mean_list: - d[col] = group[col].mean() - else: - d[col] = list(group[col]) - grouped_data.append(d) - - return grouped_data - - -class _SecondStageCostExpr(object): +def SSE(model): """ - Class to pass objective expression into the Pyomo model + Sum of squared error between `experiment_output` model and data values """ - - def __init__(self, ssc_function, data): - self._ssc_function = ssc_function - self._data = data - - def __call__(self, model): - return self._ssc_function(model, self._data) + expr = sum((y - y_hat) ** 2 for y, y_hat in model.experiment_outputs.items()) + return expr class Estimator(object): @@ -314,112 +241,196 @@ class Estimator(object): Parameters ---------- - model_function: function - Function that generates an instance of the Pyomo model using 'data' - as the input argument - data: pd.DataFrame, list of dictionaries, list of dataframes, or list of json file names - Data that is used to build an instance of the Pyomo model and build - the objective function - theta_names: list of strings - List of Var names to estimate - obj_function: function, optional - Function used to formulate parameter estimation objective, generally - sum of squared error between measurements and model variables. + experiment_list: list of Experiments + A list of experiment objects which creates one labeled model for + each experiment + obj_function: string or function (optional) + Built in objective (currently only "SSE") or custom function used to + formulate parameter estimation objective. If no function is specified, the model is used "as is" and should be defined with a "FirstStageCost" and "SecondStageCost" expression that are used to build an objective. + Default is None. tee: bool, optional - Indicates that ef solver output should be teed + If True, print the solver output to the screen. Default is False. diagnostic_mode: bool, optional - If True, print diagnostics from the solver + If True, print diagnostics from the solver. Default is False. solver_options: dict, optional - Provides options to the solver (also the name of an attribute) + Provides options to the solver (also the name of an attribute). + Default is None. """ + # The singledispatchmethod decorator is used here as a deprecation + # shim to be able to support the now deprecated Estimator interface + # which had a different number of arguments. When the deprecated API + # is removed this decorator and the _deprecated_init method below + # can be removed + @singledispatchmethod def __init__( self, - model_function, - data, - theta_names, + experiment_list, obj_function=None, tee=False, diagnostic_mode=False, solver_options=None, ): - self.model_function = model_function - assert isinstance( - data, (list, pd.DataFrame) - ), "Data must be a list or DataFrame" - # convert dataframe into a list of dataframes, each row = one scenario - if isinstance(data, pd.DataFrame): - self.callback_data = [ - data.loc[i, :].to_frame().transpose() for i in data.index - ] - else: - self.callback_data = data - assert isinstance( - self.callback_data[0], (dict, pd.DataFrame, str) - ), "The scenarios in data must be a dictionary, DataFrame or filename" + # check that we have a (non-empty) list of experiments + assert isinstance(experiment_list, list) + self.exp_list = experiment_list - if len(theta_names) == 0: - self.theta_names = ['parmest_dummy_var'] - else: - self.theta_names = theta_names + # check that an experiment has experiment_outputs and unknown_parameters + model = self.exp_list[0].get_labeled_model() + try: + outputs = [k.name for k, v in model.experiment_outputs.items()] + except: + RuntimeError( + 'Experiment list model does not have suffix ' + '"experiment_outputs".' + ) + try: + params = [k.name for k, v in model.unknown_parameters.items()] + except: + RuntimeError( + 'Experiment list model does not have suffix ' + '"unknown_parameters".' + ) + # populate keyword argument options self.obj_function = obj_function self.tee = tee self.diagnostic_mode = diagnostic_mode self.solver_options = solver_options + # TODO: delete this when the deprecated interface is removed + self.pest_deprecated = None + + # TODO This might not be needed here. + # We could collect the union (or intersect?) of thetas when the models are built + theta_names = [] + for experiment in self.exp_list: + model = experiment.get_labeled_model() + theta_names.extend([k.name for k, v in model.unknown_parameters.items()]) + self.estimator_theta_names = list(set(theta_names)) + self._second_stage_cost_exp = "SecondStageCost" # boolean to indicate if model is initialized using a square solve self.model_initialized = False + # The deprecated Estimator constructor + # This works by checking the type of the first argument passed to + # the class constructor. If it matches the old interface (i.e. is + # callable) then this _deprecated_init method is called and the + # deprecation warning is displayed. + @__init__.register(Callable) + def _deprecated_init( + self, + model_function, + data, + theta_names, + obj_function=None, + tee=False, + diagnostic_mode=False, + solver_options=None, + ): + + deprecation_warning( + "You're using the deprecated parmest interface (model_function, " + "data, theta_names). This interface will be removed in a future release, " + "please update to the new parmest interface using experiment lists.", + version='6.7.2', + ) + self.pest_deprecated = _DeprecatedEstimator( + model_function, + data, + theta_names, + obj_function, + tee, + diagnostic_mode, + solver_options, + ) + def _return_theta_names(self): """ Return list of fitted model parameter names """ - # if fitted model parameter names differ from theta_names created when Estimator object is created - if hasattr(self, 'theta_names_updated'): - return self.theta_names_updated + # check for deprecated inputs + if self.pest_deprecated: + + # if fitted model parameter names differ from theta_names + # created when Estimator object is created + if hasattr(self, 'theta_names_updated'): + return self.pest_deprecated.theta_names_updated + + else: + + # default theta_names, created when Estimator object is created + return self.pest_deprecated.theta_names else: - return ( - self.theta_names - ) # default theta_names, created when Estimator object is created - def _create_parmest_model(self, data): + # if fitted model parameter names differ from theta_names + # created when Estimator object is created + if hasattr(self, 'theta_names_updated'): + return self.theta_names_updated + + else: + + # default theta_names, created when Estimator object is created + return self.estimator_theta_names + + def _expand_indexed_unknowns(self, model_temp): + """ + Expand indexed variables to get full list of thetas + """ + + model_theta_list = [] + for c in model_temp.unknown_parameters.keys(): + if c.is_indexed(): + for _, ci in c.items(): + model_theta_list.append(ci.name) + else: + model_theta_list.append(c.name) + + return model_theta_list + + def _create_parmest_model(self, experiment_number): """ Modify the Pyomo model for parameter estimation """ - model = self.model_function(data) - if (len(self.theta_names) == 1) and ( - self.theta_names[0] == 'parmest_dummy_var' - ): + model = self.exp_list[experiment_number].get_labeled_model() + + if len(model.unknown_parameters) == 0: model.parmest_dummy_var = pyo.Var(initialize=1.0) # Add objective function (optional) if self.obj_function: - for obj in model.component_objects(pyo.Objective): - if obj.name in ["Total_Cost_Objective"]: + + # Check for component naming conflicts + reserved_names = [ + 'Total_Cost_Objective', + 'FirstStageCost', + 'SecondStageCost', + ] + for n in reserved_names: + if model.component(n) or hasattr(model, n): raise RuntimeError( - "Parmest will not override the existing model Objective named " - + obj.name + f"Parmest will not override the existing model component named {n}" ) + + # Deactivate any existing objective functions + for obj in model.component_objects(pyo.Objective): obj.deactivate() - for expr in model.component_data_objects(pyo.Expression): - if expr.name in ["FirstStageCost", "SecondStageCost"]: - raise RuntimeError( - "Parmest will not override the existing model Expression named " - + expr.name - ) + # TODO, this needs to be turned into an enum class of options that still support + # custom functions + if self.obj_function == 'SSE': + second_stage_rule = SSE + else: + # A custom function uses model.experiment_outputs as data + second_stage_rule = self.obj_function + model.FirstStageCost = pyo.Expression(expr=0) - model.SecondStageCost = pyo.Expression( - rule=_SecondStageCostExpr(self.obj_function, data) - ) + model.SecondStageCost = pyo.Expression(rule=second_stage_rule) def TotalCost_rule(model): return model.FirstStageCost + model.SecondStageCost @@ -429,45 +440,13 @@ def TotalCost_rule(model): ) # Convert theta Params to Vars, and unfix theta Vars - model = utils.convert_params_to_vars(model, self.theta_names) - - # Update theta names list to use CUID string representation - for i, theta in enumerate(self.theta_names): - var_cuid = ComponentUID(theta) - var_validate = var_cuid.find_component_on(model) - if var_validate is None: - logger.warning( - "theta_name[%s] (%s) was not found on the model", (i, theta) - ) - else: - try: - # If the component is not a variable, - # this will generate an exception (and the warning - # in the 'except') - var_validate.unfix() - self.theta_names[i] = repr(var_cuid) - except: - logger.warning(theta + ' is not a variable') + theta_names = [k.name for k, v in model.unknown_parameters.items()] + parmest_model = utils.convert_params_to_vars(model, theta_names, fix_vars=False) - self.parmest_model = model - - return model + return parmest_model def _instance_creation_callback(self, experiment_number=None, cb_data=None): - # cb_data is a list of dictionaries, list of dataframes, OR list of json file names - exp_data = cb_data[experiment_number] - if isinstance(exp_data, (dict, pd.DataFrame)): - pass - elif isinstance(exp_data, str): - try: - with open(exp_data, 'r') as infile: - exp_data = json.load(infile) - except: - raise RuntimeError(f'Could not read {exp_data} as json') - else: - raise RuntimeError(f'Unexpected data format for cb_data={cb_data}') - model = self._create_parmest_model(exp_data) - + model = self._create_parmest_model(experiment_number) return model def _Q_opt( @@ -489,7 +468,7 @@ def _Q_opt( # (Bootstrap scenarios will use indirection through the bootlist) if bootlist is None: - scenario_numbers = list(range(len(self.callback_data))) + scenario_numbers = list(range(len(self.exp_list))) scen_names = ["Scenario{}".format(i) for i in scenario_numbers] else: scen_names = ["Scenario{}".format(i) for i in range(len(bootlist))] @@ -501,8 +480,8 @@ def _Q_opt( outer_cb_data["ThetaVals"] = ThetaVals if bootlist is not None: outer_cb_data["BootList"] = bootlist - outer_cb_data["cb_data"] = self.callback_data # None is OK - outer_cb_data["theta_names"] = self.theta_names + outer_cb_data["cb_data"] = None # None is OK + outer_cb_data["theta_names"] = self.estimator_theta_names options = {"solver": "ipopt"} scenario_creator_options = {"cb_data": outer_cb_data} @@ -548,14 +527,13 @@ def _Q_opt( for ndname, Var, solval in ef_nonants(ef): ind_vars.append(Var) # calculate the reduced hessian - ( - solve_result, - inv_red_hes, - ) = inverse_reduced_hessian.inv_reduced_hessian_barrier( - self.ef_instance, - independent_variables=ind_vars, - solver_options=self.solver_options, - tee=self.tee, + (solve_result, inv_red_hes) = ( + inverse_reduced_hessian.inv_reduced_hessian_barrier( + self.ef_instance, + independent_variables=ind_vars, + solver_options=self.solver_options, + tee=self.tee, + ) ) if self.diagnostic_mode: @@ -656,7 +634,7 @@ def _Q_at_theta(self, thetavals, initialize_parmest_model=False): initialize_parmest_model: boolean If True: Solve square problem instance, build extensive form of the model for - parameter estimation, and set flag model_initialized to True + parameter estimation, and set flag model_initialized to True. Default is False. Returns ------- @@ -677,13 +655,13 @@ def _Q_at_theta(self, thetavals, initialize_parmest_model=False): "callback": self._instance_creation_callback, "ThetaVals": thetavals, "theta_names": self._return_theta_names(), - "cb_data": self.callback_data, + "cb_data": None, } else: dummy_cb = { "callback": self._instance_creation_callback, "theta_names": self._return_theta_names(), - "cb_data": self.callback_data, + "cb_data": None, } if self.diagnostic_mode: @@ -704,7 +682,7 @@ def _Q_at_theta(self, thetavals, initialize_parmest_model=False): WorstStatus = pyo.TerminationCondition.optimal totobj = 0 - scenario_numbers = list(range(len(self.callback_data))) + scenario_numbers = list(range(len(self.exp_list))) if initialize_parmest_model: # create dictionary to store pyomo model instances (scenarios) scen_dict = dict() @@ -712,13 +690,14 @@ def _Q_at_theta(self, thetavals, initialize_parmest_model=False): for snum in scenario_numbers: sname = "scenario_NODE" + str(snum) instance = _experiment_instance_creation_callback(sname, None, dummy_cb) + model_theta_names = self._expand_indexed_unknowns(instance) if initialize_parmest_model: # list to store fitted parameter names that will be unfixed # after initialization theta_init_vals = [] # use appropriate theta_names member - theta_ref = self._return_theta_names() + theta_ref = model_theta_names for i, theta in enumerate(theta_ref): # Use parser in ComponentUID to locate the component @@ -745,14 +724,1157 @@ def _Q_at_theta(self, thetavals, initialize_parmest_model=False): if self.diagnostic_mode: print(' Experiment = ', snum) print(' First solve with special diagnostics wrapper') - ( - status_obj, - solved, - iters, - time, - regu, - ) = utils.ipopt_solve_with_stats( - instance, optimizer, max_iter=500, max_cpu_time=120 + (status_obj, solved, iters, time, regu) = ( + utils.ipopt_solve_with_stats( + instance, optimizer, max_iter=500, max_cpu_time=120 + ) + ) + print( + " status_obj, solved, iters, time, regularization_stat = ", + str(status_obj), + str(solved), + str(iters), + str(time), + str(regu), + ) + + results = optimizer.solve(instance) + if self.diagnostic_mode: + print( + 'standard solve solver termination condition=', + str(results.solver.termination_condition), + ) + + if ( + results.solver.termination_condition + != pyo.TerminationCondition.optimal + ): + # DLW: Aug2018: not distinguishing "middlish" conditions + if WorstStatus != pyo.TerminationCondition.infeasible: + WorstStatus = results.solver.termination_condition + if initialize_parmest_model: + if self.diagnostic_mode: + print( + "Scenario {:d} infeasible with initialized parameter values".format( + snum + ) + ) + else: + if initialize_parmest_model: + if self.diagnostic_mode: + print( + "Scenario {:d} initialization successful with initial parameter values".format( + snum + ) + ) + if initialize_parmest_model: + # unfix parameters after initialization + for theta in theta_init_vals: + theta.unfix() + scen_dict[sname] = instance + else: + if initialize_parmest_model: + # unfix parameters after initialization + for theta in theta_init_vals: + theta.unfix() + scen_dict[sname] = instance + + objobject = getattr(instance, self._second_stage_cost_exp) + objval = pyo.value(objobject) + totobj += objval + + retval = totobj / len(scenario_numbers) # -1?? + if initialize_parmest_model and not hasattr(self, 'ef_instance'): + # create extensive form of the model using scenario dictionary + if len(scen_dict) > 0: + for scen in scen_dict.values(): + scen._mpisppy_probability = 1 / len(scen_dict) + + if use_mpisppy: + EF_instance = sputils._create_EF_from_scen_dict( + scen_dict, + EF_name="_Q_at_theta", + # suppress_warnings=True + ) + else: + EF_instance = local_ef._create_EF_from_scen_dict( + scen_dict, EF_name="_Q_at_theta", nonant_for_fixed_vars=True + ) + + self.ef_instance = EF_instance + # set self.model_initialized flag to True to skip extensive form model + # creation using theta_est() + self.model_initialized = True + + # return initialized theta values + if len(thetavals) == 0: + # use appropriate theta_names member + theta_ref = self._return_theta_names() + for i, theta in enumerate(theta_ref): + thetavals[theta] = theta_init_vals[i]() + + return retval, thetavals, WorstStatus + + def _get_sample_list(self, samplesize, num_samples, replacement=True): + samplelist = list() + + scenario_numbers = list(range(len(self.exp_list))) + + if num_samples is None: + # This could get very large + for i, l in enumerate(combinations(scenario_numbers, samplesize)): + samplelist.append((i, np.sort(l))) + else: + for i in range(num_samples): + attempts = 0 + unique_samples = 0 # check for duplicates in each sample + duplicate = False # check for duplicates between samples + while (unique_samples <= len(self._return_theta_names())) and ( + not duplicate + ): + sample = np.random.choice( + scenario_numbers, samplesize, replace=replacement + ) + sample = np.sort(sample).tolist() + unique_samples = len(np.unique(sample)) + if sample in samplelist: + duplicate = True + + attempts += 1 + if attempts > num_samples: # arbitrary timeout limit + raise RuntimeError( + """Internal error: timeout constructing + a sample, the dim of theta may be too + close to the samplesize""" + ) + + samplelist.append((i, sample)) + + return samplelist + + def theta_est( + self, solver="ef_ipopt", return_values=[], calc_cov=False, cov_n=None + ): + """ + Parameter estimation using all scenarios in the data + + Parameters + ---------- + solver: string, optional + Currently only "ef_ipopt" is supported. Default is "ef_ipopt". + return_values: list, optional + List of Variable names, used to return values from the model for data reconciliation + calc_cov: boolean, optional + If True, calculate and return the covariance matrix (only for "ef_ipopt" solver). + Default is False. + cov_n: int, optional + If calc_cov=True, then the user needs to supply the number of datapoints + that are used in the objective function. + + Returns + ------- + objectiveval: float + The objective function value + thetavals: pd.Series + Estimated values for theta + variable values: pd.DataFrame + Variable values for each variable name in return_values (only for solver='ef_ipopt') + cov: pd.DataFrame + Covariance matrix of the fitted parameters (only for solver='ef_ipopt') + """ + + # check if we are using deprecated parmest + if self.pest_deprecated is not None: + return self.pest_deprecated.theta_est( + solver=solver, + return_values=return_values, + calc_cov=calc_cov, + cov_n=cov_n, + ) + + assert isinstance(solver, str) + assert isinstance(return_values, list) + assert isinstance(calc_cov, bool) + if calc_cov: + num_unknowns = max( + [ + len(experiment.get_labeled_model().unknown_parameters) + for experiment in self.exp_list + ] + ) + assert isinstance(cov_n, int), ( + "The number of datapoints that are used in the objective function is " + "required to calculate the covariance matrix" + ) + assert ( + cov_n > num_unknowns + ), "The number of datapoints must be greater than the number of parameters to estimate" + + return self._Q_opt( + solver=solver, + return_values=return_values, + bootlist=None, + calc_cov=calc_cov, + cov_n=cov_n, + ) + + def theta_est_bootstrap( + self, + bootstrap_samples, + samplesize=None, + replacement=True, + seed=None, + return_samples=False, + ): + """ + Parameter estimation using bootstrap resampling of the data + + Parameters + ---------- + bootstrap_samples: int + Number of bootstrap samples to draw from the data + samplesize: int or None, optional + Size of each bootstrap sample. If samplesize=None, samplesize will be + set to the number of samples in the data + replacement: bool, optional + Sample with or without replacement. Default is True. + seed: int or None, optional + Random seed + return_samples: bool, optional + Return a list of sample numbers used in each bootstrap estimation. + Default is False. + + Returns + ------- + bootstrap_theta: pd.DataFrame + Theta values for each sample and (if return_samples = True) + the sample numbers used in each estimation + """ + + # check if we are using deprecated parmest + if self.pest_deprecated is not None: + return self.pest_deprecated.theta_est_bootstrap( + bootstrap_samples, + samplesize=samplesize, + replacement=replacement, + seed=seed, + return_samples=return_samples, + ) + + assert isinstance(bootstrap_samples, int) + assert isinstance(samplesize, (type(None), int)) + assert isinstance(replacement, bool) + assert isinstance(seed, (type(None), int)) + assert isinstance(return_samples, bool) + + if samplesize is None: + samplesize = len(self.exp_list) + + if seed is not None: + np.random.seed(seed) + + global_list = self._get_sample_list(samplesize, bootstrap_samples, replacement) + + task_mgr = utils.ParallelTaskManager(bootstrap_samples) + local_list = task_mgr.global_to_local_data(global_list) + + bootstrap_theta = list() + for idx, sample in local_list: + objval, thetavals = self._Q_opt(bootlist=list(sample)) + thetavals['samples'] = sample + bootstrap_theta.append(thetavals) + + global_bootstrap_theta = task_mgr.allgather_global_data(bootstrap_theta) + bootstrap_theta = pd.DataFrame(global_bootstrap_theta) + + if not return_samples: + del bootstrap_theta['samples'] + + return bootstrap_theta + + def theta_est_leaveNout( + self, lNo, lNo_samples=None, seed=None, return_samples=False + ): + """ + Parameter estimation where N data points are left out of each sample + + Parameters + ---------- + lNo: int + Number of data points to leave out for parameter estimation + lNo_samples: int + Number of leave-N-out samples. If lNo_samples=None, the maximum + number of combinations will be used + seed: int or None, optional + Random seed + return_samples: bool, optional + Return a list of sample numbers that were left out. Default is False. + + Returns + ------- + lNo_theta: pd.DataFrame + Theta values for each sample and (if return_samples = True) + the sample numbers left out of each estimation + """ + + # check if we are using deprecated parmest + if self.pest_deprecated is not None: + return self.pest_deprecated.theta_est_leaveNout( + lNo, lNo_samples=lNo_samples, seed=seed, return_samples=return_samples + ) + + assert isinstance(lNo, int) + assert isinstance(lNo_samples, (type(None), int)) + assert isinstance(seed, (type(None), int)) + assert isinstance(return_samples, bool) + + samplesize = len(self.exp_list) - lNo + + if seed is not None: + np.random.seed(seed) + + global_list = self._get_sample_list(samplesize, lNo_samples, replacement=False) + + task_mgr = utils.ParallelTaskManager(len(global_list)) + local_list = task_mgr.global_to_local_data(global_list) + + lNo_theta = list() + for idx, sample in local_list: + objval, thetavals = self._Q_opt(bootlist=list(sample)) + lNo_s = list(set(range(len(self.exp_list))) - set(sample)) + thetavals['lNo'] = np.sort(lNo_s) + lNo_theta.append(thetavals) + + global_bootstrap_theta = task_mgr.allgather_global_data(lNo_theta) + lNo_theta = pd.DataFrame(global_bootstrap_theta) + + if not return_samples: + del lNo_theta['lNo'] + + return lNo_theta + + def leaveNout_bootstrap_test( + self, lNo, lNo_samples, bootstrap_samples, distribution, alphas, seed=None + ): + """ + Leave-N-out bootstrap test to compare theta values where N data points are + left out to a bootstrap analysis using the remaining data, + results indicate if theta is within a confidence region + determined by the bootstrap analysis + + Parameters + ---------- + lNo: int + Number of data points to leave out for parameter estimation + lNo_samples: int + Leave-N-out sample size. If lNo_samples=None, the maximum number + of combinations will be used + bootstrap_samples: int: + Bootstrap sample size + distribution: string + Statistical distribution used to define a confidence region, + options = 'MVN' for multivariate_normal, 'KDE' for gaussian_kde, + and 'Rect' for rectangular. + alphas: list + List of alpha values used to determine if theta values are inside + or outside the region. + seed: int or None, optional + Random seed + + Returns + ------- + List of tuples with one entry per lNo_sample: + + * The first item in each tuple is the list of N samples that are left + out. + * The second item in each tuple is a DataFrame of theta estimated using + the N samples. + * The third item in each tuple is a DataFrame containing results from + the bootstrap analysis using the remaining samples. + + For each DataFrame a column is added for each value of alpha which + indicates if the theta estimate is in (True) or out (False) of the + alpha region for a given distribution (based on the bootstrap results) + """ + + # check if we are using deprecated parmest + if self.pest_deprecated is not None: + return self.pest_deprecated.leaveNout_bootstrap_test( + lNo, lNo_samples, bootstrap_samples, distribution, alphas, seed=seed + ) + + assert isinstance(lNo, int) + assert isinstance(lNo_samples, (type(None), int)) + assert isinstance(bootstrap_samples, int) + assert distribution in ['Rect', 'MVN', 'KDE'] + assert isinstance(alphas, list) + assert isinstance(seed, (type(None), int)) + + if seed is not None: + np.random.seed(seed) + + global_list = self._get_sample_list(lNo, lNo_samples, replacement=False) + + results = [] + for idx, sample in global_list: + + obj, theta = self.theta_est() + + bootstrap_theta = self.theta_est_bootstrap(bootstrap_samples) + + training, test = self.confidence_region_test( + bootstrap_theta, + distribution=distribution, + alphas=alphas, + test_theta_values=theta, + ) + + results.append((sample, test, training)) + + return results + + def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): + """ + Objective value for each theta + + Parameters + ---------- + theta_values: pd.DataFrame, columns=theta_names + Values of theta used to compute the objective + + initialize_parmest_model: boolean + If True: Solve square problem instance, build extensive form + of the model for parameter estimation, and set flag + model_initialized to True. Default is False. + + + Returns + ------- + obj_at_theta: pd.DataFrame + Objective value for each theta (infeasible solutions are + omitted). + """ + + # check if we are using deprecated parmest + if self.pest_deprecated is not None: + return self.pest_deprecated.objective_at_theta( + theta_values=theta_values, + initialize_parmest_model=initialize_parmest_model, + ) + + if len(self.estimator_theta_names) == 0: + pass # skip assertion if model has no fitted parameters + else: + # create a local instance of the pyomo model to access model variables and parameters + model_temp = self._create_parmest_model(0) + model_theta_list = self._expand_indexed_unknowns(model_temp) + + # if self.estimator_theta_names is not the same as temp model_theta_list, + # create self.theta_names_updated + if set(self.estimator_theta_names) == set(model_theta_list) and len( + self.estimator_theta_names + ) == len(set(model_theta_list)): + pass + else: + self.theta_names_updated = model_theta_list + + if theta_values is None: + all_thetas = {} # dictionary to store fitted variables + # use appropriate theta names member + theta_names = model_theta_list + else: + assert isinstance(theta_values, pd.DataFrame) + # for parallel code we need to use lists and dicts in the loop + theta_names = theta_values.columns + # # check if theta_names are in model + for theta in list(theta_names): + theta_temp = theta.replace("'", "") # cleaning quotes from theta_names + assert theta_temp in [ + t.replace("'", "") for t in model_theta_list + ], "Theta name {} in 'theta_values' not in 'theta_names' {}".format( + theta_temp, model_theta_list + ) + + assert len(list(theta_names)) == len(model_theta_list) + + all_thetas = theta_values.to_dict('records') + + if all_thetas: + task_mgr = utils.ParallelTaskManager(len(all_thetas)) + local_thetas = task_mgr.global_to_local_data(all_thetas) + else: + if initialize_parmest_model: + task_mgr = utils.ParallelTaskManager( + 1 + ) # initialization performed using just 1 set of theta values + # walk over the mesh, return objective function + all_obj = list() + if len(all_thetas) > 0: + for Theta in local_thetas: + obj, thetvals, worststatus = self._Q_at_theta( + Theta, initialize_parmest_model=initialize_parmest_model + ) + if worststatus != pyo.TerminationCondition.infeasible: + all_obj.append(list(Theta.values()) + [obj]) + # DLW, Aug2018: should we also store the worst solver status? + else: + obj, thetvals, worststatus = self._Q_at_theta( + thetavals={}, initialize_parmest_model=initialize_parmest_model + ) + if worststatus != pyo.TerminationCondition.infeasible: + all_obj.append(list(thetvals.values()) + [obj]) + + global_all_obj = task_mgr.allgather_global_data(all_obj) + dfcols = list(theta_names) + ['obj'] + obj_at_theta = pd.DataFrame(data=global_all_obj, columns=dfcols) + return obj_at_theta + + def likelihood_ratio_test( + self, obj_at_theta, obj_value, alphas, return_thresholds=False + ): + r""" + Likelihood ratio test to identify theta values within a confidence + region using the :math:`\chi^2` distribution + + Parameters + ---------- + obj_at_theta: pd.DataFrame, columns = theta_names + 'obj' + Objective values for each theta value (returned by + objective_at_theta) + obj_value: int or float + Objective value from parameter estimation using all data + alphas: list + List of alpha values to use in the chi2 test + return_thresholds: bool, optional + Return the threshold value for each alpha. Default is False. + + Returns + ------- + LR: pd.DataFrame + Objective values for each theta value along with True or False for + each alpha + thresholds: pd.Series + If return_threshold = True, the thresholds are also returned. + """ + + # check if we are using deprecated parmest + if self.pest_deprecated is not None: + return self.pest_deprecated.likelihood_ratio_test( + obj_at_theta, obj_value, alphas, return_thresholds=return_thresholds + ) + + assert isinstance(obj_at_theta, pd.DataFrame) + assert isinstance(obj_value, (int, float)) + assert isinstance(alphas, list) + assert isinstance(return_thresholds, bool) + + LR = obj_at_theta.copy() + S = len(self.exp_list) + thresholds = {} + for a in alphas: + chi2_val = scipy.stats.chi2.ppf(a, 2) + thresholds[a] = obj_value * ((chi2_val / (S - 2)) + 1) + LR[a] = LR['obj'] < thresholds[a] + + thresholds = pd.Series(thresholds) + + if return_thresholds: + return LR, thresholds + else: + return LR + + def confidence_region_test( + self, theta_values, distribution, alphas, test_theta_values=None + ): + """ + Confidence region test to determine if theta values are within a + rectangular, multivariate normal, or Gaussian kernel density distribution + for a range of alpha values + + Parameters + ---------- + theta_values: pd.DataFrame, columns = theta_names + Theta values used to generate a confidence region + (generally returned by theta_est_bootstrap) + distribution: string + Statistical distribution used to define a confidence region, + options = 'MVN' for multivariate_normal, 'KDE' for gaussian_kde, + and 'Rect' for rectangular. + alphas: list + List of alpha values used to determine if theta values are inside + or outside the region. + test_theta_values: pd.Series or pd.DataFrame, keys/columns = theta_names, optional + Additional theta values that are compared to the confidence region + to determine if they are inside or outside. + + Returns + ------- + training_results: pd.DataFrame + Theta value used to generate the confidence region along with True + (inside) or False (outside) for each alpha + test_results: pd.DataFrame + If test_theta_values is not None, returns test theta value along + with True (inside) or False (outside) for each alpha + """ + + # check if we are using deprecated parmest + if self.pest_deprecated is not None: + return self.pest_deprecated.confidence_region_test( + theta_values, distribution, alphas, test_theta_values=test_theta_values + ) + + assert isinstance(theta_values, pd.DataFrame) + assert distribution in ['Rect', 'MVN', 'KDE'] + assert isinstance(alphas, list) + assert isinstance( + test_theta_values, (type(None), dict, pd.Series, pd.DataFrame) + ) + + if isinstance(test_theta_values, (dict, pd.Series)): + test_theta_values = pd.Series(test_theta_values).to_frame().transpose() + + training_results = theta_values.copy() + + if test_theta_values is not None: + test_result = test_theta_values.copy() + + for a in alphas: + if distribution == 'Rect': + lb, ub = graphics.fit_rect_dist(theta_values, a) + training_results[a] = (theta_values > lb).all(axis=1) & ( + theta_values < ub + ).all(axis=1) + + if test_theta_values is not None: + # use upper and lower bound from the training set + test_result[a] = (test_theta_values > lb).all(axis=1) & ( + test_theta_values < ub + ).all(axis=1) + + elif distribution == 'MVN': + dist = graphics.fit_mvn_dist(theta_values) + Z = dist.pdf(theta_values) + score = scipy.stats.scoreatpercentile(Z, (1 - a) * 100) + training_results[a] = Z >= score + + if test_theta_values is not None: + # use score from the training set + Z = dist.pdf(test_theta_values) + test_result[a] = Z >= score + + elif distribution == 'KDE': + dist = graphics.fit_kde_dist(theta_values) + Z = dist.pdf(theta_values.transpose()) + score = scipy.stats.scoreatpercentile(Z, (1 - a) * 100) + training_results[a] = Z >= score + + if test_theta_values is not None: + # use score from the training set + Z = dist.pdf(test_theta_values.transpose()) + test_result[a] = Z >= score + + if test_theta_values is not None: + return training_results, test_result + else: + return training_results + + +################################ +# deprecated functions/classes # +################################ + + +@deprecated(version='6.7.2') +def group_data(data, groupby_column_name, use_mean=None): + """ + Group data by scenario + + Parameters + ---------- + data: DataFrame + Data + groupby_column_name: strings + Name of data column which contains scenario numbers + use_mean: list of column names or None, optional + Name of data columns which should be reduced to a single value per + scenario by taking the mean + + Returns + ---------- + grouped_data: list of dictionaries + Grouped data + """ + if use_mean is None: + use_mean_list = [] + else: + use_mean_list = use_mean + + grouped_data = [] + for exp_num, group in data.groupby(data[groupby_column_name]): + d = {} + for col in group.columns: + if col in use_mean_list: + d[col] = group[col].mean() + else: + d[col] = list(group[col]) + grouped_data.append(d) + + return grouped_data + + +class _DeprecatedSecondStageCostExpr(object): + """ + Class to pass objective expression into the Pyomo model + """ + + def __init__(self, ssc_function, data): + self._ssc_function = ssc_function + self._data = data + + def __call__(self, model): + return self._ssc_function(model, self._data) + + +class _DeprecatedEstimator(object): + """ + Parameter estimation class + + Parameters + ---------- + model_function: function + Function that generates an instance of the Pyomo model using 'data' + as the input argument + data: pd.DataFrame, list of dictionaries, list of dataframes, or list of json file names + Data that is used to build an instance of the Pyomo model and build + the objective function + theta_names: list of strings + List of Var names to estimate + obj_function: function, optional + Function used to formulate parameter estimation objective, generally + sum of squared error between measurements and model variables. + If no function is specified, the model is used + "as is" and should be defined with a "FirstStageCost" and + "SecondStageCost" expression that are used to build an objective. + tee: bool, optional + Indicates that ef solver output should be teed + diagnostic_mode: bool, optional + If True, print diagnostics from the solver + solver_options: dict, optional + Provides options to the solver (also the name of an attribute) + """ + + def __init__( + self, + model_function, + data, + theta_names, + obj_function=None, + tee=False, + diagnostic_mode=False, + solver_options=None, + ): + self.model_function = model_function + + assert isinstance( + data, (list, pd.DataFrame) + ), "Data must be a list or DataFrame" + # convert dataframe into a list of dataframes, each row = one scenario + if isinstance(data, pd.DataFrame): + self.callback_data = [ + data.loc[i, :].to_frame().transpose() for i in data.index + ] + else: + self.callback_data = data + assert isinstance( + self.callback_data[0], (dict, pd.DataFrame, str) + ), "The scenarios in data must be a dictionary, DataFrame or filename" + + if len(theta_names) == 0: + self.theta_names = ['parmest_dummy_var'] + else: + self.theta_names = theta_names + + self.obj_function = obj_function + self.tee = tee + self.diagnostic_mode = diagnostic_mode + self.solver_options = solver_options + + self._second_stage_cost_exp = "SecondStageCost" + # boolean to indicate if model is initialized using a square solve + self.model_initialized = False + + def _return_theta_names(self): + """ + Return list of fitted model parameter names + """ + # if fitted model parameter names differ from theta_names created when Estimator object is created + if hasattr(self, 'theta_names_updated'): + return self.theta_names_updated + + else: + return ( + self.theta_names + ) # default theta_names, created when Estimator object is created + + def _create_parmest_model(self, data): + """ + Modify the Pyomo model for parameter estimation + """ + model = self.model_function(data) + + if (len(self.theta_names) == 1) and ( + self.theta_names[0] == 'parmest_dummy_var' + ): + model.parmest_dummy_var = pyo.Var(initialize=1.0) + + # Add objective function (optional) + if self.obj_function: + for obj in model.component_objects(pyo.Objective): + if obj.name in ["Total_Cost_Objective"]: + raise RuntimeError( + "Parmest will not override the existing model Objective named " + + obj.name + ) + obj.deactivate() + + for expr in model.component_data_objects(pyo.Expression): + if expr.name in ["FirstStageCost", "SecondStageCost"]: + raise RuntimeError( + "Parmest will not override the existing model Expression named " + + expr.name + ) + model.FirstStageCost = pyo.Expression(expr=0) + model.SecondStageCost = pyo.Expression( + rule=_DeprecatedSecondStageCostExpr(self.obj_function, data) + ) + + def TotalCost_rule(model): + return model.FirstStageCost + model.SecondStageCost + + model.Total_Cost_Objective = pyo.Objective( + rule=TotalCost_rule, sense=pyo.minimize + ) + + # Convert theta Params to Vars, and unfix theta Vars + model = utils.convert_params_to_vars(model, self.theta_names) + + # Update theta names list to use CUID string representation + for i, theta in enumerate(self.theta_names): + var_cuid = ComponentUID(theta) + var_validate = var_cuid.find_component_on(model) + if var_validate is None: + logger.warning( + "theta_name[%s] (%s) was not found on the model", (i, theta) + ) + else: + try: + # If the component is not a variable, + # this will generate an exception (and the warning + # in the 'except') + var_validate.unfix() + self.theta_names[i] = repr(var_cuid) + except: + logger.warning(theta + ' is not a variable') + + self.parmest_model = model + + return model + + def _instance_creation_callback(self, experiment_number=None, cb_data=None): + # cb_data is a list of dictionaries, list of dataframes, OR list of json file names + exp_data = cb_data[experiment_number] + if isinstance(exp_data, (dict, pd.DataFrame)): + pass + elif isinstance(exp_data, str): + try: + with open(exp_data, 'r') as infile: + exp_data = json.load(infile) + except: + raise RuntimeError(f'Could not read {exp_data} as json') + else: + raise RuntimeError(f'Unexpected data format for cb_data={cb_data}') + model = self._create_parmest_model(exp_data) + + return model + + def _Q_opt( + self, + ThetaVals=None, + solver="ef_ipopt", + return_values=[], + bootlist=None, + calc_cov=False, + cov_n=None, + ): + """ + Set up all thetas as first stage Vars, return resulting theta + values as well as the objective function value. + + """ + if solver == "k_aug": + raise RuntimeError("k_aug no longer supported.") + + # (Bootstrap scenarios will use indirection through the bootlist) + if bootlist is None: + scenario_numbers = list(range(len(self.callback_data))) + scen_names = ["Scenario{}".format(i) for i in scenario_numbers] + else: + scen_names = ["Scenario{}".format(i) for i in range(len(bootlist))] + + # tree_model.CallbackModule = None + outer_cb_data = dict() + outer_cb_data["callback"] = self._instance_creation_callback + if ThetaVals is not None: + outer_cb_data["ThetaVals"] = ThetaVals + if bootlist is not None: + outer_cb_data["BootList"] = bootlist + outer_cb_data["cb_data"] = self.callback_data # None is OK + outer_cb_data["theta_names"] = self.theta_names + + options = {"solver": "ipopt"} + scenario_creator_options = {"cb_data": outer_cb_data} + if use_mpisppy: + ef = sputils.create_EF( + scen_names, + _experiment_instance_creation_callback, + EF_name="_Q_opt", + suppress_warnings=True, + scenario_creator_kwargs=scenario_creator_options, + ) + else: + ef = local_ef.create_EF( + scen_names, + _experiment_instance_creation_callback, + EF_name="_Q_opt", + suppress_warnings=True, + scenario_creator_kwargs=scenario_creator_options, + ) + self.ef_instance = ef + + # Solve the extensive form with ipopt + if solver == "ef_ipopt": + if not calc_cov: + # Do not calculate the reduced hessian + + solver = SolverFactory('ipopt') + if self.solver_options is not None: + for key in self.solver_options: + solver.options[key] = self.solver_options[key] + + solve_result = solver.solve(self.ef_instance, tee=self.tee) + + # The import error will be raised when we attempt to use + # inv_reduced_hessian_barrier below. + # + # elif not asl_available: + # raise ImportError("parmest requires ASL to calculate the " + # "covariance matrix with solver 'ipopt'") + else: + # parmest makes the fitted parameters stage 1 variables + ind_vars = [] + for ndname, Var, solval in ef_nonants(ef): + ind_vars.append(Var) + # calculate the reduced hessian + (solve_result, inv_red_hes) = ( + inverse_reduced_hessian.inv_reduced_hessian_barrier( + self.ef_instance, + independent_variables=ind_vars, + solver_options=self.solver_options, + tee=self.tee, + ) + ) + + if self.diagnostic_mode: + print( + ' Solver termination condition = ', + str(solve_result.solver.termination_condition), + ) + + # assume all first stage are thetas... + thetavals = {} + for ndname, Var, solval in ef_nonants(ef): + # process the name + # the scenarios are blocks, so strip the scenario name + vname = Var.name[Var.name.find(".") + 1 :] + thetavals[vname] = solval + + objval = pyo.value(ef.EF_Obj) + + if calc_cov: + # Calculate the covariance matrix + + # Number of data points considered + n = cov_n + + # Extract number of fitted parameters + l = len(thetavals) + + # Assumption: Objective value is sum of squared errors + sse = objval + + '''Calculate covariance assuming experimental observation errors are + independent and follow a Gaussian + distribution with constant variance. + + The formula used in parmest was verified against equations (7-5-15) and + (7-5-16) in "Nonlinear Parameter Estimation", Y. Bard, 1974. + + This formula is also applicable if the objective is scaled by a constant; + the constant cancels out. (was scaled by 1/n because it computes an + expected value.) + ''' + cov = 2 * sse / (n - l) * inv_red_hes + cov = pd.DataFrame( + cov, index=thetavals.keys(), columns=thetavals.keys() + ) + + thetavals = pd.Series(thetavals) + + if len(return_values) > 0: + var_values = [] + if len(scen_names) > 1: # multiple scenarios + block_objects = self.ef_instance.component_objects( + Block, descend_into=False + ) + else: # single scenario + block_objects = [self.ef_instance] + for exp_i in block_objects: + vals = {} + for var in return_values: + exp_i_var = exp_i.find_component(str(var)) + if ( + exp_i_var is None + ): # we might have a block such as _mpisppy_data + continue + # if value to return is ContinuousSet + if type(exp_i_var) == ContinuousSet: + temp = list(exp_i_var) + else: + temp = [pyo.value(_) for _ in exp_i_var.values()] + if len(temp) == 1: + vals[var] = temp[0] + else: + vals[var] = temp + if len(vals) > 0: + var_values.append(vals) + var_values = pd.DataFrame(var_values) + if calc_cov: + return objval, thetavals, var_values, cov + else: + return objval, thetavals, var_values + + if calc_cov: + return objval, thetavals, cov + else: + return objval, thetavals + + else: + raise RuntimeError("Unknown solver in Q_Opt=" + solver) + + def _Q_at_theta(self, thetavals, initialize_parmest_model=False): + """ + Return the objective function value with fixed theta values. + + Parameters + ---------- + thetavals: dict + A dictionary of theta values. + + initialize_parmest_model: boolean + If True: Solve square problem instance, build extensive form of the model for + parameter estimation, and set flag model_initialized to True + + Returns + ------- + objectiveval: float + The objective function value. + thetavals: dict + A dictionary of all values for theta that were input. + solvertermination: Pyomo TerminationCondition + Tries to return the "worst" solver status across the scenarios. + pyo.TerminationCondition.optimal is the best and + pyo.TerminationCondition.infeasible is the worst. + """ + + optimizer = pyo.SolverFactory('ipopt') + + if len(thetavals) > 0: + dummy_cb = { + "callback": self._instance_creation_callback, + "ThetaVals": thetavals, + "theta_names": self._return_theta_names(), + "cb_data": self.callback_data, + } + else: + dummy_cb = { + "callback": self._instance_creation_callback, + "theta_names": self._return_theta_names(), + "cb_data": self.callback_data, + } + + if self.diagnostic_mode: + if len(thetavals) > 0: + print(' Compute objective at theta = ', str(thetavals)) + else: + print(' Compute objective at initial theta') + + # start block of code to deal with models with no constraints + # (ipopt will crash or complain on such problems without special care) + instance = _experiment_instance_creation_callback("FOO0", None, dummy_cb) + try: # deal with special problems so Ipopt will not crash + first = next(instance.component_objects(pyo.Constraint, active=True)) + active_constraints = True + except: + active_constraints = False + # end block of code to deal with models with no constraints + + WorstStatus = pyo.TerminationCondition.optimal + totobj = 0 + scenario_numbers = list(range(len(self.callback_data))) + if initialize_parmest_model: + # create dictionary to store pyomo model instances (scenarios) + scen_dict = dict() + + for snum in scenario_numbers: + sname = "scenario_NODE" + str(snum) + instance = _experiment_instance_creation_callback(sname, None, dummy_cb) + + if initialize_parmest_model: + # list to store fitted parameter names that will be unfixed + # after initialization + theta_init_vals = [] + # use appropriate theta_names member + theta_ref = self._return_theta_names() + + for i, theta in enumerate(theta_ref): + # Use parser in ComponentUID to locate the component + var_cuid = ComponentUID(theta) + var_validate = var_cuid.find_component_on(instance) + if var_validate is None: + logger.warning( + "theta_name %s was not found on the model", (theta) + ) + else: + try: + if len(thetavals) == 0: + var_validate.fix() + else: + var_validate.fix(thetavals[theta]) + theta_init_vals.append(var_validate) + except: + logger.warning( + 'Unable to fix model parameter value for %s (not a Pyomo model Var)', + (theta), + ) + + if active_constraints: + if self.diagnostic_mode: + print(' Experiment = ', snum) + print(' First solve with special diagnostics wrapper') + (status_obj, solved, iters, time, regu) = ( + utils.ipopt_solve_with_stats( + instance, optimizer, max_iter=500, max_cpu_time=120 + ) ) print( " status_obj, solved, iters, time, regularization_stat = ", diff --git a/pyomo/contrib/parmest/scenariocreator.py b/pyomo/contrib/parmest/scenariocreator.py index 58d2d4da722..e887dd2e8be 100644 --- a/pyomo/contrib/parmest/scenariocreator.py +++ b/pyomo/contrib/parmest/scenariocreator.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -14,6 +14,10 @@ import pyomo.environ as pyo +import logging + +logger = logging.getLogger(__name__) + class ScenarioSet(object): """ @@ -119,6 +123,7 @@ class ScenarioCreator(object): """ def __init__(self, pest, solvername): + self.pest = pest self.solvername = solvername @@ -133,23 +138,32 @@ def ScenariosFromExperiments(self, addtoSet): assert isinstance(addtoSet, ScenarioSet) - scenario_numbers = list(range(len(self.pest.callback_data))) + if self.pest.pest_deprecated is not None: + scenario_numbers = list(range(len(self.pest.pest_deprecated.callback_data))) + else: + scenario_numbers = list(range(len(self.pest.exp_list))) prob = 1.0 / len(scenario_numbers) for exp_num in scenario_numbers: ##print("Experiment number=", exp_num) - model = self.pest._instance_creation_callback( - exp_num, self.pest.callback_data - ) + if self.pest.pest_deprecated is not None: + model = self.pest.pest_deprecated._instance_creation_callback( + exp_num, self.pest.pest_deprecated.callback_data + ) + else: + model = self.pest._instance_creation_callback(exp_num) opt = pyo.SolverFactory(self.solvername) results = opt.solve(model) # solves and updates model ## pyo.check_termination_optimal(results) - ThetaVals = dict() - for theta in self.pest.theta_names: - tvar = eval('model.' + theta) - tval = pyo.value(tvar) - ##print(" theta, tval=", tvar, tval) - ThetaVals[theta] = tval + if self.pest.pest_deprecated is not None: + ThetaVals = { + theta: pyo.value(model.find_component(theta)) + for theta in self.pest.pest_deprecated.theta_names + } + else: + ThetaVals = { + k.name: pyo.value(k) for k in model.unknown_parameters.keys() + } addtoSet.addone(ParmestScen("ExpScen" + str(exp_num), ThetaVals, prob)) def ScenariosFromBootstrap(self, addtoSet, numtomake, seed=None): @@ -162,5 +176,10 @@ def ScenariosFromBootstrap(self, addtoSet, numtomake, seed=None): assert isinstance(addtoSet, ScenarioSet) - bootstrap_thetas = self.pest.theta_est_bootstrap(numtomake, seed=seed) + if self.pest.pest_deprecated is not None: + bootstrap_thetas = self.pest.pest_deprecated.theta_est_bootstrap( + numtomake, seed=seed + ) + else: + bootstrap_thetas = self.pest.theta_est_bootstrap(numtomake, seed=seed) addtoSet.append_bootstrap(bootstrap_thetas) diff --git a/pyomo/contrib/parmest/tests/__init__.py b/pyomo/contrib/parmest/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/parmest/tests/__init__.py +++ b/pyomo/contrib/parmest/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/tests/test_examples.py b/pyomo/contrib/parmest/tests/test_examples.py index 67e06130384..dca05026e80 100644 --- a/pyomo/contrib/parmest/tests/test_examples.py +++ b/pyomo/contrib/parmest/tests/test_examples.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -181,7 +181,10 @@ def test_multisensor_data_example(self): multisensor_data_example.main() - @unittest.skipUnless(matplotlib_available, "test requires matplotlib") + @unittest.skipUnless( + matplotlib_available and seaborn_available, + "test requires matplotlib and seaborn", + ) def test_datarec_example(self): from pyomo.contrib.parmest.examples.reactor_design import datarec_example diff --git a/pyomo/contrib/parmest/tests/test_graphics.py b/pyomo/contrib/parmest/tests/test_graphics.py index c18659e9948..3b4d0224ebe 100644 --- a/pyomo/contrib/parmest/tests/test_graphics.py +++ b/pyomo/contrib/parmest/tests/test_graphics.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index f26ecec2fce..65e2e4a3b06 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -22,7 +22,7 @@ import platform -is_osx = platform.mac_ver()[0] != '' +is_osx = platform.mac_ver()[0] != "" import pyomo.common.unittest as unittest import sys @@ -33,16 +33,17 @@ import pyomo.contrib.parmest.parmest as parmest import pyomo.contrib.parmest.graphics as graphics import pyomo.contrib.parmest as parmestbase +from pyomo.contrib.parmest.experiment import Experiment import pyomo.environ as pyo import pyomo.dae as dae from pyomo.opt import SolverFactory -ipopt_available = SolverFactory('ipopt').available() +ipopt_available = SolverFactory("ipopt").available() from pyomo.common.fileutils import find_library -pynumero_ASL_available = False if find_library('pynumero_ASL') is None else True +pynumero_ASL_available = False if find_library("pynumero_ASL") is None else True testdir = os.path.dirname(os.path.abspath(__file__)) @@ -55,16 +56,1027 @@ class TestRooneyBiegler(unittest.TestCase): def setUp(self): from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( - rooney_biegler_model, + RooneyBieglerExperiment, + ) + + # Note, the data used in this test has been corrected to use + # data.loc[5,'hour'] = 7 (instead of 6) + data = pd.DataFrame( + data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], + columns=["hour", "y"], + ) + + # Sum of squared error function + def SSE(model): + expr = ( + model.experiment_outputs[model.y] + - model.response_function[model.experiment_outputs[model.hour]] + ) ** 2 + return expr + + # Create an experiment list + exp_list = [] + for i in range(data.shape[0]): + exp_list.append(RooneyBieglerExperiment(data.loc[i, :])) + + # Create an instance of the parmest estimator + pest = parmest.Estimator(exp_list, obj_function=SSE) + + solver_options = {"tol": 1e-8} + + self.data = data + self.pest = parmest.Estimator( + exp_list, obj_function=SSE, solver_options=solver_options, tee=True + ) + + def test_theta_est(self): + objval, thetavals = self.pest.theta_est() + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + thetavals["asymptote"], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual( + thetavals["rate_constant"], 0.5311, places=2 + ) # 0.5311 from the paper + + @unittest.skipIf( + not graphics.imports_available, "parmest.graphics imports are unavailable" + ) + def test_bootstrap(self): + objval, thetavals = self.pest.theta_est() + + num_bootstraps = 10 + theta_est = self.pest.theta_est_bootstrap(num_bootstraps, return_samples=True) + + num_samples = theta_est["samples"].apply(len) + self.assertEqual(len(theta_est.index), 10) + self.assertTrue(num_samples.equals(pd.Series([6] * 10))) + + del theta_est["samples"] + + # apply confidence region test + CR = self.pest.confidence_region_test(theta_est, "MVN", [0.5, 0.75, 1.0]) + + self.assertTrue(set(CR.columns) >= set([0.5, 0.75, 1.0])) + self.assertEqual(CR[0.5].sum(), 5) + self.assertEqual(CR[0.75].sum(), 7) + self.assertEqual(CR[1.0].sum(), 10) # all true + + graphics.pairwise_plot(theta_est) + graphics.pairwise_plot(theta_est, thetavals) + graphics.pairwise_plot(theta_est, thetavals, 0.8, ["MVN", "KDE", "Rect"]) + + @unittest.skipIf( + not graphics.imports_available, "parmest.graphics imports are unavailable" + ) + def test_likelihood_ratio(self): + objval, thetavals = self.pest.theta_est() + + asym = np.arange(10, 30, 2) + rate = np.arange(0, 1.5, 0.25) + theta_vals = pd.DataFrame( + list(product(asym, rate)), columns=['asymptote', 'rate_constant'] + ) + obj_at_theta = self.pest.objective_at_theta(theta_vals) + + LR = self.pest.likelihood_ratio_test(obj_at_theta, objval, [0.8, 0.9, 1.0]) + + self.assertTrue(set(LR.columns) >= set([0.8, 0.9, 1.0])) + self.assertEqual(LR[0.8].sum(), 6) + self.assertEqual(LR[0.9].sum(), 10) + self.assertEqual(LR[1.0].sum(), 60) # all true + + graphics.pairwise_plot(LR, thetavals, 0.8) + + def test_leaveNout(self): + lNo_theta = self.pest.theta_est_leaveNout(1) + self.assertTrue(lNo_theta.shape == (6, 2)) + + results = self.pest.leaveNout_bootstrap_test( + 1, None, 3, "Rect", [0.5, 1.0], seed=5436 + ) + self.assertEqual(len(results), 6) # 6 lNo samples + i = 1 + samples = results[i][0] # list of N samples that are left out + lno_theta = results[i][1] + bootstrap_theta = results[i][2] + self.assertTrue(samples == [1]) # sample 1 was left out + self.assertEqual(lno_theta.shape[0], 1) # lno estimate for sample 1 + self.assertTrue(set(lno_theta.columns) >= set([0.5, 1.0])) + self.assertEqual(lno_theta[1.0].sum(), 1) # all true + self.assertEqual(bootstrap_theta.shape[0], 3) # bootstrap for sample 1 + self.assertEqual(bootstrap_theta[1.0].sum(), 3) # all true + + def test_diagnostic_mode(self): + self.pest.diagnostic_mode = True + + objval, thetavals = self.pest.theta_est() + + asym = np.arange(10, 30, 2) + rate = np.arange(0, 1.5, 0.25) + theta_vals = pd.DataFrame( + list(product(asym, rate)), columns=['asymptote', 'rate_constant'] + ) + + obj_at_theta = self.pest.objective_at_theta(theta_vals) + + self.pest.diagnostic_mode = False + + @unittest.skip("Presently having trouble with mpiexec on appveyor") + def test_parallel_parmest(self): + """use mpiexec and mpi4py""" + p = str(parmestbase.__path__) + l = p.find("'") + r = p.find("'", l + 1) + parmestpath = p[l + 1 : r] + rbpath = ( + parmestpath + + os.sep + + "examples" + + os.sep + + "rooney_biegler" + + os.sep + + "rooney_biegler_parmest.py" + ) + rbpath = os.path.abspath(rbpath) # paranoia strikes deep... + rlist = ["mpiexec", "--allow-run-as-root", "-n", "2", sys.executable, rbpath] + if sys.version_info >= (3, 5): + ret = subprocess.run(rlist) + retcode = ret.returncode + else: + retcode = subprocess.call(rlist) + self.assertEqual(retcode, 0) + + @unittest.skip("Most folks don't have k_aug installed") + def test_theta_k_aug_for_Hessian(self): + # this will fail if k_aug is not installed + objval, thetavals, Hessian = self.pest.theta_est(solver="k_aug") + self.assertAlmostEqual(objval, 4.4675, places=2) + + @unittest.skipIf(not pynumero_ASL_available, "pynumero ASL is not available") + @unittest.skipIf( + not parmest.inverse_reduced_hessian_available, + "Cannot test covariance matrix: required ASL dependency is missing", + ) + def test_theta_est_cov(self): + objval, thetavals, cov = self.pest.theta_est(calc_cov=True, cov_n=6) + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + thetavals["asymptote"], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual( + thetavals["rate_constant"], 0.5311, places=2 + ) # 0.5311 from the paper + + # Covariance matrix + self.assertAlmostEqual( + cov["asymptote"]["asymptote"], 6.30579403, places=2 + ) # 6.22864 from paper + self.assertAlmostEqual( + cov["asymptote"]["rate_constant"], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov["rate_constant"]["asymptote"], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov["rate_constant"]["rate_constant"], 0.04124, places=2 + ) # 0.04124 from paper + + """ Why does the covariance matrix from parmest not match the paper? Parmest is + calculating the exact reduced Hessian. The paper (Rooney and Bielger, 2001) likely + employed the first order approximation common for nonlinear regression. The paper + values were verified with Scipy, which uses the same first order approximation. + The formula used in parmest was verified against equations (7-5-15) and (7-5-16) in + "Nonlinear Parameter Estimation", Y. Bard, 1974. + """ + + def test_cov_scipy_least_squares_comparison(self): + """ + Scipy results differ in the 3rd decimal place from the paper. It is possible + the paper used an alternative finite difference approximation for the Jacobian. + """ + + def model(theta, t): + """ + Model to be fitted y = model(theta, t) + Arguments: + theta: vector of fitted parameters + t: independent variable [hours] + + Returns: + y: model predictions [need to check paper for units] + """ + asymptote = theta[0] + rate_constant = theta[1] + + return asymptote * (1 - np.exp(-rate_constant * t)) + + def residual(theta, t, y): + """ + Calculate residuals + Arguments: + theta: vector of fitted parameters + t: independent variable [hours] + y: dependent variable [?] + """ + return y - model(theta, t) + + # define data + t = self.data["hour"].to_numpy() + y = self.data["y"].to_numpy() + + # define initial guess + theta_guess = np.array([15, 0.5]) + + ## solve with optimize.least_squares + sol = scipy.optimize.least_squares( + residual, theta_guess, method="trf", args=(t, y), verbose=2 + ) + theta_hat = sol.x + + self.assertAlmostEqual( + theta_hat[0], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual(theta_hat[1], 0.5311, places=2) # 0.5311 from the paper + + # calculate residuals + r = residual(theta_hat, t, y) + + # calculate variance of the residuals + # -2 because there are 2 fitted parameters + sigre = np.matmul(r.T, r / (len(y) - 2)) + + # approximate covariance + # Need to divide by 2 because optimize.least_squares scaled the objective by 1/2 + cov = sigre * np.linalg.inv(np.matmul(sol.jac.T, sol.jac)) + + self.assertAlmostEqual(cov[0, 0], 6.22864, places=2) # 6.22864 from paper + self.assertAlmostEqual(cov[0, 1], -0.4322, places=2) # -0.4322 from paper + self.assertAlmostEqual(cov[1, 0], -0.4322, places=2) # -0.4322 from paper + self.assertAlmostEqual(cov[1, 1], 0.04124, places=2) # 0.04124 from paper + + def test_cov_scipy_curve_fit_comparison(self): + """ + Scipy results differ in the 3rd decimal place from the paper. It is possible + the paper used an alternative finite difference approximation for the Jacobian. + """ + + ## solve with optimize.curve_fit + def model(t, asymptote, rate_constant): + return asymptote * (1 - np.exp(-rate_constant * t)) + + # define data + t = self.data["hour"].to_numpy() + y = self.data["y"].to_numpy() + + # define initial guess + theta_guess = np.array([15, 0.5]) + + theta_hat, cov = scipy.optimize.curve_fit(model, t, y, p0=theta_guess) + + self.assertAlmostEqual( + theta_hat[0], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual(theta_hat[1], 0.5311, places=2) # 0.5311 from the paper + + self.assertAlmostEqual(cov[0, 0], 6.22864, places=2) # 6.22864 from paper + self.assertAlmostEqual(cov[0, 1], -0.4322, places=2) # -0.4322 from paper + self.assertAlmostEqual(cov[1, 0], -0.4322, places=2) # -0.4322 from paper + self.assertAlmostEqual(cov[1, 1], 0.04124, places=2) # 0.04124 from paper + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestModelVariants(unittest.TestCase): + + def setUp(self): + from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( + RooneyBieglerExperiment, + ) + + self.data = pd.DataFrame( + data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], + columns=["hour", "y"], + ) + + def rooney_biegler_params(data): + model = pyo.ConcreteModel() + + model.asymptote = pyo.Param(initialize=15, mutable=True) + model.rate_constant = pyo.Param(initialize=0.5, mutable=True) + + model.hour = pyo.Param(within=pyo.PositiveReals, mutable=True) + model.y = pyo.Param(within=pyo.PositiveReals, mutable=True) + + def response_rule(m, h): + expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) + return expr + + model.response_function = pyo.Expression(data.hour, rule=response_rule) + + return model + + class RooneyBieglerExperimentParams(RooneyBieglerExperiment): + + def create_model(self): + data_df = self.data.to_frame().transpose() + self.model = rooney_biegler_params(data_df) + + rooney_biegler_params_exp_list = [] + for i in range(self.data.shape[0]): + rooney_biegler_params_exp_list.append( + RooneyBieglerExperimentParams(self.data.loc[i, :]) + ) + + def rooney_biegler_indexed_params(data): + model = pyo.ConcreteModel() + + model.param_names = pyo.Set(initialize=["asymptote", "rate_constant"]) + model.theta = pyo.Param( + model.param_names, + initialize={"asymptote": 15, "rate_constant": 0.5}, + mutable=True, + ) + + model.hour = pyo.Param(within=pyo.PositiveReals, mutable=True) + model.y = pyo.Param(within=pyo.PositiveReals, mutable=True) + + def response_rule(m, h): + expr = m.theta["asymptote"] * ( + 1 - pyo.exp(-m.theta["rate_constant"] * h) + ) + return expr + + model.response_function = pyo.Expression(data.hour, rule=response_rule) + + return model + + class RooneyBieglerExperimentIndexedParams(RooneyBieglerExperiment): + + def create_model(self): + data_df = self.data.to_frame().transpose() + self.model = rooney_biegler_indexed_params(data_df) + + def label_model(self): + + m = self.model + + m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.experiment_outputs.update( + [(m.hour, self.data["hour"]), (m.y, self.data["y"])] + ) + + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.theta]) + + rooney_biegler_indexed_params_exp_list = [] + for i in range(self.data.shape[0]): + rooney_biegler_indexed_params_exp_list.append( + RooneyBieglerExperimentIndexedParams(self.data.loc[i, :]) + ) + + def rooney_biegler_vars(data): + model = pyo.ConcreteModel() + + model.asymptote = pyo.Var(initialize=15) + model.rate_constant = pyo.Var(initialize=0.5) + model.asymptote.fixed = True # parmest will unfix theta variables + model.rate_constant.fixed = True + + model.hour = pyo.Param(within=pyo.PositiveReals, mutable=True) + model.y = pyo.Param(within=pyo.PositiveReals, mutable=True) + + def response_rule(m, h): + expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) + return expr + + model.response_function = pyo.Expression(data.hour, rule=response_rule) + + return model + + class RooneyBieglerExperimentVars(RooneyBieglerExperiment): + + def create_model(self): + data_df = self.data.to_frame().transpose() + self.model = rooney_biegler_vars(data_df) + + rooney_biegler_vars_exp_list = [] + for i in range(self.data.shape[0]): + rooney_biegler_vars_exp_list.append( + RooneyBieglerExperimentVars(self.data.loc[i, :]) + ) + + def rooney_biegler_indexed_vars(data): + model = pyo.ConcreteModel() + + model.var_names = pyo.Set(initialize=["asymptote", "rate_constant"]) + model.theta = pyo.Var( + model.var_names, initialize={"asymptote": 15, "rate_constant": 0.5} + ) + model.theta["asymptote"].fixed = ( + True # parmest will unfix theta variables, even when they are indexed + ) + model.theta["rate_constant"].fixed = True + + model.hour = pyo.Param(within=pyo.PositiveReals, mutable=True) + model.y = pyo.Param(within=pyo.PositiveReals, mutable=True) + + def response_rule(m, h): + expr = m.theta["asymptote"] * ( + 1 - pyo.exp(-m.theta["rate_constant"] * h) + ) + return expr + + model.response_function = pyo.Expression(data.hour, rule=response_rule) + + return model + + class RooneyBieglerExperimentIndexedVars(RooneyBieglerExperiment): + + def create_model(self): + data_df = self.data.to_frame().transpose() + self.model = rooney_biegler_indexed_vars(data_df) + + def label_model(self): + + m = self.model + + m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.experiment_outputs.update( + [(m.hour, self.data["hour"]), (m.y, self.data["y"])] + ) + + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.theta]) + + rooney_biegler_indexed_vars_exp_list = [] + for i in range(self.data.shape[0]): + rooney_biegler_indexed_vars_exp_list.append( + RooneyBieglerExperimentIndexedVars(self.data.loc[i, :]) + ) + + # Sum of squared error function + def SSE(model): + expr = ( + model.experiment_outputs[model.y] + - model.response_function[model.experiment_outputs[model.hour]] + ) ** 2 + return expr + + self.objective_function = SSE + + theta_vals = pd.DataFrame([20, 1], index=["asymptote", "rate_constant"]).T + theta_vals_index = pd.DataFrame( + [20, 1], index=["theta['asymptote']", "theta['rate_constant']"] + ).T + + self.input = { + "param": { + "exp_list": rooney_biegler_params_exp_list, + "theta_names": ["asymptote", "rate_constant"], + "theta_vals": theta_vals, + }, + "param_index": { + "exp_list": rooney_biegler_indexed_params_exp_list, + "theta_names": ["theta"], + "theta_vals": theta_vals_index, + }, + "vars": { + "exp_list": rooney_biegler_vars_exp_list, + "theta_names": ["asymptote", "rate_constant"], + "theta_vals": theta_vals, + }, + "vars_index": { + "exp_list": rooney_biegler_indexed_vars_exp_list, + "theta_names": ["theta"], + "theta_vals": theta_vals_index, + }, + "vars_quoted_index": { + "exp_list": rooney_biegler_indexed_vars_exp_list, + "theta_names": ["theta['asymptote']", "theta['rate_constant']"], + "theta_vals": theta_vals_index, + }, + "vars_str_index": { + "exp_list": rooney_biegler_indexed_vars_exp_list, + "theta_names": ["theta[asymptote]", "theta[rate_constant]"], + "theta_vals": theta_vals_index, + }, + } + + @unittest.skipIf(not pynumero_ASL_available, "pynumero ASL is not available") + @unittest.skipIf( + not parmest.inverse_reduced_hessian_available, + "Cannot test covariance matrix: required ASL dependency is missing", + ) + def check_rooney_biegler_results(self, objval, cov): + + # get indices in covariance matrix + cov_cols = cov.columns.to_list() + asymptote_index = [idx for idx, s in enumerate(cov_cols) if "asymptote" in s][0] + rate_constant_index = [ + idx for idx, s in enumerate(cov_cols) if "rate_constant" in s + ][0] + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + cov.iloc[asymptote_index, asymptote_index], 6.30579403, places=2 + ) # 6.22864 from paper + self.assertAlmostEqual( + cov.iloc[asymptote_index, rate_constant_index], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[rate_constant_index, asymptote_index], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[rate_constant_index, rate_constant_index], 0.04193591, places=2 + ) # 0.04124 from paper + + def test_parmest_basics(self): + + for model_type, parmest_input in self.input.items(): + pest = parmest.Estimator( + parmest_input["exp_list"], obj_function=self.objective_function + ) + + objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) + self.check_rooney_biegler_results(objval, cov) + + obj_at_theta = pest.objective_at_theta(parmest_input["theta_vals"]) + self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) + + def test_parmest_basics_with_initialize_parmest_model_option(self): + + for model_type, parmest_input in self.input.items(): + pest = parmest.Estimator( + parmest_input["exp_list"], obj_function=self.objective_function + ) + + objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) + self.check_rooney_biegler_results(objval, cov) + + obj_at_theta = pest.objective_at_theta( + parmest_input["theta_vals"], initialize_parmest_model=True + ) + + self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) + + def test_parmest_basics_with_square_problem_solve(self): + + for model_type, parmest_input in self.input.items(): + pest = parmest.Estimator( + parmest_input["exp_list"], obj_function=self.objective_function + ) + + obj_at_theta = pest.objective_at_theta( + parmest_input["theta_vals"], initialize_parmest_model=True + ) + + objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) + self.check_rooney_biegler_results(objval, cov) + + self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) + + def test_parmest_basics_with_square_problem_solve_no_theta_vals(self): + + for model_type, parmest_input in self.input.items(): + + pest = parmest.Estimator( + parmest_input["exp_list"], obj_function=self.objective_function + ) + + obj_at_theta = pest.objective_at_theta(initialize_parmest_model=True) + + objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) + self.check_rooney_biegler_results(objval, cov) + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") +class TestReactorDesign(unittest.TestCase): + def setUp(self): + from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( + ReactorDesignExperiment, + ) + + # Data from the design + data = pd.DataFrame( + data=[ + [1.05, 10000, 3458.4, 1060.8, 1683.9, 1898.5], + [1.10, 10000, 3535.1, 1064.8, 1613.3, 1893.4], + [1.15, 10000, 3609.1, 1067.8, 1547.5, 1887.8], + [1.20, 10000, 3680.7, 1070.0, 1486.1, 1881.6], + [1.25, 10000, 3750.0, 1071.4, 1428.6, 1875.0], + [1.30, 10000, 3817.1, 1072.2, 1374.6, 1868.0], + [1.35, 10000, 3882.2, 1072.4, 1324.0, 1860.7], + [1.40, 10000, 3945.4, 1072.1, 1276.3, 1853.1], + [1.45, 10000, 4006.7, 1071.3, 1231.4, 1845.3], + [1.50, 10000, 4066.4, 1070.1, 1189.0, 1837.3], + [1.55, 10000, 4124.4, 1068.5, 1148.9, 1829.1], + [1.60, 10000, 4180.9, 1066.5, 1111.0, 1820.8], + [1.65, 10000, 4235.9, 1064.3, 1075.0, 1812.4], + [1.70, 10000, 4289.5, 1061.8, 1040.9, 1803.9], + [1.75, 10000, 4341.8, 1059.0, 1008.5, 1795.3], + [1.80, 10000, 4392.8, 1056.0, 977.7, 1786.7], + [1.85, 10000, 4442.6, 1052.8, 948.4, 1778.1], + [1.90, 10000, 4491.3, 1049.4, 920.5, 1769.4], + [1.95, 10000, 4538.8, 1045.8, 893.9, 1760.8], + ], + columns=["sv", "caf", "ca", "cb", "cc", "cd"], + ) + + # Create an experiment list + exp_list = [] + for i in range(data.shape[0]): + exp_list.append(ReactorDesignExperiment(data, i)) + + solver_options = {"max_iter": 6000} + + self.pest = parmest.Estimator( + exp_list, obj_function="SSE", solver_options=solver_options + ) + + def test_theta_est(self): + # used in data reconciliation + objval, thetavals = self.pest.theta_est() + + self.assertAlmostEqual(thetavals["k1"], 5.0 / 6.0, places=4) + self.assertAlmostEqual(thetavals["k2"], 5.0 / 3.0, places=4) + self.assertAlmostEqual(thetavals["k3"], 1.0 / 6000.0, places=7) + + def test_return_values(self): + objval, thetavals, data_rec = self.pest.theta_est( + return_values=["ca", "cb", "cc", "cd", "caf"] + ) + self.assertAlmostEqual(data_rec["cc"].loc[18], 893.84924, places=3) + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") +class TestReactorDesign_DAE(unittest.TestCase): + # Based on a reactor example in `Chemical Reactor Analysis and Design Fundamentals`, + # https://sites.engineering.ucsb.edu/~jbraw/chemreacfun/ + # https://sites.engineering.ucsb.edu/~jbraw/chemreacfun/fig-html/appendix/fig-A-10.html + + def setUp(self): + def ABC_model(data): + ca_meas = data["ca"] + cb_meas = data["cb"] + cc_meas = data["cc"] + + if isinstance(data, pd.DataFrame): + meas_t = data.index # time index + else: # dictionary + meas_t = list(ca_meas.keys()) # nested dictionary + + ca0 = 1.0 + cb0 = 0.0 + cc0 = 0.0 + + m = pyo.ConcreteModel() + + m.k1 = pyo.Var(initialize=0.5, bounds=(1e-4, 10)) + m.k2 = pyo.Var(initialize=3.0, bounds=(1e-4, 10)) + + m.time = dae.ContinuousSet(bounds=(0.0, 5.0), initialize=meas_t) + + # initialization and bounds + m.ca = pyo.Var(m.time, initialize=ca0, bounds=(-1e-3, ca0 + 1e-3)) + m.cb = pyo.Var(m.time, initialize=cb0, bounds=(-1e-3, ca0 + 1e-3)) + m.cc = pyo.Var(m.time, initialize=cc0, bounds=(-1e-3, ca0 + 1e-3)) + + m.dca = dae.DerivativeVar(m.ca, wrt=m.time) + m.dcb = dae.DerivativeVar(m.cb, wrt=m.time) + m.dcc = dae.DerivativeVar(m.cc, wrt=m.time) + + def _dcarate(m, t): + if t == 0: + return pyo.Constraint.Skip + else: + return m.dca[t] == -m.k1 * m.ca[t] + + m.dcarate = pyo.Constraint(m.time, rule=_dcarate) + + def _dcbrate(m, t): + if t == 0: + return pyo.Constraint.Skip + else: + return m.dcb[t] == m.k1 * m.ca[t] - m.k2 * m.cb[t] + + m.dcbrate = pyo.Constraint(m.time, rule=_dcbrate) + + def _dccrate(m, t): + if t == 0: + return pyo.Constraint.Skip + else: + return m.dcc[t] == m.k2 * m.cb[t] + + m.dccrate = pyo.Constraint(m.time, rule=_dccrate) + + def ComputeFirstStageCost_rule(m): + return 0 + + m.FirstStageCost = pyo.Expression(rule=ComputeFirstStageCost_rule) + + def ComputeSecondStageCost_rule(m): + return sum( + (m.ca[t] - ca_meas[t]) ** 2 + + (m.cb[t] - cb_meas[t]) ** 2 + + (m.cc[t] - cc_meas[t]) ** 2 + for t in meas_t + ) + + m.SecondStageCost = pyo.Expression(rule=ComputeSecondStageCost_rule) + + def total_cost_rule(model): + return model.FirstStageCost + model.SecondStageCost + + m.Total_Cost_Objective = pyo.Objective( + rule=total_cost_rule, sense=pyo.minimize + ) + + disc = pyo.TransformationFactory("dae.collocation") + disc.apply_to(m, nfe=20, ncp=2) + + return m + + class ReactorDesignExperimentDAE(Experiment): + + def __init__(self, data): + + self.data = data + self.model = None + + def create_model(self): + self.model = ABC_model(self.data) + + def label_model(self): + + m = self.model + + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.unknown_parameters.update( + (k, pyo.ComponentUID(k)) for k in [m.k1, m.k2] + ) + + def get_labeled_model(self): + self.create_model() + self.label_model() + + return self.model + + # This example tests data formatted in 3 ways + # Each format holds 1 scenario + # 1. dataframe with time index + # 2. nested dictionary {ca: {t, val pairs}, ... } + data = [ + [0.000, 0.957, -0.031, -0.015], + [0.263, 0.557, 0.330, 0.044], + [0.526, 0.342, 0.512, 0.156], + [0.789, 0.224, 0.499, 0.310], + [1.053, 0.123, 0.428, 0.454], + [1.316, 0.079, 0.396, 0.556], + [1.579, 0.035, 0.303, 0.651], + [1.842, 0.029, 0.287, 0.658], + [2.105, 0.025, 0.221, 0.750], + [2.368, 0.017, 0.148, 0.854], + [2.632, -0.002, 0.182, 0.845], + [2.895, 0.009, 0.116, 0.893], + [3.158, -0.023, 0.079, 0.942], + [3.421, 0.006, 0.078, 0.899], + [3.684, 0.016, 0.059, 0.942], + [3.947, 0.014, 0.036, 0.991], + [4.211, -0.009, 0.014, 0.988], + [4.474, -0.030, 0.036, 0.941], + [4.737, 0.004, 0.036, 0.971], + [5.000, -0.024, 0.028, 0.985], + ] + data = pd.DataFrame(data, columns=["t", "ca", "cb", "cc"]) + data_df = data.set_index("t") + data_dict = { + "ca": {k: v for (k, v) in zip(data.t, data.ca)}, + "cb": {k: v for (k, v) in zip(data.t, data.cb)}, + "cc": {k: v for (k, v) in zip(data.t, data.cc)}, + } + + # Create an experiment list + exp_list_df = [ReactorDesignExperimentDAE(data_df)] + exp_list_dict = [ReactorDesignExperimentDAE(data_dict)] + + self.pest_df = parmest.Estimator(exp_list_df) + self.pest_dict = parmest.Estimator(exp_list_dict) + + # Estimator object with multiple scenarios + exp_list_df_multiple = [ + ReactorDesignExperimentDAE(data_df), + ReactorDesignExperimentDAE(data_df), + ] + exp_list_dict_multiple = [ + ReactorDesignExperimentDAE(data_dict), + ReactorDesignExperimentDAE(data_dict), + ] + + self.pest_df_multiple = parmest.Estimator(exp_list_df_multiple) + self.pest_dict_multiple = parmest.Estimator(exp_list_dict_multiple) + + # Create an instance of the model + self.m_df = ABC_model(data_df) + self.m_dict = ABC_model(data_dict) + + def test_dataformats(self): + obj1, theta1 = self.pest_df.theta_est() + obj2, theta2 = self.pest_dict.theta_est() + + self.assertAlmostEqual(obj1, obj2, places=6) + self.assertAlmostEqual(theta1["k1"], theta2["k1"], places=6) + self.assertAlmostEqual(theta1["k2"], theta2["k2"], places=6) + + def test_return_continuous_set(self): + """ + test if ContinuousSet elements are returned correctly from theta_est() + """ + obj1, theta1, return_vals1 = self.pest_df.theta_est(return_values=["time"]) + obj2, theta2, return_vals2 = self.pest_dict.theta_est(return_values=["time"]) + self.assertAlmostEqual(return_vals1["time"].loc[0][18], 2.368, places=3) + self.assertAlmostEqual(return_vals2["time"].loc[0][18], 2.368, places=3) + + def test_return_continuous_set_multiple_datasets(self): + """ + test if ContinuousSet elements are returned correctly from theta_est() + """ + obj1, theta1, return_vals1 = self.pest_df_multiple.theta_est( + return_values=["time"] + ) + obj2, theta2, return_vals2 = self.pest_dict_multiple.theta_est( + return_values=["time"] + ) + self.assertAlmostEqual(return_vals1["time"].loc[1][18], 2.368, places=3) + self.assertAlmostEqual(return_vals2["time"].loc[1][18], 2.368, places=3) + + def test_covariance(self): + from pyomo.contrib.interior_point.inverse_reduced_hessian import ( + inv_reduced_hessian_barrier, + ) + + # Number of datapoints. + # 3 data components (ca, cb, cc), 20 timesteps, 1 scenario = 60 + # In this example, this is the number of data points in data_df, but that's + # only because the data is indexed by time and contains no additional information. + n = 60 + + # Compute covariance using parmest + obj, theta, cov = self.pest_df.theta_est(calc_cov=True, cov_n=n) + + # Compute covariance using interior_point + vars_list = [self.m_df.k1, self.m_df.k2] + solve_result, inv_red_hes = inv_reduced_hessian_barrier( + self.m_df, independent_variables=vars_list, tee=True + ) + l = len(vars_list) + cov_interior_point = 2 * obj / (n - l) * inv_red_hes + cov_interior_point = pd.DataFrame( + cov_interior_point, ["k1", "k2"], ["k1", "k2"] + ) + + cov_diff = (cov - cov_interior_point).abs().sum().sum() + + self.assertTrue(cov.loc["k1", "k1"] > 0) + self.assertTrue(cov.loc["k2", "k2"] > 0) + self.assertAlmostEqual(cov_diff, 0, places=6) + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestSquareInitialization_RooneyBiegler(unittest.TestCase): + def setUp(self): + from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler_with_constraint import ( + RooneyBieglerExperiment, + ) + + # Note, the data used in this test has been corrected to use data.loc[5,'hour'] = 7 (instead of 6) + data = pd.DataFrame( + data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], + columns=["hour", "y"], + ) + + # Sum of squared error function + def SSE(model): + expr = ( + model.experiment_outputs[model.y] + - model.response_function[model.experiment_outputs[model.hour]] + ) ** 2 + return expr + + exp_list = [] + for i in range(data.shape[0]): + exp_list.append(RooneyBieglerExperiment(data.loc[i, :])) + + solver_options = {"tol": 1e-8} + + self.data = data + self.pest = parmest.Estimator( + exp_list, obj_function=SSE, solver_options=solver_options, tee=True + ) + + def test_theta_est_with_square_initialization(self): + obj_init = self.pest.objective_at_theta(initialize_parmest_model=True) + objval, thetavals = self.pest.theta_est() + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + thetavals["asymptote"], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual( + thetavals["rate_constant"], 0.5311, places=2 + ) # 0.5311 from the paper + + def test_theta_est_with_square_initialization_and_custom_init_theta(self): + theta_vals_init = pd.DataFrame( + data=[[19.0, 0.5]], columns=["asymptote", "rate_constant"] + ) + obj_init = self.pest.objective_at_theta( + theta_values=theta_vals_init, initialize_parmest_model=True ) + objval, thetavals = self.pest.theta_est() + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + thetavals["asymptote"], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual( + thetavals["rate_constant"], 0.5311, places=2 + ) # 0.5311 from the paper + + def test_theta_est_with_square_initialization_diagnostic_mode_true(self): + self.pest.diagnostic_mode = True + obj_init = self.pest.objective_at_theta(initialize_parmest_model=True) + objval, thetavals = self.pest.theta_est() + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + thetavals["asymptote"], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual( + thetavals["rate_constant"], 0.5311, places=2 + ) # 0.5311 from the paper + + self.pest.diagnostic_mode = False + + +########################### +# tests for deprecated UI # +########################### + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestRooneyBieglerDeprecated(unittest.TestCase): + def setUp(self): + + def rooney_biegler_model(data): + model = pyo.ConcreteModel() + + model.asymptote = pyo.Var(initialize=15) + model.rate_constant = pyo.Var(initialize=0.5) + + def response_rule(m, h): + expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) + return expr + + model.response_function = pyo.Expression(data.hour, rule=response_rule) + + def SSE_rule(m): + return sum( + (data.y[i] - m.response_function[data.hour[i]]) ** 2 + for i in data.index + ) + + model.SSE = pyo.Objective(rule=SSE_rule, sense=pyo.minimize) + + return model # Note, the data used in this test has been corrected to use data.loc[5,'hour'] = 7 (instead of 6) data = pd.DataFrame( data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], - columns=['hour', 'y'], + columns=["hour", "y"], ) - theta_names = ['asymptote', 'rate_constant'] + theta_names = ["asymptote", "rate_constant"] def SSE(model, data): expr = sum( @@ -73,7 +1085,7 @@ def SSE(model, data): ) return expr - solver_options = {'tol': 1e-8} + solver_options = {"tol": 1e-8} self.data = data self.pest = parmest.Estimator( @@ -90,10 +1102,10 @@ def test_theta_est(self): self.assertAlmostEqual(objval, 4.3317112, places=2) self.assertAlmostEqual( - thetavals['asymptote'], 19.1426, places=2 + thetavals["asymptote"], 19.1426, places=2 ) # 19.1426 from the paper self.assertAlmostEqual( - thetavals['rate_constant'], 0.5311, places=2 + thetavals["rate_constant"], 0.5311, places=2 ) # 0.5311 from the paper @unittest.skipIf( @@ -105,14 +1117,14 @@ def test_bootstrap(self): num_bootstraps = 10 theta_est = self.pest.theta_est_bootstrap(num_bootstraps, return_samples=True) - num_samples = theta_est['samples'].apply(len) + num_samples = theta_est["samples"].apply(len) self.assertTrue(len(theta_est.index), 10) self.assertTrue(num_samples.equals(pd.Series([6] * 10))) - del theta_est['samples'] + del theta_est["samples"] # apply confidence region test - CR = self.pest.confidence_region_test(theta_est, 'MVN', [0.5, 0.75, 1.0]) + CR = self.pest.confidence_region_test(theta_est, "MVN", [0.5, 0.75, 1.0]) self.assertTrue(set(CR.columns) >= set([0.5, 0.75, 1.0])) self.assertTrue(CR[0.5].sum() == 5) @@ -121,7 +1133,7 @@ def test_bootstrap(self): graphics.pairwise_plot(theta_est) graphics.pairwise_plot(theta_est, thetavals) - graphics.pairwise_plot(theta_est, thetavals, 0.8, ['MVN', 'KDE', 'Rect']) + graphics.pairwise_plot(theta_est, thetavals, 0.8, ["MVN", "KDE", "Rect"]) @unittest.skipIf( not graphics.imports_available, "parmest.graphics imports are unavailable" @@ -132,7 +1144,7 @@ def test_likelihood_ratio(self): asym = np.arange(10, 30, 2) rate = np.arange(0, 1.5, 0.25) theta_vals = pd.DataFrame( - list(product(asym, rate)), columns=self.pest.theta_names + list(product(asym, rate)), columns=self.pest._return_theta_names() ) obj_at_theta = self.pest.objective_at_theta(theta_vals) @@ -151,7 +1163,7 @@ def test_leaveNout(self): self.assertTrue(lNo_theta.shape == (6, 2)) results = self.pest.leaveNout_bootstrap_test( - 1, None, 3, 'Rect', [0.5, 1.0], seed=5436 + 1, None, 3, "Rect", [0.5, 1.0], seed=5436 ) self.assertTrue(len(results) == 6) # 6 lNo samples i = 1 @@ -173,7 +1185,7 @@ def test_diagnostic_mode(self): asym = np.arange(10, 30, 2) rate = np.arange(0, 1.5, 0.25) theta_vals = pd.DataFrame( - list(product(asym, rate)), columns=self.pest.theta_names + list(product(asym, rate)), columns=self.pest._return_theta_names() ) obj_at_theta = self.pest.objective_at_theta(theta_vals) @@ -221,10 +1233,10 @@ def test_theta_est_cov(self): self.assertAlmostEqual(objval, 4.3317112, places=2) self.assertAlmostEqual( - thetavals['asymptote'], 19.1426, places=2 + thetavals["asymptote"], 19.1426, places=2 ) # 19.1426 from the paper self.assertAlmostEqual( - thetavals['rate_constant'], 0.5311, places=2 + thetavals["rate_constant"], 0.5311, places=2 ) # 0.5311 from the paper # Covariance matrix @@ -239,22 +1251,22 @@ def test_theta_est_cov(self): ) # -0.4322 from paper self.assertAlmostEqual(cov.iloc[1, 1], 0.04124, places=2) # 0.04124 from paper - ''' Why does the covariance matrix from parmest not match the paper? Parmest is + """ Why does the covariance matrix from parmest not match the paper? Parmest is calculating the exact reduced Hessian. The paper (Rooney and Bielger, 2001) likely employed the first order approximation common for nonlinear regression. The paper values were verified with Scipy, which uses the same first order approximation. The formula used in parmest was verified against equations (7-5-15) and (7-5-16) in "Nonlinear Parameter Estimation", Y. Bard, 1974. - ''' + """ def test_cov_scipy_least_squares_comparison(self): - ''' + """ Scipy results differ in the 3rd decimal place from the paper. It is possible the paper used an alternative finite difference approximation for the Jacobian. - ''' + """ def model(theta, t): - ''' + """ Model to be fitted y = model(theta, t) Arguments: theta: vector of fitted parameters @@ -262,32 +1274,32 @@ def model(theta, t): Returns: y: model predictions [need to check paper for units] - ''' + """ asymptote = theta[0] rate_constant = theta[1] return asymptote * (1 - np.exp(-rate_constant * t)) def residual(theta, t, y): - ''' + """ Calculate residuals Arguments: theta: vector of fitted parameters t: independent variable [hours] y: dependent variable [?] - ''' + """ return y - model(theta, t) # define data - t = self.data['hour'].to_numpy() - y = self.data['y'].to_numpy() + t = self.data["hour"].to_numpy() + y = self.data["y"].to_numpy() # define initial guess theta_guess = np.array([15, 0.5]) ## solve with optimize.least_squares sol = scipy.optimize.least_squares( - residual, theta_guess, method='trf', args=(t, y), verbose=2 + residual, theta_guess, method="trf", args=(t, y), verbose=2 ) theta_hat = sol.x @@ -313,18 +1325,18 @@ def residual(theta, t, y): self.assertAlmostEqual(cov[1, 1], 0.04124, places=2) # 0.04124 from paper def test_cov_scipy_curve_fit_comparison(self): - ''' + """ Scipy results differ in the 3rd decimal place from the paper. It is possible the paper used an alternative finite difference approximation for the Jacobian. - ''' + """ ## solve with optimize.curve_fit def model(t, asymptote, rate_constant): return asymptote * (1 - np.exp(-rate_constant * t)) # define data - t = self.data['hour'].to_numpy() - y = self.data['y'].to_numpy() + t = self.data["hour"].to_numpy() + y = self.data["y"].to_numpy() # define initial guess theta_guess = np.array([15, 0.5]) @@ -347,11 +1359,11 @@ def model(t, asymptote, rate_constant): "Cannot test parmest: required dependencies are missing", ) @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") -class TestModelVariants(unittest.TestCase): +class TestModelVariantsDeprecated(unittest.TestCase): def setUp(self): self.data = pd.DataFrame( data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], - columns=['hour', 'y'], + columns=["hour", "y"], ) def rooney_biegler_params(data): @@ -371,16 +1383,16 @@ def response_rule(m, h): def rooney_biegler_indexed_params(data): model = pyo.ConcreteModel() - model.param_names = pyo.Set(initialize=['asymptote', 'rate_constant']) + model.param_names = pyo.Set(initialize=["asymptote", "rate_constant"]) model.theta = pyo.Param( model.param_names, - initialize={'asymptote': 15, 'rate_constant': 0.5}, + initialize={"asymptote": 15, "rate_constant": 0.5}, mutable=True, ) def response_rule(m, h): - expr = m.theta['asymptote'] * ( - 1 - pyo.exp(-m.theta['rate_constant'] * h) + expr = m.theta["asymptote"] * ( + 1 - pyo.exp(-m.theta["rate_constant"] * h) ) return expr @@ -407,20 +1419,18 @@ def response_rule(m, h): def rooney_biegler_indexed_vars(data): model = pyo.ConcreteModel() - model.var_names = pyo.Set(initialize=['asymptote', 'rate_constant']) + model.var_names = pyo.Set(initialize=["asymptote", "rate_constant"]) model.theta = pyo.Var( - model.var_names, initialize={'asymptote': 15, 'rate_constant': 0.5} + model.var_names, initialize={"asymptote": 15, "rate_constant": 0.5} ) - model.theta[ - 'asymptote' - ].fixed = ( + model.theta["asymptote"].fixed = ( True # parmest will unfix theta variables, even when they are indexed ) - model.theta['rate_constant'].fixed = True + model.theta["rate_constant"].fixed = True def response_rule(m, h): - expr = m.theta['asymptote'] * ( - 1 - pyo.exp(-m.theta['rate_constant'] * h) + expr = m.theta["asymptote"] * ( + 1 - pyo.exp(-m.theta["rate_constant"] * h) ) return expr @@ -437,41 +1447,41 @@ def SSE(model, data): self.objective_function = SSE - theta_vals = pd.DataFrame([20, 1], index=['asymptote', 'rate_constant']).T + theta_vals = pd.DataFrame([20, 1], index=["asymptote", "rate_constant"]).T theta_vals_index = pd.DataFrame( [20, 1], index=["theta['asymptote']", "theta['rate_constant']"] ).T self.input = { - 'param': { - 'model': rooney_biegler_params, - 'theta_names': ['asymptote', 'rate_constant'], - 'theta_vals': theta_vals, + "param": { + "model": rooney_biegler_params, + "theta_names": ["asymptote", "rate_constant"], + "theta_vals": theta_vals, }, - 'param_index': { - 'model': rooney_biegler_indexed_params, - 'theta_names': ['theta'], - 'theta_vals': theta_vals_index, + "param_index": { + "model": rooney_biegler_indexed_params, + "theta_names": ["theta"], + "theta_vals": theta_vals_index, }, - 'vars': { - 'model': rooney_biegler_vars, - 'theta_names': ['asymptote', 'rate_constant'], - 'theta_vals': theta_vals, + "vars": { + "model": rooney_biegler_vars, + "theta_names": ["asymptote", "rate_constant"], + "theta_vals": theta_vals, }, - 'vars_index': { - 'model': rooney_biegler_indexed_vars, - 'theta_names': ['theta'], - 'theta_vals': theta_vals_index, + "vars_index": { + "model": rooney_biegler_indexed_vars, + "theta_names": ["theta"], + "theta_vals": theta_vals_index, }, - 'vars_quoted_index': { - 'model': rooney_biegler_indexed_vars, - 'theta_names': ["theta['asymptote']", "theta['rate_constant']"], - 'theta_vals': theta_vals_index, + "vars_quoted_index": { + "model": rooney_biegler_indexed_vars, + "theta_names": ["theta['asymptote']", "theta['rate_constant']"], + "theta_vals": theta_vals_index, }, - 'vars_str_index': { - 'model': rooney_biegler_indexed_vars, - 'theta_names': ["theta[asymptote]", "theta[rate_constant]"], - 'theta_vals': theta_vals_index, + "vars_str_index": { + "model": rooney_biegler_indexed_vars, + "theta_names": ["theta[asymptote]", "theta[rate_constant]"], + "theta_vals": theta_vals_index, }, } @@ -483,9 +1493,9 @@ def SSE(model, data): def test_parmest_basics(self): for model_type, parmest_input in self.input.items(): pest = parmest.Estimator( - parmest_input['model'], + parmest_input["model"], self.data, - parmest_input['theta_names'], + parmest_input["theta_names"], self.objective_function, ) @@ -505,15 +1515,15 @@ def test_parmest_basics(self): cov.iloc[1, 1], 0.04193591, places=2 ) # 0.04124 from paper - obj_at_theta = pest.objective_at_theta(parmest_input['theta_vals']) - self.assertAlmostEqual(obj_at_theta['obj'][0], 16.531953, places=2) + obj_at_theta = pest.objective_at_theta(parmest_input["theta_vals"]) + self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) def test_parmest_basics_with_initialize_parmest_model_option(self): for model_type, parmest_input in self.input.items(): pest = parmest.Estimator( - parmest_input['model'], + parmest_input["model"], self.data, - parmest_input['theta_names'], + parmest_input["theta_names"], self.objective_function, ) @@ -534,22 +1544,22 @@ def test_parmest_basics_with_initialize_parmest_model_option(self): ) # 0.04124 from paper obj_at_theta = pest.objective_at_theta( - parmest_input['theta_vals'], initialize_parmest_model=True + parmest_input["theta_vals"], initialize_parmest_model=True ) - self.assertAlmostEqual(obj_at_theta['obj'][0], 16.531953, places=2) + self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) def test_parmest_basics_with_square_problem_solve(self): for model_type, parmest_input in self.input.items(): pest = parmest.Estimator( - parmest_input['model'], + parmest_input["model"], self.data, - parmest_input['theta_names'], + parmest_input["theta_names"], self.objective_function, ) obj_at_theta = pest.objective_at_theta( - parmest_input['theta_vals'], initialize_parmest_model=True + parmest_input["theta_vals"], initialize_parmest_model=True ) objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) @@ -568,14 +1578,14 @@ def test_parmest_basics_with_square_problem_solve(self): cov.iloc[1, 1], 0.04193591, places=2 ) # 0.04124 from paper - self.assertAlmostEqual(obj_at_theta['obj'][0], 16.531953, places=2) + self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) def test_parmest_basics_with_square_problem_solve_no_theta_vals(self): for model_type, parmest_input in self.input.items(): pest = parmest.Estimator( - parmest_input['model'], + parmest_input["model"], self.data, - parmest_input['theta_names'], + parmest_input["theta_names"], self.objective_function, ) @@ -603,11 +1613,84 @@ def test_parmest_basics_with_square_problem_solve_no_theta_vals(self): "Cannot test parmest: required dependencies are missing", ) @unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") -class TestReactorDesign(unittest.TestCase): +class TestReactorDesignDeprecated(unittest.TestCase): def setUp(self): - from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( - reactor_design_model, - ) + + def reactor_design_model(data): + # Create the concrete model + model = pyo.ConcreteModel() + + # Rate constants + model.k1 = pyo.Param( + initialize=5.0 / 6.0, within=pyo.PositiveReals, mutable=True + ) # min^-1 + model.k2 = pyo.Param( + initialize=5.0 / 3.0, within=pyo.PositiveReals, mutable=True + ) # min^-1 + model.k3 = pyo.Param( + initialize=1.0 / 6000.0, within=pyo.PositiveReals, mutable=True + ) # m^3/(gmol min) + + # Inlet concentration of A, gmol/m^3 + if isinstance(data, dict) or isinstance(data, pd.Series): + model.caf = pyo.Param( + initialize=float(data["caf"]), within=pyo.PositiveReals + ) + elif isinstance(data, pd.DataFrame): + model.caf = pyo.Param( + initialize=float(data.iloc[0]["caf"]), within=pyo.PositiveReals + ) + else: + raise ValueError("Unrecognized data type.") + + # Space velocity (flowrate/volume) + if isinstance(data, dict) or isinstance(data, pd.Series): + model.sv = pyo.Param( + initialize=float(data["sv"]), within=pyo.PositiveReals + ) + elif isinstance(data, pd.DataFrame): + model.sv = pyo.Param( + initialize=float(data.iloc[0]["sv"]), within=pyo.PositiveReals + ) + else: + raise ValueError("Unrecognized data type.") + + # Outlet concentration of each component + model.ca = pyo.Var(initialize=5000.0, within=pyo.PositiveReals) + model.cb = pyo.Var(initialize=2000.0, within=pyo.PositiveReals) + model.cc = pyo.Var(initialize=2000.0, within=pyo.PositiveReals) + model.cd = pyo.Var(initialize=1000.0, within=pyo.PositiveReals) + + # Objective + model.obj = pyo.Objective(expr=model.cb, sense=pyo.maximize) + + # Constraints + model.ca_bal = pyo.Constraint( + expr=( + 0 + == model.sv * model.caf + - model.sv * model.ca + - model.k1 * model.ca + - 2.0 * model.k3 * model.ca**2.0 + ) + ) + + model.cb_bal = pyo.Constraint( + expr=( + 0 + == -model.sv * model.cb + model.k1 * model.ca - model.k2 * model.cb + ) + ) + + model.cc_bal = pyo.Constraint( + expr=(0 == -model.sv * model.cc + model.k2 * model.cb) + ) + + model.cd_bal = pyo.Constraint( + expr=(0 == -model.sv * model.cd + model.k3 * model.ca**2.0) + ) + + return model # Data from the design data = pd.DataFrame( @@ -632,17 +1715,17 @@ def setUp(self): [1.90, 10000, 4491.3, 1049.4, 920.5, 1769.4], [1.95, 10000, 4538.8, 1045.8, 893.9, 1760.8], ], - columns=['sv', 'caf', 'ca', 'cb', 'cc', 'cd'], + columns=["sv", "caf", "ca", "cb", "cc", "cd"], ) - theta_names = ['k1', 'k2', 'k3'] + theta_names = ["k1", "k2", "k3"] def SSE(model, data): expr = ( - (float(data['ca']) - model.ca) ** 2 - + (float(data['cb']) - model.cb) ** 2 - + (float(data['cc']) - model.cc) ** 2 - + (float(data['cd']) - model.cd) ** 2 + (float(data.iloc[0]["ca"]) - model.ca) ** 2 + + (float(data.iloc[0]["cb"]) - model.cb) ** 2 + + (float(data.iloc[0]["cc"]) - model.cc) ** 2 + + (float(data.iloc[0]["cd"]) - model.cd) ** 2 ) return expr @@ -656,13 +1739,13 @@ def test_theta_est(self): # used in data reconciliation objval, thetavals = self.pest.theta_est() - self.assertAlmostEqual(thetavals['k1'], 5.0 / 6.0, places=4) - self.assertAlmostEqual(thetavals['k2'], 5.0 / 3.0, places=4) - self.assertAlmostEqual(thetavals['k3'], 1.0 / 6000.0, places=7) + self.assertAlmostEqual(thetavals["k1"], 5.0 / 6.0, places=4) + self.assertAlmostEqual(thetavals["k2"], 5.0 / 3.0, places=4) + self.assertAlmostEqual(thetavals["k3"], 1.0 / 6000.0, places=7) def test_return_values(self): objval, thetavals, data_rec = self.pest.theta_est( - return_values=['ca', 'cb', 'cc', 'cd', 'caf'] + return_values=["ca", "cb", "cc", "cd", "caf"] ) self.assertAlmostEqual(data_rec["cc"].loc[18], 893.84924, places=3) @@ -672,16 +1755,16 @@ def test_return_values(self): "Cannot test parmest: required dependencies are missing", ) @unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") -class TestReactorDesign_DAE(unittest.TestCase): +class TestReactorDesign_DAE_Deprecated(unittest.TestCase): # Based on a reactor example in `Chemical Reactor Analysis and Design Fundamentals`, # https://sites.engineering.ucsb.edu/~jbraw/chemreacfun/ # https://sites.engineering.ucsb.edu/~jbraw/chemreacfun/fig-html/appendix/fig-A-10.html def setUp(self): def ABC_model(data): - ca_meas = data['ca'] - cb_meas = data['cb'] - cc_meas = data['cc'] + ca_meas = data["ca"] + cb_meas = data["cb"] + cc_meas = data["cc"] if isinstance(data, pd.DataFrame): meas_t = data.index # time index @@ -754,7 +1837,7 @@ def total_cost_rule(model): rule=total_cost_rule, sense=pyo.minimize ) - disc = pyo.TransformationFactory('dae.collocation') + disc = pyo.TransformationFactory("dae.collocation") disc.apply_to(m, nfe=20, ncp=2) return m @@ -785,15 +1868,15 @@ def total_cost_rule(model): [4.737, 0.004, 0.036, 0.971], [5.000, -0.024, 0.028, 0.985], ] - data = pd.DataFrame(data, columns=['t', 'ca', 'cb', 'cc']) - data_df = data.set_index('t') + data = pd.DataFrame(data, columns=["t", "ca", "cb", "cc"]) + data_df = data.set_index("t") data_dict = { - 'ca': {k: v for (k, v) in zip(data.t, data.ca)}, - 'cb': {k: v for (k, v) in zip(data.t, data.cb)}, - 'cc': {k: v for (k, v) in zip(data.t, data.cc)}, + "ca": {k: v for (k, v) in zip(data.t, data.ca)}, + "cb": {k: v for (k, v) in zip(data.t, data.cb)}, + "cc": {k: v for (k, v) in zip(data.t, data.cc)}, } - theta_names = ['k1', 'k2'] + theta_names = ["k1", "k2"] self.pest_df = parmest.Estimator(ABC_model, [data_df], theta_names) self.pest_dict = parmest.Estimator(ABC_model, [data_dict], theta_names) @@ -815,30 +1898,30 @@ def test_dataformats(self): obj2, theta2 = self.pest_dict.theta_est() self.assertAlmostEqual(obj1, obj2, places=6) - self.assertAlmostEqual(theta1['k1'], theta2['k1'], places=6) - self.assertAlmostEqual(theta1['k2'], theta2['k2'], places=6) + self.assertAlmostEqual(theta1["k1"], theta2["k1"], places=6) + self.assertAlmostEqual(theta1["k2"], theta2["k2"], places=6) def test_return_continuous_set(self): - ''' + """ test if ContinuousSet elements are returned correctly from theta_est() - ''' - obj1, theta1, return_vals1 = self.pest_df.theta_est(return_values=['time']) - obj2, theta2, return_vals2 = self.pest_dict.theta_est(return_values=['time']) - self.assertAlmostEqual(return_vals1['time'].loc[0][18], 2.368, places=3) - self.assertAlmostEqual(return_vals2['time'].loc[0][18], 2.368, places=3) + """ + obj1, theta1, return_vals1 = self.pest_df.theta_est(return_values=["time"]) + obj2, theta2, return_vals2 = self.pest_dict.theta_est(return_values=["time"]) + self.assertAlmostEqual(return_vals1["time"].loc[0][18], 2.368, places=3) + self.assertAlmostEqual(return_vals2["time"].loc[0][18], 2.368, places=3) def test_return_continuous_set_multiple_datasets(self): - ''' + """ test if ContinuousSet elements are returned correctly from theta_est() - ''' + """ obj1, theta1, return_vals1 = self.pest_df_multiple.theta_est( - return_values=['time'] + return_values=["time"] ) obj2, theta2, return_vals2 = self.pest_dict_multiple.theta_est( - return_values=['time'] + return_values=["time"] ) - self.assertAlmostEqual(return_vals1['time'].loc[1][18], 2.368, places=3) - self.assertAlmostEqual(return_vals2['time'].loc[1][18], 2.368, places=3) + self.assertAlmostEqual(return_vals1["time"].loc[1][18], 2.368, places=3) + self.assertAlmostEqual(return_vals2["time"].loc[1][18], 2.368, places=3) def test_covariance(self): from pyomo.contrib.interior_point.inverse_reduced_hessian import ( @@ -862,13 +1945,13 @@ def test_covariance(self): l = len(vars_list) cov_interior_point = 2 * obj / (n - l) * inv_red_hes cov_interior_point = pd.DataFrame( - cov_interior_point, ['k1', 'k2'], ['k1', 'k2'] + cov_interior_point, ["k1", "k2"], ["k1", "k2"] ) cov_diff = (cov - cov_interior_point).abs().sum().sum() - self.assertTrue(cov.loc['k1', 'k1'] > 0) - self.assertTrue(cov.loc['k2', 'k2'] > 0) + self.assertTrue(cov.loc["k1", "k1"] > 0) + self.assertTrue(cov.loc["k2", "k2"] > 0) self.assertAlmostEqual(cov_diff, 0, places=6) @@ -877,19 +1960,43 @@ def test_covariance(self): "Cannot test parmest: required dependencies are missing", ) @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") -class TestSquareInitialization_RooneyBiegler(unittest.TestCase): +class TestSquareInitialization_RooneyBiegler_Deprecated(unittest.TestCase): def setUp(self): - from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler_with_constraint import ( - rooney_biegler_model_with_constraint, - ) + + def rooney_biegler_model_with_constraint(data): + model = pyo.ConcreteModel() + + model.asymptote = pyo.Var(initialize=15) + model.rate_constant = pyo.Var(initialize=0.5) + model.response_function = pyo.Var(data.hour, initialize=0.0) + + # changed from expression to constraint + def response_rule(m, h): + return m.response_function[h] == m.asymptote * ( + 1 - pyo.exp(-m.rate_constant * h) + ) + + model.response_function_constraint = pyo.Constraint( + data.hour, rule=response_rule + ) + + def SSE_rule(m): + return sum( + (data.y[i] - m.response_function[data.hour[i]]) ** 2 + for i in data.index + ) + + model.SSE = pyo.Objective(rule=SSE_rule, sense=pyo.minimize) + + return model # Note, the data used in this test has been corrected to use data.loc[5,'hour'] = 7 (instead of 6) data = pd.DataFrame( data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], - columns=['hour', 'y'], + columns=["hour", "y"], ) - theta_names = ['asymptote', 'rate_constant'] + theta_names = ["asymptote", "rate_constant"] def SSE(model, data): expr = sum( @@ -898,7 +2005,7 @@ def SSE(model, data): ) return expr - solver_options = {'tol': 1e-8} + solver_options = {"tol": 1e-8} self.data = data self.pest = parmest.Estimator( @@ -916,15 +2023,15 @@ def test_theta_est_with_square_initialization(self): self.assertAlmostEqual(objval, 4.3317112, places=2) self.assertAlmostEqual( - thetavals['asymptote'], 19.1426, places=2 + thetavals["asymptote"], 19.1426, places=2 ) # 19.1426 from the paper self.assertAlmostEqual( - thetavals['rate_constant'], 0.5311, places=2 + thetavals["rate_constant"], 0.5311, places=2 ) # 0.5311 from the paper def test_theta_est_with_square_initialization_and_custom_init_theta(self): theta_vals_init = pd.DataFrame( - data=[[19.0, 0.5]], columns=['asymptote', 'rate_constant'] + data=[[19.0, 0.5]], columns=["asymptote", "rate_constant"] ) obj_init = self.pest.objective_at_theta( theta_values=theta_vals_init, initialize_parmest_model=True @@ -932,10 +2039,10 @@ def test_theta_est_with_square_initialization_and_custom_init_theta(self): objval, thetavals = self.pest.theta_est() self.assertAlmostEqual(objval, 4.3317112, places=2) self.assertAlmostEqual( - thetavals['asymptote'], 19.1426, places=2 + thetavals["asymptote"], 19.1426, places=2 ) # 19.1426 from the paper self.assertAlmostEqual( - thetavals['rate_constant'], 0.5311, places=2 + thetavals["rate_constant"], 0.5311, places=2 ) # 0.5311 from the paper def test_theta_est_with_square_initialization_diagnostic_mode_true(self): @@ -945,14 +2052,14 @@ def test_theta_est_with_square_initialization_diagnostic_mode_true(self): self.assertAlmostEqual(objval, 4.3317112, places=2) self.assertAlmostEqual( - thetavals['asymptote'], 19.1426, places=2 + thetavals["asymptote"], 19.1426, places=2 ) # 19.1426 from the paper self.assertAlmostEqual( - thetavals['rate_constant'], 0.5311, places=2 + thetavals["rate_constant"], 0.5311, places=2 ) # 0.5311 from the paper self.pest.diagnostic_mode = False -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/parmest/tests/test_scenariocreator.py b/pyomo/contrib/parmest/tests/test_scenariocreator.py index a2dcf4c2739..af755e34b67 100644 --- a/pyomo/contrib/parmest/tests/test_scenariocreator.py +++ b/pyomo/contrib/parmest/tests/test_scenariocreator.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -24,7 +24,7 @@ import pyomo.environ as pyo from pyomo.environ import SolverFactory -ipopt_available = SolverFactory('ipopt').available() +ipopt_available = SolverFactory("ipopt").available() testdir = os.path.dirname(os.path.abspath(__file__)) @@ -37,7 +37,7 @@ class TestScenarioReactorDesign(unittest.TestCase): def setUp(self): from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( - reactor_design_model, + ReactorDesignExperiment, ) # Data from the design @@ -63,17 +63,204 @@ def setUp(self): [1.90, 10000, 4491.3, 1049.4, 920.5, 1769.4], [1.95, 10000, 4538.8, 1045.8, 893.9, 1760.8], ], - columns=['sv', 'caf', 'ca', 'cb', 'cc', 'cd'], + columns=["sv", "caf", "ca", "cb", "cc", "cd"], ) - theta_names = ['k1', 'k2', 'k3'] + # Create an experiment list + exp_list = [] + for i in range(data.shape[0]): + exp_list.append(ReactorDesignExperiment(data, i)) + + self.pest = parmest.Estimator(exp_list, obj_function='SSE') + + def test_scen_from_exps(self): + scenmaker = sc.ScenarioCreator(self.pest, "ipopt") + experimentscens = sc.ScenarioSet("Experiments") + scenmaker.ScenariosFromExperiments(experimentscens) + experimentscens.write_csv("delme_exp_csv.csv") + df = pd.read_csv("delme_exp_csv.csv") + os.remove("delme_exp_csv.csv") + # March '20: all reactor_design experiments have the same theta values! + k1val = df.loc[5].at["k1"] + self.assertAlmostEqual(k1val, 5.0 / 6.0, places=2) + tval = experimentscens.ScenarioNumber(0).ThetaVals["k1"] + self.assertAlmostEqual(tval, 5.0 / 6.0, places=2) + + @unittest.skipIf(not uuid_available, "The uuid module is not available") + def test_no_csv_if_empty(self): + # low level test of scenario sets + # verify that nothing is written, but no errors with empty set + + emptyset = sc.ScenarioSet("empty") + tfile = uuid.uuid4().hex + ".csv" + emptyset.write_csv(tfile) + self.assertFalse( + os.path.exists(tfile), "ScenarioSet wrote csv in spite of empty set" + ) + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestScenarioSemibatch(unittest.TestCase): + def setUp(self): + import pyomo.contrib.parmest.examples.semibatch.semibatch as sb + import json + + self.fbase = os.path.join(testdir, "..", "examples", "semibatch") + # Data, list of dictionaries + data = [] + for exp_num in range(10): + fname = "exp" + str(exp_num + 1) + ".out" + fullname = os.path.join(self.fbase, fname) + with open(fullname, "r") as infile: + d = json.load(infile) + data.append(d) + + # Note, the model already includes a 'SecondStageCost' expression + # for the sum of squared error that will be used in parameter estimation + + # Create an experiment list + exp_list = [] + for i in range(len(data)): + exp_list.append(sb.SemiBatchExperiment(data[i])) + + self.pest = parmest.Estimator(exp_list) + + def test_semibatch_bootstrap(self): + scenmaker = sc.ScenarioCreator(self.pest, "ipopt") + bootscens = sc.ScenarioSet("Bootstrap") + numtomake = 2 + scenmaker.ScenariosFromBootstrap(bootscens, numtomake, seed=1134) + tval = bootscens.ScenarioNumber(0).ThetaVals["k1"] + self.assertAlmostEqual(tval, 20.64, places=1) + + +########################### +# tests for deprecated UI # +########################### + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestScenarioReactorDesignDeprecated(unittest.TestCase): + def setUp(self): + + def reactor_design_model(data): + # Create the concrete model + model = pyo.ConcreteModel() + + # Rate constants + model.k1 = pyo.Param( + initialize=5.0 / 6.0, within=pyo.PositiveReals, mutable=True + ) # min^-1 + model.k2 = pyo.Param( + initialize=5.0 / 3.0, within=pyo.PositiveReals, mutable=True + ) # min^-1 + model.k3 = pyo.Param( + initialize=1.0 / 6000.0, within=pyo.PositiveReals, mutable=True + ) # m^3/(gmol min) + + # Inlet concentration of A, gmol/m^3 + if isinstance(data, dict) or isinstance(data, pd.Series): + model.caf = pyo.Param( + initialize=float(data["caf"]), within=pyo.PositiveReals + ) + elif isinstance(data, pd.DataFrame): + model.caf = pyo.Param( + initialize=float(data.iloc[0]["caf"]), within=pyo.PositiveReals + ) + else: + raise ValueError("Unrecognized data type.") + + # Space velocity (flowrate/volume) + if isinstance(data, dict) or isinstance(data, pd.Series): + model.sv = pyo.Param( + initialize=float(data["sv"]), within=pyo.PositiveReals + ) + elif isinstance(data, pd.DataFrame): + model.sv = pyo.Param( + initialize=float(data.iloc[0]["sv"]), within=pyo.PositiveReals + ) + else: + raise ValueError("Unrecognized data type.") + + # Outlet concentration of each component + model.ca = pyo.Var(initialize=5000.0, within=pyo.PositiveReals) + model.cb = pyo.Var(initialize=2000.0, within=pyo.PositiveReals) + model.cc = pyo.Var(initialize=2000.0, within=pyo.PositiveReals) + model.cd = pyo.Var(initialize=1000.0, within=pyo.PositiveReals) + + # Objective + model.obj = pyo.Objective(expr=model.cb, sense=pyo.maximize) + + # Constraints + model.ca_bal = pyo.Constraint( + expr=( + 0 + == model.sv * model.caf + - model.sv * model.ca + - model.k1 * model.ca + - 2.0 * model.k3 * model.ca**2.0 + ) + ) + + model.cb_bal = pyo.Constraint( + expr=( + 0 + == -model.sv * model.cb + model.k1 * model.ca - model.k2 * model.cb + ) + ) + + model.cc_bal = pyo.Constraint( + expr=(0 == -model.sv * model.cc + model.k2 * model.cb) + ) + + model.cd_bal = pyo.Constraint( + expr=(0 == -model.sv * model.cd + model.k3 * model.ca**2.0) + ) + + return model + + # Data from the design + data = pd.DataFrame( + data=[ + [1.05, 10000, 3458.4, 1060.8, 1683.9, 1898.5], + [1.10, 10000, 3535.1, 1064.8, 1613.3, 1893.4], + [1.15, 10000, 3609.1, 1067.8, 1547.5, 1887.8], + [1.20, 10000, 3680.7, 1070.0, 1486.1, 1881.6], + [1.25, 10000, 3750.0, 1071.4, 1428.6, 1875.0], + [1.30, 10000, 3817.1, 1072.2, 1374.6, 1868.0], + [1.35, 10000, 3882.2, 1072.4, 1324.0, 1860.7], + [1.40, 10000, 3945.4, 1072.1, 1276.3, 1853.1], + [1.45, 10000, 4006.7, 1071.3, 1231.4, 1845.3], + [1.50, 10000, 4066.4, 1070.1, 1189.0, 1837.3], + [1.55, 10000, 4124.4, 1068.5, 1148.9, 1829.1], + [1.60, 10000, 4180.9, 1066.5, 1111.0, 1820.8], + [1.65, 10000, 4235.9, 1064.3, 1075.0, 1812.4], + [1.70, 10000, 4289.5, 1061.8, 1040.9, 1803.9], + [1.75, 10000, 4341.8, 1059.0, 1008.5, 1795.3], + [1.80, 10000, 4392.8, 1056.0, 977.7, 1786.7], + [1.85, 10000, 4442.6, 1052.8, 948.4, 1778.1], + [1.90, 10000, 4491.3, 1049.4, 920.5, 1769.4], + [1.95, 10000, 4538.8, 1045.8, 893.9, 1760.8], + ], + columns=["sv", "caf", "ca", "cb", "cc", "cd"], + ) + + theta_names = ["k1", "k2", "k3"] def SSE(model, data): expr = ( - (float(data['ca']) - model.ca) ** 2 - + (float(data['cb']) - model.cb) ** 2 - + (float(data['cc']) - model.cc) ** 2 - + (float(data['cd']) - model.cd) ** 2 + (float(data.iloc[0]["ca"]) - model.ca) ** 2 + + (float(data.iloc[0]["cb"]) - model.cb) ** 2 + + (float(data.iloc[0]["cc"]) - model.cc) ** 2 + + (float(data.iloc[0]["cd"]) - model.cd) ** 2 ) return expr @@ -110,13 +297,270 @@ def test_no_csv_if_empty(self): "Cannot test parmest: required dependencies are missing", ) @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") -class TestScenarioSemibatch(unittest.TestCase): +class TestScenarioSemibatchDeprecated(unittest.TestCase): def setUp(self): - import pyomo.contrib.parmest.examples.semibatch.semibatch as sb + import json + from pyomo.environ import ( + ConcreteModel, + Set, + Param, + Var, + Constraint, + ConstraintList, + Expression, + Objective, + TransformationFactory, + SolverFactory, + exp, + minimize, + ) + from pyomo.dae import ContinuousSet, DerivativeVar + + def generate_model(data): + # if data is a file name, then load file first + if isinstance(data, str): + file_name = data + try: + with open(file_name, "r") as infile: + data = json.load(infile) + except: + raise RuntimeError(f"Could not read {file_name} as json") + + # unpack and fix the data + cameastemp = data["Ca_meas"] + cbmeastemp = data["Cb_meas"] + ccmeastemp = data["Cc_meas"] + trmeastemp = data["Tr_meas"] + + cameas = {} + cbmeas = {} + ccmeas = {} + trmeas = {} + for i in cameastemp.keys(): + cameas[float(i)] = cameastemp[i] + cbmeas[float(i)] = cbmeastemp[i] + ccmeas[float(i)] = ccmeastemp[i] + trmeas[float(i)] = trmeastemp[i] + + m = ConcreteModel() + + # + # Measurement Data + # + m.measT = Set(initialize=sorted(cameas.keys())) + m.Ca_meas = Param(m.measT, initialize=cameas) + m.Cb_meas = Param(m.measT, initialize=cbmeas) + m.Cc_meas = Param(m.measT, initialize=ccmeas) + m.Tr_meas = Param(m.measT, initialize=trmeas) + + # + # Parameters for semi-batch reactor model + # + m.R = Param(initialize=8.314) # kJ/kmol/K + m.Mwa = Param(initialize=50.0) # kg/kmol + m.rhor = Param(initialize=1000.0) # kg/m^3 + m.cpr = Param(initialize=3.9) # kJ/kg/K + m.Tf = Param(initialize=300) # K + m.deltaH1 = Param(initialize=-40000.0) # kJ/kmol + m.deltaH2 = Param(initialize=-50000.0) # kJ/kmol + m.alphaj = Param(initialize=0.8) # kJ/s/m^2/K + m.alphac = Param(initialize=0.7) # kJ/s/m^2/K + m.Aj = Param(initialize=5.0) # m^2 + m.Ac = Param(initialize=3.0) # m^2 + m.Vj = Param(initialize=0.9) # m^3 + m.Vc = Param(initialize=0.07) # m^3 + m.rhow = Param(initialize=700.0) # kg/m^3 + m.cpw = Param(initialize=3.1) # kJ/kg/K + m.Ca0 = Param(initialize=data["Ca0"]) # kmol/m^3) + m.Cb0 = Param(initialize=data["Cb0"]) # kmol/m^3) + m.Cc0 = Param(initialize=data["Cc0"]) # kmol/m^3) + m.Tr0 = Param(initialize=300.0) # K + m.Vr0 = Param(initialize=1.0) # m^3 + + m.time = ContinuousSet( + bounds=(0, 21600), initialize=m.measT + ) # Time in seconds + + # + # Control Inputs + # + def _initTc(m, t): + if t < 10800: + return data["Tc1"] + else: + return data["Tc2"] + + m.Tc = Param( + m.time, initialize=_initTc, default=_initTc + ) # bounds= (288,432) Cooling coil temp, control input + + def _initFa(m, t): + if t < 10800: + return data["Fa1"] + else: + return data["Fa2"] + + m.Fa = Param( + m.time, initialize=_initFa, default=_initFa + ) # bounds=(0,0.05) Inlet flow rate, control input + + # + # Parameters being estimated + # + m.k1 = Var(initialize=14, bounds=(2, 100)) # 1/s Actual: 15.01 + m.k2 = Var(initialize=90, bounds=(2, 150)) # 1/s Actual: 85.01 + m.E1 = Var( + initialize=27000.0, bounds=(25000, 40000) + ) # kJ/kmol Actual: 30000 + m.E2 = Var( + initialize=45000.0, bounds=(35000, 50000) + ) # kJ/kmol Actual: 40000 + # m.E1.fix(30000) + # m.E2.fix(40000) + + # + # Time dependent variables + # + m.Ca = Var(m.time, initialize=m.Ca0, bounds=(0, 25)) + m.Cb = Var(m.time, initialize=m.Cb0, bounds=(0, 25)) + m.Cc = Var(m.time, initialize=m.Cc0, bounds=(0, 25)) + m.Vr = Var(m.time, initialize=m.Vr0) + m.Tr = Var(m.time, initialize=m.Tr0) + m.Tj = Var( + m.time, initialize=310.0, bounds=(288, None) + ) # Cooling jacket temp, follows coil temp until failure + + # + # Derivatives in the model + # + m.dCa = DerivativeVar(m.Ca) + m.dCb = DerivativeVar(m.Cb) + m.dCc = DerivativeVar(m.Cc) + m.dVr = DerivativeVar(m.Vr) + m.dTr = DerivativeVar(m.Tr) + + # + # Differential Equations in the model + # + + def _dCacon(m, t): + if t == 0: + return Constraint.Skip + return ( + m.dCa[t] + == m.Fa[t] / m.Vr[t] - m.k1 * exp(-m.E1 / (m.R * m.Tr[t])) * m.Ca[t] + ) + + m.dCacon = Constraint(m.time, rule=_dCacon) + + def _dCbcon(m, t): + if t == 0: + return Constraint.Skip + return ( + m.dCb[t] + == m.k1 * exp(-m.E1 / (m.R * m.Tr[t])) * m.Ca[t] + - m.k2 * exp(-m.E2 / (m.R * m.Tr[t])) * m.Cb[t] + ) + + m.dCbcon = Constraint(m.time, rule=_dCbcon) + + def _dCccon(m, t): + if t == 0: + return Constraint.Skip + return m.dCc[t] == m.k2 * exp(-m.E2 / (m.R * m.Tr[t])) * m.Cb[t] + + m.dCccon = Constraint(m.time, rule=_dCccon) + + def _dVrcon(m, t): + if t == 0: + return Constraint.Skip + return m.dVr[t] == m.Fa[t] * m.Mwa / m.rhor + + m.dVrcon = Constraint(m.time, rule=_dVrcon) + + def _dTrcon(m, t): + if t == 0: + return Constraint.Skip + return m.rhor * m.cpr * m.dTr[t] == m.Fa[t] * m.Mwa * m.cpr / m.Vr[ + t + ] * (m.Tf - m.Tr[t]) - m.k1 * exp(-m.E1 / (m.R * m.Tr[t])) * m.Ca[ + t + ] * m.deltaH1 - m.k2 * exp( + -m.E2 / (m.R * m.Tr[t]) + ) * m.Cb[ + t + ] * m.deltaH2 + m.alphaj * m.Aj / m.Vr0 * ( + m.Tj[t] - m.Tr[t] + ) + m.alphac * m.Ac / m.Vr0 * ( + m.Tc[t] - m.Tr[t] + ) + + m.dTrcon = Constraint(m.time, rule=_dTrcon) + + def _singlecooling(m, t): + return m.Tc[t] == m.Tj[t] + + m.singlecooling = Constraint(m.time, rule=_singlecooling) + + # Initial Conditions + def _initcon(m): + yield m.Ca[m.time.first()] == m.Ca0 + yield m.Cb[m.time.first()] == m.Cb0 + yield m.Cc[m.time.first()] == m.Cc0 + yield m.Vr[m.time.first()] == m.Vr0 + yield m.Tr[m.time.first()] == m.Tr0 + + m.initcon = ConstraintList(rule=_initcon) + + # + # Stage-specific cost computations + # + def ComputeFirstStageCost_rule(model): + return 0 + + m.FirstStageCost = Expression(rule=ComputeFirstStageCost_rule) + + def AllMeasurements(m): + return sum( + (m.Ca[t] - m.Ca_meas[t]) ** 2 + + (m.Cb[t] - m.Cb_meas[t]) ** 2 + + (m.Cc[t] - m.Cc_meas[t]) ** 2 + + 0.01 * (m.Tr[t] - m.Tr_meas[t]) ** 2 + for t in m.measT + ) + + def MissingMeasurements(m): + if data["experiment"] == 1: + return sum( + (m.Ca[t] - m.Ca_meas[t]) ** 2 + + (m.Cb[t] - m.Cb_meas[t]) ** 2 + + (m.Cc[t] - m.Cc_meas[t]) ** 2 + + (m.Tr[t] - m.Tr_meas[t]) ** 2 + for t in m.measT + ) + elif data["experiment"] == 2: + return sum((m.Tr[t] - m.Tr_meas[t]) ** 2 for t in m.measT) + else: + return sum( + (m.Cb[t] - m.Cb_meas[t]) ** 2 + (m.Tr[t] - m.Tr_meas[t]) ** 2 + for t in m.measT + ) + + m.SecondStageCost = Expression(rule=MissingMeasurements) + + def total_cost_rule(model): + return model.FirstStageCost + model.SecondStageCost + + m.Total_Cost_Objective = Objective(rule=total_cost_rule, sense=minimize) + + # Discretize model + disc = TransformationFactory("dae.collocation") + disc.apply_to(m, nfe=20, ncp=4) + return m # Vars to estimate in parmest - theta_names = ['k1', 'k2', 'E1', 'E2'] + theta_names = ["k1", "k2", "E1", "E2"] self.fbase = os.path.join(testdir, "..", "examples", "semibatch") # Data, list of dictionaries @@ -124,14 +568,14 @@ def setUp(self): for exp_num in range(10): fname = "exp" + str(exp_num + 1) + ".out" fullname = os.path.join(self.fbase, fname) - with open(fullname, 'r') as infile: + with open(fullname, "r") as infile: d = json.load(infile) data.append(d) # Note, the model already includes a 'SecondStageCost' expression # for the sum of squared error that will be used in parameter estimation - self.pest = parmest.Estimator(sb.generate_model, data, theta_names) + self.pest = parmest.Estimator(generate_model, data, theta_names) def test_semibatch_bootstrap(self): scenmaker = sc.ScenarioCreator(self.pest, "ipopt") @@ -142,5 +586,5 @@ def test_semibatch_bootstrap(self): self.assertAlmostEqual(tval, 20.64, places=1) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/parmest/tests/test_solver.py b/pyomo/contrib/parmest/tests/test_solver.py index eb655023b9b..77eca3a13b6 100644 --- a/pyomo/contrib/parmest/tests/test_solver.py +++ b/pyomo/contrib/parmest/tests/test_solver.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/tests/test_utils.py b/pyomo/contrib/parmest/tests/test_utils.py index bd0706ac38d..d5e66ab58d5 100644 --- a/pyomo/contrib/parmest/tests/test_utils.py +++ b/pyomo/contrib/parmest/tests/test_utils.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -16,7 +16,7 @@ import pyomo.contrib.parmest.parmest as parmest from pyomo.opt import SolverFactory -ipopt_available = SolverFactory('ipopt').available() +ipopt_available = SolverFactory("ipopt").available() @unittest.skipIf( @@ -25,18 +25,12 @@ ) @unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") class TestUtils(unittest.TestCase): - @classmethod - def setUpClass(self): - pass - @classmethod - def tearDownClass(self): - pass - - @unittest.pytest.mark.expensive def test_convert_param_to_var(self): + # TODO: Check that this works for different structured models (indexed, blocks, etc) + from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( - reactor_design_model, + ReactorDesignExperiment, ) data = pd.DataFrame( @@ -45,23 +39,26 @@ def test_convert_param_to_var(self): [1.10, 10000, 3535.1, 1064.8, 1613.3, 1893.4], [1.15, 10000, 3609.1, 1067.8, 1547.5, 1887.8], ], - columns=['sv', 'caf', 'ca', 'cb', 'cc', 'cd'], + columns=["sv", "caf", "ca", "cb", "cc", "cd"], ) - theta_names = ['k1', 'k2', 'k3'] + # make model + exp = ReactorDesignExperiment(data, 0) + instance = exp.get_labeled_model() - instance = reactor_design_model(data.loc[0]) - solver = pyo.SolverFactory('ipopt') - solver.solve(instance) - - instance_vars = parmest.utils.convert_params_to_vars( + theta_names = ['k1', 'k2', 'k3'] + m_vars = parmest.utils.convert_params_to_vars( instance, theta_names, fix_vars=True ) - solver.solve(instance_vars) - assert instance.k1() == instance_vars.k1() - assert instance.k2() == instance_vars.k2() - assert instance.k3() == instance_vars.k3() + for v in theta_names: + self.assertTrue(hasattr(m_vars, v)) + c = m_vars.find_component(v) + self.assertIsInstance(c, pyo.Var) + self.assertTrue(c.fixed) + c_old = instance.find_component(v) + self.assertEqual(pyo.value(c), pyo.value(c_old)) + self.assertTrue(c in m_vars.unknown_parameters) if __name__ == "__main__": diff --git a/pyomo/contrib/parmest/utils/__init__.py b/pyomo/contrib/parmest/utils/__init__.py index 1615ab206f7..3c6900aa5d9 100644 --- a/pyomo/contrib/parmest/utils/__init__.py +++ b/pyomo/contrib/parmest/utils/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/utils/create_ef.py b/pyomo/contrib/parmest/utils/create_ef.py index 2e6c8541fa1..aaadc7f98b9 100644 --- a/pyomo/contrib/parmest/utils/create_ef.py +++ b/pyomo/contrib/parmest/utils/create_ef.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # This software is distributed under the 3-clause BSD License. # Copied with minor modifications from create_EF in mpisppy/utils/sputils.py # from the mpi-sppy library (https://github.com/Pyomo/mpi-sppy). diff --git a/pyomo/contrib/parmest/utils/ipopt_solver_wrapper.py b/pyomo/contrib/parmest/utils/ipopt_solver_wrapper.py index 7d8289cd181..08388dc5ec1 100644 --- a/pyomo/contrib/parmest/utils/ipopt_solver_wrapper.py +++ b/pyomo/contrib/parmest/utils/ipopt_solver_wrapper.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/utils/model_utils.py b/pyomo/contrib/parmest/utils/model_utils.py index c3c71dc2d6c..7778ebcc9f1 100644 --- a/pyomo/contrib/parmest/utils/model_utils.py +++ b/pyomo/contrib/parmest/utils/model_utils.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -15,6 +15,7 @@ from pyomo.core.expr import replace_expressions, identify_mutable_parameters from pyomo.core.base.var import IndexedVar from pyomo.core.base.param import IndexedParam +from pyomo.common.collections import ComponentMap from pyomo.environ import ComponentUID @@ -49,6 +50,7 @@ def convert_params_to_vars(model, param_names=None, fix_vars=False): # Convert Params to Vars, unfix Vars, and create a substitution map substitution_map = {} + comp_map = ComponentMap() for i, param_name in enumerate(param_names): # Leverage the parser in ComponentUID to locate the component. theta_cuid = ComponentUID(param_name) @@ -65,6 +67,7 @@ def convert_params_to_vars(model, param_names=None, fix_vars=False): theta_var_cuid = ComponentUID(theta_object.name) theta_var_object = theta_var_cuid.find_component_on(model) substitution_map[id(theta_object)] = theta_var_object + comp_map[theta_object] = theta_var_object # Indexed Param elif isinstance(theta_object, IndexedParam): @@ -90,6 +93,7 @@ def convert_params_to_vars(model, param_names=None, fix_vars=False): # Update substitution map (map each indexed param to indexed var) theta_var_cuid = ComponentUID(theta_object.name) theta_var_object = theta_var_cuid.find_component_on(model) + comp_map[theta_object] = theta_var_object var_theta_objects = [] for theta_obj in theta_var_object: theta_cuid = ComponentUID( @@ -101,6 +105,7 @@ def convert_params_to_vars(model, param_names=None, fix_vars=False): param_theta_objects, var_theta_objects ): substitution_map[id(param_theta_obj)] = var_theta_obj + comp_map[param_theta_obj] = var_theta_obj # Var or Indexed Var elif isinstance(theta_object, IndexedVar) or theta_object.is_variable_type(): @@ -182,6 +187,15 @@ def convert_params_to_vars(model, param_names=None, fix_vars=False): model.del_component(obj) model.add_component(obj.name, pyo.Objective(rule=expr, sense=obj.sense)) + # Convert Params to Vars in Suffixes + for s in model.component_objects(pyo.Suffix): + current_keys = list(s.keys()) + for c in current_keys: + if c in comp_map: + s[comp_map[c]] = s.pop(c) + + assert len(current_keys) == len(s.keys()) + # print('--- Updated Model ---') # model.pprint() # solver = pyo.SolverFactory('ipopt') diff --git a/pyomo/contrib/parmest/utils/mpi_utils.py b/pyomo/contrib/parmest/utils/mpi_utils.py index 35c4bf137bc..45e3260117d 100644 --- a/pyomo/contrib/parmest/utils/mpi_utils.py +++ b/pyomo/contrib/parmest/utils/mpi_utils.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/utils/scenario_tree.py b/pyomo/contrib/parmest/utils/scenario_tree.py index d46a8f2c5f0..f245e053cad 100644 --- a/pyomo/contrib/parmest/utils/scenario_tree.py +++ b/pyomo/contrib/parmest/utils/scenario_tree.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # This software is distributed under the 3-clause BSD License. # Copied with minor modifications from mpisppy/scenario_tree.py # from the mpi-sppy library (https://github.com/Pyomo/mpi-sppy). @@ -14,7 +25,7 @@ def build_vardatalist(self, model, varlist=None): """ - Convert a list of pyomo variables to a list of ScalarVar and _GeneralVarData. If varlist is none, builds a + Convert a list of pyomo variables to a list of ScalarVar and VarData. If varlist is none, builds a list of all variables in the model. The new list is stored in the vars_to_tighten attribute. By CD Laird Parameters diff --git a/pyomo/contrib/piecewise/__init__.py b/pyomo/contrib/piecewise/__init__.py index 33cfc6f1606..b23200b3f7d 100644 --- a/pyomo/contrib/piecewise/__init__.py +++ b/pyomo/contrib/piecewise/__init__.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.contrib.piecewise.piecewise_linear_expression import ( PiecewiseLinearExpression, ) @@ -22,3 +33,9 @@ from pyomo.contrib.piecewise.transform.convex_combination import ( ConvexCombinationTransformation, ) +from pyomo.contrib.piecewise.transform.nested_inner_repn import ( + NestedInnerRepresentationGDPTransformation, +) +from pyomo.contrib.piecewise.transform.disaggregated_logarithmic import ( + DisaggregatedLogarithmicMIPTransformation, +) diff --git a/pyomo/contrib/piecewise/piecewise_linear_expression.py b/pyomo/contrib/piecewise/piecewise_linear_expression.py index ea1d95b0f51..ddcb7c6a42f 100644 --- a/pyomo/contrib/piecewise/piecewise_linear_expression.py +++ b/pyomo/contrib/piecewise/piecewise_linear_expression.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/piecewise/piecewise_linear_function.py b/pyomo/contrib/piecewise/piecewise_linear_function.py index 6d4fa658f88..e92edacc756 100644 --- a/pyomo/contrib/piecewise/piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/piecewise_linear_function.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -20,7 +20,7 @@ PiecewiseLinearExpression, ) from pyomo.core import Any, NonNegativeIntegers, value, Var -from pyomo.core.base.block import _BlockData, Block +from pyomo.core.base.block import BlockData, Block from pyomo.core.base.component import ModelComponentFactory from pyomo.core.base.expression import Expression from pyomo.core.base.global_set import UnindexedComponent_index @@ -36,11 +36,11 @@ logger = logging.getLogger(__name__) -class PiecewiseLinearFunctionData(_BlockData): +class PiecewiseLinearFunctionData(BlockData): _Block_reserved_words = Any def __init__(self, component=None): - _BlockData.__init__(self, component) + BlockData.__init__(self, component) with self._declare_reserved_components(): self._expressions = Expression(NonNegativeIntegers) diff --git a/pyomo/contrib/piecewise/tests/__init__.py b/pyomo/contrib/piecewise/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/piecewise/tests/__init__.py +++ b/pyomo/contrib/piecewise/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/piecewise/tests/common_inner_repn_tests.py b/pyomo/contrib/piecewise/tests/common_inner_repn_tests.py new file mode 100644 index 00000000000..e0b8e878be3 --- /dev/null +++ b/pyomo/contrib/piecewise/tests/common_inner_repn_tests.py @@ -0,0 +1,80 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.core import Var +from pyomo.core.base import Constraint +from pyomo.core.expr.compare import assertExpressionsEqual + +# This file contains check methods shared between GDP inner representation-based +# transformations. Currently, those are the inner_representation_gdp and +# nested_inner_repn_gdp transformations, since each have disjuncts with the +# same structure. + + +# Check one disjunct from the log model for proper contents +def check_log_disjunct(test, d, pts, f, substitute_var, x): + test.assertEqual(len(d.component_map(Constraint)), 3) + # lambdas and indicator_var + test.assertEqual(len(d.component_map(Var)), 2) + test.assertIsInstance(d.lambdas, Var) + test.assertEqual(len(d.lambdas), 2) + for lamb in d.lambdas.values(): + test.assertEqual(lamb.lb, 0) + test.assertEqual(lamb.ub, 1) + test.assertIsInstance(d.convex_combo, Constraint) + assertExpressionsEqual(test, d.convex_combo.expr, d.lambdas[0] + d.lambdas[1] == 1) + test.assertIsInstance(d.set_substitute, Constraint) + assertExpressionsEqual( + test, d.set_substitute.expr, substitute_var == f(x), places=7 + ) + test.assertIsInstance(d.linear_combo, Constraint) + test.assertEqual(len(d.linear_combo), 1) + assertExpressionsEqual( + test, d.linear_combo[0].expr, x == pts[0] * d.lambdas[0] + pts[1] * d.lambdas[1] + ) + + +# Check one disjunct from the paraboloid model for proper contents. +def check_paraboloid_disjunct(test, d, pts, f, substitute_var, x1, x2): + test.assertEqual(len(d.component_map(Constraint)), 3) + # lambdas and indicator_var + test.assertEqual(len(d.component_map(Var)), 2) + test.assertIsInstance(d.lambdas, Var) + test.assertEqual(len(d.lambdas), 3) + for lamb in d.lambdas.values(): + test.assertEqual(lamb.lb, 0) + test.assertEqual(lamb.ub, 1) + test.assertIsInstance(d.convex_combo, Constraint) + assertExpressionsEqual( + test, d.convex_combo.expr, d.lambdas[0] + d.lambdas[1] + d.lambdas[2] == 1 + ) + test.assertIsInstance(d.set_substitute, Constraint) + assertExpressionsEqual( + test, d.set_substitute.expr, substitute_var == f(x1, x2), places=7 + ) + test.assertIsInstance(d.linear_combo, Constraint) + test.assertEqual(len(d.linear_combo), 2) + assertExpressionsEqual( + test, + d.linear_combo[0].expr, + x1 + == pts[0][0] * d.lambdas[0] + + pts[1][0] * d.lambdas[1] + + pts[2][0] * d.lambdas[2], + ) + assertExpressionsEqual( + test, + d.linear_combo[1].expr, + x2 + == pts[0][1] * d.lambdas[0] + + pts[1][1] * d.lambdas[1] + + pts[2][1] * d.lambdas[2], + ) diff --git a/pyomo/contrib/piecewise/tests/common_tests.py b/pyomo/contrib/piecewise/tests/common_tests.py index c77d7064544..23e67474934 100644 --- a/pyomo/contrib/piecewise/tests/common_tests.py +++ b/pyomo/contrib/piecewise/tests/common_tests.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/piecewise/tests/models.py b/pyomo/contrib/piecewise/tests/models.py index be2811a70a4..1a8bef04ad7 100644 --- a/pyomo/contrib/piecewise/tests/models.py +++ b/pyomo/contrib/piecewise/tests/models.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py new file mode 100644 index 00000000000..f848c610e9d --- /dev/null +++ b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py @@ -0,0 +1,275 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest +from pyomo.contrib.piecewise.tests import models +import pyomo.contrib.piecewise.tests.common_tests as ct +from pyomo.core.base import TransformationFactory +from pyomo.environ import SolverFactory, Var, Constraint +from pyomo.core.expr.compare import assertExpressionsEqual + + +class TestTransformPiecewiseModelToNestedInnerRepnMIP(unittest.TestCase): + def check_pw_log(self, m): + z = m.pw_log.get_transformation_var(m.log_expr) + self.assertIsInstance(z, Var) + # Now we can use those Vars to check on what the transformation created + log_block = z.parent_block() + + # We should have three Vars, two of which are indexed, and five + # Constraints, three of which are indexed + + self.assertEqual(len(log_block.component_map(Var)), 3) + self.assertEqual(len(log_block.component_map(Constraint)), 5) + + # Constants + simplex_count = 3 + log_simplex_count = 2 + simplex_point_count = 2 + + # Substitute var + self.assertIsInstance(log_block.substitute_var, Var) + self.assertIs(m.obj.expr.expr, log_block.substitute_var) + # Binaries + self.assertIsInstance(log_block.binaries, Var) + self.assertEqual(len(log_block.binaries), log_simplex_count) + # Lambdas + self.assertIsInstance(log_block.lambdas, Var) + self.assertEqual(len(log_block.lambdas), simplex_count * simplex_point_count) + for l in log_block.lambdas.values(): + self.assertEqual(l.lb, 0) + self.assertEqual(l.ub, 1) + + # Convex combo constraint + self.assertIsInstance(log_block.convex_combo, Constraint) + assertExpressionsEqual( + self, + log_block.convex_combo.expr, + log_block.lambdas[0, 0] + + log_block.lambdas[0, 1] + + log_block.lambdas[1, 0] + + log_block.lambdas[1, 1] + + log_block.lambdas[2, 0] + + log_block.lambdas[2, 1] + == 1, + ) + + # Set substitute constraint + self.assertIsInstance(log_block.set_substitute, Constraint) + assertExpressionsEqual( + self, + log_block.set_substitute.expr, + log_block.substitute_var + == log_block.lambdas[0, 0] * m.f1(1) + + log_block.lambdas[1, 0] * m.f2(3) + + log_block.lambdas[2, 0] * m.f3(6) + + log_block.lambdas[0, 1] * m.f1(3) + + log_block.lambdas[1, 1] * m.f2(6) + + log_block.lambdas[2, 1] * m.f3(10), + places=7, + ) + + # x constraint + self.assertIsInstance(log_block.x_constraint, Constraint) + # one-dimensional case, so there is only one x variable here + self.assertEqual(len(log_block.x_constraint), 1) + assertExpressionsEqual( + self, + log_block.x_constraint[0].expr, + m.x + == 1 * log_block.lambdas[0, 0] + + 3 * log_block.lambdas[0, 1] + + 3 * log_block.lambdas[1, 0] + + 6 * log_block.lambdas[1, 1] + + 6 * log_block.lambdas[2, 0] + + 10 * log_block.lambdas[2, 1], + ) + + # simplex choice 1 constraint enables lambdas when binaries are on + self.assertEqual(len(log_block.simplex_choice_1), log_simplex_count) + assertExpressionsEqual( + self, + log_block.simplex_choice_1[0].expr, + log_block.lambdas[2, 0] + log_block.lambdas[2, 1] <= log_block.binaries[0], + ) + assertExpressionsEqual( + self, + log_block.simplex_choice_1[1].expr, + log_block.lambdas[1, 0] + log_block.lambdas[1, 1] <= log_block.binaries[1], + ) + # simplex choice 2 constraint enables lambdas when binaries are off + self.assertEqual(len(log_block.simplex_choice_2), log_simplex_count) + assertExpressionsEqual( + self, + log_block.simplex_choice_2[0].expr, + log_block.lambdas[0, 0] + + log_block.lambdas[0, 1] + + log_block.lambdas[1, 0] + + log_block.lambdas[1, 1] + <= 1 - log_block.binaries[0], + ) + assertExpressionsEqual( + self, + log_block.simplex_choice_2[1].expr, + log_block.lambdas[0, 0] + + log_block.lambdas[0, 1] + + log_block.lambdas[2, 0] + + log_block.lambdas[2, 1] + <= 1 - log_block.binaries[1], + ) + + def check_pw_paraboloid(self, m): + # This is a little larger, but at least test that the right numbers of + # everything are created + z = m.pw_paraboloid.get_transformation_var(m.paraboloid_expr) + self.assertIsInstance(z, Var) + paraboloid_block = z.parent_block() + + self.assertEqual(len(paraboloid_block.component_map(Var)), 3) + self.assertEqual(len(paraboloid_block.component_map(Constraint)), 5) + + # Constants + simplex_count = 4 + log_simplex_count = 2 + simplex_point_count = 3 + + # Substitute var + self.assertIsInstance(paraboloid_block.substitute_var, Var) + # Binaries + self.assertIsInstance(paraboloid_block.binaries, Var) + self.assertEqual(len(paraboloid_block.binaries), log_simplex_count) + # Lambdas + self.assertIsInstance(paraboloid_block.lambdas, Var) + self.assertEqual( + len(paraboloid_block.lambdas), simplex_count * simplex_point_count + ) + for l in paraboloid_block.lambdas.values(): + self.assertEqual(l.lb, 0) + self.assertEqual(l.ub, 1) + + # Convex combo constraint + self.assertIsInstance(paraboloid_block.convex_combo, Constraint) + assertExpressionsEqual( + self, + paraboloid_block.convex_combo.expr, + paraboloid_block.lambdas[0, 0] + + paraboloid_block.lambdas[0, 1] + + paraboloid_block.lambdas[0, 2] + + paraboloid_block.lambdas[1, 0] + + paraboloid_block.lambdas[1, 1] + + paraboloid_block.lambdas[1, 2] + + paraboloid_block.lambdas[2, 0] + + paraboloid_block.lambdas[2, 1] + + paraboloid_block.lambdas[2, 2] + + paraboloid_block.lambdas[3, 0] + + paraboloid_block.lambdas[3, 1] + + paraboloid_block.lambdas[3, 2] + == 1, + ) + + # Set substitute constraint + self.assertIsInstance(paraboloid_block.set_substitute, Constraint) + assertExpressionsEqual( + self, + paraboloid_block.set_substitute.expr, + paraboloid_block.substitute_var + == paraboloid_block.lambdas[0, 0] * m.g1(0, 1) + + paraboloid_block.lambdas[1, 0] * m.g1(0, 1) + + paraboloid_block.lambdas[2, 0] * m.g2(3, 4) + + paraboloid_block.lambdas[3, 0] * m.g2(0, 7) + + paraboloid_block.lambdas[0, 1] * m.g1(0, 4) + + paraboloid_block.lambdas[1, 1] * m.g1(3, 4) + + paraboloid_block.lambdas[2, 1] * m.g2(3, 7) + + paraboloid_block.lambdas[3, 1] * m.g2(0, 4) + + paraboloid_block.lambdas[0, 2] * m.g1(3, 4) + + paraboloid_block.lambdas[1, 2] * m.g1(3, 1) + + paraboloid_block.lambdas[2, 2] * m.g2(0, 7) + + paraboloid_block.lambdas[3, 2] * m.g2(3, 4), + places=7, + ) + + # x constraint + self.assertIsInstance(paraboloid_block.x_constraint, Constraint) + # Here we have two x variables + self.assertEqual(len(paraboloid_block.x_constraint), 2) + assertExpressionsEqual( + self, + paraboloid_block.x_constraint[0].expr, + m.x1 + == 0 * paraboloid_block.lambdas[0, 0] + + 0 * paraboloid_block.lambdas[0, 1] + + 3 * paraboloid_block.lambdas[0, 2] + + 0 * paraboloid_block.lambdas[1, 0] + + 3 * paraboloid_block.lambdas[1, 1] + + 3 * paraboloid_block.lambdas[1, 2] + + 3 * paraboloid_block.lambdas[2, 0] + + 3 * paraboloid_block.lambdas[2, 1] + + 0 * paraboloid_block.lambdas[2, 2] + + 0 * paraboloid_block.lambdas[3, 0] + + 0 * paraboloid_block.lambdas[3, 1] + + 3 * paraboloid_block.lambdas[3, 2], + ) + assertExpressionsEqual( + self, + paraboloid_block.x_constraint[1].expr, + m.x2 + == 1 * paraboloid_block.lambdas[0, 0] + + 4 * paraboloid_block.lambdas[0, 1] + + 4 * paraboloid_block.lambdas[0, 2] + + 1 * paraboloid_block.lambdas[1, 0] + + 4 * paraboloid_block.lambdas[1, 1] + + 1 * paraboloid_block.lambdas[1, 2] + + 4 * paraboloid_block.lambdas[2, 0] + + 7 * paraboloid_block.lambdas[2, 1] + + 7 * paraboloid_block.lambdas[2, 2] + + 7 * paraboloid_block.lambdas[3, 0] + + 4 * paraboloid_block.lambdas[3, 1] + + 4 * paraboloid_block.lambdas[3, 2], + ) + + # The choices will get long, so let's just assert we have enough + self.assertEqual(len(paraboloid_block.simplex_choice_1), log_simplex_count) + self.assertEqual(len(paraboloid_block.simplex_choice_2), log_simplex_count) + + # Test methods using the common_tests.py code. + def test_transformation_do_not_descend(self): + ct.check_transformation_do_not_descend( + self, 'contrib.piecewise.disaggregated_logarithmic' + ) + + def test_transformation_PiecewiseLinearFunction_targets(self): + ct.check_transformation_PiecewiseLinearFunction_targets( + self, 'contrib.piecewise.disaggregated_logarithmic' + ) + + def test_descend_into_expressions(self): + ct.check_descend_into_expressions( + self, 'contrib.piecewise.disaggregated_logarithmic' + ) + + def test_descend_into_expressions_constraint_target(self): + ct.check_descend_into_expressions_constraint_target( + self, 'contrib.piecewise.disaggregated_logarithmic' + ) + + def test_descend_into_expressions_objective_target(self): + ct.check_descend_into_expressions_objective_target( + self, 'contrib.piecewise.disaggregated_logarithmic' + ) + + # Check solution of the log(x) model + @unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi is not available') + @unittest.skipUnless(SolverFactory('gurobi').license_is_valid(), 'No license') + def test_solve_log_model(self): + m = models.make_log_x_model() + TransformationFactory("contrib.piecewise.disaggregated_logarithmic").apply_to(m) + SolverFactory("gurobi").solve(m) + ct.check_log_x_model_soln(self, m) diff --git a/pyomo/contrib/piecewise/tests/test_inner_repn_gdp.py b/pyomo/contrib/piecewise/tests/test_inner_repn_gdp.py index a0dbd1cca19..e7505bb92d3 100644 --- a/pyomo/contrib/piecewise/tests/test_inner_repn_gdp.py +++ b/pyomo/contrib/piecewise/tests/test_inner_repn_gdp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -12,6 +12,7 @@ import pyomo.common.unittest as unittest from pyomo.contrib.piecewise.tests import models import pyomo.contrib.piecewise.tests.common_tests as ct +import pyomo.contrib.piecewise.tests.common_inner_repn_tests as inner_repn_tests from pyomo.core.base import TransformationFactory from pyomo.core.expr.compare import ( assertExpressionsEqual, @@ -22,67 +23,6 @@ class TestTransformPiecewiseModelToInnerRepnGDP(unittest.TestCase): - def check_log_disjunct(self, d, pts, f, substitute_var, x): - self.assertEqual(len(d.component_map(Constraint)), 3) - # lambdas and indicator_var - self.assertEqual(len(d.component_map(Var)), 2) - self.assertIsInstance(d.lambdas, Var) - self.assertEqual(len(d.lambdas), 2) - for lamb in d.lambdas.values(): - self.assertEqual(lamb.lb, 0) - self.assertEqual(lamb.ub, 1) - self.assertIsInstance(d.convex_combo, Constraint) - assertExpressionsEqual( - self, d.convex_combo.expr, d.lambdas[0] + d.lambdas[1] == 1 - ) - self.assertIsInstance(d.set_substitute, Constraint) - assertExpressionsEqual( - self, d.set_substitute.expr, substitute_var == f(x), places=7 - ) - self.assertIsInstance(d.linear_combo, Constraint) - self.assertEqual(len(d.linear_combo), 1) - assertExpressionsEqual( - self, - d.linear_combo[0].expr, - x == pts[0] * d.lambdas[0] + pts[1] * d.lambdas[1], - ) - - def check_paraboloid_disjunct(self, d, pts, f, substitute_var, x1, x2): - self.assertEqual(len(d.component_map(Constraint)), 3) - # lambdas and indicator_var - self.assertEqual(len(d.component_map(Var)), 2) - self.assertIsInstance(d.lambdas, Var) - self.assertEqual(len(d.lambdas), 3) - for lamb in d.lambdas.values(): - self.assertEqual(lamb.lb, 0) - self.assertEqual(lamb.ub, 1) - self.assertIsInstance(d.convex_combo, Constraint) - assertExpressionsEqual( - self, d.convex_combo.expr, d.lambdas[0] + d.lambdas[1] + d.lambdas[2] == 1 - ) - self.assertIsInstance(d.set_substitute, Constraint) - assertExpressionsEqual( - self, d.set_substitute.expr, substitute_var == f(x1, x2), places=7 - ) - self.assertIsInstance(d.linear_combo, Constraint) - self.assertEqual(len(d.linear_combo), 2) - assertExpressionsEqual( - self, - d.linear_combo[0].expr, - x1 - == pts[0][0] * d.lambdas[0] - + pts[1][0] * d.lambdas[1] - + pts[2][0] * d.lambdas[2], - ) - assertExpressionsEqual( - self, - d.linear_combo[1].expr, - x2 - == pts[0][1] * d.lambdas[0] - + pts[1][1] * d.lambdas[1] - + pts[2][1] * d.lambdas[2], - ) - def check_pw_log(self, m): ## # Check the transformation of the approximation of log(x) @@ -101,7 +41,9 @@ def check_pw_log(self, m): log_block.disjuncts[2]: ((6, 10), m.f3), } for d, (pts, f) in disjuncts_dict.items(): - self.check_log_disjunct(d, pts, f, log_block.substitute_var, m.x) + inner_repn_tests.check_log_disjunct( + self, d, pts, f, log_block.substitute_var, m.x + ) # Check the Disjunction self.assertIsInstance(log_block.pick_a_piece, Disjunction) @@ -129,8 +71,8 @@ def check_pw_paraboloid(self, m): paraboloid_block.disjuncts[3]: ([(0, 7), (0, 4), (3, 4)], m.g2), } for d, (pts, f) in disjuncts_dict.items(): - self.check_paraboloid_disjunct( - d, pts, f, paraboloid_block.substitute_var, m.x1, m.x2 + inner_repn_tests.check_paraboloid_disjunct( + self, d, pts, f, paraboloid_block.substitute_var, m.x1, m.x2 ) # Check the Disjunction diff --git a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py new file mode 100644 index 00000000000..2024f014f55 --- /dev/null +++ b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py @@ -0,0 +1,135 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest +from pyomo.contrib.piecewise.tests import models +import pyomo.contrib.piecewise.tests.common_tests as ct +import pyomo.contrib.piecewise.tests.common_inner_repn_tests as inner_repn_tests +from pyomo.core.base import TransformationFactory +from pyomo.environ import SolverFactory, Var, Constraint +from pyomo.gdp import Disjunction, Disjunct +from pyomo.core.expr.compare import assertExpressionsEqual + + +# Test the nested inner repn gdp model using the common_tests code +class TestTransformPiecewiseModelToNestedInnerRepnGDP(unittest.TestCase): + # Check the structure of the log PWLF Block + def check_pw_log(self, m): + z = m.pw_log.get_transformation_var(m.log_expr) + self.assertIsInstance(z, Var) + # Now we can use those Vars to check on what the transformation created + log_block = z.parent_block() + + # Not using ct.check_trans_block_structure() because these are slightly + # different + # Two top-level disjuncts + self.assertEqual(len(log_block.component_map(Disjunct)), 2) + # One disjunction + self.assertEqual(len(log_block.component_map(Disjunction)), 1) + # The 'z' var (that we will substitute in for the function being + # approximated) is here: + self.assertEqual(len(log_block.component_map(Var)), 1) + self.assertIsInstance(log_block.substitute_var, Var) + + # Check the tree structure, which should be heavier on the right + # Parent disjunction + self.assertIsInstance(log_block.disj, Disjunction) + self.assertEqual(len(log_block.disj.disjuncts), 2) + + # Left disjunct with constraints + self.assertIsInstance(log_block.d_l, Disjunct) + inner_repn_tests.check_log_disjunct( + self, log_block.d_l, (1, 3), m.f1, log_block.substitute_var, m.x + ) + + # Right disjunct with disjunction + self.assertIsInstance(log_block.d_r, Disjunct) + self.assertIsInstance(log_block.d_r.inner_disjunction_r, Disjunction) + self.assertEqual(len(log_block.d_r.inner_disjunction_r.disjuncts), 2) + + # Left and right child disjuncts with constraints + self.assertIsInstance(log_block.d_r.d_l, Disjunct) + inner_repn_tests.check_log_disjunct( + self, log_block.d_r.d_l, (3, 6), m.f2, log_block.substitute_var, m.x + ) + self.assertIsInstance(log_block.d_r.d_r, Disjunct) + inner_repn_tests.check_log_disjunct( + self, log_block.d_r.d_r, (6, 10), m.f3, log_block.substitute_var, m.x + ) + + # Check that this also became the objective + self.assertIs(m.obj.expr.expr, log_block.substitute_var) + + # Check the structure of the paraboloid PWLF block + def check_pw_paraboloid(self, m): + z = m.pw_paraboloid.get_transformation_var(m.paraboloid_expr) + self.assertIsInstance(z, Var) + paraboloid_block = z.parent_block() + + # Two top-level disjuncts + self.assertEqual(len(paraboloid_block.component_map(Disjunct)), 2) + # One disjunction + self.assertEqual(len(paraboloid_block.component_map(Disjunction)), 1) + # The 'z' var (that we will substitute in for the function being + # approximated) is here: + self.assertEqual(len(paraboloid_block.component_map(Var)), 1) + self.assertIsInstance(paraboloid_block.substitute_var, Var) + + # This one should have an even tree with four leaf disjuncts + disjuncts_dict = { + paraboloid_block.d_l.d_l: ([(0, 1), (0, 4), (3, 4)], m.g1), + paraboloid_block.d_l.d_r: ([(0, 1), (3, 4), (3, 1)], m.g1), + paraboloid_block.d_r.d_l: ([(3, 4), (3, 7), (0, 7)], m.g2), + paraboloid_block.d_r.d_r: ([(0, 7), (0, 4), (3, 4)], m.g2), + } + for d, (pts, f) in disjuncts_dict.items(): + inner_repn_tests.check_paraboloid_disjunct( + self, d, pts, f, paraboloid_block.substitute_var, m.x1, m.x2 + ) + + # And check the substitute Var is in the objective now. + self.assertIs(m.indexed_c[0].body.args[0].expr, paraboloid_block.substitute_var) + + # Test methods using the common_tests.py code. Copied in from test_inner_repn_gdp.py. + def test_transformation_do_not_descend(self): + ct.check_transformation_do_not_descend( + self, 'contrib.piecewise.nested_inner_repn_gdp' + ) + + def test_transformation_PiecewiseLinearFunction_targets(self): + ct.check_transformation_PiecewiseLinearFunction_targets( + self, 'contrib.piecewise.nested_inner_repn_gdp' + ) + + def test_descend_into_expressions(self): + ct.check_descend_into_expressions( + self, 'contrib.piecewise.nested_inner_repn_gdp' + ) + + def test_descend_into_expressions_constraint_target(self): + ct.check_descend_into_expressions_constraint_target( + self, 'contrib.piecewise.nested_inner_repn_gdp' + ) + + def test_descend_into_expressions_objective_target(self): + ct.check_descend_into_expressions_objective_target( + self, 'contrib.piecewise.nested_inner_repn_gdp' + ) + + # Check the solution of the log(x) model + @unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi is not available') + @unittest.skipUnless(SolverFactory('gurobi').license_is_valid(), 'No license') + def test_solve_log_model(self): + m = models.make_log_x_model() + TransformationFactory("contrib.piecewise.nested_inner_repn_gdp").apply_to(m) + TransformationFactory("gdp.bigm").apply_to(m) + SolverFactory("gurobi").solve(m) + ct.check_log_x_model_soln(self, m) diff --git a/pyomo/contrib/piecewise/tests/test_outer_repn_gdp.py b/pyomo/contrib/piecewise/tests/test_outer_repn_gdp.py index edc5d9d3d95..5ee18875cb9 100644 --- a/pyomo/contrib/piecewise/tests/test_outer_repn_gdp.py +++ b/pyomo/contrib/piecewise/tests/test_outer_repn_gdp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py b/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py index e740e5e3384..571601fefbc 100644 --- a/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/piecewise/tests/test_reduced_inner_repn.py b/pyomo/contrib/piecewise/tests/test_reduced_inner_repn.py index a2d41c04016..b70281c83ed 100644 --- a/pyomo/contrib/piecewise/tests/test_reduced_inner_repn.py +++ b/pyomo/contrib/piecewise/tests/test_reduced_inner_repn.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/piecewise/transform/__init__.py b/pyomo/contrib/piecewise/transform/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/piecewise/transform/__init__.py +++ b/pyomo/contrib/piecewise/transform/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/piecewise/transform/convex_combination.py b/pyomo/contrib/piecewise/transform/convex_combination.py index abfeac27129..21b72bd9e5d 100644 --- a/pyomo/contrib/piecewise/transform/convex_combination.py +++ b/pyomo/contrib/piecewise/transform/convex_combination.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/piecewise/transform/disaggregated_convex_combination.py b/pyomo/contrib/piecewise/transform/disaggregated_convex_combination.py index 44059935e09..0117bf1d045 100644 --- a/pyomo/contrib/piecewise/transform/disaggregated_convex_combination.py +++ b/pyomo/contrib/piecewise/transform/disaggregated_convex_combination.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py b/pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py new file mode 100644 index 00000000000..d582cdcfff5 --- /dev/null +++ b/pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py @@ -0,0 +1,202 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.contrib.piecewise.transform.piecewise_linear_transformation_base import ( + PiecewiseLinearTransformationBase, +) +from pyomo.core import Constraint, Binary, Var, RangeSet, Set +from pyomo.core.base import TransformationFactory +from pyomo.common.errors import DeveloperError +from math import ceil, log2 + + +@TransformationFactory.register( + "contrib.piecewise.disaggregated_logarithmic", + doc=""" + Represent a piecewise linear function "logarithmically" by using a MIP with + log_2(|P|) binary decision variables. This is a direct-to-MIP transformation; + GDP is not used. + """, +) +class DisaggregatedLogarithmicMIPTransformation(PiecewiseLinearTransformationBase): + """ + Represent a piecewise linear function "logarithmically" by using a MIP with + log_2(|P|) binary decision variables, following the "disaggregated logarithmic" + method from [1]. This is a direct-to-MIP transformation; GDP is not used. + This method of logarithmically formulating the piecewise linear function + imposes no restrictions on the family of polytopes, but we assume we have + simplices in this code. + + References + ---------- + [1] J.P. Vielma, S. Ahmed, and G. Nemhauser, "Mixed-integer models + for nonseparable piecewise-linear optimization: unifying framework + and extensions," Operations Research, vol. 58, no. 2, pp. 305-315, + 2010. + """ + + CONFIG = PiecewiseLinearTransformationBase.CONFIG() + _transformation_name = "pw_linear_disaggregated_log" + + # Implement to use PiecewiseLinearTransformationBase. This function returns the Var + # that replaces the transformed piecewise linear expr + def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): + # Get a new Block for our transformation in transformation_block.transformed_functions, + # which is a Block(Any). This is where we will put our new components. + transBlock = transformation_block.transformed_functions[ + len(transformation_block.transformed_functions) + ] + + # Dimensionality of the PWLF + dimension = pw_expr.nargs() + transBlock.dimension_indices = RangeSet(0, dimension - 1) + + # Substitute Var that will hold the value of the PWLE + substitute_var = transBlock.substitute_var = Var() + pw_linear_func.map_transformation_var(pw_expr, substitute_var) + + # Bounds for the substitute_var that we will widen + substitute_var_lb = float("inf") + substitute_var_ub = -float("inf") + + # Simplices are tuples of indices of points. Give them their own indices, too + simplices = pw_linear_func._simplices + num_simplices = len(simplices) + transBlock.simplex_indices = RangeSet(0, num_simplices - 1) + # Assumption: the simplices are really full-dimensional simplices and all have the + # same number of points, which is dimension + 1 + transBlock.simplex_point_indices = RangeSet(0, dimension) + + # Enumeration of simplices: map from simplex number to simplex object + idx_to_simplex = {k: v for k, v in zip(transBlock.simplex_indices, simplices)} + + # List of tuples of simplex indices with their linear function + simplex_indices_and_lin_funcs = list( + zip(transBlock.simplex_indices, pw_linear_func._linear_functions) + ) + + # We don't seem to get a convenient opportunity later, so let's just widen + # the bounds here. All we need to do is go through the corners of each simplex. + for P, linear_func in simplex_indices_and_lin_funcs: + for v in transBlock.simplex_point_indices: + val = linear_func(*pw_linear_func._points[idx_to_simplex[P][v]]) + if val < substitute_var_lb: + substitute_var_lb = val + if val > substitute_var_ub: + substitute_var_ub = val + transBlock.substitute_var.setlb(substitute_var_lb) + transBlock.substitute_var.setub(substitute_var_ub) + + log_dimension = ceil(log2(num_simplices)) + transBlock.log_simplex_indices = RangeSet(0, log_dimension - 1) + transBlock.binaries = Var(transBlock.log_simplex_indices, domain=Binary) + + # Injective function B: \mathcal{P} -> {0,1}^ceil(log_2(|P|)) used to identify simplices + # (really just polytopes are required) with binary vectors. Any injective function + # is enough here. + B = {} + for i in transBlock.simplex_indices: + # map index(P) -> corresponding vector in {0, 1}^n + B[i] = self._get_binary_vector(i, log_dimension) + + # Build up P_0 and P_plus ahead of time. + + # {P \in \mathcal{P} | B(P)_l = 0} + def P_0_init(m, l): + return [p for p in transBlock.simplex_indices if B[p][l] == 0] + + transBlock.P_0 = Set(transBlock.log_simplex_indices, initialize=P_0_init) + + # {P \in \mathcal{P} | B(P)_l = 1} + def P_plus_init(m, l): + return [p for p in transBlock.simplex_indices if B[p][l] == 1] + + transBlock.P_plus = Set(transBlock.log_simplex_indices, initialize=P_plus_init) + + # The lambda variables \lambda_{P,v} are indexed by the simplex and the point in it + transBlock.lambdas = Var( + transBlock.simplex_indices, transBlock.simplex_point_indices, bounds=(0, 1) + ) + + # Numbered citations are from Vielma et al 2010, Mixed-Integer Models + # for Nonseparable Piecewise-Linear Optimization + + # Sum of all lambdas is one (6b) + transBlock.convex_combo = Constraint( + expr=sum( + transBlock.lambdas[P, v] + for P in transBlock.simplex_indices + for v in transBlock.simplex_point_indices + ) + == 1 + ) + + # The branching rules, establishing using the binaries that only one simplex's lambda + # coefficients may be nonzero + # Enabling lambdas when binaries are on + @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.1) + def simplex_choice_1(b, l): + return ( + sum( + transBlock.lambdas[P, v] + for P in transBlock.P_plus[l] + for v in transBlock.simplex_point_indices + ) + <= transBlock.binaries[l] + ) + + # Disabling lambdas when binaries are on + @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.2) + def simplex_choice_2(b, l): + return ( + sum( + transBlock.lambdas[P, v] + for P in transBlock.P_0[l] + for v in transBlock.simplex_point_indices + ) + <= 1 - transBlock.binaries[l] + ) + + # for i, (simplex, pwlf) in enumerate(choices): + # x_i = sum(lambda_P,v v_i, P in polytopes, v in V(P)) + @transBlock.Constraint(transBlock.dimension_indices) # (6a.1) + def x_constraint(b, i): + return pw_expr.args[i] == sum( + transBlock.lambdas[P, v] + * pw_linear_func._points[idx_to_simplex[P][v]][i] + for P in transBlock.simplex_indices + for v in transBlock.simplex_point_indices + ) + + # Make the substitute Var equal the PWLE (6a.2) + transBlock.set_substitute = Constraint( + expr=substitute_var + == sum( + transBlock.lambdas[P, v] + * linear_func(*pw_linear_func._points[idx_to_simplex[P][v]]) + for v in transBlock.simplex_point_indices + for (P, linear_func) in simplex_indices_and_lin_funcs + ) + ) + + return substitute_var + + # Not a Gray code, just a regular binary representation + # TODO test the Gray codes too + # note: Must have num != 0 and ceil(log2(num)) > length to be valid + def _get_binary_vector(self, num, length): + ans = [] + for i in range(length): + ans.append(num & 1) + num >>= 1 + assert not num + ans.reverse() + return tuple(ans) diff --git a/pyomo/contrib/piecewise/transform/inner_representation_gdp.py b/pyomo/contrib/piecewise/transform/inner_representation_gdp.py index 627e41aeae9..e4818c1cbb9 100644 --- a/pyomo/contrib/piecewise/transform/inner_representation_gdp.py +++ b/pyomo/contrib/piecewise/transform/inner_representation_gdp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -10,8 +10,8 @@ # ___________________________________________________________________________ from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr -from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( - PiecewiseLinearToGDP, +from pyomo.contrib.piecewise.transform.piecewise_linear_transformation_base import ( + PiecewiseLinearTransformationBase, ) from pyomo.core import Constraint, NonNegativeIntegers, Suffix, Var from pyomo.core.base import TransformationFactory @@ -25,7 +25,7 @@ "simplices that are the domains of the linear " "functions.", ) -class InnerRepresentationGDPTransformation(PiecewiseLinearToGDP): +class InnerRepresentationGDPTransformation(PiecewiseLinearTransformationBase): """ Convert a model involving piecewise linear expressions into a GDP by representing the piecewise linear functions as Disjunctions where the @@ -49,7 +49,7 @@ class InnerRepresentationGDPTransformation(PiecewiseLinearToGDP): this mode, targets must be Blocks, Constraints, and/or Objectives. """ - CONFIG = PiecewiseLinearToGDP.CONFIG() + CONFIG = PiecewiseLinearTransformationBase.CONFIG() _transformation_name = 'pw_linear_inner_repn' def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): diff --git a/pyomo/contrib/piecewise/transform/multiple_choice.py b/pyomo/contrib/piecewise/transform/multiple_choice.py index 97dc8e9d2b3..9291afa8862 100644 --- a/pyomo/contrib/piecewise/transform/multiple_choice.py +++ b/pyomo/contrib/piecewise/transform/multiple_choice.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py new file mode 100644 index 00000000000..dbbd8c73bad --- /dev/null +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -0,0 +1,209 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr +from pyomo.contrib.piecewise.transform.piecewise_linear_transformation_base import ( + PiecewiseLinearTransformationBase, +) +from pyomo.core import Constraint, NonNegativeIntegers, Suffix, Var +from pyomo.core.base import TransformationFactory +from pyomo.gdp import Disjunction +from pyomo.common.errors import DeveloperError + + +@TransformationFactory.register( + "contrib.piecewise.nested_inner_repn_gdp", + doc=""" + Represent a piecewise linear function by using a nested GDP to determine + which polytope a point is in, then representing it as a convex combination + of extreme points, with multipliers "local" to that particular polytope, + i.e., not shared with neighbors. This formulation has linearly many Boolean + variables, though up to variable substitution, it has logarithmically many. + """, +) +class NestedInnerRepresentationGDPTransformation(PiecewiseLinearTransformationBase): + """ + Represent a piecewise linear function by using a nested GDP to determine + which polytope a point is in, then representing it as a convex combination + of extreme points, with multipliers "local" to that particular polytope, + i.e., not shared with neighbors. This method of formulating the piecewise + linear function imposes no restrictions on the family of polytopes. Note + that this is NOT a logarithmic formulation - it has linearly many Boolean + variables. However, it is inspired by the disaggregated logarithmic + formulation of [1]. Up to variable substitution, the amount of Boolean + variables is logarithmic, as in [1]. + + References + ---------- + [1] J.P. Vielma, S. Ahmed, and G. Nemhauser, "Mixed-integer models + for nonseparable piecewise-linear optimization: unifying framework + and extensions," Operations Research, vol. 58, no. 2, pp. 305-315, + 2010. + """ + + CONFIG = PiecewiseLinearTransformationBase.CONFIG() + _transformation_name = "pw_linear_nested_inner_repn" + + # Implement to use PiecewiseLinearTransformationBase. This function returns the Var + # that replaces the transformed piecewise linear expr + def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): + # Get a new Block() in transformation_block.transformed_functions, which + # is a Block(Any) + transBlock = transformation_block.transformed_functions[ + len(transformation_block.transformed_functions) + ] + + substitute_var = transBlock.substitute_var = Var() + pw_linear_func.map_transformation_var(pw_expr, substitute_var) + transBlock.substitute_var_lb = float("inf") + transBlock.substitute_var_ub = -float("inf") + + choices = list(zip(pw_linear_func._simplices, pw_linear_func._linear_functions)) + + # If there was only one choice, don't bother making a disjunction, just + # use the linear function directly (but still use the substitute_var for + # consistency). + if len(choices) == 1: + (_, linear_func) = choices[0] # simplex isn't important in this case + linear_func_expr = linear_func(*pw_expr.args) + transBlock.set_substitute = Constraint( + expr=substitute_var == linear_func_expr + ) + (transBlock.substitute_var_lb, transBlock.substitute_var_ub) = ( + compute_bounds_on_expr(linear_func_expr) + ) + else: + # Add the disjunction + transBlock.disj = self._get_disjunction( + choices, transBlock, pw_expr, pw_linear_func, transBlock + ) + + # Set bounds as determined when setting up the disjunction + if transBlock.substitute_var_lb < float("inf"): + transBlock.substitute_var.setlb(transBlock.substitute_var_lb) + if transBlock.substitute_var_ub > -float("inf"): + transBlock.substitute_var.setub(transBlock.substitute_var_ub) + + return substitute_var + + # Recursively form the Disjunctions and Disjuncts. This shouldn't blow up + # the stack, since the whole point is that we'll only go logarithmically + # many calls deep. + def _get_disjunction( + self, choices, parent_block, pw_expr, pw_linear_func, root_block + ): + size = len(choices) + + # Our base cases will be 3 and 2, since it would be silly to construct + # a Disjunction containing only one Disjunct. We can ensure that size + # is never 1 unless it was only passed a single choice from the start, + # which we can handle before calling. + if size > 3: + half = size // 2 # (integer divide) + # This tree will be slightly heavier on the right side + choices_l = choices[:half] + choices_r = choices[half:] + + @parent_block.Disjunct() + def d_l(b): + b.inner_disjunction_l = self._get_disjunction( + choices_l, b, pw_expr, pw_linear_func, root_block + ) + + @parent_block.Disjunct() + def d_r(b): + b.inner_disjunction_r = self._get_disjunction( + choices_r, b, pw_expr, pw_linear_func, root_block + ) + + return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) + elif size == 3: + # Let's stay heavier on the right side for consistency. So the left + # Disjunct will be the one to contain constraints, rather than a + # Disjunction + @parent_block.Disjunct() + def d_l(b): + simplex, linear_func = choices[0] + self._set_disjunct_block_constraints( + b, simplex, linear_func, pw_expr, pw_linear_func, root_block + ) + + @parent_block.Disjunct() + def d_r(b): + b.inner_disjunction_r = self._get_disjunction( + choices[1:], b, pw_expr, pw_linear_func, root_block + ) + + return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) + elif size == 2: + # In this case both sides are regular Disjuncts + @parent_block.Disjunct() + def d_l(b): + simplex, linear_func = choices[0] + self._set_disjunct_block_constraints( + b, simplex, linear_func, pw_expr, pw_linear_func, root_block + ) + + @parent_block.Disjunct() + def d_r(b): + simplex, linear_func = choices[1] + self._set_disjunct_block_constraints( + b, simplex, linear_func, pw_expr, pw_linear_func, root_block + ) + + return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) + else: + raise DeveloperError( + "Unreachable: 1 or 0 choices were passed to " + "_get_disjunction in nested_inner_repn.py." + ) + + def _set_disjunct_block_constraints( + self, b, simplex, linear_func, pw_expr, pw_linear_func, root_block + ): + # Define the lambdas sparsely like in the normal inner repn, + # only the first few will participate in constraints + b.lambdas = Var(NonNegativeIntegers, dense=False, bounds=(0, 1)) + + # Get the extreme points to add up + extreme_pts = [] + for idx in simplex: + extreme_pts.append(pw_linear_func._points[idx]) + + # Constrain sum(lambda_i) = 1 + b.convex_combo = Constraint( + expr=sum(b.lambdas[i] for i in range(len(extreme_pts))) == 1 + ) + linear_func_expr = linear_func(*pw_expr.args) + + # Make the substitute Var equal the PWLE + b.set_substitute = Constraint( + expr=root_block.substitute_var == linear_func_expr + ) + + # Widen the variable bounds to those of this linear func expression + (lb, ub) = compute_bounds_on_expr(linear_func_expr) + if lb is not None and lb < root_block.substitute_var_lb: + root_block.substitute_var_lb = lb + if ub is not None and ub > root_block.substitute_var_ub: + root_block.substitute_var_ub = ub + + # Constrain x = \sum \lambda_i v_i + @b.Constraint(range(pw_expr.nargs())) # dimension + def linear_combo(d, i): + return pw_expr.args[i] == sum( + d.lambdas[j] * pt[i] for j, pt in enumerate(extreme_pts) + ) + + # Mark the lambdas as local in order to prevent disagreggating multiple + # times in the hull transformation + b.LocalVars = Suffix(direction=Suffix.LOCAL) + b.LocalVars[b] = [v for v in b.lambdas.values()] diff --git a/pyomo/contrib/piecewise/transform/outer_representation_gdp.py b/pyomo/contrib/piecewise/transform/outer_representation_gdp.py index 04cd01e1246..6c26772fe6a 100644 --- a/pyomo/contrib/piecewise/transform/outer_representation_gdp.py +++ b/pyomo/contrib/piecewise/transform/outer_representation_gdp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -12,8 +12,8 @@ import pyomo.common.dependencies.numpy as np from pyomo.common.dependencies.scipy import spatial from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr -from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( - PiecewiseLinearToGDP, +from pyomo.contrib.piecewise.transform.piecewise_linear_transformation_base import ( + PiecewiseLinearTransformationBase, ) from pyomo.core import Constraint, NonNegativeIntegers, Suffix, Var from pyomo.core.base import TransformationFactory @@ -27,7 +27,7 @@ "the simplices that are the domains of the " "linear functions.", ) -class OuterRepresentationGDPTransformation(PiecewiseLinearToGDP): +class OuterRepresentationGDPTransformation(PiecewiseLinearTransformationBase): """ Convert a model involving piecewise linear expressions into a GDP by representing the piecewise linear functions as Disjunctions where the @@ -49,7 +49,7 @@ class OuterRepresentationGDPTransformation(PiecewiseLinearToGDP): this mode, targets must be Blocks, Constraints, and/or Objectives. """ - CONFIG = PiecewiseLinearToGDP.CONFIG() + CONFIG = PiecewiseLinearTransformationBase.CONFIG() _transformation_name = 'pw_linear_outer_repn' def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): diff --git a/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py b/pyomo/contrib/piecewise/transform/piecewise_linear_transformation_base.py similarity index 98% rename from pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py rename to pyomo/contrib/piecewise/transform/piecewise_linear_transformation_base.py index ed4902ae6d5..7e96891bbc4 100644 --- a/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py +++ b/pyomo/contrib/piecewise/transform/piecewise_linear_transformation_base.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -33,14 +33,14 @@ Any, ) from pyomo.core.base import Transformation -from pyomo.core.base.block import _BlockData, Block +from pyomo.core.base.block import Block from pyomo.core.util import target_list from pyomo.gdp import Disjunct, Disjunction from pyomo.gdp.util import is_child_of from pyomo.network import Port -class PiecewiseLinearToGDP(Transformation): +class PiecewiseLinearTransformationBase(Transformation): """ Base class for transformations of piecewise-linear models to GDPs """ @@ -147,7 +147,7 @@ def _apply_to_impl(self, instance, **kwds): self._transform_piecewise_linear_function( t, config.descend_into_expressions ) - elif t.ctype is Block or isinstance(t, _BlockData): + elif issubclass(t.ctype, Block): self._transform_block(t, config.descend_into_expressions) elif t.ctype is Constraint: if not config.descend_into_expressions: diff --git a/pyomo/contrib/piecewise/transform/piecewise_to_mip_visitor.py b/pyomo/contrib/piecewise/transform/piecewise_to_mip_visitor.py index e3347cf206a..fae95a564bf 100644 --- a/pyomo/contrib/piecewise/transform/piecewise_to_mip_visitor.py +++ b/pyomo/contrib/piecewise/transform/piecewise_to_mip_visitor.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/piecewise/transform/reduced_inner_representation_gdp.py b/pyomo/contrib/piecewise/transform/reduced_inner_representation_gdp.py index b89852530d9..a19507a93fd 100644 --- a/pyomo/contrib/piecewise/transform/reduced_inner_representation_gdp.py +++ b/pyomo/contrib/piecewise/transform/reduced_inner_representation_gdp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -10,8 +10,8 @@ # ___________________________________________________________________________ from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr -from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( - PiecewiseLinearToGDP, +from pyomo.contrib.piecewise.transform.piecewise_linear_transformation_base import ( + PiecewiseLinearTransformationBase, ) from pyomo.core import Constraint, NonNegativeIntegers, Var from pyomo.core.base import TransformationFactory @@ -25,7 +25,7 @@ "simplices that are the domains of the linear " "functions.", ) -class ReducedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): +class ReducedInnerRepresentationGDPTransformation(PiecewiseLinearTransformationBase): """ Convert a model involving piecewise linear expressions into a GDP by representing the piecewise linear functions as Disjunctions where the @@ -51,7 +51,7 @@ class ReducedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): this mode, targets must be Blocks, Constraints, and/or Objectives. """ - CONFIG = PiecewiseLinearToGDP.CONFIG() + CONFIG = PiecewiseLinearTransformationBase.CONFIG() _transformation_name = 'pw_linear_reduced_inner_repn' def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): diff --git a/pyomo/contrib/preprocessing/__init__.py b/pyomo/contrib/preprocessing/__init__.py index dcd444ad312..6458b7a6e71 100644 --- a/pyomo/contrib/preprocessing/__init__.py +++ b/pyomo/contrib/preprocessing/__init__.py @@ -1 +1,12 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.contrib.preprocessing.plugins diff --git a/pyomo/contrib/preprocessing/plugins/__init__.py b/pyomo/contrib/preprocessing/plugins/__init__.py index 12eee351308..62f5a40c6a9 100644 --- a/pyomo/contrib/preprocessing/plugins/__init__.py +++ b/pyomo/contrib/preprocessing/plugins/__init__.py @@ -1,3 +1,15 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + def load(): import pyomo.contrib.preprocessing.plugins.deactivate_trivial_constraints import pyomo.contrib.preprocessing.plugins.detect_fixed_vars diff --git a/pyomo/contrib/preprocessing/plugins/bounds_to_vars.py b/pyomo/contrib/preprocessing/plugins/bounds_to_vars.py index ece2376774c..8cc17296ac3 100644 --- a/pyomo/contrib/preprocessing/plugins/bounds_to_vars.py +++ b/pyomo/contrib/preprocessing/plugins/bounds_to_vars.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -11,7 +11,6 @@ """Transformation to convert explicit bounds to variable bounds.""" -from __future__ import division from math import fabs import math diff --git a/pyomo/contrib/preprocessing/plugins/constraint_tightener.py b/pyomo/contrib/preprocessing/plugins/constraint_tightener.py index 4c8b28e0319..73851bce618 100644 --- a/pyomo/contrib/preprocessing/plugins/constraint_tightener.py +++ b/pyomo/contrib/preprocessing/plugins/constraint_tightener.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import logging from pyomo.common import deprecated diff --git a/pyomo/contrib/preprocessing/plugins/deactivate_trivial_constraints.py b/pyomo/contrib/preprocessing/plugins/deactivate_trivial_constraints.py index a91e0a292f2..59e475e9ba1 100644 --- a/pyomo/contrib/preprocessing/plugins/deactivate_trivial_constraints.py +++ b/pyomo/contrib/preprocessing/plugins/deactivate_trivial_constraints.py @@ -2,7 +2,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/plugins/detect_fixed_vars.py b/pyomo/contrib/preprocessing/plugins/detect_fixed_vars.py index bafbec7b8bd..e48914e0a91 100644 --- a/pyomo/contrib/preprocessing/plugins/detect_fixed_vars.py +++ b/pyomo/contrib/preprocessing/plugins/detect_fixed_vars.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/plugins/equality_propagate.py b/pyomo/contrib/preprocessing/plugins/equality_propagate.py index 03e2e11dadb..357a556fcb2 100644 --- a/pyomo/contrib/preprocessing/plugins/equality_propagate.py +++ b/pyomo/contrib/preprocessing/plugins/equality_propagate.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/plugins/induced_linearity.py b/pyomo/contrib/preprocessing/plugins/induced_linearity.py index 88c062fdee2..ba291070644 100644 --- a/pyomo/contrib/preprocessing/plugins/induced_linearity.py +++ b/pyomo/contrib/preprocessing/plugins/induced_linearity.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -17,7 +17,6 @@ """ -from __future__ import division import logging import textwrap diff --git a/pyomo/contrib/preprocessing/plugins/init_vars.py b/pyomo/contrib/preprocessing/plugins/init_vars.py index 2b37e13e4cd..a81d898d52c 100644 --- a/pyomo/contrib/preprocessing/plugins/init_vars.py +++ b/pyomo/contrib/preprocessing/plugins/init_vars.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -10,7 +10,7 @@ # ___________________________________________________________________________ """Automatically initialize variables.""" -from __future__ import division + from pyomo.core.base.var import Var from pyomo.core.base.transformation import TransformationFactory diff --git a/pyomo/contrib/preprocessing/plugins/int_to_binary.py b/pyomo/contrib/preprocessing/plugins/int_to_binary.py index 8b264868ba5..e1f7f98a81b 100644 --- a/pyomo/contrib/preprocessing/plugins/int_to_binary.py +++ b/pyomo/contrib/preprocessing/plugins/int_to_binary.py @@ -1,5 +1,15 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Transformation to reformulate integer variables into binary.""" -from __future__ import division from math import floor, log import logging diff --git a/pyomo/contrib/preprocessing/plugins/remove_zero_terms.py b/pyomo/contrib/preprocessing/plugins/remove_zero_terms.py index 7cce719f98d..ca2052fa471 100644 --- a/pyomo/contrib/preprocessing/plugins/remove_zero_terms.py +++ b/pyomo/contrib/preprocessing/plugins/remove_zero_terms.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -11,7 +11,7 @@ # -*- coding: UTF-8 -*- """Transformation to remove zero terms from constraints.""" -from __future__ import division + from pyomo.core import quicksum from pyomo.core.base.constraint import Constraint diff --git a/pyomo/contrib/preprocessing/plugins/strip_bounds.py b/pyomo/contrib/preprocessing/plugins/strip_bounds.py index 51704bc9d58..196de64e405 100644 --- a/pyomo/contrib/preprocessing/plugins/strip_bounds.py +++ b/pyomo/contrib/preprocessing/plugins/strip_bounds.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/plugins/var_aggregator.py b/pyomo/contrib/preprocessing/plugins/var_aggregator.py index 0a429cb5a67..3430d29de3a 100644 --- a/pyomo/contrib/preprocessing/plugins/var_aggregator.py +++ b/pyomo/contrib/preprocessing/plugins/var_aggregator.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -11,10 +11,16 @@ """Transformation to aggregate equal variables.""" -from __future__ import division from pyomo.common.collections import ComponentMap, ComponentSet -from pyomo.core.base import Block, Constraint, VarList, Objective, TransformationFactory +from pyomo.core.base import ( + Block, + Constraint, + VarList, + Objective, + Reals, + TransformationFactory, +) from pyomo.core.expr import ExpressionReplacementVisitor from pyomo.core.expr.numvalue import value from pyomo.core.plugins.transform.hierarchy import IsomorphicTransformation @@ -249,6 +255,12 @@ def _apply_to(self, model, detect_fixed_vars=True): # the variables in its equality set. z_agg.setlb(max_if_not_None(v.lb for v in eq_set if v.has_lb())) z_agg.setub(min_if_not_None(v.ub for v in eq_set if v.has_ub())) + # Set the domain of the aggregate variable to the intersection of + # the domains of the variables in its equality set + domain = Reals + for v in eq_set: + domain = domain & v.domain + z_agg.domain = domain # Set the fixed status of the aggregate var fixed_vars = [v for v in eq_set if v.fixed] diff --git a/pyomo/contrib/preprocessing/plugins/zero_sum_propagator.py b/pyomo/contrib/preprocessing/plugins/zero_sum_propagator.py index 16c6614cb3b..df6867719d2 100644 --- a/pyomo/contrib/preprocessing/plugins/zero_sum_propagator.py +++ b/pyomo/contrib/preprocessing/plugins/zero_sum_propagator.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/tests/__init__.py b/pyomo/contrib/preprocessing/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/preprocessing/tests/__init__.py +++ b/pyomo/contrib/preprocessing/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/preprocessing/tests/test_bounds_to_vars_xfrm.py b/pyomo/contrib/preprocessing/tests/test_bounds_to_vars_xfrm.py index 5770b23eb11..0df9dd2462d 100644 --- a/pyomo/contrib/preprocessing/tests/test_bounds_to_vars_xfrm.py +++ b/pyomo/contrib/preprocessing/tests/test_bounds_to_vars_xfrm.py @@ -1,4 +1,16 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Tests explicit bound to variable bound transformation module.""" + import pyomo.common.unittest as unittest from pyomo.environ import ( ConcreteModel, diff --git a/pyomo/contrib/preprocessing/tests/test_constraint_tightener.py b/pyomo/contrib/preprocessing/tests/test_constraint_tightener.py index aa7fa52d272..acb939552f8 100644 --- a/pyomo/contrib/preprocessing/tests/test_constraint_tightener.py +++ b/pyomo/contrib/preprocessing/tests/test_constraint_tightener.py @@ -1,4 +1,16 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Tests the Bounds Tightening module.""" + import pyomo.common.unittest as unittest from pyomo.environ import ConcreteModel, Constraint, TransformationFactory, Var, value diff --git a/pyomo/contrib/preprocessing/tests/test_deactivate_trivial_constraints.py b/pyomo/contrib/preprocessing/tests/test_deactivate_trivial_constraints.py index fa0ca6cfa9a..9e26aab8b77 100644 --- a/pyomo/contrib/preprocessing/tests/test_deactivate_trivial_constraints.py +++ b/pyomo/contrib/preprocessing/tests/test_deactivate_trivial_constraints.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """Tests deactivation of trivial constraints.""" import pyomo.common.unittest as unittest diff --git a/pyomo/contrib/preprocessing/tests/test_detect_fixed_vars.py b/pyomo/contrib/preprocessing/tests/test_detect_fixed_vars.py index d40206d621b..a67291dc69f 100644 --- a/pyomo/contrib/preprocessing/tests/test_detect_fixed_vars.py +++ b/pyomo/contrib/preprocessing/tests/test_detect_fixed_vars.py @@ -1,4 +1,16 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Tests detection of fixed variables.""" + import pyomo.common.unittest as unittest from pyomo.environ import ConcreteModel, TransformationFactory, Var, value diff --git a/pyomo/contrib/preprocessing/tests/test_equality_propagate.py b/pyomo/contrib/preprocessing/tests/test_equality_propagate.py index 40e1d7eecb9..6b12f464710 100644 --- a/pyomo/contrib/preprocessing/tests/test_equality_propagate.py +++ b/pyomo/contrib/preprocessing/tests/test_equality_propagate.py @@ -1,4 +1,16 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Tests the equality set propagation module.""" + import pyomo.common.unittest as unittest from pyomo.common.errors import InfeasibleConstraintException diff --git a/pyomo/contrib/preprocessing/tests/test_induced_linearity.py b/pyomo/contrib/preprocessing/tests/test_induced_linearity.py index c2c24c33f14..4853cb838df 100644 --- a/pyomo/contrib/preprocessing/tests/test_induced_linearity.py +++ b/pyomo/contrib/preprocessing/tests/test_induced_linearity.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/tests/test_init_vars.py b/pyomo/contrib/preprocessing/tests/test_init_vars.py index f65773f7dbb..a90d39af91c 100644 --- a/pyomo/contrib/preprocessing/tests/test_init_vars.py +++ b/pyomo/contrib/preprocessing/tests/test_init_vars.py @@ -1,4 +1,16 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Tests initialization of uninitialized variables.""" + import pyomo.common.unittest as unittest from pyomo.environ import ConcreteModel, TransformationFactory, value, Var diff --git a/pyomo/contrib/preprocessing/tests/test_int_to_binary.py b/pyomo/contrib/preprocessing/tests/test_int_to_binary.py index bb75a075592..8aa244212ed 100644 --- a/pyomo/contrib/preprocessing/tests/test_int_to_binary.py +++ b/pyomo/contrib/preprocessing/tests/test_int_to_binary.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/tests/test_strip_bounds.py b/pyomo/contrib/preprocessing/tests/test_strip_bounds.py index deb1b6c8b37..f36ff4e9f52 100644 --- a/pyomo/contrib/preprocessing/tests/test_strip_bounds.py +++ b/pyomo/contrib/preprocessing/tests/test_strip_bounds.py @@ -1,4 +1,16 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Tests stripping of variable bounds.""" + import pyomo.common.unittest as unittest from pyomo.environ import ( diff --git a/pyomo/contrib/preprocessing/tests/test_var_aggregator.py b/pyomo/contrib/preprocessing/tests/test_var_aggregator.py index d44f8abdeb2..b0b672b76b0 100644 --- a/pyomo/contrib/preprocessing/tests/test_var_aggregator.py +++ b/pyomo/contrib/preprocessing/tests/test_var_aggregator.py @@ -1,4 +1,16 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Tests the variable aggregation module.""" + import pyomo.common.unittest as unittest from pyomo.common.collections import ComponentSet from pyomo.contrib.preprocessing.plugins.var_aggregator import ( @@ -7,12 +19,16 @@ max_if_not_None, min_if_not_None, ) +from pyomo.core.expr.compare import assertExpressionsEqual from pyomo.environ import ( + Binary, ConcreteModel, Constraint, ConstraintList, + maximize, Objective, RangeSet, + Reals, SolverFactory, TransformationFactory, Var, @@ -198,6 +214,36 @@ def test_var_update(self): self.assertEqual(m.x.value, 0) self.assertEqual(m.y.value, 0) + def test_binary_inequality(self): + m = ConcreteModel() + m.x = Var(domain=Binary) + m.y = Var(domain=Binary) + m.c = Constraint(expr=m.x == m.y) + m.o = Objective(expr=0.5 * m.x + m.y, sense=maximize) + TransformationFactory('contrib.aggregate_vars').apply_to(m) + var_to_z = m._var_aggregator_info.var_to_z + z = var_to_z[m.x] + self.assertIs(var_to_z[m.y], z) + self.assertEqual(z.domain, Binary) + self.assertEqual(z.lb, 0) + self.assertEqual(z.ub, 1) + assertExpressionsEqual(self, m.o.expr, 0.5 * z + z) + + def test_equality_different_domains(self): + m = ConcreteModel() + m.x = Var(domain=Reals, bounds=(1, 2)) + m.y = Var(domain=Binary) + m.c = Constraint(expr=m.x == m.y) + m.o = Objective(expr=0.5 * m.x + m.y, sense=maximize) + TransformationFactory('contrib.aggregate_vars').apply_to(m) + var_to_z = m._var_aggregator_info.var_to_z + z = var_to_z[m.x] + self.assertIs(var_to_z[m.y], z) + self.assertEqual(z.lb, 1) + self.assertEqual(z.ub, 1) + self.assertEqual(z.domain, Binary) + assertExpressionsEqual(self, m.o.expr, 0.5 * z + z) + if __name__ == '__main__': unittest.main() diff --git a/pyomo/contrib/preprocessing/tests/test_zero_sum_propagate.py b/pyomo/contrib/preprocessing/tests/test_zero_sum_propagate.py index e5dc132628b..41ece8e804f 100644 --- a/pyomo/contrib/preprocessing/tests/test_zero_sum_propagate.py +++ b/pyomo/contrib/preprocessing/tests/test_zero_sum_propagate.py @@ -1,4 +1,16 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Tests the zero sum propagation module.""" + import pyomo.common.unittest as unittest from pyomo.environ import ( ConcreteModel, diff --git a/pyomo/contrib/preprocessing/tests/test_zero_term_removal.py b/pyomo/contrib/preprocessing/tests/test_zero_term_removal.py index 7ff40b6ae32..c5b7477c8f6 100644 --- a/pyomo/contrib/preprocessing/tests/test_zero_term_removal.py +++ b/pyomo/contrib/preprocessing/tests/test_zero_term_removal.py @@ -1,4 +1,16 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Tests detection of zero terms.""" + import pyomo.common.unittest as unittest from pyomo.environ import ConcreteModel, Constraint, TransformationFactory, Var import pyomo.core.expr as EXPR diff --git a/pyomo/contrib/preprocessing/util.py b/pyomo/contrib/preprocessing/util.py index 69182f56656..13f3e5dd18c 100644 --- a/pyomo/contrib/preprocessing/util.py +++ b/pyomo/contrib/preprocessing/util.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import logging from io import StringIO diff --git a/pyomo/contrib/pynumero/README.md b/pyomo/contrib/pynumero/README.md index 0d165dbc39c..f881e400d51 100644 --- a/pyomo/contrib/pynumero/README.md +++ b/pyomo/contrib/pynumero/README.md @@ -71,3 +71,75 @@ Prerequisites - cmake - a C/C++ compiler - MA57 library or COIN-HSL Full + +Code organization +================= + +PyNumero was initially designed around three core components: linear solver +interfaces, an interface for function and derivative callbacks, and block +vector and matrix classes. Since then, it has incorporated additional +functionality in an ad-hoc manner. The original "core functionality" of +PyNumero, as well as the solver interfaces accessible through +`SolverFactory`, should be considered stable and will only change after +appropriate deprecation warnings. Other functionality should be considered +experimental and subject to change without warning. + +The following is a rough overview of PyNumero, by directory: + +`linalg` +-------- + +Python interfaces to linear solvers. This is core functionality. + +`interfaces` +------------ + +- Classes that define and implement an API for function and derivative callbacks +required by nonlinear optimization solvers, e.g. `nlp.py` and `pyomo_nlp.py` +- Various wrappers around these NLP classes to support "hybrid" implementations, +e.g. `PyomoNLPWithGreyBoxBlocks` +- The `ExternalGreyBoxBlock` Pyomo modeling component and +`ExternalGreyBoxModel` API +- The `ExternalPyomoModel` implementation of `ExternalGreyBoxModel`, which allows +definition of an external grey box via an implicit function +- The `CyIpoptNLP` class, which wraps an object implementing the NLP API in +the interface required by CyIpopt + +Of the above, only `PyomoNLP` and the `NLP` base class should be considered core +functionality. + +`src` +----- + +C++ interfaces to ASL, MA27, and MA57. The ASL and MA27 interfaces are +core functionality. + +`sparse` +-------- + +Block vector and block matrix classes, including MPI variations. +These are core functionality. + +`algorithms` +------------ + +Originally intended to hold various useful algorithms implemented +on NLP objects rather than Pyomo models. Any files added here should +be considered experimental. + +`algorithms/solvers` +-------------------- + +Interfaces to Python solvers using the NLP API defined in `interfaces`. +Only the solvers accessible through `SolverFactory`, e.g. `PyomoCyIpoptSolver` +and `PyomoFsolveSolver`, should be considered core functionality. +The supported way to access these solvers is via `SolverFactory`. *The locations +of the underlying solver objects are subject to change without warning.* + +`examples` +---------- + +The examples demonstrated in `nlp_interface.py`, `nlp_interface_2.py1`, +`feasibility.py`, `mumps_example.py`, `sensitivity.py`, `sqp.py`, +`parallel_matvec.py`, and `parallel_vector_ops.py` are stable. All other +examples should be considered experimental. diff --git a/pyomo/contrib/pynumero/__init__.py b/pyomo/contrib/pynumero/__init__.py index 9364a552999..39ee2197cbf 100644 --- a/pyomo/contrib/pynumero/__init__.py +++ b/pyomo/contrib/pynumero/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/algorithms/__init__.py b/pyomo/contrib/pynumero/algorithms/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/pynumero/algorithms/__init__.py +++ b/pyomo/contrib/pynumero/algorithms/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/algorithms/solvers/__init__.py b/pyomo/contrib/pynumero/algorithms/solvers/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/__init__.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py b/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py index 766ef96322a..0999550711c 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -24,6 +24,8 @@ from pyomo.common.deprecation import relocated_module_attribute from pyomo.common.dependencies import attempt_import, numpy as np, numpy_available from pyomo.common.tee import redirect_fd, TeeStream +from pyomo.common.modeling import unique_component_name +from pyomo.core.base.objective import Objective # Because pynumero.interfaces requires numpy, we will leverage deferred # imports here so that the solver can be registered even when numpy is @@ -63,7 +65,7 @@ from pyomo.common.config import ConfigBlock, ConfigValue from pyomo.common.timing import TicTocTimer from pyomo.core.base import Block, Objective, minimize -from pyomo.opt import SolverStatus, SolverResults, TerminationCondition, ProblemSense +from pyomo.opt import SolverStatus, SolverResults, TerminationCondition from pyomo.opt.results.solution import Solution logger = logging.getLogger(__name__) @@ -289,6 +291,13 @@ class PyomoCyIpoptSolver(object): description="Set the function that will be called each iteration.", ), ) + CONFIG.declare( + "halt_on_evaluation_error", + ConfigValue( + default=None, + description="Whether to halt if a function or derivative evaluation fails", + ), + ) def __init__(self, **kwds): """Create an instance of the CyIpoptSolver. You must @@ -310,7 +319,13 @@ def license_is_valid(self): return True def version(self): - return tuple(int(_) for _ in cyipopt.__version__.split(".")) + def _int(x): + try: + return int(x) + except: + return x + + return tuple(_int(_) for _ in cyipopt_interface.cyipopt.__version__.split(".")) def solve(self, model, **kwds): config = self.config(kwds, preserve_implicit=True) @@ -325,14 +340,27 @@ def solve(self, model, **kwds): grey_box_blocks = list( model.component_data_objects(egb.ExternalGreyBoxBlock, active=True) ) - if grey_box_blocks: - # nlp = pyomo_nlp.PyomoGreyBoxNLP(model) - nlp = pyomo_grey_box.PyomoNLPWithGreyBoxBlocks(model) - else: - nlp = pyomo_nlp.PyomoNLP(model) + # if there is no objective, add one temporarily so we can construct an NLP + objectives = list(model.component_data_objects(Objective, active=True)) + if not objectives: + objname = unique_component_name(model, "_obj") + objective = model.add_component(objname, Objective(expr=0.0)) + try: + if grey_box_blocks: + # nlp = pyomo_nlp.PyomoGreyBoxNLP(model) + nlp = pyomo_grey_box.PyomoNLPWithGreyBoxBlocks(model) + else: + nlp = pyomo_nlp.PyomoNLP(model) + finally: + # We only need the objective to construct the NLP, so we delete + # it from the model ASAP + if not objectives: + model.del_component(objective) problem = cyipopt_interface.CyIpoptNLP( - nlp, intermediate_callback=config.intermediate_callback + nlp, + intermediate_callback=config.intermediate_callback, + halt_on_evaluation_error=config.halt_on_evaluation_error, ) ng = len(problem.g_lb()) nx = len(problem.x_lb()) @@ -419,11 +447,10 @@ def solve(self, model, **kwds): results.problem.name = model.name obj = next(model.component_data_objects(Objective, active=True)) + results.problem.sense = obj.sense if obj.sense == minimize: - results.problem.sense = ProblemSense.minimize results.problem.upper_bound = info["obj_val"] else: - results.problem.sense = ProblemSense.maximize results.problem.lower_bound = info["obj_val"] results.problem.number_of_objectives = 1 results.problem.number_of_constraints = ng diff --git a/pyomo/contrib/pynumero/algorithms/solvers/implicit_functions.py b/pyomo/contrib/pynumero/algorithms/solvers/implicit_functions.py index e0bc0170d33..e40580c1161 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/implicit_functions.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/implicit_functions.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/algorithms/solvers/pyomo_ext_cyipopt.py b/pyomo/contrib/pynumero/algorithms/solvers/pyomo_ext_cyipopt.py index b234d2f0890..7f43f6ac7c0 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/pyomo_ext_cyipopt.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/pyomo_ext_cyipopt.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -16,7 +16,7 @@ from pyomo.contrib.pynumero.interfaces.pyomo_nlp import PyomoNLP from pyomo.contrib.pynumero.sparse.block_vector import BlockVector from pyomo.environ import Var, Constraint, value -from pyomo.core.base.var import _VarData +from pyomo.core.base.var import VarData from pyomo.common.modeling import unique_component_name """ @@ -109,12 +109,12 @@ def __init__( An instance of a derived class (from ExternalInputOutputModel) that provides the methods to compute the outputs and the derivatives. - inputs : list of Pyomo variables (_VarData) + inputs : list of Pyomo variables (VarData) The Pyomo model needs to have variables to represent the inputs to the external model. This is the list of those input variables in the order that corresponds to the input_values vector provided in the set_inputs call. - outputs : list of Pyomo variables (_VarData) + outputs : list of Pyomo variables (VarData) The Pyomo model needs to have variables to represent the outputs from the external model. This is the list of those output variables in the order that corresponds to the numpy array returned from the evaluate_outputs call. @@ -130,7 +130,7 @@ def __init__( # verify that the inputs and outputs were passed correctly self._inputs = [v for v in inputs] for v in self._inputs: - if not isinstance(v, _VarData): + if not isinstance(v, VarData): raise RuntimeError( 'Argument inputs passed to PyomoExternalCyIpoptProblem must be' ' a list of VarData objects. Note: if you have an indexed variable, pass' @@ -139,7 +139,7 @@ def __init__( self._outputs = [v for v in outputs] for v in self._outputs: - if not isinstance(v, _VarData): + if not isinstance(v, VarData): raise RuntimeError( 'Argument outputs passed to PyomoExternalCyIpoptProblem must be' ' a list of VarData objects. Note: if you have an indexed variable, pass' diff --git a/pyomo/contrib/pynumero/algorithms/solvers/scipy_solvers.py b/pyomo/contrib/pynumero/algorithms/solvers/scipy_solvers.py index 53f657c984f..ec1f106b73c 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/scipy_solvers.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/scipy_solvers.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/algorithms/solvers/square_solver_base.py b/pyomo/contrib/pynumero/algorithms/solvers/square_solver_base.py index c4a33d97611..1be3032c358 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/square_solver_base.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/square_solver_base.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/algorithms/solvers/tests/__init__.py b/pyomo/contrib/pynumero/algorithms/solvers/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/tests/__init__.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_interfaces.py b/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_interfaces.py index 119c4604f19..88d4df1e17d 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_interfaces.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_interfaces.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_solver.py b/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_solver.py index 2a7edb430d4..0af5a772c98 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_solver.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_solver.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -24,6 +24,7 @@ if not (numpy_available and scipy_available): raise unittest.SkipTest("Pynumero needs scipy and numpy to run NLP tests") +from pyomo.contrib.pynumero.exceptions import PyNumeroEvaluationError from pyomo.contrib.pynumero.asl import AmplInterface if not AmplInterface.available(): @@ -34,12 +35,18 @@ from pyomo.contrib.pynumero.interfaces.pyomo_nlp import PyomoNLP from pyomo.contrib.pynumero.interfaces.cyipopt_interface import ( + cyipopt, cyipopt_available, CyIpoptNLP, ) from pyomo.contrib.pynumero.algorithms.solvers.cyipopt_solver import CyIpoptSolver +if cyipopt_available: + # We don't raise unittest.SkipTest if not cyipopt_available as there is a + # test below that tests an exception when cyipopt is unavailable. + cyipopt_ge_1_3 = hasattr(cyipopt, "CyIpoptEvaluationError") + def create_model1(): m = pyo.ConcreteModel() @@ -155,6 +162,29 @@ def f(model): return model +def make_hs071_model(): + # This is a model that is mathematically equivalent to the Hock-Schittkowski + # test problem 071, but that will trigger an evaluation error if x[0] goes + # above 1.1. + m = pyo.ConcreteModel() + m.x = pyo.Var([0, 1, 2, 3], bounds=(1.0, 5.0)) + m.x[0] = 1.0 + m.x[1] = 5.0 + m.x[2] = 5.0 + m.x[3] = 1.0 + m.obj = pyo.Objective(expr=m.x[0] * m.x[3] * (m.x[0] + m.x[1] + m.x[2]) + m.x[2]) + # This expression evaluates to zero, but is not well defined when x[0] > 1.1 + trivial_expr_with_eval_error = (pyo.sqrt(1.1 - m.x[0])) ** 2 + m.x[0] - 1.1 + m.ineq1 = pyo.Constraint(expr=m.x[0] * m.x[1] * m.x[2] * m.x[3] >= 25.0) + m.eq1 = pyo.Constraint( + expr=( + m.x[0] ** 2 + m.x[1] ** 2 + m.x[2] ** 2 + m.x[3] ** 2 + == 40.0 + trivial_expr_with_eval_error + ) + ) + return m + + @unittest.skipIf(cyipopt_available, "cyipopt is available") class TestCyIpoptNotAvailable(unittest.TestCase): def test_not_available_exception(self): @@ -257,3 +287,42 @@ def test_options(self): x, info = solver.solve(tee=False) nlp.set_primals(x) self.assertAlmostEqual(nlp.evaluate_objective(), -5.0879028e02, places=5) + + @unittest.skipUnless( + cyipopt_available and cyipopt_ge_1_3, "cyipopt version < 1.3.0" + ) + def test_hs071_evalerror(self): + m = make_hs071_model() + solver = pyo.SolverFactory("cyipopt") + res = solver.solve(m, tee=True) + + x = list(m.x[:].value) + expected_x = np.array([1.0, 4.74299964, 3.82114998, 1.37940829]) + np.testing.assert_allclose(x, expected_x) + + def test_hs071_evalerror_halt(self): + m = make_hs071_model() + solver = pyo.SolverFactory("cyipopt", halt_on_evaluation_error=True) + msg = "Error in AMPL evaluation" + with self.assertRaisesRegex(PyNumeroEvaluationError, msg): + res = solver.solve(m, tee=True) + + @unittest.skipIf( + not cyipopt_available or cyipopt_ge_1_3, "cyipopt version >= 1.3.0" + ) + def test_hs071_evalerror_old_cyipopt(self): + m = make_hs071_model() + solver = pyo.SolverFactory("cyipopt") + msg = "Error in AMPL evaluation" + with self.assertRaisesRegex(PyNumeroEvaluationError, msg): + res = solver.solve(m, tee=True) + + def test_solve_without_objective(self): + m = create_model1() + m.o.deactivate() + m.x[2].fix(0.0) + m.x[3].fix(4.0) + solver = pyo.SolverFactory("cyipopt") + res = solver.solve(m, tee=True) + pyo.assert_optimal_termination(res) + self.assertAlmostEqual(m.x[1].value, 9.0) diff --git a/pyomo/contrib/pynumero/algorithms/solvers/tests/test_implicit_functions.py b/pyomo/contrib/pynumero/algorithms/solvers/tests/test_implicit_functions.py index 04d4ed321f1..3a13c1a7598 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/tests/test_implicit_functions.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/tests/test_implicit_functions.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/algorithms/solvers/tests/test_pyomo_ext_cyipopt.py b/pyomo/contrib/pynumero/algorithms/solvers/tests/test_pyomo_ext_cyipopt.py index 82a37873d5f..0036a6b3623 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/tests/test_pyomo_ext_cyipopt.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/tests/test_pyomo_ext_cyipopt.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/algorithms/solvers/tests/test_scipy_solvers.py b/pyomo/contrib/pynumero/algorithms/solvers/tests/test_scipy_solvers.py index 6636dc3d6e2..33b58f17887 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/tests/test_scipy_solvers.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/tests/test_scipy_solvers.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/asl.py b/pyomo/contrib/pynumero/asl.py index a28741fb230..55ecc7fd0ee 100644 --- a/pyomo/contrib/pynumero/asl.py +++ b/pyomo/contrib/pynumero/asl.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/build.py b/pyomo/contrib/pynumero/build.py index 08b5c512ab7..bb8443640d5 100644 --- a/pyomo/contrib/pynumero/build.py +++ b/pyomo/contrib/pynumero/build.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/dependencies.py b/pyomo/contrib/pynumero/dependencies.py index d386bbc3dda..d323bd43e84 100644 --- a/pyomo/contrib/pynumero/dependencies.py +++ b/pyomo/contrib/pynumero/dependencies.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -17,7 +17,7 @@ 'numpy', 'Pynumero requires the optional Pyomo dependency "numpy"', minimum_version='1.13.0', - defer_check=False, + defer_import=False, ) if not numpy_available: diff --git a/pyomo/contrib/pynumero/examples/__init__.py b/pyomo/contrib/pynumero/examples/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/pynumero/examples/__init__.py +++ b/pyomo/contrib/pynumero/examples/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/callback/__init__.py b/pyomo/contrib/pynumero/examples/callback/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/pynumero/examples/callback/__init__.py +++ b/pyomo/contrib/pynumero/examples/callback/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/pynumero/examples/callback/cyipopt_callback.py b/pyomo/contrib/pynumero/examples/callback/cyipopt_callback.py index 6bd86c006a1..f66374f6213 100644 --- a/pyomo/contrib/pynumero/examples/callback/cyipopt_callback.py +++ b/pyomo/contrib/pynumero/examples/callback/cyipopt_callback.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo from pyomo.contrib.pynumero.examples.callback.reactor_design import model as m import logging diff --git a/pyomo/contrib/pynumero/examples/callback/cyipopt_callback_halt.py b/pyomo/contrib/pynumero/examples/callback/cyipopt_callback_halt.py index 18fad2bbcd8..9e88f8d4964 100644 --- a/pyomo/contrib/pynumero/examples/callback/cyipopt_callback_halt.py +++ b/pyomo/contrib/pynumero/examples/callback/cyipopt_callback_halt.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo from pyomo.contrib.pynumero.examples.callback.reactor_design import model as m diff --git a/pyomo/contrib/pynumero/examples/callback/cyipopt_functor_callback.py b/pyomo/contrib/pynumero/examples/callback/cyipopt_functor_callback.py index f977a2701a2..4befc816e1b 100644 --- a/pyomo/contrib/pynumero/examples/callback/cyipopt_functor_callback.py +++ b/pyomo/contrib/pynumero/examples/callback/cyipopt_functor_callback.py @@ -1,6 +1,17 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo from pyomo.contrib.pynumero.examples.callback.reactor_design import model as m -import pandas as pd +from pyomo.common.dependencies import pandas as pd """ This example uses an iteration callback with a functor to store diff --git a/pyomo/contrib/pynumero/examples/callback/reactor_design.py b/pyomo/contrib/pynumero/examples/callback/reactor_design.py index 927b25f9bc9..3d9e19a446e 100644 --- a/pyomo/contrib/pynumero/examples/callback/reactor_design.py +++ b/pyomo/contrib/pynumero/examples/callback/reactor_design.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ from pyomo.core import * diff --git a/pyomo/contrib/pynumero/examples/external_grey_box/__init__.py b/pyomo/contrib/pynumero/examples/external_grey_box/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/pynumero/examples/external_grey_box/__init__.py +++ b/pyomo/contrib/pynumero/examples/external_grey_box/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/pynumero/examples/external_grey_box/param_est/__init__.py b/pyomo/contrib/pynumero/examples/external_grey_box/param_est/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/pynumero/examples/external_grey_box/param_est/__init__.py +++ b/pyomo/contrib/pynumero/examples/external_grey_box/param_est/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/pynumero/examples/external_grey_box/param_est/generate_data.py b/pyomo/contrib/pynumero/examples/external_grey_box/param_est/generate_data.py index 3588ba3853d..65bb2c82de8 100644 --- a/pyomo/contrib/pynumero/examples/external_grey_box/param_est/generate_data.py +++ b/pyomo/contrib/pynumero/examples/external_grey_box/param_est/generate_data.py @@ -1,7 +1,18 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo import numpy.random as rnd import pyomo.contrib.pynumero.examples.external_grey_box.param_est.models as pm -import pandas as pd +from pyomo.common.dependencies import pandas as pd def generate_data(N, UA_mean, UA_std, seed=42): diff --git a/pyomo/contrib/pynumero/examples/external_grey_box/param_est/models.py b/pyomo/contrib/pynumero/examples/external_grey_box/param_est/models.py index a8b9befb188..c6560b4f9c5 100644 --- a/pyomo/contrib/pynumero/examples/external_grey_box/param_est/models.py +++ b/pyomo/contrib/pynumero/examples/external_grey_box/param_est/models.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo from pyomo.contrib.pynumero.interfaces.external_grey_box import ( ExternalGreyBoxModel, diff --git a/pyomo/contrib/pynumero/examples/external_grey_box/param_est/perform_estimation.py b/pyomo/contrib/pynumero/examples/external_grey_box/param_est/perform_estimation.py index 29ca7145475..142b47f8172 100644 --- a/pyomo/contrib/pynumero/examples/external_grey_box/param_est/perform_estimation.py +++ b/pyomo/contrib/pynumero/examples/external_grey_box/param_est/perform_estimation.py @@ -1,7 +1,18 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import sys import pyomo.environ as pyo import numpy.random as rnd -import pandas as pd +from pyomo.common.dependencies import pandas as pd import pyomo.contrib.pynumero.examples.external_grey_box.param_est.models as po diff --git a/pyomo/contrib/pynumero/examples/external_grey_box/react_example/__init__.py b/pyomo/contrib/pynumero/examples/external_grey_box/react_example/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/pynumero/examples/external_grey_box/react_example/__init__.py +++ b/pyomo/contrib/pynumero/examples/external_grey_box/react_example/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/pynumero/examples/external_grey_box/react_example/maximize_cb_outputs.py b/pyomo/contrib/pynumero/examples/external_grey_box/react_example/maximize_cb_outputs.py index eff4f34cabc..e6afd8995a2 100644 --- a/pyomo/contrib/pynumero/examples/external_grey_box/react_example/maximize_cb_outputs.py +++ b/pyomo/contrib/pynumero/examples/external_grey_box/react_example/maximize_cb_outputs.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,7 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from __future__ import division + import pyomo.environ as pyo from pyomo.contrib.pynumero.interfaces.external_grey_box import ExternalGreyBoxBlock from pyomo.contrib.pynumero.examples.external_grey_box.react_example.reactor_model_outputs import ( diff --git a/pyomo/contrib/pynumero/examples/external_grey_box/react_example/maximize_cb_ratio_residuals.py b/pyomo/contrib/pynumero/examples/external_grey_box/react_example/maximize_cb_ratio_residuals.py index 26d70c7921e..415b58bee54 100644 --- a/pyomo/contrib/pynumero/examples/external_grey_box/react_example/maximize_cb_ratio_residuals.py +++ b/pyomo/contrib/pynumero/examples/external_grey_box/react_example/maximize_cb_ratio_residuals.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/external_grey_box/react_example/reactor_model_outputs.py b/pyomo/contrib/pynumero/examples/external_grey_box/react_example/reactor_model_outputs.py index 7570a20b066..ef8b2783237 100644 --- a/pyomo/contrib/pynumero/examples/external_grey_box/react_example/reactor_model_outputs.py +++ b/pyomo/contrib/pynumero/examples/external_grey_box/react_example/reactor_model_outputs.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -21,7 +21,6 @@ box model interface. """ -from __future__ import division import numpy as np from scipy.optimize import fsolve diff --git a/pyomo/contrib/pynumero/examples/external_grey_box/react_example/reactor_model_residuals.py b/pyomo/contrib/pynumero/examples/external_grey_box/react_example/reactor_model_residuals.py index 6a6ae9bb652..bc5a2ca4ce4 100644 --- a/pyomo/contrib/pynumero/examples/external_grey_box/react_example/reactor_model_residuals.py +++ b/pyomo/contrib/pynumero/examples/external_grey_box/react_example/reactor_model_residuals.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -19,7 +19,6 @@ box model interface. """ -from __future__ import division import pyomo.environ as pyo import numpy as np diff --git a/pyomo/contrib/pynumero/examples/feasibility.py b/pyomo/contrib/pynumero/examples/feasibility.py index 94baabb7bec..59e4edcc9ec 100644 --- a/pyomo/contrib/pynumero/examples/feasibility.py +++ b/pyomo/contrib/pynumero/examples/feasibility.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/mumps_example.py b/pyomo/contrib/pynumero/examples/mumps_example.py index 938fab99279..588ce58bc12 100644 --- a/pyomo/contrib/pynumero/examples/mumps_example.py +++ b/pyomo/contrib/pynumero/examples/mumps_example.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import numpy as np import scipy.sparse as sp from scipy.linalg import hilbert diff --git a/pyomo/contrib/pynumero/examples/nlp_interface.py b/pyomo/contrib/pynumero/examples/nlp_interface.py index 730e0fbda47..556b8ec0713 100644 --- a/pyomo/contrib/pynumero/examples/nlp_interface.py +++ b/pyomo/contrib/pynumero/examples/nlp_interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/nlp_interface_2.py b/pyomo/contrib/pynumero/examples/nlp_interface_2.py index ecd63d28c49..4a288a178b1 100644 --- a/pyomo/contrib/pynumero/examples/nlp_interface_2.py +++ b/pyomo/contrib/pynumero/examples/nlp_interface_2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/parallel_matvec.py b/pyomo/contrib/pynumero/examples/parallel_matvec.py index 26a2ec9a632..cd77bcfabc9 100644 --- a/pyomo/contrib/pynumero/examples/parallel_matvec.py +++ b/pyomo/contrib/pynumero/examples/parallel_matvec.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import numpy as np from pyomo.common.dependencies import mpi4py from pyomo.contrib.pynumero.sparse.mpi_block_vector import MPIBlockVector diff --git a/pyomo/contrib/pynumero/examples/parallel_vector_ops.py b/pyomo/contrib/pynumero/examples/parallel_vector_ops.py index 4b155ce7493..fe49ff29e59 100644 --- a/pyomo/contrib/pynumero/examples/parallel_vector_ops.py +++ b/pyomo/contrib/pynumero/examples/parallel_vector_ops.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import numpy as np from pyomo.common.dependencies import mpi4py from pyomo.contrib.pynumero.sparse.mpi_block_vector import MPIBlockVector diff --git a/pyomo/contrib/pynumero/examples/sensitivity.py b/pyomo/contrib/pynumero/examples/sensitivity.py index a3927d637b3..0bb0fb3a740 100644 --- a/pyomo/contrib/pynumero/examples/sensitivity.py +++ b/pyomo/contrib/pynumero/examples/sensitivity.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/sqp.py b/pyomo/contrib/pynumero/examples/sqp.py index 7d321676817..925cab4c20b 100644 --- a/pyomo/contrib/pynumero/examples/sqp.py +++ b/pyomo/contrib/pynumero/examples/sqp.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.contrib.pynumero.interfaces.nlp import NLP from pyomo.contrib.pynumero.sparse import BlockVector, BlockMatrix from pyomo.contrib.pynumero.linalg.ma27_interface import MA27 diff --git a/pyomo/contrib/pynumero/examples/tests/__init__.py b/pyomo/contrib/pynumero/examples/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/pynumero/examples/tests/__init__.py +++ b/pyomo/contrib/pynumero/examples/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py b/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py index 167b0601f7a..2df43c1e797 100644 --- a/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py +++ b/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -35,7 +35,7 @@ 'One of the tests below requires a recent version of pandas for' ' comparing with a tolerance.', minimum_version='1.1.0', - defer_check=False, + defer_import=False, ) from pyomo.contrib.pynumero.asl import AmplInterface @@ -44,11 +44,13 @@ raise unittest.SkipTest("Pynumero needs the ASL extension to run CyIpopt tests") import pyomo.contrib.pynumero.algorithms.solvers.cyipopt_solver as cyipopt_solver +from pyomo.contrib.pynumero.interfaces.cyipopt_interface import cyipopt_available -if not cyipopt_solver.cyipopt_available: +if not cyipopt_available: raise unittest.SkipTest("PyNumero needs CyIpopt installed to run CyIpopt tests") import cyipopt as cyipopt_core + example_dir = os.path.join(this_file_dir(), '..') @@ -266,6 +268,11 @@ def test_cyipopt_functor(self): s = df['ca_bal'] self.assertAlmostEqual(s.iloc[6], 0, places=3) + @unittest.skipIf( + cyipopt_solver.PyomoCyIpoptSolver().version() == (1, 4, 0), + "Terminating Ipopt through a user callback is broken in CyIpopt 1.4.0 " + "(see mechmotum/cyipopt#249)", + ) def test_cyipopt_callback_halt(self): ex = import_file( os.path.join(example_dir, 'callback', 'cyipopt_callback_halt.py') diff --git a/pyomo/contrib/pynumero/examples/tests/test_examples.py b/pyomo/contrib/pynumero/examples/tests/test_examples.py index 5c7993ebbb6..d1494bab557 100644 --- a/pyomo/contrib/pynumero/examples/tests/test_examples.py +++ b/pyomo/contrib/pynumero/examples/tests/test_examples.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.contrib.pynumero.dependencies import numpy_available, scipy_available import pyomo.common.unittest as unittest diff --git a/pyomo/contrib/pynumero/examples/tests/test_mpi_examples.py b/pyomo/contrib/pynumero/examples/tests/test_mpi_examples.py index 68fe907a8ef..1ee02bb70ca 100644 --- a/pyomo/contrib/pynumero/examples/tests/test_mpi_examples.py +++ b/pyomo/contrib/pynumero/examples/tests/test_mpi_examples.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.common.unittest as unittest from pyomo.contrib.pynumero.dependencies import ( diff --git a/pyomo/contrib/pynumero/exceptions.py b/pyomo/contrib/pynumero/exceptions.py index dc2167d75d2..6b46dd2d9a7 100644 --- a/pyomo/contrib/pynumero/exceptions.py +++ b/pyomo/contrib/pynumero/exceptions.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/__init__.py b/pyomo/contrib/pynumero/interfaces/__init__.py index debe453e175..e2de0dd25cc 100644 --- a/pyomo/contrib/pynumero/interfaces/__init__.py +++ b/pyomo/contrib/pynumero/interfaces/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/ampl_nlp.py b/pyomo/contrib/pynumero/interfaces/ampl_nlp.py index f5bd56696cf..30258b3e685 100644 --- a/pyomo/contrib/pynumero/interfaces/ampl_nlp.py +++ b/pyomo/contrib/pynumero/interfaces/ampl_nlp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -27,10 +27,8 @@ from pyomo.common.deprecation import deprecated from pyomo.contrib.pynumero.interfaces.nlp import ExtendedNLP -__all__ = ['AslNLP', 'AmplNLP'] - -# ToDo: need to add support for modifying bounds. +# TODO: need to add support for modifying bounds. # support for changing variable bounds seems possible. # support for changing inequality bounds would require more work. (this is less frequent?) # TODO: check performance impacts of caching - memory and computational time. diff --git a/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py b/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py index 19e74625d03..7845a4c189e 100644 --- a/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py +++ b/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -23,6 +23,7 @@ import abc from pyomo.common.dependencies import attempt_import, numpy as np, numpy_available +from pyomo.contrib.pynumero.exceptions import PyNumeroEvaluationError def _cyipopt_importer(): @@ -252,7 +253,7 @@ def intermediate( class CyIpoptNLP(CyIpoptProblemInterface): - def __init__(self, nlp, intermediate_callback=None): + def __init__(self, nlp, intermediate_callback=None, halt_on_evaluation_error=None): """This class provides a CyIpoptProblemInterface for use with the CyIpoptSolver class that can take in an NLP as long as it provides vectors as numpy ndarrays and @@ -263,6 +264,23 @@ def __init__(self, nlp, intermediate_callback=None): self._nlp = nlp self._intermediate_callback = intermediate_callback + cyipopt_has_eval_error = cyipopt_available and hasattr( + cyipopt, "CyIpoptEvaluationError" + ) + if halt_on_evaluation_error is None: + # If using cyipopt >= 1.3, the default is to continue. + # Otherwise, the default is to halt (because we are forced to). + # + # If CyIpopt is not available, we "halt" (re-raise the original + # exception). + self._halt_on_evaluation_error = not cyipopt_has_eval_error + elif not halt_on_evaluation_error and not cyipopt_has_eval_error: + raise ValueError( + "halt_on_evaluation_error=False is only supported for cyipopt >= 1.3.0" + ) + else: + self._halt_on_evaluation_error = halt_on_evaluation_error + x = nlp.init_primals() y = nlp.init_duals() if np.any(np.isnan(y)): @@ -328,24 +346,54 @@ def scaling_factors(self): return obj_scaling, x_scaling, g_scaling def objective(self, x): - self._set_primals_if_necessary(x) - return self._nlp.evaluate_objective() + try: + self._set_primals_if_necessary(x) + return self._nlp.evaluate_objective() + except PyNumeroEvaluationError: + if self._halt_on_evaluation_error: + raise + else: + raise cyipopt.CyIpoptEvaluationError( + "Error in objective function evaluation" + ) def gradient(self, x): - self._set_primals_if_necessary(x) - return self._nlp.evaluate_grad_objective() + try: + self._set_primals_if_necessary(x) + return self._nlp.evaluate_grad_objective() + except PyNumeroEvaluationError: + if self._halt_on_evaluation_error: + raise + else: + raise cyipopt.CyIpoptEvaluationError( + "Error in objective gradient evaluation" + ) def constraints(self, x): - self._set_primals_if_necessary(x) - return self._nlp.evaluate_constraints() + try: + self._set_primals_if_necessary(x) + return self._nlp.evaluate_constraints() + except PyNumeroEvaluationError: + if self._halt_on_evaluation_error: + raise + else: + raise cyipopt.CyIpoptEvaluationError("Error in constraint evaluation") def jacobianstructure(self): return self._jac_g.row, self._jac_g.col def jacobian(self, x): - self._set_primals_if_necessary(x) - self._nlp.evaluate_jacobian(out=self._jac_g) - return self._jac_g.data + try: + self._set_primals_if_necessary(x) + self._nlp.evaluate_jacobian(out=self._jac_g) + return self._jac_g.data + except PyNumeroEvaluationError: + if self._halt_on_evaluation_error: + raise + else: + raise cyipopt.CyIpoptEvaluationError( + "Error in constraint Jacobian evaluation" + ) def hessianstructure(self): if not self._hessian_available: @@ -359,12 +407,20 @@ def hessian(self, x, y, obj_factor): if not self._hessian_available: raise ValueError("Hessian requested, but not supported by the NLP") - self._set_primals_if_necessary(x) - self._set_duals_if_necessary(y) - self._set_obj_factor_if_necessary(obj_factor) - self._nlp.evaluate_hessian_lag(out=self._hess_lag) - data = np.compress(self._hess_lower_mask, self._hess_lag.data) - return data + try: + self._set_primals_if_necessary(x) + self._set_duals_if_necessary(y) + self._set_obj_factor_if_necessary(obj_factor) + self._nlp.evaluate_hessian_lag(out=self._hess_lag) + data = np.compress(self._hess_lower_mask, self._hess_lag.data) + return data + except PyNumeroEvaluationError: + if self._halt_on_evaluation_error: + raise + else: + raise cyipopt.CyIpoptEvaluationError( + "Error in Lagrangian Hessian evaluation" + ) def intermediate( self, diff --git a/pyomo/contrib/pynumero/interfaces/external_grey_box.py b/pyomo/contrib/pynumero/interfaces/external_grey_box.py index 8fd728a7c9b..68e652575cc 100644 --- a/pyomo/contrib/pynumero/interfaces/external_grey_box.py +++ b/pyomo/contrib/pynumero/interfaces/external_grey_box.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -11,14 +11,14 @@ import abc import logging -import numpy as np from scipy.sparse import coo_matrix +from pyomo.common.dependencies import numpy as np from pyomo.common.deprecation import RenamedClass from pyomo.common.log import is_debug_set from pyomo.common.timing import ConstructionTimer from pyomo.core.base import Var, Set, Constraint, value -from pyomo.core.base.block import _BlockData, Block, declare_custom_block +from pyomo.core.base.block import BlockData, Block, declare_custom_block from pyomo.core.base.global_set import UnindexedComponent_index from pyomo.core.base.initializer import Initializer from pyomo.core.base.set import UnindexedComponent_set @@ -316,7 +316,7 @@ def evaluate_jacobian_outputs(self): # -class ExternalGreyBoxBlockData(_BlockData): +class ExternalGreyBoxBlockData(BlockData): def set_external_model(self, external_grey_box_model, inputs=None, outputs=None): """ Parameters @@ -424,7 +424,7 @@ class ScalarExternalGreyBoxBlock(ExternalGreyBoxBlockData, ExternalGreyBoxBlock) def __init__(self, *args, **kwds): ExternalGreyBoxBlockData.__init__(self, component=self) ExternalGreyBoxBlock.__init__(self, *args, **kwds) - # The above inherit from Block and _BlockData, so it's not until here + # The above inherit from Block and BlockData, so it's not until here # that we know it's scalar. So we set the index accordingly. self._index = UnindexedComponent_index diff --git a/pyomo/contrib/pynumero/interfaces/external_pyomo_model.py b/pyomo/contrib/pynumero/interfaces/external_pyomo_model.py index d0e6c21fa64..bae3e0b8159 100644 --- a/pyomo/contrib/pynumero/interfaces/external_pyomo_model.py +++ b/pyomo/contrib/pynumero/interfaces/external_pyomo_model.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/nlp.py b/pyomo/contrib/pynumero/interfaces/nlp.py index 95c05f06a61..d6571086429 100644 --- a/pyomo/contrib/pynumero/interfaces/nlp.py +++ b/pyomo/contrib/pynumero/interfaces/nlp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -50,9 +50,8 @@ .. rubric:: Contents """ -import abc -__all__ = ['NLP'] +import abc class NLP(object, metaclass=abc.ABCMeta): diff --git a/pyomo/contrib/pynumero/interfaces/nlp_projections.py b/pyomo/contrib/pynumero/interfaces/nlp_projections.py index 68cb0eef15f..4be3cd28dd5 100644 --- a/pyomo/contrib/pynumero/interfaces/nlp_projections.py +++ b/pyomo/contrib/pynumero/interfaces/nlp_projections.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.contrib.pynumero.interfaces.nlp import NLP, ExtendedNLP import numpy as np import scipy.sparse as sp diff --git a/pyomo/contrib/pynumero/interfaces/pyomo_grey_box_nlp.py b/pyomo/contrib/pynumero/interfaces/pyomo_grey_box_nlp.py index 945e9a05f51..e6ed40e9974 100644 --- a/pyomo/contrib/pynumero/interfaces/pyomo_grey_box_nlp.py +++ b/pyomo/contrib/pynumero/interfaces/pyomo_grey_box_nlp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py b/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py index 8017c642854..e12d0cf568b 100644 --- a/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py +++ b/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py @@ -1,6 +1,6 @@ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -22,15 +22,13 @@ import pyomo.core.base as pyo from pyomo.common.collections import ComponentMap from pyomo.common.env import CtypesEnviron +from pyomo.solvers.amplfunc_merge import amplfunc_merge from ..sparse.block_matrix import BlockMatrix from pyomo.contrib.pynumero.interfaces.ampl_nlp import AslNLP from pyomo.contrib.pynumero.interfaces.nlp import NLP from .external_grey_box import ExternalGreyBoxBlock -__all__ = ['PyomoNLP'] - - # TODO: There are todos in the code below class PyomoNLP(AslNLP): def __init__(self, pyomo_model, nl_file_options=None): @@ -95,15 +93,8 @@ def __init__(self, pyomo_model, nl_file_options=None): # The NL writer advertises the external function libraries # through the PYOMO_AMPLFUNC environment variable; merge it # with any preexisting AMPLFUNC definitions - amplfunc = "\n".join( - filter( - None, - ( - os.environ.get('AMPLFUNC', None), - os.environ.get('PYOMO_AMPLFUNC', None), - ), - ) - ) + amplfunc = amplfunc_merge(os.environ) + with CtypesEnviron(AMPLFUNC=amplfunc): super(PyomoNLP, self).__init__(nl_file) diff --git a/pyomo/contrib/pynumero/interfaces/tests/__init__.py b/pyomo/contrib/pynumero/interfaces/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/pynumero/interfaces/tests/__init__.py +++ b/pyomo/contrib/pynumero/interfaces/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/tests/compare_utils.py b/pyomo/contrib/pynumero/interfaces/tests/compare_utils.py index d30cfb8f56a..8296ea2d1af 100644 --- a/pyomo/contrib/pynumero/interfaces/tests/compare_utils.py +++ b/pyomo/contrib/pynumero/interfaces/tests/compare_utils.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/tests/external_grey_box_models.py b/pyomo/contrib/pynumero/interfaces/tests/external_grey_box_models.py index 1f2a5169857..b81731b209e 100644 --- a/pyomo/contrib/pynumero/interfaces/tests/external_grey_box_models.py +++ b/pyomo/contrib/pynumero/interfaces/tests/external_grey_box_models.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.contrib.pynumero.dependencies import ( numpy as np, numpy_available, @@ -298,8 +309,7 @@ def evaluate_equality_constraints(self): P2 = self._input_values[3] Pout = self._input_values[4] return np.asarray( - [P2 - (Pin - 2 * c * F**2), Pout - (P2 - 2 * c * F**2)], - dtype=np.float64, + [P2 - (Pin - 2 * c * F**2), Pout - (P2 - 2 * c * F**2)], dtype=np.float64 ) def evaluate_jacobian_equality_constraints(self): diff --git a/pyomo/contrib/pynumero/interfaces/tests/test_cyipopt_interface.py b/pyomo/contrib/pynumero/interfaces/tests/test_cyipopt_interface.py index 2c5d8ff7e4e..bbcd6d4f26d 100644 --- a/pyomo/contrib/pynumero/interfaces/tests/test_cyipopt_interface.py +++ b/pyomo/contrib/pynumero/interfaces/tests/test_cyipopt_interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -10,6 +10,7 @@ # ___________________________________________________________________________ import pyomo.common.unittest as unittest +import pyomo.environ as pyo from pyomo.contrib.pynumero.dependencies import ( numpy as np, @@ -20,20 +21,27 @@ if not (numpy_available and scipy_available): raise unittest.SkipTest("Pynumero needs scipy and numpy to run CyIpopt tests") +from pyomo.contrib.pynumero.exceptions import PyNumeroEvaluationError from pyomo.contrib.pynumero.asl import AmplInterface if not AmplInterface.available(): raise unittest.SkipTest("Pynumero needs the ASL extension to run CyIpopt tests") +from pyomo.contrib.pynumero.interfaces.pyomo_nlp import PyomoNLP from pyomo.contrib.pynumero.interfaces.cyipopt_interface import ( + cyipopt, cyipopt_available, CyIpoptProblemInterface, + CyIpoptNLP, ) if not cyipopt_available: raise unittest.SkipTest("CyIpopt is not available") +cyipopt_ge_1_3 = hasattr(cyipopt, "CyIpoptEvaluationError") + + class TestSubclassCyIpoptInterface(unittest.TestCase): def test_subclass_no_init(self): class MyCyIpoptProblem(CyIpoptProblemInterface): @@ -88,5 +96,129 @@ def hessian(self, x, y, obj_factor): problem.solve(x0) +def _get_model_nlp_interface(halt_on_evaluation_error=None): + m = pyo.ConcreteModel() + m.x = pyo.Var([1, 2, 3], initialize=1.0) + m.obj = pyo.Objective(expr=m.x[1] * pyo.sqrt(m.x[2]) + m.x[1] * m.x[3]) + m.eq1 = pyo.Constraint(expr=m.x[1] * pyo.sqrt(m.x[2]) == 1.0) + nlp = PyomoNLP(m) + interface = CyIpoptNLP(nlp, halt_on_evaluation_error=halt_on_evaluation_error) + bad_primals = np.array([1.0, -2.0, 3.0]) + indices = nlp.get_primal_indices([m.x[1], m.x[2], m.x[3]]) + bad_primals = bad_primals[indices] + return m, nlp, interface, bad_primals + + +class TestCyIpoptVersionDependentConfig(unittest.TestCase): + @unittest.skipIf(cyipopt_ge_1_3, "cyipopt version >= 1.3.0") + def test_config_error(self): + _, nlp, _, _ = _get_model_nlp_interface() + with self.assertRaisesRegex(ValueError, "halt_on_evaluation_error"): + interface = CyIpoptNLP(nlp, halt_on_evaluation_error=False) + + @unittest.skipIf(cyipopt_ge_1_3, "cyipopt version >= 1.3.0") + def test_default_config_with_old_cyipopt(self): + _, nlp, _, bad_x = _get_model_nlp_interface() + interface = CyIpoptNLP(nlp) + msg = "Error in AMPL evaluation" + with self.assertRaisesRegex(PyNumeroEvaluationError, msg): + interface.objective(bad_x) + + @unittest.skipIf(not cyipopt_ge_1_3, "cyipopt version < 1.3.0") + def test_default_config_with_new_cyipopt(self): + _, nlp, _, bad_x = _get_model_nlp_interface() + interface = CyIpoptNLP(nlp) + msg = "Error in objective function" + with self.assertRaisesRegex(cyipopt.CyIpoptEvaluationError, msg): + interface.objective(bad_x) + + +class TestCyIpoptEvaluationErrors(unittest.TestCase): + @unittest.skipUnless(cyipopt_ge_1_3, "cyipopt version < 1.3.0") + def test_error_in_objective(self): + m, nlp, interface, bad_x = _get_model_nlp_interface( + halt_on_evaluation_error=False + ) + msg = "Error in objective function" + with self.assertRaisesRegex(cyipopt.CyIpoptEvaluationError, msg): + interface.objective(bad_x) + + def test_error_in_objective_halt(self): + m, nlp, interface, bad_x = _get_model_nlp_interface( + halt_on_evaluation_error=True + ) + msg = "Error in AMPL evaluation" + with self.assertRaisesRegex(PyNumeroEvaluationError, msg): + interface.objective(bad_x) + + @unittest.skipUnless(cyipopt_ge_1_3, "cyipopt version < 1.3.0") + def test_error_in_gradient(self): + m, nlp, interface, bad_x = _get_model_nlp_interface( + halt_on_evaluation_error=False + ) + msg = "Error in objective gradient" + with self.assertRaisesRegex(cyipopt.CyIpoptEvaluationError, msg): + interface.gradient(bad_x) + + def test_error_in_gradient_halt(self): + m, nlp, interface, bad_x = _get_model_nlp_interface( + halt_on_evaluation_error=True + ) + msg = "Error in AMPL evaluation" + with self.assertRaisesRegex(PyNumeroEvaluationError, msg): + interface.gradient(bad_x) + + @unittest.skipUnless(cyipopt_ge_1_3, "cyipopt version < 1.3.0") + def test_error_in_constraints(self): + m, nlp, interface, bad_x = _get_model_nlp_interface( + halt_on_evaluation_error=False + ) + msg = "Error in constraint evaluation" + with self.assertRaisesRegex(cyipopt.CyIpoptEvaluationError, msg): + interface.constraints(bad_x) + + def test_error_in_constraints_halt(self): + m, nlp, interface, bad_x = _get_model_nlp_interface( + halt_on_evaluation_error=True + ) + msg = "Error in AMPL evaluation" + with self.assertRaisesRegex(PyNumeroEvaluationError, msg): + interface.constraints(bad_x) + + @unittest.skipUnless(cyipopt_ge_1_3, "cyipopt version < 1.3.0") + def test_error_in_jacobian(self): + m, nlp, interface, bad_x = _get_model_nlp_interface( + halt_on_evaluation_error=False + ) + msg = "Error in constraint Jacobian" + with self.assertRaisesRegex(cyipopt.CyIpoptEvaluationError, msg): + interface.jacobian(bad_x) + + def test_error_in_jacobian_halt(self): + m, nlp, interface, bad_x = _get_model_nlp_interface( + halt_on_evaluation_error=True + ) + msg = "Error in AMPL evaluation" + with self.assertRaisesRegex(PyNumeroEvaluationError, msg): + interface.jacobian(bad_x) + + @unittest.skipUnless(cyipopt_ge_1_3, "cyipopt version < 1.3.0") + def test_error_in_hessian(self): + m, nlp, interface, bad_x = _get_model_nlp_interface( + halt_on_evaluation_error=False + ) + msg = "Error in Lagrangian Hessian" + with self.assertRaisesRegex(cyipopt.CyIpoptEvaluationError, msg): + interface.hessian(bad_x, [1.0], 0.0) + + def test_error_in_hessian_halt(self): + m, nlp, interface, bad_x = _get_model_nlp_interface( + halt_on_evaluation_error=True + ) + msg = "Error in AMPL evaluation" + with self.assertRaisesRegex(PyNumeroEvaluationError, msg): + interface.hessian(bad_x, [1.0], 0.0) + + if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/pynumero/interfaces/tests/test_dynamic_model.py b/pyomo/contrib/pynumero/interfaces/tests/test_dynamic_model.py index ddd56afb5b4..5b8a8d688dd 100644 --- a/pyomo/contrib/pynumero/interfaces/tests/test_dynamic_model.py +++ b/pyomo/contrib/pynumero/interfaces/tests/test_dynamic_model.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/tests/test_external_asl_function.py b/pyomo/contrib/pynumero/interfaces/tests/test_external_asl_function.py index 88a4024aeeb..9ca0aef4187 100644 --- a/pyomo/contrib/pynumero/interfaces/tests/test_external_asl_function.py +++ b/pyomo/contrib/pynumero/interfaces/tests/test_external_asl_function.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/tests/test_external_grey_box_model.py b/pyomo/contrib/pynumero/interfaces/tests/test_external_grey_box_model.py index 58e08a409f0..0fc342c4e40 100644 --- a/pyomo/contrib/pynumero/interfaces/tests/test_external_grey_box_model.py +++ b/pyomo/contrib/pynumero/interfaces/tests/test_external_grey_box_model.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/tests/test_external_pyomo_block.py b/pyomo/contrib/pynumero/interfaces/tests/test_external_pyomo_block.py index 2d758e2e1a9..913d3055c9c 100644 --- a/pyomo/contrib/pynumero/interfaces/tests/test_external_pyomo_block.py +++ b/pyomo/contrib/pynumero/interfaces/tests/test_external_pyomo_block.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -68,12 +68,8 @@ def _make_external_model(): m.y_out = pyo.Var() m.c_out_1 = pyo.Constraint(expr=m.x_out - m.x == 0) m.c_out_2 = pyo.Constraint(expr=m.y_out - m.y == 0) - m.c_ex_1 = pyo.Constraint( - expr=m.x**3 - 2 * m.y == m.a**2 + m.b**3 - m.r**3 - 2 - ) - m.c_ex_2 = pyo.Constraint( - expr=m.x + m.y**3 == m.a**3 + 2 * m.b**2 + m.r**2 + 1 - ) + m.c_ex_1 = pyo.Constraint(expr=m.x**3 - 2 * m.y == m.a**2 + m.b**3 - m.r**3 - 2) + m.c_ex_2 = pyo.Constraint(expr=m.x + m.y**3 == m.a**3 + 2 * m.b**2 + m.r**2 + 1) return m diff --git a/pyomo/contrib/pynumero/interfaces/tests/test_external_pyomo_model.py b/pyomo/contrib/pynumero/interfaces/tests/test_external_pyomo_model.py index f808decf26c..9773fa7e4a8 100644 --- a/pyomo/contrib/pynumero/interfaces/tests/test_external_pyomo_model.py +++ b/pyomo/contrib/pynumero/interfaces/tests/test_external_pyomo_model.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -901,11 +901,9 @@ def test_full_space_lagrangian_hessians(self): # multipliers won't necessarily correspond). external_model.set_external_constraint_multipliers(lam) hlxx, hlxy, hlyy = external_model.get_full_space_lagrangian_hessians() - ( - pred_hlxx, - pred_hlxy, - pred_hlyy, - ) = model.calculate_full_space_lagrangian_hessians(lam, x) + (pred_hlxx, pred_hlxy, pred_hlyy) = ( + model.calculate_full_space_lagrangian_hessians(lam, x) + ) # TODO: Is comparing the array representation sufficient here? # Should I make sure I get the sparse representation I expect? diff --git a/pyomo/contrib/pynumero/interfaces/tests/test_nlp.py b/pyomo/contrib/pynumero/interfaces/tests/test_nlp.py index 38d44473a67..4f735e06de7 100644 --- a/pyomo/contrib/pynumero/interfaces/tests/test_nlp.py +++ b/pyomo/contrib/pynumero/interfaces/tests/test_nlp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/tests/test_nlp_projections.py b/pyomo/contrib/pynumero/interfaces/tests/test_nlp_projections.py index 7bf693b1eb6..2fada5f679a 100644 --- a/pyomo/contrib/pynumero/interfaces/tests/test_nlp_projections.py +++ b/pyomo/contrib/pynumero/interfaces/tests/test_nlp_projections.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/tests/test_pyomo_grey_box_nlp.py b/pyomo/contrib/pynumero/interfaces/tests/test_pyomo_grey_box_nlp.py index 52536dd9c06..ecadf40e5cf 100644 --- a/pyomo/contrib/pynumero/interfaces/tests/test_pyomo_grey_box_nlp.py +++ b/pyomo/contrib/pynumero/interfaces/tests/test_pyomo_grey_box_nlp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/tests/test_utils.py b/pyomo/contrib/pynumero/interfaces/tests/test_utils.py index dafe89ca2c7..474d26836b9 100644 --- a/pyomo/contrib/pynumero/interfaces/tests/test_utils.py +++ b/pyomo/contrib/pynumero/interfaces/tests/test_utils.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/utils.py b/pyomo/contrib/pynumero/interfaces/utils.py index c7bd04eb002..2aa30fc5946 100644 --- a/pyomo/contrib/pynumero/interfaces/utils.py +++ b/pyomo/contrib/pynumero/interfaces/utils.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/intrinsic.py b/pyomo/contrib/pynumero/intrinsic.py index 5a2dccb64e7..34054e7ffa2 100644 --- a/pyomo/contrib/pynumero/intrinsic.py +++ b/pyomo/contrib/pynumero/intrinsic.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -11,9 +11,7 @@ from pyomo.common.dependencies import numpy as np, attempt_import -block_vector = attempt_import( - 'pyomo.contrib.pynumero.sparse.block_vector', defer_check=True -)[0] +block_vector = attempt_import('pyomo.contrib.pynumero.sparse.block_vector')[0] def norm(x, ord=None): diff --git a/pyomo/contrib/pynumero/linalg/__init__.py b/pyomo/contrib/pynumero/linalg/__init__.py index 09bccd7449b..c1d9ff38825 100644 --- a/pyomo/contrib/pynumero/linalg/__init__.py +++ b/pyomo/contrib/pynumero/linalg/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/linalg/base.py b/pyomo/contrib/pynumero/linalg/base.py index 2b4eeaef451..21565b052a5 100644 --- a/pyomo/contrib/pynumero/linalg/base.py +++ b/pyomo/contrib/pynumero/linalg/base.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from abc import ABCMeta, abstractmethod import enum from typing import Optional, Union, Tuple diff --git a/pyomo/contrib/pynumero/linalg/ma27.py b/pyomo/contrib/pynumero/linalg/ma27.py index 21c137e837b..40a7d0e1064 100644 --- a/pyomo/contrib/pynumero/linalg/ma27.py +++ b/pyomo/contrib/pynumero/linalg/ma27.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/linalg/ma27_interface.py b/pyomo/contrib/pynumero/linalg/ma27_interface.py index 1ae02fe3290..42ac6e73154 100644 --- a/pyomo/contrib/pynumero/linalg/ma27_interface.py +++ b/pyomo/contrib/pynumero/linalg/ma27_interface.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from .base import DirectLinearSolverInterface, LinearSolverStatus, LinearSolverResults from .ma27 import MA27Interface from scipy.sparse import isspmatrix_coo, tril, spmatrix diff --git a/pyomo/contrib/pynumero/linalg/ma57.py b/pyomo/contrib/pynumero/linalg/ma57.py index 1be6c8abcf7..baaa3f34100 100644 --- a/pyomo/contrib/pynumero/linalg/ma57.py +++ b/pyomo/contrib/pynumero/linalg/ma57.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/linalg/ma57_interface.py b/pyomo/contrib/pynumero/linalg/ma57_interface.py index ef80ac653cf..93004406612 100644 --- a/pyomo/contrib/pynumero/linalg/ma57_interface.py +++ b/pyomo/contrib/pynumero/linalg/ma57_interface.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from .base import DirectLinearSolverInterface, LinearSolverStatus, LinearSolverResults from .ma57 import MA57Interface from scipy.sparse import isspmatrix_coo, tril, spmatrix diff --git a/pyomo/contrib/pynumero/linalg/mumps_interface.py b/pyomo/contrib/pynumero/linalg/mumps_interface.py index 95aca114f2f..8735994f16c 100644 --- a/pyomo/contrib/pynumero/linalg/mumps_interface.py +++ b/pyomo/contrib/pynumero/linalg/mumps_interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -175,7 +175,10 @@ def do_numeric_factorization( res.status = LinearSolverStatus.successful elif stat in {-6, -10}: res.status = LinearSolverStatus.singular - elif stat in {-8, -9}: + elif stat in {-8, -9, -19}: + # -8: Integer workspace too small for factorization + # -9: Real workspace too small for factorization + # -19: Maximum size of working memory is too small res.status = LinearSolverStatus.not_enough_memory elif stat < 0: res.status = LinearSolverStatus.error diff --git a/pyomo/contrib/pynumero/linalg/scipy_interface.py b/pyomo/contrib/pynumero/linalg/scipy_interface.py index 819e22ff1aa..025cc539245 100644 --- a/pyomo/contrib/pynumero/linalg/scipy_interface.py +++ b/pyomo/contrib/pynumero/linalg/scipy_interface.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from .base import ( DirectLinearSolverInterface, LinearSolverStatus, diff --git a/pyomo/contrib/pynumero/linalg/tests/__init__.py b/pyomo/contrib/pynumero/linalg/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/pynumero/linalg/tests/__init__.py +++ b/pyomo/contrib/pynumero/linalg/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/pynumero/linalg/tests/test_linear_solvers.py b/pyomo/contrib/pynumero/linalg/tests/test_linear_solvers.py index 8d19127dde6..d2fa955434c 100644 --- a/pyomo/contrib/pynumero/linalg/tests/test_linear_solvers.py +++ b/pyomo/contrib/pynumero/linalg/tests/test_linear_solvers.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.common import unittest from pyomo.contrib.pynumero.dependencies import numpy_available, scipy_available diff --git a/pyomo/contrib/pynumero/linalg/tests/test_ma27.py b/pyomo/contrib/pynumero/linalg/tests/test_ma27.py index 5a02871306a..979be6f747a 100644 --- a/pyomo/contrib/pynumero/linalg/tests/test_ma27.py +++ b/pyomo/contrib/pynumero/linalg/tests/test_ma27.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/linalg/tests/test_ma57.py b/pyomo/contrib/pynumero/linalg/tests/test_ma57.py index 86dbbd3ca50..de245172f96 100644 --- a/pyomo/contrib/pynumero/linalg/tests/test_ma57.py +++ b/pyomo/contrib/pynumero/linalg/tests/test_ma57.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/linalg/tests/test_mumps_interface.py b/pyomo/contrib/pynumero/linalg/tests/test_mumps_interface.py index 9b0aba96be1..8e5b924fb65 100644 --- a/pyomo/contrib/pynumero/linalg/tests/test_mumps_interface.py +++ b/pyomo/contrib/pynumero/linalg/tests/test_mumps_interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/linalg/utils.py b/pyomo/contrib/pynumero/linalg/utils.py index 2b7a9e99142..adec9ae5f35 100644 --- a/pyomo/contrib/pynumero/linalg/utils.py +++ b/pyomo/contrib/pynumero/linalg/utils.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/plugins.py b/pyomo/contrib/pynumero/plugins.py index 06bb0a5a059..c6890cbbb4d 100644 --- a/pyomo/contrib/pynumero/plugins.py +++ b/pyomo/contrib/pynumero/plugins.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/sparse/__init__.py b/pyomo/contrib/pynumero/sparse/__init__.py index e72d1cd7b2d..ee8196566db 100644 --- a/pyomo/contrib/pynumero/sparse/__init__.py +++ b/pyomo/contrib/pynumero/sparse/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/sparse/base_block.py b/pyomo/contrib/pynumero/sparse/base_block.py index 4f2ae385a7e..0b923ce6efb 100644 --- a/pyomo/contrib/pynumero/sparse/base_block.py +++ b/pyomo/contrib/pynumero/sparse/base_block.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/sparse/block_matrix.py b/pyomo/contrib/pynumero/sparse/block_matrix.py index 97e090fec4c..02ad584928b 100644 --- a/pyomo/contrib/pynumero/sparse/block_matrix.py +++ b/pyomo/contrib/pynumero/sparse/block_matrix.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -31,8 +31,6 @@ import logging import warnings -__all__ = ['BlockMatrix', 'NotFullyDefinedBlockMatrixError'] - logger = logging.getLogger(__name__) diff --git a/pyomo/contrib/pynumero/sparse/block_vector.py b/pyomo/contrib/pynumero/sparse/block_vector.py index 00733a71752..b636dd74203 100644 --- a/pyomo/contrib/pynumero/sparse/block_vector.py +++ b/pyomo/contrib/pynumero/sparse/block_vector.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -27,8 +27,6 @@ from ..dependencies import numpy as np from .base_block import BaseBlockVector -__all__ = ['BlockVector', 'NotFullyDefinedBlockVectorError'] - class NotFullyDefinedBlockVectorError(Exception): pass diff --git a/pyomo/contrib/pynumero/sparse/mpi_block_matrix.py b/pyomo/contrib/pynumero/sparse/mpi_block_matrix.py index ee045464dec..d32adebce0e 100644 --- a/pyomo/contrib/pynumero/sparse/mpi_block_matrix.py +++ b/pyomo/contrib/pynumero/sparse/mpi_block_matrix.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -32,8 +32,6 @@ from scipy.sparse import coo_matrix import operator -__all__ = ['MPIBlockMatrix'] - def assert_block_structure(mat: MPIBlockMatrix): if mat.has_undefined_row_sizes() or mat.has_undefined_col_sizes(): diff --git a/pyomo/contrib/pynumero/sparse/mpi_block_vector.py b/pyomo/contrib/pynumero/sparse/mpi_block_vector.py index 5d89bbf5522..89cf136a5f7 100644 --- a/pyomo/contrib/pynumero/sparse/mpi_block_vector.py +++ b/pyomo/contrib/pynumero/sparse/mpi_block_vector.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -17,8 +17,6 @@ import numpy as np import operator -__all__ = ['MPIBlockVector'] - def assert_block_structure(vec): if vec.has_none: @@ -1112,9 +1110,9 @@ def make_local_copy(self): if ndx in block_indices: blk = self.get_block(ndx) if isinstance(blk, BlockVector): - local_data[ - offset : offset + self.get_block_size(ndx) - ] = blk.flatten() + local_data[offset : offset + self.get_block_size(ndx)] = ( + blk.flatten() + ) elif isinstance(blk, np.ndarray): local_data[offset : offset + self.get_block_size(ndx)] = blk else: diff --git a/pyomo/contrib/pynumero/sparse/tests/__init__.py b/pyomo/contrib/pynumero/sparse/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/pynumero/sparse/tests/__init__.py +++ b/pyomo/contrib/pynumero/sparse/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/sparse/tests/test_block_matrix.py b/pyomo/contrib/pynumero/sparse/tests/test_block_matrix.py index 7402881a285..48c1d3dc77e 100644 --- a/pyomo/contrib/pynumero/sparse/tests/test_block_matrix.py +++ b/pyomo/contrib/pynumero/sparse/tests/test_block_matrix.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/sparse/tests/test_block_vector.py b/pyomo/contrib/pynumero/sparse/tests/test_block_vector.py index 2d1bc7b640d..610d41a09a4 100644 --- a/pyomo/contrib/pynumero/sparse/tests/test_block_vector.py +++ b/pyomo/contrib/pynumero/sparse/tests/test_block_vector.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,7 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from __future__ import division + import pyomo.common.unittest as unittest from pyomo.contrib.pynumero.dependencies import ( diff --git a/pyomo/contrib/pynumero/sparse/tests/test_intrinsics.py b/pyomo/contrib/pynumero/sparse/tests/test_intrinsics.py index 0768442c2c4..ef0a5142849 100644 --- a/pyomo/contrib/pynumero/sparse/tests/test_intrinsics.py +++ b/pyomo/contrib/pynumero/sparse/tests/test_intrinsics.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/sparse/tests/test_mpi_block_matrix.py b/pyomo/contrib/pynumero/sparse/tests/test_mpi_block_matrix.py index 1415636c50d..917b4433120 100644 --- a/pyomo/contrib/pynumero/sparse/tests/test_mpi_block_matrix.py +++ b/pyomo/contrib/pynumero/sparse/tests/test_mpi_block_matrix.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/sparse/tests/test_mpi_block_vector.py b/pyomo/contrib/pynumero/sparse/tests/test_mpi_block_vector.py index cd37b7543a2..c28c524823a 100644 --- a/pyomo/contrib/pynumero/sparse/tests/test_mpi_block_vector.py +++ b/pyomo/contrib/pynumero/sparse/tests/test_mpi_block_vector.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/src/AmplInterface.cpp b/pyomo/contrib/pynumero/src/AmplInterface.cpp index 26053a9611b..805955f7671 100644 --- a/pyomo/contrib/pynumero/src/AmplInterface.cpp +++ b/pyomo/contrib/pynumero/src/AmplInterface.cpp @@ -1,7 +1,7 @@ /**___________________________________________________________________________ * * Pyomo: Python Optimization Modeling Objects - * Copyright (c) 2008-2022 + * Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC * Under the terms of Contract DE-NA0003525 with National Technology and * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/src/AmplInterface.hpp b/pyomo/contrib/pynumero/src/AmplInterface.hpp index 259cf88d895..bedc6d4f669 100644 --- a/pyomo/contrib/pynumero/src/AmplInterface.hpp +++ b/pyomo/contrib/pynumero/src/AmplInterface.hpp @@ -1,7 +1,7 @@ /**___________________________________________________________________________ * * Pyomo: Python Optimization Modeling Objects - * Copyright (c) 2008-2022 + * Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC * Under the terms of Contract DE-NA0003525 with National Technology and * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/src/AssertUtils.hpp b/pyomo/contrib/pynumero/src/AssertUtils.hpp index ba2e5dc887f..061442eb6e9 100644 --- a/pyomo/contrib/pynumero/src/AssertUtils.hpp +++ b/pyomo/contrib/pynumero/src/AssertUtils.hpp @@ -1,7 +1,7 @@ /**___________________________________________________________________________ * * Pyomo: Python Optimization Modeling Objects - * Copyright (c) 2008-2022 + * Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC * Under the terms of Contract DE-NA0003525 with National Technology and * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/src/ma27Interface.cpp b/pyomo/contrib/pynumero/src/ma27Interface.cpp index 624c7edd6f3..4816e1274e3 100644 --- a/pyomo/contrib/pynumero/src/ma27Interface.cpp +++ b/pyomo/contrib/pynumero/src/ma27Interface.cpp @@ -1,3 +1,15 @@ +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2024 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + #include #include #include diff --git a/pyomo/contrib/pynumero/src/ma57Interface.cpp b/pyomo/contrib/pynumero/src/ma57Interface.cpp index 99b98ef6215..fa9cf4e6811 100644 --- a/pyomo/contrib/pynumero/src/ma57Interface.cpp +++ b/pyomo/contrib/pynumero/src/ma57Interface.cpp @@ -1,3 +1,15 @@ +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2024 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + #include #include #include diff --git a/pyomo/contrib/pynumero/src/tests/simple_test.cpp b/pyomo/contrib/pynumero/src/tests/simple_test.cpp index 4edbbb67a35..9f39fbbd8ff 100644 --- a/pyomo/contrib/pynumero/src/tests/simple_test.cpp +++ b/pyomo/contrib/pynumero/src/tests/simple_test.cpp @@ -1,3 +1,15 @@ +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2024 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + #include #include "AmplInterface.hpp" diff --git a/pyomo/contrib/pynumero/tests/__init__.py b/pyomo/contrib/pynumero/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/pynumero/tests/__init__.py +++ b/pyomo/contrib/pynumero/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/pyros/CHANGELOG.txt b/pyomo/contrib/pyros/CHANGELOG.txt index b1866ed955c..52cd7a6db47 100644 --- a/pyomo/contrib/pyros/CHANGELOG.txt +++ b/pyomo/contrib/pyros/CHANGELOG.txt @@ -2,6 +2,49 @@ PyROS CHANGELOG =============== +------------------------------------------------------------------------------- +PyROS 1.2.11 17 Mar 2024 +------------------------------------------------------------------------------- +- Standardize calls to subordinate solvers across all PyROS subproblem types +- Account for user-specified subsolver time limits when automatically + adjusting subsolver time limits +- Add support for automatic adjustment of SCIP subsolver time limit +- Move start point of main PyROS solver timer to just before argument + validation begins + + +------------------------------------------------------------------------------- +PyROS 1.2.10 07 Feb 2024 +------------------------------------------------------------------------------- +- Update argument resolution and validation routines of `PyROS.solve()` +- Use methods of `common.config` for docstring of `PyROS.solve()` + + +------------------------------------------------------------------------------- +PyROS 1.2.9 15 Dec 2023 +------------------------------------------------------------------------------- +- Fix DR polishing optimality constraint for case of nominal objective focus +- Use previous separation solution to initialize second-stage and state + variables of new master block; simplify the master feasibility problem +- Use best known solution from master to initialize separation problems + per performance constraint +- Refactor DR variable and constraint declaration routines. +- Refactor DR polishing routine; initialize auxiliary variables + to values they are meant to represent + + +------------------------------------------------------------------------------- +PyROS 1.2.8 12 Oct 2023 +------------------------------------------------------------------------------- +- Refactor PyROS separation routine, fix scenario selection heuristic +- Add efficiency for discrete uncertainty set separation +- Fix coefficient matching routine +- Fix subproblem timers and time accumulators +- Update and document PyROS solver logging system +- Fix iteration overcounting in event of `max_iter` termination status +- Fixes to (assembly of) PyROS `ROSolveResults` object + + ------------------------------------------------------------------------------- PyROS 1.2.7 26 Apr 2023 ------------------------------------------------------------------------------- diff --git a/pyomo/contrib/pyros/__init__.py b/pyomo/contrib/pyros/__init__.py index aeb92eb13fd..4e134ef1166 100644 --- a/pyomo/contrib/pyros/__init__.py +++ b/pyomo/contrib/pyros/__init__.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.contrib.pyros.pyros import PyROS from pyomo.contrib.pyros.pyros import ObjectiveType, pyrosTerminationCondition from pyomo.contrib.pyros.uncertainty_sets import ( diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py new file mode 100644 index 00000000000..c02dcd7ed0f --- /dev/null +++ b/pyomo/contrib/pyros/config.py @@ -0,0 +1,879 @@ +""" +Interfaces for managing PyROS solver options. +""" + +from collections.abc import Iterable +import logging + +from pyomo.common.collections import ComponentSet +from pyomo.common.config import ( + ConfigDict, + ConfigValue, + In, + IsInstance, + NonNegativeFloat, + InEnum, + Path, +) +from pyomo.common.errors import ApplicationError, PyomoException +from pyomo.core.base import Var, VarData +from pyomo.core.base.param import Param, ParamData +from pyomo.opt import SolverFactory +from pyomo.contrib.pyros.util import ObjectiveType, setup_pyros_logger +from pyomo.contrib.pyros.uncertainty_sets import UncertaintySet + + +default_pyros_solver_logger = setup_pyros_logger() + + +def logger_domain(obj): + """ + Domain validator for logger-type arguments. + + This admits any object of type ``logging.Logger``, + or which can be cast to ``logging.Logger``. + """ + if isinstance(obj, logging.Logger): + return obj + else: + return logging.getLogger(obj) + + +logger_domain.domain_name = "None, str or logging.Logger" + + +def positive_int_or_minus_one(obj): + """ + Domain validator for objects castable to a strictly + positive int or -1. + """ + ans = int(obj) + if ans != float(obj) or (ans <= 0 and ans != -1): + raise ValueError(f"Expected positive int or -1, but received value {obj!r}") + return ans + + +positive_int_or_minus_one.domain_name = "positive int or -1" + + +def mutable_param_validator(param_obj): + """ + Check that Param-like object has attribute `mutable=True`. + + Parameters + ---------- + param_obj : Param or ParamData + Param-like object of interest. + + Raises + ------ + ValueError + If lengths of the param object and the accompanying + index set do not match. This may occur if some entry + of the Param is not initialized. + ValueError + If attribute `mutable` is of value False. + """ + if len(param_obj) != len(param_obj.index_set()): + raise ValueError( + f"Length of Param component object with " + f"name {param_obj.name!r} is {len(param_obj)}, " + "and does not match that of its index set, " + f"which is of length {len(param_obj.index_set())}. " + "Check that all entries of the component object " + "have been initialized." + ) + if not param_obj.mutable: + raise ValueError(f"Param object with name {param_obj.name!r} is immutable.") + + +class InputDataStandardizer(object): + """ + Standardizer for objects castable to a list of Pyomo + component types. + + Parameters + ---------- + ctype : type + Pyomo component type, such as Component, Var or Param. + cdatatype : type + Corresponding Pyomo component data type, such as + ComponentData, VarData, or ParamData. + ctype_validator : callable, optional + Validator function for objects of type `ctype`. + cdatatype_validator : callable, optional + Validator function for objects of type `cdatatype`. + allow_repeats : bool, optional + True to allow duplicate component data entries in final + list to which argument is cast, False otherwise. + + Attributes + ---------- + ctype + cdatatype + ctype_validator + cdatatype_validator + allow_repeats + """ + + def __init__( + self, + ctype, + cdatatype, + ctype_validator=None, + cdatatype_validator=None, + allow_repeats=False, + ): + """Initialize self (see class docstring).""" + self.ctype = ctype + self.cdatatype = cdatatype + self.ctype_validator = ctype_validator + self.cdatatype_validator = cdatatype_validator + self.allow_repeats = allow_repeats + + def standardize_ctype_obj(self, obj): + """ + Standardize object of type ``self.ctype`` to list + of objects of type ``self.cdatatype``. + """ + if self.ctype_validator is not None: + self.ctype_validator(obj) + return list(obj.values()) + + def standardize_cdatatype_obj(self, obj): + """ + Standardize object of type ``self.cdatatype`` to + ``[obj]``. + """ + if self.cdatatype_validator is not None: + self.cdatatype_validator(obj) + return [obj] + + def __call__(self, obj, from_iterable=None, allow_repeats=None): + """ + Cast object to a flat list of Pyomo component data type + entries. + + Parameters + ---------- + obj : object + Object to be cast. + from_iterable : Iterable or None, optional + Iterable from which `obj` obtained, if any. + allow_repeats : bool or None, optional + True if list can contain repeated entries, + False otherwise. + + Raises + ------ + TypeError + If all entries in the resulting list + are not of type ``self.cdatatype``. + ValueError + If the resulting list contains duplicate entries. + """ + if allow_repeats is None: + allow_repeats = self.allow_repeats + + if isinstance(obj, self.ctype): + ans = self.standardize_ctype_obj(obj) + elif isinstance(obj, self.cdatatype): + ans = self.standardize_cdatatype_obj(obj) + elif isinstance(obj, Iterable) and not isinstance(obj, str): + ans = [] + for item in obj: + ans.extend(self.__call__(item, from_iterable=obj)) + else: + from_iterable_qual = ( + f" (entry of iterable {from_iterable})" + if from_iterable is not None + else "" + ) + raise TypeError( + f"Input object {obj!r}{from_iterable_qual} " + "is not of valid component type " + f"{self.ctype.__name__} or component data type " + f"{self.cdatatype.__name__}." + ) + + # check for duplicates if desired + if not allow_repeats and len(ans) != len(ComponentSet(ans)): + comp_name_list = [comp.name for comp in ans] + raise ValueError( + f"Standardized component list {comp_name_list} " + f"derived from input {obj} " + "contains duplicate entries." + ) + + return ans + + def domain_name(self): + """Return str briefly describing domain encompassed by self.""" + return ( + f"{self.cdatatype.__name__}, {self.ctype.__name__}, " + f"or Iterable of {self.cdatatype.__name__}/{self.ctype.__name__}" + ) + + +class SolverNotResolvable(PyomoException): + """ + Exception type for failure to cast an object to a Pyomo solver. + """ + + +class SolverResolvable(object): + """ + Callable for casting an object (such as a str) + to a Pyomo solver. + + Parameters + ---------- + require_available : bool, optional + True if `available()` method of a standardized solver + object obtained through `self` must return `True`, + False otherwise. + solver_desc : str, optional + Descriptor for the solver obtained through `self`, + such as 'local solver' + or 'global solver'. This argument is used + for constructing error/exception messages. + + Attributes + ---------- + require_available + solver_desc + """ + + def __init__(self, require_available=True, solver_desc="solver"): + """Initialize self (see class docstring).""" + self.require_available = require_available + self.solver_desc = solver_desc + + @staticmethod + def is_solver_type(obj): + """ + Return True if object is considered a Pyomo solver, + False otherwise. + + An object is considered a Pyomo solver provided that + it has callable attributes named 'solve' and + 'available'. + """ + return callable(getattr(obj, "solve", None)) and callable( + getattr(obj, "available", None) + ) + + def __call__(self, obj, require_available=None, solver_desc=None): + """ + Cast object to a Pyomo solver. + + If `obj` is a string, then ``SolverFactory(obj.lower())`` + is returned. If `obj` is a Pyomo solver type, then + `obj` is returned. + + Parameters + ---------- + obj : object + Object to be cast to Pyomo solver type. + require_available : bool or None, optional + True if `available()` method of the resolved solver + object must return True, False otherwise. + If `None` is passed, then ``self.require_available`` + is used. + solver_desc : str or None, optional + Brief description of the solver, such as 'local solver' + or 'backup global solver'. This argument is used + for constructing error/exception messages. + If `None` is passed, then ``self.solver_desc`` + is used. + + Returns + ------- + Solver + Pyomo solver. + + Raises + ------ + SolverNotResolvable + If `obj` cannot be cast to a Pyomo solver because + it is neither a str nor a Pyomo solver type. + ApplicationError + In event that solver is not available, the + method `available(exception_flag=True)` of the + solver to which `obj` is cast should raise an + exception of this type. The present method + will also emit a more detailed error message + through the default PyROS logger. + """ + # resort to defaults if necessary + if require_available is None: + require_available = self.require_available + if solver_desc is None: + solver_desc = self.solver_desc + + # perform casting + if isinstance(obj, str): + solver = SolverFactory(obj.lower()) + elif self.is_solver_type(obj): + solver = obj + else: + raise SolverNotResolvable( + f"Cannot cast object `{obj!r}` to a Pyomo optimizer for use as " + f"{solver_desc}, as the object is neither a str nor a " + f"Pyomo Solver type (got type {type(obj).__name__})." + ) + + # availability check, if so desired + if require_available: + try: + solver.available(exception_flag=True) + except ApplicationError: + default_pyros_solver_logger.exception( + f"Output of `available()` method for {solver_desc} " + f"with repr {solver!r} resolved from object {obj} " + "is not `True`. " + "Check solver and any required dependencies " + "have been set up properly." + ) + raise + + return solver + + def domain_name(self): + """Return str briefly describing domain encompassed by self.""" + return "str or Solver" + + +class SolverIterable(object): + """ + Callable for casting an iterable (such as a list of strs) + to a list of Pyomo solvers. + + Parameters + ---------- + require_available : bool, optional + True if `available()` method of a standardized solver + object obtained through `self` must return `True`, + False otherwise. + filter_by_availability : bool, optional + True to remove standardized solvers for which `available()` + does not return True, False otherwise. + solver_desc : str, optional + Descriptor for the solver obtained through `self`, + such as 'backup local solver' + or 'backup global solver'. + """ + + def __init__( + self, require_available=True, filter_by_availability=True, solver_desc="solver" + ): + """Initialize self (see class docstring).""" + self.require_available = require_available + self.filter_by_availability = filter_by_availability + self.solver_desc = solver_desc + + def __call__( + self, obj, require_available=None, filter_by_availability=None, solver_desc=None + ): + """ + Cast iterable object to a list of Pyomo solver objects. + + Parameters + ---------- + obj : str, Solver, or Iterable of str/Solver + Object of interest. + require_available : bool or None, optional + True if `available()` method of each solver + object must return True, False otherwise. + If `None` is passed, then ``self.require_available`` + is used. + solver_desc : str or None, optional + Descriptor for the solver, such as 'backup local solver' + or 'backup global solver'. This argument is used + for constructing error/exception messages. + If `None` is passed, then ``self.solver_desc`` + is used. + + Returns + ------- + solvers : list of solver type + List of solver objects to which obj is cast. + + Raises + ------ + TypeError + If `obj` is a str. + """ + if require_available is None: + require_available = self.require_available + if filter_by_availability is None: + filter_by_availability = self.filter_by_availability + if solver_desc is None: + solver_desc = self.solver_desc + + solver_resolve_func = SolverResolvable() + + if isinstance(obj, str) or solver_resolve_func.is_solver_type(obj): + # single solver resolvable is cast to singleton list. + # perform explicit check for str, otherwise this method + # would attempt to resolve each character. + obj_as_list = [obj] + else: + obj_as_list = list(obj) + + solvers = [] + for idx, val in enumerate(obj_as_list): + solver_desc_str = f"{solver_desc} " f"(index {idx})" + opt = solver_resolve_func( + obj=val, + require_available=require_available, + solver_desc=solver_desc_str, + ) + if filter_by_availability and not opt.available(exception_flag=False): + default_pyros_solver_logger.warning( + f"Output of `available()` method for solver object {opt} " + f"resolved from object {val} of sequence {obj_as_list} " + f"to be used as {self.solver_desc} " + "is not `True`. " + "Removing from list of standardized solvers." + ) + else: + solvers.append(opt) + + return solvers + + def domain_name(self): + """Return str briefly describing domain encompassed by self.""" + return "str, solver type, or Iterable of str/solver type" + + +def pyros_config(): + CONFIG = ConfigDict('PyROS') + + # ================================================ + # === Options common to all solvers + # ================================================ + CONFIG.declare( + 'time_limit', + ConfigValue( + default=None, + domain=NonNegativeFloat, + doc=( + """ + Wall time limit for the execution of the PyROS solver + in seconds (including time spent by subsolvers). + If `None` is provided, then no time limit is enforced. + """ + ), + ), + ) + CONFIG.declare( + 'keepfiles', + ConfigValue( + default=False, + domain=bool, + description=( + """ + Export subproblems with a non-acceptable termination status + for debugging purposes. + If True is provided, then the argument + `subproblem_file_directory` must also be specified. + """ + ), + ), + ) + CONFIG.declare( + 'tee', + ConfigValue( + default=False, + domain=bool, + description="Output subordinate solver logs for all subproblems.", + ), + ) + CONFIG.declare( + 'load_solution', + ConfigValue( + default=True, + domain=bool, + description=( + """ + Load final solution(s) found by PyROS to the deterministic + model provided. + """ + ), + ), + ) + CONFIG.declare( + 'symbolic_solver_labels', + ConfigValue( + default=False, + domain=bool, + description=( + """ + True to ensure the component names given to the + subordinate solvers for every subproblem reflect + the names of the corresponding Pyomo modeling components, + False otherwise. + """ + ), + ), + ) + + # ================================================ + # === Required User Inputs + # ================================================ + CONFIG.declare( + "first_stage_variables", + ConfigValue( + default=[], + domain=InputDataStandardizer(Var, VarData, allow_repeats=False), + description="First-stage (or design) variables.", + visibility=1, + ), + ) + CONFIG.declare( + "second_stage_variables", + ConfigValue( + default=[], + domain=InputDataStandardizer(Var, VarData, allow_repeats=False), + description="Second-stage (or control) variables.", + visibility=1, + ), + ) + CONFIG.declare( + "uncertain_params", + ConfigValue( + default=[], + domain=InputDataStandardizer( + ctype=Param, + cdatatype=ParamData, + ctype_validator=mutable_param_validator, + allow_repeats=False, + ), + description=( + """ + Uncertain model parameters. + The `mutable` attribute for all uncertain parameter + objects should be set to True. + """ + ), + visibility=1, + ), + ) + CONFIG.declare( + "uncertainty_set", + ConfigValue( + default=None, + domain=IsInstance(UncertaintySet), + description=( + """ + Uncertainty set against which the + final solution(s) returned by PyROS should be certified + to be robust. + """ + ), + visibility=1, + ), + ) + CONFIG.declare( + "local_solver", + ConfigValue( + default=None, + domain=SolverResolvable(solver_desc="local solver", require_available=True), + description="Subordinate local NLP solver.", + visibility=1, + ), + ) + CONFIG.declare( + "global_solver", + ConfigValue( + default=None, + domain=SolverResolvable( + solver_desc="global solver", require_available=True + ), + description="Subordinate global NLP solver.", + visibility=1, + ), + ) + # ================================================ + # === Optional User Inputs + # ================================================ + CONFIG.declare( + "objective_focus", + ConfigValue( + default=ObjectiveType.nominal, + domain=InEnum(ObjectiveType), + description=( + """ + Choice of objective focus to optimize in the master problems. + Choices are: `ObjectiveType.worst_case`, + `ObjectiveType.nominal`. + """ + ), + doc=( + """ + Objective focus for the master problems: + + - `ObjectiveType.nominal`: + Optimize the objective function subject to the nominal + uncertain parameter realization. + - `ObjectiveType.worst_case`: + Optimize the objective function subject to the worst-case + uncertain parameter realization. + + By default, `ObjectiveType.nominal` is chosen. + + A worst-case objective focus is required for certification + of robust optimality of the final solution(s) returned + by PyROS. + If a nominal objective focus is chosen, then only robust + feasibility is guaranteed. + """ + ), + ), + ) + CONFIG.declare( + "nominal_uncertain_param_vals", + ConfigValue( + default=[], + domain=list, + doc=( + """ + Nominal uncertain parameter realization. + Entries should be provided in an order consistent with the + entries of the argument `uncertain_params`. + If an empty list is provided, then the values of the `Param` + objects specified through `uncertain_params` are chosen. + """ + ), + ), + ) + CONFIG.declare( + "decision_rule_order", + ConfigValue( + default=0, + domain=In([0, 1, 2]), + description=( + """ + Order (or degree) of the polynomial decision rule functions + used for approximating the adjustability of the second stage + variables with respect to the uncertain parameters. + """ + ), + doc=( + """ + Order (or degree) of the polynomial decision rule functions + for approximating the adjustability of the second stage + variables with respect to the uncertain parameters. + + Choices are: + + - 0: static recourse + - 1: affine recourse + - 2: quadratic recourse + """ + ), + ), + ) + CONFIG.declare( + "solve_master_globally", + ConfigValue( + default=False, + domain=bool, + doc=( + """ + True to solve all master problems with the subordinate + global solver, False to solve all master problems with + the subordinate local solver. + Along with a worst-case objective focus + (see argument `objective_focus`), + solving the master problems to global optimality is required + for certification + of robust optimality of the final solution(s) returned + by PyROS. Otherwise, only robust feasibility is guaranteed. + """ + ), + ), + ) + CONFIG.declare( + "max_iter", + ConfigValue( + default=-1, + domain=positive_int_or_minus_one, + description=( + """ + Iteration limit. If -1 is provided, then no iteration + limit is enforced. + """ + ), + ), + ) + CONFIG.declare( + "robust_feasibility_tolerance", + ConfigValue( + default=1e-4, + domain=NonNegativeFloat, + description=( + """ + Relative tolerance for assessing maximal inequality + constraint violations during the GRCS separation step. + """ + ), + ), + ) + CONFIG.declare( + "separation_priority_order", + ConfigValue( + default={}, + domain=dict, + doc=( + """ + Mapping from model inequality constraint names + to positive integers specifying the priorities + of their corresponding separation subproblems. + A higher integer value indicates a higher priority. + Constraints not referenced in the `dict` assume + a priority of 0. + Separation subproblems are solved in order of decreasing + priority. + """ + ), + ), + ) + CONFIG.declare( + "progress_logger", + ConfigValue( + default=default_pyros_solver_logger, + domain=logger_domain, + doc=( + """ + Logger (or name thereof) used for reporting PyROS solver + progress. If `None` or a `str` is provided, then + ``progress_logger`` + is cast to ``logging.getLogger(progress_logger)``. + In the default case, `progress_logger` is set to + a :class:`pyomo.contrib.pyros.util.PreformattedLogger` + object of level ``logging.INFO``. + """ + ), + ), + ) + CONFIG.declare( + "backup_local_solvers", + ConfigValue( + default=[], + domain=SolverIterable( + solver_desc="backup local solver", + require_available=False, + filter_by_availability=True, + ), + doc=( + """ + Additional subordinate local NLP optimizers to invoke + in the event the primary local NLP optimizer fails + to solve a subproblem to an acceptable termination condition. + """ + ), + ), + ) + CONFIG.declare( + "backup_global_solvers", + ConfigValue( + default=[], + domain=SolverIterable( + solver_desc="backup global solver", + require_available=False, + filter_by_availability=True, + ), + doc=( + """ + Additional subordinate global NLP optimizers to invoke + in the event the primary global NLP optimizer fails + to solve a subproblem to an acceptable termination condition. + """ + ), + ), + ) + CONFIG.declare( + "subproblem_file_directory", + ConfigValue( + default=None, + domain=Path(), + description=( + """ + Directory to which to export subproblems not successfully + solved to an acceptable termination condition. + In the event ``keepfiles=True`` is specified, a str or + path-like referring to an existing directory must be + provided. + """ + ), + ), + ) + + # ================================================ + # === Advanced Options + # ================================================ + CONFIG.declare( + "bypass_local_separation", + ConfigValue( + default=False, + domain=bool, + description=( + """ + This is an advanced option. + Solve all separation subproblems with the subordinate global + solver(s) only. + This option is useful for expediting PyROS + in the event that the subordinate global optimizer(s) provided + can quickly solve separation subproblems to global optimality. + """ + ), + ), + ) + CONFIG.declare( + "bypass_global_separation", + ConfigValue( + default=False, + domain=bool, + doc=( + """ + This is an advanced option. + Solve all separation subproblems with the subordinate local + solver(s) only. + If `True` is chosen, then robustness of the final solution(s) + returned by PyROS is not guaranteed, and a warning will + be issued at termination. + This option is useful for expediting PyROS + in the event that the subordinate global optimizer provided + cannot tractably solve separation subproblems to global + optimality. + """ + ), + ), + ) + CONFIG.declare( + "p_robustness", + ConfigValue( + default={}, + domain=dict, + doc=( + """ + This is an advanced option. + Add p-robustness constraints to all master subproblems. + If an empty dict is provided, then p-robustness constraints + are not added. + Otherwise, the dict must map a `str` of value ``'rho'`` + to a non-negative `float`. PyROS automatically + specifies ``1 + p_robustness['rho']`` + as an upper bound for the ratio of the + objective function value under any PyROS-sampled uncertain + parameter realization to the objective function under + the nominal parameter realization. + """ + ), + visibility=1, + ), + ) + + return CONFIG diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index a0e2245cab1..2af38c1d582 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -1,6 +1,18 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ Functions for handling the construction and solving of the GRCS master problem via ROSolver """ + from pyomo.core.base import ( ConcreteModel, Block, @@ -15,6 +27,7 @@ from pyomo.core.expr import value from pyomo.core.base.set_types import NonNegativeIntegers, NonNegativeReals from pyomo.contrib.pyros.util import ( + call_solver, selective_clone, ObjectiveType, pyrosTerminationCondition, @@ -22,7 +35,6 @@ adjust_solver_time_settings, revert_solver_max_time_adjustment, get_main_elapsed_time, - output_logger, ) from pyomo.contrib.pyros.solve_data import MasterProblemData, MasterResult from pyomo.opt.results import check_optimal_termination @@ -37,7 +49,7 @@ from pyomo.common.modeling import unique_component_name from pyomo.common.timing import TicTocTimer -from pyomo.contrib.pyros.util import TIC_TOC_SOLVE_TIME_ATTR +from pyomo.contrib.pyros.util import TIC_TOC_SOLVE_TIME_ATTR, enforce_dr_degree def initial_construct_master(model_data): @@ -103,6 +115,11 @@ def construct_master_feasibility_problem(model_data, config): Slack variable model. """ + # clone master model. current state: + # - variables for all but newest block are set to values from + # master solution from previous iteration + # - variables for newest block are set to values from separation + # solution chosen in previous iteration model = model_data.master_model.clone() # obtain mapping from master problem to master feasibility @@ -124,53 +141,28 @@ def construct_master_feasibility_problem(model_data, config): obj.deactivate() iteration = model_data.iteration - # first stage vars are already initialized appropriately. - # initialize second-stage DOF variables using DR equation expressions - if model.scenarios[iteration, 0].util.second_stage_variables: - for blk in model.scenarios[iteration, :]: - for eq in blk.util.decision_rule_eqns: - vars_in_dr_eq = ComponentSet(identify_variables(eq.body)) - ssv_set = ComponentSet(blk.util.second_stage_variables) - - # get second-stage var in DR eqn. should only be one var - ssv_in_dr_eq = [var for var in vars_in_dr_eq if var in ssv_set][0] - - # update var value for initialization - # fine since DR eqns are f(d) - z == 0 (not z - f(d) == 0) - ssv_in_dr_eq.set_value(0) - ssv_in_dr_eq.set_value(value(eq.body)) - - # initialize state vars to previous master solution values - if iteration != 0: - stvar_map = get_state_vars(model, [iteration, iteration - 1]) - for current, prev in zip(stvar_map[iteration], stvar_map[iteration - 1]): - current.set_value(value(prev)) - - # constraints to which slacks should be added - # (all the constraints for the current iteration, except the DR eqns) + # add slacks only to inequality constraints for the newest + # master block. these should be the only constraints which + # may have been violated by the previous master and separation + # solution(s) targets = [] for blk in model.scenarios[iteration, :]: - if blk.util.second_stage_variables: - dr_eqs = blk.util.decision_rule_eqns - else: - dr_eqs = list() - targets.extend( [ con for con in blk.component_data_objects( Constraint, active=True, descend_into=True ) - if con not in dr_eqs + if not con.equality ] ) - # retain original constraint exprs (for slack initialization and scaling) + # retain original constraint expressions + # (for slack initialization and scaling) pre_slack_con_exprs = ComponentMap((con, con.body - con.upper) for con in targets) # add slack variables and objective - # inequalities g(v) <= b become g(v) -- s^-<= b - # equalities h(v) == b become h(v) -- s^- + s^+ == b + # inequalities g(v) <= b become g(v) - s^- <= b TransformationFactory("core.add_slack_variables").apply_to(model, targets=targets) slack_vars = ComponentSet( model._core_add_slack_variables.component_data_objects(Var, descend_into=True) @@ -178,8 +170,8 @@ def construct_master_feasibility_problem(model_data, config): # initialize and scale slack variables for con in pre_slack_con_exprs: - # obtain slack vars in updated constraints - # and their coefficients (+/-1) in the constraint expression + # get mapping from slack variables to their (linear) + # coefficients (+/-1) in the updated constraint expressions repn = generate_standard_repn(con.body) slack_var_coef_map = ComponentMap() for idx in range(len(repn.linear_vars)): @@ -188,19 +180,19 @@ def construct_master_feasibility_problem(model_data, config): slack_var_coef_map[var] = repn.linear_coefs[idx] slack_substitution_map = dict() - for slack_var in slack_var_coef_map: - # coefficient determines whether the slack is a +ve or -ve slack + # coefficient determines whether the slack + # is a +ve or -ve slack if slack_var_coef_map[slack_var] == -1: con_slack = max(0, value(pre_slack_con_exprs[con])) else: con_slack = max(0, -value(pre_slack_con_exprs[con])) - # initialize slack var, evaluate scaling coefficient - scaling_coeff = 1 + # initialize slack variable, evaluate scaling coefficient slack_var.set_value(con_slack) + scaling_coeff = 1 - # update expression replacement map + # update expression replacement map for slack scaling slack_substitution_map[id(slack_var)] = scaling_coeff * slack_var # finally, scale slack(s) @@ -236,34 +228,30 @@ def solve_master_feasibility_problem(model_data, config): """ model = construct_master_feasibility_problem(model_data, config) + active_obj = next(model.component_data_objects(Objective, active=True)) + + config.progress_logger.debug("Solving master feasibility problem") + config.progress_logger.debug( + f" Initial objective (total slack): {value(active_obj)}" + ) + if config.solve_master_globally: solver = config.global_solver else: solver = config.local_solver - timer = TicTocTimer() - orig_setting, custom_setting_present = adjust_solver_time_settings( - model_data.timing, solver, config + results = call_solver( + model=model, + solver=solver, + config=config, + timing_obj=model_data.timing, + timer_name="main.master_feasibility", + err_msg=( + f"Optimizer {repr(solver)} encountered exception " + "attempting to solve master feasibility problem in iteration " + f"{model_data.iteration}." + ), ) - timer.tic(msg=None) - try: - results = solver.solve(model, tee=config.tee, load_solutions=False) - except ApplicationError: - # account for possible external subsolver errors - # (such as segmentation faults, function evaluation - # errors, etc.) - config.progress_logger.error( - f"Solver {repr(solver)} encountered exception attempting to " - "optimize master feasibility problem in iteration " - f"{model_data.iteration}" - ) - raise - else: - setattr(results.solver, TIC_TOC_SOLVE_TIME_ATTR, timer.toc(msg=None)) - finally: - revert_solver_max_time_adjustment( - solver, orig_setting, custom_setting_present, config - ) feasible_terminations = { tc.optimal, @@ -273,6 +261,24 @@ def solve_master_feasibility_problem(model_data, config): } if results.solver.termination_condition in feasible_terminations: model.solutions.load_from(results) + config.progress_logger.debug( + f" Final objective (total slack): {value(active_obj)}" + ) + config.progress_logger.debug( + f" Termination condition: {results.solver.termination_condition}" + ) + config.progress_logger.debug( + f" Solve time: {getattr(results.solver, TIC_TOC_SOLVE_TIME_ATTR)}s" + ) + else: + config.progress_logger.warning( + "Could not successfully solve master feasibility problem " + f"of iteration {model_data.iteration} with primary subordinate " + f"{'global' if config.solve_master_globally else 'local'} solver " + "to acceptable level. " + f"Termination stats:\n{results.solver}\n" + "Maintaining unoptimized point for master problem initialization." + ) # load master feasibility point to master model for master_var, feas_var in model_data.feasibility_problem_varmap: @@ -281,11 +287,10 @@ def solve_master_feasibility_problem(model_data, config): return results -def minimize_dr_vars(model_data, config): +def construct_dr_polishing_problem(model_data, config): """ - Polish the PyROS decision rule determined for the most - recently solved master problem by minimizing the collective - L1 norm of the vector of all decision rule variables. + Construct DR polishing problem from most recently added + master problem. Parameters ---------- @@ -296,208 +301,216 @@ def minimize_dr_vars(model_data, config): Returns ------- - results : SolverResults - Subordinate solver results for the polishing problem. + polishing_model : ConcreteModel + Polishing model. + + Note + ---- + Polishing problem is to minimize the L1-norm of the vector of + all decision rule polynomial terms, subject to the original + master problem constraints, with all first-stage variables + (including epigraph) fixed. Optimality of the polished + DR with respect to the master objective is also enforced. """ - # config.progress_logger.info("Executing decision rule variable polishing solve.") - model = model_data.master_model - polishing_model = model.clone() - - first_stage_variables = polishing_model.scenarios[0, 0].util.first_stage_variables - decision_rule_vars = polishing_model.scenarios[0, 0].util.decision_rule_vars + # clone master problem + master_model = model_data.master_model + polishing_model = master_model.clone() + nominal_polishing_block = polishing_model.scenarios[0, 0] + + # fix first-stage variables (including epigraph, where applicable) + decision_rule_var_set = ComponentSet( + var + for indexed_dr_var in nominal_polishing_block.util.decision_rule_vars + for var in indexed_dr_var.values() + ) + first_stage_vars = nominal_polishing_block.util.first_stage_variables + for var in first_stage_vars: + if var not in decision_rule_var_set: + var.fix() + + # ensure master optimality constraint enforced + if config.objective_focus == ObjectiveType.worst_case: + polishing_model.zeta.fix() + else: + optimal_master_obj_value = value(polishing_model.obj) + polishing_model.nominal_optimality_con = Constraint( + expr=( + nominal_polishing_block.first_stage_objective + + nominal_polishing_block.second_stage_objective + <= optimal_master_obj_value + ) + ) + # deactivate master problem objective polishing_model.obj.deactivate() - index_set = decision_rule_vars[0].index_set() - polishing_model.tau_vars = [] - # ========== - for idx in range(len(decision_rule_vars)): - polishing_model.scenarios[0, 0].add_component( - "polishing_var_" + str(idx), - Var(index_set, initialize=1e6, domain=NonNegativeReals), - ) - polishing_model.tau_vars.append( - getattr(polishing_model.scenarios[0, 0], "polishing_var_" + str(idx)) + + decision_rule_vars = nominal_polishing_block.util.decision_rule_vars + nominal_polishing_block.util.polishing_vars = polishing_vars = [] + for idx, indexed_dr_var in enumerate(decision_rule_vars): + # declare auxiliary 'polishing' variables. + # these are meant to represent the absolute values + # of the terms of DR polynomial + indexed_polishing_var = Var( + list(indexed_dr_var.keys()), domain=NonNegativeReals ) - # ========== - this_iter = polishing_model.scenarios[max(polishing_model.scenarios.keys())[0], 0] - nom_block = polishing_model.scenarios[0, 0] - if config.objective_focus == ObjectiveType.nominal: - obj_val = value( - this_iter.second_stage_objective + this_iter.first_stage_objective + nominal_polishing_block.add_component( + unique_component_name(nominal_polishing_block, f"dr_polishing_var_{idx}"), + indexed_polishing_var, ) - polishing_model.scenarios[0, 0].polishing_constraint = Constraint( - expr=obj_val - >= nom_block.second_stage_objective + nom_block.first_stage_objective + polishing_vars.append(indexed_polishing_var) + + dr_eq_var_zip = zip( + nominal_polishing_block.util.decision_rule_eqns, + polishing_vars, + nominal_polishing_block.util.second_stage_variables, + ) + nominal_polishing_block.util.polishing_abs_val_lb_cons = all_lb_cons = [] + nominal_polishing_block.util.polishing_abs_val_ub_cons = all_ub_cons = [] + for idx, (dr_eq, indexed_polishing_var, ss_var) in enumerate(dr_eq_var_zip): + # set up absolute value constraint components + polishing_absolute_value_lb_cons = Constraint(indexed_polishing_var.index_set()) + polishing_absolute_value_ub_cons = Constraint(indexed_polishing_var.index_set()) + + # add constraints to polishing model + nominal_polishing_block.add_component( + unique_component_name(polishing_model, f"polishing_abs_val_lb_con_{idx}"), + polishing_absolute_value_lb_cons, ) - elif config.objective_focus == ObjectiveType.worst_case: - polishing_model.zeta.fix() # Searching equivalent optimal solutions given optimal zeta - - # === Make absolute value constraints on polishing_vars - polishing_model.scenarios[ - 0, 0 - ].util.absolute_var_constraints = cons = ConstraintList() - uncertain_params = nom_block.util.uncertain_params - if config.decision_rule_order == 1: - for i, tau in enumerate(polishing_model.tau_vars): - for j in range(len(this_iter.util.decision_rule_vars[i])): - if j == 0: - cons.add(-tau[j] <= this_iter.util.decision_rule_vars[i][j]) - cons.add(this_iter.util.decision_rule_vars[i][j] <= tau[j]) - else: - cons.add( - -tau[j] - <= this_iter.util.decision_rule_vars[i][j] - * uncertain_params[j - 1] - ) - cons.add( - this_iter.util.decision_rule_vars[i][j] - * uncertain_params[j - 1] - <= tau[j] - ) - elif config.decision_rule_order == 2: - l = list(range(len(uncertain_params))) - index_pairs = list(it.combinations(l, 2)) - for i, tau in enumerate(polishing_model.tau_vars): - Z = this_iter.util.decision_rule_vars[i] - indices = list(k for k in range(len(Z))) - for r in indices: - if r == 0: - cons.add(-tau[r] <= Z[r]) - cons.add(Z[r] <= tau[r]) - elif r <= len(uncertain_params) and r > 0: - cons.add(-tau[r] <= Z[r] * uncertain_params[r - 1]) - cons.add(Z[r] * uncertain_params[r - 1] <= tau[r]) - elif r <= len(indices) - len(uncertain_params) - 1 and r > len( - uncertain_params - ): - cons.add( - -tau[r] - <= Z[r] - * uncertain_params[ - index_pairs[r - len(uncertain_params) - 1][0] - ] - * uncertain_params[ - index_pairs[r - len(uncertain_params) - 1][1] - ] - ) - cons.add( - Z[r] - * uncertain_params[ - index_pairs[r - len(uncertain_params) - 1][0] - ] - * uncertain_params[ - index_pairs[r - len(uncertain_params) - 1][1] - ] - <= tau[r] - ) - elif r > len(indices) - len(uncertain_params) - 1: - cons.add( - -tau[r] - <= Z[r] - * uncertain_params[ - r - len(index_pairs) - len(uncertain_params) - 1 - ] - ** 2 - ) - cons.add( - Z[r] - * uncertain_params[ - r - len(index_pairs) - len(uncertain_params) - 1 - ] - ** 2 - <= tau[r] - ) - else: - raise NotImplementedError( - "Decision rule variable polishing has not been generalized to decision_rule_order " - + str(config.decision_rule_order) - + "." + nominal_polishing_block.add_component( + unique_component_name(polishing_model, f"polishing_abs_val_ub_con_{idx}"), + polishing_absolute_value_ub_cons, ) - polishing_model.scenarios[0, 0].polishing_obj = Objective( - expr=sum( - sum(tau[j] for j in tau.index_set()) for tau in polishing_model.tau_vars - ) + # update list of absolute value cons + all_lb_cons.append(polishing_absolute_value_lb_cons) + all_ub_cons.append(polishing_absolute_value_ub_cons) + + # get monomials; ensure second-stage variable term excluded + # + # the dr_eq is a linear sum where the first term is the + # second-stage variable: the remainder of the terms will be + # either MonomialTermExpressions or bare VarData + dr_expr_terms = dr_eq.body.args[:-1] + + for dr_eq_term in dr_expr_terms: + if dr_eq_term.is_expression_type(): + dr_var_in_term = dr_eq_term.args[-1] + else: + dr_var_in_term = dr_eq_term + dr_var_in_term_idx = dr_var_in_term.index() + + # get corresponding polishing variable + polishing_var = indexed_polishing_var[dr_var_in_term_idx] + + # add polishing constraints + polishing_absolute_value_lb_cons[dr_var_in_term_idx] = ( + -polishing_var - dr_eq_term <= 0 + ) + polishing_absolute_value_ub_cons[dr_var_in_term_idx] = ( + dr_eq_term - polishing_var <= 0 + ) + + # if DR var is fixed, then fix corresponding polishing + # variable, and deactivate the absolute value constraints + if dr_var_in_term.fixed: + polishing_var.fix() + polishing_absolute_value_lb_cons[dr_var_in_term_idx].deactivate() + polishing_absolute_value_ub_cons[dr_var_in_term_idx].deactivate() + + # initialize polishing variable to absolute value of + # the DR term. polishing constraints should now be + # satisfied (to equality) at the initial point + polishing_var.set_value(abs(value(dr_eq_term))) + + # polishing problem objective is taken to be 1-norm + # of DR monomials, or equivalently, sum of the polishing + # variables. + polishing_model.polishing_obj = Objective( + expr=sum(sum(polishing_var.values()) for polishing_var in polishing_vars) ) - # === Fix design - for d in first_stage_variables: - d.fix() - - # === Unfix DR vars - num_dr_vars = len( - model.scenarios[0, 0].util.decision_rule_vars[0] - ) # there is at least one dr var - num_uncertain_params = len(config.uncertain_params) - - if model.const_efficiency_applied: - for d in decision_rule_vars: - for i in range(1, num_dr_vars): - d[i].fix(0) - d[0].unfix() - elif model.linear_efficiency_applied: - for d in decision_rule_vars: - d.unfix() - for i in range(num_uncertain_params + 1, num_dr_vars): - d[i].fix(0) - else: - for d in decision_rule_vars: - d.unfix() - - # === Unfix all control var values - for block in polishing_model.scenarios.values(): - for c in block.util.second_stage_variables: - c.unfix() - if model.const_efficiency_applied: - for d in block.util.decision_rule_vars: - for i in range(1, num_dr_vars): - d[i].fix(0) - d[0].unfix() - elif model.linear_efficiency_applied: - for d in block.util.decision_rule_vars: - d.unfix() - for i in range(num_uncertain_params + 1, num_dr_vars): - d[i].fix(0) - else: - for d in block.util.decision_rule_vars: - d.unfix() + return polishing_model + + +def minimize_dr_vars(model_data, config): + """ + Polish decision rule of most recent master problem solution. + + Parameters + ---------- + model_data : MasterProblemData + Master problem data. + config : ConfigDict + PyROS solver settings. + + Returns + ------- + results : SolverResults + Subordinate solver results for the polishing problem. + polishing_successful : bool + True if polishing model was solved to acceptable level, + False otherwise. + """ + # create polishing NLP + polishing_model = construct_dr_polishing_problem( + model_data=model_data, config=config + ) if config.solve_master_globally: solver = config.global_solver else: solver = config.local_solver + config.progress_logger.debug("Solving DR polishing problem") + + # polishing objective should be consistent with value of sum + # of absolute values of polynomial DR terms provided + # auxiliary variables initialized correctly + polishing_obj = polishing_model.polishing_obj + config.progress_logger.debug(f" Initial DR norm: {value(polishing_obj)}") + # === Solve the polishing model - timer = TicTocTimer() - orig_setting, custom_setting_present = adjust_solver_time_settings( - model_data.timing, solver, config - ) - timer.tic(msg=None) - try: - results = solver.solve(polishing_model, tee=config.tee, load_solutions=False) - except ApplicationError: - config.progress_logger.error( + results = call_solver( + model=polishing_model, + solver=solver, + config=config, + timing_obj=model_data.timing, + timer_name="main.dr_polishing", + err_msg=( f"Optimizer {repr(solver)} encountered an exception " "attempting to solve decision rule polishing problem " f"in iteration {model_data.iteration}" - ) - raise - else: - setattr(results.solver, TIC_TOC_SOLVE_TIME_ATTR, timer.toc(msg=None)) - finally: - revert_solver_max_time_adjustment( - solver, orig_setting, custom_setting_present, config - ) + ), + ) + + # interested in the time and termination status for debugging + # purposes + config.progress_logger.debug(" Done solving DR polishing problem") + config.progress_logger.debug( + f" Termination condition: {results.solver.termination_condition} " + ) + config.progress_logger.debug( + f" Solve time: {getattr(results.solver, TIC_TOC_SOLVE_TIME_ATTR)} s" + ) # === Process solution by termination condition - acceptable = {tc.globallyOptimal, tc.optimal, tc.locallyOptimal, tc.feasible} + acceptable = {tc.globallyOptimal, tc.optimal, tc.locallyOptimal} if results.solver.termination_condition not in acceptable: # continue with "unpolished" master model solution - return results + config.progress_logger.warning( + "Could not successfully solve DR polishing problem " + f"of iteration {model_data.iteration} with primary subordinate " + f"{'global' if config.solve_master_globally else 'local'} solver " + "to acceptable level. " + f"Termination stats:\n{results.solver}\n" + "Maintaining unpolished master problem solution." + ) + return results, False # update master model second-stage, state, and decision rule # variables to polishing model solution polishing_model.solutions.load_from(results) + for idx, blk in model_data.master_model.scenarios.items(): ssv_zip = zip( blk.util.second_stage_variables, @@ -520,7 +533,34 @@ def minimize_dr_vars(model_data, config): for mvar, pvar in zip(master_dr.values(), polish_dr.values()): mvar.set_value(value(pvar), skip_validation=True) - return results + config.progress_logger.debug(f" Optimized DR norm: {value(polishing_obj)}") + config.progress_logger.debug(" Polished master objective:") + + # print breakdown of objective value of polished master solution + if config.objective_focus == ObjectiveType.worst_case: + eval_obj_blk_idx = max( + model_data.master_model.scenarios.keys(), + key=lambda idx: value( + model_data.master_model.scenarios[idx].second_stage_objective + ), + ) + else: + eval_obj_blk_idx = (0, 0) + + # debugging: summarize objective breakdown + eval_obj_blk = model_data.master_model.scenarios[eval_obj_blk_idx] + config.progress_logger.debug( + " First-stage objective: " f"{value(eval_obj_blk.first_stage_objective)}" + ) + config.progress_logger.debug( + " Second-stage objective: " f"{value(eval_obj_blk.second_stage_objective)}" + ) + polished_master_obj = value( + eval_obj_blk.first_stage_objective + eval_obj_blk.second_stage_objective + ) + config.progress_logger.debug(f" Objective: {polished_master_obj}") + + return results, True def add_p_robust_constraint(model_data, config): @@ -566,37 +606,64 @@ def add_scenario_to_master(model_data, violations): return -def higher_order_decision_rule_efficiency(config, model_data): - # === Efficiencies for decision rules - # if iteration <= |q| then all d^n where n > 1 are fixed to 0 - # if iteration == 0, all d^n, n > 0 are fixed to 0 - # These efficiencies should be carried through as d* to polishing - nlp_model = model_data.master_model - if config.decision_rule_order != None and len(config.second_stage_variables) > 0: - # Ensure all are unfixed unless next conditions are met... - for dr_var in nlp_model.scenarios[0, 0].util.decision_rule_vars: - dr_var.unfix() - num_dr_vars = len( - nlp_model.scenarios[0, 0].util.decision_rule_vars[0] - ) # there is at least one dr var - num_uncertain_params = len(config.uncertain_params) - nlp_model.const_efficiency_applied = False - nlp_model.linear_efficiency_applied = False - if model_data.iteration == 0: - nlp_model.const_efficiency_applied = True - for dr_var in nlp_model.scenarios[0, 0].util.decision_rule_vars: - for i in range(1, num_dr_vars): - dr_var[i].fix(0) - elif ( - model_data.iteration <= num_uncertain_params - and config.decision_rule_order > 1 - ): - # Only applied in DR order > 1 case - for dr_var in nlp_model.scenarios[0, 0].util.decision_rule_vars: - for i in range(num_uncertain_params + 1, num_dr_vars): - nlp_model.linear_efficiency_applied = True - dr_var[i].fix(0) - return +def get_master_dr_degree(model_data, config): + """ + Determine DR polynomial degree to enforce based on + the iteration number. + + Currently, the degree is set to: + + - 0 if iteration number is 0 + - min(1, config.decision_rule_order) if iteration number + otherwise does not exceed number of uncertain parameters + - min(2, config.decision_rule_order) otherwise. + + Parameters + ---------- + model_data : MasterProblemData + Master problem data. + config : ConfigDict + PyROS solver options. + + Returns + ------- + int + DR order, or polynomial degree, to enforce. + """ + if model_data.iteration == 0: + return 0 + elif model_data.iteration <= len(config.uncertain_params): + return min(1, config.decision_rule_order) + else: + return min(2, config.decision_rule_order) + + +def higher_order_decision_rule_efficiency(model_data, config): + """ + Enforce DR coefficient variable efficiencies for + master problem-like formulation. + + Parameters + ---------- + model_data : MasterProblemData + Master problem data. + config : ConfigDict + PyROS solver options. + + Note + ---- + The DR coefficient variable efficiencies consist of + setting the degree of the DR polynomial expressions + by fixing the appropriate variables to 0. The degree + to be set depends on the iteration number; + see ``get_master_dr_degree``. + """ + order_to_enforce = get_master_dr_degree(model_data, config) + enforce_dr_degree( + blk=model_data.master_model.scenarios[0, 0], + config=config, + degree=order_to_enforce, + ) def solver_call_master(model_data, config, solver, solve_data): @@ -628,41 +695,34 @@ def solver_call_master(model_data, config, solver, solve_data): solver_term_cond_dict = {} if config.solve_master_globally: - backup_solvers = deepcopy(config.backup_global_solvers) + solvers = [solver] + config.backup_global_solvers else: - backup_solvers = deepcopy(config.backup_local_solvers) - backup_solvers.insert(0, solver) + solvers = [solver] + config.backup_local_solvers - higher_order_decision_rule_efficiency(config, model_data) + higher_order_decision_rule_efficiency(model_data=model_data, config=config) - timer = TicTocTimer() - for opt in backup_solvers: - orig_setting, custom_setting_present = adjust_solver_time_settings( - model_data.timing, opt, config - ) - timer.tic(msg=None) - try: - results = opt.solve( - nlp_model, - tee=config.tee, - load_solutions=False, - symbolic_solver_labels=True, - ) - except ApplicationError: - # account for possible external subsolver errors - # (such as segmentation faults, function evaluation - # errors, etc.) - config.progress_logger.error( - f"Solver {repr(opt)} encountered exception attempting to " - f"optimize master problem in iteration {model_data.iteration}" - ) - raise - else: - setattr(results.solver, TIC_TOC_SOLVE_TIME_ATTR, timer.toc(msg=None)) - finally: - revert_solver_max_time_adjustment( - solver, orig_setting, custom_setting_present, config + solve_mode = "global" if config.solve_master_globally else "local" + config.progress_logger.debug("Solving master problem") + + for idx, opt in enumerate(solvers): + if idx > 0: + config.progress_logger.warning( + f"Invoking backup solver {opt!r} " + f"(solver {idx + 1} of {len(solvers)}) for " + f"master problem of iteration {model_data.iteration}." ) + results = call_solver( + model=nlp_model, + solver=opt, + config=config, + timing_obj=model_data.timing, + timer_name="main.master", + err_msg=( + f"Optimizer {repr(opt)} ({idx + 1} of {len(solvers)}) " + "encountered exception attempting to " + f"solve master problem in iteration {model_data.iteration}" + ), + ) optimal_termination = check_optimal_termination(results) infeasible = results.solver.termination_condition == tc.infeasible @@ -677,12 +737,9 @@ def solver_call_master(model_data, config, solver, solve_data): solver_term_cond_dict[str(opt)] = str(results.solver.termination_condition) master_soln.termination_condition = results.solver.termination_condition master_soln.pyros_termination_condition = None - ( - try_backup, - _, - ) = ( - master_soln.master_subsolver_results - ) = process_termination_condition_master_problem(config=config, results=results) + (try_backup, _) = master_soln.master_subsolver_results = ( + process_termination_condition_master_problem(config=config, results=results) + ) master_soln.nominal_block = nlp_model.scenarios[0, 0] master_soln.results = results @@ -715,6 +772,36 @@ def solver_call_master(model_data, config, solver, solve_data): nlp_model.scenarios[0, 0].first_stage_objective ) + # debugging: log breakdown of master objective + if config.objective_focus == ObjectiveType.worst_case: + eval_obj_blk_idx = max( + nlp_model.scenarios.keys(), + key=lambda idx: value( + nlp_model.scenarios[idx].second_stage_objective + ), + ) + else: + eval_obj_blk_idx = (0, 0) + + eval_obj_blk = nlp_model.scenarios[eval_obj_blk_idx] + config.progress_logger.debug(" Optimized master objective breakdown:") + config.progress_logger.debug( + f" First-stage objective: {value(eval_obj_blk.first_stage_objective)}" + ) + config.progress_logger.debug( + f" Second-stage objective: {value(eval_obj_blk.second_stage_objective)}" + ) + master_obj = ( + eval_obj_blk.first_stage_objective + eval_obj_blk.second_stage_objective + ) + config.progress_logger.debug(f" Objective: {value(master_obj)}") + config.progress_logger.debug( + f" Termination condition: {results.solver.termination_condition}" + ) + config.progress_logger.debug( + f" Solve time: {getattr(results.solver, TIC_TOC_SOLVE_TIME_ATTR)}s" + ) + master_soln.nominal_block = nlp_model.scenarios[0, 0] master_soln.results = results master_soln.master_model = nlp_model @@ -731,7 +818,6 @@ def solver_call_master(model_data, config, solver, solve_data): master_soln.pyros_termination_condition = ( pyrosTerminationCondition.time_out ) - output_logger(config=config, time_out=True, elapsed=elapsed) if not try_backup: return master_soln @@ -742,8 +828,9 @@ def solver_call_master(model_data, config, solver, solve_data): # NOTE: subproblem is written with variables set to their # initial values (not the final subsolver iterate) save_dir = config.subproblem_file_directory + serialization_msg = "" if save_dir and config.keepfiles: - name = os.path.join( + output_problem_path = os.path.join( save_dir, ( config.uncertainty_set.type @@ -754,15 +841,37 @@ def solver_call_master(model_data, config, solver, solve_data): + ".bar" ), ) - nlp_model.write(name, io_options={'symbolic_solver_labels': True}) - output_logger( - config=config, - master_error=True, - status_dict=solver_term_cond_dict, - filename=name, - iteration=model_data.iteration, + nlp_model.write( + output_problem_path, io_options={'symbolic_solver_labels': True} ) + serialization_msg = ( + " For debugging, problem has been serialized to the file " + f"{output_problem_path!r}." + ) + + deterministic_model_qual = ( + " (i.e., the deterministic model)" if model_data.iteration == 0 else "" + ) + deterministic_msg = ( + ( + " Please ensure your deterministic model " + f"is solvable by at least one of the subordinate {solve_mode} " + "optimizers provided." + ) + if model_data.iteration == 0 + else "" + ) master_soln.pyros_termination_condition = pyrosTerminationCondition.subsolver_error + config.progress_logger.warning( + f"Could not successfully solve master problem of iteration " + f"{model_data.iteration}{deterministic_model_qual} with any of the " + f"provided subordinate {solve_mode} optimizers. " + f"(Termination statuses: " + f"{[term_cond for term_cond in solver_term_cond_dict.values()]}.)" + f"{deterministic_msg}" + f"{serialization_msg}" + ) + return master_soln @@ -798,10 +907,6 @@ def solve_master(model_data, config): None, pyrosTerminationCondition.time_out, ) - - # log time out message - output_logger(config=config, time_out=True, elapsed=elapsed) - return master_soln solver = ( diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 34db54b64e6..582233c4a56 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -11,622 +11,72 @@ # pyros.py: Generalized Robust Cutting-Set Algorithm for Pyomo import logging -from textwrap import indent, dedent, wrap -from pyomo.common.collections import Bunch, ComponentSet -from pyomo.common.config import ConfigDict, ConfigValue, In, NonNegativeFloat +from pyomo.common.config import document_kwargs_from_configdict from pyomo.core.base.block import Block from pyomo.core.expr import value -from pyomo.core.base.var import Var, _VarData -from pyomo.core.base.param import Param, _ParamData -from pyomo.core.base.objective import Objective, maximize -from pyomo.contrib.pyros.util import a_logger, time_code, get_main_elapsed_time +from pyomo.core.base.var import Var +from pyomo.core.base.objective import Objective +from pyomo.contrib.pyros.util import time_code from pyomo.common.modeling import unique_component_name from pyomo.opt import SolverFactory +from pyomo.contrib.pyros.config import pyros_config, logger_domain from pyomo.contrib.pyros.util import ( - model_is_valid, recast_to_min_obj, add_decision_rule_constraints, add_decision_rule_variables, load_final_solution, pyrosTerminationCondition, - ValidEnum, ObjectiveType, - validate_uncertainty_set, identify_objective_functions, - validate_kwarg_inputs, + validate_pyros_inputs, transform_to_standard_form, turn_bounds_to_constraints, replace_uncertain_bounds_with_constraints, - output_logger, + IterationLogRecord, + setup_pyros_logger, + TimingData, ) from pyomo.contrib.pyros.solve_data import ROSolveResults from pyomo.contrib.pyros.pyros_algorithm_methods import ROSolver_iterative_solve -from pyomo.contrib.pyros.uncertainty_sets import uncertainty_sets from pyomo.core.base import Constraint +from datetime import datetime -__version__ = "1.2.7" - -def NonNegIntOrMinusOne(obj): - ''' - if obj is a non-negative int, return the non-negative int - if obj is -1, return -1 - else, error - ''' - ans = int(obj) - if ans != float(obj) or (ans < 0 and ans != -1): - raise ValueError("Expected non-negative int, but received %s" % (obj,)) - return ans - - -def PositiveIntOrMinusOne(obj): - ''' - if obj is a positive int, return the int - if obj is -1, return -1 - else, error - ''' - ans = int(obj) - if ans != float(obj) or (ans <= 0 and ans != -1): - raise ValueError("Expected positive int, but received %s" % (obj,)) - return ans - - -class SolverResolvable(object): - def __call__(self, obj): - ''' - if obj is a string, return the Solver object for that solver name - if obj is a Solver object, return a copy of the Solver - if obj is a list, and each element of list is solver resolvable, return list of solvers - ''' - if isinstance(obj, str): - return SolverFactory(obj.lower()) - elif callable(getattr(obj, "solve", None)): - return obj - elif isinstance(obj, list): - return [self(o) for o in obj] - else: - raise ValueError( - "Expected a Pyomo solver or string object, " - "instead received {1}".format(obj.__class__.__name__) - ) +__version__ = "1.2.11" -class InputDataStandardizer(object): - def __init__(self, ctype, cdatatype): - self.ctype = ctype - self.cdatatype = cdatatype +default_pyros_solver_logger = setup_pyros_logger() - def __call__(self, obj): - if isinstance(obj, self.ctype): - return list(obj.values()) - if isinstance(obj, self.cdatatype): - return [obj] - ans = [] - for item in obj: - ans.extend(self.__call__(item)) - for _ in ans: - assert isinstance(_, self.cdatatype) - return ans - -class PyROSConfigValue(ConfigValue): +def _get_pyomo_version_info(): """ - Subclass of ``common.collections.ConfigValue``, - with a few attributes added to facilitate documentation - of the PyROS solver. - An instance of this class is used for storing and - documenting an argument to the PyROS solver. - - Attributes - ---------- - is_optional : bool - Argument is optional. - document_default : bool, optional - Document the default value of the argument - in any docstring generated from this instance, - or a `ConfigDict` object containing this instance. - dtype_spec_str : None or str, optional - String documenting valid types for this argument. - If `None` is provided, then this string is automatically - determined based on the `domain` argument to the - constructor. - - NOTES - ----- - Cleaner way to access protected attributes - (particularly _doc, _description) inherited from ConfigValue? - + Get Pyomo version information. """ - - def __init__( - self, - default=None, - domain=None, - description=None, - doc=None, - visibility=0, - is_optional=True, - document_default=True, - dtype_spec_str=None, - ): - """Initialize self (see class docstring).""" - - # initialize base class attributes - super(self.__class__, self).__init__( - default=default, - domain=domain, - description=description, - doc=doc, - visibility=visibility, + import os + import subprocess + from pyomo.version import version + + pyomo_version = version + commit_hash = "unknown" + + pyros_dir = os.path.join(*os.path.split(__file__)[:-1]) + commit_hash_command_args = [ + "git", + "-C", + f"{pyros_dir}", + "rev-parse", + "--short", + "HEAD", + ] + try: + commit_hash = ( + subprocess.check_output(commit_hash_command_args).decode("ascii").strip() ) + except subprocess.CalledProcessError: + commit_hash = "unknown" - self.is_optional = is_optional - self.document_default = document_default - - if dtype_spec_str is None: - self.dtype_spec_str = self.domain_name() - # except AttributeError: - # self.dtype_spec_str = repr(self._domain) - else: - self.dtype_spec_str = dtype_spec_str - - -def pyros_config(): - CONFIG = ConfigDict('PyROS') - - # ================================================ - # === Options common to all solvers - # ================================================ - CONFIG.declare( - 'time_limit', - PyROSConfigValue( - default=None, - domain=NonNegativeFloat, - doc=( - """ - Wall time limit for the execution of the PyROS solver - in seconds (including time spent by subsolvers). - If `None` is provided, then no time limit is enforced. - """ - ), - is_optional=True, - document_default=False, - dtype_spec_str="None or NonNegativeFloat", - ), - ) - CONFIG.declare( - 'keepfiles', - PyROSConfigValue( - default=False, - domain=bool, - description=( - """ - Export subproblems with a non-acceptable termination status - for debugging purposes. - If True is provided, then the argument `subproblem_file_directory` - must also be specified. - """ - ), - is_optional=True, - document_default=True, - dtype_spec_str=None, - ), - ) - CONFIG.declare( - 'tee', - PyROSConfigValue( - default=False, - domain=bool, - description="Output subordinate solver logs for all subproblems.", - is_optional=True, - document_default=True, - dtype_spec_str=None, - ), - ) - CONFIG.declare( - 'load_solution', - PyROSConfigValue( - default=True, - domain=bool, - description=( - """ - Load final solution(s) found by PyROS to the deterministic model - provided. - """ - ), - is_optional=True, - document_default=True, - dtype_spec_str=None, - ), - ) - - # ================================================ - # === Required User Inputs - # ================================================ - CONFIG.declare( - "first_stage_variables", - PyROSConfigValue( - default=[], - domain=InputDataStandardizer(Var, _VarData), - description="First-stage (or design) variables.", - is_optional=False, - dtype_spec_str="list of Var", - ), - ) - CONFIG.declare( - "second_stage_variables", - PyROSConfigValue( - default=[], - domain=InputDataStandardizer(Var, _VarData), - description="Second-stage (or control) variables.", - is_optional=False, - dtype_spec_str="list of Var", - ), - ) - CONFIG.declare( - "uncertain_params", - PyROSConfigValue( - default=[], - domain=InputDataStandardizer(Param, _ParamData), - description=( - """ - Uncertain model parameters. - The `mutable` attribute for all uncertain parameter - objects should be set to True. - """ - ), - is_optional=False, - dtype_spec_str="list of Param", - ), - ) - CONFIG.declare( - "uncertainty_set", - PyROSConfigValue( - default=None, - domain=uncertainty_sets, - description=( - """ - Uncertainty set against which the - final solution(s) returned by PyROS should be certified - to be robust. - """ - ), - is_optional=False, - dtype_spec_str="UncertaintySet", - ), - ) - CONFIG.declare( - "local_solver", - PyROSConfigValue( - default=None, - domain=SolverResolvable(), - description="Subordinate local NLP solver.", - is_optional=False, - dtype_spec_str="Solver", - ), - ) - CONFIG.declare( - "global_solver", - PyROSConfigValue( - default=None, - domain=SolverResolvable(), - description="Subordinate global NLP solver.", - is_optional=False, - dtype_spec_str="Solver", - ), - ) - # ================================================ - # === Optional User Inputs - # ================================================ - CONFIG.declare( - "objective_focus", - PyROSConfigValue( - default=ObjectiveType.nominal, - domain=ValidEnum(ObjectiveType), - description=( - """ - Choice of objective focus to optimize in the master problems. - Choices are: `ObjectiveType.worst_case`, - `ObjectiveType.nominal`. - """ - ), - doc=( - """ - Objective focus for the master problems: - - - `ObjectiveType.nominal`: - Optimize the objective function subject to the nominal - uncertain parameter realization. - - `ObjectiveType.worst_case`: - Optimize the objective function subject to the worst-case - uncertain parameter realization. - - By default, `ObjectiveType.nominal` is chosen. - - A worst-case objective focus is required for certification - of robust optimality of the final solution(s) returned - by PyROS. - If a nominal objective focus is chosen, then only robust - feasibility is guaranteed. - """ - ), - is_optional=True, - document_default=False, - dtype_spec_str="ObjectiveType", - ), - ) - CONFIG.declare( - "nominal_uncertain_param_vals", - PyROSConfigValue( - default=[], - domain=list, - doc=( - """ - Nominal uncertain parameter realization. - Entries should be provided in an order consistent with the - entries of the argument `uncertain_params`. - If an empty list is provided, then the values of the `Param` - objects specified through `uncertain_params` are chosen. - """ - ), - is_optional=True, - document_default=True, - dtype_spec_str="list of float", - ), - ) - CONFIG.declare( - "decision_rule_order", - PyROSConfigValue( - default=0, - domain=In([0, 1, 2]), - description=( - """ - Order (or degree) of the polynomial decision rule functions used - for approximating the adjustability of the second stage - variables with respect to the uncertain parameters. - """ - ), - doc=( - """ - Order (or degree) of the polynomial decision rule functions used - for approximating the adjustability of the second stage - variables with respect to the uncertain parameters. - - Choices are: - - - 0: static recourse - - 1: affine recourse - - 2: quadratic recourse - """ - ), - is_optional=True, - document_default=True, - dtype_spec_str=None, - ), - ) - CONFIG.declare( - "solve_master_globally", - PyROSConfigValue( - default=False, - domain=bool, - doc=( - """ - True to solve all master problems with the subordinate - global solver, False to solve all master problems with - the subordinate local solver. - Along with a worst-case objective focus - (see argument `objective_focus`), - solving the master problems to global optimality is required - for certification - of robust optimality of the final solution(s) returned - by PyROS. Otherwise, only robust feasibility is guaranteed. - """ - ), - is_optional=True, - document_default=True, - dtype_spec_str=None, - ), - ) - CONFIG.declare( - "max_iter", - PyROSConfigValue( - default=-1, - domain=PositiveIntOrMinusOne, - description=( - """ - Iteration limit. If -1 is provided, then no iteration - limit is enforced. - """ - ), - is_optional=True, - document_default=True, - dtype_spec_str="int", - ), - ) - CONFIG.declare( - "robust_feasibility_tolerance", - PyROSConfigValue( - default=1e-4, - domain=NonNegativeFloat, - description=( - """ - Relative tolerance for assessing maximal inequality - constraint violations during the GRCS separation step. - """ - ), - is_optional=True, - document_default=True, - dtype_spec_str=None, - ), - ) - CONFIG.declare( - "separation_priority_order", - PyROSConfigValue( - default={}, - domain=dict, - doc=( - """ - Mapping from model inequality constraint names - to positive integers specifying the priorities - of their corresponding separation subproblems. - A higher integer value indicates a higher priority. - Constraints not referenced in the `dict` assume - a priority of 0. - Separation subproblems are solved in order of decreasing - priority. - """ - ), - is_optional=True, - document_default=True, - dtype_spec_str=None, - ), - ) - CONFIG.declare( - "progress_logger", - PyROSConfigValue( - default="pyomo.contrib.pyros", - domain=a_logger, - doc=( - """ - Logger (or name thereof) used for reporting PyROS solver - progress. If a `str` is specified, then - ``logging.getLogger(progress_logger)`` is used. - """ - ), - is_optional=True, - document_default=True, - dtype_spec_str="str or logging.Logger", - ), - ) - CONFIG.declare( - "backup_local_solvers", - PyROSConfigValue( - default=[], - domain=SolverResolvable(), - doc=( - """ - Additional subordinate local NLP optimizers to invoke - in the event the primary local NLP optimizer fails - to solve a subproblem to an acceptable termination condition. - """ - ), - is_optional=True, - document_default=True, - dtype_spec_str="list of Solver", - ), - ) - CONFIG.declare( - "backup_global_solvers", - PyROSConfigValue( - default=[], - domain=SolverResolvable(), - doc=( - """ - Additional subordinate global NLP optimizers to invoke - in the event the primary global NLP optimizer fails - to solve a subproblem to an acceptable termination condition. - """ - ), - is_optional=True, - document_default=True, - dtype_spec_str="list of Solver", - ), - ) - CONFIG.declare( - "subproblem_file_directory", - PyROSConfigValue( - default=None, - domain=str, - description=( - """ - Directory to which to export subproblems not successfully - solved to an acceptable termination condition. - In the event ``keepfiles=True`` is specified, a str or - path-like referring to an existing directory must be - provided. - """ - ), - is_optional=True, - document_default=True, - dtype_spec_str="None, str, or path-like", - ), - ) - - # ================================================ - # === Advanced Options - # ================================================ - CONFIG.declare( - "bypass_local_separation", - PyROSConfigValue( - default=False, - domain=bool, - description=( - """ - This is an advanced option. - Solve all separation subproblems with the subordinate global - solver(s) only. - This option is useful for expediting PyROS - in the event that the subordinate global optimizer(s) provided - can quickly solve separation subproblems to global optimality. - """ - ), - is_optional=True, - document_default=True, - dtype_spec_str=None, - ), - ) - CONFIG.declare( - "bypass_global_separation", - PyROSConfigValue( - default=False, - domain=bool, - doc=( - """ - This is an advanced option. - Solve all separation subproblems with the subordinate local - solver(s) only. - If `True` is chosen, then robustness of the final solution(s) - returned by PyROS is not guaranteed, and a warning will - be issued at termination. - This option is useful for expediting PyROS - in the event that the subordinate global optimizer provided - cannot tractably solve separation subproblems to global - optimality. - """ - ), - is_optional=True, - document_default=True, - dtype_spec_str=None, - ), - ) - CONFIG.declare( - "p_robustness", - PyROSConfigValue( - default={}, - domain=dict, - doc=( - """ - This is an advanced option. - Add p-robustness constraints to all master subproblems. - If an empty dict is provided, then p-robustness constraints - are not added. - Otherwise, the dict must map a `str` of value ``'rho'`` - to a non-negative `float`. PyROS automatically - specifies ``1 + p_robustness['rho']`` - as an upper bound for the ratio of the - objective function value under any PyROS-sampled uncertain - parameter realization to the objective function under - the nominal parameter realization. - """ - ), - is_optional=True, - document_default=True, - dtype_spec_str=None, - ), - ) - - return CONFIG + return {"Pyomo version": pyomo_version, "Commit hash": commit_hash} @SolverFactory.register( @@ -642,6 +92,7 @@ class PyROS(object): ''' CONFIG = pyros_config() + _LOG_LINE_LENGTH = 78 def available(self, exception_flag=True): """Check if solver is available.""" @@ -663,6 +114,178 @@ def __enter__(self): def __exit__(self, et, ev, tb): pass + def _log_intro(self, logger, **log_kwargs): + """ + Log PyROS solver introductory messages. + + Parameters + ---------- + logger : logging.Logger + Logger through which to emit messages. + **log_kwargs : dict, optional + Keyword arguments to ``logger.log()`` callable. + Should not include `msg`. + """ + logger.log(msg="=" * self._LOG_LINE_LENGTH, **log_kwargs) + logger.log( + msg=f"PyROS: The Pyomo Robust Optimization Solver, v{self.version()}.", + **log_kwargs, + ) + + # git_info_str = ", ".join( + # f"{field}: {val}" for field, val in _get_pyomo_git_info().items() + # ) + version_info = _get_pyomo_version_info() + version_info_str = ' ' * len("PyROS: ") + ("\n" + ' ' * len("PyROS: ")).join( + f"{key}: {val}" for key, val in version_info.items() + ) + logger.log(msg=version_info_str, **log_kwargs) + logger.log( + msg=( + f"{' ' * len('PyROS:')} " + f"Invoked at UTC {datetime.utcnow().isoformat()}" + ), + **log_kwargs, + ) + logger.log(msg="", **log_kwargs) + logger.log( + msg=("Developed by: Natalie M. Isenberg (1), Jason A. F. Sherman (1),"), + **log_kwargs, + ) + logger.log( + msg=( + f"{' ' * len('Developed by:')} " + "John D. Siirola (2), Chrysanthos E. Gounaris (1)" + ), + **log_kwargs, + ) + logger.log( + msg=( + "(1) Carnegie Mellon University, " "Department of Chemical Engineering" + ), + **log_kwargs, + ) + logger.log( + msg="(2) Sandia National Laboratories, Center for Computing Research", + **log_kwargs, + ) + logger.log(msg="", **log_kwargs) + logger.log( + msg=( + "The developers gratefully acknowledge support " + "from the U.S. Department" + ), + **log_kwargs, + ) + logger.log( + msg=( + "of Energy's " + "Institute for the Design of Advanced Energy Systems (IDAES)." + ), + **log_kwargs, + ) + logger.log(msg="=" * self._LOG_LINE_LENGTH, **log_kwargs) + + def _log_disclaimer(self, logger, **log_kwargs): + """ + Log PyROS solver disclaimer messages. + + Parameters + ---------- + logger : logging.Logger + Logger through which to emit messages. + **log_kwargs : dict, optional + Keyword arguments to ``logger.log()`` callable. + Should not include `msg`. + """ + disclaimer_header = " DISCLAIMER ".center(self._LOG_LINE_LENGTH, "=") + + logger.log(msg=disclaimer_header, **log_kwargs) + logger.log(msg="PyROS is still under development. ", **log_kwargs) + logger.log( + msg=( + "Please provide feedback and/or report any issues by creating " + "a ticket at" + ), + **log_kwargs, + ) + logger.log(msg="https://github.com/Pyomo/pyomo/issues/new/choose", **log_kwargs) + logger.log(msg="=" * self._LOG_LINE_LENGTH, **log_kwargs) + + def _log_config(self, logger, config, exclude_options=None, **log_kwargs): + """ + Log PyROS solver options. + + Parameters + ---------- + logger : logging.Logger + Logger for the solver options. + config : ConfigDict + PyROS solver options. + exclude_options : None or iterable of str, optional + Options (keys of the ConfigDict) to exclude from + logging. If `None` passed, then the names of the + required arguments to ``self.solve()`` are skipped. + **log_kwargs : dict, optional + Keyword arguments to each statement of ``logger.log()``. + """ + # log solver options + if exclude_options is None: + exclude_options = [ + "first_stage_variables", + "second_stage_variables", + "uncertain_params", + "uncertainty_set", + "local_solver", + "global_solver", + ] + + logger.log(msg="Solver options:", **log_kwargs) + for key, val in config.items(): + if key not in exclude_options: + logger.log(msg=f" {key}={val!r}", **log_kwargs) + logger.log(msg="-" * self._LOG_LINE_LENGTH, **log_kwargs) + + def _resolve_and_validate_pyros_args(self, model, **kwds): + """ + Resolve and validate arguments to ``self.solve()``. + + Parameters + ---------- + model : ConcreteModel + Deterministic model object passed to ``self.solve()``. + **kwds : dict + All other arguments to ``self.solve()``. + + Returns + ------- + config : ConfigDict + Standardized arguments. + + Note + ---- + This method can be broken down into three steps: + + 1. Cast arguments to ConfigDict. Argument-wise + validation is performed automatically. + Note that arguments specified directly take + precedence over arguments specified indirectly + through direct argument 'options'. + 2. Inter-argument validation. + """ + config = self.CONFIG(kwds.pop("options", {})) + config = config(kwds) + state_vars = validate_pyros_inputs(model, config) + + return config, state_vars + + @document_kwargs_from_configdict( + config=CONFIG, + section="Keyword Arguments", + indent_spacing=4, + width=72, + visibility=0, + ) def solve( self, model, @@ -680,21 +303,25 @@ def solve( ---------- model: ConcreteModel The deterministic model. - first_stage_variables: list of Var + first_stage_variables: VarData, Var, or iterable of VarData/Var First-stage model variables (or design variables). - second_stage_variables: list of Var + second_stage_variables: VarData, Var, or iterable of VarData/Var Second-stage model variables (or control variables). - uncertain_params: list of Param + uncertain_params: ParamData, Param, or iterable of ParamData/Param Uncertain model parameters. - The `mutable` attribute for every uncertain parameter - objects must be set to True. + The `mutable` attribute for all uncertain parameter objects + must be set to True. uncertainty_set: UncertaintySet Uncertainty set against which the solution(s) returned will be confirmed to be robust. - local_solver: Solver + local_solver: str or solver type Subordinate local NLP solver. - global_solver: Solver + If a `str` is passed, then the `str` is cast to + ``SolverFactory(local_solver)``. + global_solver: str or solver type Subordinate global NLP solver. + If a `str` is passed, then the `str` is cast to + ``SolverFactory(global_solver)``. Returns ------- @@ -702,69 +329,63 @@ def solve( Summary of PyROS termination outcome. """ - - # === Add the explicit arguments to the config - config = self.CONFIG(kwds.pop('options', {})) - config.first_stage_variables = first_stage_variables - config.second_stage_variables = second_stage_variables - config.uncertain_params = uncertain_params - config.uncertainty_set = uncertainty_set - config.local_solver = local_solver - config.global_solver = global_solver - - dev_options = kwds.pop('dev_options', {}) - config.set_value(kwds) - config.set_value(dev_options) - - model = model - - # === Validate kwarg inputs - validate_kwarg_inputs(model, config) - - # === Validate ability of grcs RO solver to handle this model - if not model_is_valid(model): - raise AttributeError( - "This model structure is not currently handled by the ROSolver." + model_data = ROSolveResults() + model_data.timing = TimingData() + with time_code( + timing_data_obj=model_data.timing, + code_block_name="main", + is_main_timer=True, + ): + kwds.update( + dict( + first_stage_variables=first_stage_variables, + second_stage_variables=second_stage_variables, + uncertain_params=uncertain_params, + uncertainty_set=uncertainty_set, + local_solver=local_solver, + global_solver=global_solver, + ) ) - # === Define nominal point if not specified - if len(config.nominal_uncertain_param_vals) == 0: - config.nominal_uncertain_param_vals = list( - p.value for p in config.uncertain_params + # we want to log the intro and disclaimer in + # advance of assembling the config. + # this helps clarify to the user that any + # messages logged during assembly of the config + # were, in fact, logged after PyROS was initiated + progress_logger = logger_domain( + kwds.get( + "progress_logger", + kwds.get("options", dict()).get( + "progress_logger", default_pyros_solver_logger + ), + ) ) - elif len(config.nominal_uncertain_param_vals) != len(config.uncertain_params): - raise AttributeError( - "The nominal_uncertain_param_vals list must be the same length" - "as the uncertain_params list" + self._log_intro(logger=progress_logger, level=logging.INFO) + self._log_disclaimer(logger=progress_logger, level=logging.INFO) + + config, state_vars = self._resolve_and_validate_pyros_args(model, **kwds) + self._log_config( + logger=config.progress_logger, + config=config, + exclude_options=None, + level=logging.INFO, ) - # === Create data containers - model_data = ROSolveResults() - model_data.timing = Bunch() - - # === Set up logger for logging results - with time_code(model_data.timing, 'total', is_main_timer=True): - config.progress_logger.setLevel(logging.INFO) - - # === PREAMBLE - output_logger(config=config, preamble=True, version=str(self.version())) - - # === DISCLAIMER - output_logger(config=config, disclaimer=True) + # begin preprocessing + config.progress_logger.info("Preprocessing...") + model_data.timing.start_timer("main.preprocessing") # === A block to hold list-type data to make cloning easy util = Block(concrete=True) util.first_stage_variables = config.first_stage_variables util.second_stage_variables = config.second_stage_variables + util.state_vars = state_vars util.uncertain_params = config.uncertain_params model_data.util_block = unique_component_name(model, 'util') model.add_component(model_data.util_block, util) # Note: model.component(model_data.util_block) is util - # === Validate uncertainty set happens here, requires util block for Cardinality and FactorModel sets - validate_uncertainty_set(config=config) - # === Leads to a logger warning here for inactive obj when cloning model_data.original_model = model # === For keeping track of variables after cloning @@ -783,6 +404,7 @@ def solve( ) assert len(active_objs) == 1 active_obj = active_objs[0] + active_obj_original_sense = active_obj.sense recast_to_min_obj(model_data.working_model, active_obj) # === Determine first and second-stage objectives @@ -805,21 +427,10 @@ def solve( # === Move bounds on control variables to explicit ineq constraints wm_util = model_data.working_model - # === Assuming all other Var objects in the model are state variables - fsv = ComponentSet(model_data.working_model.util.first_stage_variables) - ssv = ComponentSet(model_data.working_model.util.second_stage_variables) - sv = ComponentSet() - model_data.working_model.util.state_vars = [] - for v in model_data.working_model.component_data_objects(Var): - if v not in fsv and v not in ssv and v not in sv: - model_data.working_model.util.state_vars.append(v) - sv.add(v) - - # Bounds on second stage variables and state variables are separation objectives, - # they are brought in this was as explicit constraints + # cast bounds on second-stage and state variables to + # explicit constraints for separation objectives for c in model_data.working_model.util.second_stage_variables: turn_bounds_to_constraints(c, wm_util, config) - for c in model_data.working_model.util.state_vars: turn_bounds_to_constraints(c, wm_util, config) @@ -831,10 +442,18 @@ def solve( if "bound_con" in c.name: wm_util.ssv_bounds.append(c) + model_data.timing.stop_timer("main.preprocessing") + preprocessing_time = model_data.timing.get_total_time("main.preprocessing") + config.progress_logger.info( + f"Done preprocessing; required wall time of " + f"{preprocessing_time:.3f}s." + ) + # === Solve and load solution into model pyros_soln, final_iter_separation_solns = ROSolver_iterative_solve( model_data, config ) + IterationLogRecord.log_header_rule(config.progress_logger.info) return_soln = ROSolveResults() if pyros_soln is not None and final_iter_separation_solns is not None: @@ -846,31 +465,24 @@ def solve( ): load_final_solution(model_data, pyros_soln.master_soln, config) - # === Return time info - model_data.total_cpu_time = get_main_elapsed_time(model_data.timing) - iterations = pyros_soln.total_iters + 1 - - # === Return config to user - return_soln.config = config - # Report the negative of the objective value if it was originally maximize, since we use the minimize form in the algorithm - if next(model.component_data_objects(Objective)).sense == maximize: - negation = -1 - else: - negation = 1 + # account for sense of the original model objective + # when reporting the final PyROS (master) objective, + # since maximization objective is changed to + # minimization objective during preprocessing if config.objective_focus == ObjectiveType.nominal: - return_soln.final_objective_value = negation * value( - pyros_soln.master_soln.master_model.obj + return_soln.final_objective_value = ( + active_obj_original_sense + * value(pyros_soln.master_soln.master_model.obj) ) elif config.objective_focus == ObjectiveType.worst_case: - return_soln.final_objective_value = negation * value( - pyros_soln.master_soln.master_model.zeta + return_soln.final_objective_value = ( + active_obj_original_sense + * value(pyros_soln.master_soln.master_model.zeta) ) return_soln.pyros_termination_condition = ( pyros_soln.pyros_termination_condition ) - - return_soln.time = model_data.total_cpu_time - return_soln.iterations = iterations + return_soln.iterations = pyros_soln.total_iters + 1 # === Remove util block model.del_component(model_data.util_block) @@ -878,138 +490,23 @@ def solve( del pyros_soln.util_block del pyros_soln.working_model else: + return_soln.final_objective_value = None return_soln.pyros_termination_condition = ( pyrosTerminationCondition.robust_infeasible ) - return_soln.final_objective_value = None - return_soln.time = get_main_elapsed_time(model_data.timing) return_soln.iterations = 0 - return return_soln + return_soln.config = config + return_soln.time = model_data.timing.get_total_time("main") -def _generate_filtered_docstring(): - """ - Add Numpy-style 'Keyword arguments' section to `PyROS.solve()` - docstring. - """ - cfg = PyROS.CONFIG() - - # mandatory args already documented - exclude_args = [ - "first_stage_variables", - "second_stage_variables", - "uncertain_params", - "uncertainty_set", - "local_solver", - "global_solver", - ] - - indent_by = 8 - width = 72 - before = PyROS.solve.__doc__ - section_name = "Keyword Arguments" - - indent_str = ' ' * indent_by - wrap_width = width - indent_by - cfg = pyros_config() + # log termination-related messages + config.progress_logger.info(return_soln.pyros_termination_condition.message) + config.progress_logger.info("-" * self._LOG_LINE_LENGTH) + config.progress_logger.info(f"Timing breakdown:\n\n{model_data.timing}") + config.progress_logger.info("-" * self._LOG_LINE_LENGTH) + config.progress_logger.info(return_soln) + config.progress_logger.info("-" * self._LOG_LINE_LENGTH) + config.progress_logger.info("All done. Exiting PyROS.") + config.progress_logger.info("=" * self._LOG_LINE_LENGTH) - arg_docs = [] - - def wrap_doc(doc, indent_by, width): - """ - Wrap a string, accounting for paragraph - breaks ('\n\n') and bullet points (paragraphs - which, when dedented, are such that each line - starts with '- ' or ' '). - """ - paragraphs = doc.split("\n\n") - wrapped_pars = [] - for par in paragraphs: - lines = dedent(par).split("\n") - has_bullets = all( - line.startswith("- ") or line.startswith(" ") - for line in lines - if line != "" - ) - if has_bullets: - # obtain strings of each bullet point - # (dedented, bullet dash and bullet indent removed) - bullet_groups = [] - new_group = False - group = "" - for line in lines: - new_group = line.startswith("- ") - if new_group: - bullet_groups.append(group) - group = "" - new_line = line[2:] - group += f"{new_line}\n" - if group != "": - # ensure last bullet not skipped - bullet_groups.append(group) - - # first entry is just ''; remove - bullet_groups = bullet_groups[1:] - - # wrap each bullet point, then add bullet - # and indents as necessary - wrapped_groups = [] - for group in bullet_groups: - wrapped_groups.append( - "\n".join( - f"{'- ' if idx == 0 else ' '}{line}" - for idx, line in enumerate( - wrap(group, width - 2 - indent_by) - ) - ) - ) - - # now combine bullets into single 'paragraph' - wrapped_pars.append( - indent("\n".join(wrapped_groups), prefix=' ' * indent_by) - ) - else: - wrapped_pars.append( - indent( - "\n".join(wrap(dedent(par), width=width - indent_by)), - prefix=' ' * indent_by, - ) - ) - - return "\n\n".join(wrapped_pars) - - section_header = indent(f"{section_name}\n" + "-" * len(section_name), indent_str) - for key, itm in cfg._data.items(): - if key in exclude_args: - continue - arg_name = key - arg_dtype = itm.dtype_spec_str - - if itm.is_optional: - if itm.document_default: - optional_str = f", default={repr(itm._default)}" - else: - optional_str = ", optional" - else: - optional_str = "" - - arg_header = f"{indent_str}{arg_name} : {arg_dtype}{optional_str}" - - # dedented_doc_str = dedent(itm.doc).replace("\n", ' ').strip() - if itm._doc is not None: - raw_arg_desc = itm._doc - else: - raw_arg_desc = itm._description - - arg_description = wrap_doc( - raw_arg_desc, width=wrap_width, indent_by=indent_by + 4 - ) - - arg_docs.append(f"{arg_header}\n{arg_description}") - - kwargs_section_doc = "\n".join([section_header] + arg_docs) - - return f"{before}\n{kwargs_section_doc}\n" - - -PyROS.solve.__doc__ = _generate_filtered_docstring() + return return_soln diff --git a/pyomo/contrib/pyros/pyros_algorithm_methods.py b/pyomo/contrib/pyros/pyros_algorithm_methods.py index 7a0c990d549..cfb57b08c7f 100644 --- a/pyomo/contrib/pyros/pyros_algorithm_methods.py +++ b/pyomo/contrib/pyros/pyros_algorithm_methods.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + ''' Methods for the execution of the grcs algorithm ''' @@ -11,14 +22,15 @@ ObjectiveType, get_time_from_solver, pyrosTerminationCondition, + IterationLogRecord, ) -from pyomo.contrib.pyros.util import ( - get_main_elapsed_time, - output_logger, - coefficient_matching, -) +from pyomo.contrib.pyros.util import get_main_elapsed_time, coefficient_matching from pyomo.core.base import value -from pyomo.common.collections import ComponentSet +from pyomo.core.expr import MonomialTermExpression +from pyomo.common.collections import ComponentSet, ComponentMap +from pyomo.core.base.var import VarData as VarData +from itertools import chain +from pyomo.common.dependencies import numpy as np def update_grcs_solve_data( @@ -47,6 +59,289 @@ def update_grcs_solve_data( return +def get_dr_var_to_scaled_expr_map( + decision_rule_eqns, second_stage_vars, uncertain_params, decision_rule_vars +): + """ + Generate mapping from decision rule variables + to their terms in a model's DR expression. + """ + var_to_scaled_expr_map = ComponentMap() + ssv_dr_eq_zip = zip(second_stage_vars, decision_rule_eqns) + for ssv_idx, (ssv, dr_eq) in enumerate(ssv_dr_eq_zip): + for term in dr_eq.body.args: + if isinstance(term, MonomialTermExpression): + is_ssv_term = ( + isinstance(term.args[0], int) + and term.args[0] == -1 + and isinstance(term.args[1], VarData) + ) + if not is_ssv_term: + dr_var = term.args[1] + var_to_scaled_expr_map[dr_var] = term + elif isinstance(term, VarData): + var_to_scaled_expr_map[term] = MonomialTermExpression((1, term)) + + return var_to_scaled_expr_map + + +def evaluate_and_log_component_stats(model_data, separation_model, config): + """ + Evaluate and log model component statistics. + """ + IterationLogRecord.log_header_rule(config.progress_logger.info) + config.progress_logger.info("Model statistics:") + # print model statistics + dr_var_set = ComponentSet( + chain( + *tuple( + indexed_dr_var.values() + for indexed_dr_var in model_data.working_model.util.decision_rule_vars + ) + ) + ) + first_stage_vars = [ + var + for var in model_data.working_model.util.first_stage_variables + if var not in dr_var_set + ] + + # account for epigraph constraint + sep_model_epigraph_con = getattr(separation_model, "epigraph_constr", None) + has_epigraph_con = sep_model_epigraph_con is not None + + num_fsv = len(first_stage_vars) + num_ssv = len(model_data.working_model.util.second_stage_variables) + num_sv = len(model_data.working_model.util.state_vars) + num_dr_vars = len(dr_var_set) + num_vars = int(has_epigraph_con) + num_fsv + num_ssv + num_sv + num_dr_vars + + num_uncertain_params = len(model_data.working_model.util.uncertain_params) + + eq_cons = [ + con + for con in model_data.working_model.component_data_objects( + Constraint, active=True + ) + if con.equality + ] + dr_eq_set = ComponentSet( + chain( + *tuple( + indexed_dr_eq.values() + for indexed_dr_eq in model_data.working_model.util.decision_rule_eqns + ) + ) + ) + num_eq_cons = len(eq_cons) + num_dr_cons = len(dr_eq_set) + num_coefficient_matching_cons = len( + getattr(model_data.working_model, "coefficient_matching_constraints", []) + ) + num_other_eq_cons = num_eq_cons - num_dr_cons - num_coefficient_matching_cons + + # get performance constraints as referenced in the separation + # model object + new_sep_con_map = separation_model.util.map_new_constraint_list_to_original_con + perf_con_set = ComponentSet( + new_sep_con_map.get(con, con) + for con in separation_model.util.performance_constraints + ) + is_epigraph_con_first_stage = ( + has_epigraph_con and sep_model_epigraph_con not in perf_con_set + ) + working_model_perf_con_set = ComponentSet( + model_data.working_model.find_component(new_sep_con_map.get(con, con)) + for con in separation_model.util.performance_constraints + if con is not None + ) + + num_perf_cons = len(separation_model.util.performance_constraints) + num_fsv_bounds = sum( + int(var.lower is not None) + int(var.upper is not None) + for var in first_stage_vars + ) + ineq_con_set = [ + con + for con in model_data.working_model.component_data_objects( + Constraint, active=True + ) + if not con.equality + ] + num_fsv_ineqs = ( + num_fsv_bounds + + len([con for con in ineq_con_set if con not in working_model_perf_con_set]) + + is_epigraph_con_first_stage + ) + num_ineq_cons = len(ineq_con_set) + has_epigraph_con + num_fsv_bounds + + config.progress_logger.info(f"{' Number of variables'} : {num_vars}") + config.progress_logger.info(f"{' Epigraph variable'} : {int(has_epigraph_con)}") + config.progress_logger.info(f"{' First-stage variables'} : {num_fsv}") + config.progress_logger.info(f"{' Second-stage variables'} : {num_ssv}") + config.progress_logger.info(f"{' State variables'} : {num_sv}") + config.progress_logger.info(f"{' Decision rule variables'} : {num_dr_vars}") + config.progress_logger.info( + f"{' Number of uncertain parameters'} : {num_uncertain_params}" + ) + config.progress_logger.info( + f"{' Number of constraints'} : " f"{num_ineq_cons + num_eq_cons}" + ) + config.progress_logger.info(f"{' Equality constraints'} : {num_eq_cons}") + config.progress_logger.info( + f"{' Coefficient matching constraints'} : " + f"{num_coefficient_matching_cons}" + ) + config.progress_logger.info(f"{' Decision rule equations'} : {num_dr_cons}") + config.progress_logger.info( + f"{' All other equality constraints'} : " f"{num_other_eq_cons}" + ) + config.progress_logger.info(f"{' Inequality constraints'} : {num_ineq_cons}") + config.progress_logger.info( + f"{' First-stage inequalities (incl. certain var bounds)'} : " + f"{num_fsv_ineqs}" + ) + config.progress_logger.info( + f"{' Performance constraints (incl. var bounds)'} : {num_perf_cons}" + ) + + +def evaluate_first_stage_var_shift( + current_master_fsv_vals, previous_master_fsv_vals, first_iter_master_fsv_vals +): + """ + Evaluate first-stage variable "shift": the maximum relative + difference between first-stage variable values from the current + and previous master iterations. + + Parameters + ---------- + current_master_fsv_vals : ComponentMap + First-stage variable values from the current master + iteration. + previous_master_fsv_vals : ComponentMap + First-stage variable values from the previous master + iteration. + first_iter_master_fsv_vals : ComponentMap + First-stage variable values from the first master + iteration. + + Returns + ------- + None + Returned only if `current_master_fsv_vals` is empty, + which should occur only if the problem has no first-stage + variables. + float + The maximum relative difference + Returned only if `current_master_fsv_vals` is not empty. + """ + if not current_master_fsv_vals: + # there are no first-stage variables + return None + else: + return max( + abs(current_master_fsv_vals[var] - previous_master_fsv_vals[var]) + / max((abs(first_iter_master_fsv_vals[var]), 1)) + for var in previous_master_fsv_vals + ) + + +def evaluate_second_stage_var_shift( + current_master_nom_ssv_vals, + previous_master_nom_ssv_vals, + first_iter_master_nom_ssv_vals, +): + """ + Evaluate second-stage variable "shift": the maximum relative + difference between second-stage variable values from the current + and previous master iterations as evaluated subject to the + nominal uncertain parameter realization. + + Parameters + ---------- + current_master_nom_ssv_vals : ComponentMap + Second-stage variable values from the current master + iteration, evaluated subject to the nominal uncertain + parameter realization. + previous_master_nom_ssv_vals : ComponentMap + Second-stage variable values from the previous master + iteration, evaluated subject to the nominal uncertain + parameter realization. + first_iter_master_nom_ssv_vals : ComponentMap + Second-stage variable values from the first master + iteration, evaluated subject to the nominal uncertain + parameter realization. + + Returns + ------- + None + Returned only if `current_master_nom_ssv_vals` is empty, + which should occur only if the problem has no second-stage + variables. + float + The maximum relative difference. + Returned only if `current_master_nom_ssv_vals` is not empty. + """ + if not current_master_nom_ssv_vals: + return None + else: + return max( + abs(current_master_nom_ssv_vals[ssv] - previous_master_nom_ssv_vals[ssv]) + / max((abs(first_iter_master_nom_ssv_vals[ssv]), 1)) + for ssv in previous_master_nom_ssv_vals + ) + + +def evaluate_dr_var_shift( + current_master_dr_var_vals, + previous_master_dr_var_vals, + first_iter_master_nom_ssv_vals, + dr_var_to_ssv_map, +): + """ + Evaluate decision rule variable "shift": the maximum relative + difference between scaled decision rule (DR) variable expressions + (terms in the DR equations) from the current + and previous master iterations. + + Parameters + ---------- + current_master_dr_var_vals : ComponentMap + DR variable values from the current master + iteration. + previous_master_dr_var_vals : ComponentMap + DR variable values from the previous master + iteration. + first_iter_master_nom_ssv_vals : ComponentMap + Second-stage variable values (evaluated subject to the + nominal uncertain parameter realization) + from the first master iteration. + dr_var_to_ssv_map : ComponentMap + Mapping from each DR variable to the + second-stage variable whose value is a function of the + DR variable. + + Returns + ------- + None + Returned only if `current_master_dr_var_vals` is empty, + which should occur only if the problem has no decision rule + (or equivalently, second-stage) variables. + float + The maximum relative difference. + Returned only if `current_master_dr_var_vals` is not empty. + """ + if not current_master_dr_var_vals: + return None + else: + return max( + abs(current_master_dr_var_vals[drvar] - previous_master_dr_var_vals[drvar]) + / max((1, abs(first_iter_master_nom_ssv_vals[dr_var_to_ssv_map[drvar]]))) + for drvar in previous_master_dr_var_vals + ) + + def ROSolver_iterative_solve(model_data, config): ''' GRCS algorithm implementation @@ -75,20 +370,23 @@ def ROSolver_iterative_solve(model_data, config): config=config, ) if not coeff_matching_success and not robust_infeasible: - raise ValueError( - "Equality constraint \"%s\" cannot be guaranteed to be robustly feasible, " - "given the current partitioning between first-stage, second-stage and state variables. " - "You might consider editing this constraint to reference some second-stage " - "and/or state variable(s)." % c.name + config.progress_logger.error( + f"Equality constraint {c.name!r} cannot be guaranteed to " + "be robustly feasible, given the current partitioning " + "among first-stage, second-stage, and state variables. " + "Consider editing this constraint to reference some " + "second-stage and/or state variable(s)." ) + raise ValueError("Coefficient matching unsuccessful. See the solver logs.") elif not coeff_matching_success and robust_infeasible: config.progress_logger.info( "PyROS has determined that the model is robust infeasible. " - "One reason for this is that equality constraint \"%s\" cannot be satisfied " - "against all realizations of uncertainty, " - "given the current partitioning between first-stage, second-stage and state variables. " - "You might consider editing this constraint to reference some (additional) second-stage " - "and/or state variable(s)." % c.name + f"One reason for this is that the equality constraint {c.name} " + "cannot be satisfied against all realizations of uncertainty, " + "given the current partitioning between " + "first-stage, second-stage, and state variables. " + "Consider editing this constraint to reference some (additional) " + "second-stage and/or state variable(s)." ) return None, None else: @@ -156,6 +454,10 @@ def ROSolver_iterative_solve(model_data, config): model_data=master_data, config=config ) + evaluate_and_log_component_stats( + model_data=model_data, separation_model=separation_model, config=config + ) + # === Create separation problem data container object and add information to catalog during solve separation_data = SeparationProblemData() separation_data.separation_model = separation_model @@ -204,6 +506,53 @@ def ROSolver_iterative_solve(model_data, config): dr_var_lists_original = [] dr_var_lists_polished = [] + # set up first-stage variable and DR variable sets + master_dr_var_set = ComponentSet( + chain( + *tuple( + indexed_var.values() + for indexed_var in master_data.master_model.scenarios[ + 0, 0 + ].util.decision_rule_vars + ) + ) + ) + master_fsv_set = ComponentSet( + var + for var in master_data.master_model.scenarios[0, 0].util.first_stage_variables + if var not in master_dr_var_set + ) + master_nom_ssv_set = ComponentSet( + master_data.master_model.scenarios[0, 0].util.second_stage_variables + ) + previous_master_fsv_vals = ComponentMap((var, None) for var in master_fsv_set) + previous_master_dr_var_vals = ComponentMap((var, None) for var in master_dr_var_set) + previous_master_nom_ssv_vals = ComponentMap( + (var, None) for var in master_nom_ssv_set + ) + + first_iter_master_fsv_vals = ComponentMap((var, None) for var in master_fsv_set) + first_iter_master_nom_ssv_vals = ComponentMap( + (var, None) for var in master_nom_ssv_set + ) + first_iter_dr_var_vals = ComponentMap((var, None) for var in master_dr_var_set) + nom_master_util_blk = master_data.master_model.scenarios[0, 0].util + dr_var_scaled_expr_map = get_dr_var_to_scaled_expr_map( + decision_rule_vars=nom_master_util_blk.decision_rule_vars, + decision_rule_eqns=nom_master_util_blk.decision_rule_eqns, + second_stage_vars=nom_master_util_blk.second_stage_variables, + uncertain_params=nom_master_util_blk.uncertain_params, + ) + dr_var_to_ssv_map = ComponentMap() + dr_ssv_zip = zip( + nom_master_util_blk.decision_rule_vars, + nom_master_util_blk.second_stage_variables, + ) + for indexed_dr_var, ssv in dr_ssv_zip: + for drvar in indexed_dr_var.values(): + dr_var_to_ssv_map[drvar] = ssv + + IterationLogRecord.log_header(config.progress_logger.info) k = 0 master_statuses = [] while config.max_iter == -1 or k < config.max_iter: @@ -216,7 +565,7 @@ def ROSolver_iterative_solve(model_data, config): ) # === Solve Master Problem - config.progress_logger.info("PyROS working on iteration %s..." % k) + config.progress_logger.debug(f"PyROS working on iteration {k}...") master_soln = master_problem_methods.solve_master( model_data=master_data, config=config ) @@ -239,7 +588,6 @@ def ROSolver_iterative_solve(model_data, config): is pyrosTerminationCondition.robust_infeasible ): term_cond = pyrosTerminationCondition.robust_infeasible - output_logger(config=config, robust_infeasible=True) elif ( master_soln.pyros_termination_condition is pyrosTerminationCondition.subsolver_error @@ -257,6 +605,20 @@ def ROSolver_iterative_solve(model_data, config): pyrosTerminationCondition.time_out, pyrosTerminationCondition.robust_infeasible, }: + log_record = IterationLogRecord( + iteration=k, + objective=None, + first_stage_var_shift=None, + second_stage_var_shift=None, + dr_var_shift=None, + num_violated_cons=None, + max_violation=None, + dr_polishing_success=None, + all_sep_problems_solved=None, + global_separation=None, + elapsed_time=get_main_elapsed_time(model_data.timing), + ) + log_record.log(config.progress_logger.info) update_grcs_solve_data( pyros_soln=model_data, k=k, @@ -280,6 +642,7 @@ def ROSolver_iterative_solve(model_data, config): nominal_data.nom_second_stage_cost = master_soln.second_stage_objective nominal_data.nom_obj = value(master_data.master_model.obj) + polishing_successful = True if ( config.decision_rule_order != 0 and len(config.second_stage_variables) > 0 @@ -294,8 +657,10 @@ def ROSolver_iterative_solve(model_data, config): vals.append(dvar.value) dr_var_lists_original.append(vals) - polishing_results = master_problem_methods.minimize_dr_vars( - model_data=master_data, config=config + (polishing_results, polishing_successful) = ( + master_problem_methods.minimize_dr_vars( + model_data=master_data, config=config + ) ) timing_data.total_dr_polish_time += get_time_from_solver(polishing_results) @@ -308,11 +673,63 @@ def ROSolver_iterative_solve(model_data, config): vals.append(dvar.value) dr_var_lists_polished.append(vals) - # === Check if time limit reached - elapsed = get_main_elapsed_time(model_data.timing) + # get current first-stage and DR variable values + # and compare with previous first-stage and DR variable + # values + current_master_fsv_vals = ComponentMap( + (var, value(var)) for var in master_fsv_set + ) + current_master_nom_ssv_vals = ComponentMap( + (var, value(var)) for var in master_nom_ssv_set + ) + current_master_dr_var_vals = ComponentMap( + (var, value(expr)) for var, expr in dr_var_scaled_expr_map.items() + ) + if k > 0: + first_stage_var_shift = evaluate_first_stage_var_shift( + current_master_fsv_vals=current_master_fsv_vals, + previous_master_fsv_vals=previous_master_fsv_vals, + first_iter_master_fsv_vals=first_iter_master_fsv_vals, + ) + second_stage_var_shift = evaluate_second_stage_var_shift( + current_master_nom_ssv_vals=current_master_nom_ssv_vals, + previous_master_nom_ssv_vals=previous_master_nom_ssv_vals, + first_iter_master_nom_ssv_vals=first_iter_master_nom_ssv_vals, + ) + dr_var_shift = evaluate_dr_var_shift( + current_master_dr_var_vals=current_master_dr_var_vals, + previous_master_dr_var_vals=previous_master_dr_var_vals, + first_iter_master_nom_ssv_vals=first_iter_master_nom_ssv_vals, + dr_var_to_ssv_map=dr_var_to_ssv_map, + ) + else: + for fsv in first_iter_master_fsv_vals: + first_iter_master_fsv_vals[fsv] = value(fsv) + for ssv in first_iter_master_nom_ssv_vals: + first_iter_master_nom_ssv_vals[ssv] = value(ssv) + for drvar in first_iter_dr_var_vals: + first_iter_dr_var_vals[drvar] = value(dr_var_scaled_expr_map[drvar]) + first_stage_var_shift = None + second_stage_var_shift = None + dr_var_shift = None + + # === Check if time limit reached after polishing if config.time_limit: + elapsed = get_main_elapsed_time(model_data.timing) if elapsed >= config.time_limit: - output_logger(config=config, time_out=True, elapsed=elapsed) + iter_log_record = IterationLogRecord( + iteration=k, + objective=value(master_data.master_model.obj), + first_stage_var_shift=first_stage_var_shift, + second_stage_var_shift=second_stage_var_shift, + dr_var_shift=dr_var_shift, + num_violated_cons=None, + max_violation=None, + dr_polishing_success=polishing_successful, + all_sep_problems_solved=None, + global_separation=None, + elapsed_time=elapsed, + ) update_grcs_solve_data( pyros_soln=model_data, k=k, @@ -322,6 +739,7 @@ def ROSolver_iterative_solve(model_data, config): separation_data=separation_data, master_soln=master_soln, ) + iter_log_record.log(config.progress_logger.info) return model_data, [] # === Set up for the separation problem @@ -376,10 +794,40 @@ def ROSolver_iterative_solve(model_data, config): separation_results.violating_param_realization ) + scaled_violations = [ + solve_call_res.scaled_violations[con] + for con, solve_call_res in separation_results.main_loop_results.solver_call_results.items() + if solve_call_res.scaled_violations is not None + ] + if scaled_violations: + max_sep_con_violation = max(scaled_violations) + else: + max_sep_con_violation = None + num_violated_cons = len(separation_results.violated_performance_constraints) + + all_sep_problems_solved = ( + len(scaled_violations) == len(separation_model.util.performance_constraints) + and not separation_results.subsolver_error + and not separation_results.time_out + ) or separation_results.all_discrete_scenarios_exhausted + + iter_log_record = IterationLogRecord( + iteration=k, + objective=value(master_data.master_model.obj), + first_stage_var_shift=first_stage_var_shift, + second_stage_var_shift=second_stage_var_shift, + dr_var_shift=dr_var_shift, + num_violated_cons=num_violated_cons, + max_violation=max_sep_con_violation, + dr_polishing_success=polishing_successful, + all_sep_problems_solved=all_sep_problems_solved, + global_separation=separation_results.solved_globally, + elapsed_time=get_main_elapsed_time(model_data.timing), + ) + # terminate on time limit elapsed = get_main_elapsed_time(model_data.timing) if separation_results.time_out: - output_logger(config=config, time_out=True, elapsed=elapsed) termination_condition = pyrosTerminationCondition.time_out update_grcs_solve_data( pyros_soln=model_data, @@ -390,6 +838,7 @@ def ROSolver_iterative_solve(model_data, config): separation_data=separation_data, master_soln=master_soln, ) + iter_log_record.log(config.progress_logger.info) return model_data, separation_results # terminate on separation subsolver error @@ -404,24 +853,26 @@ def ROSolver_iterative_solve(model_data, config): separation_data=separation_data, master_soln=master_soln, ) + iter_log_record.log(config.progress_logger.info) return model_data, separation_results # === Check if we terminate due to robust optimality or feasibility, # or in the event of bypassing global separation, no violations robustness_certified = separation_results.robustness_certified if robustness_certified: - output_logger( - config=config, bypass_global_separation=config.bypass_global_separation - ) + if config.bypass_global_separation: + config.progress_logger.warning( + "Option to bypass global separation was chosen. " + "Robust feasibility and optimality of the reported " + "solution are not guaranteed." + ) robust_optimal = ( config.solve_master_globally and config.objective_focus is ObjectiveType.worst_case ) if robust_optimal: - output_logger(config=config, robust_optimal=True) termination_condition = pyrosTerminationCondition.robust_optimal else: - output_logger(config=config, robust_feasible=True) termination_condition = pyrosTerminationCondition.robust_feasible update_grcs_solve_data( pyros_soln=model_data, @@ -432,6 +883,7 @@ def ROSolver_iterative_solve(model_data, config): separation_data=separation_data, master_soln=master_soln, ) + iter_log_record.log(config.progress_logger.info) return model_data, separation_results # === Add block to master at violation @@ -443,13 +895,32 @@ def ROSolver_iterative_solve(model_data, config): separation_results.violating_param_realization ) + config.progress_logger.debug("Points added to master:") + config.progress_logger.debug( + np.array([pt for pt in separation_data.points_added_to_master]) + ) + + # initialize second-stage and state variables + # for new master block to separation + # solution chosen by heuristic. consequently, + # equality constraints should all be satisfied (up to tolerances). + for var, val in separation_results.violating_separation_variable_values.items(): + master_var = master_data.master_model.scenarios[k + 1, 0].find_component( + var + ) + master_var.set_value(val) + k += 1 + iter_log_record.log(config.progress_logger.info) + previous_master_fsv_vals = current_master_fsv_vals + previous_master_nom_ssv_vals = current_master_nom_ssv_vals + previous_master_dr_var_vals = current_master_dr_var_vals + # Iteration limit reached - output_logger(config=config, max_iter=True) update_grcs_solve_data( pyros_soln=model_data, - k=k, + k=k - 1, # remove last increment to fix iteration count term_cond=pyrosTerminationCondition.max_iter, nominal_data=nominal_data, timing_data=timing_data, diff --git a/pyomo/contrib/pyros/separation_problem_methods.py b/pyomo/contrib/pyros/separation_problem_methods.py index 2c41c869474..18d0925bab0 100644 --- a/pyomo/contrib/pyros/separation_problem_methods.py +++ b/pyomo/contrib/pyros/separation_problem_methods.py @@ -1,12 +1,23 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ Functions for the construction and solving of the GRCS separation problem via ROsolver """ + from pyomo.core.base.constraint import Constraint, ConstraintList from pyomo.core.base.objective import Objective, maximize, value from pyomo.core.base import Var, Param from pyomo.common.collections import ComponentSet, ComponentMap from pyomo.common.dependencies import numpy as np -from pyomo.contrib.pyros.util import ObjectiveType, get_time_from_solver, output_logger from pyomo.contrib.pyros.solve_data import ( DiscreteSeparationSolveCallResults, SeparationSolveCallResults, @@ -25,9 +36,11 @@ from pyomo.contrib.pyros.util import ABS_CON_CHECK_FEAS_TOL from pyomo.common.timing import TicTocTimer from pyomo.contrib.pyros.util import ( - TIC_TOC_SOLVE_TIME_ATTR, adjust_solver_time_settings, + call_solver, + ObjectiveType, revert_solver_max_time_adjustment, + TIC_TOC_SOLVE_TIME_ATTR, ) import os from copy import deepcopy @@ -536,6 +549,54 @@ def get_worst_discrete_separation_solution( ) +def get_con_name_repr(separation_model, con, with_orig_name=True, with_obj_name=True): + """ + Get string representation of performance constraint + and any other modeling components to which it has + been mapped. + + Parameters + ---------- + separation_model : ConcreteModel + Separation model. + con : ScalarConstraint or ConstraintData + Constraint for which to get the representation. + with_orig_name : bool, optional + If constraint was added during construction of the + separation problem (i.e. if the constraint is a member of + in `separation_model.util.new_constraints`), + include the name of the original constraint from which + `perf_con` was created. + with_obj_name : bool, optional + Include name of separation model objective to which + constraint is mapped. Applicable only to performance + constraints of the separation problem. + + Returns + ------- + str + Constraint name representation. + """ + + qual_strs = [] + if with_orig_name: + # check performance constraint was not added + # at construction of separation problem + orig_con = separation_model.util.map_new_constraint_list_to_original_con.get( + con, con + ) + if orig_con is not con: + qual_strs.append(f"originally {orig_con.name!r}") + if with_obj_name: + objectives_map = separation_model.util.map_obj_to_constr + separation_obj = objectives_map[con] + qual_strs.append(f"mapped to objective {separation_obj.name!r}") + + final_qual_str = f" ({', '.join(qual_strs)})" if qual_strs else "" + + return f"{con.name!r}{final_qual_str}" + + def perform_separation_loop(model_data, config, solve_globally): """ Loop through, and solve, PyROS separation problems to @@ -589,6 +650,7 @@ def perform_separation_loop(model_data, config, solve_globally): solver_call_results=ComponentMap(), solved_globally=solve_globally, worst_case_perf_con=None, + all_discrete_scenarios_exhausted=True, ) perf_con_to_maximize = sorted_priority_groups[ @@ -633,12 +695,22 @@ def perform_separation_loop(model_data, config, solve_globally): ) all_solve_call_results = ComponentMap() - for priority, perf_constraints in sorted_priority_groups.items(): + priority_groups_enum = enumerate(sorted_priority_groups.items()) + for group_idx, (priority, perf_constraints) in priority_groups_enum: priority_group_solve_call_results = ComponentMap() - for perf_con in perf_constraints: - # config.progress_logger.info( - # f"Separating constraint {perf_con.name}" - # ) + for idx, perf_con in enumerate(perf_constraints): + # log progress of separation loop + solve_adverb = "Globally" if solve_globally else "Locally" + config.progress_logger.debug( + f"{solve_adverb} separating performance constraint " + f"{get_con_name_repr(model_data.separation_model, perf_con)} " + f"(priority {priority}, priority group {group_idx + 1} of " + f"{len(sorted_priority_groups)}, " + f"constraint {idx + 1} of {len(perf_constraints)} " + "in priority group, " + f"{len(all_solve_call_results) + idx + 1} of " + f"{len(all_performance_constraints)} total)" + ) # solve separation problem for this performance constraint if uncertainty_set_is_discrete: @@ -689,21 +761,33 @@ def perform_separation_loop(model_data, config, solve_globally): ) # # auxiliary log messages - # objectives_map = ( - # model_data.separation_model.util.map_obj_to_constr - # ) - # violated_con_name = list(objectives_map.keys())[ - # worst_case_perf_con - # ] - # config.progress_logger.info( - # f"Violation found for constraint {violated_con_name} " - # "under realization " - # f"{worst_case_res.violating_param_realization}" - # ) + violated_con_names = "\n ".join( + get_con_name_repr(model_data.separation_model, con) + for con, res in all_solve_call_results.items() + if res.found_violation + ) + config.progress_logger.debug( + f"Violated constraints:\n {violated_con_names} " + ) + config.progress_logger.debug( + "Worst-case constraint: " + f"{get_con_name_repr(model_data.separation_model, worst_case_perf_con)} " + "under realization " + f"{worst_case_res.violating_param_realization}." + ) + config.progress_logger.debug( + f"Maximal scaled violation " + f"{worst_case_res.scaled_violations[worst_case_perf_con]} " + "from this constraint " + "exceeds the robust feasibility tolerance " + f"{config.robust_feasibility_tolerance}" + ) # violating separation problem solution now chosen. # exit loop break + else: + config.progress_logger.debug("No violated performance constraints found.") return SeparationLoopResults( solver_call_results=all_solve_call_results, @@ -786,7 +870,7 @@ def evaluate_performance_constraint_violations( return (violating_param_realization, scaled_violations, constraint_violated) -def initialize_separation(model_data, config): +def initialize_separation(perf_con_to_maximize, model_data, config): """ Initialize separation problem variables, and fix all first-stage variables to their corresponding values from most recent @@ -794,6 +878,9 @@ def initialize_separation(model_data, config): Parameters ---------- + perf_con_to_maximize : ConstraintData + Performance constraint whose violation is to be maximized + for the separation problem of interest. model_data : SeparationProblemData Separation problem data. config : ConfigDict @@ -809,13 +896,33 @@ def initialize_separation(model_data, config): discrete geometry (as there is no master model block corresponding to any of the remaining discrete scenarios against which we separate). + + This method assumes that the master model has only one block + per iteration. """ - # initialize to values from nominal block if nominal objective. - # else, initialize to values from latest block added to master - if config.objective_focus == ObjectiveType.nominal: - block_num = 0 - else: - block_num = model_data.iteration + + def eval_master_violation(block_idx): + """ + Evaluate violation of `perf_con` by variables of + specified master block. + """ + new_con_map = ( + model_data.separation_model.util.map_new_constraint_list_to_original_con + ) + in_new_cons = perf_con_to_maximize in new_con_map + if in_new_cons: + sep_con = new_con_map[perf_con_to_maximize] + else: + sep_con = perf_con_to_maximize + master_con = model_data.master_model.scenarios[block_idx, 0].find_component( + sep_con + ) + return value(master_con) + + # initialize from master block with max violation of the + # performance constraint of interest. This gives the best known + # feasible solution (for case of non-discrete uncertainty sets). + block_num = max(range(model_data.iteration + 1), key=eval_master_violation) master_blk = model_data.master_model.scenarios[block_num, 0] master_blks = list(model_data.master_model.scenarios.values()) @@ -875,13 +982,34 @@ def get_parent_master_blk(var): "All h(x,q) type constraints must be deactivated in separation." ) - # check: initial point feasible? + # confirm the initial point is feasible for cases where + # we expect it to be (i.e. non-discrete uncertainty sets). + # otherwise, log the violated constraints + tol = ABS_CON_CHECK_FEAS_TOL + perf_con_name_repr = get_con_name_repr( + separation_model=model_data.separation_model, + con=perf_con_to_maximize, + with_orig_name=True, + with_obj_name=True, + ) + uncertainty_set_is_discrete = ( + config.uncertainty_set.geometry is Geometry.DISCRETE_SCENARIOS + ) for con in sep_model.component_data_objects(Constraint, active=True): - lb, val, ub = value(con.lb), value(con.body), value(con.ub) - lb_viol = val < lb - ABS_CON_CHECK_FEAS_TOL if lb is not None else False - ub_viol = val > ub + ABS_CON_CHECK_FEAS_TOL if ub is not None else False - if lb_viol or ub_viol: - config.progress_logger.debug(con.name, lb, val, ub) + lslack, uslack = con.lslack(), con.uslack() + if (lslack < -tol or uslack < -tol) and not uncertainty_set_is_discrete: + con_name_repr = get_con_name_repr( + separation_model=model_data.separation_model, + con=con, + with_orig_name=True, + with_obj_name=False, + ) + config.progress_logger.debug( + f"Initial point for separation of performance constraint " + f"{perf_con_name_repr} violates the model constraint " + f"{con_name_repr} by more than {tol}. " + f"(lslack={con.lslack()}, uslack={con.uslack()})" + ) locally_acceptable = {tc.optimal, tc.locallyOptimal, tc.globallyOptimal} @@ -929,11 +1057,21 @@ def solver_call_separation( solver_status_dict = {} nlp_model = model_data.separation_model + # get name of constraint for loggers + con_name_repr = get_con_name_repr( + separation_model=nlp_model, + con=perf_con_to_maximize, + with_orig_name=True, + with_obj_name=True, + ) + solve_mode = "global" if solve_globally else "local" + # === Initialize separation problem; fix first-stage variables - initialize_separation(model_data, config) + initialize_separation(perf_con_to_maximize, model_data, config) separation_obj.activate() + solve_mode_adverb = "globally" if solve_globally else "locally" solve_call_results = SeparationSolveCallResults( solved_globally=solve_globally, time_out=False, @@ -941,35 +1079,27 @@ def solver_call_separation( found_violation=False, subsolver_error=False, ) - timer = TicTocTimer() - for opt in solvers: - orig_setting, custom_setting_present = adjust_solver_time_settings( - model_data.timing, opt, config - ) - timer.tic(msg=None) - try: - results = opt.solve( - nlp_model, - tee=config.tee, - load_solutions=False, - symbolic_solver_labels=True, - ) - except ApplicationError: - # account for possible external subsolver errors - # (such as segmentation faults, function evaluation - # errors, etc.) - config.progress_logger.error( - f"Solver {repr(opt)} encountered exception attempting to " - "optimize separation problem in iteration " - f"{model_data.iteration}" - ) - raise - else: - setattr(results.solver, TIC_TOC_SOLVE_TIME_ATTR, timer.toc(msg=None)) - finally: - revert_solver_max_time_adjustment( - opt, orig_setting, custom_setting_present, config + for idx, opt in enumerate(solvers): + if idx > 0: + config.progress_logger.warning( + f"Invoking backup solver {opt!r} " + f"(solver {idx + 1} of {len(solvers)}) for {solve_mode} " + f"separation of performance constraint {con_name_repr} " + f"in iteration {model_data.iteration}." ) + results = call_solver( + model=nlp_model, + solver=opt, + config=config, + timing_obj=model_data.timing, + timer_name=f"main.{solve_mode}_separation", + err_msg=( + f"Optimizer {repr(opt)} ({idx + 1} of {len(solvers)}) " + f"encountered exception attempting " + f"to {solve_mode_adverb} solve separation problem for constraint " + f"{con_name_repr} in iteration {model_data.iteration}." + ), + ) # record termination condition for this particular solver solver_status_dict[str(opt)] = results.solver.termination_condition @@ -1014,15 +1144,25 @@ def solver_call_separation( separation_obj.deactivate() return solve_call_results + else: + config.progress_logger.debug( + f"Solver {opt} ({idx + 1} of {len(solvers)}) " + f"failed for {solve_mode} separation of performance " + f"constraint {con_name_repr} in iteration " + f"{model_data.iteration}. Termination condition: " + f"{results.solver.termination_condition!r}." + ) + config.progress_logger.debug(f"Results:\n{results.solver}") # All subordinate solvers failed to optimize model to appropriate # termination condition. PyROS will terminate with subsolver # error. At this point, export model if desired solve_call_results.subsolver_error = True save_dir = config.subproblem_file_directory + serialization_msg = "" if save_dir and config.keepfiles: objective = separation_obj.name - name = os.path.join( + output_problem_path = os.path.join( save_dir, ( config.uncertainty_set.type @@ -1035,15 +1175,23 @@ def solver_call_separation( + ".bar" ), ) - nlp_model.write(name, io_options={'symbolic_solver_labels': True}) - output_logger( - config=config, - separation_error=True, - filename=name, - iteration=model_data.iteration, - objective=objective, - status_dict=solver_status_dict, + nlp_model.write( + output_problem_path, io_options={'symbolic_solver_labels': True} + ) + serialization_msg = ( + " For debugging, problem has been serialized to the file " + f"{output_problem_path!r}." ) + solve_call_results.message = ( + "Could not successfully solve separation problem of iteration " + f"{model_data.iteration} " + f"for performance constraint {con_name_repr} with any of the " + f"provided subordinate {solve_mode} optimizers. " + f"(Termination statuses: " + f"{[str(term_cond) for term_cond in solver_status_dict.values()]}.)" + f"{serialization_msg}" + ) + config.progress_logger.warning(solve_call_results.message) separation_obj.deactivate() diff --git a/pyomo/contrib/pyros/solve_data.py b/pyomo/contrib/pyros/solve_data.py index 63e7fdd7ebd..73eee5202aa 100644 --- a/pyomo/contrib/pyros/solve_data.py +++ b/pyomo/contrib/pyros/solve_data.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ Objects to contain all model data and solve results for the ROSolver """ @@ -5,17 +16,71 @@ class ROSolveResults(object): """ - Container for solve-instance data returned to the user after solving with PyROS. + PyROS solver results object. - Attributes: - :pyros_termination_condition: termination condition of the PyROS algorithm - :config: the config block for this solve instance - :time: Total solver CPU time - :iterations: total iterations done by PyROS solver - :final_objective_value: objective function value at termination + Parameters + ---------- + config : ConfigDict, optional + User-specified solver settings. + iterations : int, optional + Number of iterations required. + time : float, optional + Total elapsed time (or wall time), in seconds. + final_objective_value : float, optional + Final objective function value to report. + pyros_termination_condition : pyrosTerminationCondition, optional + PyROS-specific termination condition. + + Attributes + ---------- + config : ConfigDict, optional + User-specified solver settings. + iterations : int, optional + Number of iterations required by PyROS. + time : float, optional + Total elapsed time (or wall time), in seconds. + final_objective_value : float, optional + Final objective function value to report. + pyros_termination_condition : pyros.util.pyrosTerminationStatus + Indicator of the manner of termination. """ - pass + def __init__( + self, + config=None, + iterations=None, + time=None, + final_objective_value=None, + pyros_termination_condition=None, + ): + """Initialize self (see class docstring).""" + self.config = config + self.iterations = iterations + self.time = time + self.final_objective_value = final_objective_value + self.pyros_termination_condition = pyros_termination_condition + + def __str__(self): + """ + Generate string representation of self. + Does not include any information about `self.config`. + """ + lines = ["Termination stats:"] + attr_name_format_dict = { + "iterations": ("Iterations", "f'{val}'"), + "time": ("Solve time (wall s)", "f'{val:.3f}'"), + "final_objective_value": ("Final objective value", "f'{val:.4e}'"), + "pyros_termination_condition": ("Termination condition", "f'{val}'"), + } + attr_desc_pad_length = max( + len(desc) for desc, _ in attr_name_format_dict.values() + ) + for attr_name, (attr_desc, fmt_str) in attr_name_format_dict.items(): + val = getattr(self, attr_name) + val_str = eval(fmt_str) if val is not None else str(val) + lines.append(f" {attr_desc:<{attr_desc_pad_length}s} : {val_str}") + + return "\n".join(lines) class MasterProblemData(object): @@ -282,16 +347,23 @@ class SeparationLoopResults: solver_call_results : ComponentMap Mapping from performance constraints to corresponding ``SeparationSolveCallResults`` objects. - worst_case_perf_con : None or int, optional + worst_case_perf_con : None or Constraint Performance constraint mapped to ``SeparationSolveCallResults`` object in `self` corresponding to maximally violating separation problem solution. + all_discrete_scenarios_exhausted : bool, optional + For problems with discrete uncertainty sets, + True if all scenarios were explicitly accounted for in master + (which occurs if there have been + as many PyROS iterations as there are scenarios in the set) + False otherwise. Attributes ---------- solver_call_results solved_globally worst_case_perf_con + all_discrete_scenarios_exhausted found_violation violating_param_realization scaled_violations @@ -300,11 +372,18 @@ class SeparationLoopResults: time_out """ - def __init__(self, solved_globally, solver_call_results, worst_case_perf_con): + def __init__( + self, + solved_globally, + solver_call_results, + worst_case_perf_con, + all_discrete_scenarios_exhausted=False, + ): """Initialize self (see class docstring).""" self.solver_call_results = solver_call_results self.solved_globally = solved_globally self.worst_case_perf_con = worst_case_perf_con + self.all_discrete_scenarios_exhausted = all_discrete_scenarios_exhausted @property def found_violation(self): @@ -443,6 +522,7 @@ class SeparationResults: ---------- local_separation_loop_results global_separation_loop_results + main_loop_results subsolver_error time_out solved_locally @@ -462,7 +542,7 @@ def __init__(self, local_separation_loop_results, global_separation_loop_results @property def time_out(self): """ - Return True if time out found for local or global + bool : True if time out found for local or global separation loop, False otherwise. """ local_time_out = ( @@ -476,7 +556,7 @@ def time_out(self): @property def subsolver_error(self): """ - Return True if subsolver error found for local or global + bool : True if subsolver error found for local or global separation loop, False otherwise. """ local_subsolver_error = ( @@ -490,7 +570,7 @@ def subsolver_error(self): @property def solved_locally(self): """ - Return true if local separation loop was invoked, + bool : true if local separation loop was invoked, False otherwise. """ return self.local_separation_loop_results is not None @@ -498,13 +578,18 @@ def solved_locally(self): @property def solved_globally(self): """ - Return True if global separation loop was invoked, + bool : True if global separation loop was invoked, False otherwise. """ return self.global_separation_loop_results is not None def get_violating_attr(self, attr_name): """ + If separation problems solved globally, returns + value of attribute of global separation loop results. + + Otherwise, if separation problems solved locally, + returns value of attribute of local separation loop results. If local separation loop results specified, return value of attribute of local separation loop results. @@ -526,27 +611,44 @@ def get_violating_attr(self, attr_name): object Attribute value. """ - if self.solved_locally: - local_loop_val = getattr(self.local_separation_loop_results, attr_name) - else: - local_loop_val = None + return getattr(self.main_loop_results, attr_name, None) - if local_loop_val is not None: - attr_val = local_loop_val - else: - if self.solved_globally: - attr_val = getattr(self.global_separation_loop_results, attr_name) - else: - attr_val = None + @property + def all_discrete_scenarios_exhausted(self): + """ + bool : For problems where the uncertainty set is of type + DiscreteScenarioSet, + True if last master problem solved explicitly + accounts for all scenarios in the uncertainty set, + False otherwise. + """ + return self.get_violating_attr("all_discrete_scenarios_exhausted") - return attr_val + @property + def worst_case_perf_con(self): + """ + ConstraintData : Performance constraint corresponding to the + separation solution chosen for the next master problem. + """ + return self.get_violating_attr("worst_case_perf_con") + + @property + def main_loop_results(self): + """ + SeparationLoopResults : Main separation loop results. + In particular, this is considered to be the global + loop result if solved globally, and the local loop + results otherwise. + """ + if self.solved_globally: + return self.global_separation_loop_results + return self.local_separation_loop_results @property def found_violation(self): """ - bool: True if ``found_violation`` attribute for - local or global separation loop results found - to be True, False otherwise. + bool : True if ``found_violation`` attribute for + main separation loop results is True, False otherwise. """ found_viol = self.get_violating_attr("found_violation") if found_viol is None: diff --git a/pyomo/contrib/pyros/tests/__init__.py b/pyomo/contrib/pyros/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/pyros/tests/__init__.py +++ b/pyomo/contrib/pyros/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py new file mode 100644 index 00000000000..166fbada4ff --- /dev/null +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -0,0 +1,620 @@ +""" +Test objects for construction of PyROS ConfigDict. +""" + +import logging +import unittest + +from pyomo.core.base import ConcreteModel, Var, VarData +from pyomo.common.log import LoggingIntercept +from pyomo.common.errors import ApplicationError +from pyomo.core.base.param import Param, ParamData +from pyomo.contrib.pyros.config import ( + InputDataStandardizer, + mutable_param_validator, + logger_domain, + SolverNotResolvable, + positive_int_or_minus_one, + pyros_config, + SolverIterable, + SolverResolvable, +) +from pyomo.contrib.pyros.util import ObjectiveType +from pyomo.opt import SolverFactory, SolverResults +from pyomo.contrib.pyros.uncertainty_sets import BoxSet +from pyomo.common.dependencies import numpy_available + + +class TestInputDataStandardizer(unittest.TestCase): + """ + Test standardizer method for Pyomo component-type inputs. + """ + + def test_single_component_data(self): + """ + Test standardizer works for single component + data-type entry. + """ + mdl = ConcreteModel() + mdl.v = Var([0, 1]) + + standardizer_func = InputDataStandardizer(Var, VarData) + + standardizer_input = mdl.v[0] + standardizer_output = standardizer_func(standardizer_input) + + self.assertIsInstance( + standardizer_output, + list, + msg=( + "Standardized output should be of type list, " + f"but is of type {standardizer_output.__class__.__name__}." + ), + ) + self.assertEqual( + len(standardizer_output), + 1, + msg="Length of standardizer output is not as expected.", + ) + self.assertIs( + standardizer_output[0], + mdl.v[0], + msg=( + f"Entry {standardizer_output[0]} (id {id(standardizer_output[0])}) " + "is not identical to " + f"input component data object {mdl.v[0]} " + f"(id {id(mdl.v[0])})" + ), + ) + + def test_standardizer_indexed_component(self): + """ + Test component standardizer works on indexed component. + """ + mdl = ConcreteModel() + mdl.v = Var([0, 1]) + + standardizer_func = InputDataStandardizer(Var, VarData) + + standardizer_input = mdl.v + standardizer_output = standardizer_func(standardizer_input) + + self.assertIsInstance( + standardizer_output, + list, + msg=( + "Standardized output should be of type list, " + f"but is of type {standardizer_output.__class__.__name__}." + ), + ) + self.assertEqual( + len(standardizer_output), + 2, + msg="Length of standardizer output is not as expected.", + ) + enum_zip = enumerate(zip(standardizer_input.values(), standardizer_output)) + for idx, (input, output) in enum_zip: + self.assertIs( + input, + output, + msg=( + f"Entry {input} (id {id(input)}) " + "is not identical to " + f"input component data object {output} " + f"(id {id(output)})" + ), + ) + + def test_standardizer_multiple_components(self): + """ + Test standardizer works on sequence of components. + """ + mdl = ConcreteModel() + mdl.v = Var([0, 1]) + mdl.x = Var(["a", "b"]) + + standardizer_func = InputDataStandardizer(Var, VarData) + + standardizer_input = [mdl.v[0], mdl.x] + standardizer_output = standardizer_func(standardizer_input) + expected_standardizer_output = [mdl.v[0], mdl.x["a"], mdl.x["b"]] + + self.assertIsInstance( + standardizer_output, + list, + msg=( + "Standardized output should be of type list, " + f"but is of type {standardizer_output.__class__.__name__}." + ), + ) + self.assertEqual( + len(standardizer_output), + len(expected_standardizer_output), + msg="Length of standardizer output is not as expected.", + ) + enum_zip = enumerate(zip(expected_standardizer_output, standardizer_output)) + for idx, (input, output) in enum_zip: + self.assertIs( + input, + output, + msg=( + f"Entry {input} (id {id(input)}) " + "is not identical to " + f"input component data object {output} " + f"(id {id(output)})" + ), + ) + + def test_standardizer_invalid_duplicates(self): + """ + Test standardizer raises exception if input contains duplicates + and duplicates are not allowed. + """ + mdl = ConcreteModel() + mdl.v = Var([0, 1]) + mdl.x = Var(["a", "b"]) + + standardizer_func = InputDataStandardizer(Var, VarData, allow_repeats=False) + + exc_str = r"Standardized.*list.*contains duplicate entries\." + with self.assertRaisesRegex(ValueError, exc_str): + standardizer_func([mdl.x, mdl.v, mdl.x]) + + def test_standardizer_invalid_type(self): + """ + Test standardizer raises exception as expected + when input is of invalid type. + """ + standardizer_func = InputDataStandardizer(Var, VarData) + + exc_str = r"Input object .*is not of valid component type.*" + with self.assertRaisesRegex(TypeError, exc_str): + standardizer_func(2) + + def test_standardizer_iterable_with_invalid_type(self): + """ + Test standardizer raises exception as expected + when input is an iterable with entries of invalid type. + """ + mdl = ConcreteModel() + mdl.v = Var([0, 1]) + standardizer_func = InputDataStandardizer(Var, VarData) + + exc_str = r"Input object .*entry of iterable.*is not of valid component type.*" + with self.assertRaisesRegex(TypeError, exc_str): + standardizer_func([mdl.v, 2]) + + def test_standardizer_invalid_str_passed(self): + """ + Test standardizer raises exception as expected + when input is of invalid type str. + """ + standardizer_func = InputDataStandardizer(Var, VarData) + + exc_str = r"Input object .*is not of valid component type.*" + with self.assertRaisesRegex(TypeError, exc_str): + standardizer_func("abcd") + + def test_standardizer_invalid_uninitialized_params(self): + """ + Test standardizer raises exception when Param with + uninitialized entries passed. + """ + standardizer_func = InputDataStandardizer( + ctype=Param, cdatatype=ParamData, ctype_validator=mutable_param_validator + ) + + mdl = ConcreteModel() + mdl.p = Param([0, 1]) + + exc_str = r"Length of .*does not match that of.*index set" + with self.assertRaisesRegex(ValueError, exc_str): + standardizer_func(mdl.p) + + def test_standardizer_invalid_immutable_params(self): + """ + Test standardizer raises exception when immutable + Param object(s) passed. + """ + standardizer_func = InputDataStandardizer( + ctype=Param, cdatatype=ParamData, ctype_validator=mutable_param_validator + ) + + mdl = ConcreteModel() + mdl.p = Param([0, 1], initialize=1) + + exc_str = r"Param object with name .*immutable" + with self.assertRaisesRegex(ValueError, exc_str): + standardizer_func(mdl.p) + + def test_standardizer_valid_mutable_params(self): + """ + Test Param-like standardizer works as expected for sequence + of valid mutable Param objects. + """ + mdl = ConcreteModel() + mdl.p1 = Param([0, 1], initialize=0, mutable=True) + mdl.p2 = Param(["a", "b"], initialize=1, mutable=True) + + standardizer_func = InputDataStandardizer( + ctype=Param, cdatatype=ParamData, ctype_validator=mutable_param_validator + ) + + standardizer_input = [mdl.p1[0], mdl.p2] + standardizer_output = standardizer_func(standardizer_input) + expected_standardizer_output = [mdl.p1[0], mdl.p2["a"], mdl.p2["b"]] + + self.assertIsInstance( + standardizer_output, + list, + msg=( + "Standardized output should be of type list, " + f"but is of type {standardizer_output.__class__.__name__}." + ), + ) + self.assertEqual( + len(standardizer_output), + len(expected_standardizer_output), + msg="Length of standardizer output is not as expected.", + ) + enum_zip = enumerate(zip(expected_standardizer_output, standardizer_output)) + for idx, (input, output) in enum_zip: + self.assertIs( + input, + output, + msg=( + f"Entry {input} (id {id(input)}) " + "is not identical to " + f"input component data object {output} " + f"(id {id(output)})" + ), + ) + + +AVAILABLE_SOLVER_TYPE_NAME = "available_pyros_test_solver" + + +class AvailableSolver: + """ + Perennially available placeholder solver. + """ + + def available(self, exception_flag=False): + """ + Check solver available. + """ + return True + + def solve(self, model, **kwds): + """ + Return SolverResults object with 'unknown' termination + condition. Model remains unchanged. + """ + return SolverResults() + + +class UnavailableSolver: + def available(self, exception_flag=True): + if exception_flag: + raise ApplicationError(f"Solver {self.__class__} not available") + return False + + def solve(self, model, *args, **kwargs): + return SolverResults() + + +class TestSolverResolvable(unittest.TestCase): + """ + Test PyROS standardizer for solver-type objects. + """ + + def setUp(self): + SolverFactory.register(AVAILABLE_SOLVER_TYPE_NAME)(AvailableSolver) + + def tearDown(self): + SolverFactory.unregister(AVAILABLE_SOLVER_TYPE_NAME) + + def test_solver_resolvable_valid_str(self): + """ + Test solver resolvable class is valid for string + type. + """ + solver_str = AVAILABLE_SOLVER_TYPE_NAME + standardizer_func = SolverResolvable() + solver = standardizer_func(solver_str) + expected_solver_type = type(SolverFactory(solver_str)) + + self.assertIsInstance( + solver, + type(SolverFactory(solver_str)), + msg=( + "SolverResolvable object should be of type " + f"{expected_solver_type.__name__}, " + f"but got object of type {solver.__class__.__name__}." + ), + ) + + def test_solver_resolvable_valid_solver_type(self): + """ + Test solver resolvable class is valid for string + type. + """ + solver = SolverFactory(AVAILABLE_SOLVER_TYPE_NAME) + standardizer_func = SolverResolvable() + standardized_solver = standardizer_func(solver) + + self.assertIs( + solver, + standardized_solver, + msg=( + f"Test solver {solver} and standardized solver " + f"{standardized_solver} are not identical." + ), + ) + + def test_solver_resolvable_invalid_type(self): + """ + Test solver resolvable object raises expected + exception when invalid entry is provided. + """ + invalid_object = 2 + standardizer_func = SolverResolvable(solver_desc="local solver") + + exc_str = ( + r"Cannot cast object `2` to a Pyomo optimizer.*" + r"local solver.*got type int.*" + ) + with self.assertRaisesRegex(SolverNotResolvable, exc_str): + standardizer_func(invalid_object) + + def test_solver_resolvable_unavailable_solver(self): + """ + Test solver standardizer fails in event solver is + unavailable. + """ + unavailable_solver = UnavailableSolver() + standardizer_func = SolverResolvable( + solver_desc="local solver", require_available=True + ) + + exc_str = r"Solver.*UnavailableSolver.*not available" + with self.assertRaisesRegex(ApplicationError, exc_str): + with LoggingIntercept(level=logging.ERROR) as LOG: + standardizer_func(unavailable_solver) + + error_msgs = LOG.getvalue()[:-1] + self.assertRegex( + error_msgs, r"Output of `available\(\)` method.*local solver.*" + ) + + +class TestSolverIterable(unittest.TestCase): + """ + Test standardizer method for iterable of solvers, + used to validate `backup_local_solvers` and `backup_global_solvers` + arguments. + """ + + def setUp(self): + SolverFactory.register(AVAILABLE_SOLVER_TYPE_NAME)(AvailableSolver) + + def tearDown(self): + SolverFactory.unregister(AVAILABLE_SOLVER_TYPE_NAME) + + def test_solver_iterable_valid_list(self): + """ + Test solver type standardizer works for list of valid + objects castable to solver. + """ + solver_list = [ + AVAILABLE_SOLVER_TYPE_NAME, + SolverFactory(AVAILABLE_SOLVER_TYPE_NAME), + ] + expected_solver_types = [AvailableSolver] * 2 + standardizer_func = SolverIterable() + + standardized_solver_list = standardizer_func(solver_list) + + # check list of solver types returned + for idx, standardized_solver in enumerate(standardized_solver_list): + self.assertIsInstance( + standardized_solver, + expected_solver_types[idx], + msg=( + f"Standardized solver {standardized_solver} " + f"(index {idx}) expected to be of type " + f"{expected_solver_types[idx].__name__}, " + f"but is of type {standardized_solver.__class__.__name__}" + ), + ) + + # second entry of standardized solver list should be the same + # object as that of input list, since the input solver is a Pyomo + # solver type + self.assertIs( + standardized_solver_list[1], + solver_list[1], + msg=( + f"Test solver {solver_list[1]} and standardized solver " + f"{standardized_solver_list[1]} should be identical." + ), + ) + + def test_solver_iterable_valid_str(self): + """ + Test SolverIterable raises exception when str passed. + """ + solver_str = AVAILABLE_SOLVER_TYPE_NAME + standardizer_func = SolverIterable() + + solver_list = standardizer_func(solver_str) + self.assertEqual( + len(solver_list), 1, "Standardized solver list is not of expected length" + ) + + def test_solver_iterable_unavailable_solver(self): + """ + Test SolverIterable addresses unavailable solvers appropriately. + """ + solvers = (AvailableSolver(), UnavailableSolver()) + + standardizer_func = SolverIterable( + require_available=True, + filter_by_availability=True, + solver_desc="example solver list", + ) + exc_str = r"Solver.*UnavailableSolver.* not available" + with self.assertRaisesRegex(ApplicationError, exc_str): + standardizer_func(solvers) + with self.assertRaisesRegex(ApplicationError, exc_str): + standardizer_func(solvers, filter_by_availability=False) + + standardized_solver_list = standardizer_func( + solvers, filter_by_availability=True, require_available=False + ) + self.assertEqual( + len(standardized_solver_list), + 1, + msg=("Length of filtered standardized solver list not as " "expected."), + ) + self.assertIs( + standardized_solver_list[0], + solvers[0], + msg="Entry of filtered standardized solver list not as expected.", + ) + + standardized_solver_list = standardizer_func( + solvers, filter_by_availability=False, require_available=False + ) + self.assertEqual( + len(standardized_solver_list), + 2, + msg=("Length of filtered standardized solver list not as " "expected."), + ) + self.assertEqual( + standardized_solver_list, + list(solvers), + msg="Entry of filtered standardized solver list not as expected.", + ) + + def test_solver_iterable_invalid_list(self): + """ + Test SolverIterable raises exception if iterable contains + at least one invalid object. + """ + invalid_object = [AVAILABLE_SOLVER_TYPE_NAME, 2] + standardizer_func = SolverIterable(solver_desc="backup solver") + + exc_str = ( + r"Cannot cast object `2` to a Pyomo optimizer.*" + r"backup solver.*index 1.*got type int.*" + ) + with self.assertRaisesRegex(SolverNotResolvable, exc_str): + standardizer_func(invalid_object) + + +class TestPyROSConfig(unittest.TestCase): + """ + Test PyROS ConfigDict behaves as expected. + """ + + CONFIG = pyros_config() + + def test_config_objective_focus(self): + """ + Test config parses objective focus as expected. + """ + config = self.CONFIG() + + for obj_focus_name in ["nominal", "worst_case"]: + config.objective_focus = obj_focus_name + self.assertEqual( + config.objective_focus, + ObjectiveType[obj_focus_name], + msg="Objective focus not set as expected.", + ) + + for obj_focus in ObjectiveType: + config.objective_focus = obj_focus + self.assertEqual( + config.objective_focus, + obj_focus, + msg="Objective focus not set as expected.", + ) + + invalid_focus = "test_example" + exc_str = f".*{invalid_focus!r} is not a valid ObjectiveType" + with self.assertRaisesRegex(ValueError, exc_str): + config.objective_focus = invalid_focus + + +class TestPositiveIntOrMinusOne(unittest.TestCase): + """ + Test validator for -1 or positive int works as expected. + """ + + def test_positive_int_or_minus_one(self): + """ + Test positive int or -1 validator works as expected. + """ + standardizer_func = positive_int_or_minus_one + ans = standardizer_func(1.0) + self.assertEqual( + ans, + 1, + msg=f"{positive_int_or_minus_one.__name__} output value not as expected.", + ) + self.assertIs( + type(ans), + int, + msg=f"{positive_int_or_minus_one.__name__} output type not as expected.", + ) + + ans = standardizer_func(-1.0) + self.assertEqual( + ans, + -1, + msg=f"{positive_int_or_minus_one.__name__} output value not as expected.", + ) + self.assertIs( + type(ans), + int, + msg=f"{positive_int_or_minus_one.__name__} output type not as expected.", + ) + + exc_str = r"Expected positive int or -1, but received value.*" + with self.assertRaisesRegex(ValueError, exc_str): + standardizer_func(1.5) + with self.assertRaisesRegex(ValueError, exc_str): + standardizer_func(0) + + +class TestLoggerDomain(unittest.TestCase): + """ + Test logger type domain validator. + """ + + def test_logger_type(self): + """ + Test logger type validator. + """ + standardizer_func = logger_domain + mylogger = logging.getLogger("example") + self.assertIs( + standardizer_func(mylogger), + mylogger, + msg=f"{standardizer_func.__name__} output not as expected", + ) + self.assertIs( + standardizer_func(mylogger.name), + mylogger, + msg=f"{standardizer_func.__name__} output not as expected", + ) + + exc_str = r"A logger name must be a string" + with self.assertRaisesRegex(Exception, exc_str): + standardizer_func(2) + + +if __name__ == "__main__": + unittest.main() diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 0d24b799b99..f7efec4d6e7 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + ''' Unit tests for the grcs API One class per function being tested, minimum one test per class @@ -5,35 +16,55 @@ import pyomo.common.unittest as unittest from pyomo.common.log import LoggingIntercept -from pyomo.common.collections import ComponentSet +from pyomo.common.collections import ComponentSet, ComponentMap from pyomo.common.config import ConfigBlock, ConfigValue from pyomo.core.base.set_types import NonNegativeIntegers -from pyomo.core.expr import identify_variables, identify_mutable_parameters +from pyomo.core.base.var import VarData +from pyomo.core.expr import ( + identify_variables, + identify_mutable_parameters, + MonomialTermExpression, + SumExpression, +) from pyomo.contrib.pyros.util import ( selective_clone, add_decision_rule_variables, add_decision_rule_constraints, - model_is_valid, turn_bounds_to_constraints, transform_to_standard_form, ObjectiveType, pyrosTerminationCondition, coefficient_matching, + TimingData, + IterationLogRecord, ) from pyomo.contrib.pyros.util import replace_uncertain_bounds_with_constraints from pyomo.contrib.pyros.util import get_vars_from_component from pyomo.contrib.pyros.util import identify_objective_functions from pyomo.common.collections import Bunch import time +import math from pyomo.contrib.pyros.util import time_code -from pyomo.contrib.pyros.uncertainty_sets import * +from pyomo.contrib.pyros.uncertainty_sets import ( + UncertaintySet, + BoxSet, + CardinalitySet, + BudgetSet, + FactorModelSet, + PolyhedralSet, + EllipsoidalSet, + AxisAlignedEllipsoidalSet, + IntersectionSet, + DiscreteScenarioSet, + Geometry, +) from pyomo.contrib.pyros.master_problem_methods import ( add_scenario_to_master, initial_construct_master, solve_master, minimize_dr_vars, ) -from pyomo.contrib.pyros.solve_data import MasterProblemData +from pyomo.contrib.pyros.solve_data import MasterProblemData, ROSolveResults from pyomo.common.dependencies import numpy as np, numpy_available from pyomo.common.dependencies import scipy as sp, scipy_available from pyomo.environ import maximize as pyo_max @@ -46,6 +77,11 @@ Solution, ) from pyomo.environ import ( + Reals, + Set, + Block, + ConstraintList, + ConcreteModel, Constraint, Expression, Objective, @@ -58,7 +94,14 @@ sin, sqrt, value, + maximize, + minimize, ) +import logging +from itertools import chain + + +logger = logging.getLogger(__name__) if not (numpy_available and scipy_available): @@ -88,6 +131,9 @@ scip_license_is_valid = False scip_version = (0, 0, 0) +_ipopt = SolverFactory("ipopt") +ipopt_available = _ipopt.available(exception_flag=False) + # @SolverFactory.register("time_delay_solver") class TimeDelaySolver(object): @@ -105,7 +151,7 @@ def __init__(self, calls_to_sleep, max_time, sub_solver): self.num_calls = 0 self.options = Bunch() - def available(self): + def available(self, exception_flag=True): return True def license_is_valid(self): @@ -231,198 +277,358 @@ def test_cloning_positive_case(self): class testAddDecisionRuleVars(unittest.TestCase): - ''' - Testing the method to add decision rule variables to a Pyomo model. This function should add decision rule - variables to the list of first_stage_variables in a model object. The number of decision rule variables added - depends on the number of control variables in the model and the number of uncertain parameters in the model. - ''' + """ + Test method for adding decision rule variables to working model. + The number of decision rule variables per control variable + should depend on: - @unittest.skipIf(not scipy_available, 'Scipy is not available.') - def test_add_decision_rule_vars_positive_case(self): - ''' - Testing whether the correct number of decision rule variables is created in each DR type case - ''' + - the number of uncertain parameters in the model + - the decision rule order specified by the user. + """ + + def make_simple_test_model(self): + """ + Make simple test model for DR variable + declaration testing. + """ m = ConcreteModel() - m.p1 = Param(initialize=0, mutable=True) - m.p2 = Param(initialize=0, mutable=True) - m.z1 = Var(initialize=0) - m.z2 = Var(initialize=0) - m.working_model = ConcreteModel() - m.working_model.util = Block() + # uncertain parameters + m.p = Param(range(3), initialize=0, mutable=True) - m.working_model.util.second_stage_variables = [m.z1, m.z2] - m.working_model.util.uncertain_params = [m.p1, m.p2] - m.working_model.util.first_stage_variables = [] + # second-stage variables + m.z = Var([0, 1], initialize=0) - m.working_model.util.first_stage_variables = [] - config = Block() + # util block + m.util = Block() + m.util.first_stage_variables = [] + m.util.second_stage_variables = list(m.z.values()) + m.util.uncertain_params = list(m.p.values()) + + return m + + @unittest.skipIf(not scipy_available, 'Scipy is not available.') + def test_correct_num_dr_vars_static(self): + """ + Test DR variable setup routines declare the correct + number of DR coefficient variables, static DR case. + """ + model_data = ROSolveResults() + model_data.working_model = m = self.make_simple_test_model() + + config = Bunch() config.decision_rule_order = 0 - add_decision_rule_variables(model_data=m, config=config) + add_decision_rule_variables(model_data=model_data, config=config) + + for indexed_dr_var in m.util.decision_rule_vars: + self.assertEqual( + len(indexed_dr_var), + 1, + msg=( + "Number of decision rule coefficient variables " + f"in indexed Var object {indexed_dr_var.name!r}" + "does not match correct value." + ), + ) self.assertEqual( - len(m.working_model.util.first_stage_variables), - len(m.working_model.util.second_stage_variables), - msg="For static approximation decision rule the number of decision rule variables" - "added to the list of design variables should equal the number of control variables.", + len(ComponentSet(m.util.decision_rule_vars)), + len(m.util.second_stage_variables), + msg=( + "Number of unique indexed DR variable components should equal " + "number of second-stage variables." + ), ) - m.working_model.util.first_stage_variables = [] - - m.working_model.del_component(m.working_model.decision_rule_var_0) - m.working_model.del_component(m.working_model.decision_rule_var_1) + @unittest.skipIf(not scipy_available, 'Scipy is not available.') + def test_correct_num_dr_vars_affine(self): + """ + Test DR variable setup routines declare the correct + number of DR coefficient variables, affine DR case. + """ + model_data = ROSolveResults() + model_data.working_model = m = self.make_simple_test_model() + config = Bunch() config.decision_rule_order = 1 - add_decision_rule_variables(m, config=config) + add_decision_rule_variables(model_data=model_data, config=config) + + for indexed_dr_var in m.util.decision_rule_vars: + self.assertEqual( + len(indexed_dr_var), + 1 + len(m.util.uncertain_params), + msg=( + "Number of decision rule coefficient variables " + f"in indexed Var object {indexed_dr_var.name!r}" + "does not match correct value." + ), + ) self.assertEqual( - len(m.working_model.util.first_stage_variables), - len(m.working_model.util.second_stage_variables) - * (1 + len(m.working_model.util.uncertain_params)), - msg="For affine decision rule the number of decision rule variables add to the " - "list of design variables should equal the number of control variables" - "multiplied by the number of uncertain parameters plus 1.", + len(ComponentSet(m.util.decision_rule_vars)), + len(m.util.second_stage_variables), + msg=( + "Number of unique indexed DR variable components should equal " + "number of second-stage variables." + ), ) - m.working_model.util.first_stage_variables = [] - - m.working_model.del_component(m.working_model.decision_rule_var_0) - m.working_model.del_component(m.working_model.decision_rule_var_1) - m.working_model.del_component(m.working_model.decision_rule_var_0_index) - m.working_model.del_component(m.working_model.decision_rule_var_1_index) + @unittest.skipIf(not scipy_available, 'Scipy is not available.') + def test_correct_num_dr_vars_quadratic(self): + """ + Test DR variable setup routines declare the correct + number of DR coefficient variables, quadratic DR case. + """ + model_data = ROSolveResults() + model_data.working_model = m = self.make_simple_test_model() + config = Bunch() config.decision_rule_order = 2 - add_decision_rule_variables(m, config=config) + add_decision_rule_variables(model_data=model_data, config=config) + + num_params = len(m.util.uncertain_params) + correct_num_dr_vars = ( + 1 # static term + + num_params # affine terms + + sp.special.comb(num_params, 2, repetition=True, exact=True) + # quadratic terms + ) + for indexed_dr_var in m.util.decision_rule_vars: + self.assertEqual( + len(indexed_dr_var), + correct_num_dr_vars, + msg=( + "Number of decision rule coefficient variables " + f"in indexed Var object {indexed_dr_var.name!r}" + "does not match correct value." + ), + ) self.assertEqual( - len(m.working_model.util.first_stage_variables), - len(m.working_model.util.second_stage_variables) - * int( - 2 * len(m.working_model.util.uncertain_params) - + sp.special.comb(N=len(m.working_model.util.uncertain_params), k=2) - + 1 + len(ComponentSet(m.util.decision_rule_vars)), + len(m.util.second_stage_variables), + msg=( + "Number of unique indexed DR variable components should equal " + "number of second-stage variables." ), - msg="For quadratic decision rule the number of decision rule variables add to the " - "list of design variables should equal the number of control variables" - "multiplied by 2 time the number of uncertain parameters plus all 2-combinations" - "of uncertain parameters plus 1.", ) class testAddDecisionRuleConstraints(unittest.TestCase): - ''' - Testing the addition of decision rule constraints functionally relating second-stage (control) variables to - uncertain parameters and decision rule variables. This method should add constraints to the model object equal - to the number of control variables. These constraints should reference the uncertain parameters and unique - decision rule variables per control variable. - ''' + """ + Test method for adding decision rule equality constraints + to the working model. There should be as many decision + rule equality constraints as there are second-stage + variables, and each constraint should relate a second-stage + variable to the uncertain parameters and corresponding + decision rule variables. + """ - def test_correct_number_of_decision_rule_constraints(self): - ''' - Number of decision rule constraints added to the model should equal number of control variables in - list "second_stage_variables". - ''' + def make_simple_test_model(self): + """ + Make simple model for DR constraint testing. + """ m = ConcreteModel() - m.p1 = Param(initialize=0, mutable=True) - m.p2 = Param(initialize=0, mutable=True) - m.z1 = Var(initialize=0) - m.z2 = Var(initialize=0) - m.working_model = ConcreteModel() - m.working_model.util = Block() + # uncertain parameters + m.p = Param(range(3), initialize=0, mutable=True) - # === Decision rule vars have been added - m.working_model.decision_rule_var_0 = Var(initialize=0) - m.working_model.decision_rule_var_1 = Var(initialize=0) + # second-stage variables + m.z = Var([0, 1], initialize=0) - m.working_model.util.second_stage_variables = [m.z1, m.z2] - m.working_model.util.uncertain_params = [m.p1, m.p2] + # util block + m.util = Block() + m.util.first_stage_variables = [] + m.util.second_stage_variables = list(m.z.values()) + m.util.uncertain_params = list(m.p.values()) - decision_rule_cons = [] - config = Block() - config.decision_rule_order = 0 + return m - add_decision_rule_constraints(model_data=m, config=config) + @unittest.skipIf(not scipy_available, 'Scipy is not available.') + def test_num_dr_eqns_added_correct(self): + """ + Check that number of DR equality constraints added + by constraint declaration routines matches the number + of second-stage variables in the model. + """ + model_data = ROSolveResults() + model_data.working_model = m = self.make_simple_test_model() + + # === Decision rule vars have been added + m.decision_rule_var_0 = Var([0], initialize=0) + m.decision_rule_var_1 = Var([0], initialize=0) + m.util.decision_rule_vars = [m.decision_rule_var_0, m.decision_rule_var_1] + + # set up simple config-like object + config = Bunch() + config.decision_rule_order = 0 - for c in m.working_model.component_data_objects(Constraint, descend_into=True): - if "decision_rule_eqn_" in c.name: - decision_rule_cons.append(c) - m.working_model.del_component(c) + add_decision_rule_constraints(model_data=model_data, config=config) self.assertEqual( - len(decision_rule_cons), - len(m.working_model.util.second_stage_variables), + len(m.util.decision_rule_eqns), + len(m.util.second_stage_variables), msg="The number of decision rule constraints added to model should equal" "the number of control variables in the model.", ) - decision_rule_cons = [] - config.decision_rule_order = 1 + @unittest.skipIf(not scipy_available, 'Scipy is not available.') + def test_dr_eqns_form_correct(self): + """ + Check that form of decision rule equality constraints + is as expected. - # === Decision rule vars have been added - m.working_model.del_component(m.working_model.decision_rule_var_0) - m.working_model.del_component(m.working_model.decision_rule_var_1) + Decision rule equations should be of the standard form: + (sum of DR monomial terms) - (second-stage variable) == 0 + where each monomial term should be of form: + (product of uncertain parameters) * (decision rule variable) - m.working_model.decision_rule_var_0 = Var([0, 1, 2], initialize=0) - m.working_model.decision_rule_var_1 = Var([0, 1, 2], initialize=0) + This test checks that the equality constraints are of this + standard form. + """ + # set up simple model data like object + model_data = ROSolveResults() + model_data.working_model = m = self.make_simple_test_model() - add_decision_rule_constraints(model_data=m, config=config) + # set up simple config-like object + config = Bunch() + config.decision_rule_order = 2 - for c in m.working_model.component_data_objects(Constraint, descend_into=True): - if "decision_rule_eqn_" in c.name: - decision_rule_cons.append(c) - m.working_model.del_component(c) + # add DR variables and constraints + add_decision_rule_variables(model_data, config) + add_decision_rule_constraints(model_data, config) + + # DR polynomial terms and order in which they should + # appear depends on number of uncertain parameters + # and order in which the parameters are listed. + # so uncertain parameters participating in each term + # of the monomial is known, and listed out here. + dr_monomial_param_combos = [ + (1,), + (m.p[0],), + (m.p[1],), + (m.p[2],), + (m.p[0], m.p[0]), + (m.p[0], m.p[1]), + (m.p[0], m.p[2]), + (m.p[1], m.p[1]), + (m.p[1], m.p[2]), + (m.p[2], m.p[2]), + ] - self.assertEqual( - len(decision_rule_cons), - len(m.working_model.util.second_stage_variables), - msg="The number of decision rule constraints added to model should equal" - "the number of control variables in the model.", + dr_zip = zip( + m.util.second_stage_variables, + m.util.decision_rule_vars, + m.util.decision_rule_eqns, ) + for ss_var, indexed_dr_var, dr_eq in dr_zip: + dr_eq_terms = dr_eq.body.args - decision_rule_cons = [] - config.decision_rule_order = 2 - - # === Decision rule vars have been added - m.working_model.del_component(m.working_model.decision_rule_var_0) - m.working_model.del_component(m.working_model.decision_rule_var_1) - m.working_model.del_component(m.working_model.decision_rule_var_0_index) - m.working_model.del_component(m.working_model.decision_rule_var_1_index) - - m.working_model.decision_rule_var_0 = Var([0, 1, 2, 3, 4, 5], initialize=0) - m.working_model.decision_rule_var_1 = Var([0, 1, 2, 3, 4, 5], initialize=0) + # check constraint body is sum expression + self.assertTrue( + isinstance(dr_eq.body, SumExpression), + msg=( + f"Body of DR constraint {dr_eq.name!r} is not of type " + f"{SumExpression.__name__}." + ), + ) - add_decision_rule_constraints(model_data=m, config=config) + # ensure DR equation has correct number of (additive) terms + self.assertEqual( + len(dr_eq_terms), + len(dr_monomial_param_combos) + 1, + msg=( + "Number of additive terms in the DR expression of " + f"DR constraint with name {dr_eq.name!r} does not match " + "expected value." + ), + ) - for c in m.working_model.component_data_objects(Constraint, descend_into=True): - if "decision_rule_eqn_" in c.name: - decision_rule_cons.append(c) - m.working_model.del_component(c) + # check last term is negative of second-stage variable + second_stage_var_term = dr_eq_terms[-1] + last_term_is_neg_ss_var = ( + isinstance(second_stage_var_term, MonomialTermExpression) + and (second_stage_var_term.args[0] == -1) + and (second_stage_var_term.args[1] is ss_var) + and len(second_stage_var_term.args) == 2 + ) + self.assertTrue( + last_term_is_neg_ss_var, + msg=( + "Last argument of last term in second-stage variable" + f"term of DR constraint with name {dr_eq.name!r} " + "is not the negative corresponding second-stage variable " + f"{ss_var.name!r}" + ), + ) - self.assertEqual( - len(decision_rule_cons), - len(m.working_model.util.second_stage_variables), - msg="The number of decision rule constraints added to model should equal" - "the number of control variables in the model.", - ) + # now we check the other terms. + # these should comprise the DR polynomial expression + dr_polynomial_terms = dr_eq_terms[:-1] + dr_polynomial_zip = zip( + dr_polynomial_terms, indexed_dr_var.values(), dr_monomial_param_combos + ) + for idx, (term, dr_var, param_combo) in enumerate(dr_polynomial_zip): + # term should be either a monomial expression or scalar variable + if isinstance(term, MonomialTermExpression): + # should be of form (uncertain parameter product) * + # (decision rule variable) so length of expression + # object should be 2 + self.assertEqual( + len(term.args), + 2, + msg=( + f"Length of `args` attribute of term {str(term)} " + f"of DR equation {dr_eq.name!r} is not as expected. " + f"Args: {term.args}" + ), + ) + # check that uncertain parameters participating in + # the monomial are as expected + param_product_multiplicand = term.args[0] + dr_var_multiplicand = term.args[1] + else: + self.assertIsInstance(term, VarData) + param_product_multiplicand = 1 + dr_var_multiplicand = term + + if idx == 0: + # static DR term + param_combo_found_in_term = (param_product_multiplicand,) + param_names = (str(param) for param in param_combo) + elif len(param_combo) == 1: + # affine DR terms + param_combo_found_in_term = (param_product_multiplicand,) + param_names = (param.name for param in param_combo) + else: + # higher-order DR terms + param_combo_found_in_term = param_product_multiplicand.args + param_names = (param.name for param in param_combo) + + self.assertEqual( + param_combo_found_in_term, + param_combo, + msg=( + f"All but last multiplicand of DR monomial {str(term)} " + f"is not the uncertain parameter tuple " + f"({', '.join(param_names)})." + ), + ) -class testModelIsValid(unittest.TestCase): - def test_model_is_valid_via_possible_inputs(self): - m = ConcreteModel() - m.x = Var() - m.obj1 = Objective(expr=m.x**2) - self.assertTrue(model_is_valid(m)) - m.obj2 = Objective(expr=m.x) - self.assertFalse(model_is_valid(m)) - m.obj2.deactivate() - self.assertTrue(model_is_valid(m)) - m.del_component("obj1") - m.del_component("obj2") - self.assertFalse(model_is_valid(m)) + # check that DR variable participating in the monomial + # is as expected + self.assertIs( + dr_var_multiplicand, + dr_var, + msg=( + f"Last multiplicand of DR monomial {str(term)} " + f"is not the DR variable {dr_var.name!r}." + ), + ) class testTurnBoundsToConstraints(unittest.TestCase): @@ -3349,10 +3555,7 @@ class behaves like a regular Python list. # assigning to slices should work fine all_sets[3:] = [BoxSet([[1, 1.5]]), BoxSet([[1, 3]])] - @unittest.skipUnless( - SolverFactory('ipopt').available(exception_flag=False), - "Local NLP solver is not available.", - ) + @unittest.skipUnless(ipopt_available, "IPOPT is not available.") def test_uncertainty_set_with_correct_params(self): ''' Case in which the UncertaintySet is constructed using the uncertain_param objects from the model to @@ -3391,10 +3594,7 @@ def test_uncertainty_set_with_correct_params(self): " be the same uncertain param Var objects in the original model.", ) - @unittest.skipUnless( - SolverFactory('ipopt').available(exception_flag=False), - "Local NLP solver is not available.", - ) + @unittest.skipUnless(ipopt_available, "IPOPT is not available.") def test_uncertainty_set_with_incorrect_params(self): ''' Case in which the set is constructed using uncertain_param objects which are Params instead of @@ -3566,8 +3766,11 @@ def test_solve_master(self): master_data.master_model.scenarios[0, 0].second_stage_objective = Expression( expr=master_data.master_model.scenarios[0, 0].x ) + master_data.master_model.scenarios[0, 0].util.dr_var_to_exponent_map = ( + ComponentMap() + ) master_data.iteration = 0 - master_data.timing = Bunch() + master_data.timing = TimingData() box_set = BoxSet(bounds=[(0, 2)]) solver = SolverFactory(global_solver) @@ -3589,8 +3792,12 @@ def test_solve_master(self): ) config.declare("subproblem_file_directory", ConfigValue(default=None)) config.declare("time_limit", ConfigValue(default=None)) + config.declare( + "progress_logger", ConfigValue(default=logging.getLogger(__name__)) + ) + config.declare("symbolic_solver_labels", ConfigValue(default=False)) - with time_code(master_data.timing, "total", is_main_timer=True): + with time_code(master_data.timing, "main", is_main_timer=True): master_soln = solve_master(master_data, config) self.assertEqual( master_soln.termination_condition, @@ -3866,7 +4073,7 @@ def test_minimize_dr_norm(self): m.working_model.util.state_vars = [] m.working_model.util.first_stage_variables = [] - config = Block() + config = Bunch() config.decision_rule_order = 1 config.objective_focus = ObjectiveType.nominal config.global_solver = SolverFactory('baron') @@ -3874,6 +4081,7 @@ def test_minimize_dr_norm(self): config.tee = False config.solve_master_globally = True config.time_limit = None + config.progress_logger = logging.getLogger(__name__) add_decision_rule_variables(model_data=m, config=config) add_decision_rule_constraints(model_data=m, config=config) @@ -3892,15 +4100,19 @@ def test_minimize_dr_norm(self): master_data.master_model = master master_data.master_model.const_efficiency_applied = False master_data.master_model.linear_efficiency_applied = False + master_data.iteration = 0 - master_data.timing = Bunch() - with time_code(master_data.timing, "total", is_main_timer=True): - results = minimize_dr_vars(model_data=master_data, config=config) + master_data.timing = TimingData() + with time_code(master_data.timing, "main", is_main_timer=True): + results, success = minimize_dr_vars(model_data=master_data, config=config) self.assertEqual( results.solver.termination_condition, TerminationCondition.optimal, msg="Minimize dr norm did not solve to optimality.", ) + self.assertTrue( + success, msg=f"DR polishing success {success}, expected True." + ) @unittest.skipUnless( baron_license_is_valid, "Global NLP solver is not available and licensed." @@ -3957,7 +4169,8 @@ def test_identifying_violating_param_realization(self): baron_license_is_valid, "Global NLP solver is not available and licensed." ) @unittest.skipUnless( - baron_version < (23, 1, 5), "Test known to fail beginning with Baron 23.1.5" + baron_version < (23, 1, 5) or baron_version >= (23, 6, 23), + "Test known to fail for BARON 23.1.5 and versions preceding 23.6.23", ) def test_terminate_with_max_iter(self): m = ConcreteModel() @@ -4004,6 +4217,15 @@ def test_terminate_with_max_iter(self): msg="Returned termination condition is not return max_iter.", ) + self.assertEqual( + results.iterations, + 1, + msg=( + f"Number of iterations in results object is {results.iterations}, " + f"but expected value 1." + ), + ) + @unittest.skipUnless( baron_license_is_valid, "Global NLP solver is not available and licensed." ) @@ -4120,14 +4342,16 @@ def test_separation_terminate_time_limit(self): ) @unittest.skipUnless( - SolverFactory('gams').license_is_valid() - and SolverFactory('baron').license_is_valid(), - "Global NLP solver is not available and licensed.", + ipopt_available + and SolverFactory('gams').license_is_valid() + and SolverFactory('baron').license_is_valid() + and SolverFactory("scip").license_is_valid(), + "IPOPT not available or one of GAMS/BARON/SCIP not licensed", ) - def test_gams_successful_time_limit(self): + def test_pyros_subsolver_time_limit_adjustment(self): """ - Test PyROS time limit status returned in event - separation problem times out. + Check that PyROS does not ultimately alter state of + subordinate solver options due to time limit adjustments. """ m = ConcreteModel() m.x1 = Var(initialize=0, bounds=(0, None)) @@ -4146,20 +4370,26 @@ def test_gams_successful_time_limit(self): # Instantiate the PyROS solver pyros_solver = SolverFactory("pyros") - # Define subsolvers utilized in the algorithm - # two GAMS solvers, one of which has reslim set - # (overridden when invoked in PyROS) + # subordinate solvers to test. + # for testing, we pass each as the 'local' solver, + # and the BARON solver without custom options + # as the 'global' solver + baron_no_options = SolverFactory("baron") local_subsolvers = [ SolverFactory("gams:conopt"), SolverFactory("gams:conopt"), SolverFactory("ipopt"), + SolverFactory("ipopt", options={"max_cpu_time": 300}), + SolverFactory("scip"), + SolverFactory("scip", options={"limits/time": 300}), + baron_no_options, + SolverFactory("baron", options={"MaxTime": 300}), ] local_subsolvers[0].options["add_options"] = ["option reslim=100;"] - global_subsolver = SolverFactory("baron") - global_subsolver.options["MaxTime"] = 300 # Call the PyROS solver for idx, opt in enumerate(local_subsolvers): + original_solver_options = opt.options.copy() results = pyros_solver.solve( model=m, first_stage_variables=[m.x1, m.x2], @@ -4167,68 +4397,25 @@ def test_gams_successful_time_limit(self): uncertain_params=[m.u], uncertainty_set=interval, local_solver=opt, - global_solver=global_subsolver, + global_solver=baron_no_options, objective_focus=ObjectiveType.worst_case, solve_master_globally=True, time_limit=100, ) - self.assertEqual( results.pyros_termination_condition, pyrosTerminationCondition.robust_optimal, msg=( - f"Returned termination condition with local " - "subsolver {idx + 1} of 2 is not robust_optimal." + "Returned termination condition with local " + f"subsolver {idx + 1} of 2 is not robust_optimal." ), ) - - # check first local subsolver settings - # remain unchanged after PyROS exit - self.assertEqual( - len(list(local_subsolvers[0].options["add_options"])), - 1, - msg=( - f"Local subsolver {local_subsolvers[0]} options 'add_options'" - "were changed by PyROS" - ), - ) - self.assertEqual( - local_subsolvers[0].options["add_options"][0], - "option reslim=100;", - msg=( - f"Local subsolver {local_subsolvers[0]} setting " - "'add_options' was modified " - "by PyROS, but changes were not properly undone" - ), - ) - - # check global subsolver settings unchanged - self.assertEqual( - len(list(global_subsolver.options.keys())), - 1, - msg=(f"Global subsolver {global_subsolver} options were changed by PyROS"), - ) - self.assertEqual( - global_subsolver.options["MaxTime"], - 300, - msg=( - f"Global subsolver {global_subsolver} setting " - "'MaxTime' was modified " - "by PyROS, but changes were not properly undone" - ), - ) - - # check other local subsolvers remain unchanged - for slvr, key in zip(local_subsolvers[1:], ["add_options", "max_cpu_time"]): - # no custom options were added to the `options` - # attribute of the optimizer, so any attribute - # of `options` should be `None` - self.assertIs( - getattr(slvr.options, key, None), - None, + self.assertEqual( + opt.options, + original_solver_options, msg=( - f"Local subsolver {slvr} setting '{key}' was added " - "by PyROS, but not reverted" + f"Options for subordinate solver {opt} were changed " + "by PyROS, and the changes wee not properly reverted." ), ) @@ -4655,9 +4842,7 @@ def test_higher_order_decision_rules(self): msg="Returned termination condition is not return robust_optimal.", ) - @unittest.skipUnless( - baron_license_is_valid, "Global NLP solver is not available and licensed." - ) + @unittest.skipUnless(scip_available, "Global NLP solver is not available.") def test_coefficient_matching_solve(self): # Write the deterministic Pyomo model m = ConcreteModel() @@ -4681,8 +4866,8 @@ def test_coefficient_matching_solve(self): pyros_solver = SolverFactory("pyros") # Define subsolvers utilized in the algorithm - local_subsolver = SolverFactory('baron') - global_subsolver = SolverFactory("baron") + local_subsolver = SolverFactory('scip') + global_subsolver = SolverFactory("scip") # Call the PyROS solver results = pyros_solver.solve( @@ -4788,10 +4973,7 @@ def test_coeff_matching_solver_insensitive(self): ), ) - @unittest.skipUnless( - baron_license_is_valid and baron_version >= (23, 2, 27), - "BARON licensing and version requirements not met", - ) + @unittest.skipUnless(scip_available, "NLP solver is not available.") def test_coefficient_matching_partitioning_insensitive(self): """ Check that result for instance with constraint subject to @@ -4802,7 +4984,7 @@ def test_coefficient_matching_partitioning_insensitive(self): m = self.create_mitsos_4_3() # instantiate BARON subsolver and PyROS solver - baron = SolverFactory("baron") + baron = SolverFactory("scip") pyros_solver = SolverFactory("pyros") # solve with PyROS @@ -4864,14 +5046,16 @@ def test_coefficient_matching_raises_error_4_3(self): # solve with PyROS dr_orders = [1, 2] for dr_order in dr_orders: - with self.assertRaisesRegex( + regex_assert_mgr = self.assertRaisesRegex( ValueError, expected_regex=( - "Equality constraint.*cannot be guaranteed to be robustly " - "feasible.*" + "Coefficient matching unsuccessful. See the solver logs." ), - ): - res = pyros_solver.solve( + ) + logging_intercept_mgr = LoggingIntercept(level=logging.ERROR) + + with regex_assert_mgr, logging_intercept_mgr as LOG: + pyros_solver.solve( model=m, first_stage_variables=[], second_stage_variables=[m.x1, m.x2, m.x3], @@ -4886,6 +5070,16 @@ def test_coefficient_matching_raises_error_4_3(self): robust_feasibility_tolerance=1e-4, ) + detailed_error_msg = LOG.getvalue() + self.assertRegex( + detailed_error_msg[:-1], + ( + r"Equality constraint.*cannot be guaranteed to " + r"be robustly feasible.*" + r"Consider editing this constraint.*" + ), + ) + def test_coefficient_matching_robust_infeasible_proof_in_pyros(self): # Write the deterministic Pyomo model m = ConcreteModel() @@ -4982,10 +5176,7 @@ def test_coefficient_matching_nonlinear_expr(self): ) -@unittest.skipUnless( - baron_available and baron_license_is_valid, - "Global NLP solver is not available and licensed.", -) +@unittest.skipUnless(scip_available, "Global NLP solver is not available.") class testBypassingSeparation(unittest.TestCase): def test_bypass_global_separation(self): """Test bypassing of global separation solve calls.""" @@ -5008,31 +5199,45 @@ def test_bypass_global_separation(self): # Define subsolvers utilized in the algorithm local_subsolver = SolverFactory('ipopt') - global_subsolver = SolverFactory("baron") + global_subsolver = SolverFactory("scip") # Call the PyROS solver - results = pyros_solver.solve( - model=m, - first_stage_variables=[m.x1], - second_stage_variables=[m.x2], - uncertain_params=[m.u], - uncertainty_set=interval, - local_solver=local_subsolver, - global_solver=global_subsolver, - options={ - "objective_focus": ObjectiveType.worst_case, - "solve_master_globally": True, - "decision_rule_order": 0, - "bypass_global_separation": True, - }, - ) + with LoggingIntercept(level=logging.WARNING) as LOG: + results = pyros_solver.solve( + model=m, + first_stage_variables=[m.x1], + second_stage_variables=[m.x2], + uncertain_params=[m.u], + uncertainty_set=interval, + local_solver=local_subsolver, + global_solver=global_subsolver, + options={ + "objective_focus": ObjectiveType.worst_case, + "solve_master_globally": True, + "decision_rule_order": 0, + "bypass_global_separation": True, + }, + ) + # check termination robust optimal self.assertEqual( results.pyros_termination_condition, pyrosTerminationCondition.robust_optimal, msg="Returned termination condition is not return robust_optimal.", ) + # since robust optimal, we also expect warning-level logger + # message about bypassing of global separation subproblems + warning_msgs = LOG.getvalue() + self.assertRegex( + warning_msgs, + ( + r".*Option to bypass global separation was chosen\. " + r"Robust feasibility and optimality of the reported " + r"solution are not guaranteed\." + ), + ) + @unittest.skipUnless( baron_available and baron_license_is_valid, @@ -5113,10 +5318,7 @@ def test_uninitialized_vars(self): ) -@unittest.skipUnless( - baron_available and baron_license_is_valid, - "Global NLP solver is not available and licensed.", -) +@unittest.skipUnless(scip_available, "Global NLP solver is not available.") class testModelMultipleObjectives(unittest.TestCase): """ This class contains tests for models with multiple @@ -5151,7 +5353,7 @@ def test_multiple_objs(self): # Define subsolvers utilized in the algorithm local_subsolver = SolverFactory('ipopt') - global_subsolver = SolverFactory("baron") + global_subsolver = SolverFactory("scip") solve_kwargs = dict( model=m, @@ -5170,16 +5372,14 @@ def test_multiple_objs(self): # check validation error raised due to multiple objectives with self.assertRaisesRegex( - AttributeError, - "This model structure is not currently handled by the ROSolver.", + ValueError, r"Expected model with exactly 1 active objective.*has 3" ): pyros_solver.solve(**solve_kwargs) # check validation error raised due to multiple objectives m.b.obj.deactivate() with self.assertRaisesRegex( - AttributeError, - "This model structure is not currently handled by the ROSolver.", + ValueError, r"Expected model with exactly 1 active objective.*has 2" ): pyros_solver.solve(**solve_kwargs) @@ -5199,12 +5399,26 @@ def test_multiple_objs(self): # and solve again m.obj_max = Objective(expr=-m.obj.expr, sense=pyo_max) m.obj.deactivate() - res = pyros_solver.solve(**solve_kwargs) + max_obj_res = pyros_solver.solve(**solve_kwargs) # check active objectives self.assertEqual(len(list(m.component_data_objects(Objective, active=True))), 1) self.assertTrue(m.obj_max.active) + self.assertTrue( + math.isclose( + res.final_objective_value, + -max_obj_res.final_objective_value, + abs_tol=2e-4, # 2x the default robust feasibility tolerance + ), + msg=( + f"Robust optimal objective value {res.final_objective_value} " + "for problem with minimization objective not close to " + f"negative of value {max_obj_res.final_objective_value} " + "of equivalent maximization objective." + ), + ) + class testModelIdentifyObjectives(unittest.TestCase): """ @@ -5631,5 +5845,1020 @@ def test_two_stg_mod_with_intersection_set(self): ) +class TestIterationLogRecord(unittest.TestCase): + """ + Test the PyROS `IterationLogRecord` class. + """ + + def test_log_header(self): + """Test method for logging iteration log table header.""" + ans = ( + "------------------------------------------------------------------------------\n" + "Itn Objective 1-Stg Shift 2-Stg Shift #CViol Max Viol Wall Time (s)\n" + "------------------------------------------------------------------------------\n" + ) + with LoggingIntercept(level=logging.INFO) as LOG: + IterationLogRecord.log_header(logger.info) + + self.assertEqual( + LOG.getvalue(), + ans, + msg="Messages logged for iteration table header do not match expected result", + ) + + def test_log_standard_iter_record(self): + """Test logging function for PyROS IterationLogRecord.""" + + # for some fields, we choose floats with more than four + # four decimal points to ensure rounding also matches + iter_record = IterationLogRecord( + iteration=4, + objective=1.234567, + first_stage_var_shift=2.3456789e-8, + second_stage_var_shift=3.456789e-7, + dr_var_shift=1.234567e-7, + num_violated_cons=10, + max_violation=7.654321e-3, + elapsed_time=21.2, + dr_polishing_success=True, + all_sep_problems_solved=True, + global_separation=False, + ) + + # now check record logged as expected + ans = ( + "4 1.2346e+00 2.3457e-08 3.4568e-07 10 7.6543e-03 " + "21.200 \n" + ) + with LoggingIntercept(level=logging.INFO) as LOG: + iter_record.log(logger.info) + result = LOG.getvalue() + + self.assertEqual( + ans, + result, + msg="Iteration log record message does not match expected result", + ) + + def test_log_iter_record_polishing_failed(self): + """Test iteration log record in event of polishing failure.""" + # for some fields, we choose floats with more than four + # four decimal points to ensure rounding also matches + iter_record = IterationLogRecord( + iteration=4, + objective=1.234567, + first_stage_var_shift=2.3456789e-8, + second_stage_var_shift=3.456789e-7, + dr_var_shift=1.234567e-7, + num_violated_cons=10, + max_violation=7.654321e-3, + elapsed_time=21.2, + dr_polishing_success=False, + all_sep_problems_solved=True, + global_separation=False, + ) + + # now check record logged as expected + ans = ( + "4 1.2346e+00 2.3457e-08 3.4568e-07* 10 7.6543e-03 " + "21.200 \n" + ) + with LoggingIntercept(level=logging.INFO) as LOG: + iter_record.log(logger.info) + result = LOG.getvalue() + + self.assertEqual( + ans, + result, + msg="Iteration log record message does not match expected result", + ) + + def test_log_iter_record_global_separation(self): + """ + Test iteration log record in event global separation performed. + In this case, a 'g' should be appended to the max violation + reported. Useful in the event neither local nor global separation + was bypassed. + """ + # for some fields, we choose floats with more than four + # four decimal points to ensure rounding also matches + iter_record = IterationLogRecord( + iteration=4, + objective=1.234567, + first_stage_var_shift=2.3456789e-8, + second_stage_var_shift=3.456789e-7, + dr_var_shift=1.234567e-7, + num_violated_cons=10, + max_violation=7.654321e-3, + elapsed_time=21.2, + dr_polishing_success=True, + all_sep_problems_solved=True, + global_separation=True, + ) + + # now check record logged as expected + ans = ( + "4 1.2346e+00 2.3457e-08 3.4568e-07 10 7.6543e-03g " + "21.200 \n" + ) + with LoggingIntercept(level=logging.INFO) as LOG: + iter_record.log(logger.info) + result = LOG.getvalue() + + self.assertEqual( + ans, + result, + msg="Iteration log record message does not match expected result", + ) + + def test_log_iter_record_not_all_sep_solved(self): + """ + Test iteration log record in event not all separation problems + were solved successfully. This may have occurred if the PyROS + solver time limit was reached, or the user-provides subordinate + optimizer(s) were unable to solve a separation subproblem + to an acceptable level. + A '+' should be appended to the number of performance constraints + found to be violated. + """ + # for some fields, we choose floats with more than four + # four decimal points to ensure rounding also matches + iter_record = IterationLogRecord( + iteration=4, + objective=1.234567, + first_stage_var_shift=2.3456789e-8, + second_stage_var_shift=3.456789e-7, + dr_var_shift=1.234567e-7, + num_violated_cons=10, + max_violation=7.654321e-3, + elapsed_time=21.2, + dr_polishing_success=True, + all_sep_problems_solved=False, + global_separation=False, + ) + + # now check record logged as expected + ans = ( + "4 1.2346e+00 2.3457e-08 3.4568e-07 10+ 7.6543e-03 " + "21.200 \n" + ) + with LoggingIntercept(level=logging.INFO) as LOG: + iter_record.log(logger.info) + result = LOG.getvalue() + + self.assertEqual( + ans, + result, + msg="Iteration log record message does not match expected result", + ) + + def test_log_iter_record_all_special(self): + """ + Test iteration log record in event DR polishing and global + separation failed. + """ + # for some fields, we choose floats with more than four + # four decimal points to ensure rounding also matches + iter_record = IterationLogRecord( + iteration=4, + objective=1.234567, + first_stage_var_shift=2.3456789e-8, + second_stage_var_shift=3.456789e-7, + dr_var_shift=1.234567e-7, + num_violated_cons=10, + max_violation=7.654321e-3, + elapsed_time=21.2, + dr_polishing_success=False, + all_sep_problems_solved=False, + global_separation=True, + ) + + # now check record logged as expected + ans = ( + "4 1.2346e+00 2.3457e-08 3.4568e-07* 10+ 7.6543e-03g " + "21.200 \n" + ) + with LoggingIntercept(level=logging.INFO) as LOG: + iter_record.log(logger.info) + result = LOG.getvalue() + + self.assertEqual( + ans, + result, + msg="Iteration log record message does not match expected result", + ) + + def test_log_iter_record_attrs_none(self): + """ + Test logging of iteration record in event some + attributes are of value `None`. In this case, a '-' + should be printed in lieu of a numerical value. + Example where this occurs: the first iteration, + in which there is no first-stage shift or DR shift. + """ + # for some fields, we choose floats with more than four + # four decimal points to ensure rounding also matches + iter_record = IterationLogRecord( + iteration=0, + objective=-1.234567, + first_stage_var_shift=None, + second_stage_var_shift=None, + dr_var_shift=None, + num_violated_cons=10, + max_violation=7.654321e-3, + elapsed_time=21.2, + dr_polishing_success=True, + all_sep_problems_solved=False, + global_separation=True, + ) + + # now check record logged as expected + ans = ( + "0 -1.2346e+00 - - 10+ 7.6543e-03g " + "21.200 \n" + ) + with LoggingIntercept(level=logging.INFO) as LOG: + iter_record.log(logger.info) + result = LOG.getvalue() + + self.assertEqual( + ans, + result, + msg="Iteration log record message does not match expected result", + ) + + +class TestROSolveResults(unittest.TestCase): + """ + Test PyROS solver results object. + """ + + def test_ro_solve_results_str(self): + """ + Test string representation of RO solve results object. + """ + res = ROSolveResults( + config=SolverFactory("pyros").CONFIG(), + iterations=4, + final_objective_value=123.456789, + time=300.34567, + pyros_termination_condition=pyrosTerminationCondition.robust_optimal, + ) + ans = ( + "Termination stats:\n" + " Iterations : 4\n" + " Solve time (wall s) : 300.346\n" + " Final objective value : 1.2346e+02\n" + " Termination condition : pyrosTerminationCondition.robust_optimal" + ) + self.assertEqual( + str(res), + ans, + msg=( + "String representation of PyROS results object does not " + "match expected value" + ), + ) + + def test_ro_solve_results_str_attrs_none(self): + """ + Test string representation of PyROS solve results in event + one of the printed attributes is of value `None`. + This may occur at instantiation or, for example, + whenever the PyROS solver confirms robust infeasibility through + coefficient matching. + """ + res = ROSolveResults( + config=SolverFactory("pyros").CONFIG(), + iterations=0, + final_objective_value=None, + time=300.34567, + pyros_termination_condition=pyrosTerminationCondition.robust_optimal, + ) + ans = ( + "Termination stats:\n" + " Iterations : 0\n" + " Solve time (wall s) : 300.346\n" + " Final objective value : None\n" + " Termination condition : pyrosTerminationCondition.robust_optimal" + ) + self.assertEqual( + str(res), + ans, + msg=( + "String representation of PyROS results object does not " + "match expected value" + ), + ) + + +class TestPyROSSolverLogIntros(unittest.TestCase): + """ + Test logging of introductory information by PyROS solver. + """ + + def test_log_config(self): + """ + Test method for logging PyROS solver config dict. + """ + pyros_solver = SolverFactory("pyros") + config = pyros_solver.CONFIG(dict(nominal_uncertain_param_vals=[0.5])) + with LoggingIntercept(level=logging.INFO) as LOG: + pyros_solver._log_config(logger=logger, config=config, level=logging.INFO) + + ans = ( + "Solver options:\n" + " time_limit=None\n" + " keepfiles=False\n" + " tee=False\n" + " load_solution=True\n" + " symbolic_solver_labels=False\n" + " objective_focus=\n" + " nominal_uncertain_param_vals=[0.5]\n" + " decision_rule_order=0\n" + " solve_master_globally=False\n" + " max_iter=-1\n" + " robust_feasibility_tolerance=0.0001\n" + " separation_priority_order={}\n" + " progress_logger=\n" + " backup_local_solvers=[]\n" + " backup_global_solvers=[]\n" + " subproblem_file_directory=None\n" + " bypass_local_separation=False\n" + " bypass_global_separation=False\n" + " p_robustness={}\n" + "-" * 78 + "\n" + ) + + logged_str = LOG.getvalue() + self.assertEqual( + logged_str, + ans, + msg=( + "Logger output for PyROS solver config (default case) " + "does not match expected result." + ), + ) + + def test_log_intro(self): + """ + Test logging of PyROS solver introductory messages. + """ + pyros_solver = SolverFactory("pyros") + with LoggingIntercept(level=logging.INFO) as LOG: + pyros_solver._log_intro(logger=logger, level=logging.INFO) + + intro_msgs = LOG.getvalue() + + # last character should be newline; disregard it + intro_msg_lines = intro_msgs.split("\n")[:-1] + + # check number of lines is as expected + self.assertEqual( + len(intro_msg_lines), + 14, + msg=( + "PyROS solver introductory message does not contain" + "the expected number of lines." + ), + ) + + # first and last lines of the introductory section + self.assertEqual(intro_msg_lines[0], "=" * 78) + self.assertEqual(intro_msg_lines[-1], "=" * 78) + + # check regex main text + self.assertRegex( + " ".join(intro_msg_lines[1:-1]), + r"PyROS: The Pyomo Robust Optimization Solver, v.* \(IDAES\)\.", + ) + + def test_log_disclaimer(self): + """ + Test logging of PyROS solver disclaimer messages. + """ + pyros_solver = SolverFactory("pyros") + with LoggingIntercept(level=logging.INFO) as LOG: + pyros_solver._log_disclaimer(logger=logger, level=logging.INFO) + + disclaimer_msgs = LOG.getvalue() + + # last character should be newline; disregard it + disclaimer_msg_lines = disclaimer_msgs.split("\n")[:-1] + + # check number of lines is as expected + self.assertEqual( + len(disclaimer_msg_lines), + 5, + msg=( + "PyROS solver disclaimer message does not contain" + "the expected number of lines." + ), + ) + + # regex first line of disclaimer section + self.assertRegex(disclaimer_msg_lines[0], r"=.* DISCLAIMER .*=") + # check last line of disclaimer section + self.assertEqual(disclaimer_msg_lines[-1], "=" * 78) + + # check regex main text + self.assertRegex( + " ".join(disclaimer_msg_lines[1:-1]), + r"PyROS is still under development.*ticket at.*", + ) + + +class UnavailableSolver: + def available(self, exception_flag=True): + if exception_flag: + raise ApplicationError(f"Solver {self.__class__} not available") + return False + + def solve(self, model, *args, **kwargs): + return SolverResults() + + +class TestPyROSUnavailableSubsolvers(unittest.TestCase): + """ + Check that appropriate exceptionsa are raised if + PyROS is invoked with unavailable subsolvers. + """ + + def test_pyros_unavailable_subsolver(self): + """ + Test PyROS raises expected error message when + unavailable subsolver is passed. + """ + m = ConcreteModel() + m.p = Param(range(3), initialize=0, mutable=True) + m.z = Var([0, 1], initialize=0) + m.con = Constraint(expr=m.z[0] + m.z[1] >= m.p[0]) + m.obj = Objective(expr=m.z[0] + m.z[1]) + + pyros_solver = SolverFactory("pyros") + + exc_str = r".*Solver.*UnavailableSolver.*not available" + with self.assertRaisesRegex(ValueError, exc_str): + # note: ConfigDict interface raises ValueError + # once any exception is triggered, + # so we check for that instead of ApplicationError + with LoggingIntercept(level=logging.ERROR) as LOG: + pyros_solver.solve( + model=m, + first_stage_variables=[m.z[0]], + second_stage_variables=[m.z[1]], + uncertain_params=[m.p[0]], + uncertainty_set=BoxSet([[0, 1]]), + local_solver=SimpleTestSolver(), + global_solver=UnavailableSolver(), + ) + + error_msgs = LOG.getvalue()[:-1] + self.assertRegex( + error_msgs, r"Output of `available\(\)` method.*global solver.*" + ) + + @unittest.skipUnless(ipopt_available, "IPOPT is not available.") + def test_pyros_unavailable_backup_subsolver(self): + """ + Test PyROS raises expected error message when + unavailable backup subsolver is passed. + """ + m = ConcreteModel() + m.p = Param(range(3), initialize=0, mutable=True) + m.z = Var([0, 1], initialize=0) + m.con = Constraint(expr=m.z[0] + m.z[1] >= m.p[0]) + m.obj = Objective(expr=m.z[0] + m.z[1]) + + pyros_solver = SolverFactory("pyros") + + # note: ConfigDict interface raises ValueError + # once any exception is triggered, + # so we check for that instead of ApplicationError + with LoggingIntercept(level=logging.WARNING) as LOG: + pyros_solver.solve( + model=m, + first_stage_variables=[m.z[0]], + second_stage_variables=[m.z[1]], + uncertain_params=[m.p[0]], + uncertainty_set=BoxSet([[0, 1]]), + local_solver=SolverFactory("ipopt"), + global_solver=SolverFactory("ipopt"), + backup_global_solvers=[UnavailableSolver()], + bypass_global_separation=True, + ) + + error_msgs = LOG.getvalue()[:-1] + self.assertRegex( + error_msgs, + r"Output of `available\(\)` method.*backup global solver.*" + r"Removing from list.*", + ) + + +class TestPyROSResolveKwargs(unittest.TestCase): + """ + Test PyROS resolves kwargs as expected. + """ + + @unittest.skipUnless(ipopt_available, "IPOPT is not available.") + @unittest.skipUnless( + baron_license_is_valid, "Global NLP solver is not available and licensed." + ) + def test_pyros_kwargs_with_overlap(self): + """ + Test PyROS works as expected when there is overlap between + keyword arguments passed explicitly and implicitly + through `options`. + """ + # define model + m = ConcreteModel() + m.x1 = Var(initialize=0, bounds=(0, None)) + m.x2 = Var(initialize=0, bounds=(0, None)) + m.x3 = Var(initialize=0, bounds=(None, None)) + m.u1 = Param(initialize=1.125, mutable=True) + m.u2 = Param(initialize=1, mutable=True) + + m.con1 = Constraint(expr=m.x1 * m.u1 ** (0.5) - m.x2 * m.u1 <= 2) + m.con2 = Constraint(expr=m.x1**2 - m.x2**2 * m.u1 == m.x3) + + m.obj = Objective(expr=(m.x1 - 4) ** 2 + (m.x2 - m.u2) ** 2) + + # Define the uncertainty set + # we take the parameter `u2` to be 'fixed' + ellipsoid = AxisAlignedEllipsoidalSet(center=[1.125, 1], half_lengths=[1, 0]) + + # Instantiate the PyROS solver + pyros_solver = SolverFactory("pyros") + + # Define subsolvers utilized in the algorithm + local_subsolver = SolverFactory('ipopt') + global_subsolver = SolverFactory("baron") + + # Call the PyROS solver + results = pyros_solver.solve( + model=m, + first_stage_variables=[m.x1, m.x2], + second_stage_variables=[], + uncertain_params=[m.u1, m.u2], + uncertainty_set=ellipsoid, + local_solver=local_subsolver, + global_solver=global_subsolver, + bypass_local_separation=True, + solve_master_globally=True, + options={ + "objective_focus": ObjectiveType.worst_case, + "solve_master_globally": False, + "max_iter": 1, + "time_limit": 1000, + }, + ) + + # check termination status as expected + self.assertEqual( + results.pyros_termination_condition, + pyrosTerminationCondition.max_iter, + msg="Termination condition not as expected", + ) + self.assertEqual( + results.iterations, 1, msg="Number of iterations not as expected" + ) + + # check config resolved as expected + config = results.config + self.assertEqual( + config.bypass_local_separation, + True, + msg="Resolved value of kwarg `bypass_local_separation` not as expected.", + ) + self.assertEqual( + config.solve_master_globally, + True, + msg="Resolved value of kwarg `solve_master_globally` not as expected.", + ) + self.assertEqual( + config.max_iter, + 1, + msg="Resolved value of kwarg `max_iter` not as expected.", + ) + self.assertEqual( + config.objective_focus, + ObjectiveType.worst_case, + msg="Resolved value of kwarg `objective_focus` not as expected.", + ) + self.assertEqual( + config.time_limit, + 1e3, + msg="Resolved value of kwarg `time_limit` not as expected.", + ) + + +class SimpleTestSolver: + """ + Simple test solver class with no actual solve() + functionality. Written to test unrelated aspects + of PyROS functionality. + """ + + def available(self, exception_flag=False): + """ + Check solver available. + """ + return True + + def solve(self, model, **kwds): + """ + Return SolverResults object with 'unknown' termination + condition. Model remains unchanged. + """ + res = SolverResults() + res.solver.termination_condition = TerminationCondition.unknown + + return res + + +class TestPyROSSolverAdvancedValidation(unittest.TestCase): + """ + Test PyROS solver returns expected exception messages + when arguments are invalid. + """ + + def build_simple_test_model(self): + """ + Build simple valid test model. + """ + m = ConcreteModel(name="test_model") + + m.x1 = Var(initialize=0, bounds=(0, None)) + m.x2 = Var(initialize=0, bounds=(0, None)) + m.u = Param(initialize=1.125, mutable=True) + + m.con1 = Constraint(expr=m.x1 * m.u ** (0.5) - m.x2 * m.u <= 2) + + m.obj = Objective(expr=(m.x1 - 4) ** 2 + (m.x2 - 1) ** 2) + + return m + + def test_pyros_invalid_model_type(self): + """ + Test PyROS fails if model is not of correct class. + """ + mdl = self.build_simple_test_model() + + local_solver = SimpleTestSolver() + global_solver = SimpleTestSolver() + + pyros = SolverFactory("pyros") + + exc_str = "Model should be of type.*but is of type.*" + with self.assertRaisesRegex(TypeError, exc_str): + pyros.solve( + model=2, + first_stage_variables=[mdl.x1], + second_stage_variables=[mdl.x2], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + ) + + def test_pyros_multiple_objectives(self): + """ + Test PyROS raises exception if input model has multiple + objectives. + """ + mdl = self.build_simple_test_model() + mdl.obj2 = Objective(expr=(mdl.x1 + mdl.x2)) + + local_solver = SimpleTestSolver() + global_solver = SimpleTestSolver() + + pyros = SolverFactory("pyros") + + exc_str = "Expected model with exactly 1 active.*but.*has 2" + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=[mdl.x1], + second_stage_variables=[mdl.x2], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + ) + + def test_pyros_empty_dof_vars(self): + """ + Test PyROS solver raises exception raised if there are no + first-stage variables or second-stage variables. + """ + # build model + mdl = self.build_simple_test_model() + + # prepare solvers + pyros = SolverFactory("pyros") + local_solver = SimpleTestSolver() + global_solver = SimpleTestSolver() + + # perform checks + exc_str = ( + "Arguments `first_stage_variables` and " + "`second_stage_variables` are both empty lists." + ) + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=[], + second_stage_variables=[], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + ) + + def test_pyros_overlap_dof_vars(self): + """ + Test PyROS solver raises exception raised if there are Vars + passed as both first-stage and second-stage. + """ + # build model + mdl = self.build_simple_test_model() + + # prepare solvers + pyros = SolverFactory("pyros") + local_solver = SimpleTestSolver() + global_solver = SimpleTestSolver() + + # perform checks + exc_str = ( + "Arguments `first_stage_variables` and `second_stage_variables` " + "contain at least one common Var object." + ) + with LoggingIntercept(level=logging.ERROR) as LOG: + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=[mdl.x1], + second_stage_variables=[mdl.x1, mdl.x2], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + ) + + # check logger output is as expected + log_msgs = LOG.getvalue().split("\n")[:-1] + self.assertEqual( + len(log_msgs), 3, "Error message does not contain expected number of lines." + ) + self.assertRegex( + text=log_msgs[0], + expected_regex=( + "The following Vars were found in both `first_stage_variables`" + "and `second_stage_variables`.*" + ), + ) + self.assertRegex(text=log_msgs[1], expected_regex=" 'x1'") + self.assertRegex( + text=log_msgs[2], + expected_regex="Ensure no Vars are included in both arguments.", + ) + + def test_pyros_vars_not_in_model(self): + """ + Test PyROS appropriately raises exception if there are + variables not included in active model objective + or constraints which are not descended from model. + """ + # set up model + mdl = self.build_simple_test_model() + mdl.name = "model1" + mdl2 = self.build_simple_test_model() + mdl2.name = "model2" + + # set up solvers + local_solver = SimpleTestSolver() + global_solver = SimpleTestSolver() + pyros = SolverFactory("pyros") + + mdl.bad_con = Constraint(expr=mdl2.x1 + mdl2.x2 >= 1) + + desc_dof_map = [ + ("first-stage", [mdl2.x1], [], 2), + ("second-stage", [], [mdl2.x2], 2), + ("state", [mdl.x1], [], 3), + ] + + # now perform checks + for vardesc, first_stage_vars, second_stage_vars, numlines in desc_dof_map: + with LoggingIntercept(level=logging.ERROR) as LOG: + exc_str = ( + "Found entries of " + f"{vardesc} variables not descended from.*model.*" + ) + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=first_stage_vars, + second_stage_variables=second_stage_vars, + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + ) + + log_msgs = LOG.getvalue().split("\n")[:-1] + + # check detailed log message is as expected + self.assertEqual( + len(log_msgs), + numlines, + "Error-level log message does not contain expected number of lines.", + ) + self.assertRegex( + text=log_msgs[0], + expected_regex=( + f"The following {vardesc} variables" + ".*not descended from.*model with name 'model1'" + ), + ) + + def test_pyros_non_continuous_vars(self): + """ + Test PyROS raises exception if model contains + non-continuous variables. + """ + # build model; make one variable discrete + mdl = self.build_simple_test_model() + mdl.x2.domain = NonNegativeIntegers + + # prepare solvers + pyros = SolverFactory("pyros") + local_solver = SimpleTestSolver() + global_solver = SimpleTestSolver() + + # perform checks + exc_str = "Model with name 'test_model' contains non-continuous Vars." + with LoggingIntercept(level=logging.ERROR) as LOG: + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=[mdl.x1], + second_stage_variables=[mdl.x2], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + ) + + # check logger output is as expected + log_msgs = LOG.getvalue().split("\n")[:-1] + self.assertEqual( + len(log_msgs), 3, "Error message does not contain expected number of lines." + ) + self.assertRegex( + text=log_msgs[0], + expected_regex=( + "The following Vars of model with name 'test_model' " + "are non-continuous:" + ), + ) + self.assertRegex(text=log_msgs[1], expected_regex=" 'x2'") + self.assertRegex( + text=log_msgs[2], + expected_regex=( + "Ensure all model variables passed to " "PyROS solver are continuous." + ), + ) + + def test_pyros_uncertainty_dimension_mismatch(self): + """ + Test PyROS solver raises exception if uncertainty + set dimension does not match the number + of uncertain parameters. + """ + # build model + mdl = self.build_simple_test_model() + + # prepare solvers + pyros = SolverFactory("pyros") + local_solver = SimpleTestSolver() + global_solver = SimpleTestSolver() + + # perform checks + exc_str = ( + r"Length of argument `uncertain_params` does not match dimension " + r"of argument `uncertainty_set` \(1 != 2\)." + ) + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=[mdl.x1], + second_stage_variables=[mdl.x2], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2], [0, 1]]), + local_solver=local_solver, + global_solver=global_solver, + ) + + @unittest.skipUnless(ipopt_available, "IPOPT is not available.") + def test_pyros_nominal_point_not_in_set(self): + """ + Test PyROS raises exception if nominal point is not in the + uncertainty set. + + NOTE: need executable solvers to solve set bounding problems + for validity checks. + """ + # build model + mdl = self.build_simple_test_model() + + # prepare solvers + pyros = SolverFactory("pyros") + local_solver = SolverFactory("ipopt") + global_solver = SolverFactory("ipopt") + + # perform checks + exc_str = ( + r"Nominal uncertain parameter realization \[0\] " + "is not a point in the uncertainty set.*" + ) + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=[mdl.x1], + second_stage_variables=[mdl.x2], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + nominal_uncertain_param_vals=[0], + ) + + @unittest.skipUnless(ipopt_available, "IPOPT is not available.") + def test_pyros_nominal_point_len_mismatch(self): + """ + Test PyROS raises exception if there is mismatch between length + of nominal uncertain parameter specification and number + of uncertain parameters. + """ + # build model + mdl = self.build_simple_test_model() + + # prepare solvers + pyros = SolverFactory("pyros") + local_solver = SolverFactory("ipopt") + global_solver = SolverFactory("ipopt") + + # perform checks + exc_str = ( + r"Lengths of arguments `uncertain_params` " + r"and `nominal_uncertain_param_vals` " + r"do not match \(1 != 2\)." + ) + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=[mdl.x1], + second_stage_variables=[mdl.x2], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + nominal_uncertain_param_vals=[0, 1], + ) + + @unittest.skipUnless(ipopt_available, "IPOPT is not available.") + def test_pyros_invalid_bypass_separation(self): + """ + Test PyROS raises exception if both local and + global separation are set to be bypassed. + """ + # build model + mdl = self.build_simple_test_model() + + # prepare solvers + pyros = SolverFactory("pyros") + local_solver = SolverFactory("ipopt") + global_solver = SolverFactory("ipopt") + + # perform checks + exc_str = ( + r"Arguments `bypass_local_separation` and `bypass_global_separation` " + r"cannot both be True." + ) + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=[mdl.x1], + second_stage_variables=[mdl.x2], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + bypass_local_separation=True, + bypass_global_separation=True, + ) + + if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 54a268f204e..028a9f38da1 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ Abstract and pre-defined classes for representing uncertainty sets (or uncertain parameter spaces) of two-stage nonlinear robust optimization @@ -44,7 +55,6 @@ ``UncertaintySet`` object. """ - import abc import math import functools @@ -273,14 +283,6 @@ def generate_shape_str(shape, required_shape): ) -def uncertainty_sets(obj): - if not isinstance(obj, UncertaintySet): - raise ValueError( - "Expected an UncertaintySet object, instead received %s" % (obj,) - ) - return obj - - def column(matrix, i): # Get column i of a given multi-dimensional list return [row[i] for row in matrix] diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 2c1a309ced3..3b0187af7dd 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -1,10 +1,24 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + ''' Utility functions for the PyROS solver ''' + import copy from enum import Enum, auto -from pyomo.common.collections import ComponentSet +from pyomo.common.collections import ComponentSet, ComponentMap +from pyomo.common.errors import ApplicationError from pyomo.common.modeling import unique_component_name +from pyomo.common.timing import TicTocTimer from pyomo.core.base import ( Constraint, Var, @@ -17,11 +31,11 @@ Block, Param, ) +from pyomo.core.util import prod from pyomo.core.base.var import IndexedVar from pyomo.core.base.set_types import Reals from pyomo.opt import TerminationCondition as tc from pyomo.core.expr import value -import pyomo.core.expr as EXPR from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression from pyomo.repn.standard_repn import generate_standard_repn from pyomo.core.expr.visitor import ( @@ -39,8 +53,9 @@ import timeit from contextlib import contextmanager import logging -from pprint import pprint import math +from pyomo.common.timing import HierarchicalTimer +from pyomo.common.log import Preformatted # Tolerances used in the code @@ -50,6 +65,135 @@ COEFF_MATCH_ABS_TOL = 0 ABS_CON_CHECK_FEAS_TOL = 1e-5 TIC_TOC_SOLVE_TIME_ATTR = "pyros_tic_toc_time" +DEFAULT_LOGGER_NAME = "pyomo.contrib.pyros" + + +class TimingData: + """ + PyROS solver timing data object. + + Implemented as a wrapper around `common.timing.HierarchicalTimer`, + with added functionality for enforcing a standardized + hierarchy of identifiers. + + Attributes + ---------- + hierarchical_timer_full_ids : set of str + (Class attribute.) Valid identifiers for use with + the encapsulated hierarchical timer. + """ + + hierarchical_timer_full_ids = { + "main", + "main.preprocessing", + "main.master_feasibility", + "main.master", + "main.dr_polishing", + "main.local_separation", + "main.global_separation", + } + + def __init__(self): + """Initialize self (see class docstring).""" + self._hierarchical_timer = HierarchicalTimer() + + def __str__(self): + """ + String representation of `self`. Currently + returns the string representation of `self.hierarchical_timer`. + + Returns + ------- + str + String representation. + """ + return self._hierarchical_timer.__str__() + + def _validate_full_identifier(self, full_identifier): + """ + Validate identifier for hierarchical timer. + + Parameters + ---------- + full_identifier : str + Identifier to validate. + + Raises + ------ + ValueError + If identifier not in `TimingData.hierarchical_timer_full_ids`. + """ + if full_identifier not in self.hierarchical_timer_full_ids: + raise ValueError( + "PyROS timing data object does not support timing ID: " + f"{full_identifier}." + ) + + def start_timer(self, full_identifier): + """ + Start timer for `self.hierarchical_timer`. + + Parameters + ---------- + full_identifier : str + Full identifier for the timer to be started. + Must be an entry of + `TimingData.hierarchical_timer_full_ids`. + """ + self._validate_full_identifier(full_identifier) + identifier = full_identifier.split(".")[-1] + return self._hierarchical_timer.start(identifier=identifier) + + def stop_timer(self, full_identifier): + """ + Stop timer for `self.hierarchical_timer`. + + Parameters + ---------- + full_identifier : str + Full identifier for the timer to be stopped. + Must be an entry of + `TimingData.hierarchical_timer_full_ids`. + """ + self._validate_full_identifier(full_identifier) + identifier = full_identifier.split(".")[-1] + return self._hierarchical_timer.stop(identifier=identifier) + + def get_total_time(self, full_identifier): + """ + Get total time spent with identifier active. + + Parameters + ---------- + full_identifier : str + Full identifier for the timer of interest. + + Returns + ------- + float + Total time spent with identifier active. + """ + return self._hierarchical_timer.get_total_time(identifier=full_identifier) + + def get_main_elapsed_time(self): + """ + Get total time elapsed for main timer of + the HierarchicalTimer contained in self. + + Returns + ------- + float + Total elapsed time. + + Note + ---- + This method is meant for use while the main timer is active. + Otherwise, use ``self.get_total_time("main")``. + """ + # clean? + return self._hierarchical_timer.timers["main"].tic_toc.toc( + msg=None, delta=False + ) '''Code borrowed from gdpopt: time_code, get_main_elapsed_time, a_logger.''' @@ -57,44 +201,46 @@ @contextmanager def time_code(timing_data_obj, code_block_name, is_main_timer=False): - """Starts timer at entry, stores elapsed time at exit + """ + Starts timer at entry, stores elapsed time at exit. + + Parameters + ---------- + timing_data_obj : TimingData + Timing data object. + code_block_name : str + Name of code block being timed. If `is_main_timer=True`, the start time is stored in the timing_data_obj, allowing calculation of total elapsed time 'on the fly' (e.g. to enforce a time limit) using `get_main_elapsed_time(timing_data_obj)`. """ + # initialize tic toc timer + timing_data_obj.start_timer(code_block_name) + start_time = timeit.default_timer() if is_main_timer: timing_data_obj.main_timer_start_time = start_time yield - elapsed_time = timeit.default_timer() - start_time - prev_time = timing_data_obj.get(code_block_name, 0) - timing_data_obj[code_block_name] = prev_time + elapsed_time + timing_data_obj.stop_timer(code_block_name) def get_main_elapsed_time(timing_data_obj): """Returns the time since entering the main `time_code` context""" - current_time = timeit.default_timer() - try: - return current_time - timing_data_obj.main_timer_start_time - except AttributeError as e: - if 'main_timer_start_time' in str(e): - raise AttributeError( - "You need to be in a 'time_code' context to use `get_main_elapsed_time()`." - ) + return timing_data_obj.get_main_elapsed_time() def adjust_solver_time_settings(timing_data_obj, solver, config): """ - Adjust solver max time setting based on current PyROS elapsed - time. + Adjust maximum time allowed for subordinate solver, based + on total PyROS solver elapsed time up to this point. Parameters ---------- timing_data_obj : Bunch PyROS timekeeper. solver : solver type - Solver for which to adjust the max time setting. + Subordinate solver for which to adjust the max time setting. config : ConfigDict PyROS solver config. @@ -116,26 +262,37 @@ def adjust_solver_time_settings(timing_data_obj, solver, config): ---- (1) Adjustment only supported for GAMS, BARON, and IPOPT interfaces. This routine can be generalized to other solvers - after a generic interface to the time limit setting + after a generic Pyomo interface to the time limit setting is introduced. - (2) For IPOPT, and probably also BARON, the CPU time limit - rather than the wallclock time limit, is adjusted, as - no interface to wallclock limit available. - For this reason, extra 30s is added to time remaining - for subsolver time limit. - (The extra 30s is large enough to ensure solver - elapsed time is not beneath elapsed time - user time limit, - but not so large as to overshoot the user-specified time limit - by an inordinate margin.) + (2) For IPOPT and BARON, the CPU time limit, + rather than the wallclock time limit, may be adjusted, + as there may be no means by which to specify the wall time + limit explicitly. + (3) For GAMS, we adjust the time limit through the GAMS Reslim + option. However, this may be overridden by any user + specifications included in a GAMS optfile, which may be + difficult to track down. + (4) To ensure the time limit is specified to a strictly + positive value, the time limit is adjusted to a value of + at least 1 second. """ + # in case there is no time remaining: we set time limit + # to a minimum of 1s, as some solvers require a strictly + # positive time limit + time_limit_buffer = 1 + if config.time_limit is not None: time_remaining = config.time_limit - get_main_elapsed_time(timing_data_obj) if isinstance(solver, type(SolverFactory("gams", solver_io="shell"))): original_max_time_setting = solver.options["add_options"] custom_setting_present = "add_options" in solver.options - # adjust GAMS solver time - reslim_str = f"option reslim={max(30, 30 + time_remaining)};" + # note: our time limit will be overridden by any + # time limits specified by the user through a + # GAMS optfile, but tracking down the optfile + # and/or the GAMS subsolver specific option + # is more difficult + reslim_str = "option reslim=" f"{max(time_limit_buffer, time_remaining)};" if isinstance(solver.options["add_options"], list): solver.options["add_options"].append(reslim_str) else: @@ -145,7 +302,16 @@ def adjust_solver_time_settings(timing_data_obj, solver, config): if isinstance(solver, SolverFactory.get_class("baron")): options_key = "MaxTime" elif isinstance(solver, SolverFactory.get_class("ipopt")): - options_key = "max_cpu_time" + options_key = ( + # IPOPT 3.14.0+ added support for specifying + # wall time limit explicitly; this is preferred + # over CPU time limit + "max_wall_time" + if solver.version() >= (3, 14, 0, 0) + else "max_cpu_time" + ) + elif isinstance(solver, SolverFactory.get_class("scip")): + options_key = "limits/time" else: options_key = None @@ -153,8 +319,19 @@ def adjust_solver_time_settings(timing_data_obj, solver, config): custom_setting_present = options_key in solver.options original_max_time_setting = solver.options[options_key] - # ensure positive value assigned to avoid application error - solver.options[options_key] = max(30, 30 + time_remaining) + # account for elapsed time remaining and + # original time limit setting. + # if no original time limit is set, then we assume + # there is no time limit, rather than tracking + # down the solver-specific default + orig_max_time = ( + float("inf") + if original_max_time_setting is None + else original_max_time_setting + ) + solver.options[options_key] = min( + max(time_limit_buffer, time_remaining), orig_max_time + ) else: custom_setting_present = False original_max_time_setting = None @@ -201,6 +378,8 @@ def revert_solver_max_time_adjustment( options_key = "MaxTime" elif isinstance(solver, SolverFactory.get_class("ipopt")): options_key = "max_cpu_time" + elif isinstance(solver, SolverFactory.get_class("scip")): + options_key = "limits/time" else: options_key = None @@ -215,38 +394,85 @@ def revert_solver_max_time_adjustment( if isinstance(solver, type(SolverFactory("gams", solver_io="shell"))): solver.options[options_key].pop() else: - # remove the max time specification introduced. - # All lines are needed here to completely remove the option - # from access through getattr and dictionary reference. delattr(solver.options, options_key) - if options_key in solver.options.keys(): - del solver.options[options_key] -def a_logger(str_or_logger): - """Returns a logger when passed either a logger name or logger object.""" - if isinstance(str_or_logger, logging.Logger): - return str_or_logger - else: - return logging.getLogger(str_or_logger) +class PreformattedLogger(logging.Logger): + """ + A specialized logger object designed to cast log messages + to Pyomo `Preformatted` objects prior to logging the messages. + Useful for circumventing the formatters of the standard Pyomo + logger in the event an instance is a descendant of the Pyomo + logger. + """ + def critical(self, msg, *args, **kwargs): + """ + Preformat and log ``msg % args`` with severity + `logging.CRITICAL`. + """ + return super(PreformattedLogger, self).critical( + Preformatted(msg % args if args else msg), **kwargs + ) -def ValidEnum(enum_class): - ''' - Python 3 dependent format string - ''' + def error(self, msg, *args, **kwargs): + """ + Preformat and log ``msg % args`` with severity + `logging.ERROR`. + """ + return super(PreformattedLogger, self).error( + Preformatted(msg % args if args else msg), **kwargs + ) - def fcn(obj): - if obj not in enum_class: - raise ValueError( - "Expected an {0} object, " - "instead received {1}".format( - enum_class.__name__, obj.__class__.__name__ - ) - ) - return obj + def warning(self, msg, *args, **kwargs): + """ + Preformat and log ``msg % args`` with severity + `logging.WARNING`. + """ + return super(PreformattedLogger, self).warning( + Preformatted(msg % args if args else msg), **kwargs + ) - return fcn + def info(self, msg, *args, **kwargs): + """ + Preformat and log ``msg % args`` with severity + `logging.INFO`. + """ + return super(PreformattedLogger, self).info( + Preformatted(msg % args if args else msg), **kwargs + ) + + def debug(self, msg, *args, **kwargs): + """ + Preformat and log ``msg % args`` with severity + `logging.DEBUG`. + """ + return super(PreformattedLogger, self).debug( + Preformatted(msg % args if args else msg), **kwargs + ) + + def log(self, level, msg, *args, **kwargs): + """ + Preformat and log ``msg % args`` with integer + severity `level`. + """ + return super(PreformattedLogger, self).log( + level, Preformatted(msg % args if args else msg), **kwargs + ) + + +def setup_pyros_logger(name=DEFAULT_LOGGER_NAME): + """ + Set up pyros logger. + """ + # default logger: INFO level, with preformatted messages + current_logger_class = logging.getLoggerClass() + logging.setLoggerClass(PreformattedLogger) + logger = logging.getLogger(name=name) + logger.setLevel(logging.INFO) + logging.setLoggerClass(current_logger_class) + + return logger class pyrosTerminationCondition(Enum): @@ -271,6 +497,25 @@ class pyrosTerminationCondition(Enum): time_out = 5 """Maximum allowable time exceeded.""" + @property + def message(self): + """ + str : Message associated with a given PyROS + termination condition. + """ + message_dict = { + self.robust_optimal: "Robust optimal solution identified.", + self.robust_feasible: "Robust feasible solution identified.", + self.robust_infeasible: "Problem is robust infeasible.", + self.time_out: "Maximum allowable time exceeded.", + self.max_iter: "Maximum number of iterations reached.", + self.subsolver_error: ( + "Subordinate optimizer(s) could not solve a subproblem " + "to an acceptable status." + ), + } + return message_dict[self] + class SeparationStrategy(Enum): all_violations = auto() @@ -308,14 +553,6 @@ def recast_to_min_obj(model, obj): obj.sense = minimize -def model_is_valid(model): - """ - Assess whether model is valid on basis of the number of active - Objectives. A valid model must contain exactly one active Objective. - """ - return len(list(model.component_data_objects(Objective, active=True))) == 1 - - def turn_bounds_to_constraints(variable, model, config=None): ''' Turn the variable in question's "bounds" into direct inequality constraints on the model. @@ -399,41 +636,6 @@ def get_time_from_solver(results): return float("nan") if solve_time is None else solve_time -def validate_uncertainty_set(config): - ''' - Confirm expression output from uncertainty set function references all q in q. - Typecheck the uncertainty_set.q is Params referenced inside of m. - Give warning that the nominal point (default value in the model) is not in the specified uncertainty set. - :param config: solver config - ''' - # === Check that q in UncertaintySet object constraint expression is referencing q in model.uncertain_params - uncertain_params = config.uncertain_params - - # === Non-zero number of uncertain parameters - if len(uncertain_params) == 0: - raise AttributeError( - "Must provide uncertain params, uncertain_params list length is 0." - ) - # === No duplicate parameters - if len(uncertain_params) != len(ComponentSet(uncertain_params)): - raise AttributeError("No duplicates allowed for uncertain param objects.") - # === Ensure nominal point is in the set - if not config.uncertainty_set.point_in_set( - point=config.nominal_uncertain_param_vals - ): - raise AttributeError( - "Nominal point for uncertain parameters must be in the uncertainty set." - ) - # === Check set validity via boundedness and non-emptiness - if not config.uncertainty_set.is_valid(config=config): - raise AttributeError( - "Invalid uncertainty set detected. Check the uncertainty set object to " - "ensure non-emptiness and boundedness." - ) - - return - - def add_bounds_for_uncertain_parameters(model, config): ''' This function solves a set of optimization problems to determine bounds on the uncertain parameters @@ -613,98 +815,345 @@ def replace_uncertain_bounds_with_constraints(model, uncertain_params): v.setlb(None) -def validate_kwarg_inputs(model, config): - ''' - Confirm kwarg inputs satisfy PyROS requirements. - :param model: the deterministic model - :param config: the config for this PyROS instance - :return: - ''' - - # === Check if model is ConcreteModel object - if not isinstance(model, ConcreteModel): - raise ValueError("Model passed to PyROS solver must be a ConcreteModel object.") +def check_components_descended_from_model(model, components, components_name, config): + """ + Check all members in a provided sequence of Pyomo component + objects are descended from a given ConcreteModel object. - first_stage_variables = config.first_stage_variables - second_stage_variables = config.second_stage_variables - uncertain_params = config.uncertain_params + Parameters + ---------- + model : ConcreteModel + Model from which components should all be descended. + components : Iterable of Component + Components of interest. + components_name : str + Brief description or name for the sequence of components. + Used for constructing error messages. + config : ConfigDict + PyROS solver options. - if not config.first_stage_variables and not config.second_stage_variables: - # Must have non-zero DOF + Raises + ------ + ValueError + If at least one entry of `components` is not descended + from `model`. + """ + components_not_in_model = [comp for comp in components if comp.model() is not model] + if components_not_in_model: + comp_names_str = "\n ".join( + f"{comp.name!r}, from model with name {comp.model().name!r}" + for comp in components_not_in_model + ) + config.progress_logger.error( + f"The following {components_name} " + "are not descended from the " + f"input deterministic model with name {model.name!r}:\n " + f"{comp_names_str}" + ) raise ValueError( - "first_stage_variables and " - "second_stage_variables cannot both be empty lists." + f"Found entries of {components_name} " + "not descended from input model. " + "Check logger output messages." ) - if ComponentSet(first_stage_variables) != ComponentSet( - config.first_stage_variables - ): + +def get_state_vars(blk, first_stage_variables, second_stage_variables): + """ + Get state variables of a modeling block. + + The state variables with respect to `blk` are the unfixed + `VarData` objects participating in the active objective + or constraints descended from `blk` which are not + first-stage variables or second-stage variables. + + Parameters + ---------- + blk : ScalarBlock + Block of interest. + first_stage_variables : Iterable of VarData + First-stage variables. + second_stage_variables : Iterable of VarData + Second-stage variables. + + Yields + ------ + VarData + State variable. + """ + dof_var_set = ComponentSet(first_stage_variables) | ComponentSet( + second_stage_variables + ) + for var in get_vars_from_component(blk, (Objective, Constraint)): + is_state_var = not var.fixed and var not in dof_var_set + if is_state_var: + yield var + + +def check_variables_continuous(model, vars, config): + """ + Check that all DOF and state variables of the model + are continuous. + + Parameters + ---------- + model : ConcreteModel + Input deterministic model. + config : ConfigDict + PyROS solver options. + + Raises + ------ + ValueError + If at least one variable is found to not be continuous. + + Note + ---- + A variable is considered continuous if the `is_continuous()` + method returns True. + """ + non_continuous_vars = [var for var in vars if not var.is_continuous()] + if non_continuous_vars: + non_continuous_vars_str = "\n ".join( + f"{var.name!r}" for var in non_continuous_vars + ) + config.progress_logger.error( + f"The following Vars of model with name {model.name!r} " + f"are non-continuous:\n {non_continuous_vars_str}\n" + "Ensure all model variables passed to PyROS solver are continuous." + ) raise ValueError( - "All elements in first_stage_variables must be Var members of the model object." + f"Model with name {model.name!r} contains non-continuous Vars." ) - if ComponentSet(second_stage_variables) != ComponentSet( - config.second_stage_variables - ): + +def validate_model(model, config): + """ + Validate deterministic model passed to PyROS solver. + + Parameters + ---------- + model : ConcreteModel + Deterministic model. Should have only one active Objective. + config : ConfigDict + PyROS solver options. + + Returns + ------- + ComponentSet + The variables participating in the active Objective + and Constraint expressions of `model`. + + Raises + ------ + TypeError + If model is not of type ConcreteModel. + ValueError + If model does not have exactly one active Objective + component. + """ + # note: only support ConcreteModel. no support for Blocks + if not isinstance(model, ConcreteModel): + raise TypeError( + f"Model should be of type {ConcreteModel.__name__}, " + f"but is of type {type(model).__name__}." + ) + + # active objectives check + active_objs_list = list( + model.component_data_objects(Objective, active=True, descend_into=True) + ) + if len(active_objs_list) != 1: raise ValueError( - "All elements in second_stage_variables must be Var members of the model object." + "Expected model with exactly 1 active objective, but " + f"model provided has {len(active_objs_list)}." ) - if any( - v in ComponentSet(second_stage_variables) - for v in ComponentSet(first_stage_variables) - ): + +def validate_variable_partitioning(model, config): + """ + Check that partitioning of the first-stage variables, + second-stage variables, and uncertain parameters + is valid. + + Parameters + ---------- + model : ConcreteModel + Input deterministic model. + config : ConfigDict + PyROS solver options. + + Returns + ------- + list of VarData + State variables of the model. + + Raises + ------ + ValueError + If first-stage variables and second-stage variables + overlap, or there are no first-stage variables + and no second-stage variables. + """ + # at least one DOF required + if not config.first_stage_variables and not config.second_stage_variables: raise ValueError( - "No common elements allowed between first_stage_variables and second_stage_variables." + "Arguments `first_stage_variables` and " + "`second_stage_variables` are both empty lists." ) - if ComponentSet(uncertain_params) != ComponentSet(config.uncertain_params): + # ensure no overlap between DOF var sets + overlapping_vars = ComponentSet(config.first_stage_variables) & ComponentSet( + config.second_stage_variables + ) + if overlapping_vars: + overlapping_var_list = "\n ".join(f"{var.name!r}" for var in overlapping_vars) + config.progress_logger.error( + "The following Vars were found in both `first_stage_variables`" + f"and `second_stage_variables`:\n {overlapping_var_list}" + "\nEnsure no Vars are included in both arguments." + ) raise ValueError( - "uncertain_params must be mutable Param members of the model object." + "Arguments `first_stage_variables` and `second_stage_variables` " + "contain at least one common Var object." + ) + + state_vars = list( + get_state_vars( + model, + first_stage_variables=config.first_stage_variables, + second_stage_variables=config.second_stage_variables, ) + ) + var_type_list_map = { + "first-stage variables": config.first_stage_variables, + "second-stage variables": config.second_stage_variables, + "state variables": state_vars, + } + for desc, vars in var_type_list_map.items(): + check_components_descended_from_model( + model=model, components=vars, components_name=desc, config=config + ) + + all_vars = config.first_stage_variables + config.second_stage_variables + state_vars + check_variables_continuous(model, all_vars, config) + + return state_vars + + +def validate_uncertainty_specification(model, config): + """ + Validate specification of uncertain parameters and uncertainty + set. + + Parameters + ---------- + model : ConcreteModel + Input deterministic model. + config : ConfigDict + PyROS solver options. + + Raises + ------ + ValueError + If at least one of the following holds: + + - dimension of uncertainty set does not equal number of + uncertain parameters + - uncertainty set `is_valid()` method does not return + true. + - nominal parameter realization is not in the uncertainty set. + """ + check_components_descended_from_model( + model=model, + components=config.uncertain_params, + components_name="uncertain parameters", + config=config, + ) - if not config.uncertainty_set: + if len(config.uncertain_params) != config.uncertainty_set.dim: raise ValueError( - "An UncertaintySet object must be provided to the PyROS solver." + "Length of argument `uncertain_params` does not match dimension " + "of argument `uncertainty_set` " + f"({len(config.uncertain_params)} != {config.uncertainty_set.dim})." ) - non_mutable_params = [] - for p in config.uncertain_params: - if not ( - not p.is_constant() and p.is_fixed() and not p.is_potentially_variable() - ): - non_mutable_params.append(p) - if non_mutable_params: - raise ValueError( - "Param objects which are uncertain must have attribute mutable=True. " - "Offending Params: %s" % [p.name for p in non_mutable_params] - ) + # validate uncertainty set + if not config.uncertainty_set.is_valid(config=config): + raise ValueError( + f"Uncertainty set {config.uncertainty_set} is invalid, " + "as it is either empty or unbounded." + ) - # === Solvers provided check - if not config.local_solver or not config.global_solver: + # fill-in nominal point as necessary, if not provided. + # otherwise, check length matches uncertainty dimension + if not config.nominal_uncertain_param_vals: + config.nominal_uncertain_param_vals = [ + value(param, exception=True) for param in config.uncertain_params + ] + elif len(config.nominal_uncertain_param_vals) != len(config.uncertain_params): raise ValueError( - "User must designate both a local and global optimization solver via the local_solver" - " and global_solver options." + "Lengths of arguments `uncertain_params` and " + "`nominal_uncertain_param_vals` " + "do not match " + f"({len(config.uncertain_params)} != " + f"{len(config.nominal_uncertain_param_vals)})." ) - if config.bypass_local_separation and config.bypass_global_separation: + # uncertainty set should contain nominal point + nominal_point_in_set = config.uncertainty_set.point_in_set( + point=config.nominal_uncertain_param_vals + ) + if not nominal_point_in_set: raise ValueError( - "User cannot simultaneously enable options " - "'bypass_local_separation' and " - "'bypass_global_separation'." + "Nominal uncertain parameter realization " + f"{config.nominal_uncertain_param_vals} " + "is not a point in the uncertainty set " + f"{config.uncertainty_set!r}." ) - # === Degrees of freedom provided check - if len(config.first_stage_variables) + len(config.second_stage_variables) == 0: + +def validate_separation_problem_options(model, config): + """ + Validate separation problem arguments to the PyROS solver. + + Parameters + ---------- + model : ConcreteModel + Input deterministic model. + config : ConfigDict + PyROS solver options. + + Raises + ------ + ValueError + If options `bypass_local_separation` and + `bypass_global_separation` are set to False. + """ + if config.bypass_local_separation and config.bypass_global_separation: raise ValueError( - "User must designate at least one first- and/or second-stage variable." + "Arguments `bypass_local_separation` " + "and `bypass_global_separation` " + "cannot both be True." ) - # === Uncertain params provided check - if len(config.uncertain_params) == 0: - raise ValueError("User must designate at least one uncertain parameter.") - return +def validate_pyros_inputs(model, config): + """ + Perform advanced validation of PyROS solver arguments. + + Parameters + ---------- + model : ConcreteModel + Input deterministic model. + config : ConfigDict + PyROS solver options. + """ + validate_model(model, config) + state_vars = validate_variable_partitioning(model, config) + validate_uncertainty_specification(model, config) + validate_separation_problem_options(model, config) + + return state_vars def substitute_ssv_in_dr_constraints(model, constraint): @@ -1026,220 +1475,149 @@ def selective_clone(block, first_stage_vars): def add_decision_rule_variables(model_data, config): - ''' - Function to add decision rule (DR) variables to the working model. DR variables become first-stage design - variables which do not get copied at each iteration. Currently support static_approx (no DR), affine DR, - and quadratic DR. - :param model_data: the data container for the working model - :param config: the config block - :return: - ''' + """ + Add variables for polynomial decision rules to the working + model. + + Parameters + ---------- + model_data : ROSolveResults + Model data. + config : config_dict + PyROS solver options. + + Note + ---- + Decision rule variables are considered first-stage decision + variables which do not get copied at each iteration. + PyROS currently supports static (zeroth order), + affine (first-order), and quadratic DR. + """ second_stage_variables = model_data.working_model.util.second_stage_variables first_stage_variables = model_data.working_model.util.first_stage_variables - uncertain_params = model_data.working_model.util.uncertain_params decision_rule_vars = [] + + # since DR expression is a general polynomial in the uncertain + # parameters, the exact number of DR variables per second-stage + # variable depends on DR order and uncertainty set dimension degree = config.decision_rule_order - bounds = (None, None) - if degree == 0: - for i in range(len(second_stage_variables)): - model_data.working_model.add_component( - "decision_rule_var_" + str(i), - Var( - initialize=value(second_stage_variables[i], exception=False), - bounds=bounds, - domain=Reals, - ), - ) - first_stage_variables.extend( - getattr( - model_data.working_model, "decision_rule_var_" + str(i) - ).values() - ) - decision_rule_vars.append( - getattr(model_data.working_model, "decision_rule_var_" + str(i)) - ) - elif degree == 1: - for i in range(len(second_stage_variables)): - index_set = list(range(len(uncertain_params) + 1)) - model_data.working_model.add_component( - "decision_rule_var_" + str(i), - Var(index_set, initialize=0, bounds=bounds, domain=Reals), - ) - # === For affine drs, the [0]th constant term is initialized to the control variable values, all other terms are initialized to 0 - getattr(model_data.working_model, "decision_rule_var_" + str(i))[ - 0 - ].set_value( - value(second_stage_variables[i], exception=False), skip_validation=True - ) - first_stage_variables.extend( - list( - getattr( - model_data.working_model, "decision_rule_var_" + str(i) - ).values() - ) - ) - decision_rule_vars.append( - getattr(model_data.working_model, "decision_rule_var_" + str(i)) - ) - elif degree == 2 or degree == 3 or degree == 4: - for i in range(len(second_stage_variables)): - num_vars = int(sp.special.comb(N=len(uncertain_params) + degree, k=degree)) - dict_init = {} - for r in range(num_vars): - if r == 0: - dict_init.update( - {r: value(second_stage_variables[i], exception=False)} - ) - else: - dict_init.update({r: 0}) - model_data.working_model.add_component( - "decision_rule_var_" + str(i), - Var( - list(range(num_vars)), - initialize=dict_init, - bounds=bounds, - domain=Reals, - ), - ) - first_stage_variables.extend( - list( - getattr( - model_data.working_model, "decision_rule_var_" + str(i) - ).values() - ) - ) - decision_rule_vars.append( - getattr(model_data.working_model, "decision_rule_var_" + str(i)) - ) - else: - raise ValueError( - "Decision rule order " - + str(config.decision_rule_order) - + " is not yet supported. PyROS supports polynomials of degree 0 (static approximation), 1, 2." + num_uncertain_params = len(model_data.working_model.util.uncertain_params) + num_dr_vars = sp.special.comb( + N=num_uncertain_params + degree, k=degree, exact=True, repetition=False + ) + + for idx, ss_var in enumerate(second_stage_variables): + # declare DR coefficients for current second-stage variable + indexed_dr_var = Var( + range(num_dr_vars), initialize=0, bounds=(None, None), domain=Reals + ) + model_data.working_model.add_component( + f"decision_rule_var_{idx}", indexed_dr_var ) - model_data.working_model.util.decision_rule_vars = decision_rule_vars + # index 0 entry of the IndexedVar is the static + # DR term. initialize to user-provided value of + # the corresponding second-stage variable. + # all other entries remain initialized to 0. + indexed_dr_var[0].set_value(value(ss_var, exception=False)) -def partition_powers(n, v): - """Partition a total degree n across v variables + # update attributes + first_stage_variables.extend(indexed_dr_var.values()) + decision_rule_vars.append(indexed_dr_var) - This is an implementation of the "stars and bars" algorithm from - combinatorial mathematics. + model_data.working_model.util.decision_rule_vars = decision_rule_vars - This partitions a "total integer degree" of n across v variables - such that each variable gets an integer degree >= 0. You can think - of this as dividing a set of n+v things into v groupings, with the - power for each v_i being 1 less than the number of things in the - i'th group (because the v is part of the group). It is therefore - sufficient to just get the v-1 starting points chosen from a list of - indices n+v long (the first starting point is fixed to be 0). +def add_decision_rule_constraints(model_data, config): """ - for starts in it.combinations(range(1, n + v), v - 1): - # add the initial starting point to the beginning and the total - # number of objects (degree counters and variables) to the end - # of the list. The degree for each variable is 1 less than the - # difference of sequential starting points (to account for the - # variable itself) - starts = (0,) + starts + (n + v,) - yield [starts[i + 1] - starts[i] - 1 for i in range(v)] - - -def sort_partitioned_powers(powers_list): - powers_list = sorted(powers_list, reverse=True) - powers_list = sorted(powers_list, key=lambda elem: max(elem)) - return powers_list + Add decision rule equality constraints to the working model. - -def add_decision_rule_constraints(model_data, config): - ''' - Function to add the defining Constraint relationships for the decision rules to the working model. - :param model_data: model data container object - :param config: the config object - :return: - ''' + Parameters + ---------- + model_data : ROSolveResults + Model data. + config : ConfigDict + PyROS solver options. + """ second_stage_variables = model_data.working_model.util.second_stage_variables uncertain_params = model_data.working_model.util.uncertain_params decision_rule_eqns = [] + decision_rule_vars_list = model_data.working_model.util.decision_rule_vars degree = config.decision_rule_order - if degree == 0: - for i in range(len(second_stage_variables)): - model_data.working_model.add_component( - "decision_rule_eqn_" + str(i), - Constraint( - expr=getattr( - model_data.working_model, "decision_rule_var_" + str(i) - ) - == second_stage_variables[i] - ), - ) - decision_rule_eqns.append( - getattr(model_data.working_model, "decision_rule_eqn_" + str(i)) - ) - elif degree == 1: - for i in range(len(second_stage_variables)): - expr = 0 - for j in range( - len(getattr(model_data.working_model, "decision_rule_var_" + str(i))) - ): - if j == 0: - expr += getattr( - model_data.working_model, "decision_rule_var_" + str(i) - )[j] - else: - expr += ( - getattr( - model_data.working_model, "decision_rule_var_" + str(i) - )[j] - * uncertain_params[j - 1] - ) - model_data.working_model.add_component( - "decision_rule_eqn_" + str(i), - Constraint(expr=expr == second_stage_variables[i]), - ) - decision_rule_eqns.append( - getattr(model_data.working_model, "decision_rule_eqn_" + str(i)) - ) - elif degree >= 2: - # Using bars and stars groupings of variable powers, construct x1^a * .... * xn^b terms for all c <= a+...+b = degree - all_powers = [] - for n in range(1, degree + 1): - all_powers.append( - sort_partitioned_powers( - list(partition_powers(n, len(uncertain_params))) - ) - ) - for i in range(len(second_stage_variables)): - Z = list( - z - for z in getattr( - model_data.working_model, "decision_rule_var_" + str(i) - ).values() - ) - e = Z.pop(0) - for degree_param_powers in all_powers: - for param_powers in degree_param_powers: - product = 1 - for idx, power in enumerate(param_powers): - if power == 0: - pass - else: - product = product * uncertain_params[idx] ** power - e += Z.pop(0) * product - model_data.working_model.add_component( - "decision_rule_eqn_" + str(i), - Constraint(expr=e == second_stage_variables[i]), - ) - decision_rule_eqns.append( - getattr(model_data.working_model, "decision_rule_eqn_" + str(i)) + + # keeping track of degree of monomial in which each + # DR coefficient participates will be useful for later + dr_var_to_exponent_map = ComponentMap() + + # set up uncertain parameter combinations for + # construction of the monomials of the DR expressions + monomial_param_combos = [] + for power in range(degree + 1): + power_combos = it.combinations_with_replacement(uncertain_params, power) + monomial_param_combos.extend(power_combos) + + # now construct DR equations and declare them on the working model + second_stage_dr_var_zip = zip(second_stage_variables, decision_rule_vars_list) + for idx, (ss_var, indexed_dr_var) in enumerate(second_stage_dr_var_zip): + # for each DR equation, the number of coefficients should match + # the number of monomial terms exactly + if len(monomial_param_combos) != len(indexed_dr_var.index_set()): + raise ValueError( + f"Mismatch between number of DR coefficient variables " + f"and number of DR monomials for DR equation index {idx}, " + f"corresponding to second-stage variable {ss_var.name!r}. " + f"({len(indexed_dr_var.index_set())}!= {len(monomial_param_combos)})" ) - if len(Z) != 0: - raise RuntimeError( - "Construction of the decision rule functions did not work correctly! " - "Did not use all coefficient terms." - ) + + # construct the DR polynomial + dr_expression = 0 + for dr_var, param_combo in zip(indexed_dr_var.values(), monomial_param_combos): + dr_expression += dr_var * prod(param_combo) + + # map decision rule var to degree (exponent) of the + # associated monomial with respect to the uncertain params + dr_var_to_exponent_map[dr_var] = len(param_combo) + + # declare constraint on model + dr_eqn = Constraint(expr=dr_expression - ss_var == 0) + model_data.working_model.add_component(f"decision_rule_eqn_{idx}", dr_eqn) + + # append to list of DR equality constraints + decision_rule_eqns.append(dr_eqn) + + # finally, add attributes to util block model_data.working_model.util.decision_rule_eqns = decision_rule_eqns + model_data.working_model.util.dr_var_to_exponent_map = dr_var_to_exponent_map + + +def enforce_dr_degree(blk, config, degree): + """ + Make decision rule polynomials of a given degree + by fixing value of the appropriate subset of the decision + rule coefficients to 0. + + Parameters + ---------- + blk : ScalarBlock + Working model, or master problem block. + config : ConfigDict + PyROS solver options. + degree : int + Degree of the DR polynomials that is to be enforced. + """ + second_stage_vars = blk.util.second_stage_variables + indexed_dr_vars = blk.util.decision_rule_vars + dr_var_to_exponent_map = blk.util.dr_var_to_exponent_map + + for ss_var, indexed_dr_var in zip(second_stage_vars, indexed_dr_vars): + for dr_var in indexed_dr_var.values(): + dr_var_degree = dr_var_to_exponent_map[dr_var] + + if dr_var_degree > degree: + dr_var.fix(0) + else: + dr_var.unfix() def identify_objective_functions(model, objective): @@ -1383,111 +1761,279 @@ def process_termination_condition_master_problem(config, results): ) -def output_logger(config, **kwargs): - ''' - All user returned messages (termination conditions, runtime errors) are here - Includes when - "sub-solver %s returned status infeasible..." - :return: - ''' +def call_solver(model, solver, config, timing_obj, timer_name, err_msg): + """ + Solve a model with a given optimizer, keeping track of + wall time requirements. - # === PREAMBLE + LICENSING - # Version printing - if "preamble" in kwargs: - if kwargs["preamble"]: - version = str(kwargs["version"]) - preamble = ( - "===========================================================================================\n" - "PyROS: Pyomo Robust Optimization Solver v.%s \n" - "Developed by: Natalie M. Isenberg (1), Jason A. F. Sherman (1), \n" - " John D. Siirola (2), Chrysanthos E. Gounaris (1) \n" - "(1) Carnegie Mellon University, Department of Chemical Engineering \n" - "(2) Sandia National Laboratories, Center for Computing Research\n\n" - "The developers gratefully acknowledge support from the U.S. Department of Energy's \n" - "Institute for the Design of Advanced Energy Systems (IDAES) \n" - "===========================================================================================" - % version - ) - print(preamble) - # === DISCLAIMER - if "disclaimer" in kwargs: - if kwargs["disclaimer"]: - print( - "======================================== DISCLAIMER =======================================\n" - "PyROS is still under development. \n" - "Please provide feedback and/or report any issues by opening a Pyomo ticket.\n" - "===========================================================================================\n" - ) - # === ALL LOGGER RETURN MESSAGES - if "bypass_global_separation" in kwargs: - if kwargs["bypass_global_separation"]: - config.progress_logger.info( - "NOTE: Option to bypass global separation was chosen. " - "Robust feasibility and optimality of the reported " - "solution are not guaranteed." - ) - if "robust_optimal" in kwargs: - if kwargs["robust_optimal"]: - config.progress_logger.info( - 'Robust optimal solution identified. Exiting PyROS.' - ) + Parameters + ---------- + model : ConcreteModel + Model of interest. + solver : Pyomo solver type + Subordinate optimizer. + config : ConfigDict + PyROS solver settings. + timing_obj : TimingData + PyROS solver timing data object. + timer_name : str + Name of sub timer under the hierarchical timer contained in + ``timing_obj`` to start/stop for keeping track of solve + time requirements. + err_msg : str + Message to log through ``config.progress_logger.exception()`` + in event an ApplicationError is raised while attempting to + solve the model. - if "robust_feasible" in kwargs: - if kwargs["robust_feasible"]: - config.progress_logger.info( - 'Robust feasible solution identified. Exiting PyROS.' - ) + Returns + ------- + SolverResults + Solve results. Note that ``results.solver`` contains + an additional attribute, named after + ``TIC_TOC_SOLVE_TIME_ATTR``, of which the value is set to the + recorded solver wall time. + + Raises + ------ + ApplicationError + If ApplicationError is raised by the solver. + In this case, `err_msg` is logged through + ``config.progress_logger.exception()`` before + the exception is raised. + """ + tt_timer = TicTocTimer() - if "robust_infeasible" in kwargs: - if kwargs["robust_infeasible"]: - config.progress_logger.info('Robust infeasible problem. Exiting PyROS.') - - if "time_out" in kwargs: - if kwargs["time_out"]: - config.progress_logger.info( - 'PyROS was unable to identify robust solution ' - 'before exceeding time limit of %s seconds. ' - 'Consider increasing the time limit via option time_limit.' - % config.time_limit - ) + orig_setting, custom_setting_present = adjust_solver_time_settings( + timing_obj, solver, config + ) + timing_obj.start_timer(timer_name) + tt_timer.tic(msg=None) - if "max_iter" in kwargs: - if kwargs["max_iter"]: - config.progress_logger.info( - 'PyROS was unable to identify robust solution ' - 'within %s iterations of the GRCS algorithm. ' - 'Consider increasing the iteration limit via option max_iter.' - % config.max_iter - ) + try: + results = solver.solve( + model, + tee=config.tee, + load_solutions=False, + symbolic_solver_labels=config.symbolic_solver_labels, + ) + except ApplicationError: + # account for possible external subsolver errors + # (such as segmentation faults, function evaluation + # errors, etc.) + config.progress_logger.error(err_msg) + raise + else: + setattr( + results.solver, TIC_TOC_SOLVE_TIME_ATTR, tt_timer.toc(msg=None, delta=True) + ) + finally: + timing_obj.stop_timer(timer_name) + revert_solver_max_time_adjustment( + solver, orig_setting, custom_setting_present, config + ) - if "master_error" in kwargs: - if kwargs["master_error"]: - status_dict = kwargs["status_dict"] - filename = kwargs["filename"] # solver name to solver termination condition - if kwargs["iteration"] == 0: - raise AttributeError( - "User-supplied solver(s) could not solve the deterministic model. " - "Returned termination conditions were: %s" - "Please ensure deterministic model is solvable by at least one of the supplied solvers. " - "Exiting PyROS." % pprint(status_dict, width=1) - ) - config.progress_logger.info( - "User-supplied solver(s) could not solve the master model at iteration %s.\n" - "Returned termination conditions were: %s\n" - "For debugging, this problem has been written to a GAMS file titled %s. Exiting PyROS." - % (kwargs["iteration"], pprint(status_dict), filename) - ) - if "separation_error" in kwargs: - if kwargs["separation_error"]: - status_dict = kwargs["status_dict"] - filename = kwargs["filename"] - iteration = kwargs["iteration"] - obj = kwargs["objective"] - config.progress_logger.info( - "User-supplied solver(s) could not solve the separation problem at iteration %s under separation objective %s.\n" - "Returned termination conditions were: %s\n" - "For debugging, this problem has been written to a GAMS file titled %s. Exiting PyROS." - % (iteration, obj, pprint(status_dict, width=1), filename) - ) + return results - return + +class IterationLogRecord: + """ + PyROS solver iteration log record. + + Parameters + ---------- + iteration : int or None, optional + Iteration number. + objective : int or None, optional + Master problem objective value. + Note: if the sense of the original model is maximization, + then this is the negative of the objective value + of the original model. + first_stage_var_shift : float or None, optional + Infinity norm of the difference between first-stage + variable vectors for the current and previous iterations. + second_stage_var_shift : float or None, optional + Infinity norm of the difference between decision rule + variable vectors for the current and previous iterations. + dr_polishing_success : bool or None, optional + True if DR polishing solved successfully, False otherwise. + num_violated_cons : int or None, optional + Number of performance constraints found to be violated + during separation step. + all_sep_problems_solved : int or None, optional + True if all separation problems were solved successfully, + False otherwise (such as if there was a time out, subsolver + error, or only a subset of the problems were solved due to + custom constraint prioritization). + global_separation : bool, optional + True if separation problems were solved with the subordinate + global optimizer(s), False otherwise. + max_violation : int or None + Maximum scaled violation of any performance constraint + found during separation step. + elapsed_time : float, optional + Total time elapsed up to the current iteration, in seconds. + + Attributes + ---------- + iteration : int or None + Iteration number. + objective : int or None + Master problem objective value. + Note: if the sense of the original model is maximization, + then this is the negative of the objective value + of the original model. + first_stage_var_shift : float or None + Infinity norm of the relative difference between first-stage + variable vectors for the current and previous iterations. + second_stage_var_shift : float or None + Infinity norm of the relative difference between second-stage + variable vectors (evaluated subject to the nominal uncertain + parameter realization) for the current and previous iterations. + dr_var_shift : float or None + Infinity norm of the relative difference between decision rule + variable vectors for the current and previous iterations. + NOTE: This value is not reported in log messages. + dr_polishing_success : bool or None + True if DR polishing was solved successfully, False otherwise. + num_violated_cons : int or None + Number of performance constraints found to be violated + during separation step. + all_sep_problems_solved : int or None + True if all separation problems were solved successfully, + False otherwise (such as if there was a time out, subsolver + error, or only a subset of the problems were solved due to + custom constraint prioritization). + global_separation : bool + True if separation problems were solved with the subordinate + global optimizer(s), False otherwise. + max_violation : int or None + Maximum scaled violation of any performance constraint + found during separation step. + elapsed_time : float + Total time elapsed up to the current iteration, in seconds. + """ + + _LINE_LENGTH = 78 + _ATTR_FORMAT_LENGTHS = { + "iteration": 5, + "objective": 13, + "first_stage_var_shift": 13, + "second_stage_var_shift": 13, + "dr_var_shift": 13, + "num_violated_cons": 8, + "max_violation": 13, + "elapsed_time": 13, + } + _ATTR_HEADER_NAMES = { + "iteration": "Itn", + "objective": "Objective", + "first_stage_var_shift": "1-Stg Shift", + "second_stage_var_shift": "2-Stg Shift", + "dr_var_shift": "DR Shift", + "num_violated_cons": "#CViol", + "max_violation": "Max Viol", + "elapsed_time": "Wall Time (s)", + } + + def __init__( + self, + iteration, + objective, + first_stage_var_shift, + second_stage_var_shift, + dr_var_shift, + dr_polishing_success, + num_violated_cons, + all_sep_problems_solved, + global_separation, + max_violation, + elapsed_time, + ): + """Initialize self (see class docstring).""" + self.iteration = iteration + self.objective = objective + self.first_stage_var_shift = first_stage_var_shift + self.second_stage_var_shift = second_stage_var_shift + self.dr_var_shift = dr_var_shift + self.dr_polishing_success = dr_polishing_success + self.num_violated_cons = num_violated_cons + self.all_sep_problems_solved = all_sep_problems_solved + self.global_separation = global_separation + self.max_violation = max_violation + self.elapsed_time = elapsed_time + + def get_log_str(self): + """Get iteration log string.""" + attrs = [ + "iteration", + "objective", + "first_stage_var_shift", + "second_stage_var_shift", + # "dr_var_shift", + "num_violated_cons", + "max_violation", + "elapsed_time", + ] + return "".join(self._format_record_attr(attr) for attr in attrs) + + def _format_record_attr(self, attr_name): + """Format attribute record for logging.""" + attr_val = getattr(self, attr_name) + if attr_val is None: + fmt_str = f"<{self._ATTR_FORMAT_LENGTHS[attr_name]}s" + return f"{'-':{fmt_str}}" + else: + attr_val_fstrs = { + "iteration": "f'{attr_val:d}'", + "objective": "f'{attr_val: .4e}'", + "first_stage_var_shift": "f'{attr_val:.4e}'", + "second_stage_var_shift": "f'{attr_val:.4e}'", + "dr_var_shift": "f'{attr_val:.4e}'", + "num_violated_cons": "f'{attr_val:d}'", + "max_violation": "f'{attr_val:.4e}'", + "elapsed_time": "f'{attr_val:.3f}'", + } + + # qualifier for DR polishing and separation columns + if attr_name in ["second_stage_var_shift", "dr_var_shift"]: + qual = "*" if not self.dr_polishing_success else "" + elif attr_name == "num_violated_cons": + qual = "+" if not self.all_sep_problems_solved else "" + elif attr_name == "max_violation": + qual = "g" if self.global_separation else "" + else: + qual = "" + + attr_val_str = f"{eval(attr_val_fstrs[attr_name])}{qual}" + + return f"{attr_val_str:{f'<{self._ATTR_FORMAT_LENGTHS[attr_name]}'}}" + + def log(self, log_func, **log_func_kwargs): + """Log self.""" + log_str = self.get_log_str() + log_func(log_str, **log_func_kwargs) + + @staticmethod + def get_log_header_str(): + """Get string for iteration log header.""" + fmt_lengths_dict = IterationLogRecord._ATTR_FORMAT_LENGTHS + header_names_dict = IterationLogRecord._ATTR_HEADER_NAMES + return "".join( + f"{header_names_dict[attr]:<{fmt_lengths_dict[attr]}s}" + for attr in fmt_lengths_dict + if attr != "dr_var_shift" + ) + + @staticmethod + def log_header(log_func, with_rules=True, **log_func_kwargs): + """Log header.""" + if with_rules: + IterationLogRecord.log_header_rule(log_func, **log_func_kwargs) + log_func(IterationLogRecord.get_log_header_str(), **log_func_kwargs) + if with_rules: + IterationLogRecord.log_header_rule(log_func, **log_func_kwargs) + + @staticmethod + def log_header_rule(log_func, fillchar="-", **log_func_kwargs): + """Log header rule.""" + log_func(fillchar * IterationLogRecord._LINE_LENGTH, **log_func_kwargs) diff --git a/pyomo/contrib/satsolver/__init__.py b/pyomo/contrib/satsolver/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/satsolver/__init__.py +++ b/pyomo/contrib/satsolver/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/satsolver/satsolver.py b/pyomo/contrib/satsolver/satsolver.py index 139b5218169..b5004d6a611 100644 --- a/pyomo/contrib/satsolver/satsolver.py +++ b/pyomo/contrib/satsolver/satsolver.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import math from pyomo.common.dependencies import attempt_import diff --git a/pyomo/contrib/satsolver/test_satsolver.py b/pyomo/contrib/satsolver/test_satsolver.py index 7ac7aaff03f..f19f172f7b2 100644 --- a/pyomo/contrib/satsolver/test_satsolver.py +++ b/pyomo/contrib/satsolver/test_satsolver.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/sensitivity_toolbox/__init__.py b/pyomo/contrib/sensitivity_toolbox/__init__.py index cac6562157e..a20cbc389d7 100644 --- a/pyomo/contrib/sensitivity_toolbox/__init__.py +++ b/pyomo/contrib/sensitivity_toolbox/__init__.py @@ -1,7 +1,18 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/sensitivity_toolbox/examples/HIV_Transmission.py b/pyomo/contrib/sensitivity_toolbox/examples/HIV_Transmission.py index 146baedd8aa..8d43dea26b2 100755 --- a/pyomo/contrib/sensitivity_toolbox/examples/HIV_Transmission.py +++ b/pyomo/contrib/sensitivity_toolbox/examples/HIV_Transmission.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -14,7 +14,7 @@ # and D.K. Owen 1998, Interfaces # -from __future__ import division + from pyomo.environ import ( ConcreteModel, Param, diff --git a/pyomo/contrib/sensitivity_toolbox/examples/__init__.py b/pyomo/contrib/sensitivity_toolbox/examples/__init__.py index 5223f39bbc1..a408b878891 100644 --- a/pyomo/contrib/sensitivity_toolbox/examples/__init__.py +++ b/pyomo/contrib/sensitivity_toolbox/examples/__init__.py @@ -1,7 +1,18 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/sensitivity_toolbox/examples/feedbackController.py b/pyomo/contrib/sensitivity_toolbox/examples/feedbackController.py index 1112a0c82b3..d973bedf5ba 100644 --- a/pyomo/contrib/sensitivity_toolbox/examples/feedbackController.py +++ b/pyomo/contrib/sensitivity_toolbox/examples/feedbackController.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/sensitivity_toolbox/examples/parameter.py b/pyomo/contrib/sensitivity_toolbox/examples/parameter.py index 93c6124701b..85d31d3303e 100644 --- a/pyomo/contrib/sensitivity_toolbox/examples/parameter.py +++ b/pyomo/contrib/sensitivity_toolbox/examples/parameter.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -13,7 +13,7 @@ # # Original implementation by Hans Pirany is in pyomo/examples/pyomo/suffixes # -from __future__ import print_function + from pyomo.environ import ( ConcreteModel, Param, diff --git a/pyomo/contrib/sensitivity_toolbox/examples/parameter_kaug.py b/pyomo/contrib/sensitivity_toolbox/examples/parameter_kaug.py index f54e7903442..c5e61307046 100644 --- a/pyomo/contrib/sensitivity_toolbox/examples/parameter_kaug.py +++ b/pyomo/contrib/sensitivity_toolbox/examples/parameter_kaug.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/sensitivity_toolbox/examples/rangeInequality.py b/pyomo/contrib/sensitivity_toolbox/examples/rangeInequality.py index 39e4d26f695..b06cc8390d2 100644 --- a/pyomo/contrib/sensitivity_toolbox/examples/rangeInequality.py +++ b/pyomo/contrib/sensitivity_toolbox/examples/rangeInequality.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/sensitivity_toolbox/examples/rooney_biegler.py b/pyomo/contrib/sensitivity_toolbox/examples/rooney_biegler.py index 701d3f71bb4..3efb20bd44b 100644 --- a/pyomo/contrib/sensitivity_toolbox/examples/rooney_biegler.py +++ b/pyomo/contrib/sensitivity_toolbox/examples/rooney_biegler.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + ############################################################################## # Institute for the Design of Advanced Energy Systems Process Systems # Engineering Framework (IDAES PSE Framework) Copyright (c) 2018-2019, by the @@ -15,7 +26,7 @@ model parameter uncertainty using nonlinear confidence regions. AIChE Journal, 47(8), 1794-1804. """ -import pandas as pd +from pyomo.common.dependencies import pandas as pd import pyomo.environ as pyo diff --git a/pyomo/contrib/sensitivity_toolbox/k_aug.py b/pyomo/contrib/sensitivity_toolbox/k_aug.py index 8d739506492..a7fc10569fe 100644 --- a/pyomo/contrib/sensitivity_toolbox/k_aug.py +++ b/pyomo/contrib/sensitivity_toolbox/k_aug.py @@ -1,7 +1,18 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ______________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/sensitivity_toolbox/sens.py b/pyomo/contrib/sensitivity_toolbox/sens.py index e1c69d75974..a3d69b2c7b1 100644 --- a/pyomo/contrib/sensitivity_toolbox/sens.py +++ b/pyomo/contrib/sensitivity_toolbox/sens.py @@ -1,7 +1,18 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ______________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/sensitivity_toolbox/tests/__init__.py b/pyomo/contrib/sensitivity_toolbox/tests/__init__.py index 557846ee521..53f447ece43 100644 --- a/pyomo/contrib/sensitivity_toolbox/tests/__init__.py +++ b/pyomo/contrib/sensitivity_toolbox/tests/__init__.py @@ -1,7 +1,18 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/sensitivity_toolbox/tests/test_k_aug_interface.py b/pyomo/contrib/sensitivity_toolbox/tests/test_k_aug_interface.py index 8c14cfc91d0..e941656a392 100644 --- a/pyomo/contrib/sensitivity_toolbox/tests/test_k_aug_interface.py +++ b/pyomo/contrib/sensitivity_toolbox/tests/test_k_aug_interface.py @@ -1,7 +1,18 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ____________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/sensitivity_toolbox/tests/test_sens.py b/pyomo/contrib/sensitivity_toolbox/tests/test_sens.py index f4b3fb5548c..69cf0303987 100644 --- a/pyomo/contrib/sensitivity_toolbox/tests/test_sens.py +++ b/pyomo/contrib/sensitivity_toolbox/tests/test_sens.py @@ -1,7 +1,18 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ____________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/sensitivity_toolbox/tests/test_sens_unit.py b/pyomo/contrib/sensitivity_toolbox/tests/test_sens_unit.py index 05faada3007..9f4bcb2b497 100644 --- a/pyomo/contrib/sensitivity_toolbox/tests/test_sens_unit.py +++ b/pyomo/contrib/sensitivity_toolbox/tests/test_sens_unit.py @@ -1,7 +1,18 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ____________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/simplemodel/__init__.py b/pyomo/contrib/simplemodel/__init__.py index 4fa4fa2dd16..f2f4922223e 100644 --- a/pyomo/contrib/simplemodel/__init__.py +++ b/pyomo/contrib/simplemodel/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/simplification/__init__.py b/pyomo/contrib/simplification/__init__.py new file mode 100644 index 00000000000..c6111ddcb89 --- /dev/null +++ b/pyomo/contrib/simplification/__init__.py @@ -0,0 +1,12 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from .simplify import Simplifier diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py new file mode 100644 index 00000000000..b4bec63088a --- /dev/null +++ b/pyomo/contrib/simplification/build.py @@ -0,0 +1,209 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import glob +import logging +import os +import shutil +import sys +import subprocess + +from pyomo.common.download import FileDownloader +from pyomo.common.envvar import PYOMO_CONFIG_DIR +from pyomo.common.fileutils import find_library, this_file_dir +from pyomo.common.tempfiles import TempfileManager + +logger = logging.getLogger(__name__ if __name__ != '__main__' else 'pyomo') + + +def build_ginac_library(parallel=None, argv=None, env=None): + sys.stdout.write("\n**** Building GiNaC library ****\n") + + configure_cmd = [ + os.path.join('.', 'configure'), + '--prefix=' + PYOMO_CONFIG_DIR, + '--disable-static', + ] + make_cmd = ['make'] + if parallel: + make_cmd.append(f'-j{parallel}') + install_cmd = ['make', 'install'] + + env = dict(os.environ) + pcdir = os.path.join(PYOMO_CONFIG_DIR, 'lib', 'pkgconfig') + if 'PKG_CONFIG_PATH' in env: + pcdir += os.pathsep + env['PKG_CONFIG_PATH'] + env['PKG_CONFIG_PATH'] = pcdir + + with TempfileManager.new_context() as tempfile: + tmpdir = tempfile.mkdtemp() + + downloader = FileDownloader() + if argv: + downloader.parse_args(argv) + + url = 'https://www.ginac.de/CLN/cln-1.3.7.tar.bz2' + cln_dir = os.path.join(tmpdir, 'cln') + downloader.set_destination_filename(cln_dir) + logger.info( + "Fetching CLN from %s and installing it to %s" + % (url, downloader.destination()) + ) + downloader.get_tar_archive(url, dirOffset=1) + assert subprocess.run(configure_cmd, cwd=cln_dir, env=env).returncode == 0 + logger.info("\nBuilding CLN\n") + assert subprocess.run(make_cmd, cwd=cln_dir, env=env).returncode == 0 + assert subprocess.run(install_cmd, cwd=cln_dir, env=env).returncode == 0 + + url = 'https://www.ginac.de/ginac-1.8.7.tar.bz2' + ginac_dir = os.path.join(tmpdir, 'ginac') + downloader.set_destination_filename(ginac_dir) + logger.info( + "Fetching GiNaC from %s and installing it to %s" + % (url, downloader.destination()) + ) + downloader.get_tar_archive(url, dirOffset=1) + assert subprocess.run(configure_cmd, cwd=ginac_dir, env=env).returncode == 0 + logger.info("\nBuilding GiNaC\n") + assert subprocess.run(make_cmd, cwd=ginac_dir, env=env).returncode == 0 + assert subprocess.run(install_cmd, cwd=ginac_dir, env=env).returncode == 0 + print("Installed GiNaC to %s" % (ginac_dir,)) + + +def _find_include(libdir, incpaths): + rel_path = ('include',) + incpaths + while 1: + basedir = os.path.dirname(libdir) + if not basedir or basedir == libdir: + return None + if os.path.exists(os.path.join(basedir, *rel_path)): + return os.path.join(basedir, *(rel_path[: -len(incpaths)])) + libdir = basedir + + +def build_ginac_interface(parallel=None, args=None): + from distutils.dist import Distribution + from pybind11.setup_helpers import Pybind11Extension, build_ext + from pyomo.common.cmake_builder import handleReadonly + + sys.stdout.write("\n**** Building GiNaC interface ****\n") + + if args is None: + args = [] + sources = [ + os.path.join(this_file_dir(), 'ginac', 'src', fname) + for fname in ['ginac_interface.cpp'] + ] + + ginac_lib = find_library('ginac') + if not ginac_lib: + raise RuntimeError( + 'could not find the GiNaC library; please make sure either to install ' + 'the library and development headers system-wide, or include the ' + 'path to the library in the LD_LIBRARY_PATH environment variable' + ) + ginac_lib_dir = os.path.dirname(ginac_lib) + ginac_include_dir = _find_include(ginac_lib_dir, ('ginac', 'ginac.h')) + if not ginac_include_dir: + raise RuntimeError('could not find GiNaC include directory') + + cln_lib = find_library('cln') + if not cln_lib: + raise RuntimeError( + 'could not find the CLN library; please make sure either to install ' + 'the library and development headers system-wide, or include the ' + 'path to the library in the LD_LIBRARY_PATH environment variable' + ) + cln_lib_dir = os.path.dirname(cln_lib) + cln_include_dir = _find_include(cln_lib_dir, ('cln', 'cln.h')) + if not cln_include_dir: + raise RuntimeError('could not find CLN include directory') + + extra_args = ['-std=c++11'] + ext = Pybind11Extension( + 'ginac_interface', + sources=sources, + language='c++', + include_dirs=[cln_include_dir, ginac_include_dir], + library_dirs=[cln_lib_dir, ginac_lib_dir], + libraries=['cln', 'ginac'], + extra_compile_args=extra_args, + ) + + class ginacBuildExt(build_ext): + def run(self): + basedir = os.path.abspath(os.path.curdir) + with TempfileManager.new_context() as tempfile: + if self.inplace: + tmpdir = os.path.join(this_file_dir(), 'ginac') + else: + tmpdir = os.path.abspath(tempfile.mkdtemp()) + sys.stdout.write("Building in '%s'\n" % tmpdir) + os.chdir(tmpdir) + super(ginacBuildExt, self).run() + if not self.inplace: + library = glob.glob("build/*/ginac_interface.*")[0] + target = os.path.join( + PYOMO_CONFIG_DIR, + 'lib', + 'python%s.%s' % sys.version_info[:2], + 'site-packages', + '.', + ) + if not os.path.exists(target): + os.makedirs(target) + sys.stdout.write(f"Installing {library} in {target}\n") + shutil.copy(library, target) + + package_config = { + 'name': 'ginac_interface', + 'packages': [], + 'ext_modules': [ext], + 'cmdclass': {"build_ext": ginacBuildExt}, + } + + dist = Distribution(package_config) + dist.script_args = ['build_ext'] + args + dist.parse_command_line() + dist.run_command('build_ext') + + +class GiNaCInterfaceBuilder(object): + def __call__(self, parallel): + return build_ginac_interface(parallel) + + def skip(self): + return not find_library('ginac') + + +if __name__ == '__main__': + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument( + "-j", + dest='parallel', + type=int, + default=None, + help="Enable parallel build with PARALLEL cores", + ) + parser.add_argument( + "--build-deps", + dest='build_deps', + action='store_true', + default=False, + help="Download and build the CLN/GiNaC libraries", + ) + options, argv = parser.parse_known_args(sys.argv) + logging.getLogger('pyomo').setLevel(logging.INFO) + if options.build_deps: + build_ginac_library(options.parallel, []) + build_ginac_interface(options.parallel, argv[1:]) diff --git a/pyomo/contrib/simplification/ginac/__init__.py b/pyomo/contrib/simplification/ginac/__init__.py new file mode 100644 index 00000000000..6896bec12c4 --- /dev/null +++ b/pyomo/contrib/simplification/ginac/__init__.py @@ -0,0 +1,52 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common.dependencies import attempt_import as _attempt_import + + +def _importer(): + import os + import sys + from ctypes import cdll + from pyomo.common.envvar import PYOMO_CONFIG_DIR + from pyomo.common.fileutils import find_library + + try: + pyomo_config_dir = os.path.join( + PYOMO_CONFIG_DIR, + 'lib', + 'python%s.%s' % sys.version_info[:2], + 'site-packages', + ) + sys.path.insert(0, pyomo_config_dir) + # GiNaC needs 2 libraries that are generally dynamically linked + # to the interface library. If we built those ourselves, then + # the libraries will be PYOMO_CONFIG_DIR/lib ... but that + # directory is very likely to NOT be on the library search path + # when the Python interpreter was started. We will manually + # look for those two libraries, and if we find them, load them + # into this process (so the interface can find them) + for lib in ('cln', 'ginac'): + fname = find_library(lib) + if fname is not None: + cdll.LoadLibrary(fname) + + import ginac_interface + except ImportError: + from . import ginac_interface + finally: + assert sys.path[0] == pyomo_config_dir + sys.path.pop(0) + + return ginac_interface + + +interface, interface_available = _attempt_import('ginac_interface', importer=_importer) diff --git a/pyomo/contrib/simplification/ginac/src/ginac_interface.cpp b/pyomo/contrib/simplification/ginac/src/ginac_interface.cpp new file mode 100644 index 00000000000..9b05baf71ca --- /dev/null +++ b/pyomo/contrib/simplification/ginac/src/ginac_interface.cpp @@ -0,0 +1,332 @@ +// ___________________________________________________________________________ +// +// Pyomo: Python Optimization Modeling Objects +// Copyright (c) 2008-2024 +// National Technology and Engineering Solutions of Sandia, LLC +// Under the terms of Contract DE-NA0003525 with National Technology and +// Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +// rights in this software. +// This software is distributed under the 3-clause BSD License. +// ___________________________________________________________________________ + +#include "ginac_interface.hpp" + + +bool is_integer(double x) { + return std::floor(x) == x; +} + + +ex ginac_expr_from_pyomo_node( + py::handle expr, + std::unordered_map &leaf_map, + std::unordered_map &ginac_pyomo_map, + PyomoExprTypes &expr_types, + bool symbolic_solver_labels + ) { + ex res; + ExprType tmp_type = + expr_types.expr_type_map[py::type::of(expr)].cast(); + + switch (tmp_type) { + case py_float: { + double val = expr.cast(); + if (is_integer(val)) { + res = numeric((long) val); + } + else { + res = numeric(val); + } + break; + } + case var: { + long expr_id = expr_types.id(expr).cast(); + if (leaf_map.count(expr_id) == 0) { + std::string vname; + if (symbolic_solver_labels) { + vname = expr.attr("name").cast(); + } + else { + vname = "x" + std::to_string(expr_id); + } + py::object lb = expr.attr("lb"); + if (lb.is_none() || lb.cast() < 0) { + leaf_map[expr_id] = realsymbol(vname); + } + else { + leaf_map[expr_id] = possymbol(vname); + } + ginac_pyomo_map[leaf_map[expr_id]] = expr.cast(); + } + res = leaf_map[expr_id]; + break; + } + case param: { + long expr_id = expr_types.id(expr).cast(); + if (leaf_map.count(expr_id) == 0) { + std::string pname; + if (symbolic_solver_labels) { + pname = expr.attr("name").cast(); + } + else { + pname = "p" + std::to_string(expr_id); + } + leaf_map[expr_id] = realsymbol(pname); + ginac_pyomo_map[leaf_map[expr_id]] = expr.cast(); + } + res = leaf_map[expr_id]; + break; + } + case product: { + py::list pyomo_args = expr.attr("args"); + res = ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels) * ginac_expr_from_pyomo_node(pyomo_args[1], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels); + break; + } + case sum: { + py::list pyomo_args = expr.attr("args"); + for (py::handle arg : pyomo_args) { + res += ginac_expr_from_pyomo_node(arg, leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels); + } + break; + } + case negation: { + py::list pyomo_args = expr.attr("args"); + res = - ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels); + break; + } + case external_func: { + long expr_id = expr_types.id(expr).cast(); + if (leaf_map.count(expr_id) == 0) { + leaf_map[expr_id] = realsymbol("f" + std::to_string(expr_id)); + ginac_pyomo_map[leaf_map[expr_id]] = expr.cast(); + } + res = leaf_map[expr_id]; + break; + } + case ExprType::power: { + py::list pyomo_args = expr.attr("args"); + res = pow(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels), ginac_expr_from_pyomo_node(pyomo_args[1], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); + break; + } + case division: { + py::list pyomo_args = expr.attr("args"); + res = ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels) / ginac_expr_from_pyomo_node(pyomo_args[1], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels); + break; + } + case unary_func: { + std::string function_name = expr.attr("getname")().cast(); + py::list pyomo_args = expr.attr("args"); + if (function_name == "exp") + res = exp(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); + else if (function_name == "log") + res = log(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); + else if (function_name == "sin") + res = sin(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); + else if (function_name == "cos") + res = cos(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); + else if (function_name == "tan") + res = tan(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); + else if (function_name == "asin") + res = asin(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); + else if (function_name == "acos") + res = acos(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); + else if (function_name == "atan") + res = atan(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); + else if (function_name == "sqrt") + res = sqrt(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); + else + throw py::value_error("Unrecognized expression type: " + function_name); + break; + } + case linear: { + py::list pyomo_args = expr.attr("args"); + for (py::handle arg : pyomo_args) { + res += ginac_expr_from_pyomo_node(arg, leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels); + } + break; + } + case named_expr: { + res = ginac_expr_from_pyomo_node(expr.attr("expr"), leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels); + break; + } + case numeric_constant: { + res = numeric(expr.attr("value").cast()); + break; + } + case pyomo_unit: { + res = numeric(1.0); + break; + } + case unary_abs: { + py::list pyomo_args = expr.attr("args"); + res = abs(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); + break; + } + default: { + throw py::value_error("Unrecognized expression type: " + + expr_types.builtins.attr("str")(py::type::of(expr)) + .cast()); + break; + } + } + return res; +} + +ex pyomo_expr_to_ginac_expr( + py::handle expr, + std::unordered_map &leaf_map, + std::unordered_map &ginac_pyomo_map, + PyomoExprTypes &expr_types, + bool symbolic_solver_labels + ) { + ex res = ginac_expr_from_pyomo_node(expr, leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels); + return res; + } + +ex pyomo_to_ginac(py::handle expr, PyomoExprTypes &expr_types) { + std::unordered_map leaf_map; + std::unordered_map ginac_pyomo_map; + ex res = ginac_expr_from_pyomo_node(expr, leaf_map, ginac_pyomo_map, expr_types, true); + return res; +} + + +class GinacToPyomoVisitor +: public visitor, + public symbol::visitor, + public numeric::visitor, + public add::visitor, + public mul::visitor, + public GiNaC::power::visitor, + public function::visitor, + public basic::visitor +{ + public: + std::unordered_map *leaf_map; + std::unordered_map node_map; + PyomoExprTypes *expr_types; + + GinacToPyomoVisitor(std::unordered_map *_leaf_map, PyomoExprTypes *_expr_types) : leaf_map(_leaf_map), expr_types(_expr_types) {} + ~GinacToPyomoVisitor() = default; + + void visit(const symbol& e) { + node_map[e] = leaf_map->at(e); + } + + void visit(const numeric& e) { + double val = e.to_double(); + node_map[e] = expr_types->NumericConstant(py::cast(val)); + } + + void visit(const add& e) { + size_t n = e.nops(); + py::object pe = node_map[e.op(0)]; + for (unsigned long ndx=1; ndx < n; ++ndx) { + pe = pe.attr("__add__")(node_map[e.op(ndx)]); + } + node_map[e] = pe; + } + + void visit(const mul& e) { + size_t n = e.nops(); + py::object pe = node_map[e.op(0)]; + for (unsigned long ndx=1; ndx < n; ++ndx) { + pe = pe.attr("__mul__")(node_map[e.op(ndx)]); + } + node_map[e] = pe; + } + + void visit(const GiNaC::power& e) { + py::object arg1 = node_map[e.op(0)]; + py::object arg2 = node_map[e.op(1)]; + py::object pe = arg1.attr("__pow__")(arg2); + node_map[e] = pe; + } + + void visit(const function& e) { + py::object arg = node_map[e.op(0)]; + std::string func_type = e.get_name(); + py::object pe; + if (func_type == "exp") { + pe = expr_types->exp(arg); + } + else if (func_type == "log") { + pe = expr_types->log(arg); + } + else if (func_type == "sin") { + pe = expr_types->sin(arg); + } + else if (func_type == "cos") { + pe = expr_types->cos(arg); + } + else if (func_type == "tan") { + pe = expr_types->tan(arg); + } + else if (func_type == "asin") { + pe = expr_types->asin(arg); + } + else if (func_type == "acos") { + pe = expr_types->acos(arg); + } + else if (func_type == "atan") { + pe = expr_types->atan(arg); + } + else if (func_type == "sqrt") { + pe = expr_types->sqrt(arg); + } + else { + throw py::value_error("unrecognized unary function: " + func_type); + } + node_map[e] = pe; + } + + void visit(const basic& e) { + throw py::value_error("unrecognized ginac expression type"); + } +}; + + +ex GinacInterface::to_ginac(py::handle expr) { + return pyomo_expr_to_ginac_expr(expr, leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels); +} + +py::object GinacInterface::from_ginac(ex &ge) { + GinacToPyomoVisitor v(&ginac_pyomo_map, &expr_types); + ge.traverse_postorder(v); + return v.node_map[ge]; +} + +PYBIND11_MODULE(ginac_interface, m) { + m.def("pyomo_to_ginac", &pyomo_to_ginac); + py::class_(m, "PyomoExprTypes", py::module_local()) + .def(py::init<>()); + py::class_(m, "ginac_expression") + .def("expand", [](ex &ge) { + return ge.expand(); + }) + .def("normal", &ex::normal) + .def("__str__", [](ex &ge) { + std::ostringstream stream; + stream << ge; + return stream.str(); + }); + py::class_(m, "GinacInterface") + .def(py::init()) + .def("to_ginac", &GinacInterface::to_ginac) + .def("from_ginac", &GinacInterface::from_ginac); + py::enum_(m, "ExprType", py::module_local()) + .value("py_float", ExprType::py_float) + .value("var", ExprType::var) + .value("param", ExprType::param) + .value("product", ExprType::product) + .value("sum", ExprType::sum) + .value("negation", ExprType::negation) + .value("external_func", ExprType::external_func) + .value("power", ExprType::power) + .value("division", ExprType::division) + .value("unary_func", ExprType::unary_func) + .value("linear", ExprType::linear) + .value("named_expr", ExprType::named_expr) + .value("numeric_constant", ExprType::numeric_constant) + .export_values(); +} diff --git a/pyomo/contrib/simplification/ginac/src/ginac_interface.hpp b/pyomo/contrib/simplification/ginac/src/ginac_interface.hpp new file mode 100644 index 00000000000..bc5b0d7b6fc --- /dev/null +++ b/pyomo/contrib/simplification/ginac/src/ginac_interface.hpp @@ -0,0 +1,190 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#define PYBIND11_DETAILED_ERROR_MESSAGES + +namespace py = pybind11; +using namespace pybind11::literals; +using namespace GiNaC; + +enum ExprType { + py_float = 0, + var = 1, + param = 2, + product = 3, + sum = 4, + negation = 5, + external_func = 6, + power = 7, + division = 8, + unary_func = 9, + linear = 10, + named_expr = 11, + numeric_constant = 12, + pyomo_unit = 13, + unary_abs = 14 +}; + +class PyomoExprTypes { +public: + PyomoExprTypes() { + expr_type_map[int_] = py_float; + expr_type_map[float_] = py_float; + expr_type_map[np_int16] = py_float; + expr_type_map[np_int32] = py_float; + expr_type_map[np_int64] = py_float; + expr_type_map[np_longlong] = py_float; + expr_type_map[np_uint16] = py_float; + expr_type_map[np_uint32] = py_float; + expr_type_map[np_uint64] = py_float; + expr_type_map[np_ulonglong] = py_float; + expr_type_map[np_float16] = py_float; + expr_type_map[np_float32] = py_float; + expr_type_map[np_float64] = py_float; + expr_type_map[ScalarVar] = var; + expr_type_map[_GeneralVarData] = var; + expr_type_map[AutoLinkedBinaryVar] = var; + expr_type_map[ScalarParam] = param; + expr_type_map[_ParamData] = param; + expr_type_map[MonomialTermExpression] = product; + expr_type_map[ProductExpression] = product; + expr_type_map[NPV_ProductExpression] = product; + expr_type_map[SumExpression] = sum; + expr_type_map[NPV_SumExpression] = sum; + expr_type_map[NegationExpression] = negation; + expr_type_map[NPV_NegationExpression] = negation; + expr_type_map[ExternalFunctionExpression] = external_func; + expr_type_map[NPV_ExternalFunctionExpression] = external_func; + expr_type_map[PowExpression] = ExprType::power; + expr_type_map[NPV_PowExpression] = ExprType::power; + expr_type_map[DivisionExpression] = division; + expr_type_map[NPV_DivisionExpression] = division; + expr_type_map[UnaryFunctionExpression] = unary_func; + expr_type_map[NPV_UnaryFunctionExpression] = unary_func; + expr_type_map[LinearExpression] = linear; + expr_type_map[_GeneralExpressionData] = named_expr; + expr_type_map[ScalarExpression] = named_expr; + expr_type_map[Integral] = named_expr; + expr_type_map[ScalarIntegral] = named_expr; + expr_type_map[NumericConstant] = numeric_constant; + expr_type_map[_PyomoUnit] = pyomo_unit; + expr_type_map[AbsExpression] = unary_abs; + expr_type_map[NPV_AbsExpression] = unary_abs; + } + ~PyomoExprTypes() = default; + py::int_ ione = 1; + py::float_ fone = 1.0; + py::type int_ = py::type::of(ione); + py::type float_ = py::type::of(fone); + py::object np = py::module_::import("numpy"); + py::type np_int16 = np.attr("int16"); + py::type np_int32 = np.attr("int32"); + py::type np_int64 = np.attr("int64"); + py::type np_longlong = np.attr("longlong"); + py::type np_uint16 = np.attr("uint16"); + py::type np_uint32 = np.attr("uint32"); + py::type np_uint64 = np.attr("uint64"); + py::type np_ulonglong = np.attr("ulonglong"); + py::type np_float16 = np.attr("float16"); + py::type np_float32 = np.attr("float32"); + py::type np_float64 = np.attr("float64"); + py::object ScalarParam = + py::module_::import("pyomo.core.base.param").attr("ScalarParam"); + py::object _ParamData = + py::module_::import("pyomo.core.base.param").attr("_ParamData"); + py::object ScalarVar = + py::module_::import("pyomo.core.base.var").attr("ScalarVar"); + py::object _GeneralVarData = + py::module_::import("pyomo.core.base.var").attr("_GeneralVarData"); + py::object AutoLinkedBinaryVar = + py::module_::import("pyomo.gdp.disjunct").attr("AutoLinkedBinaryVar"); + py::object numeric_expr = py::module_::import("pyomo.core.expr.numeric_expr"); + py::object NegationExpression = numeric_expr.attr("NegationExpression"); + py::object NPV_NegationExpression = + numeric_expr.attr("NPV_NegationExpression"); + py::object ExternalFunctionExpression = + numeric_expr.attr("ExternalFunctionExpression"); + py::object NPV_ExternalFunctionExpression = + numeric_expr.attr("NPV_ExternalFunctionExpression"); + py::object PowExpression = numeric_expr.attr("PowExpression"); + py::object NPV_PowExpression = numeric_expr.attr("NPV_PowExpression"); + py::object ProductExpression = numeric_expr.attr("ProductExpression"); + py::object NPV_ProductExpression = numeric_expr.attr("NPV_ProductExpression"); + py::object MonomialTermExpression = + numeric_expr.attr("MonomialTermExpression"); + py::object DivisionExpression = numeric_expr.attr("DivisionExpression"); + py::object NPV_DivisionExpression = + numeric_expr.attr("NPV_DivisionExpression"); + py::object SumExpression = numeric_expr.attr("SumExpression"); + py::object NPV_SumExpression = numeric_expr.attr("NPV_SumExpression"); + py::object UnaryFunctionExpression = + numeric_expr.attr("UnaryFunctionExpression"); + py::object AbsExpression = numeric_expr.attr("AbsExpression"); + py::object NPV_AbsExpression = numeric_expr.attr("NPV_AbsExpression"); + py::object NPV_UnaryFunctionExpression = + numeric_expr.attr("NPV_UnaryFunctionExpression"); + py::object LinearExpression = numeric_expr.attr("LinearExpression"); + py::object NumericConstant = + py::module_::import("pyomo.core.expr.numvalue").attr("NumericConstant"); + py::object expr_module = py::module_::import("pyomo.core.base.expression"); + py::object _GeneralExpressionData = + expr_module.attr("_GeneralExpressionData"); + py::object ScalarExpression = expr_module.attr("ScalarExpression"); + py::object ScalarIntegral = + py::module_::import("pyomo.dae.integral").attr("ScalarIntegral"); + py::object Integral = + py::module_::import("pyomo.dae.integral").attr("Integral"); + py::object _PyomoUnit = + py::module_::import("pyomo.core.base.units_container").attr("_PyomoUnit"); + py::object exp = numeric_expr.attr("exp"); + py::object log = numeric_expr.attr("log"); + py::object sin = numeric_expr.attr("sin"); + py::object cos = numeric_expr.attr("cos"); + py::object tan = numeric_expr.attr("tan"); + py::object asin = numeric_expr.attr("asin"); + py::object acos = numeric_expr.attr("acos"); + py::object atan = numeric_expr.attr("atan"); + py::object sqrt = numeric_expr.attr("sqrt"); + py::object builtins = py::module_::import("builtins"); + py::object id = builtins.attr("id"); + py::object len = builtins.attr("len"); + py::dict expr_type_map; +}; + +ex pyomo_to_ginac(py::handle expr, PyomoExprTypes &expr_types); + + +class GinacInterface { + public: + std::unordered_map leaf_map; + std::unordered_map ginac_pyomo_map; + PyomoExprTypes expr_types; + bool symbolic_solver_labels = false; + + GinacInterface() = default; + GinacInterface(bool _symbolic_solver_labels) : symbolic_solver_labels(_symbolic_solver_labels) {} + ~GinacInterface() = default; + + ex to_ginac(py::handle expr); + py::object from_ginac(ex &ginac_expr); +}; diff --git a/pyomo/contrib/simplification/plugins.py b/pyomo/contrib/simplification/plugins.py new file mode 100644 index 00000000000..6b08f7be4d7 --- /dev/null +++ b/pyomo/contrib/simplification/plugins.py @@ -0,0 +1,17 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common.extensions import ExtensionBuilderFactory +from .build import GiNaCInterfaceBuilder + + +def load(): + ExtensionBuilderFactory.register('ginac')(GiNaCInterfaceBuilder) diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py new file mode 100644 index 00000000000..874b5b1e801 --- /dev/null +++ b/pyomo/contrib/simplification/simplify.py @@ -0,0 +1,75 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import logging +import warnings + +from pyomo.common.enums import NamedIntEnum +from pyomo.core.expr.sympy_tools import sympy2pyomo_expression, sympyify_expression +from pyomo.core.expr.numeric_expr import NumericExpression +from pyomo.core.expr.numvalue import value, is_constant + +from pyomo.contrib.simplification.ginac import ( + interface as ginac_interface, + interface_available as ginac_available, +) + + +def simplify_with_sympy(expr: NumericExpression): + if is_constant(expr): + return value(expr) + object_map, sympy_expr = sympyify_expression(expr, keep_mutable_parameters=True) + new_expr = sympy2pyomo_expression(sympy_expr.simplify(), object_map) + if is_constant(new_expr): + new_expr = value(new_expr) + return new_expr + + +def simplify_with_ginac(expr: NumericExpression, ginac_interface): + if is_constant(expr): + return value(expr) + ginac_expr = ginac_interface.to_ginac(expr) + return ginac_interface.from_ginac(ginac_expr.normal()) + + +class Simplifier(object): + class Mode(NamedIntEnum): + auto = 0 + sympy = 1 + ginac = 2 + + def __init__( + self, suppress_no_ginac_warnings: bool = False, mode: Mode = Mode.auto + ) -> None: + if mode == Simplifier.Mode.auto: + if ginac_available: + mode = Simplifier.Mode.ginac + else: + if not suppress_no_ginac_warnings: + msg = ( + "GiNaC does not seem to be available. Using SymPy. " + + "Note that the GiNaC interface is significantly faster." + ) + logging.getLogger(__name__).warning(msg) + warnings.warn(msg) + mode = Simplifier.Mode.sympy + + if mode == Simplifier.Mode.ginac: + self.gi = ginac_interface.GinacInterface(False) + self.simplify = self._simplify_with_ginac + else: + self.simplify = self._simplify_with_sympy + + def _simplify_with_ginac(self, expr: NumericExpression): + return simplify_with_ginac(expr, self.gi) + + def _simplify_with_sympy(self, expr: NumericExpression): + return simplify_with_sympy(expr) diff --git a/pyomo/contrib/simplification/tests/__init__.py b/pyomo/contrib/simplification/tests/__init__.py new file mode 100644 index 00000000000..a4a626013c4 --- /dev/null +++ b/pyomo/contrib/simplification/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py new file mode 100644 index 00000000000..1ff9f5a3cc4 --- /dev/null +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -0,0 +1,123 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common import unittest +from pyomo.common.fileutils import this_file_dir +from pyomo.contrib.simplification import Simplifier +from pyomo.contrib.simplification.simplify import ginac_available +from pyomo.core.expr.compare import assertExpressionsEqual, compare_expressions +from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd +from pyomo.core.expr.sympy_tools import sympy_available + +import pyomo.environ as pe + + +class SimplificationMixin: + def compare_against_possible_results(self, got, expected_list): + success = False + for exp in expected_list: + if compare_expressions(got, exp): + success = True + break + self.assertTrue(success) + + def test_simplify(self): + m = pe.ConcreteModel() + x = m.x = pe.Var(bounds=(0, None)) + e = x * pe.log(x) + der1 = reverse_sd(e)[x] + der2 = reverse_sd(der1)[x] + der2_simp = self.simp.simplify(der2) + expected = x**-1.0 + assertExpressionsEqual(self, expected, der2_simp) + + def test_mul(self): + m = pe.ConcreteModel() + x = m.x = pe.Var() + e = 2 * x + e2 = self.simp.simplify(e) + expected = 2.0 * x + assertExpressionsEqual(self, expected, e2) + + def test_sum(self): + m = pe.ConcreteModel() + x = m.x = pe.Var() + e = 2 + x + e2 = self.simp.simplify(e) + self.compare_against_possible_results(e2, [2.0 + x, x + 2.0]) + + def test_neg(self): + m = pe.ConcreteModel() + x = m.x = pe.Var() + e = -pe.log(x) + e2 = self.simp.simplify(e) + self.compare_against_possible_results( + e2, [(-1.0) * pe.log(x), pe.log(x) * (-1.0), -pe.log(x)] + ) + + def test_pow(self): + m = pe.ConcreteModel() + x = m.x = pe.Var() + e = x**2.0 + e2 = self.simp.simplify(e) + assertExpressionsEqual(self, e, e2) + + def test_div(self): + m = pe.ConcreteModel() + x = m.x = pe.Var() + y = m.y = pe.Var() + e = x / y + y / x - x / y + e2 = self.simp.simplify(e) + self.compare_against_possible_results( + e2, [y / x, y * (1.0 / x), y * x**-1.0, x**-1.0 * y] + ) + + def test_unary(self): + m = pe.ConcreteModel() + x = m.x = pe.Var() + func_list = [pe.log, pe.sin, pe.cos, pe.tan, pe.asin, pe.acos, pe.atan] + for func in func_list: + e = func(x) + e2 = self.simp.simplify(e) + assertExpressionsEqual(self, e, e2) + + def test_param(self): + m = pe.ConcreteModel() + x = m.x = pe.Var() + p = m.p = pe.Param(mutable=True) + e1 = p * x**2 + p * x + p * x**2 + e2 = self.simp.simplify(e1) + self.compare_against_possible_results( + e2, + [ + p * x**2.0 * 2.0 + p * x, + p * x + p * x**2.0 * 2.0, + 2.0 * p * x**2.0 + p * x, + p * x + 2.0 * p * x**2.0, + x**2.0 * p * 2.0 + p * x, + p * x + x**2.0 * p * 2.0, + p * x * (1 + 2 * x), + ], + ) + + +@unittest.skipUnless(sympy_available, 'sympy is not available') +class TestSimplificationSympy(unittest.TestCase, SimplificationMixin): + def setUp(self): + self.simp = Simplifier(mode=Simplifier.Mode.sympy) + + +@unittest.pytest.mark.default +@unittest.pytest.mark.builders +@unittest.skipUnless(ginac_available, 'GiNaC is not available') +class TestSimplificationGiNaC(unittest.TestCase, SimplificationMixin): + def setUp(self): + self.simp = Simplifier(mode=Simplifier.Mode.ginac) diff --git a/pyomo/contrib/solver/__init__.py b/pyomo/contrib/solver/__init__.py new file mode 100644 index 00000000000..a4a626013c4 --- /dev/null +++ b/pyomo/contrib/solver/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py new file mode 100644 index 00000000000..98bf3836004 --- /dev/null +++ b/pyomo/contrib/solver/base.py @@ -0,0 +1,638 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import abc +import enum +from typing import Sequence, Dict, Optional, Mapping, NoReturn, List, Tuple +import os + +from pyomo.core.base.constraint import ConstraintData +from pyomo.core.base.var import VarData +from pyomo.core.base.param import ParamData +from pyomo.core.base.block import BlockData +from pyomo.core.base.objective import Objective, ObjectiveData +from pyomo.common.config import document_kwargs_from_configdict, ConfigValue +from pyomo.common.errors import ApplicationError +from pyomo.common.deprecation import deprecation_warning +from pyomo.common.modeling import NOTSET +from pyomo.opt.results.results_ import SolverResults as LegacySolverResults +from pyomo.opt.results.solution import Solution as LegacySolution +from pyomo.core.kernel.objective import minimize +from pyomo.core.base import SymbolMap +from pyomo.core.base.label import NumericLabeler +from pyomo.core.staleflag import StaleFlagManager +from pyomo.contrib.solver.config import SolverConfig, PersistentSolverConfig +from pyomo.contrib.solver.util import get_objective +from pyomo.contrib.solver.results import ( + Results, + legacy_solver_status_map, + legacy_termination_condition_map, + legacy_solution_status_map, +) + + +class SolverBase(abc.ABC): + """ + This base class defines the methods required for all solvers: + - available: Determines whether the solver is able to be run, + combining both whether it can be found on the system and if the license is valid. + - solve: The main method of every solver + - version: The version of the solver + - is_persistent: Set to false for all non-persistent solvers. + + Additionally, solvers should have a :attr:`config` attribute that + inherits from one of :class:`SolverConfig`, + :class:`BranchAndBoundConfig`, + :class:`PersistentSolverConfig`, or + :class:`PersistentBranchAndBoundConfig`. + """ + + CONFIG = SolverConfig() + + def __init__(self, **kwds) -> None: + # We allow the user and/or developer to name the solver something else, + # if they really desire. + # Otherwise it defaults to the name defined when the solver was registered + # in the SolverFactory or the class name (all lowercase), whichever is + # applicable + if "name" in kwds: + self.name = kwds.pop('name') + elif not hasattr(self, 'name'): + self.name = type(self).__name__.lower() + self.config = self.CONFIG(value=kwds) + + # + # Support "with" statements. Forgetting to call deactivate + # on Plugins is a common source of memory leaks + # + def __enter__(self): + return self + + def __exit__(self, t, v, traceback): + """Exit statement - enables `with` statements.""" + + class Availability(enum.IntEnum): + """ + Class to capture different statuses in which a solver can exist in + order to record its availability for use. + """ + + FullLicense = 2 + LimitedLicense = 1 + NotFound = 0 + BadVersion = -1 + BadLicense = -2 + NeedsCompiledExtension = -3 + + def __bool__(self): + return self._value_ > 0 + + def __format__(self, format_spec): + # We want general formatting of this Enum to return the + # formatted string value and not the int (which is the + # default implementation from IntEnum) + return format(self.name, format_spec) + + def __str__(self): + # Note: Python 3.11 changed the core enums so that the + # "mixin" type for standard enums overrides the behavior + # specified in __format__. We will override str() here to + # preserve the previous behavior + return self.name + + @document_kwargs_from_configdict(CONFIG) + @abc.abstractmethod + def solve(self, model: BlockData, **kwargs) -> Results: + """ + Solve a Pyomo model. + + Parameters + ---------- + model: BlockData + The Pyomo model to be solved + **kwargs + Additional keyword arguments (including solver_options - passthrough + options; delivered directly to the solver (with no validation)) + + Returns + ------- + results: :class:`Results` + A results object + """ + + @abc.abstractmethod + def available(self) -> bool: + """Test if the solver is available on this system. + + Nominally, this will return True if the solver interface is + valid and can be used to solve problems and False if it cannot. + + Note that for licensed solvers there are a number of "levels" of + available: depending on the license, the solver may be available + with limitations on problem size or runtime (e.g., 'demo' + vs. 'community' vs. 'full'). In these cases, the solver may + return a subclass of enum.IntEnum, with members that resolve to + True if the solver is available (possibly with limitations). + The Enum may also have multiple members that all resolve to + False indicating the reason why the interface is not available + (not found, bad license, unsupported version, etc). + + Returns + ------- + available: SolverBase.Availability + An enum that indicates "how available" the solver is. + Note that the enum can be cast to bool, which will + be True if the solver is runable at all and False + otherwise. + """ + + @abc.abstractmethod + def version(self) -> Tuple: + """ + Returns + ------- + version: tuple + A tuple representing the version + """ + + def is_persistent(self) -> bool: + """ + Returns + ------- + is_persistent: bool + True if the solver is a persistent solver. + """ + return False + + +class PersistentSolverBase(SolverBase): + """ + Base class upon which persistent solvers can be built. This inherits the + methods from the solver base class and adds those methods that are necessary + for persistent solvers. + + Example usage can be seen in the Gurobi interface. + """ + + @document_kwargs_from_configdict(PersistentSolverConfig()) + @abc.abstractmethod + def solve(self, model: BlockData, **kwargs) -> Results: + super().solve(model, kwargs) + + def is_persistent(self): + """ + Returns + ------- + is_persistent: bool + True if the solver is a persistent solver. + """ + return True + + def _load_vars(self, vars_to_load: Optional[Sequence[VarData]] = None) -> NoReturn: + """ + Load the solution of the primal variables into the value attribute of the variables. + + Parameters + ---------- + vars_to_load: list + A list of the variables whose solution should be loaded. If vars_to_load is None, then the solution + to all primal variables will be loaded. + """ + for v, val in self._get_primals(vars_to_load=vars_to_load).items(): + v.set_value(val, skip_validation=True) + StaleFlagManager.mark_all_as_stale(delayed=True) + + @abc.abstractmethod + def _get_primals( + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: + """ + Get mapping of variables to primals. + + Parameters + ---------- + vars_to_load : Optional[Sequence[VarData]], optional + Which vars to be populated into the map. The default is None. + + Returns + ------- + Mapping[VarData, float] + A map of variables to primals. + """ + raise NotImplementedError( + f'{type(self)} does not support the get_primals method' + ) + + def _get_duals( + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: + """ + Declare sign convention in docstring here. + + Parameters + ---------- + cons_to_load: list + A list of the constraints whose duals should be loaded. If cons_to_load is None, then the duals for all + constraints will be loaded. + + Returns + ------- + duals: dict + Maps constraints to dual values + """ + raise NotImplementedError(f'{type(self)} does not support the get_duals method') + + def _get_reduced_costs( + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: + """ + Parameters + ---------- + vars_to_load: list + A list of the variables whose reduced cost should be loaded. If vars_to_load is None, then all reduced costs + will be loaded. + + Returns + ------- + reduced_costs: ComponentMap + Maps variable to reduced cost + """ + raise NotImplementedError( + f'{type(self)} does not support the get_reduced_costs method' + ) + + @abc.abstractmethod + def set_instance(self, model): + """ + Set an instance of the model + """ + + @abc.abstractmethod + def set_objective(self, obj: ObjectiveData): + """ + Set current objective for the model + """ + + @abc.abstractmethod + def add_variables(self, variables: List[VarData]): + """ + Add variables to the model + """ + + @abc.abstractmethod + def add_parameters(self, params: List[ParamData]): + """ + Add parameters to the model + """ + + @abc.abstractmethod + def add_constraints(self, cons: List[ConstraintData]): + """ + Add constraints to the model + """ + + @abc.abstractmethod + def add_block(self, block: BlockData): + """ + Add a block to the model + """ + + @abc.abstractmethod + def remove_variables(self, variables: List[VarData]): + """ + Remove variables from the model + """ + + @abc.abstractmethod + def remove_parameters(self, params: List[ParamData]): + """ + Remove parameters from the model + """ + + @abc.abstractmethod + def remove_constraints(self, cons: List[ConstraintData]): + """ + Remove constraints from the model + """ + + @abc.abstractmethod + def remove_block(self, block: BlockData): + """ + Remove a block from the model + """ + + @abc.abstractmethod + def update_variables(self, variables: List[VarData]): + """ + Update variables on the model + """ + + @abc.abstractmethod + def update_parameters(self): + """ + Update parameters on the model + """ + + +class LegacySolverWrapper: + """ + Class to map the new solver interface features into the legacy solver + interface. Necessary for backwards compatibility. + """ + + def __init__(self, **kwargs): + if 'solver_io' in kwargs: + raise NotImplementedError('Still working on this') + # There is no reason for a user to be trying to mix both old + # and new options. That is silly. So we will yell at them. + self.options = kwargs.pop('options', None) + if 'solver_options' in kwargs: + if self.options is not None: + raise ValueError( + "Both 'options' and 'solver_options' were requested. " + "Please use one or the other, not both." + ) + self.options = kwargs.pop('solver_options') + super().__init__(**kwargs) + + # + # Support "with" statements + # + def __enter__(self): + return self + + def __exit__(self, t, v, traceback): + """Exit statement - enables `with` statements.""" + + def _map_config( + self, + tee=NOTSET, + load_solutions=NOTSET, + symbolic_solver_labels=NOTSET, + timelimit=NOTSET, + report_timing=NOTSET, + raise_exception_on_nonoptimal_result=NOTSET, + solver_io=NOTSET, + suffixes=NOTSET, + logfile=NOTSET, + keepfiles=NOTSET, + solnfile=NOTSET, + options=NOTSET, + solver_options=NOTSET, + writer_config=NOTSET, + ): + """Map between legacy and new interface configuration options""" + self.config = self.config() + if 'report_timing' not in self.config: + self.config.declare( + 'report_timing', ConfigValue(domain=bool, default=False) + ) + if tee is not NOTSET: + self.config.tee = tee + if load_solutions is not NOTSET: + self.config.load_solutions = load_solutions + if symbolic_solver_labels is not NOTSET: + self.config.symbolic_solver_labels = symbolic_solver_labels + if timelimit is not NOTSET: + self.config.time_limit = timelimit + if report_timing is not NOTSET: + self.config.report_timing = report_timing + if self.options is not None: + self.config.solver_options.set_value(self.options) + if (options is not NOTSET) and (solver_options is not NOTSET): + # There is no reason for a user to be trying to mix both old + # and new options. That is silly. So we will yell at them. + # Example that would raise an error: + # solver.solve(model, options={'foo' : 'bar'}, solver_options={'foo' : 'not_bar'}) + raise ValueError( + "Both 'options' and 'solver_options' were requested. " + "Please use one or the other, not both." + ) + elif options is not NOTSET: + # This block is trying to mimic the existing logic in the legacy + # interface that allows users to pass initialized options to + # the solver object and override them in the solve call. + self.config.solver_options.set_value(options) + elif solver_options is not NOTSET: + self.config.solver_options.set_value(solver_options) + if writer_config is not NOTSET: + self.config.writer_config.set_value(writer_config) + # This is a new flag in the interface. To preserve backwards compatibility, + # its default is set to "False" + if raise_exception_on_nonoptimal_result is not NOTSET: + self.config.raise_exception_on_nonoptimal_result = ( + raise_exception_on_nonoptimal_result + ) + if solver_io is not NOTSET and solver_io is not None: + raise NotImplementedError('Still working on this') + if suffixes is not NOTSET and suffixes is not None: + raise NotImplementedError('Still working on this') + if logfile is not NOTSET and logfile is not None: + raise NotImplementedError('Still working on this') + if keepfiles or 'keepfiles' in self.config: + cwd = os.getcwd() + deprecation_warning( + "`keepfiles` has been deprecated in the new solver interface. " + "Use `working_dir` instead to designate a directory in which files " + f"should be generated and saved. Setting `working_dir` to `{cwd}`.", + version='6.7.1', + ) + self.config.working_dir = cwd + # I believe this currently does nothing; however, it is unclear what + # our desired behavior is for this. + if solnfile is not NOTSET: + if 'filename' in self.config: + filename = os.path.splitext(solnfile)[0] + self.config.filename = filename + + def _map_results(self, model, results): + """Map between legacy and new Results objects""" + legacy_results = LegacySolverResults() + legacy_soln = LegacySolution() + legacy_results.solver.status = legacy_solver_status_map[ + results.termination_condition + ] + legacy_results.solver.termination_condition = legacy_termination_condition_map[ + results.termination_condition + ] + legacy_soln.status = legacy_solution_status_map[results.solution_status] + legacy_results.solver.termination_message = str(results.termination_condition) + legacy_results.problem.number_of_constraints = float('nan') + legacy_results.problem.number_of_variables = float('nan') + number_of_objectives = sum( + 1 + for _ in model.component_data_objects( + Objective, active=True, descend_into=True + ) + ) + legacy_results.problem.number_of_objectives = number_of_objectives + if number_of_objectives == 1: + obj = get_objective(model) + legacy_results.problem.sense = obj.sense + + if obj.sense == minimize: + legacy_results.problem.lower_bound = results.objective_bound + legacy_results.problem.upper_bound = results.incumbent_objective + else: + legacy_results.problem.upper_bound = results.objective_bound + legacy_results.problem.lower_bound = results.incumbent_objective + if ( + results.incumbent_objective is not None + and results.objective_bound is not None + ): + legacy_soln.gap = abs(results.incumbent_objective - results.objective_bound) + else: + legacy_soln.gap = None + return legacy_results, legacy_soln + + def _solution_handler( + self, load_solutions, model, results, legacy_results, legacy_soln + ): + """Method to handle the preferred action for the solution""" + symbol_map = SymbolMap() + symbol_map.default_labeler = NumericLabeler('x') + if not hasattr(model, 'solutions'): + # This logic gets around Issue #2130 in which + # solutions is not an attribute on Blocks + from pyomo.core.base.PyomoModel import ModelSolutions + + setattr(model, 'solutions', ModelSolutions(model)) + model.solutions.add_symbol_map(symbol_map) + legacy_results._smap_id = id(symbol_map) + delete_legacy_soln = True + if load_solutions: + if hasattr(model, 'dual') and model.dual.import_enabled(): + for c, val in results.solution_loader.get_duals().items(): + model.dual[c] = val + if hasattr(model, 'rc') and model.rc.import_enabled(): + for v, val in results.solution_loader.get_reduced_costs().items(): + model.rc[v] = val + elif results.incumbent_objective is not None: + delete_legacy_soln = False + for v, val in results.solution_loader.get_primals().items(): + legacy_soln.variable[symbol_map.getSymbol(v)] = {'Value': val} + if hasattr(model, 'dual') and model.dual.import_enabled(): + for c, val in results.solution_loader.get_duals().items(): + legacy_soln.constraint[symbol_map.getSymbol(c)] = {'Dual': val} + if hasattr(model, 'rc') and model.rc.import_enabled(): + for v, val in results.solution_loader.get_reduced_costs().items(): + legacy_soln.variable['Rc'] = val + + legacy_results.solution.insert(legacy_soln) + # Timing info was not originally on the legacy results, but we want + # to make it accessible to folks who are utilizing the backwards + # compatible version. + legacy_results.timing_info = results.timing_info + if delete_legacy_soln: + legacy_results.solution.delete(0) + return legacy_results + + def solve( + self, + model: BlockData, + tee: bool = False, + load_solutions: bool = True, + logfile: Optional[str] = None, + solnfile: Optional[str] = None, + timelimit: Optional[float] = None, + report_timing: bool = False, + solver_io: Optional[str] = None, + suffixes: Optional[Sequence] = None, + options: Optional[Dict] = None, + keepfiles: bool = False, + symbolic_solver_labels: bool = False, + # These are for forward-compatibility + raise_exception_on_nonoptimal_result: bool = False, + solver_options: Optional[Dict] = None, + writer_config: Optional[Dict] = None, + ): + """ + Solve method: maps new solve method style to backwards compatible version. + + Returns + ------- + legacy_results + Legacy results object + + """ + original_config = self.config + + map_args = ( + 'tee', + 'load_solutions', + 'symbolic_solver_labels', + 'timelimit', + 'report_timing', + 'raise_exception_on_nonoptimal_result', + 'solver_io', + 'suffixes', + 'logfile', + 'keepfiles', + 'solnfile', + 'options', + 'solver_options', + 'writer_config', + ) + loc = locals() + filtered_args = {k: loc[k] for k in map_args if loc.get(k, None) is not None} + self._map_config(**filtered_args) + + results: Results = super().solve(model) + legacy_results, legacy_soln = self._map_results(model, results) + legacy_results = self._solution_handler( + load_solutions, model, results, legacy_results, legacy_soln + ) + + if self.config.report_timing: + print(results.timing_info.timer) + + self.config = original_config + + return legacy_results + + def available(self, exception_flag=True): + """ + Returns a bool determining whether the requested solver is available + on the system. + """ + ans = super().available() + if exception_flag and not ans: + raise ApplicationError( + f'Solver "{self.name}" is not available. ' + f'The returned status is: {ans}.' + ) + return bool(ans) + + def license_is_valid(self) -> bool: + """Test if the solver license is valid on this system. + + Note that this method is included for compatibility with the + legacy SolverFactory interface. Unlicensed or open source + solvers will return True by definition. Licensed solvers will + return True if a valid license is found. + + Returns + ------- + available: bool + True if the solver license is valid. Otherwise, False. + + """ + return bool(self.available()) + + def config_block(self, init=False): + from pyomo.scripting.solve_config import default_config_block + + return default_config_block(self, init)[0] + + def set_options(self, options): + opts = {k: v for k, v in options.value().items() if v is not None} + if opts: + self._map_config(**opts) diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py new file mode 100644 index 00000000000..e60219a74b5 --- /dev/null +++ b/pyomo/contrib/solver/config.py @@ -0,0 +1,406 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import io +import logging +import sys + +from collections.abc import Sequence +from typing import Optional, List, TextIO + +from pyomo.common.config import ( + ConfigDict, + ConfigValue, + NonNegativeFloat, + NonNegativeInt, + ADVANCED_OPTION, + Bool, + Path, +) +from pyomo.common.log import LogStream +from pyomo.common.numeric_types import native_logical_types +from pyomo.common.timing import HierarchicalTimer + + +def TextIO_or_Logger(val): + ans = [] + if not isinstance(val, Sequence): + val = [val] + for v in val: + if v.__class__ in native_logical_types: + if v: + ans.append(sys.stdout) + elif isinstance(v, io.TextIOBase): + ans.append(v) + elif isinstance(v, logging.Logger): + ans.append(LogStream(level=logging.INFO, logger=v)) + else: + raise ValueError( + "Expected bool, TextIOBase, or Logger, but received {v.__class__}" + ) + return ans + + +class SolverConfig(ConfigDict): + """ + Base config for all direct solver interfaces + """ + + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.tee: List[TextIO] = self.declare( + 'tee', + ConfigValue( + domain=TextIO_or_Logger, + default=False, + description="""``tee`` accepts :py:class:`bool`, + :py:class:`io.TextIOBase`, or :py:class:`logging.Logger` + (or a list of these types). ``True`` is mapped to + ``sys.stdout``. The solver log will be printed to each of + these streams / destinations.""", + ), + ) + self.working_dir: Optional[Path] = self.declare( + 'working_dir', + ConfigValue( + domain=Path(), + default=None, + description="The directory in which generated files should be saved. " + "This replaces the `keepfiles` option.", + ), + ) + self.load_solutions: bool = self.declare( + 'load_solutions', + ConfigValue( + domain=Bool, + default=True, + description="If True, the values of the primal variables will be loaded into the model.", + ), + ) + self.raise_exception_on_nonoptimal_result: bool = self.declare( + 'raise_exception_on_nonoptimal_result', + ConfigValue( + domain=Bool, + default=True, + description="If False, the `solve` method will continue processing " + "even if the returned result is nonoptimal.", + ), + ) + self.symbolic_solver_labels: bool = self.declare( + 'symbolic_solver_labels', + ConfigValue( + domain=Bool, + default=False, + description="If True, the names given to the solver will reflect the names of the Pyomo components. " + "Cannot be changed after set_instance is called.", + ), + ) + self.timer: Optional[HierarchicalTimer] = self.declare( + 'timer', + ConfigValue( + default=None, + description="A timer object for recording relevant process timing data.", + ), + ) + self.threads: Optional[int] = self.declare( + 'threads', + ConfigValue( + domain=NonNegativeInt, + description="Number of threads to be used by a solver.", + default=None, + ), + ) + self.time_limit: Optional[float] = self.declare( + 'time_limit', + ConfigValue( + domain=NonNegativeFloat, + description="Time limit applied to the solver (in seconds).", + ), + ) + self.solver_options: ConfigDict = self.declare( + 'solver_options', + ConfigDict(implicit=True, description="Options to pass to the solver."), + ) + + +class BranchAndBoundConfig(SolverConfig): + """ + Base config for all direct MIP solver interfaces + + Attributes + ---------- + rel_gap: float + The relative value of the gap in relation to the best bound + abs_gap: float + The absolute value of the difference between the incumbent and best bound + """ + + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.rel_gap: Optional[float] = self.declare( + 'rel_gap', + ConfigValue( + domain=NonNegativeFloat, + description="Optional termination condition; the relative value of the " + "gap in relation to the best bound", + ), + ) + self.abs_gap: Optional[float] = self.declare( + 'abs_gap', + ConfigValue( + domain=NonNegativeFloat, + description="Optional termination condition; the absolute value of the " + "difference between the incumbent and best bound", + ), + ) + + +class AutoUpdateConfig(ConfigDict): + """ + This is necessary for persistent solvers. + + Attributes + ---------- + check_for_new_or_removed_constraints: bool + check_for_new_or_removed_vars: bool + check_for_new_or_removed_params: bool + check_for_new_objective: bool + update_constraints: bool + update_vars: bool + update_parameters: bool + update_named_expressions: bool + update_objective: bool + treat_fixed_vars_as_params: bool + """ + + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + if doc is None: + doc = 'Configuration options to detect changes in model between solves' + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.check_for_new_or_removed_constraints: bool = self.declare( + 'check_for_new_or_removed_constraints', + ConfigValue( + domain=bool, + default=True, + description=""" + If False, new/old constraints will not be automatically detected on subsequent + solves. Use False only when manually updating the solver with opt.add_constraints() + and opt.remove_constraints() or when you are certain constraints are not being + added to/removed from the model.""", + ), + ) + self.check_for_new_or_removed_vars: bool = self.declare( + 'check_for_new_or_removed_vars', + ConfigValue( + domain=bool, + default=True, + description=""" + If False, new/old variables will not be automatically detected on subsequent + solves. Use False only when manually updating the solver with opt.add_variables() and + opt.remove_variables() or when you are certain variables are not being added to / + removed from the model.""", + ), + ) + self.check_for_new_or_removed_params: bool = self.declare( + 'check_for_new_or_removed_params', + ConfigValue( + domain=bool, + default=True, + description=""" + If False, new/old parameters will not be automatically detected on subsequent + solves. Use False only when manually updating the solver with opt.add_parameters() and + opt.remove_parameters() or when you are certain parameters are not being added to / + removed from the model.""", + ), + ) + self.check_for_new_objective: bool = self.declare( + 'check_for_new_objective', + ConfigValue( + domain=bool, + default=True, + description=""" + If False, new/old objectives will not be automatically detected on subsequent + solves. Use False only when manually updating the solver with opt.set_objective() or + when you are certain objectives are not being added to / removed from the model.""", + ), + ) + self.update_constraints: bool = self.declare( + 'update_constraints', + ConfigValue( + domain=bool, + default=True, + description=""" + If False, changes to existing constraints will not be automatically detected on + subsequent solves. This includes changes to the lower, body, and upper attributes of + constraints. Use False only when manually updating the solver with + opt.remove_constraints() and opt.add_constraints() or when you are certain constraints + are not being modified.""", + ), + ) + self.update_vars: bool = self.declare( + 'update_vars', + ConfigValue( + domain=bool, + default=True, + description=""" + If False, changes to existing variables will not be automatically detected on + subsequent solves. This includes changes to the lb, ub, domain, and fixed + attributes of variables. Use False only when manually updating the solver with + opt.update_variables() or when you are certain variables are not being modified.""", + ), + ) + self.update_parameters: bool = self.declare( + 'update_parameters', + ConfigValue( + domain=bool, + default=True, + description=""" + If False, changes to parameter values will not be automatically detected on + subsequent solves. Use False only when manually updating the solver with + opt.update_parameters() or when you are certain parameters are not being modified.""", + ), + ) + self.update_named_expressions: bool = self.declare( + 'update_named_expressions', + ConfigValue( + domain=bool, + default=True, + description=""" + If False, changes to Expressions will not be automatically detected on + subsequent solves. Use False only when manually updating the solver with + opt.remove_constraints() and opt.add_constraints() or when you are certain + Expressions are not being modified.""", + ), + ) + self.update_objective: bool = self.declare( + 'update_objective', + ConfigValue( + domain=bool, + default=True, + description=""" + If False, changes to objectives will not be automatically detected on + subsequent solves. This includes the expr and sense attributes of objectives. Use + False only when manually updating the solver with opt.set_objective() or when you are + certain objectives are not being modified.""", + ), + ) + self.treat_fixed_vars_as_params: bool = self.declare( + 'treat_fixed_vars_as_params', + ConfigValue( + domain=bool, + default=True, + visibility=ADVANCED_OPTION, + description=""" + This is an advanced option that should only be used in special circumstances. + With the default setting of True, fixed variables will be treated like parameters. + This means that z == x*y will be linear if x or y is fixed and the constraint + can be written to an LP file. If the value of the fixed variable gets changed, we have + to completely reprocess all constraints using that variable. If + treat_fixed_vars_as_params is False, then constraints will be processed as if fixed + variables are not fixed, and the solver will be told the variable is fixed. This means + z == x*y could not be written to an LP file even if x and/or y is fixed. However, + updating the values of fixed variables is much faster this way.""", + ), + ) + + +class PersistentSolverConfig(SolverConfig): + """ + Base config for all persistent solver interfaces + """ + + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.auto_updates: AutoUpdateConfig = self.declare( + 'auto_updates', AutoUpdateConfig() + ) + + +class PersistentBranchAndBoundConfig(BranchAndBoundConfig): + """ + Base config for all persistent MIP solver interfaces + """ + + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.auto_updates: AutoUpdateConfig = self.declare( + 'auto_updates', AutoUpdateConfig() + ) diff --git a/pyomo/contrib/solver/factory.py b/pyomo/contrib/solver/factory.py new file mode 100644 index 00000000000..d3ca1329af3 --- /dev/null +++ b/pyomo/contrib/solver/factory.py @@ -0,0 +1,41 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + +from pyomo.opt.base.solvers import LegacySolverFactory +from pyomo.common.factory import Factory +from pyomo.contrib.solver.base import LegacySolverWrapper + + +class SolverFactoryClass(Factory): + def register(self, name, legacy_name=None, doc=None): + if legacy_name is None: + legacy_name = name + + def decorator(cls): + self._cls[name] = cls + self._doc[name] = doc + + class LegacySolver(LegacySolverWrapper, cls): + pass + + LegacySolverFactory.register(legacy_name, doc + " (new interface)")( + LegacySolver + ) + + # Preserve the preferred name, as registered in the Factory + cls.name = name + return cls + + return decorator + + +SolverFactory = SolverFactoryClass() diff --git a/pyomo/contrib/solver/gurobi.py b/pyomo/contrib/solver/gurobi.py new file mode 100644 index 00000000000..10d8120c8b3 --- /dev/null +++ b/pyomo/contrib/solver/gurobi.py @@ -0,0 +1,1505 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from collections.abc import Iterable +import logging +import math +from typing import List, Optional +from pyomo.common.collections import ComponentSet, ComponentMap, OrderedSet +from pyomo.common.dependencies import attempt_import +from pyomo.common.errors import PyomoException +from pyomo.common.tee import capture_output, TeeStream +from pyomo.common.timing import HierarchicalTimer +from pyomo.common.shutdown import python_is_shutting_down +from pyomo.common.config import ConfigValue +from pyomo.core.kernel.objective import minimize, maximize +from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler +from pyomo.core.base.var import VarData +from pyomo.core.base.constraint import ConstraintData +from pyomo.core.base.sos import SOSConstraintData +from pyomo.core.base.param import ParamData +from pyomo.core.expr.numvalue import value, is_constant, is_fixed, native_numeric_types +from pyomo.repn import generate_standard_repn +from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression +from pyomo.contrib.solver.base import PersistentSolverBase +from pyomo.contrib.solver.results import Results, TerminationCondition, SolutionStatus +from pyomo.contrib.solver.config import PersistentBranchAndBoundConfig +from pyomo.contrib.solver.persistent import PersistentSolverUtils +from pyomo.contrib.solver.solution import PersistentSolutionLoader +from pyomo.core.staleflag import StaleFlagManager +import sys +import datetime +import io + +logger = logging.getLogger(__name__) + + +def _import_gurobipy(): + try: + import gurobipy + except ImportError: + Gurobi._available = Gurobi.Availability.NotFound + raise + if gurobipy.GRB.VERSION_MAJOR < 7: + Gurobi._available = Gurobi.Availability.BadVersion + raise ImportError('The APPSI Gurobi interface requires gurobipy>=7.0.0') + return gurobipy + + +gurobipy, gurobipy_available = attempt_import('gurobipy', importer=_import_gurobipy) + + +class DegreeError(PyomoException): + pass + + +class GurobiConfig(PersistentBranchAndBoundConfig): + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super(GurobiConfig, self).__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + self.use_mipstart: bool = self.declare( + 'use_mipstart', + ConfigValue( + default=False, + domain=bool, + description="If True, the values of the integer variables will be passed to Gurobi.", + ), + ) + + +class GurobiSolutionLoader(PersistentSolutionLoader): + def load_vars(self, vars_to_load=None, solution_number=0): + self._assert_solution_still_valid() + self._solver._load_vars( + vars_to_load=vars_to_load, solution_number=solution_number + ) + + def get_primals(self, vars_to_load=None, solution_number=0): + self._assert_solution_still_valid() + return self._solver._get_primals( + vars_to_load=vars_to_load, solution_number=solution_number + ) + + +class _MutableLowerBound(object): + def __init__(self, expr): + self.var = None + self.expr = expr + + def update(self): + self.var.setAttr('lb', value(self.expr)) + + +class _MutableUpperBound(object): + def __init__(self, expr): + self.var = None + self.expr = expr + + def update(self): + self.var.setAttr('ub', value(self.expr)) + + +class _MutableLinearCoefficient(object): + def __init__(self): + self.expr = None + self.var = None + self.con = None + self.gurobi_model = None + + def update(self): + self.gurobi_model.chgCoeff(self.con, self.var, value(self.expr)) + + +class _MutableRangeConstant(object): + def __init__(self): + self.lhs_expr = None + self.rhs_expr = None + self.con = None + self.slack_name = None + self.gurobi_model = None + + def update(self): + rhs_val = value(self.rhs_expr) + lhs_val = value(self.lhs_expr) + self.con.rhs = rhs_val + slack = self.gurobi_model.getVarByName(self.slack_name) + slack.ub = rhs_val - lhs_val + + +class _MutableConstant(object): + def __init__(self): + self.expr = None + self.con = None + + def update(self): + self.con.rhs = value(self.expr) + + +class _MutableQuadraticConstraint(object): + def __init__( + self, gurobi_model, gurobi_con, constant, linear_coefs, quadratic_coefs + ): + self.con = gurobi_con + self.gurobi_model = gurobi_model + self.constant = constant + self.last_constant_value = value(self.constant.expr) + self.linear_coefs = linear_coefs + self.last_linear_coef_values = [value(i.expr) for i in self.linear_coefs] + self.quadratic_coefs = quadratic_coefs + self.last_quadratic_coef_values = [value(i.expr) for i in self.quadratic_coefs] + + def get_updated_expression(self): + gurobi_expr = self.gurobi_model.getQCRow(self.con) + for ndx, coef in enumerate(self.linear_coefs): + current_coef_value = value(coef.expr) + incremental_coef_value = ( + current_coef_value - self.last_linear_coef_values[ndx] + ) + gurobi_expr += incremental_coef_value * coef.var + self.last_linear_coef_values[ndx] = current_coef_value + for ndx, coef in enumerate(self.quadratic_coefs): + current_coef_value = value(coef.expr) + incremental_coef_value = ( + current_coef_value - self.last_quadratic_coef_values[ndx] + ) + gurobi_expr += incremental_coef_value * coef.var1 * coef.var2 + self.last_quadratic_coef_values[ndx] = current_coef_value + return gurobi_expr + + def get_updated_rhs(self): + return value(self.constant.expr) + + +class _MutableObjective(object): + def __init__(self, gurobi_model, constant, linear_coefs, quadratic_coefs): + self.gurobi_model = gurobi_model + self.constant = constant + self.linear_coefs = linear_coefs + self.quadratic_coefs = quadratic_coefs + self.last_quadratic_coef_values = [value(i.expr) for i in self.quadratic_coefs] + + def get_updated_expression(self): + for ndx, coef in enumerate(self.linear_coefs): + coef.var.obj = value(coef.expr) + self.gurobi_model.ObjCon = value(self.constant.expr) + + gurobi_expr = None + for ndx, coef in enumerate(self.quadratic_coefs): + if value(coef.expr) != self.last_quadratic_coef_values[ndx]: + if gurobi_expr is None: + self.gurobi_model.update() + gurobi_expr = self.gurobi_model.getObjective() + current_coef_value = value(coef.expr) + incremental_coef_value = ( + current_coef_value - self.last_quadratic_coef_values[ndx] + ) + gurobi_expr += incremental_coef_value * coef.var1 * coef.var2 + self.last_quadratic_coef_values[ndx] = current_coef_value + return gurobi_expr + + +class _MutableQuadraticCoefficient(object): + def __init__(self): + self.expr = None + self.var1 = None + self.var2 = None + + +class Gurobi(PersistentSolverUtils, PersistentSolverBase): + """ + Interface to Gurobi + """ + + CONFIG = GurobiConfig() + + _available = None + _num_instances = 0 + + def __init__(self, **kwds): + PersistentSolverUtils.__init__(self) + PersistentSolverBase.__init__(self, **kwds) + Gurobi._num_instances += 1 + self._solver_model = None + self._symbol_map = SymbolMap() + self._labeler = None + self._pyomo_var_to_solver_var_map = dict() + self._pyomo_con_to_solver_con_map = dict() + self._solver_con_to_pyomo_con_map = dict() + self._pyomo_sos_to_solver_sos_map = dict() + self._range_constraints = OrderedSet() + self._mutable_helpers = dict() + self._mutable_bounds = dict() + self._mutable_quadratic_helpers = dict() + self._mutable_objective = None + self._needs_updated = True + self._callback = None + self._callback_func = None + self._constraints_added_since_update = OrderedSet() + self._vars_added_since_update = ComponentSet() + self._last_results_object: Optional[Results] = None + self._config: Optional[GurobiConfig] = None + + def available(self): + if not gurobipy_available: # this triggers the deferred import + return self.Availability.NotFound + elif self._available == self.Availability.BadVersion: + return self.Availability.BadVersion + else: + return self._check_license() + + def _check_license(self): + avail = False + try: + # Gurobipy writes out license file information when creating + # the environment + with capture_output(capture_fd=True): + m = gurobipy.Model() + if self._solver_model is None: + self._solver_model = m + avail = True + except gurobipy.GurobiError: + avail = False + + if avail: + if self._available is None: + self._available = Gurobi._check_full_license() + return self._available + else: + return self.Availability.BadLicense + + @classmethod + def _check_full_license(cls): + m = gurobipy.Model() + m.setParam('OutputFlag', 0) + try: + m.addVars(range(2001)) + m.optimize() + return cls.Availability.FullLicense + except gurobipy.GurobiError: + return cls.Availability.LimitedLicense + + def release_license(self): + self._reinit() + if gurobipy_available: + with capture_output(capture_fd=True): + gurobipy.disposeDefaultEnv() + + def __del__(self): + if not python_is_shutting_down(): + Gurobi._num_instances -= 1 + if Gurobi._num_instances == 0: + self.release_license() + + def version(self): + version = ( + gurobipy.GRB.VERSION_MAJOR, + gurobipy.GRB.VERSION_MINOR, + gurobipy.GRB.VERSION_TECHNICAL, + ) + return version + + @property + def symbol_map(self): + return self._symbol_map + + def _solve(self): + config = self._config + timer = config.timer + ostreams = [io.StringIO()] + config.tee + + with TeeStream(*ostreams) as t, capture_output(t.STDOUT, capture_fd=False): + options = config.solver_options + + self._solver_model.setParam('LogToConsole', 1) + + if config.threads is not None: + self._solver_model.setParam('Threads', config.threads) + if config.time_limit is not None: + self._solver_model.setParam('TimeLimit', config.time_limit) + if config.rel_gap is not None: + self._solver_model.setParam('MIPGap', config.rel_gap) + if config.abs_gap is not None: + self._solver_model.setParam('MIPGapAbs', config.abs_gap) + + if config.use_mipstart: + for ( + pyomo_var_id, + gurobi_var, + ) in self._pyomo_var_to_solver_var_map.items(): + pyomo_var = self._vars[pyomo_var_id][0] + if pyomo_var.is_integer() and pyomo_var.value is not None: + self.set_var_attr(pyomo_var, 'Start', pyomo_var.value) + + for key, option in options.items(): + self._solver_model.setParam(key, option) + + timer.start('optimize') + self._solver_model.optimize(self._callback) + timer.stop('optimize') + + self._needs_updated = False + res = self._postsolve(timer) + res.solver_configuration = config + res.solver_name = 'Gurobi' + res.solver_version = self.version() + res.solver_log = ostreams[0].getvalue() + return res + + def solve(self, model, **kwds) -> Results: + start_timestamp = datetime.datetime.now(datetime.timezone.utc) + self._config = config = self.config(value=kwds, preserve_implicit=True) + StaleFlagManager.mark_all_as_stale() + # Note: solver availability check happens in set_instance(), + # which will be called (either by the user before this call, or + # below) before this method calls self._solve. + if self._last_results_object is not None: + self._last_results_object.solution_loader.invalidate() + if config.timer is None: + config.timer = HierarchicalTimer() + timer = config.timer + if model is not self._model: + timer.start('set_instance') + self.set_instance(model) + timer.stop('set_instance') + else: + timer.start('update') + self.update(timer=timer) + timer.stop('update') + res = self._solve() + self._last_results_object = res + end_timestamp = datetime.datetime.now(datetime.timezone.utc) + res.timing_info.start_timestamp = start_timestamp + res.timing_info.wall_time = (end_timestamp - start_timestamp).total_seconds() + res.timing_info.timer = timer + return res + + def _process_domain_and_bounds( + self, var, var_id, mutable_lbs, mutable_ubs, ndx, gurobipy_var + ): + _v, _lb, _ub, _fixed, _domain_interval, _value = self._vars[id(var)] + lb, ub, step = _domain_interval + if lb is None: + lb = -gurobipy.GRB.INFINITY + if ub is None: + ub = gurobipy.GRB.INFINITY + if step == 0: + vtype = gurobipy.GRB.CONTINUOUS + elif step == 1: + if lb == 0 and ub == 1: + vtype = gurobipy.GRB.BINARY + else: + vtype = gurobipy.GRB.INTEGER + else: + raise ValueError( + f'Unrecognized domain step: {step} (should be either 0 or 1)' + ) + if _fixed: + lb = _value + ub = _value + else: + if _lb is not None: + if not is_constant(_lb): + mutable_bound = _MutableLowerBound(NPV_MaxExpression((_lb, lb))) + if gurobipy_var is None: + mutable_lbs[ndx] = mutable_bound + else: + mutable_bound.var = gurobipy_var + self._mutable_bounds[var_id, 'lb'] = (var, mutable_bound) + lb = max(value(_lb), lb) + if _ub is not None: + if not is_constant(_ub): + mutable_bound = _MutableUpperBound(NPV_MinExpression((_ub, ub))) + if gurobipy_var is None: + mutable_ubs[ndx] = mutable_bound + else: + mutable_bound.var = gurobipy_var + self._mutable_bounds[var_id, 'ub'] = (var, mutable_bound) + ub = min(value(_ub), ub) + + return lb, ub, vtype + + def _add_variables(self, variables: List[VarData]): + var_names = list() + vtypes = list() + lbs = list() + ubs = list() + mutable_lbs = dict() + mutable_ubs = dict() + for ndx, var in enumerate(variables): + varname = self._symbol_map.getSymbol(var, self._labeler) + lb, ub, vtype = self._process_domain_and_bounds( + var, id(var), mutable_lbs, mutable_ubs, ndx, None + ) + var_names.append(varname) + vtypes.append(vtype) + lbs.append(lb) + ubs.append(ub) + + gurobi_vars = self._solver_model.addVars( + len(variables), lb=lbs, ub=ubs, vtype=vtypes, name=var_names + ) + + for ndx, pyomo_var in enumerate(variables): + gurobi_var = gurobi_vars[ndx] + self._pyomo_var_to_solver_var_map[id(pyomo_var)] = gurobi_var + for ndx, mutable_bound in mutable_lbs.items(): + mutable_bound.var = gurobi_vars[ndx] + for ndx, mutable_bound in mutable_ubs.items(): + mutable_bound.var = gurobi_vars[ndx] + self._vars_added_since_update.update(variables) + self._needs_updated = True + + def _add_parameters(self, params: List[ParamData]): + pass + + def _reinit(self): + saved_config = self.config + saved_tmp_config = self._config + self.__init__() + self.config = saved_config + self._config = saved_tmp_config + + def set_instance(self, model): + if self._last_results_object is not None: + self._last_results_object.solution_loader.invalidate() + if not self.available(): + c = self.__class__ + raise PyomoException( + f'Solver {c.__module__}.{c.__qualname__} is not available ' + f'({self.available()}).' + ) + self._reinit() + self._model = model + + if self.config.symbolic_solver_labels: + self._labeler = TextLabeler() + else: + self._labeler = NumericLabeler('x') + + if model.name is not None: + self._solver_model = gurobipy.Model(model.name) + else: + self._solver_model = gurobipy.Model() + + self.add_block(model) + if self._objective is None: + self.set_objective(None) + + def _get_expr_from_pyomo_expr(self, expr): + mutable_linear_coefficients = list() + mutable_quadratic_coefficients = list() + repn = generate_standard_repn(expr, quadratic=True, compute_values=False) + + degree = repn.polynomial_degree() + if (degree is None) or (degree > 2): + raise DegreeError( + 'GurobiAuto does not support expressions of degree {0}.'.format(degree) + ) + + if len(repn.linear_vars) > 0: + linear_coef_vals = list() + for ndx, coef in enumerate(repn.linear_coefs): + if not is_constant(coef): + mutable_linear_coefficient = _MutableLinearCoefficient() + mutable_linear_coefficient.expr = coef + mutable_linear_coefficient.var = self._pyomo_var_to_solver_var_map[ + id(repn.linear_vars[ndx]) + ] + mutable_linear_coefficients.append(mutable_linear_coefficient) + linear_coef_vals.append(value(coef)) + new_expr = gurobipy.LinExpr( + linear_coef_vals, + [self._pyomo_var_to_solver_var_map[id(i)] for i in repn.linear_vars], + ) + else: + new_expr = 0.0 + + for ndx, v in enumerate(repn.quadratic_vars): + x, y = v + gurobi_x = self._pyomo_var_to_solver_var_map[id(x)] + gurobi_y = self._pyomo_var_to_solver_var_map[id(y)] + coef = repn.quadratic_coefs[ndx] + if not is_constant(coef): + mutable_quadratic_coefficient = _MutableQuadraticCoefficient() + mutable_quadratic_coefficient.expr = coef + mutable_quadratic_coefficient.var1 = gurobi_x + mutable_quadratic_coefficient.var2 = gurobi_y + mutable_quadratic_coefficients.append(mutable_quadratic_coefficient) + coef_val = value(coef) + new_expr += coef_val * gurobi_x * gurobi_y + + return ( + new_expr, + repn.constant, + mutable_linear_coefficients, + mutable_quadratic_coefficients, + ) + + def _add_constraints(self, cons: List[ConstraintData]): + for con in cons: + conname = self._symbol_map.getSymbol(con, self._labeler) + ( + gurobi_expr, + repn_constant, + mutable_linear_coefficients, + mutable_quadratic_coefficients, + ) = self._get_expr_from_pyomo_expr(con.body) + + if ( + gurobi_expr.__class__ in {gurobipy.LinExpr, gurobipy.Var} + or gurobi_expr.__class__ in native_numeric_types + ): + if con.equality: + rhs_expr = con.lower - repn_constant + rhs_val = value(rhs_expr) + gurobipy_con = self._solver_model.addLConstr( + gurobi_expr, gurobipy.GRB.EQUAL, rhs_val, name=conname + ) + if not is_constant(rhs_expr): + mutable_constant = _MutableConstant() + mutable_constant.expr = rhs_expr + mutable_constant.con = gurobipy_con + self._mutable_helpers[con] = [mutable_constant] + elif con.has_lb() and con.has_ub(): + lhs_expr = con.lower - repn_constant + rhs_expr = con.upper - repn_constant + lhs_val = value(lhs_expr) + rhs_val = value(rhs_expr) + gurobipy_con = self._solver_model.addRange( + gurobi_expr, lhs_val, rhs_val, name=conname + ) + self._range_constraints.add(con) + if not is_constant(lhs_expr) or not is_constant(rhs_expr): + mutable_range_constant = _MutableRangeConstant() + mutable_range_constant.lhs_expr = lhs_expr + mutable_range_constant.rhs_expr = rhs_expr + mutable_range_constant.con = gurobipy_con + mutable_range_constant.slack_name = 'Rg' + conname + mutable_range_constant.gurobi_model = self._solver_model + self._mutable_helpers[con] = [mutable_range_constant] + elif con.has_lb(): + rhs_expr = con.lower - repn_constant + rhs_val = value(rhs_expr) + gurobipy_con = self._solver_model.addLConstr( + gurobi_expr, gurobipy.GRB.GREATER_EQUAL, rhs_val, name=conname + ) + if not is_constant(rhs_expr): + mutable_constant = _MutableConstant() + mutable_constant.expr = rhs_expr + mutable_constant.con = gurobipy_con + self._mutable_helpers[con] = [mutable_constant] + elif con.has_ub(): + rhs_expr = con.upper - repn_constant + rhs_val = value(rhs_expr) + gurobipy_con = self._solver_model.addLConstr( + gurobi_expr, gurobipy.GRB.LESS_EQUAL, rhs_val, name=conname + ) + if not is_constant(rhs_expr): + mutable_constant = _MutableConstant() + mutable_constant.expr = rhs_expr + mutable_constant.con = gurobipy_con + self._mutable_helpers[con] = [mutable_constant] + else: + raise ValueError( + "Constraint does not have a lower " + "or an upper bound: {0} \n".format(con) + ) + for tmp in mutable_linear_coefficients: + tmp.con = gurobipy_con + tmp.gurobi_model = self._solver_model + if len(mutable_linear_coefficients) > 0: + if con not in self._mutable_helpers: + self._mutable_helpers[con] = mutable_linear_coefficients + else: + self._mutable_helpers[con].extend(mutable_linear_coefficients) + elif gurobi_expr.__class__ is gurobipy.QuadExpr: + if con.equality: + rhs_expr = con.lower - repn_constant + rhs_val = value(rhs_expr) + gurobipy_con = self._solver_model.addQConstr( + gurobi_expr, gurobipy.GRB.EQUAL, rhs_val, name=conname + ) + elif con.has_lb() and con.has_ub(): + raise NotImplementedError( + 'Quadratic range constraints are not supported' + ) + elif con.has_lb(): + rhs_expr = con.lower - repn_constant + rhs_val = value(rhs_expr) + gurobipy_con = self._solver_model.addQConstr( + gurobi_expr, gurobipy.GRB.GREATER_EQUAL, rhs_val, name=conname + ) + elif con.has_ub(): + rhs_expr = con.upper - repn_constant + rhs_val = value(rhs_expr) + gurobipy_con = self._solver_model.addQConstr( + gurobi_expr, gurobipy.GRB.LESS_EQUAL, rhs_val, name=conname + ) + else: + raise ValueError( + "Constraint does not have a lower " + "or an upper bound: {0} \n".format(con) + ) + if ( + len(mutable_linear_coefficients) > 0 + or len(mutable_quadratic_coefficients) > 0 + or not is_constant(repn_constant) + ): + mutable_constant = _MutableConstant() + mutable_constant.expr = rhs_expr + mutable_quadratic_constraint = _MutableQuadraticConstraint( + self._solver_model, + gurobipy_con, + mutable_constant, + mutable_linear_coefficients, + mutable_quadratic_coefficients, + ) + self._mutable_quadratic_helpers[con] = mutable_quadratic_constraint + else: + raise ValueError( + 'Unrecognized Gurobi expression type: ' + str(gurobi_expr.__class__) + ) + + self._pyomo_con_to_solver_con_map[con] = gurobipy_con + self._solver_con_to_pyomo_con_map[id(gurobipy_con)] = con + self._constraints_added_since_update.update(cons) + self._needs_updated = True + + def _add_sos_constraints(self, cons: List[SOSConstraintData]): + for con in cons: + conname = self._symbol_map.getSymbol(con, self._labeler) + level = con.level + if level == 1: + sos_type = gurobipy.GRB.SOS_TYPE1 + elif level == 2: + sos_type = gurobipy.GRB.SOS_TYPE2 + else: + raise ValueError( + "Solver does not support SOS level {0} constraints".format(level) + ) + + gurobi_vars = [] + weights = [] + + for v, w in con.get_items(): + v_id = id(v) + gurobi_vars.append(self._pyomo_var_to_solver_var_map[v_id]) + weights.append(w) + + gurobipy_con = self._solver_model.addSOS(sos_type, gurobi_vars, weights) + self._pyomo_sos_to_solver_sos_map[con] = gurobipy_con + self._constraints_added_since_update.update(cons) + self._needs_updated = True + + def _remove_constraints(self, cons: List[ConstraintData]): + for con in cons: + if con in self._constraints_added_since_update: + self._update_gurobi_model() + solver_con = self._pyomo_con_to_solver_con_map[con] + self._solver_model.remove(solver_con) + self._symbol_map.removeSymbol(con) + del self._pyomo_con_to_solver_con_map[con] + del self._solver_con_to_pyomo_con_map[id(solver_con)] + self._range_constraints.discard(con) + self._mutable_helpers.pop(con, None) + self._mutable_quadratic_helpers.pop(con, None) + self._needs_updated = True + + def _remove_sos_constraints(self, cons: List[SOSConstraintData]): + for con in cons: + if con in self._constraints_added_since_update: + self._update_gurobi_model() + solver_sos_con = self._pyomo_sos_to_solver_sos_map[con] + self._solver_model.remove(solver_sos_con) + self._symbol_map.removeSymbol(con) + del self._pyomo_sos_to_solver_sos_map[con] + self._needs_updated = True + + def _remove_variables(self, variables: List[VarData]): + for var in variables: + v_id = id(var) + if var in self._vars_added_since_update: + self._update_gurobi_model() + solver_var = self._pyomo_var_to_solver_var_map[v_id] + self._solver_model.remove(solver_var) + self._symbol_map.removeSymbol(var) + del self._pyomo_var_to_solver_var_map[v_id] + self._mutable_bounds.pop(v_id, None) + self._needs_updated = True + + def _remove_parameters(self, params: List[ParamData]): + pass + + def _update_variables(self, variables: List[VarData]): + for var in variables: + var_id = id(var) + if var_id not in self._pyomo_var_to_solver_var_map: + raise ValueError( + 'The Var provided to update_var needs to be added first: {0}'.format( + var + ) + ) + self._mutable_bounds.pop((var_id, 'lb'), None) + self._mutable_bounds.pop((var_id, 'ub'), None) + gurobipy_var = self._pyomo_var_to_solver_var_map[var_id] + lb, ub, vtype = self._process_domain_and_bounds( + var, var_id, None, None, None, gurobipy_var + ) + gurobipy_var.setAttr('lb', lb) + gurobipy_var.setAttr('ub', ub) + gurobipy_var.setAttr('vtype', vtype) + self._needs_updated = True + + def update_parameters(self): + for con, helpers in self._mutable_helpers.items(): + for helper in helpers: + helper.update() + for k, (v, helper) in self._mutable_bounds.items(): + helper.update() + + for con, helper in self._mutable_quadratic_helpers.items(): + if con in self._constraints_added_since_update: + self._update_gurobi_model() + gurobi_con = helper.con + new_gurobi_expr = helper.get_updated_expression() + new_rhs = helper.get_updated_rhs() + new_sense = gurobi_con.qcsense + pyomo_con = self._solver_con_to_pyomo_con_map[id(gurobi_con)] + name = self._symbol_map.getSymbol(pyomo_con, self._labeler) + self._solver_model.remove(gurobi_con) + new_con = self._solver_model.addQConstr( + new_gurobi_expr, new_sense, new_rhs, name=name + ) + self._pyomo_con_to_solver_con_map[id(pyomo_con)] = new_con + del self._solver_con_to_pyomo_con_map[id(gurobi_con)] + self._solver_con_to_pyomo_con_map[id(new_con)] = pyomo_con + helper.con = new_con + self._constraints_added_since_update.add(con) + + helper = self._mutable_objective + pyomo_obj = self._objective + new_gurobi_expr = helper.get_updated_expression() + if new_gurobi_expr is not None: + if pyomo_obj.sense == minimize: + sense = gurobipy.GRB.MINIMIZE + else: + sense = gurobipy.GRB.MAXIMIZE + self._solver_model.setObjective(new_gurobi_expr, sense=sense) + + def _set_objective(self, obj): + if obj is None: + sense = gurobipy.GRB.MINIMIZE + gurobi_expr = 0 + repn_constant = 0 + mutable_linear_coefficients = list() + mutable_quadratic_coefficients = list() + else: + if obj.sense == minimize: + sense = gurobipy.GRB.MINIMIZE + elif obj.sense == maximize: + sense = gurobipy.GRB.MAXIMIZE + else: + raise ValueError( + 'Objective sense is not recognized: {0}'.format(obj.sense) + ) + + ( + gurobi_expr, + repn_constant, + mutable_linear_coefficients, + mutable_quadratic_coefficients, + ) = self._get_expr_from_pyomo_expr(obj.expr) + + mutable_constant = _MutableConstant() + mutable_constant.expr = repn_constant + mutable_objective = _MutableObjective( + self._solver_model, + mutable_constant, + mutable_linear_coefficients, + mutable_quadratic_coefficients, + ) + self._mutable_objective = mutable_objective + + # These two lines are needed as a workaround + # see PR #2454 + self._solver_model.setObjective(0) + self._solver_model.update() + + self._solver_model.setObjective(gurobi_expr + value(repn_constant), sense=sense) + self._needs_updated = True + + def _postsolve(self, timer: HierarchicalTimer): + config = self._config + + gprob = self._solver_model + grb = gurobipy.GRB + status = gprob.Status + + results = Results() + results.solution_loader = GurobiSolutionLoader(self) + results.timing_info.gurobi_time = gprob.Runtime + + if gprob.SolCount > 0: + if status == grb.OPTIMAL: + results.solution_status = SolutionStatus.optimal + else: + results.solution_status = SolutionStatus.feasible + else: + results.solution_status = SolutionStatus.noSolution + + if status == grb.LOADED: # problem is loaded, but no solution + results.termination_condition = TerminationCondition.unknown + elif status == grb.OPTIMAL: # optimal + results.termination_condition = ( + TerminationCondition.convergenceCriteriaSatisfied + ) + elif status == grb.INFEASIBLE: + results.termination_condition = TerminationCondition.provenInfeasible + elif status == grb.INF_OR_UNBD: + results.termination_condition = TerminationCondition.infeasibleOrUnbounded + elif status == grb.UNBOUNDED: + results.termination_condition = TerminationCondition.unbounded + elif status == grb.CUTOFF: + results.termination_condition = TerminationCondition.objectiveLimit + elif status == grb.ITERATION_LIMIT: + results.termination_condition = TerminationCondition.iterationLimit + elif status == grb.NODE_LIMIT: + results.termination_condition = TerminationCondition.iterationLimit + elif status == grb.TIME_LIMIT: + results.termination_condition = TerminationCondition.maxTimeLimit + elif status == grb.SOLUTION_LIMIT: + results.termination_condition = TerminationCondition.unknown + elif status == grb.INTERRUPTED: + results.termination_condition = TerminationCondition.interrupted + elif status == grb.NUMERIC: + results.termination_condition = TerminationCondition.unknown + elif status == grb.SUBOPTIMAL: + results.termination_condition = TerminationCondition.unknown + elif status == grb.USER_OBJ_LIMIT: + results.termination_condition = TerminationCondition.objectiveLimit + else: + results.termination_condition = TerminationCondition.unknown + + if ( + results.termination_condition + != TerminationCondition.convergenceCriteriaSatisfied + and config.raise_exception_on_nonoptimal_result + ): + raise RuntimeError( + 'Solver did not find the optimal solution. Set opt.config.raise_exception_on_nonoptimal_result = False to bypass this error.' + ) + + results.incumbent_objective = None + results.objective_bound = None + if self._objective is not None: + try: + results.incumbent_objective = gprob.ObjVal + except (gurobipy.GurobiError, AttributeError): + results.incumbent_objective = None + try: + results.objective_bound = gprob.ObjBound + except (gurobipy.GurobiError, AttributeError): + if self._objective.sense == minimize: + results.objective_bound = -math.inf + else: + results.objective_bound = math.inf + + if results.incumbent_objective is not None and not math.isfinite( + results.incumbent_objective + ): + results.incumbent_objective = None + + results.iteration_count = gprob.getAttr('IterCount') + + timer.start('load solution') + if config.load_solutions: + if gprob.SolCount > 0: + self._load_vars() + else: + raise RuntimeError( + 'A feasible solution was not found, so no solution can be loaded.' + 'Please set opt.config.load_solutions=False and check ' + 'results.solution_status and ' + 'results.incumbent_objective before loading a solution.' + ) + timer.stop('load solution') + + return results + + def _load_suboptimal_mip_solution(self, vars_to_load, solution_number): + if ( + self.get_model_attr('NumIntVars') == 0 + and self.get_model_attr('NumBinVars') == 0 + ): + raise ValueError( + 'Cannot obtain suboptimal solutions for a continuous model' + ) + var_map = self._pyomo_var_to_solver_var_map + ref_vars = self._referenced_variables + original_solution_number = self.get_gurobi_param_info('SolutionNumber')[2] + self.set_gurobi_param('SolutionNumber', solution_number) + gurobi_vars_to_load = [var_map[pyomo_var] for pyomo_var in vars_to_load] + vals = self._solver_model.getAttr("Xn", gurobi_vars_to_load) + res = ComponentMap() + for var_id, val in zip(vars_to_load, vals): + using_cons, using_sos, using_obj = ref_vars[var_id] + if using_cons or using_sos or (using_obj is not None): + res[self._vars[var_id][0]] = val + self.set_gurobi_param('SolutionNumber', original_solution_number) + return res + + def _load_vars(self, vars_to_load=None, solution_number=0): + for v, val in self._get_primals( + vars_to_load=vars_to_load, solution_number=solution_number + ).items(): + v.set_value(val, skip_validation=True) + StaleFlagManager.mark_all_as_stale(delayed=True) + + def _get_primals(self, vars_to_load=None, solution_number=0): + if self._needs_updated: + self._update_gurobi_model() # this is needed to ensure that solutions cannot be loaded after the model has been changed + + if self._solver_model.SolCount == 0: + raise RuntimeError( + 'Solver does not currently have a valid solution. Please ' + 'check the termination condition.' + ) + + var_map = self._pyomo_var_to_solver_var_map + ref_vars = self._referenced_variables + if vars_to_load is None: + vars_to_load = self._pyomo_var_to_solver_var_map.keys() + else: + vars_to_load = [id(v) for v in vars_to_load] + + if solution_number != 0: + return self._load_suboptimal_mip_solution( + vars_to_load=vars_to_load, solution_number=solution_number + ) + else: + gurobi_vars_to_load = [ + var_map[pyomo_var_id] for pyomo_var_id in vars_to_load + ] + vals = self._solver_model.getAttr("X", gurobi_vars_to_load) + + res = ComponentMap() + for var_id, val in zip(vars_to_load, vals): + using_cons, using_sos, using_obj = ref_vars[var_id] + if using_cons or using_sos or (using_obj is not None): + res[self._vars[var_id][0]] = val + return res + + def _get_reduced_costs(self, vars_to_load=None): + if self._needs_updated: + self._update_gurobi_model() + + if self._solver_model.Status != gurobipy.GRB.OPTIMAL: + raise RuntimeError( + 'Solver does not currently have valid reduced costs. Please ' + 'check the termination condition.' + ) + + var_map = self._pyomo_var_to_solver_var_map + ref_vars = self._referenced_variables + res = ComponentMap() + if vars_to_load is None: + vars_to_load = self._pyomo_var_to_solver_var_map.keys() + else: + vars_to_load = [id(v) for v in vars_to_load] + + gurobi_vars_to_load = [var_map[pyomo_var_id] for pyomo_var_id in vars_to_load] + vals = self._solver_model.getAttr("Rc", gurobi_vars_to_load) + + for var_id, val in zip(vars_to_load, vals): + using_cons, using_sos, using_obj = ref_vars[var_id] + if using_cons or using_sos or (using_obj is not None): + res[self._vars[var_id][0]] = val + + return res + + def _get_duals(self, cons_to_load=None): + if self._needs_updated: + self._update_gurobi_model() + + if self._solver_model.Status != gurobipy.GRB.OPTIMAL: + raise RuntimeError( + 'Solver does not currently have valid duals. Please ' + 'check the termination condition.' + ) + + con_map = self._pyomo_con_to_solver_con_map + reverse_con_map = self._solver_con_to_pyomo_con_map + dual = dict() + + if cons_to_load is None: + linear_cons_to_load = self._solver_model.getConstrs() + quadratic_cons_to_load = self._solver_model.getQConstrs() + else: + gurobi_cons_to_load = OrderedSet( + [con_map[pyomo_con] for pyomo_con in cons_to_load] + ) + linear_cons_to_load = list( + gurobi_cons_to_load.intersection( + OrderedSet(self._solver_model.getConstrs()) + ) + ) + quadratic_cons_to_load = list( + gurobi_cons_to_load.intersection( + OrderedSet(self._solver_model.getQConstrs()) + ) + ) + linear_vals = self._solver_model.getAttr("Pi", linear_cons_to_load) + quadratic_vals = self._solver_model.getAttr("QCPi", quadratic_cons_to_load) + + for gurobi_con, val in zip(linear_cons_to_load, linear_vals): + pyomo_con = reverse_con_map[id(gurobi_con)] + dual[pyomo_con] = val + for gurobi_con, val in zip(quadratic_cons_to_load, quadratic_vals): + pyomo_con = reverse_con_map[id(gurobi_con)] + dual[pyomo_con] = val + + return dual + + def update(self, timer: HierarchicalTimer = None): + if self._needs_updated: + self._update_gurobi_model() + super(Gurobi, self).update(timer=timer) + self._update_gurobi_model() + + def _update_gurobi_model(self): + self._solver_model.update() + self._constraints_added_since_update = OrderedSet() + self._vars_added_since_update = ComponentSet() + self._needs_updated = False + + def get_model_attr(self, attr): + """ + Get the value of an attribute on the Gurobi model. + + Parameters + ---------- + attr: str + The attribute to get. See Gurobi documentation for descriptions of the attributes. + """ + if self._needs_updated: + self._update_gurobi_model() + return self._solver_model.getAttr(attr) + + def write(self, filename): + """ + Write the model to a file (e.g., and lp file). + + Parameters + ---------- + filename: str + Name of the file to which the model should be written. + """ + self._solver_model.write(filename) + self._constraints_added_since_update = OrderedSet() + self._vars_added_since_update = ComponentSet() + self._needs_updated = False + + def set_linear_constraint_attr(self, con, attr, val): + """ + Set the value of an attribute on a gurobi linear constraint. + + Parameters + ---------- + con: pyomo.core.base.constraint.ConstraintData + The pyomo constraint for which the corresponding gurobi constraint attribute + should be modified. + attr: str + The attribute to be modified. Options are: + CBasis + DStart + Lazy + val: any + See gurobi documentation for acceptable values. + """ + if attr in {'Sense', 'RHS', 'ConstrName'}: + raise ValueError( + 'Linear constraint attr {0} cannot be set with'.format(attr) + + ' the set_linear_constraint_attr method. Please use' + + ' the remove_constraint and add_constraint methods.' + ) + self._pyomo_con_to_solver_con_map[con].setAttr(attr, val) + self._needs_updated = True + + def set_var_attr(self, var, attr, val): + """ + Set the value of an attribute on a gurobi variable. + + Parameters + ---------- + var: pyomo.core.base.var.VarData + The pyomo var for which the corresponding gurobi var attribute + should be modified. + attr: str + The attribute to be modified. Options are: + Start + VarHintVal + VarHintPri + BranchPriority + VBasis + PStart + val: any + See gurobi documentation for acceptable values. + """ + if attr in {'LB', 'UB', 'VType', 'VarName'}: + raise ValueError( + 'Var attr {0} cannot be set with'.format(attr) + + ' the set_var_attr method. Please use' + + ' the update_var method.' + ) + if attr == 'Obj': + raise ValueError( + 'Var attr Obj cannot be set with' + + ' the set_var_attr method. Please use' + + ' the set_objective method.' + ) + self._pyomo_var_to_solver_var_map[id(var)].setAttr(attr, val) + self._needs_updated = True + + def get_var_attr(self, var, attr): + """ + Get the value of an attribute on a gurobi var. + + Parameters + ---------- + var: pyomo.core.base.var.VarData + The pyomo var for which the corresponding gurobi var attribute + should be retrieved. + attr: str + The attribute to get. See gurobi documentation + """ + if self._needs_updated: + self._update_gurobi_model() + return self._pyomo_var_to_solver_var_map[id(var)].getAttr(attr) + + def get_linear_constraint_attr(self, con, attr): + """ + Get the value of an attribute on a gurobi linear constraint. + + Parameters + ---------- + con: pyomo.core.base.constraint.ConstraintData + The pyomo constraint for which the corresponding gurobi constraint attribute + should be retrieved. + attr: str + The attribute to get. See the Gurobi documentation + """ + if self._needs_updated: + self._update_gurobi_model() + return self._pyomo_con_to_solver_con_map[con].getAttr(attr) + + def get_sos_attr(self, con, attr): + """ + Get the value of an attribute on a gurobi sos constraint. + + Parameters + ---------- + con: pyomo.core.base.sos.SOSConstraintData + The pyomo SOS constraint for which the corresponding gurobi SOS constraint attribute + should be retrieved. + attr: str + The attribute to get. See the Gurobi documentation + """ + if self._needs_updated: + self._update_gurobi_model() + return self._pyomo_sos_to_solver_sos_map[con].getAttr(attr) + + def get_quadratic_constraint_attr(self, con, attr): + """ + Get the value of an attribute on a gurobi quadratic constraint. + + Parameters + ---------- + con: pyomo.core.base.constraint.ConstraintData + The pyomo constraint for which the corresponding gurobi constraint attribute + should be retrieved. + attr: str + The attribute to get. See the Gurobi documentation + """ + if self._needs_updated: + self._update_gurobi_model() + return self._pyomo_con_to_solver_con_map[con].getAttr(attr) + + def set_gurobi_param(self, param, val): + """ + Set a gurobi parameter. + + Parameters + ---------- + param: str + The gurobi parameter to set. Options include any gurobi parameter. + Please see the Gurobi documentation for options. + val: any + The value to set the parameter to. See Gurobi documentation for possible values. + """ + self._solver_model.setParam(param, val) + + def get_gurobi_param_info(self, param): + """ + Get information about a gurobi parameter. + + Parameters + ---------- + param: str + The gurobi parameter to get info for. See Gurobi documentation for possible options. + + Returns + ------- + six-tuple containing the parameter name, type, value, minimum value, maximum value, and default value. + """ + return self._solver_model.getParamInfo(param) + + def _intermediate_callback(self): + def f(gurobi_model, where): + self._callback_func(self._model, self, where) + + return f + + def set_callback(self, func=None): + """ + Specify a callback for gurobi to use. + + Parameters + ---------- + func: function + The function to call. The function should have three arguments. The first will be the pyomo model being + solved. The second will be the GurobiPersistent instance. The third will be an enum member of + gurobipy.GRB.Callback. This will indicate where in the branch and bound algorithm gurobi is at. For + example, suppose we want to solve + + .. math:: + + min 2*x + y + + s.t. + + y >= (x-2)**2 + + 0 <= x <= 4 + + y >= 0 + + y integer + + as an MILP using extended cutting planes in callbacks. + + >>> from gurobipy import GRB # doctest:+SKIP + >>> import pyomo.environ as pe + >>> from pyomo.core.expr.taylor_series import taylor_series_expansion + >>> from pyomo.contrib import appsi + >>> + >>> m = pe.ConcreteModel() + >>> m.x = pe.Var(bounds=(0, 4)) + >>> m.y = pe.Var(within=pe.Integers, bounds=(0, None)) + >>> m.obj = pe.Objective(expr=2*m.x + m.y) + >>> m.cons = pe.ConstraintList() # for the cutting planes + >>> + >>> def _add_cut(xval): + ... # a function to generate the cut + ... m.x.value = xval + ... return m.cons.add(m.y >= taylor_series_expansion((m.x - 2)**2)) + ... + >>> _c = _add_cut(0) # start with 2 cuts at the bounds of x + >>> _c = _add_cut(4) # this is an arbitrary choice + >>> + >>> opt = appsi.solvers.Gurobi() + >>> opt.config.stream_solver = True + >>> opt.set_instance(m) # doctest:+SKIP + >>> opt.gurobi_options['PreCrush'] = 1 + >>> opt.gurobi_options['LazyConstraints'] = 1 + >>> + >>> def my_callback(cb_m, cb_opt, cb_where): + ... if cb_where == GRB.Callback.MIPSOL: + ... cb_opt.cbGetSolution(vars=[m.x, m.y]) + ... if m.y.value < (m.x.value - 2)**2 - 1e-6: + ... cb_opt.cbLazy(_add_cut(m.x.value)) + ... + >>> opt.set_callback(my_callback) + >>> res = opt.solve(m) # doctest:+SKIP + + """ + if func is not None: + self._callback_func = func + self._callback = self._intermediate_callback() + else: + self._callback = None + self._callback_func = None + + def cbCut(self, con): + """ + Add a cut within a callback. + + Parameters + ---------- + con: pyomo.core.base.constraint.ConstraintData + The cut to add + """ + if not con.active: + raise ValueError('cbCut expected an active constraint.') + + if is_fixed(con.body): + raise ValueError('cbCut expected a non-trivial constraint') + + ( + gurobi_expr, + repn_constant, + mutable_linear_coefficients, + mutable_quadratic_coefficients, + ) = self._get_expr_from_pyomo_expr(con.body) + + if con.has_lb(): + if con.has_ub(): + raise ValueError('Range constraints are not supported in cbCut.') + if not is_fixed(con.lower): + raise ValueError( + 'Lower bound of constraint {0} is not constant.'.format(con) + ) + if con.has_ub(): + if not is_fixed(con.upper): + raise ValueError( + 'Upper bound of constraint {0} is not constant.'.format(con) + ) + + if con.equality: + self._solver_model.cbCut( + lhs=gurobi_expr, + sense=gurobipy.GRB.EQUAL, + rhs=value(con.lower - repn_constant), + ) + elif con.has_lb() and (value(con.lower) > -float('inf')): + self._solver_model.cbCut( + lhs=gurobi_expr, + sense=gurobipy.GRB.GREATER_EQUAL, + rhs=value(con.lower - repn_constant), + ) + elif con.has_ub() and (value(con.upper) < float('inf')): + self._solver_model.cbCut( + lhs=gurobi_expr, + sense=gurobipy.GRB.LESS_EQUAL, + rhs=value(con.upper - repn_constant), + ) + else: + raise ValueError( + 'Constraint does not have a lower or an upper bound {0} \n'.format(con) + ) + + def cbGet(self, what): + return self._solver_model.cbGet(what) + + def cbGetNodeRel(self, vars): + """ + Parameters + ---------- + vars: Var or iterable of Var + """ + if not isinstance(vars, Iterable): + vars = [vars] + gurobi_vars = [self._pyomo_var_to_solver_var_map[id(i)] for i in vars] + var_values = self._solver_model.cbGetNodeRel(gurobi_vars) + for i, v in enumerate(vars): + v.set_value(var_values[i], skip_validation=True) + + def cbGetSolution(self, vars): + """ + Parameters + ---------- + vars: iterable of vars + """ + if not isinstance(vars, Iterable): + vars = [vars] + gurobi_vars = [self._pyomo_var_to_solver_var_map[id(i)] for i in vars] + var_values = self._solver_model.cbGetSolution(gurobi_vars) + for i, v in enumerate(vars): + v.set_value(var_values[i], skip_validation=True) + + def cbLazy(self, con): + """ + Parameters + ---------- + con: pyomo.core.base.constraint.ConstraintData + The lazy constraint to add + """ + if not con.active: + raise ValueError('cbLazy expected an active constraint.') + + if is_fixed(con.body): + raise ValueError('cbLazy expected a non-trivial constraint') + + ( + gurobi_expr, + repn_constant, + mutable_linear_coefficients, + mutable_quadratic_coefficients, + ) = self._get_expr_from_pyomo_expr(con.body) + + if con.has_lb(): + if con.has_ub(): + raise ValueError('Range constraints are not supported in cbLazy.') + if not is_fixed(con.lower): + raise ValueError( + 'Lower bound of constraint {0} is not constant.'.format(con) + ) + if con.has_ub(): + if not is_fixed(con.upper): + raise ValueError( + 'Upper bound of constraint {0} is not constant.'.format(con) + ) + + if con.equality: + self._solver_model.cbLazy( + lhs=gurobi_expr, + sense=gurobipy.GRB.EQUAL, + rhs=value(con.lower - repn_constant), + ) + elif con.has_lb() and (value(con.lower) > -float('inf')): + self._solver_model.cbLazy( + lhs=gurobi_expr, + sense=gurobipy.GRB.GREATER_EQUAL, + rhs=value(con.lower - repn_constant), + ) + elif con.has_ub() and (value(con.upper) < float('inf')): + self._solver_model.cbLazy( + lhs=gurobi_expr, + sense=gurobipy.GRB.LESS_EQUAL, + rhs=value(con.upper - repn_constant), + ) + else: + raise ValueError( + 'Constraint does not have a lower or an upper bound {0} \n'.format(con) + ) + + def cbSetSolution(self, vars, solution): + if not isinstance(vars, Iterable): + vars = [vars] + gurobi_vars = [self._pyomo_var_to_solver_var_map[id(i)] for i in vars] + self._solver_model.cbSetSolution(gurobi_vars, solution) + + def cbUseSolution(self): + return self._solver_model.cbUseSolution() + + def reset(self): + self._solver_model.reset() diff --git a/pyomo/contrib/solver/gurobi_direct.py b/pyomo/contrib/solver/gurobi_direct.py new file mode 100644 index 00000000000..edca7018f92 --- /dev/null +++ b/pyomo/contrib/solver/gurobi_direct.py @@ -0,0 +1,420 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import datetime +import io +import math +import os + +from pyomo.common.config import ConfigValue +from pyomo.common.collections import ComponentMap, ComponentSet +from pyomo.common.dependencies import attempt_import +from pyomo.common.enums import ObjectiveSense +from pyomo.common.shutdown import python_is_shutting_down +from pyomo.common.tee import capture_output, TeeStream +from pyomo.common.timing import HierarchicalTimer + +from pyomo.contrib.solver.base import SolverBase +from pyomo.contrib.solver.config import BranchAndBoundConfig +from pyomo.contrib.solver.results import Results, SolutionStatus, TerminationCondition +from pyomo.contrib.solver.solution import SolutionLoaderBase + +from pyomo.core.staleflag import StaleFlagManager + +from pyomo.repn.plugins.standard_form import LinearStandardFormCompiler + +gurobipy, gurobipy_available = attempt_import('gurobipy') + + +class GurobiConfig(BranchAndBoundConfig): + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super(GurobiConfig, self).__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + self.use_mipstart: bool = self.declare( + 'use_mipstart', + ConfigValue( + default=False, + domain=bool, + description="If True, the current values of the integer variables " + "will be passed to Gurobi.", + ), + ) + + +class GurobiDirectSolutionLoader(SolutionLoaderBase): + def __init__(self, grb_model, grb_cons, grb_vars, pyo_cons, pyo_vars, pyo_obj): + self._grb_model = grb_model + self._grb_cons = grb_cons + self._grb_vars = grb_vars + self._pyo_cons = pyo_cons + self._pyo_vars = pyo_vars + self._pyo_obj = pyo_obj + GurobiDirect._num_instances += 1 + + def __del__(self): + if not python_is_shutting_down(): + GurobiDirect._num_instances -= 1 + if GurobiDirect._num_instances == 0: + GurobiDirect.release_license() + + def load_vars(self, vars_to_load=None, solution_number=0): + assert solution_number == 0 + if self._grb_model.SolCount == 0: + raise RuntimeError( + 'Solver does not currently have a valid solution. Please ' + 'check the termination condition.' + ) + + iterator = zip(self._pyo_vars, self._grb_vars.x.tolist()) + if vars_to_load: + vars_to_load = ComponentSet(vars_to_load) + iterator = filter(lambda var_val: var_val[0] in vars_to_load, iterator) + for p_var, g_var in iterator: + p_var.set_value(g_var, skip_validation=True) + StaleFlagManager.mark_all_as_stale(delayed=True) + + def get_primals(self, vars_to_load=None, solution_number=0): + assert solution_number == 0 + if self._grb_model.SolCount == 0: + raise RuntimeError( + 'Solver does not currently have a valid solution. Please ' + 'check the termination condition.' + ) + + iterator = zip(self._pyo_vars, self._grb_vars.x.tolist()) + if vars_to_load: + vars_to_load = ComponentSet(vars_to_load) + iterator = filter(lambda var_val: var_val[0] in vars_to_load, iterator) + return ComponentMap(iterator) + + def get_duals(self, cons_to_load=None): + if self._grb_model.Status != gurobipy.GRB.OPTIMAL: + raise RuntimeError( + 'Solver does not currently have valid duals. Please ' + 'check the termination condition.' + ) + + def dedup(_iter): + last = None + for con_info_dual in _iter: + if not con_info_dual[1] and con_info_dual[0][0] is last: + continue + last = con_info_dual[0][0] + yield con_info_dual + + iterator = dedup(zip(self._pyo_cons, self._grb_cons.getAttr('Pi').tolist())) + if cons_to_load: + cons_to_load = set(cons_to_load) + iterator = filter( + lambda con_info_dual: con_info_dual[0][0] in cons_to_load, iterator + ) + return {con_info[0]: dual for con_info, dual in iterator} + + def get_reduced_costs(self, vars_to_load=None): + if self._grb_model.Status != gurobipy.GRB.OPTIMAL: + raise RuntimeError( + 'Solver does not currently have valid reduced costs. Please ' + 'check the termination condition.' + ) + + iterator = zip(self._pyo_vars, self._grb_vars.getAttr('Rc').tolist()) + if vars_to_load: + vars_to_load = ComponentSet(vars_to_load) + iterator = filter(lambda var_rc: var_rc[0] in vars_to_load, iterator) + return ComponentMap(iterator) + + +class GurobiDirect(SolverBase): + CONFIG = GurobiConfig() + + _available = None + _num_instances = 0 + _tc_map = None + + def __init__(self, **kwds): + super().__init__(**kwds) + GurobiDirect._num_instances += 1 + + def available(self): + if not gurobipy_available: # this triggers the deferred import + return self.Availability.NotFound + elif self._available == self.Availability.BadVersion: + return self.Availability.BadVersion + else: + return self._check_license() + + def _check_license(self): + avail = False + try: + # Gurobipy writes out license file information when creating + # the environment + with capture_output(capture_fd=True): + m = gurobipy.Model() + avail = True + except gurobipy.GurobiError: + avail = False + + if avail: + if self._available is None: + self._available = GurobiDirect._check_full_license(m) + return self._available + else: + return self.Availability.BadLicense + + @classmethod + def _check_full_license(cls, model=None): + if model is None: + model = gurobipy.Model() + model.setParam('OutputFlag', 0) + try: + model.addVars(range(2001)) + model.optimize() + return cls.Availability.FullLicense + except gurobipy.GurobiError: + return cls.Availability.LimitedLicense + + def __del__(self): + if not python_is_shutting_down(): + GurobiDirect._num_instances -= 1 + if GurobiDirect._num_instances == 0: + self.release_license() + + @staticmethod + def release_license(): + if gurobipy_available: + with capture_output(capture_fd=True): + gurobipy.disposeDefaultEnv() + + def version(self): + version = ( + gurobipy.GRB.VERSION_MAJOR, + gurobipy.GRB.VERSION_MINOR, + gurobipy.GRB.VERSION_TECHNICAL, + ) + return version + + def solve(self, model, **kwds) -> Results: + start_timestamp = datetime.datetime.now(datetime.timezone.utc) + config = self.config(value=kwds, preserve_implicit=True) + if config.timer is None: + config.timer = HierarchicalTimer() + timer = config.timer + + StaleFlagManager.mark_all_as_stale() + + timer.start('compile_model') + repn = LinearStandardFormCompiler().write( + model, mixed_form=True, set_sense=None + ) + timer.stop('compile_model') + + if len(repn.objectives) > 1: + raise ValueError( + f"The {self.__class__.__name__} solver only supports models " + f"with zero or one objectives (received {len(repn.objectives)})." + ) + + timer.start('prepare_matrices') + inf = float('inf') + ninf = -inf + lb = [] + ub = [] + for v in repn.columns: + _l, _u = v.bounds + if _l is None: + _l = ninf + if _u is None: + _u = inf + lb.append(_l) + ub.append(_u) + CON = gurobipy.GRB.CONTINUOUS + BIN = gurobipy.GRB.BINARY + INT = gurobipy.GRB.INTEGER + vtype = [ + ( + CON + if v.is_continuous() + else (BIN if v.is_binary() else INT if v.is_integer() else '?') + ) + for v in repn.columns + ] + sense_type = '=<>' # Note: ordering matches 0, 1, -1 + sense = [sense_type[r[1]] for r in repn.rows] + timer.stop('prepare_matrices') + + ostreams = [io.StringIO()] + config.tee + + try: + orig_cwd = os.getcwd() + if config.working_dir: + os.chdir(config.working_dir) + with TeeStream(*ostreams) as t, capture_output(t.STDOUT, capture_fd=False): + gurobi_model = gurobipy.Model() + + timer.start('transfer_model') + x = gurobi_model.addMVar( + len(repn.columns), + lb=lb, + ub=ub, + obj=repn.c.todense()[0] if repn.c.shape[0] else 0, + vtype=vtype, + ) + A = gurobi_model.addMConstr(repn.A, x, sense, repn.rhs) + if repn.c.shape[0]: + gurobi_model.setAttr('ObjCon', repn.c_offset[0]) + gurobi_model.setAttr('ModelSense', int(repn.objectives[0].sense)) + # Note: calling gurobi_model.update() here is not + # necessary (it will happen as part of optimize()) + timer.stop('transfer_model') + + options = config.solver_options + + gurobi_model.setParam('LogToConsole', 1) + + if config.threads is not None: + gurobi_model.setParam('Threads', config.threads) + if config.time_limit is not None: + gurobi_model.setParam('TimeLimit', config.time_limit) + if config.rel_gap is not None: + gurobi_model.setParam('MIPGap', config.rel_gap) + if config.abs_gap is not None: + gurobi_model.setParam('MIPGapAbs', config.abs_gap) + + if config.use_mipstart: + raise MouseTrap("MIPSTART not yet supported") + + for key, option in options.items(): + gurobi_model.setParam(key, option) + + timer.start('optimize') + gurobi_model.optimize() + timer.stop('optimize') + finally: + os.chdir(orig_cwd) + + res = self._postsolve( + timer, + config, + GurobiDirectSolutionLoader( + gurobi_model, A, x, repn.rows, repn.columns, repn.objectives + ), + ) + res.solver_configuration = config + res.solver_name = 'Gurobi' + res.solver_version = self.version() + res.solver_log = ostreams[0].getvalue() + + end_timestamp = datetime.datetime.now(datetime.timezone.utc) + res.timing_info.start_timestamp = start_timestamp + res.timing_info.wall_time = (end_timestamp - start_timestamp).total_seconds() + res.timing_info.timer = timer + return res + + def _postsolve(self, timer: HierarchicalTimer, config, loader): + grb_model = loader._grb_model + status = grb_model.Status + + results = Results() + results.solution_loader = loader + results.timing_info.gurobi_time = grb_model.Runtime + + if grb_model.SolCount > 0: + if status == gurobipy.GRB.OPTIMAL: + results.solution_status = SolutionStatus.optimal + else: + results.solution_status = SolutionStatus.feasible + else: + results.solution_status = SolutionStatus.noSolution + + results.termination_condition = self._get_tc_map().get( + status, TerminationCondition.unknown + ) + + if ( + results.termination_condition + != TerminationCondition.convergenceCriteriaSatisfied + and config.raise_exception_on_nonoptimal_result + ): + raise RuntimeError( + 'Solver did not find the optimal solution. Set ' + 'opt.config.raise_exception_on_nonoptimal_result=False ' + 'to bypass this error.' + ) + + if loader._pyo_obj: + try: + if math.isfinite(grb_model.ObjVal): + results.incumbent_objective = grb_model.ObjVal + else: + results.incumbent_objective = None + except (gurobipy.GurobiError, AttributeError): + results.incumbent_objective = None + try: + results.objective_bound = grb_model.ObjBound + except (gurobipy.GurobiError, AttributeError): + if grb_model.ModelSense == ObjectiveSense.minimize: + results.objective_bound = -math.inf + else: + results.objective_bound = math.inf + else: + results.incumbent_objective = None + results.objective_bound = None + + results.iteration_count = grb_model.getAttr('IterCount') + + timer.start('load solution') + if config.load_solutions: + if grb_model.SolCount > 0: + results.solution_loader.load_vars() + else: + raise RuntimeError( + 'A feasible solution was not found, so no solution can be loaded.' + 'Please set opt.config.load_solutions=False and check ' + 'results.solution_status and ' + 'results.incumbent_objective before loading a solution.' + ) + timer.stop('load solution') + + return results + + def _get_tc_map(self): + if GurobiDirect._tc_map is None: + grb = gurobipy.GRB + tc = TerminationCondition + GurobiDirect._tc_map = { + grb.LOADED: tc.unknown, # problem is loaded, but no solution + grb.OPTIMAL: tc.convergenceCriteriaSatisfied, + grb.INFEASIBLE: tc.provenInfeasible, + grb.INF_OR_UNBD: tc.infeasibleOrUnbounded, + grb.UNBOUNDED: tc.unbounded, + grb.CUTOFF: tc.objectiveLimit, + grb.ITERATION_LIMIT: tc.iterationLimit, + grb.NODE_LIMIT: tc.iterationLimit, + grb.TIME_LIMIT: tc.maxTimeLimit, + grb.SOLUTION_LIMIT: tc.unknown, + grb.INTERRUPTED: tc.interrupted, + grb.NUMERIC: tc.unknown, + grb.SUBOPTIMAL: tc.unknown, + grb.USER_OBJ_LIMIT: tc.objectiveLimit, + } + return GurobiDirect._tc_map diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py new file mode 100644 index 00000000000..c88696f531b --- /dev/null +++ b/pyomo/contrib/solver/ipopt.py @@ -0,0 +1,537 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import logging +import os +import subprocess +import datetime +import io +from typing import Mapping, Optional, Sequence + +from pyomo.common import Executable +from pyomo.common.config import ConfigValue, document_kwargs_from_configdict, ConfigDict +from pyomo.common.errors import ( + PyomoException, + DeveloperError, + InfeasibleConstraintException, +) +from pyomo.common.tempfiles import TempfileManager +from pyomo.common.timing import HierarchicalTimer +from pyomo.core.base.var import VarData +from pyomo.core.staleflag import StaleFlagManager +from pyomo.repn.plugins.nl_writer import NLWriter, NLWriterInfo +from pyomo.contrib.solver.base import SolverBase +from pyomo.contrib.solver.config import SolverConfig +from pyomo.contrib.solver.results import Results, TerminationCondition, SolutionStatus +from pyomo.contrib.solver.sol_reader import parse_sol_file +from pyomo.contrib.solver.solution import SolSolutionLoader +from pyomo.common.tee import TeeStream +from pyomo.core.expr.visitor import replace_expressions +from pyomo.core.expr.numvalue import value +from pyomo.core.base.suffix import Suffix +from pyomo.common.collections import ComponentMap + +logger = logging.getLogger(__name__) + + +class IpoptSolverError(PyomoException): + """ + General exception to catch solver system errors + """ + + +class IpoptConfig(SolverConfig): + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.executable: Executable = self.declare( + 'executable', + ConfigValue( + default=Executable('ipopt'), + description="Preferred executable for ipopt. Defaults to searching the " + "``PATH`` for the first available ``ipopt``.", + ), + ) + self.writer_config: ConfigDict = self.declare( + 'writer_config', NLWriter.CONFIG() + ) + + +class IpoptSolutionLoader(SolSolutionLoader): + def get_reduced_costs( + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: + if self._nl_info is None: + raise RuntimeError( + 'Solution loader does not currently have a valid solution. Please ' + 'check results.TerminationCondition and/or results.SolutionStatus.' + ) + if len(self._nl_info.eliminated_vars) > 0: + raise NotImplementedError( + 'For now, turn presolve off (opt.config.writer_config.linear_presolve=False) ' + 'to get dual variable values.' + ) + if self._sol_data is None: + raise DeveloperError( + "Solution data is empty. This should not " + "have happened. Report this error to the Pyomo Developers." + ) + if self._nl_info.scaling is None: + scale_list = [1] * len(self._nl_info.variables) + obj_scale = 1 + else: + scale_list = self._nl_info.scaling.variables + obj_scale = self._nl_info.scaling.objectives[0] + sol_data = self._sol_data + nl_info = self._nl_info + zl_map = sol_data.var_suffixes['ipopt_zL_out'] + zu_map = sol_data.var_suffixes['ipopt_zU_out'] + rc = dict() + for ndx, v in enumerate(nl_info.variables): + scale = scale_list[ndx] + v_id = id(v) + rc[v_id] = (v, 0) + if ndx in zl_map: + zl = zl_map[ndx] * scale / obj_scale + if abs(zl) > abs(rc[v_id][1]): + rc[v_id] = (v, zl) + if ndx in zu_map: + zu = zu_map[ndx] * scale / obj_scale + if abs(zu) > abs(rc[v_id][1]): + rc[v_id] = (v, zu) + + if vars_to_load is None: + res = ComponentMap(rc.values()) + for v, _ in nl_info.eliminated_vars: + res[v] = 0 + else: + res = ComponentMap() + for v in vars_to_load: + if id(v) in rc: + res[v] = rc[id(v)][1] + else: + # eliminated vars + res[v] = 0 + return res + + +ipopt_command_line_options = { + 'acceptable_compl_inf_tol', + 'acceptable_constr_viol_tol', + 'acceptable_dual_inf_tol', + 'acceptable_tol', + 'alpha_for_y', + 'bound_frac', + 'bound_mult_init_val', + 'bound_push', + 'bound_relax_factor', + 'compl_inf_tol', + 'constr_mult_init_max', + 'constr_viol_tol', + 'diverging_iterates_tol', + 'dual_inf_tol', + 'expect_infeasible_problem', + 'file_print_level', + 'halt_on_ampl_error', + 'hessian_approximation', + 'honor_original_bounds', + 'linear_scaling_on_demand', + 'linear_solver', + 'linear_system_scaling', + 'ma27_pivtol', + 'ma27_pivtolmax', + 'ma57_pivot_order', + 'ma57_pivtol', + 'ma57_pivtolmax', + 'max_cpu_time', + 'max_iter', + 'max_refinement_steps', + 'max_soc', + 'maxit', + 'min_refinement_steps', + 'mu_init', + 'mu_max', + 'mu_oracle', + 'mu_strategy', + 'nlp_scaling_max_gradient', + 'nlp_scaling_method', + 'obj_scaling_factor', + 'option_file_name', + 'outlev', + 'output_file', + 'pardiso_matching_strategy', + 'print_level', + 'print_options_documentation', + 'print_user_options', + 'required_infeasibility_reduction', + 'slack_bound_frac', + 'slack_bound_push', + 'tol', + 'wantsol', + 'warm_start_bound_push', + 'warm_start_init_point', + 'warm_start_mult_bound_push', + 'watchdog_shortened_iter_trigger', +} + + +class Ipopt(SolverBase): + CONFIG = IpoptConfig() + + def __init__(self, **kwds): + super().__init__(**kwds) + self._writer = NLWriter() + self._available_cache = None + self._version_cache = None + self._version_timeout = 2 + + def available(self, config=None): + if config is None: + config = self.config + pth = config.executable.path() + if self._available_cache is None or self._available_cache[0] != pth: + if pth is None: + self._available_cache = (None, self.Availability.NotFound) + else: + self._available_cache = (pth, self.Availability.FullLicense) + return self._available_cache[1] + + def version(self, config=None): + if config is None: + config = self.config + pth = config.executable.path() + if self._version_cache is None or self._version_cache[0] != pth: + if pth is None: + self._version_cache = (None, None) + else: + results = subprocess.run( + [str(pth), '--version'], + timeout=self._version_timeout, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True, + ) + version = results.stdout.splitlines()[0] + version = version.split(' ')[1].strip() + version = tuple(int(i) for i in version.split('.')) + self._version_cache = (pth, version) + return self._version_cache[1] + + def _write_options_file(self, filename: str, options: Mapping): + # First we need to determine if we even need to create a file. + # If options is empty, then we return False + opt_file_exists = False + if not options: + return False + # If it has options in it, parse them and write them to a file. + # If they are command line options, ignore them; they will be + # parsed during _create_command_line + for k, val in options.items(): + if k not in ipopt_command_line_options: + opt_file_exists = True + with open(filename + '.opt', 'a+') as opt_file: + opt_file.write(str(k) + ' ' + str(val) + '\n') + return opt_file_exists + + def _create_command_line(self, basename: str, config: IpoptConfig, opt_file: bool): + cmd = [str(config.executable), basename + '.nl', '-AMPL'] + if opt_file: + cmd.append('option_file_name=' + basename + '.opt') + if 'option_file_name' in config.solver_options: + raise ValueError( + 'Pyomo generates the ipopt options file as part of the `solve` method. ' + 'Add all options to ipopt.config.solver_options instead.' + ) + if ( + config.time_limit is not None + and 'max_cpu_time' not in config.solver_options + ): + config.solver_options['max_cpu_time'] = config.time_limit + for k, val in config.solver_options.items(): + if k in ipopt_command_line_options: + cmd.append(str(k) + '=' + str(val)) + return cmd + + @document_kwargs_from_configdict(CONFIG) + def solve(self, model, **kwds): + # Begin time tracking + start_timestamp = datetime.datetime.now(datetime.timezone.utc) + # Update configuration options, based on keywords passed to solve + config: IpoptConfig = self.config(value=kwds, preserve_implicit=True) + # Check if solver is available + avail = self.available(config) + if not avail: + raise IpoptSolverError( + f'Solver {self.__class__} is not available ({avail}).' + ) + if config.threads: + logger.log( + logging.WARNING, + msg=f"The `threads` option was specified, but this is not used by {self.__class__}.", + ) + if config.timer is None: + timer = HierarchicalTimer() + else: + timer = config.timer + StaleFlagManager.mark_all_as_stale() + with TempfileManager.new_context() as tempfile: + if config.working_dir is None: + dname = tempfile.mkdtemp() + else: + dname = config.working_dir + if not os.path.exists(dname): + os.mkdir(dname) + basename = os.path.join(dname, model.name) + if os.path.exists(basename + '.nl'): + raise RuntimeError( + f"NL file with the same name {basename + '.nl'} already exists!" + ) + # Note: the ASL has an issue where string constants written + # to the NL file (e.g. arguments in external functions) MUST + # be terminated with '\n' regardless of platform. We will + # disable universal newlines in the NL file to prevent + # Python from mapping those '\n' to '\r\n' on Windows. + with open(basename + '.nl', 'w', newline='\n') as nl_file, open( + basename + '.row', 'w' + ) as row_file, open(basename + '.col', 'w') as col_file: + timer.start('write_nl_file') + self._writer.config.set_value(config.writer_config) + try: + nl_info = self._writer.write( + model, + nl_file, + row_file, + col_file, + symbolic_solver_labels=config.symbolic_solver_labels, + ) + proven_infeasible = False + except InfeasibleConstraintException: + proven_infeasible = True + timer.stop('write_nl_file') + if not proven_infeasible and len(nl_info.variables) > 0: + # Get a copy of the environment to pass to the subprocess + env = os.environ.copy() + if nl_info.external_function_libraries: + if env.get('AMPLFUNC'): + nl_info.external_function_libraries.append(env.get('AMPLFUNC')) + env['AMPLFUNC'] = "\n".join(nl_info.external_function_libraries) + # Write the opt_file, if there should be one; return a bool to say + # whether or not we have one (so we can correctly build the command line) + opt_file = self._write_options_file( + filename=basename, options=config.solver_options + ) + # Call ipopt - passing the files via the subprocess + cmd = self._create_command_line( + basename=basename, config=config, opt_file=opt_file + ) + # this seems silly, but we have to give the subprocess slightly longer to finish than + # ipopt + if config.time_limit is not None: + timeout = config.time_limit + min( + max(1.0, 0.01 * config.time_limit), 100 + ) + else: + timeout = None + + ostreams = [io.StringIO()] + config.tee + with TeeStream(*ostreams) as t: + timer.start('subprocess') + process = subprocess.run( + cmd, + timeout=timeout, + env=env, + universal_newlines=True, + stdout=t.STDOUT, + stderr=t.STDERR, + ) + timer.stop('subprocess') + # This is the stuff we need to parse to get the iterations + # and time + (iters, ipopt_time_nofunc, ipopt_time_func, ipopt_total_time) = ( + self._parse_ipopt_output(ostreams[0]) + ) + + if proven_infeasible: + results = Results() + results.termination_condition = TerminationCondition.provenInfeasible + results.solution_loader = SolSolutionLoader(None, None) + results.iteration_count = 0 + results.timing_info.total_seconds = 0 + elif len(nl_info.variables) == 0: + if len(nl_info.eliminated_vars) == 0: + results = Results() + results.termination_condition = TerminationCondition.emptyModel + results.solution_loader = SolSolutionLoader(None, None) + else: + results = Results() + results.termination_condition = ( + TerminationCondition.convergenceCriteriaSatisfied + ) + results.solution_status = SolutionStatus.optimal + results.solution_loader = SolSolutionLoader(None, nl_info=nl_info) + results.iteration_count = 0 + results.timing_info.total_seconds = 0 + else: + if os.path.isfile(basename + '.sol'): + with open(basename + '.sol', 'r') as sol_file: + timer.start('parse_sol') + results = self._parse_solution(sol_file, nl_info) + timer.stop('parse_sol') + else: + results = Results() + if process.returncode != 0: + results.extra_info.return_code = process.returncode + results.termination_condition = TerminationCondition.error + results.solution_loader = SolSolutionLoader(None, None) + else: + results.iteration_count = iters + if ipopt_time_nofunc is not None: + results.timing_info.ipopt_excluding_nlp_functions = ( + ipopt_time_nofunc + ) + + if ipopt_time_func is not None: + results.timing_info.nlp_function_evaluations = ipopt_time_func + if ipopt_total_time is not None: + results.timing_info.total_seconds = ipopt_total_time + if ( + config.raise_exception_on_nonoptimal_result + and results.solution_status != SolutionStatus.optimal + ): + raise RuntimeError( + 'Solver did not find the optimal solution. Set ' + 'opt.config.raise_exception_on_nonoptimal_result = False to bypass this error.' + ) + + results.solver_name = self.name + results.solver_version = self.version(config) + if ( + config.load_solutions + and results.solution_status == SolutionStatus.noSolution + ): + raise RuntimeError( + 'A feasible solution was not found, so no solution can be loaded.' + 'Please set opt.config.load_solutions=False to bypass this error.' + ) + + if config.load_solutions: + results.solution_loader.load_vars() + if ( + hasattr(model, 'dual') + and isinstance(model.dual, Suffix) + and model.dual.import_enabled() + ): + model.dual.update(results.solution_loader.get_duals()) + if ( + hasattr(model, 'rc') + and isinstance(model.rc, Suffix) + and model.rc.import_enabled() + ): + model.rc.update(results.solution_loader.get_reduced_costs()) + + if ( + results.solution_status in {SolutionStatus.feasible, SolutionStatus.optimal} + and len(nl_info.objectives) > 0 + ): + if config.load_solutions: + results.incumbent_objective = value(nl_info.objectives[0]) + else: + results.incumbent_objective = value( + replace_expressions( + nl_info.objectives[0].expr, + substitution_map={ + id(v): val + for v, val in results.solution_loader.get_primals().items() + }, + descend_into_named_expressions=True, + remove_named_expressions=True, + ) + ) + + results.solver_configuration = config + if not proven_infeasible and len(nl_info.variables) > 0: + results.solver_log = ostreams[0].getvalue() + + # Capture/record end-time / wall-time + end_timestamp = datetime.datetime.now(datetime.timezone.utc) + results.timing_info.start_timestamp = start_timestamp + results.timing_info.wall_time = ( + end_timestamp - start_timestamp + ).total_seconds() + results.timing_info.timer = timer + return results + + def _parse_ipopt_output(self, stream: io.StringIO): + """ + Parse an IPOPT output file and return: + + * number of iterations + * time in IPOPT + + """ + + iters = None + nofunc_time = None + func_time = None + total_time = None + # parse the output stream to get the iteration count and solver time + for line in stream.getvalue().splitlines(): + if line.startswith("Number of Iterations....:"): + tokens = line.split() + iters = int(tokens[-1]) + elif line.startswith( + "Total seconds in IPOPT =" + ): + # Newer versions of IPOPT no longer separate timing into + # two different values. This is so we have compatibility with + # both new and old versions + tokens = line.split() + total_time = float(tokens[-1]) + elif line.startswith( + "Total CPU secs in IPOPT (w/o function evaluations) =" + ): + tokens = line.split() + nofunc_time = float(tokens[-1]) + elif line.startswith( + "Total CPU secs in NLP function evaluations =" + ): + tokens = line.split() + func_time = float(tokens[-1]) + + return iters, nofunc_time, func_time, total_time + + def _parse_solution(self, instream: io.TextIOBase, nl_info: NLWriterInfo): + results = Results() + res, sol_data = parse_sol_file( + sol_file=instream, nl_info=nl_info, result=results + ) + + if res.solution_status == SolutionStatus.noSolution: + res.solution_loader = SolSolutionLoader(None, None) + else: + res.solution_loader = IpoptSolutionLoader( + sol_data=sol_data, nl_info=nl_info + ) + + return res diff --git a/pyomo/contrib/solver/persistent.py b/pyomo/contrib/solver/persistent.py new file mode 100644 index 00000000000..71322b7043e --- /dev/null +++ b/pyomo/contrib/solver/persistent.py @@ -0,0 +1,523 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# __________________________________________________________________________ + +import abc +from typing import List + +from pyomo.core.base.constraint import ConstraintData, Constraint +from pyomo.core.base.sos import SOSConstraintData, SOSConstraint +from pyomo.core.base.var import VarData +from pyomo.core.base.param import ParamData, Param +from pyomo.core.base.objective import ObjectiveData +from pyomo.common.collections import ComponentMap +from pyomo.common.timing import HierarchicalTimer +from pyomo.core.expr.numvalue import NumericConstant +from pyomo.contrib.solver.util import collect_vars_and_named_exprs, get_objective + + +class PersistentSolverUtils(abc.ABC): + def __init__(self): + self._model = None + self._active_constraints = {} # maps constraint to (lower, body, upper) + self._vars = {} # maps var id to (var, lb, ub, fixed, domain, value) + self._params = {} # maps param id to param + self._objective = None + self._objective_expr = None + self._objective_sense = None + self._named_expressions = ( + {} + ) # maps constraint to list of tuples (named_expr, named_expr.expr) + self._external_functions = ComponentMap() + self._obj_named_expressions = [] + self._referenced_variables = ( + {} + ) # var_id: [dict[constraints, None], dict[sos constraints, None], None or objective] + self._vars_referenced_by_con = {} + self._vars_referenced_by_obj = [] + self._expr_types = None + + def set_instance(self, model): + saved_config = self.config + self.__init__() + self.config = saved_config + self._model = model + self.add_block(model) + if self._objective is None: + self.set_objective(None) + + @abc.abstractmethod + def _add_variables(self, variables: List[VarData]): + pass + + def add_variables(self, variables: List[VarData]): + for v in variables: + if id(v) in self._referenced_variables: + raise ValueError( + 'variable {name} has already been added'.format(name=v.name) + ) + self._referenced_variables[id(v)] = [{}, {}, None] + self._vars[id(v)] = ( + v, + v._lb, + v._ub, + v.fixed, + v.domain.get_interval(), + v.value, + ) + self._add_variables(variables) + + @abc.abstractmethod + def _add_parameters(self, params: List[ParamData]): + pass + + def add_parameters(self, params: List[ParamData]): + for p in params: + self._params[id(p)] = p + self._add_parameters(params) + + @abc.abstractmethod + def _add_constraints(self, cons: List[ConstraintData]): + pass + + def _check_for_new_vars(self, variables: List[VarData]): + new_vars = {} + for v in variables: + v_id = id(v) + if v_id not in self._referenced_variables: + new_vars[v_id] = v + self.add_variables(list(new_vars.values())) + + def _check_to_remove_vars(self, variables: List[VarData]): + vars_to_remove = {} + for v in variables: + v_id = id(v) + ref_cons, ref_sos, ref_obj = self._referenced_variables[v_id] + if len(ref_cons) == 0 and len(ref_sos) == 0 and ref_obj is None: + vars_to_remove[v_id] = v + self.remove_variables(list(vars_to_remove.values())) + + def add_constraints(self, cons: List[ConstraintData]): + all_fixed_vars = {} + for con in cons: + if con in self._named_expressions: + raise ValueError( + 'constraint {name} has already been added'.format(name=con.name) + ) + self._active_constraints[con] = (con.lower, con.body, con.upper) + tmp = collect_vars_and_named_exprs(con.body) + named_exprs, variables, fixed_vars, external_functions = tmp + self._check_for_new_vars(variables) + self._named_expressions[con] = [(e, e.expr) for e in named_exprs] + if len(external_functions) > 0: + self._external_functions[con] = external_functions + self._vars_referenced_by_con[con] = variables + for v in variables: + self._referenced_variables[id(v)][0][con] = None + if not self.config.auto_updates.treat_fixed_vars_as_params: + for v in fixed_vars: + v.unfix() + all_fixed_vars[id(v)] = v + self._add_constraints(cons) + for v in all_fixed_vars.values(): + v.fix() + + @abc.abstractmethod + def _add_sos_constraints(self, cons: List[SOSConstraintData]): + pass + + def add_sos_constraints(self, cons: List[SOSConstraintData]): + for con in cons: + if con in self._vars_referenced_by_con: + raise ValueError( + 'constraint {name} has already been added'.format(name=con.name) + ) + self._active_constraints[con] = tuple() + variables = con.get_variables() + self._check_for_new_vars(variables) + self._named_expressions[con] = [] + self._vars_referenced_by_con[con] = variables + for v in variables: + self._referenced_variables[id(v)][1][con] = None + self._add_sos_constraints(cons) + + @abc.abstractmethod + def _set_objective(self, obj: ObjectiveData): + pass + + def set_objective(self, obj: ObjectiveData): + if self._objective is not None: + for v in self._vars_referenced_by_obj: + self._referenced_variables[id(v)][2] = None + self._check_to_remove_vars(self._vars_referenced_by_obj) + self._external_functions.pop(self._objective, None) + if obj is not None: + self._objective = obj + self._objective_expr = obj.expr + self._objective_sense = obj.sense + tmp = collect_vars_and_named_exprs(obj.expr) + named_exprs, variables, fixed_vars, external_functions = tmp + self._check_for_new_vars(variables) + self._obj_named_expressions = [(i, i.expr) for i in named_exprs] + if len(external_functions) > 0: + self._external_functions[obj] = external_functions + self._vars_referenced_by_obj = variables + for v in variables: + self._referenced_variables[id(v)][2] = obj + if not self.config.auto_updates.treat_fixed_vars_as_params: + for v in fixed_vars: + v.unfix() + self._set_objective(obj) + for v in fixed_vars: + v.fix() + else: + self._vars_referenced_by_obj = [] + self._objective = None + self._objective_expr = None + self._objective_sense = None + self._obj_named_expressions = [] + self._set_objective(obj) + + def add_block(self, block): + param_dict = {} + for p in block.component_objects(Param, descend_into=True): + if p.mutable: + for _p in p.values(): + param_dict[id(_p)] = _p + self.add_parameters(list(param_dict.values())) + self.add_constraints( + list( + block.component_data_objects(Constraint, descend_into=True, active=True) + ) + ) + self.add_sos_constraints( + list( + block.component_data_objects( + SOSConstraint, descend_into=True, active=True + ) + ) + ) + obj = get_objective(block) + if obj is not None: + self.set_objective(obj) + + @abc.abstractmethod + def _remove_constraints(self, cons: List[ConstraintData]): + pass + + def remove_constraints(self, cons: List[ConstraintData]): + self._remove_constraints(cons) + for con in cons: + if con not in self._named_expressions: + raise ValueError( + 'cannot remove constraint {name} - it was not added'.format( + name=con.name + ) + ) + for v in self._vars_referenced_by_con[con]: + self._referenced_variables[id(v)][0].pop(con) + self._check_to_remove_vars(self._vars_referenced_by_con[con]) + del self._active_constraints[con] + del self._named_expressions[con] + self._external_functions.pop(con, None) + del self._vars_referenced_by_con[con] + + @abc.abstractmethod + def _remove_sos_constraints(self, cons: List[SOSConstraintData]): + pass + + def remove_sos_constraints(self, cons: List[SOSConstraintData]): + self._remove_sos_constraints(cons) + for con in cons: + if con not in self._vars_referenced_by_con: + raise ValueError( + 'cannot remove constraint {name} - it was not added'.format( + name=con.name + ) + ) + for v in self._vars_referenced_by_con[con]: + self._referenced_variables[id(v)][1].pop(con) + self._check_to_remove_vars(self._vars_referenced_by_con[con]) + del self._active_constraints[con] + del self._named_expressions[con] + del self._vars_referenced_by_con[con] + + @abc.abstractmethod + def _remove_variables(self, variables: List[VarData]): + pass + + def remove_variables(self, variables: List[VarData]): + self._remove_variables(variables) + for v in variables: + v_id = id(v) + if v_id not in self._referenced_variables: + raise ValueError( + 'cannot remove variable {name} - it has not been added'.format( + name=v.name + ) + ) + cons_using, sos_using, obj_using = self._referenced_variables[v_id] + if cons_using or sos_using or (obj_using is not None): + raise ValueError( + 'cannot remove variable {name} - it is still being used by constraints or the objective'.format( + name=v.name + ) + ) + del self._referenced_variables[v_id] + del self._vars[v_id] + + @abc.abstractmethod + def _remove_parameters(self, params: List[ParamData]): + pass + + def remove_parameters(self, params: List[ParamData]): + self._remove_parameters(params) + for p in params: + del self._params[id(p)] + + def remove_block(self, block): + self.remove_constraints( + list( + block.component_data_objects( + ctype=Constraint, descend_into=True, active=True + ) + ) + ) + self.remove_sos_constraints( + list( + block.component_data_objects( + ctype=SOSConstraint, descend_into=True, active=True + ) + ) + ) + self.remove_parameters( + list( + dict( + (id(p), p) + for p in block.component_data_objects( + ctype=Param, descend_into=True + ) + ).values() + ) + ) + + @abc.abstractmethod + def _update_variables(self, variables: List[VarData]): + pass + + def update_variables(self, variables: List[VarData]): + for v in variables: + self._vars[id(v)] = ( + v, + v._lb, + v._ub, + v.fixed, + v.domain.get_interval(), + v.value, + ) + self._update_variables(variables) + + @abc.abstractmethod + def update_parameters(self): + pass + + def update(self, timer: HierarchicalTimer = None): + if timer is None: + timer = HierarchicalTimer() + config = self.config.auto_updates + new_vars = [] + old_vars = [] + new_params = [] + old_params = [] + new_cons = [] + old_cons = [] + old_sos = [] + new_sos = [] + current_vars_dict = {} + current_cons_dict = {} + current_sos_dict = {} + timer.start('vars') + if config.update_vars: + start_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} + timer.stop('vars') + timer.start('params') + if config.check_for_new_or_removed_params: + current_params_dict = {} + for p in self._model.component_objects(Param, descend_into=True): + if p.mutable: + for _p in p.values(): + current_params_dict[id(_p)] = _p + for p_id, p in current_params_dict.items(): + if p_id not in self._params: + new_params.append(p) + for p_id, p in self._params.items(): + if p_id not in current_params_dict: + old_params.append(p) + timer.stop('params') + timer.start('cons') + if config.check_for_new_or_removed_constraints or config.update_constraints: + current_cons_dict = { + c: None + for c in self._model.component_data_objects( + Constraint, descend_into=True, active=True + ) + } + current_sos_dict = { + c: None + for c in self._model.component_data_objects( + SOSConstraint, descend_into=True, active=True + ) + } + for c in current_cons_dict.keys(): + if c not in self._vars_referenced_by_con: + new_cons.append(c) + for c in current_sos_dict.keys(): + if c not in self._vars_referenced_by_con: + new_sos.append(c) + for c in self._vars_referenced_by_con.keys(): + if c not in current_cons_dict and c not in current_sos_dict: + if (c.ctype is Constraint) or ( + c.ctype is None and isinstance(c, ConstraintData) + ): + old_cons.append(c) + else: + assert (c.ctype is SOSConstraint) or ( + c.ctype is None and isinstance(c, SOSConstraintData) + ) + old_sos.append(c) + self.remove_constraints(old_cons) + self.remove_sos_constraints(old_sos) + timer.stop('cons') + timer.start('params') + self.remove_parameters(old_params) + + # sticking this between removal and addition + # is important so that we don't do unnecessary work + if config.update_parameters: + self.update_parameters() + + self.add_parameters(new_params) + timer.stop('params') + timer.start('vars') + self.add_variables(new_vars) + timer.stop('vars') + timer.start('cons') + self.add_constraints(new_cons) + self.add_sos_constraints(new_sos) + new_cons_set = set(new_cons) + new_sos_set = set(new_sos) + new_vars_set = set(id(v) for v in new_vars) + cons_to_remove_and_add = {} + need_to_set_objective = False + if config.update_constraints: + cons_to_update = [] + sos_to_update = [] + for c in current_cons_dict.keys(): + if c not in new_cons_set: + cons_to_update.append(c) + for c in current_sos_dict.keys(): + if c not in new_sos_set: + sos_to_update.append(c) + for c in cons_to_update: + lower, body, upper = self._active_constraints[c] + new_lower, new_body, new_upper = c.lower, c.body, c.upper + if new_body is not body: + cons_to_remove_and_add[c] = None + continue + if new_lower is not lower: + if ( + type(new_lower) is NumericConstant + and type(lower) is NumericConstant + and new_lower.value == lower.value + ): + pass + else: + cons_to_remove_and_add[c] = None + continue + if new_upper is not upper: + if ( + type(new_upper) is NumericConstant + and type(upper) is NumericConstant + and new_upper.value == upper.value + ): + pass + else: + cons_to_remove_and_add[c] = None + continue + self.remove_sos_constraints(sos_to_update) + self.add_sos_constraints(sos_to_update) + timer.stop('cons') + timer.start('vars') + if config.update_vars: + end_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} + vars_to_check = [v for v_id, v in end_vars.items() if v_id in start_vars] + if config.update_vars: + vars_to_update = [] + for v in vars_to_check: + _v, lb, ub, fixed, domain_interval, value = self._vars[id(v)] + if (fixed != v.fixed) or (fixed and (value != v.value)): + vars_to_update.append(v) + if self.config.auto_updates.treat_fixed_vars_as_params: + for c in self._referenced_variables[id(v)][0]: + cons_to_remove_and_add[c] = None + if self._referenced_variables[id(v)][2] is not None: + need_to_set_objective = True + elif lb is not v._lb: + vars_to_update.append(v) + elif ub is not v._ub: + vars_to_update.append(v) + elif domain_interval != v.domain.get_interval(): + vars_to_update.append(v) + self.update_variables(vars_to_update) + timer.stop('vars') + timer.start('cons') + cons_to_remove_and_add = list(cons_to_remove_and_add.keys()) + self.remove_constraints(cons_to_remove_and_add) + self.add_constraints(cons_to_remove_and_add) + timer.stop('cons') + timer.start('named expressions') + if config.update_named_expressions: + cons_to_update = [] + for c, expr_list in self._named_expressions.items(): + if c in new_cons_set: + continue + for named_expr, old_expr in expr_list: + if named_expr.expr is not old_expr: + cons_to_update.append(c) + break + self.remove_constraints(cons_to_update) + self.add_constraints(cons_to_update) + for named_expr, old_expr in self._obj_named_expressions: + if named_expr.expr is not old_expr: + need_to_set_objective = True + break + timer.stop('named expressions') + timer.start('objective') + if self.config.auto_updates.check_for_new_objective: + pyomo_obj = get_objective(self._model) + if pyomo_obj is not self._objective: + need_to_set_objective = True + else: + pyomo_obj = self._objective + if self.config.auto_updates.update_objective: + if pyomo_obj is not None and pyomo_obj.expr is not self._objective_expr: + need_to_set_objective = True + elif pyomo_obj is not None and pyomo_obj.sense is not self._objective_sense: + # we can definitely do something faster here than resetting the whole objective + need_to_set_objective = True + if need_to_set_objective: + self.set_objective(pyomo_obj) + timer.stop('objective') + + # this has to be done after the objective and constraints in case the + # old objective/constraints use old variables + timer.start('vars') + self.remove_variables(old_vars) + timer.stop('vars') diff --git a/pyomo/contrib/solver/plugins.py b/pyomo/contrib/solver/plugins.py new file mode 100644 index 00000000000..82c10a32fd8 --- /dev/null +++ b/pyomo/contrib/solver/plugins.py @@ -0,0 +1,30 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + +from .factory import SolverFactory +from .ipopt import Ipopt +from .gurobi import Gurobi +from .gurobi_direct import GurobiDirect + + +def load(): + SolverFactory.register( + name='ipopt', legacy_name='ipopt_v2', doc='The IPOPT NLP solver' + )(Ipopt) + SolverFactory.register( + name='gurobi', legacy_name='gurobi_v2', doc='Persistent interface to Gurobi' + )(Gurobi) + SolverFactory.register( + name='gurobi_direct', + legacy_name='gurobi_direct_v2', + doc='Direct (scipy-based) interface to Gurobi', + )(GurobiDirect) diff --git a/pyomo/contrib/solver/results.py b/pyomo/contrib/solver/results.py new file mode 100644 index 00000000000..cbc04681235 --- /dev/null +++ b/pyomo/contrib/solver/results.py @@ -0,0 +1,353 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import enum +from typing import Optional, Tuple +from datetime import datetime + +from pyomo.common.config import ( + ConfigDict, + ConfigValue, + IsInstance, + NonNegativeInt, + In, + NonNegativeFloat, + ADVANCED_OPTION, +) +from pyomo.opt.results.solution import SolutionStatus as LegacySolutionStatus +from pyomo.opt.results.solver import ( + TerminationCondition as LegacyTerminationCondition, + SolverStatus as LegacySolverStatus, +) + + +class TerminationCondition(enum.Enum): + """ + An Enum that enumerates all possible exit statuses for a solver call. + + Attributes + ---------- + convergenceCriteriaSatisfied: 0 + The solver exited because convergence criteria of the problem were + satisfied. + maxTimeLimit: 1 + The solver exited due to reaching a specified time limit. + iterationLimit: 2 + The solver exited due to reaching a specified iteration limit. + objectiveLimit: 3 + The solver exited due to reaching an objective limit. For example, + in Gurobi, the exit message "Optimal objective for model was proven to + be worse than the value specified in the Cutoff parameter" would map + to objectiveLimit. + minStepLength: 4 + The solver exited due to a minimum step length. + Minimum step length reached may mean that the problem is infeasible or + that the problem is feasible but the solver could not converge. + unbounded: 5 + The solver exited because the problem has been found to be unbounded. + provenInfeasible: 6 + The solver exited because the problem has been proven infeasible. + locallyInfeasible: 7 + The solver exited because no feasible solution was found to the + submitted problem, but it could not be proven that no such solution exists. + infeasibleOrUnbounded: 8 + Some solvers do not specify between infeasibility or unboundedness and + instead return that one or the other has occurred. For example, in + Gurobi, this may occur because there are some steps in presolve that + prevent Gurobi from distinguishing between infeasibility and unboundedness. + error: 9 + The solver exited with some error. The error message will also be + captured and returned. + interrupted: 10 + The solver was interrupted while running. + licensingProblems: 11 + The solver experienced issues with licensing. This could be that no + license was found, the license is of the wrong type for the problem (e.g., + problem is too big for type of license), or there was an issue contacting + a licensing server. + emptyModel: 12 + The model being solved did not have any variables + unknown: 42 + All other unrecognized exit statuses fall in this category. + """ + + convergenceCriteriaSatisfied = 0 + + maxTimeLimit = 1 + + iterationLimit = 2 + + objectiveLimit = 3 + + minStepLength = 4 + + unbounded = 5 + + provenInfeasible = 6 + + locallyInfeasible = 7 + + infeasibleOrUnbounded = 8 + + error = 9 + + interrupted = 10 + + licensingProblems = 11 + + emptyModel = 12 + + unknown = 42 + + +class SolutionStatus(enum.Enum): + """ + An enumeration for interpreting the result of a termination. This describes the designated + status by the solver to be loaded back into the model. + + Attributes + ---------- + noSolution: 0 + No (single) solution was found; possible that a population of solutions + was returned. + infeasible: 10 + Solution point does not satisfy some domains and/or constraints. + feasible: 20 + A solution for which all of the constraints in the model are satisfied. + optimal: 30 + A feasible solution where the objective function reaches its specified + sense (e.g., maximum, minimum) + """ + + noSolution = 0 + + infeasible = 10 + + feasible = 20 + + optimal = 30 + + +class Results(ConfigDict): + """ + Attributes + ---------- + solution_loader: SolutionLoaderBase + Object for loading the solution back into the model. + termination_condition: :class:`TerminationCondition` + The reason the solver exited. This is a member of the + TerminationCondition enum. + solution_status: :class:`SolutionStatus` + The result of the solve call. This is a member of the SolutionStatus + enum. + incumbent_objective: float + If a feasible solution was found, this is the objective value of + the best solution found. If no feasible solution was found, this is + None. + objective_bound: float + The best objective bound found. For minimization problems, this is + the lower bound. For maximization problems, this is the upper bound. + For solvers that do not provide an objective bound, this should be -inf + (minimization) or inf (maximization) + solver_name: str + The name of the solver in use. + solver_version: tuple + A tuple representing the version of the solver in use. + iteration_count: int + The total number of iterations. + timing_info: ConfigDict + A ConfigDict containing three pieces of information: + - ``start_timestamp``: UTC timestamp of when run was initiated + - ``wall_time``: elapsed wall clock time for entire process + - ``timer``: a HierarchicalTimer object containing timing data about the solve + + Specific solvers may add other relevant timing information, as appropriate. + extra_info: ConfigDict + A ConfigDict to store extra information such as solver messages. + solver_configuration: ConfigDict + A copy of the SolverConfig ConfigDict, for later inspection/reproducibility. + solver_log: str + (ADVANCED OPTION) Any solver log messages. + """ + + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.solution_loader = self.declare( + 'solution_loader', + ConfigValue( + description="Object for loading the solution back into the model." + ), + ) + self.termination_condition: TerminationCondition = self.declare( + 'termination_condition', + ConfigValue( + domain=In(TerminationCondition), + default=TerminationCondition.unknown, + description="The reason the solver exited. This is a member of the " + "TerminationCondition enum.", + ), + ) + self.solution_status: SolutionStatus = self.declare( + 'solution_status', + ConfigValue( + domain=In(SolutionStatus), + default=SolutionStatus.noSolution, + description="The result of the solve call. This is a member of " + "the SolutionStatus enum.", + ), + ) + self.incumbent_objective: Optional[float] = self.declare( + 'incumbent_objective', + ConfigValue( + domain=float, + default=None, + description="If a feasible solution was found, this is the objective " + "value of the best solution found. If no feasible solution was found, this is None.", + ), + ) + self.objective_bound: Optional[float] = self.declare( + 'objective_bound', + ConfigValue( + domain=float, + default=None, + description="The best objective bound found. For minimization problems, " + "this is the lower bound. For maximization problems, this is the " + "upper bound. For solvers that do not provide an objective bound, " + "this should be -inf (minimization) or inf (maximization)", + ), + ) + self.solver_name: Optional[str] = self.declare( + 'solver_name', + ConfigValue(domain=str, description="The name of the solver in use."), + ) + self.solver_version: Optional[Tuple[int, ...]] = self.declare( + 'solver_version', + ConfigValue( + domain=tuple, + description="A tuple representing the version of the solver in use.", + ), + ) + self.iteration_count: Optional[int] = self.declare( + 'iteration_count', + ConfigValue( + domain=NonNegativeInt, + default=None, + description="The total number of iterations.", + ), + ) + self.timing_info: ConfigDict = self.declare( + 'timing_info', ConfigDict(implicit=True) + ) + + self.timing_info.start_timestamp: datetime = self.timing_info.declare( + 'start_timestamp', + ConfigValue( + domain=IsInstance(datetime), + description="UTC timestamp of when run was initiated.", + ), + ) + self.timing_info.wall_time: Optional[float] = self.timing_info.declare( + 'wall_time', + ConfigValue( + domain=NonNegativeFloat, + description="Elapsed wall clock time for entire process.", + ), + ) + self.extra_info: ConfigDict = self.declare( + 'extra_info', ConfigDict(implicit=True) + ) + self.solver_configuration: ConfigDict = self.declare( + 'solver_configuration', + ConfigValue( + description="A copy of the config object used in the solve call.", + visibility=ADVANCED_OPTION, + ), + ) + self.solver_log: str = self.declare( + 'solver_log', + ConfigValue( + domain=str, + default=None, + visibility=ADVANCED_OPTION, + description="Any solver log messages.", + ), + ) + + def display( + self, content_filter=None, indent_spacing=2, ostream=None, visibility=0 + ): + return super().display(content_filter, indent_spacing, ostream, visibility) + + +# Everything below here preserves backwards compatibility + +legacy_termination_condition_map = { + TerminationCondition.unknown: LegacyTerminationCondition.unknown, + TerminationCondition.maxTimeLimit: LegacyTerminationCondition.maxTimeLimit, + TerminationCondition.iterationLimit: LegacyTerminationCondition.maxIterations, + TerminationCondition.objectiveLimit: LegacyTerminationCondition.minFunctionValue, + TerminationCondition.minStepLength: LegacyTerminationCondition.minStepLength, + TerminationCondition.convergenceCriteriaSatisfied: LegacyTerminationCondition.optimal, + TerminationCondition.unbounded: LegacyTerminationCondition.unbounded, + TerminationCondition.provenInfeasible: LegacyTerminationCondition.infeasible, + TerminationCondition.locallyInfeasible: LegacyTerminationCondition.infeasible, + TerminationCondition.infeasibleOrUnbounded: LegacyTerminationCondition.infeasibleOrUnbounded, + TerminationCondition.error: LegacyTerminationCondition.error, + TerminationCondition.interrupted: LegacyTerminationCondition.resourceInterrupt, + TerminationCondition.licensingProblems: LegacyTerminationCondition.licensingProblems, +} + + +legacy_solver_status_map = { + TerminationCondition.unknown: LegacySolverStatus.unknown, + TerminationCondition.maxTimeLimit: LegacySolverStatus.aborted, + TerminationCondition.iterationLimit: LegacySolverStatus.aborted, + TerminationCondition.objectiveLimit: LegacySolverStatus.aborted, + TerminationCondition.minStepLength: LegacySolverStatus.error, + TerminationCondition.convergenceCriteriaSatisfied: LegacySolverStatus.ok, + TerminationCondition.unbounded: LegacySolverStatus.error, + TerminationCondition.provenInfeasible: LegacySolverStatus.error, + TerminationCondition.locallyInfeasible: LegacySolverStatus.error, + TerminationCondition.infeasibleOrUnbounded: LegacySolverStatus.error, + TerminationCondition.error: LegacySolverStatus.error, + TerminationCondition.interrupted: LegacySolverStatus.aborted, + TerminationCondition.licensingProblems: LegacySolverStatus.error, +} + + +legacy_solution_status_map = { + SolutionStatus.noSolution: LegacySolutionStatus.unknown, + SolutionStatus.noSolution: LegacySolutionStatus.stoppedByLimit, + SolutionStatus.noSolution: LegacySolutionStatus.error, + SolutionStatus.noSolution: LegacySolutionStatus.other, + SolutionStatus.noSolution: LegacySolutionStatus.unsure, + SolutionStatus.noSolution: LegacySolutionStatus.unbounded, + SolutionStatus.optimal: LegacySolutionStatus.locallyOptimal, + SolutionStatus.optimal: LegacySolutionStatus.globallyOptimal, + SolutionStatus.optimal: LegacySolutionStatus.optimal, + SolutionStatus.infeasible: LegacySolutionStatus.infeasible, + SolutionStatus.feasible: LegacySolutionStatus.feasible, + SolutionStatus.feasible: LegacySolutionStatus.bestSoFar, +} diff --git a/pyomo/contrib/solver/sol_reader.py b/pyomo/contrib/solver/sol_reader.py new file mode 100644 index 00000000000..41d840f8d07 --- /dev/null +++ b/pyomo/contrib/solver/sol_reader.py @@ -0,0 +1,207 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + +from typing import Tuple, Dict, Any, List +import io + +from pyomo.common.errors import DeveloperError, PyomoException +from pyomo.repn.plugins.nl_writer import NLWriterInfo +from pyomo.contrib.solver.results import Results, SolutionStatus, TerminationCondition + + +class SolFileData: + def __init__(self) -> None: + self.primals: List[float] = list() + self.duals: List[float] = list() + self.var_suffixes: Dict[str, Dict[int, Any]] = dict() + self.con_suffixes: Dict[str, Dict[Any]] = dict() + self.obj_suffixes: Dict[str, Dict[int, Any]] = dict() + self.problem_suffixes: Dict[str, List[Any]] = dict() + self.other: List(str) = list() + + +def parse_sol_file( + sol_file: io.TextIOBase, nl_info: NLWriterInfo, result: Results +) -> Tuple[Results, SolFileData]: + sol_data = SolFileData() + + # + # Some solvers (minto) do not write a message. We will assume + # all non-blank lines up to the 'Options' line is the message. + # For backwards compatibility and general safety, we will parse all + # lines until "Options" appears. Anything before "Options" we will + # consider to be the solver message. + message = [] + for line in sol_file: + if not line: + break + line = line.strip() + if "Options" in line: + break + message.append(line) + message = '\n'.join(message) + # Once "Options" appears, we must now read the content under it. + model_objects = [] + if "Options" in line: + line = sol_file.readline() + number_of_options = int(line) + # We are adding in this DeveloperError to see if the alternative case + # is ever actually hit in the wild. In a previous iteration of the sol + # reader, there was logic to check for the number of options, but it + # was uncovered by tests and unclear if actually necessary. + if number_of_options > 4: + raise DeveloperError( + """ +The sol file reader has hit an unexpected error while parsing. The number of +options recorded is greater than 4. Please report this error to the Pyomo +developers. + """ + ) + for i in range(number_of_options + 4): + line = sol_file.readline() + model_objects.append(int(line)) + else: + raise PyomoException("ERROR READING `sol` FILE. No 'Options' line found.") + # Identify the total number of variables and constraints + number_of_cons = model_objects[number_of_options + 1] + number_of_vars = model_objects[number_of_options + 3] + assert number_of_cons == len(nl_info.constraints) + assert number_of_vars == len(nl_info.variables) + + duals = [float(sol_file.readline()) for i in range(number_of_cons)] + variable_vals = [float(sol_file.readline()) for i in range(number_of_vars)] + + # Parse the exit code line and capture it + exit_code = [0, 0] + line = sol_file.readline() + if line and ('objno' in line): + exit_code_line = line.split() + if len(exit_code_line) != 3: + raise PyomoException( + f"ERROR READING `sol` FILE. Expected two numbers in `objno` line; received {line}." + ) + exit_code = [int(exit_code_line[1]), int(exit_code_line[2])] + else: + raise PyomoException( + f"ERROR READING `sol` FILE. Expected `objno`; received {line}." + ) + result.extra_info.solver_message = message.strip().replace('\n', '; ') + exit_code_message = '' + if (exit_code[1] >= 0) and (exit_code[1] <= 99): + result.solution_status = SolutionStatus.optimal + result.termination_condition = TerminationCondition.convergenceCriteriaSatisfied + elif (exit_code[1] >= 100) and (exit_code[1] <= 199): + exit_code_message = "Optimal solution indicated, but ERROR LIKELY!" + result.solution_status = SolutionStatus.feasible + result.termination_condition = TerminationCondition.error + elif (exit_code[1] >= 200) and (exit_code[1] <= 299): + exit_code_message = "INFEASIBLE SOLUTION: constraints cannot be satisfied!" + result.solution_status = SolutionStatus.infeasible + result.termination_condition = TerminationCondition.locallyInfeasible + elif (exit_code[1] >= 300) and (exit_code[1] <= 399): + exit_code_message = ( + "UNBOUNDED PROBLEM: the objective can be improved without limit!" + ) + result.solution_status = SolutionStatus.noSolution + result.termination_condition = TerminationCondition.unbounded + elif (exit_code[1] >= 400) and (exit_code[1] <= 499): + exit_code_message = ( + "EXCEEDED MAXIMUM NUMBER OF ITERATIONS: the solver " + "was stopped by a limit that you set!" + ) + result.solution_status = SolutionStatus.infeasible + result.termination_condition = ( + TerminationCondition.iterationLimit + ) # this is not always correct + elif (exit_code[1] >= 500) and (exit_code[1] <= 599): + exit_code_message = ( + "FAILURE: the solver stopped by an error condition " + "in the solver routines!" + ) + result.termination_condition = TerminationCondition.error + + if result.extra_info.solver_message: + if exit_code_message: + result.extra_info.solver_message += '; ' + exit_code_message + else: + result.extra_info.solver_message = exit_code_message + + if result.solution_status != SolutionStatus.noSolution: + sol_data.primals = variable_vals + sol_data.duals = duals + ### Read suffixes ### + line = sol_file.readline() + while line: + line = line.strip() + if line == "": + continue + line = line.split() + # Some sort of garbage we tag onto the solver message, assuming we are past the suffixes + if line[0] != 'suffix': + # We assume this is the start of a + # section like kestrel_option, which + # comes after all suffixes. + remaining = "" + line = sol_file.readline() + while line: + remaining += line.strip() + "; " + line = sol_file.readline() + result.extra_info.solver_message += remaining + break + read_data_type = int(line[1]) + data_type = read_data_type & 3 # 0-var, 1-con, 2-obj, 3-prob + convert_function = int + if (read_data_type & 4) == 4: + convert_function = float + number_of_entries = int(line[2]) + # The third entry is name length, and it is length+1. This is unnecessary + # except for data validation. + # The fourth entry is table "length", e.g., memory size. + number_of_string_lines = int(line[5]) + suffix_name = sol_file.readline().strip() + # Add any arbitrary string lines to the "other" list + for line in range(number_of_string_lines): + sol_data.other.append(sol_file.readline()) + if data_type == 0: # Var + sol_data.var_suffixes[suffix_name] = dict() + for cnt in range(number_of_entries): + suf_line = sol_file.readline().split() + var_ndx = int(suf_line[0]) + sol_data.var_suffixes[suffix_name][var_ndx] = convert_function( + suf_line[1] + ) + elif data_type == 1: # Con + sol_data.con_suffixes[suffix_name] = dict() + for cnt in range(number_of_entries): + suf_line = sol_file.readline().split() + con_ndx = int(suf_line[0]) + sol_data.con_suffixes[suffix_name][con_ndx] = convert_function( + suf_line[1] + ) + elif data_type == 2: # Obj + sol_data.obj_suffixes[suffix_name] = dict() + for cnt in range(number_of_entries): + suf_line = sol_file.readline().split() + obj_ndx = int(suf_line[0]) + sol_data.obj_suffixes[suffix_name][obj_ndx] = convert_function( + suf_line[1] + ) + elif data_type == 3: # Prob + sol_data.problem_suffixes[suffix_name] = list() + for cnt in range(number_of_entries): + suf_line = sol_file.readline().split() + sol_data.problem_suffixes[suffix_name].append( + convert_function(suf_line[1]) + ) + line = sol_file.readline() + + return result, sol_data diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py new file mode 100644 index 00000000000..a3e66475982 --- /dev/null +++ b/pyomo/contrib/solver/solution.py @@ -0,0 +1,237 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import abc +from typing import Sequence, Dict, Optional, Mapping, NoReturn + +from pyomo.core.base.constraint import ConstraintData +from pyomo.core.base.var import VarData +from pyomo.core.expr import value +from pyomo.common.collections import ComponentMap +from pyomo.common.errors import DeveloperError +from pyomo.core.staleflag import StaleFlagManager +from pyomo.contrib.solver.sol_reader import SolFileData +from pyomo.repn.plugins.nl_writer import NLWriterInfo +from pyomo.core.expr.visitor import replace_expressions + + +class SolutionLoaderBase(abc.ABC): + """ + Base class for all future SolutionLoader classes. + + Intent of this class and its children is to load the solution back into the model. + """ + + def load_vars(self, vars_to_load: Optional[Sequence[VarData]] = None) -> NoReturn: + """ + Load the solution of the primal variables into the value attribute of the variables. + + Parameters + ---------- + vars_to_load: list + The minimum set of variables whose solution should be loaded. If vars_to_load is None, then the solution + to all primal variables will be loaded. Even if vars_to_load is specified, the values of other + variables may also be loaded depending on the interface. + """ + for v, val in self.get_primals(vars_to_load=vars_to_load).items(): + v.set_value(val, skip_validation=True) + StaleFlagManager.mark_all_as_stale(delayed=True) + + @abc.abstractmethod + def get_primals( + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: + """ + Returns a ComponentMap mapping variable to var value. + + Parameters + ---------- + vars_to_load: list + A list of the variables whose solution value should be retrieved. If vars_to_load is None, + then the values for all variables will be retrieved. + + Returns + ------- + primals: ComponentMap + Maps variables to solution values + """ + + def get_duals( + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: + """ + Returns a dictionary mapping constraint to dual value. + + Parameters + ---------- + cons_to_load: list + A list of the constraints whose duals should be retrieved. If cons_to_load is None, then the duals for all + constraints will be retrieved. + + Returns + ------- + duals: dict + Maps constraints to dual values + """ + raise NotImplementedError(f'{type(self)} does not support the get_duals method') + + def get_reduced_costs( + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: + """ + Returns a ComponentMap mapping variable to reduced cost. + + Parameters + ---------- + vars_to_load: list + A list of the variables whose reduced cost should be retrieved. If vars_to_load is None, then the + reduced costs for all variables will be loaded. + + Returns + ------- + reduced_costs: ComponentMap + Maps variables to reduced costs + """ + raise NotImplementedError( + f'{type(self)} does not support the get_reduced_costs method' + ) + + +class PersistentSolutionLoader(SolutionLoaderBase): + def __init__(self, solver): + self._solver = solver + self._valid = True + + def _assert_solution_still_valid(self): + if not self._valid: + raise RuntimeError('The results in the solver are no longer valid.') + + def get_primals(self, vars_to_load=None): + self._assert_solution_still_valid() + return self._solver._get_primals(vars_to_load=vars_to_load) + + def get_duals( + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: + self._assert_solution_still_valid() + return self._solver._get_duals(cons_to_load=cons_to_load) + + def get_reduced_costs( + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: + self._assert_solution_still_valid() + return self._solver._get_reduced_costs(vars_to_load=vars_to_load) + + def invalidate(self): + self._valid = False + + +class SolSolutionLoader(SolutionLoaderBase): + def __init__(self, sol_data: SolFileData, nl_info: NLWriterInfo) -> None: + self._sol_data = sol_data + self._nl_info = nl_info + + def load_vars(self, vars_to_load: Optional[Sequence[VarData]] = None) -> NoReturn: + if self._nl_info is None: + raise RuntimeError( + 'Solution loader does not currently have a valid solution. Please ' + 'check results.TerminationCondition and/or results.SolutionStatus.' + ) + if self._sol_data is None: + assert len(self._nl_info.variables) == 0 + else: + if self._nl_info.scaling: + for v, val, scale in zip( + self._nl_info.variables, + self._sol_data.primals, + self._nl_info.scaling.variables, + ): + v.set_value(val / scale, skip_validation=True) + else: + for v, val in zip(self._nl_info.variables, self._sol_data.primals): + v.set_value(val, skip_validation=True) + + for v, v_expr in self._nl_info.eliminated_vars: + v.value = value(v_expr) + + StaleFlagManager.mark_all_as_stale(delayed=True) + + def get_primals( + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: + if self._nl_info is None: + raise RuntimeError( + 'Solution loader does not currently have a valid solution. Please ' + 'check results.TerminationCondition and/or results.SolutionStatus.' + ) + val_map = dict() + if self._sol_data is None: + assert len(self._nl_info.variables) == 0 + else: + if self._nl_info.scaling is None: + scale_list = [1] * len(self._nl_info.variables) + else: + scale_list = self._nl_info.scaling.variables + for v, val, scale in zip( + self._nl_info.variables, self._sol_data.primals, scale_list + ): + val_map[id(v)] = val / scale + + for v, v_expr in self._nl_info.eliminated_vars: + val = replace_expressions(v_expr, substitution_map=val_map) + v_id = id(v) + val_map[v_id] = val + + res = ComponentMap() + if vars_to_load is None: + vars_to_load = self._nl_info.variables + [ + v for v, _ in self._nl_info.eliminated_vars + ] + for v in vars_to_load: + res[v] = val_map[id(v)] + + return res + + def get_duals( + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: + if self._nl_info is None: + raise RuntimeError( + 'Solution loader does not currently have a valid solution. Please ' + 'check results.TerminationCondition and/or results.SolutionStatus.' + ) + if len(self._nl_info.eliminated_vars) > 0: + raise NotImplementedError( + 'For now, turn presolve off (opt.config.writer_config.linear_presolve=False) ' + 'to get dual variable values.' + ) + if self._sol_data is None: + raise DeveloperError( + "Solution data is empty. This should not " + "have happened. Report this error to the Pyomo Developers." + ) + res = dict() + if self._nl_info.scaling is None: + scale_list = [1] * len(self._nl_info.constraints) + obj_scale = 1 + else: + scale_list = self._nl_info.scaling.constraints + obj_scale = self._nl_info.scaling.objectives[0] + if cons_to_load is None: + cons_to_load = set(self._nl_info.constraints) + else: + cons_to_load = set(cons_to_load) + for c, val, scale in zip( + self._nl_info.constraints, self._sol_data.duals, scale_list + ): + if c in cons_to_load: + res[c] = val * scale / obj_scale + return res diff --git a/pyomo/contrib/solver/tests/__init__.py b/pyomo/contrib/solver/tests/__init__.py new file mode 100644 index 00000000000..a4a626013c4 --- /dev/null +++ b/pyomo/contrib/solver/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/solver/tests/solvers/__init__.py b/pyomo/contrib/solver/tests/solvers/__init__.py new file mode 100644 index 00000000000..a4a626013c4 --- /dev/null +++ b/pyomo/contrib/solver/tests/solvers/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py b/pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py new file mode 100644 index 00000000000..2f281e2abf0 --- /dev/null +++ b/pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py @@ -0,0 +1,700 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest +import pyomo.environ as pe +from pyomo.contrib.solver.gurobi import Gurobi +from pyomo.contrib.solver.results import SolutionStatus +from pyomo.core.expr.taylor_series import taylor_series_expansion + + +opt = Gurobi() +if not opt.available(): + raise unittest.SkipTest +import gurobipy + + +def create_pmedian_model(): + d_dict = { + (1, 1): 1.777356642700564, + (1, 2): 1.6698255595592497, + (1, 3): 1.099139603924817, + (1, 4): 1.3529705111901453, + (1, 5): 1.467907742900842, + (1, 6): 1.5346837414708774, + (2, 1): 1.9783090609123972, + (2, 2): 1.130315350158659, + (2, 3): 1.6712434682302661, + (2, 4): 1.3642294159473756, + (2, 5): 1.4888357071619858, + (2, 6): 1.2030122107340537, + (3, 1): 1.6661983755713592, + (3, 2): 1.227663031206932, + (3, 3): 1.4580640582967632, + (3, 4): 1.0407223975549575, + (3, 5): 1.9742897953778287, + (3, 6): 1.4874760742689066, + (4, 1): 1.4616138636373597, + (4, 2): 1.7141471558082002, + (4, 3): 1.4157281494999725, + (4, 4): 1.888011688001529, + (4, 5): 1.0232934487237717, + (4, 6): 1.8335062677845464, + (5, 1): 1.468494740997508, + (5, 2): 1.8114798126442795, + (5, 3): 1.9455914886158723, + (5, 4): 1.983088378194899, + (5, 5): 1.1761820755785306, + (5, 6): 1.698655759576308, + (6, 1): 1.108855711312383, + (6, 2): 1.1602637342062019, + (6, 3): 1.0928602740245892, + (6, 4): 1.3140620798928404, + (6, 5): 1.0165386843386672, + (6, 6): 1.854049125736362, + (7, 1): 1.2910160386456968, + (7, 2): 1.7800475863350327, + (7, 3): 1.5480965161255695, + (7, 4): 1.1943306766997612, + (7, 5): 1.2920382721805297, + (7, 6): 1.3194527773994338, + (8, 1): 1.6585982235379078, + (8, 2): 1.2315210354122292, + (8, 3): 1.6194303369953538, + (8, 4): 1.8953386098022103, + (8, 5): 1.8694342085696831, + (8, 6): 1.2938069356684523, + (9, 1): 1.4582048085805495, + (9, 2): 1.484979797871119, + (9, 3): 1.2803882693587225, + (9, 4): 1.3289569463506004, + (9, 5): 1.9842424240265042, + (9, 6): 1.0119441379208745, + (10, 1): 1.1429007682932852, + (10, 2): 1.6519772165446711, + (10, 3): 1.0749931799469326, + (10, 4): 1.2920787022811089, + (10, 5): 1.7934429721917704, + (10, 6): 1.9115931008709737, + } + + model = pe.ConcreteModel() + model.N = pe.Param(initialize=10) + model.Locations = pe.RangeSet(1, model.N) + model.P = pe.Param(initialize=3) + model.M = pe.Param(initialize=6) + model.Customers = pe.RangeSet(1, model.M) + model.d = pe.Param( + model.Locations, model.Customers, initialize=d_dict, within=pe.Reals + ) + model.x = pe.Var(model.Locations, model.Customers, bounds=(0.0, 1.0)) + model.y = pe.Var(model.Locations, within=pe.Binary) + + def rule(model): + return sum( + model.d[n, m] * model.x[n, m] + for n in model.Locations + for m in model.Customers + ) + + model.obj = pe.Objective(rule=rule) + + def rule(model, m): + return (sum(model.x[n, m] for n in model.Locations), 1.0) + + model.single_x = pe.Constraint(model.Customers, rule=rule) + + def rule(model, n, m): + return (None, model.x[n, m] - model.y[n], 0.0) + + model.bound_y = pe.Constraint(model.Locations, model.Customers, rule=rule) + + def rule(model): + return (sum(model.y[n] for n in model.Locations) - model.P, 0.0) + + model.num_facilities = pe.Constraint(rule=rule) + + return model + + +class TestGurobiPersistentSimpleLPUpdates(unittest.TestCase): + def setUp(self): + self.m = pe.ConcreteModel() + m = self.m + m.x = pe.Var() + m.y = pe.Var() + m.p1 = pe.Param(mutable=True) + m.p2 = pe.Param(mutable=True) + m.p3 = pe.Param(mutable=True) + m.p4 = pe.Param(mutable=True) + m.obj = pe.Objective(expr=m.x + m.y) + m.c1 = pe.Constraint(expr=m.y - m.p1 * m.x >= m.p2) + m.c2 = pe.Constraint(expr=m.y - m.p3 * m.x >= m.p4) + + def get_solution(self): + try: + import numpy as np + except: + raise unittest.SkipTest('numpy is not available') + p1 = self.m.p1.value + p2 = self.m.p2.value + p3 = self.m.p3.value + p4 = self.m.p4.value + A = np.array([[1, -p1], [1, -p3]]) + rhs = np.array([p2, p4]) + sol = np.linalg.solve(A, rhs) + x = float(sol[1]) + y = float(sol[0]) + return x, y + + def set_params(self, p1, p2, p3, p4): + self.m.p1.value = p1 + self.m.p2.value = p2 + self.m.p3.value = p3 + self.m.p4.value = p4 + + def test_lp(self): + self.set_params(-1, -2, 0.1, -2) + x, y = self.get_solution() + opt = Gurobi() + res = opt.solve(self.m) + self.assertAlmostEqual(x + y, res.incumbent_objective) + self.assertAlmostEqual(x + y, res.objective_bound) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertTrue(res.incumbent_objective is not None) + self.assertAlmostEqual(x, self.m.x.value) + self.assertAlmostEqual(y, self.m.y.value) + + self.set_params(-1.25, -1, 0.5, -2) + opt.config.load_solutions = False + res = opt.solve(self.m) + self.assertAlmostEqual(x, self.m.x.value) + self.assertAlmostEqual(y, self.m.y.value) + x, y = self.get_solution() + self.assertNotAlmostEqual(x, self.m.x.value) + self.assertNotAlmostEqual(y, self.m.y.value) + res.solution_loader.load_vars() + self.assertAlmostEqual(x, self.m.x.value) + self.assertAlmostEqual(y, self.m.y.value) + + +class TestGurobiPersistent(unittest.TestCase): + def test_nonconvex_qcp_objective_bound_1(self): + # the goal of this test is to ensure we can get an objective bound + # for nonconvex but continuous problems even if a feasible solution + # is not found + # + # This is a fragile test because it could fail if Gurobi's algorithms improve + # (e.g., a heuristic solution is found before an objective bound of -8 is reached + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-5, 5)) + m.y = pe.Var(bounds=(-5, 5)) + m.obj = pe.Objective(expr=-m.x**2 - m.y) + m.c1 = pe.Constraint(expr=m.y <= -2 * m.x + 1) + m.c2 = pe.Constraint(expr=m.y <= m.x - 2) + opt = Gurobi() + opt.config.solver_options['nonconvex'] = 2 + opt.config.solver_options['BestBdStop'] = -8 + opt.config.load_solutions = False + opt.config.raise_exception_on_nonoptimal_result = False + res = opt.solve(m) + self.assertEqual(res.incumbent_objective, None) + self.assertAlmostEqual(res.objective_bound, -8) + + def test_nonconvex_qcp_objective_bound_2(self): + # the goal of this test is to ensure we can objective_bound properly + # for nonconvex but continuous problems when the solver terminates with a nonzero gap + # + # This is a fragile test because it could fail if Gurobi's algorithms change + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-5, 5)) + m.y = pe.Var(bounds=(-5, 5)) + m.obj = pe.Objective(expr=-m.x**2 - m.y) + m.c1 = pe.Constraint(expr=m.y <= -2 * m.x + 1) + m.c2 = pe.Constraint(expr=m.y <= m.x - 2) + opt = Gurobi() + opt.config.solver_options['nonconvex'] = 2 + opt.config.solver_options['MIPGap'] = 0.5 + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, -4) + self.assertAlmostEqual(res.objective_bound, -6) + + def test_range_constraints(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.xl = pe.Param(initialize=-1, mutable=True) + m.xu = pe.Param(initialize=1, mutable=True) + m.c = pe.Constraint(expr=pe.inequality(m.xl, m.x, m.xu)) + m.obj = pe.Objective(expr=m.x) + + opt = Gurobi() + opt.set_instance(m) + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -1) + + m.xl.value = -3 + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -3) + + del m.obj + m.obj = pe.Objective(expr=m.x, sense=pe.maximize) + + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 1) + + m.xu.value = 3 + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 3) + + def test_quadratic_constraint_with_params(self): + m = pe.ConcreteModel() + m.a = pe.Param(initialize=1, mutable=True) + m.b = pe.Param(initialize=1, mutable=True) + m.c = pe.Param(initialize=1, mutable=True) + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.y) + m.con = pe.Constraint(expr=m.y >= m.a * m.x**2 + m.b * m.x + m.c) + + opt = Gurobi() + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -m.b.value / (2 * m.a.value)) + self.assertAlmostEqual( + m.y.value, m.a.value * m.x.value**2 + m.b.value * m.x.value + m.c.value + ) + + m.a.value = 2 + m.b.value = 4 + m.c.value = -1 + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -m.b.value / (2 * m.a.value)) + self.assertAlmostEqual( + m.y.value, m.a.value * m.x.value**2 + m.b.value * m.x.value + m.c.value + ) + + def test_quadratic_objective(self): + m = pe.ConcreteModel() + m.a = pe.Param(initialize=1, mutable=True) + m.b = pe.Param(initialize=1, mutable=True) + m.c = pe.Param(initialize=1, mutable=True) + m.x = pe.Var() + m.obj = pe.Objective(expr=m.a * m.x**2 + m.b * m.x + m.c) + + opt = Gurobi() + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -m.b.value / (2 * m.a.value)) + self.assertAlmostEqual( + res.incumbent_objective, + m.a.value * m.x.value**2 + m.b.value * m.x.value + m.c.value, + ) + + m.a.value = 2 + m.b.value = 4 + m.c.value = -1 + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -m.b.value / (2 * m.a.value)) + self.assertAlmostEqual( + res.incumbent_objective, + m.a.value * m.x.value**2 + m.b.value * m.x.value + m.c.value, + ) + + def test_var_bounds(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1, 1)) + m.obj = pe.Objective(expr=m.x) + + opt = Gurobi() + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -1) + + m.x.setlb(-3) + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -3) + + del m.obj + m.obj = pe.Objective(expr=m.x, sense=pe.maximize) + + opt = Gurobi() + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 1) + + m.x.setub(3) + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 3) + + def test_fixed_var(self): + m = pe.ConcreteModel() + m.a = pe.Param(initialize=1, mutable=True) + m.b = pe.Param(initialize=1, mutable=True) + m.c = pe.Param(initialize=1, mutable=True) + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.y) + m.con = pe.Constraint(expr=m.y >= m.a * m.x**2 + m.b * m.x + m.c) + + m.x.fix(1) + opt = Gurobi() + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 1) + self.assertAlmostEqual(m.y.value, 3) + + m.x.value = 2 + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 2) + self.assertAlmostEqual(m.y.value, 7) + + m.x.unfix() + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -m.b.value / (2 * m.a.value)) + self.assertAlmostEqual( + m.y.value, m.a.value * m.x.value**2 + m.b.value * m.x.value + m.c.value + ) + + def test_linear_constraint_attr(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.c = pe.Constraint(expr=m.x + m.y == 1) + + opt = Gurobi() + opt.set_instance(m) + opt.set_linear_constraint_attr(m.c, 'Lazy', 1) + self.assertEqual(opt.get_linear_constraint_attr(m.c, 'Lazy'), 1) + + def test_quadratic_constraint_attr(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.c = pe.Constraint(expr=m.y >= m.x**2) + + opt = Gurobi() + opt.set_instance(m) + self.assertEqual(opt.get_quadratic_constraint_attr(m.c, 'QCRHS'), 0) + + def test_var_attr(self): + m = pe.ConcreteModel() + m.x = pe.Var(within=pe.Binary) + m.obj = pe.Objective(expr=m.x) + + opt = Gurobi() + opt.set_instance(m) + opt.set_var_attr(m.x, 'Start', 1) + self.assertEqual(opt.get_var_attr(m.x, 'Start'), 1) + + def test_callback(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(0, 4)) + m.y = pe.Var(within=pe.Integers, bounds=(0, None)) + m.obj = pe.Objective(expr=2 * m.x + m.y) + m.cons = pe.ConstraintList() + + def _add_cut(xval): + m.x.value = xval + return m.cons.add(m.y >= taylor_series_expansion((m.x - 2) ** 2)) + + _add_cut(0) + _add_cut(4) + + opt = Gurobi() + opt.set_instance(m) + opt.set_gurobi_param('PreCrush', 1) + opt.set_gurobi_param('LazyConstraints', 1) + + def _my_callback(cb_m, cb_opt, cb_where): + if cb_where == gurobipy.GRB.Callback.MIPSOL: + cb_opt.cbGetSolution(vars=[m.x, m.y]) + if m.y.value < (m.x.value - 2) ** 2 - 1e-6: + cb_opt.cbLazy(_add_cut(m.x.value)) + + opt.set_callback(_my_callback) + opt.solve(m) + self.assertAlmostEqual(m.x.value, 1) + self.assertAlmostEqual(m.y.value, 1) + + def test_nonconvex(self): + if gurobipy.GRB.VERSION_MAJOR < 9: + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.x**2 + m.y**2) + m.c = pe.Constraint(expr=m.y == (m.x - 1) ** 2 - 2) + opt = Gurobi() + opt.config.solver_options['nonconvex'] = 2 + opt.solve(m) + self.assertAlmostEqual(m.x.value, -0.3660254037844423, 2) + self.assertAlmostEqual(m.y.value, -0.13397459621555508, 2) + + def test_nonconvex2(self): + if gurobipy.GRB.VERSION_MAJOR < 9: + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.x**2 + m.y**2) + m.c1 = pe.Constraint(expr=0 <= -m.y + (m.x - 1) ** 2 - 2) + m.c2 = pe.Constraint(expr=0 >= -m.y + (m.x - 1) ** 2 - 2) + opt = Gurobi() + opt.config.solver_options['nonconvex'] = 2 + opt.solve(m) + self.assertAlmostEqual(m.x.value, -0.3660254037844423, 2) + self.assertAlmostEqual(m.y.value, -0.13397459621555508, 2) + + def test_solution_number(self): + m = create_pmedian_model() + opt = Gurobi() + opt.config.solver_options['PoolSolutions'] = 3 + opt.config.solver_options['PoolSearchMode'] = 2 + res = opt.solve(m) + num_solutions = opt.get_model_attr('SolCount') + self.assertEqual(num_solutions, 3) + res.solution_loader.load_vars(solution_number=0) + self.assertAlmostEqual(pe.value(m.obj.expr), 6.431184939357673) + res.solution_loader.load_vars(solution_number=1) + self.assertAlmostEqual(pe.value(m.obj.expr), 6.584793218502477) + res.solution_loader.load_vars(solution_number=2) + self.assertAlmostEqual(pe.value(m.obj.expr), 6.592304628123309) + + def test_zero_time_limit(self): + m = create_pmedian_model() + opt = Gurobi() + opt.config.time_limit = 0 + opt.config.load_solutions = False + opt.config.raise_exception_on_nonoptimal_result = False + res = opt.solve(m) + num_solutions = opt.get_model_attr('SolCount') + + # Behavior is different on different platforms, so + # we have to see if there are any solutions + # This means that there is no guarantee we are testing + # what we are trying to test. Unfortunately, I'm + # not sure of a good way to guarantee that + if num_solutions == 0: + self.assertIsNone(res.incumbent_objective) + + +class TestManualModel(unittest.TestCase): + def setUp(self): + opt = Gurobi() + opt.config.auto_updates.check_for_new_or_removed_params = False + opt.config.auto_updates.check_for_new_or_removed_vars = False + opt.config.auto_updates.check_for_new_or_removed_constraints = False + opt.config.auto_updates.update_parameters = False + opt.config.auto_updates.update_vars = False + opt.config.auto_updates.update_constraints = False + opt.config.auto_updates.update_named_expressions = False + self.opt = opt + + def test_basics(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-10, 10)) + m.y = pe.Var() + m.obj = pe.Objective(expr=m.x**2 + m.y**2) + m.c1 = pe.Constraint(expr=m.y >= 2 * m.x + 1) + + opt = self.opt + opt.set_instance(m) + + self.assertEqual(opt.get_model_attr('NumVars'), 2) + self.assertEqual(opt.get_model_attr('NumConstrs'), 1) + self.assertEqual(opt.get_model_attr('NumQConstrs'), 0) + self.assertEqual(opt.get_var_attr(m.x, 'LB'), -10) + self.assertEqual(opt.get_var_attr(m.x, 'UB'), 10) + + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -0.4) + self.assertAlmostEqual(m.y.value, 0.2) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], -0.4) + + m.c2 = pe.Constraint(expr=m.y >= -m.x + 1) + opt.add_constraints([m.c2]) + self.assertEqual(opt.get_model_attr('NumVars'), 2) + self.assertEqual(opt.get_model_attr('NumConstrs'), 2) + self.assertEqual(opt.get_model_attr('NumQConstrs'), 0) + + opt.config.load_solutions = False + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -0.4) + self.assertAlmostEqual(m.y.value, 0.2) + res.solution_loader.load_vars() + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 1) + + opt.remove_constraints([m.c2]) + m.del_component(m.c2) + self.assertEqual(opt.get_model_attr('NumVars'), 2) + self.assertEqual(opt.get_model_attr('NumConstrs'), 1) + self.assertEqual(opt.get_model_attr('NumQConstrs'), 0) + + self.assertEqual(opt.get_gurobi_param_info('FeasibilityTol')[2], 1e-6) + opt.config.solver_options['FeasibilityTol'] = 1e-7 + opt.config.load_solutions = True + res = opt.solve(m) + self.assertEqual(opt.get_gurobi_param_info('FeasibilityTol')[2], 1e-7) + self.assertAlmostEqual(m.x.value, -0.4) + self.assertAlmostEqual(m.y.value, 0.2) + + m.x.setlb(-5) + m.x.setub(5) + opt.update_variables([m.x]) + self.assertEqual(opt.get_var_attr(m.x, 'LB'), -5) + self.assertEqual(opt.get_var_attr(m.x, 'UB'), 5) + + m.x.fix(0) + opt.update_variables([m.x]) + self.assertEqual(opt.get_var_attr(m.x, 'LB'), 0) + self.assertEqual(opt.get_var_attr(m.x, 'UB'), 0) + + m.x.unfix() + opt.update_variables([m.x]) + self.assertEqual(opt.get_var_attr(m.x, 'LB'), -5) + self.assertEqual(opt.get_var_attr(m.x, 'UB'), 5) + + m.c2 = pe.Constraint(expr=m.y >= m.x**2) + opt.add_constraints([m.c2]) + self.assertEqual(opt.get_model_attr('NumVars'), 2) + self.assertEqual(opt.get_model_attr('NumConstrs'), 1) + self.assertEqual(opt.get_model_attr('NumQConstrs'), 1) + + opt.remove_constraints([m.c2]) + m.del_component(m.c2) + self.assertEqual(opt.get_model_attr('NumVars'), 2) + self.assertEqual(opt.get_model_attr('NumConstrs'), 1) + self.assertEqual(opt.get_model_attr('NumQConstrs'), 0) + + m.z = pe.Var() + opt.add_variables([m.z]) + self.assertEqual(opt.get_model_attr('NumVars'), 3) + opt.remove_variables([m.z]) + del m.z + self.assertEqual(opt.get_model_attr('NumVars'), 2) + + def test_update1(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var() + m.obj = pe.Objective(expr=m.z) + m.c1 = pe.Constraint(expr=m.z >= m.x**2 + m.y**2) + + opt = self.opt + opt.set_instance(m) + self.assertEqual(opt._solver_model.getAttr('NumQConstrs'), 1) + + opt.remove_constraints([m.c1]) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumQConstrs'), 0) + + opt.add_constraints([m.c1]) + self.assertEqual(opt._solver_model.getAttr('NumQConstrs'), 0) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumQConstrs'), 1) + + def test_update2(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var() + m.obj = pe.Objective(expr=m.z) + m.c2 = pe.Constraint(expr=m.x + m.y == 1) + + opt = self.opt + opt.config.symbolic_solver_labels = True + opt.set_instance(m) + self.assertEqual(opt._solver_model.getAttr('NumConstrs'), 1) + + opt.remove_constraints([m.c2]) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumConstrs'), 0) + + opt.add_constraints([m.c2]) + self.assertEqual(opt._solver_model.getAttr('NumConstrs'), 0) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumConstrs'), 1) + + def test_update3(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var() + m.obj = pe.Objective(expr=m.z) + m.c1 = pe.Constraint(expr=m.z >= m.x**2 + m.y**2) + + opt = self.opt + opt.set_instance(m) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumQConstrs'), 1) + m.c2 = pe.Constraint(expr=m.y >= m.x**2) + opt.add_constraints([m.c2]) + self.assertEqual(opt._solver_model.getAttr('NumQConstrs'), 1) + opt.remove_constraints([m.c2]) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumQConstrs'), 1) + + def test_update4(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var() + m.obj = pe.Objective(expr=m.z) + m.c1 = pe.Constraint(expr=m.z >= m.x + m.y) + + opt = self.opt + opt.set_instance(m) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumConstrs'), 1) + m.c2 = pe.Constraint(expr=m.y >= m.x) + opt.add_constraints([m.c2]) + self.assertEqual(opt._solver_model.getAttr('NumConstrs'), 1) + opt.remove_constraints([m.c2]) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumConstrs'), 1) + + def test_update5(self): + m = pe.ConcreteModel() + m.a = pe.Set(initialize=[1, 2, 3], ordered=True) + m.x = pe.Var(m.a, within=pe.Binary) + m.y = pe.Var(within=pe.Binary) + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.SOSConstraint(var=m.x, sos=1) + + opt = self.opt + opt.set_instance(m) + self.assertEqual(opt._solver_model.getAttr('NumSOS'), 1) + + opt.remove_sos_constraints([m.c1]) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumSOS'), 0) + + opt.add_sos_constraints([m.c1]) + self.assertEqual(opt._solver_model.getAttr('NumSOS'), 0) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumSOS'), 1) + + def test_update6(self): + m = pe.ConcreteModel() + m.a = pe.Set(initialize=[1, 2, 3], ordered=True) + m.x = pe.Var(m.a, within=pe.Binary) + m.y = pe.Var(within=pe.Binary) + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.SOSConstraint(var=m.x, sos=1) + + opt = self.opt + opt.set_instance(m) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumSOS'), 1) + m.c2 = pe.SOSConstraint(var=m.x, sos=2) + opt.add_sos_constraints([m.c2]) + self.assertEqual(opt._solver_model.getAttr('NumSOS'), 1) + opt.remove_sos_constraints([m.c2]) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumSOS'), 1) diff --git a/pyomo/contrib/solver/tests/solvers/test_ipopt.py b/pyomo/contrib/solver/tests/solvers/test_ipopt.py new file mode 100644 index 00000000000..d5d82981ed8 --- /dev/null +++ b/pyomo/contrib/solver/tests/solvers/test_ipopt.py @@ -0,0 +1,57 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + +import pyomo.environ as pyo +from pyomo.common.fileutils import ExecutableData +from pyomo.common.config import ConfigDict +from pyomo.contrib.solver.ipopt import IpoptConfig +from pyomo.contrib.solver.factory import SolverFactory +from pyomo.common import unittest + + +""" +TODO: + - Test unique configuration options + - Test unique results options + - Ensure that `*.opt` file is only created when needed + - Ensure options are correctly parsing to env or opt file + - Failures at appropriate times +""" + + +class TestIpopt(unittest.TestCase): + def create_model(self): + model = pyo.ConcreteModel() + model.x = pyo.Var(initialize=1.5) + model.y = pyo.Var(initialize=1.5) + + def rosenbrock(m): + return (1.0 - m.x) ** 2 + 100.0 * (m.y - m.x**2) ** 2 + + model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize) + return model + + def test_ipopt_config(self): + # Test default initialization + config = IpoptConfig() + self.assertTrue(config.load_solutions) + self.assertIsInstance(config.solver_options, ConfigDict) + self.assertIsInstance(config.executable, ExecutableData) + + # Test custom initialization + solver = SolverFactory('ipopt', executable='/path/to/exe') + self.assertFalse(solver.config.tee) + self.assertTrue(solver.config.executable.startswith('/path')) + + # Change value on a solve call + # model = self.create_model() + # result = solver.solve(model, tee=True) diff --git a/pyomo/contrib/solver/tests/solvers/test_solvers.py b/pyomo/contrib/solver/tests/solvers/test_solvers.py new file mode 100644 index 00000000000..f91de2287b7 --- /dev/null +++ b/pyomo/contrib/solver/tests/solvers/test_solvers.py @@ -0,0 +1,1682 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import random +import math +from typing import Type + +import pyomo.environ as pe +from pyomo import gdp +from pyomo.common.dependencies import attempt_import +import pyomo.common.unittest as unittest +from pyomo.contrib.solver.results import TerminationCondition, SolutionStatus, Results +from pyomo.contrib.solver.base import SolverBase +from pyomo.contrib.solver.ipopt import Ipopt +from pyomo.contrib.solver.gurobi import Gurobi +from pyomo.contrib.solver.gurobi_direct import GurobiDirect +from pyomo.core.expr.numeric_expr import LinearExpression + + +np, numpy_available = attempt_import('numpy') +parameterized, param_available = attempt_import('parameterized') +parameterized = parameterized.parameterized + + +if not param_available: + raise unittest.SkipTest('Parameterized is not available.') + +all_solvers = [('gurobi', Gurobi), ('gurobi_direct', GurobiDirect), ('ipopt', Ipopt)] +mip_solvers = [('gurobi', Gurobi), ('gurobi_direct', GurobiDirect)] +nlp_solvers = [('ipopt', Ipopt)] +qcp_solvers = [('gurobi', Gurobi), ('ipopt', Ipopt)] +miqcqp_solvers = [('gurobi', Gurobi)] +nl_solvers = [('ipopt', Ipopt)] +nl_solvers_set = {i[0] for i in nl_solvers} + + +def _load_tests(solver_list): + res = list() + for solver_name, solver in solver_list: + if solver_name in nl_solvers_set: + test_name = f"{solver_name}_presolve" + res.append((test_name, solver, True)) + test_name = f"{solver_name}" + res.append((test_name, solver, False)) + else: + test_name = f"{solver_name}" + res.append((test_name, solver, None)) + return res + + +@unittest.skipUnless(numpy_available, 'numpy is not available') +class TestSolvers(unittest.TestCase): + @parameterized.expand(input=all_solvers) + def test_config_overwrite(self, name: str, opt_class: Type[SolverBase]): + self.assertIsNot(SolverBase.CONFIG, opt_class.CONFIG) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_remove_variable_and_objective( + self, name: str, opt_class: Type[SolverBase], use_presolve + ): + # this test is for issue #2888 + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(2, None)) + m.obj = pe.Objective(expr=m.x) + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, 2) + + del m.x + del m.obj + m.x = pe.Var(bounds=(2, None)) + m.obj = pe.Objective(expr=m.x) + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, 2) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_stale_vars( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var() + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=m.y >= m.x) + m.c2 = pe.Constraint(expr=m.y >= -m.x) + m.x.value = 1 + m.y.value = 1 + m.z.value = 1 + self.assertFalse(m.x.stale) + self.assertFalse(m.y.stale) + self.assertFalse(m.z.stale) + + res = opt.solve(m) + self.assertFalse(m.x.stale) + self.assertFalse(m.y.stale) + self.assertTrue(m.z.stale) + + opt.config.load_solutions = False + res = opt.solve(m) + self.assertTrue(m.x.stale) + self.assertTrue(m.y.stale) + self.assertTrue(m.z.stale) + res.solution_loader.load_vars() + self.assertFalse(m.x.stale) + self.assertFalse(m.y.stale) + self.assertTrue(m.z.stale) + + res = opt.solve(m) + self.assertTrue(m.x.stale) + self.assertTrue(m.y.stale) + self.assertTrue(m.z.stale) + res.solution_loader.load_vars([m.y]) + self.assertFalse(m.y.stale) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_range_constraint( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + m = pe.ConcreteModel() + m.x = pe.Var() + m.obj = pe.Objective(expr=m.x) + m.c = pe.Constraint(expr=(-1, m.x, 1)) + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, -1) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c], 1) + m.obj.sense = pe.maximize + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, 1) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c], 1) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_reduced_costs( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1, 1)) + m.y = pe.Var(bounds=(-2, 2)) + m.obj = pe.Objective(expr=3 * m.x + 4 * m.y) + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, -1) + self.assertAlmostEqual(m.y.value, -2) + rc = res.solution_loader.get_reduced_costs() + self.assertAlmostEqual(rc[m.x], 3) + self.assertAlmostEqual(rc[m.y], 4) + m.obj.expr *= -1 + res = opt.solve(m) + rc = res.solution_loader.get_reduced_costs() + self.assertAlmostEqual(rc[m.x], -3) + self.assertAlmostEqual(rc[m.y], -4) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_reduced_costs2( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1, 1)) + m.obj = pe.Objective(expr=m.x) + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, -1) + rc = res.solution_loader.get_reduced_costs() + self.assertAlmostEqual(rc[m.x], 1) + m.obj.sense = pe.maximize + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, 1) + rc = res.solution_loader.get_reduced_costs() + self.assertAlmostEqual(rc[m.x], 1) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_param_changes( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.a1 = pe.Param(mutable=True) + m.a2 = pe.Param(mutable=True) + m.b1 = pe.Param(mutable=True) + m.b2 = pe.Param(mutable=True) + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=(0, m.y - m.a1 * m.x - m.b1, None)) + m.c2 = pe.Constraint(expr=(None, -m.y + m.a2 * m.x + m.b2, 0)) + + params_to_test = [(1, -1, 2, 1), (1, -2, 2, 1), (1, -1, 3, 1)] + for a1, a2, b1, b2 in params_to_test: + m.a1.value = a1 + m.a2.value = a2 + m.b1.value = b1 + m.b2.value = b2 + res: Results = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + if res.objective_bound is None: + bound = -math.inf + else: + bound = res.objective_bound + self.assertTrue(bound <= m.y.value) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_immutable_param( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): + """ + This test is important because component_data_objects returns immutable params as floats. + We want to make sure we process these correctly. + """ + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.a1 = pe.Param(mutable=True) + m.a2 = pe.Param(initialize=-1) + m.b1 = pe.Param(mutable=True) + m.b2 = pe.Param(mutable=True) + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=(0, m.y - m.a1 * m.x - m.b1, None)) + m.c2 = pe.Constraint(expr=(None, -m.y + m.a2 * m.x + m.b2, 0)) + + params_to_test = [(1, 2, 1), (1, 2, 1), (1, 3, 1)] + for a1, b1, b2 in params_to_test: + a2 = m.a2.value + m.a1.value = a1 + m.b1.value = b1 + m.b2.value = b2 + res: Results = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + if res.objective_bound is None: + bound = -math.inf + else: + bound = res.objective_bound + self.assertTrue(bound <= m.y.value) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_equality(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + check_duals = True + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + check_duals = False + else: + opt.config.writer_config.linear_presolve = False + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.a1 = pe.Param(mutable=True) + m.a2 = pe.Param(mutable=True) + m.b1 = pe.Param(mutable=True) + m.b2 = pe.Param(mutable=True) + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=m.y == m.a1 * m.x + m.b1) + m.c2 = pe.Constraint(expr=m.y == m.a2 * m.x + m.b2) + + params_to_test = [(1, -1, 2, 1), (1, -2, 2, 1), (1, -1, 3, 1)] + for a1, a2, b1, b2 in params_to_test: + m.a1.value = a1 + m.a2.value = a2 + m.b1.value = b1 + m.b2.value = b2 + res: Results = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + if res.objective_bound is None: + bound = -math.inf + else: + bound = res.objective_bound + self.assertTrue(bound <= m.y.value) + if check_duals: + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], -a1 / (a2 - a1)) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_linear_expression( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.a1 = pe.Param(mutable=True) + m.a2 = pe.Param(mutable=True) + m.b1 = pe.Param(mutable=True) + m.b2 = pe.Param(mutable=True) + m.obj = pe.Objective(expr=m.y) + e = LinearExpression( + constant=m.b1, linear_coefs=[-1, m.a1], linear_vars=[m.y, m.x] + ) + m.c1 = pe.Constraint(expr=e == 0) + e = LinearExpression( + constant=m.b2, linear_coefs=[-1, m.a2], linear_vars=[m.y, m.x] + ) + m.c2 = pe.Constraint(expr=e == 0) + + params_to_test = [(1, -1, 2, 1), (1, -2, 2, 1), (1, -1, 3, 1)] + for a1, a2, b1, b2 in params_to_test: + m.a1.value = a1 + m.a2.value = a2 + m.b1.value = b1 + m.b2.value = b2 + res: Results = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + if res.objective_bound is None: + bound = -math.inf + else: + bound = res.objective_bound + self.assertTrue(bound <= m.y.value) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_no_objective( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + check_duals = True + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + check_duals = False + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.a1 = pe.Param(mutable=True) + m.a2 = pe.Param(mutable=True) + m.b1 = pe.Param(mutable=True) + m.b2 = pe.Param(mutable=True) + m.c1 = pe.Constraint(expr=m.y == m.a1 * m.x + m.b1) + m.c2 = pe.Constraint(expr=m.y == m.a2 * m.x + m.b2) + + params_to_test = [(1, -1, 2, 1), (1, -2, 2, 1), (1, -1, 3, 1)] + for a1, a2, b1, b2 in params_to_test: + m.a1.value = a1 + m.a2.value = a2 + m.b1.value = b1 + m.b2.value = b2 + res: Results = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + self.assertEqual(res.incumbent_objective, None) + self.assertEqual(res.objective_bound, None) + if check_duals: + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], 0) + self.assertAlmostEqual(duals[m.c2], 0) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_add_remove_cons( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + a1 = -1 + a2 = 1 + b1 = 1 + b2 = 2 + a3 = 1 + b3 = 3 + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=m.y >= a1 * m.x + b1) + m.c2 = pe.Constraint(expr=m.y >= a2 * m.x + b2) + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + if res.objective_bound is None: + bound = -math.inf + else: + bound = res.objective_bound + self.assertTrue(bound <= m.y.value) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) + + m.c3 = pe.Constraint(expr=m.y >= a3 * m.x + b3) + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, (b3 - b1) / (a1 - a3)) + self.assertAlmostEqual(m.y.value, a1 * (b3 - b1) / (a1 - a3) + b1) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + self.assertTrue(res.objective_bound is None or res.objective_bound <= m.y.value) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a3 - a1))) + self.assertAlmostEqual(duals[m.c2], 0) + self.assertAlmostEqual(duals[m.c3], a1 / (a3 - a1)) + + del m.c3 + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + self.assertTrue(res.objective_bound is None or res.objective_bound <= m.y.value) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_results_infeasible( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=m.y >= m.x) + m.c2 = pe.Constraint(expr=m.y <= m.x - 1) + with self.assertRaises(Exception): + res = opt.solve(m) + opt.config.load_solutions = False + opt.config.raise_exception_on_nonoptimal_result = False + res = opt.solve(m) + self.assertNotEqual(res.solution_status, SolutionStatus.optimal) + if isinstance(opt, Ipopt): + acceptable_termination_conditions = { + TerminationCondition.locallyInfeasible, + TerminationCondition.unbounded, + } + else: + acceptable_termination_conditions = { + TerminationCondition.provenInfeasible, + TerminationCondition.infeasibleOrUnbounded, + } + self.assertIn(res.termination_condition, acceptable_termination_conditions) + self.assertAlmostEqual(m.x.value, None) + self.assertAlmostEqual(m.y.value, None) + self.assertTrue(res.incumbent_objective is None) + + if not isinstance(opt, Ipopt): + # ipopt can return the values of the variables/duals at the last iterate + # even if it did not converge; raise_exception_on_nonoptimal_result + # is set to False, so we are free to load infeasible solutions + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have a valid solution.*' + ): + res.solution_loader.load_vars() + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have valid duals.*' + ): + res.solution_loader.get_duals() + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have valid reduced costs.*' + ): + res.solution_loader.get_reduced_costs() + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_duals(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=m.y - m.x >= 0) + m.c2 = pe.Constraint(expr=m.y + m.x - 2 >= 0) + + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 1) + self.assertAlmostEqual(m.y.value, 1) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], 0.5) + self.assertAlmostEqual(duals[m.c2], 0.5) + + duals = res.solution_loader.get_duals(cons_to_load=[m.c1]) + self.assertAlmostEqual(duals[m.c1], 0.5) + self.assertNotIn(m.c2, duals) + + @parameterized.expand(input=_load_tests(qcp_solvers)) + def test_mutable_quadratic_coefficient( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.a = pe.Param(initialize=1, mutable=True) + m.b = pe.Param(initialize=-1, mutable=True) + m.obj = pe.Objective(expr=m.x**2 + m.y**2) + m.c = pe.Constraint(expr=m.y >= (m.a * m.x + m.b) ** 2) + + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 0.41024548525899274, 4) + self.assertAlmostEqual(m.y.value, 0.34781038127030117, 4) + m.a.value = 2 + m.b.value = -0.5 + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 0.10256137418973625, 4) + self.assertAlmostEqual(m.y.value, 0.0869525991355825, 4) + + @parameterized.expand(input=_load_tests(qcp_solvers)) + def test_mutable_quadratic_objective( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.a = pe.Param(initialize=1, mutable=True) + m.b = pe.Param(initialize=-1, mutable=True) + m.c = pe.Param(initialize=1, mutable=True) + m.d = pe.Param(initialize=1, mutable=True) + m.obj = pe.Objective(expr=m.x**2 + m.c * m.y**2 + m.d * m.x) + m.ccon = pe.Constraint(expr=m.y >= (m.a * m.x + m.b) ** 2) + + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 0.2719178742733325, 4) + self.assertAlmostEqual(m.y.value, 0.5301035741688002, 4) + m.c.value = 3.5 + m.d.value = -1 + res = opt.solve(m) + + self.assertAlmostEqual(m.x.value, 0.6962249634573562, 4) + self.assertAlmostEqual(m.y.value, 0.09227926676152151, 4) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_fixed_vars( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): + for treat_fixed_vars_as_params in [True, False]: + opt: SolverBase = opt_class() + if opt.is_persistent(): + opt.config.auto_updates.treat_fixed_vars_as_params = ( + treat_fixed_vars_as_params + ) + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + m = pe.ConcreteModel() + m.x = pe.Var() + m.x.fix(0) + m.y = pe.Var() + a1 = 1 + a2 = -1 + b1 = 1 + b2 = 2 + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=m.y >= a1 * m.x + b1) + m.c2 = pe.Constraint(expr=m.y >= a2 * m.x + b2) + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 2) + m.x.unfix() + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + m.x.fix(0) + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 2) + m.x.value = 2 + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 2) + self.assertAlmostEqual(m.y.value, 3) + m.x.value = 0 + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 2) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_fixed_vars_2( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): + opt: SolverBase = opt_class() + if opt.is_persistent(): + opt.config.auto_updates.treat_fixed_vars_as_params = True + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + m = pe.ConcreteModel() + m.x = pe.Var() + m.x.fix(0) + m.y = pe.Var() + a1 = 1 + a2 = -1 + b1 = 1 + b2 = 2 + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=m.y >= a1 * m.x + b1) + m.c2 = pe.Constraint(expr=m.y >= a2 * m.x + b2) + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 2) + m.x.unfix() + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + m.x.fix(0) + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 2) + m.x.value = 2 + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 2) + self.assertAlmostEqual(m.y.value, 3) + m.x.value = 0 + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 2) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_fixed_vars_3( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): + opt: SolverBase = opt_class() + if opt.is_persistent(): + opt.config.auto_updates.treat_fixed_vars_as_params = True + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.x + m.y) + m.c1 = pe.Constraint(expr=m.x == 2 / m.y) + m.y.fix(1) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 3) + self.assertAlmostEqual(m.x.value, 2) + + @parameterized.expand(input=_load_tests(nlp_solvers)) + def test_fixed_vars_4( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): + opt: SolverBase = opt_class() + if opt.is_persistent(): + opt.config.auto_updates.treat_fixed_vars_as_params = True + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.x**2 + m.y**2) + m.c1 = pe.Constraint(expr=m.x == 2 / m.y) + m.y.fix(1) + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 2) + m.y.unfix() + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 2**0.5) + self.assertAlmostEqual(m.y.value, 2**0.5) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_mutable_param_with_range( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.a1 = pe.Param(initialize=0, mutable=True) + m.a2 = pe.Param(initialize=0, mutable=True) + m.b1 = pe.Param(initialize=0, mutable=True) + m.b2 = pe.Param(initialize=0, mutable=True) + m.c1 = pe.Param(initialize=0, mutable=True) + m.c2 = pe.Param(initialize=0, mutable=True) + m.obj = pe.Objective(expr=m.y) + m.con1 = pe.Constraint(expr=(m.b1, m.y - m.a1 * m.x, m.c1)) + m.con2 = pe.Constraint(expr=(m.b2, m.y - m.a2 * m.x, m.c2)) + + np.random.seed(0) + params_to_test = [ + ( + np.random.uniform(0, 10), + np.random.uniform(-10, 0), + np.random.uniform(-5, 2.5), + np.random.uniform(-5, 2.5), + np.random.uniform(2.5, 10), + np.random.uniform(2.5, 10), + pe.minimize, + ), + ( + np.random.uniform(0, 10), + np.random.uniform(-10, 0), + np.random.uniform(-5, 2.5), + np.random.uniform(-5, 2.5), + np.random.uniform(2.5, 10), + np.random.uniform(2.5, 10), + pe.maximize, + ), + ( + np.random.uniform(0, 10), + np.random.uniform(-10, 0), + np.random.uniform(-5, 2.5), + np.random.uniform(-5, 2.5), + np.random.uniform(2.5, 10), + np.random.uniform(2.5, 10), + pe.minimize, + ), + ( + np.random.uniform(0, 10), + np.random.uniform(-10, 0), + np.random.uniform(-5, 2.5), + np.random.uniform(-5, 2.5), + np.random.uniform(2.5, 10), + np.random.uniform(2.5, 10), + pe.maximize, + ), + ] + for a1, a2, b1, b2, c1, c2, sense in params_to_test: + m.a1.value = float(a1) + m.a2.value = float(a2) + m.b1.value = float(b1) + m.b2.value = float(b2) + m.c1.value = float(c1) + m.c2.value = float(c2) + m.obj.sense = sense + res: Results = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + if sense is pe.minimize: + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2), 6) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1, 6) + self.assertAlmostEqual(res.incumbent_objective, m.y.value, 6) + self.assertTrue( + res.objective_bound is None + or res.objective_bound <= m.y.value + 1e-12 + ) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) + self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) + else: + self.assertAlmostEqual(m.x.value, (c2 - c1) / (a1 - a2), 6) + self.assertAlmostEqual(m.y.value, a1 * (c2 - c1) / (a1 - a2) + c1, 6) + self.assertAlmostEqual(res.incumbent_objective, m.y.value, 6) + self.assertTrue( + res.objective_bound is None + or res.objective_bound >= m.y.value - 1e-12 + ) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) + self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_add_and_remove_vars( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): + opt = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + m = pe.ConcreteModel() + m.y = pe.Var(bounds=(-1, None)) + m.obj = pe.Objective(expr=m.y) + if opt.is_persistent(): + opt.config.auto_updates.update_parameters = False + opt.config.auto_updates.update_vars = False + opt.config.auto_updates.update_constraints = False + opt.config.auto_updates.update_named_expressions = False + opt.config.auto_updates.check_for_new_or_removed_params = False + opt.config.auto_updates.check_for_new_or_removed_constraints = False + opt.config.auto_updates.check_for_new_or_removed_vars = False + opt.config.load_solutions = False + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + res.solution_loader.load_vars() + self.assertAlmostEqual(m.y.value, -1) + m.x = pe.Var() + a1 = 1 + a2 = -1 + b1 = 2 + b2 = 1 + m.c1 = pe.Constraint(expr=(0, m.y - a1 * m.x - b1, None)) + m.c2 = pe.Constraint(expr=(None, -m.y + a2 * m.x + b2, 0)) + if opt.is_persistent(): + opt.add_constraints([m.c1, m.c2]) + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + res.solution_loader.load_vars() + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + m.c1.deactivate() + m.c2.deactivate() + if opt.is_persistent(): + opt.remove_constraints([m.c1, m.c2]) + m.x.value = None + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + res.solution_loader.load_vars() + self.assertEqual(m.x.value, None) + self.assertAlmostEqual(m.y.value, -1) + + @parameterized.expand(input=_load_tests(nlp_solvers)) + def test_exp(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + opt = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.x**2 + m.y**2) + m.c1 = pe.Constraint(expr=m.y >= pe.exp(m.x)) + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -0.42630274815985264) + self.assertAlmostEqual(m.y.value, 0.6529186341994245) + + @parameterized.expand(input=_load_tests(nlp_solvers)) + def test_log(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + opt = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + m = pe.ConcreteModel() + m.x = pe.Var(initialize=1) + m.y = pe.Var() + m.obj = pe.Objective(expr=m.x**2 + m.y**2) + m.c1 = pe.Constraint(expr=m.y <= pe.log(m.x)) + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 0.6529186341994245) + self.assertAlmostEqual(m.y.value, -0.42630274815985264) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_with_numpy( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.y) + a1 = 1 + b1 = 3 + a2 = -2 + b2 = 1 + m.c1 = pe.Constraint( + expr=(np.float64(0), m.y - np.int64(1) * m.x - np.float32(3), None) + ) + m.c2 = pe.Constraint( + expr=(None, -m.y + np.int32(-2) * m.x + np.float64(1), np.float16(0)) + ) + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_bounds_with_params( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + m = pe.ConcreteModel() + m.y = pe.Var() + m.p = pe.Param(mutable=True) + m.y.setlb(m.p) + m.p.value = 1 + m.obj = pe.Objective(expr=m.y) + res = opt.solve(m) + self.assertAlmostEqual(m.y.value, 1) + m.p.value = -1 + res = opt.solve(m) + self.assertAlmostEqual(m.y.value, -1) + m.y.setlb(None) + m.y.setub(m.p) + m.obj.sense = pe.maximize + m.p.value = 5 + res = opt.solve(m) + self.assertAlmostEqual(m.y.value, 5) + m.p.value = 4 + res = opt.solve(m) + self.assertAlmostEqual(m.y.value, 4) + m.y.setub(None) + m.y.setlb(m.p) + m.obj.sense = pe.minimize + m.p.value = 3 + res = opt.solve(m) + self.assertAlmostEqual(m.y.value, 3) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_solution_loader( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(1, None)) + m.y = pe.Var() + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=(0, m.y - m.x, None)) + m.c2 = pe.Constraint(expr=(0, m.y - m.x + 1, None)) + opt.config.load_solutions = False + res = opt.solve(m) + self.assertIsNone(m.x.value) + self.assertIsNone(m.y.value) + res.solution_loader.load_vars() + self.assertAlmostEqual(m.x.value, 1) + self.assertAlmostEqual(m.y.value, 1) + m.x.value = None + m.y.value = None + res.solution_loader.load_vars([m.y]) + self.assertAlmostEqual(m.y.value, 1) + primals = res.solution_loader.get_primals() + self.assertIn(m.x, primals) + self.assertIn(m.y, primals) + self.assertAlmostEqual(primals[m.x], 1) + self.assertAlmostEqual(primals[m.y], 1) + primals = res.solution_loader.get_primals([m.y]) + self.assertNotIn(m.x, primals) + self.assertIn(m.y, primals) + self.assertAlmostEqual(primals[m.y], 1) + reduced_costs = res.solution_loader.get_reduced_costs() + self.assertIn(m.x, reduced_costs) + self.assertIn(m.y, reduced_costs) + self.assertAlmostEqual(reduced_costs[m.x], 1) + self.assertAlmostEqual(reduced_costs[m.y], 0) + reduced_costs = res.solution_loader.get_reduced_costs([m.y]) + self.assertNotIn(m.x, reduced_costs) + self.assertIn(m.y, reduced_costs) + self.assertAlmostEqual(reduced_costs[m.y], 0) + duals = res.solution_loader.get_duals() + self.assertIn(m.c1, duals) + self.assertIn(m.c2, duals) + self.assertAlmostEqual(duals[m.c1], 1) + self.assertAlmostEqual(duals[m.c2], 0) + duals = res.solution_loader.get_duals([m.c1]) + self.assertNotIn(m.c2, duals) + self.assertIn(m.c1, duals) + self.assertAlmostEqual(duals[m.c1], 1) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_time_limit( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + from sys import platform + + if platform == 'win32': + raise unittest.SkipTest + + N = 30 + m = pe.ConcreteModel() + m.jobs = pe.Set(initialize=list(range(N))) + m.tasks = pe.Set(initialize=list(range(N))) + m.x = pe.Var(m.jobs, m.tasks, bounds=(0, 1)) + + random.seed(0) + coefs = list() + lin_vars = list() + for j in m.jobs: + for t in m.tasks: + coefs.append(random.uniform(0, 10)) + lin_vars.append(m.x[j, t]) + obj_expr = LinearExpression( + linear_coefs=coefs, linear_vars=lin_vars, constant=0 + ) + m.obj = pe.Objective(expr=obj_expr, sense=pe.maximize) + + m.c1 = pe.Constraint(m.jobs) + m.c2 = pe.Constraint(m.tasks) + for j in m.jobs: + expr = LinearExpression( + linear_coefs=[1] * N, + linear_vars=[m.x[j, t] for t in m.tasks], + constant=0, + ) + m.c1[j] = expr == 1 + for t in m.tasks: + expr = LinearExpression( + linear_coefs=[1] * N, + linear_vars=[m.x[j, t] for j in m.jobs], + constant=0, + ) + m.c2[t] = expr == 1 + if isinstance(opt, Ipopt): + opt.config.time_limit = 1e-6 + else: + opt.config.time_limit = 0 + opt.config.load_solutions = False + opt.config.raise_exception_on_nonoptimal_result = False + res = opt.solve(m) + self.assertIn( + res.termination_condition, + {TerminationCondition.maxTimeLimit, TerminationCondition.iterationLimit}, + ) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_objective_changes( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.c1 = pe.Constraint(expr=m.y >= m.x + 1) + m.c2 = pe.Constraint(expr=m.y >= -m.x + 1) + m.obj = pe.Objective(expr=m.y) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 1) + del m.obj + m.obj = pe.Objective(expr=2 * m.y) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 2) + m.obj.expr = 3 * m.y + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 3) + m.obj.sense = pe.maximize + opt.config.raise_exception_on_nonoptimal_result = False + opt.config.load_solutions = False + res = opt.solve(m) + self.assertIn( + res.termination_condition, + { + TerminationCondition.unbounded, + TerminationCondition.infeasibleOrUnbounded, + }, + ) + m.obj.sense = pe.minimize + opt.config.load_solutions = True + del m.obj + m.obj = pe.Objective(expr=m.x * m.y) + m.x.fix(2) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 6, 6) + m.x.fix(3) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 12, 6) + m.x.unfix() + m.y.fix(2) + m.x.setlb(-3) + m.x.setub(5) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, -2, 6) + m.y.unfix() + m.x.setlb(None) + m.x.setub(None) + m.e = pe.Expression(expr=2) + del m.obj + m.obj = pe.Objective(expr=m.e * m.y) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 2) + m.e.expr = 3 + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 3) + if opt.is_persistent(): + opt.config.auto_updates.check_for_new_objective = False + m.e.expr = 4 + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 4) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_domain(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(1, None), domain=pe.NonNegativeReals) + m.obj = pe.Objective(expr=m.x) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 1) + m.x.setlb(-1) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 0) + m.x.setlb(1) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 1) + m.x.setlb(-1) + m.x.domain = pe.Reals + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, -1) + m.x.domain = pe.NonNegativeReals + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 0) + + @parameterized.expand(input=_load_tests(mip_solvers)) + def test_domain_with_integers( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1, None), domain=pe.NonNegativeIntegers) + m.obj = pe.Objective(expr=m.x) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 0) + m.x.setlb(0.5) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 1) + m.x.setlb(-5.5) + m.x.domain = pe.Integers + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, -5) + m.x.domain = pe.Binary + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 0) + m.x.setlb(0.5) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 1) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_fixed_binaries( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + m = pe.ConcreteModel() + m.x = pe.Var(domain=pe.Binary) + m.y = pe.Var() + m.obj = pe.Objective(expr=m.y) + m.c = pe.Constraint(expr=m.y >= m.x) + m.x.fix(0) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 0) + m.x.fix(1) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 1) + + opt: SolverBase = opt_class() + if opt.is_persistent(): + opt.config.auto_updates.treat_fixed_vars_as_params = False + m.x.fix(0) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 0) + m.x.fix(1) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 1) + + @parameterized.expand(input=_load_tests(mip_solvers)) + def test_with_gdp(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-10, 10)) + m.y = pe.Var(bounds=(-10, 10)) + m.obj = pe.Objective(expr=m.y) + m.d1 = gdp.Disjunct() + m.d1.c1 = pe.Constraint(expr=m.y >= m.x + 2) + m.d1.c2 = pe.Constraint(expr=m.y >= -m.x + 2) + m.d2 = gdp.Disjunct() + m.d2.c1 = pe.Constraint(expr=m.y >= m.x + 1) + m.d2.c2 = pe.Constraint(expr=m.y >= -m.x + 1) + m.disjunction = gdp.Disjunction(expr=[m.d2, m.d1]) + pe.TransformationFactory("gdp.bigm").apply_to(m) + + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 1) + + opt: SolverBase = opt_class() + opt.use_extensions = True + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 1) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_variables_elsewhere( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.b = pe.Block() + m.b.obj = pe.Objective(expr=m.y) + m.b.c1 = pe.Constraint(expr=m.y >= m.x + 2) + m.b.c2 = pe.Constraint(expr=m.y >= -m.x) + + res = opt.solve(m.b) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertAlmostEqual(m.x.value, -1) + self.assertAlmostEqual(m.y.value, 1) + + m.x.setlb(0) + res = opt.solve(m.b) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(res.incumbent_objective, 2) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 2) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_variables_elsewhere2( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var() + + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=m.y >= m.x) + m.c2 = pe.Constraint(expr=m.y >= -m.x) + m.c3 = pe.Constraint(expr=m.y >= m.z + 1) + m.c4 = pe.Constraint(expr=m.y >= -m.z + 1) + + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(res.incumbent_objective, 1) + sol = res.solution_loader.get_primals() + self.assertIn(m.x, sol) + self.assertIn(m.y, sol) + self.assertIn(m.z, sol) + + del m.c3 + del m.c4 + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(res.incumbent_objective, 0) + sol = res.solution_loader.get_primals() + self.assertIn(m.x, sol) + self.assertIn(m.y, sol) + self.assertNotIn(m.z, sol) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_bug_1(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(3, 7)) + m.y = pe.Var(bounds=(-10, 10)) + m.p = pe.Param(mutable=True, initialize=0) + + m.obj = pe.Objective(expr=m.y) + m.c = pe.Constraint(expr=m.y >= m.p * m.x) + + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(res.incumbent_objective, 0) + + m.p.value = 1 + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(res.incumbent_objective, 3) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_bug_2(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + """ + This test is for a bug where an objective containing a fixed variable does + not get updated properly when the variable is unfixed. + """ + for fixed_var_option in [True, False]: + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + if opt.is_persistent(): + opt.config.auto_updates.treat_fixed_vars_as_params = fixed_var_option + + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-10, 10)) + m.y = pe.Var() + m.obj = pe.Objective(expr=3 * m.y - m.x) + m.c = pe.Constraint(expr=m.y >= m.x) + + m.x.fix(1) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 2, 5) + + m.x.unfix() + m.x.setlb(-9) + m.x.setub(9) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, -18, 5) + + @parameterized.expand(input=_load_tests(nl_solvers)) + def test_presolve_with_zero_coef( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + + """ + when c2 gets presolved out, c1 becomes + x - y + y = 0 which becomes + x - 0*y == 0 which is the zero we are testing for + """ + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var() + m.obj = pe.Objective(expr=m.x**2 + m.y**2 + m.z**2) + m.c1 = pe.Constraint(expr=m.x == m.y + m.z + 1.5) + m.c2 = pe.Constraint(expr=m.z == -m.y) + + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 2.25) + self.assertAlmostEqual(m.x.value, 1.5) + self.assertAlmostEqual(m.y.value, 0) + self.assertAlmostEqual(m.z.value, 0) + + m.x.setlb(2) + res = opt.solve( + m, load_solutions=False, raise_exception_on_nonoptimal_result=False + ) + if use_presolve: + exp = TerminationCondition.provenInfeasible + else: + exp = TerminationCondition.locallyInfeasible + self.assertEqual(res.termination_condition, exp) + + m = pe.ConcreteModel() + m.w = pe.Var() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var() + m.obj = pe.Objective(expr=m.x**2 + m.y**2 + m.z**2 + m.w**2) + m.c1 = pe.Constraint(expr=m.x + m.w == m.y + m.z) + m.c2 = pe.Constraint(expr=m.z == -m.y) + m.c3 = pe.Constraint(expr=m.x == -m.w) + + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 0) + self.assertAlmostEqual(m.w.value, 0) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 0) + self.assertAlmostEqual(m.z.value, 0) + + del m.c1 + m.c1 = pe.Constraint(expr=m.x + m.w == m.y + m.z + 1.5) + res = opt.solve( + m, load_solutions=False, raise_exception_on_nonoptimal_result=False + ) + self.assertEqual(res.termination_condition, exp) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_scaling(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + check_duals = True + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + check_duals = False + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=m.y >= (m.x - 1) + 1) + m.c2 = pe.Constraint(expr=m.y >= -(m.x - 1) + 1) + m.scaling_factor = pe.Suffix(direction=pe.Suffix.EXPORT) + m.scaling_factor[m.x] = 0.5 + m.scaling_factor[m.y] = 2 + m.scaling_factor[m.c1] = 0.5 + m.scaling_factor[m.c2] = 2 + m.scaling_factor[m.obj] = 2 + + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertAlmostEqual(m.x.value, 1) + self.assertAlmostEqual(m.y.value, 1) + primals = res.solution_loader.get_primals() + self.assertAlmostEqual(primals[m.x], 1) + self.assertAlmostEqual(primals[m.y], 1) + if check_duals: + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], -0.5) + self.assertAlmostEqual(duals[m.c2], -0.5) + rc = res.solution_loader.get_reduced_costs() + self.assertAlmostEqual(rc[m.x], 0) + self.assertAlmostEqual(rc[m.y], 0) + + m.x.setlb(2) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 2) + self.assertAlmostEqual(m.x.value, 2) + self.assertAlmostEqual(m.y.value, 2) + primals = res.solution_loader.get_primals() + self.assertAlmostEqual(primals[m.x], 2) + self.assertAlmostEqual(primals[m.y], 2) + if check_duals: + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], -1) + self.assertAlmostEqual(duals[m.c2], 0) + rc = res.solution_loader.get_reduced_costs() + self.assertAlmostEqual(rc[m.x], 1) + self.assertAlmostEqual(rc[m.y], 0) + + +class TestLegacySolverInterface(unittest.TestCase): + @parameterized.expand(input=all_solvers) + def test_param_updates(self, name: str, opt_class: Type[SolverBase]): + opt = pe.SolverFactory(name + '_v2') + if not opt.available(exception_flag=False): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.a1 = pe.Param(mutable=True) + m.a2 = pe.Param(mutable=True) + m.b1 = pe.Param(mutable=True) + m.b2 = pe.Param(mutable=True) + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=(0, m.y - m.a1 * m.x - m.b1, None)) + m.c2 = pe.Constraint(expr=(None, -m.y + m.a2 * m.x + m.b2, 0)) + m.dual = pe.Suffix(direction=pe.Suffix.IMPORT) + + params_to_test = [(1, -1, 2, 1), (1, -2, 2, 1), (1, -1, 3, 1)] + for a1, a2, b1, b2 in params_to_test: + m.a1.value = a1 + m.a2.value = a2 + m.b1.value = b1 + m.b2.value = b2 + res = opt.solve(m) + pe.assert_optimal_termination(res) + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + self.assertAlmostEqual(m.dual[m.c1], (1 + a1 / (a2 - a1))) + self.assertAlmostEqual(m.dual[m.c2], a1 / (a2 - a1)) + + @parameterized.expand(input=all_solvers) + def test_load_solutions(self, name: str, opt_class: Type[SolverBase]): + opt = pe.SolverFactory(name + '_v2') + if not opt.available(exception_flag=False): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + m = pe.ConcreteModel() + m.x = pe.Var() + m.obj = pe.Objective(expr=m.x) + m.c = pe.Constraint(expr=(-1, m.x, 1)) + m.dual = pe.Suffix(direction=pe.Suffix.IMPORT) + res = opt.solve(m, load_solutions=False) + pe.assert_optimal_termination(res) + self.assertIsNone(m.x.value) + self.assertNotIn(m.c, m.dual) + m.solutions.load_from(res) + self.assertAlmostEqual(m.x.value, -1) + self.assertAlmostEqual(m.dual[m.c], 1) diff --git a/pyomo/contrib/solver/tests/unit/__init__.py b/pyomo/contrib/solver/tests/unit/__init__.py new file mode 100644 index 00000000000..a4a626013c4 --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/solver/tests/unit/sol_files/__init__.py b/pyomo/contrib/solver/tests/unit/sol_files/__init__.py new file mode 100644 index 00000000000..a4a626013c4 --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/sol_files/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/solver/tests/unit/sol_files/bad_objno.sol b/pyomo/contrib/solver/tests/unit/sol_files/bad_objno.sol new file mode 100644 index 00000000000..a7eccfca388 --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/sol_files/bad_objno.sol @@ -0,0 +1,22 @@ +CONOPT 3.17A: Optimal; objective 1 +4 iterations; evals: nf = 2, ng = 0, nc = 2, nJ = 0, nH = 0, nHv = 0 + +Options +3 +1 +1 +0 +1 +1 +1 +1 +1 +1 +Xobjno 0 0 +suffix 0 1 8 0 0 +sstatus +0 1 +suffix 1 1 8 0 0 +sstatus +0 3 + diff --git a/pyomo/contrib/solver/tests/unit/sol_files/bad_objnoline.sol b/pyomo/contrib/solver/tests/unit/sol_files/bad_objnoline.sol new file mode 100644 index 00000000000..6abcacbb3c4 --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/sol_files/bad_objnoline.sol @@ -0,0 +1,22 @@ +CONOPT 3.17A: Optimal; objective 1 +4 iterations; evals: nf = 2, ng = 0, nc = 2, nJ = 0, nH = 0, nHv = 0 + +Options +3 +1 +1 +0 +1 +1 +1 +1 +1 +1 +objno 0 0 1 +suffix 0 1 8 0 0 +sstatus +0 1 +suffix 1 1 8 0 0 +sstatus +0 3 + diff --git a/pyomo/contrib/solver/tests/unit/sol_files/bad_options.sol b/pyomo/contrib/solver/tests/unit/sol_files/bad_options.sol new file mode 100644 index 00000000000..f59a2ffd3b4 --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/sol_files/bad_options.sol @@ -0,0 +1,22 @@ +CONOPT 3.17A: Optimal; objective 1 +4 iterations; evals: nf = 2, ng = 0, nc = 2, nJ = 0, nH = 0, nHv = 0 + +OXptions +3 +1 +1 +0 +1 +1 +1 +1 +1 +1 +objno 0 0 +suffix 0 1 8 0 0 +sstatus +0 1 +suffix 1 1 8 0 0 +sstatus +0 3 + diff --git a/pyomo/contrib/solver/tests/unit/sol_files/conopt_optimal.sol b/pyomo/contrib/solver/tests/unit/sol_files/conopt_optimal.sol new file mode 100644 index 00000000000..4ff14b50bc7 --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/sol_files/conopt_optimal.sol @@ -0,0 +1,22 @@ +CONOPT 3.17A: Optimal; objective 1 +4 iterations; evals: nf = 2, ng = 0, nc = 2, nJ = 0, nH = 0, nHv = 0 + +Options +3 +1 +1 +0 +1 +1 +1 +1 +1 +1 +objno 0 0 +suffix 0 1 8 0 0 +sstatus +0 1 +suffix 1 1 8 0 0 +sstatus +0 3 + diff --git a/pyomo/contrib/solver/tests/unit/sol_files/depr_solver.sol b/pyomo/contrib/solver/tests/unit/sol_files/depr_solver.sol new file mode 100644 index 00000000000..01ceb566334 --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/sol_files/depr_solver.sol @@ -0,0 +1,67 @@ +PICO Solver: final f = 88.200000 + +Options +3 +0 +0 +0 +24 +24 +32 +32 +0 +0 +0.12599999999999997 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +46.666666666666664 +0 +0 +0 +0 +0 +0 +933.3333333333336 +10000 +10000 +10000 +10000 +0 +100 +0 +100 +0 +100 +0 +100 +46.666666666666664 +53.333333333333336 +0 +100 +0 +100 +0 +100 diff --git a/pyomo/contrib/solver/tests/unit/sol_files/iis_no_variable_values.sol b/pyomo/contrib/solver/tests/unit/sol_files/iis_no_variable_values.sol new file mode 100644 index 00000000000..641a3162a8f --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/sol_files/iis_no_variable_values.sol @@ -0,0 +1,34 @@ +CPLEX 12.8.0.0: integer infeasible. +0 MIP simplex iterations +0 branch-and-bound nodes +Returning an IIS of 2 variables and 1 constraints. +No basis. + +Options +3 +1 +1 +0 +1 +0 +2 +0 +objno 0 220 +suffix 0 2 4 181 11 +iis + +0 non not in the iis +1 low at lower bound +2 fix fixed +3 upp at upper bound +4 mem member +5 pmem possible member +6 plow possibly at lower bound +7 pupp possibly at upper bound +8 bug + +0 1 +1 1 +suffix 1 1 4 0 0 +iis +0 4 diff --git a/pyomo/contrib/solver/tests/unit/sol_files/infeasible1.sol b/pyomo/contrib/solver/tests/unit/sol_files/infeasible1.sol new file mode 100644 index 00000000000..9e7c47f2091 --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/sol_files/infeasible1.sol @@ -0,0 +1,491 @@ + +Ipopt 3.12: Converged to a locally infeasible point. Problem may be infeasible. + +Options +3 +1 +1 +0 +242 +242 +86 +86 +-3.5031247438024307e-14 +-3.5234584915901186e-14 +-3.5172095867741636e-14 +-3.530546013164763e-14 +-3.5172095867741636e-14 +-3.5305460131648396e-14 +-2.366093398247632e-13 +-2.3660933995816667e-13 +-2.366093403160036e-13 +-2.366093402111279e-13 +-2.366093403160036e-13 +-2.366093402111279e-13 +-3.230618014133495e-14 +-3.229008861611988e-14 +-3.2372291959738883e-14 +-3.233107904711923e-14 +-3.2372291959738883e-14 +-3.233107904711986e-14 +-2.366093402825742e-13 +-2.3660934046399004e-13 +-2.366093408240676e-13 +-2.3660934074259244e-13 +-2.366093408240676e-13 +-2.3660934074259244e-13 +-3.5337260190603076e-15 +-3.5384985959538063e-15 +-3.5360752870197467e-15 +-3.5401103667524204e-15 +-3.5360752870197475e-15 +-3.540110366752954e-15 +-1.1241014244910024e-13 +-7.229408362081387e-14 +-1.1241014257725814e-13 +-7.229408365067014e-14 +-1.1241014257725814e-13 +-7.229408365067014e-14 +-0.045045044618550245 +-2.2503048100082865e-13 +-0.04504504461894986 +-2.3019280438209537e-13 +-2.4246742873024166e-13 +-2.3089017630512727e-13 +-2.303517676239642e-13 +-2.3258460904987257e-13 +-2.2657149778091163e-13 +-2.3561210481068387e-13 +-2.260257681221233e-13 +-2.4196851090379605e-13 +-2.2609595226592818e-13 +-0.04504504461900244 +-2.249595193064585e-13 +-0.04504504461913233 +-2.2215413967954347e-13 +-0.045045044619133334 +1.4720100770836167e-13 +0.5405405354313707 +-1.1746366725687393e-13 +-8.181817954545458e-14 +3.3628105937413004e-10 +2.5420446367682183e-10 +-4.068865957494519e-10 +-3.3083656247909664e-10 +2.0162505532975142e-10 +1.3899803000287233e-10 +1.9264257030343367e-10 +1.5784707460270425e-10 +4.0453655296452274e-10 +1.8623815108786813e-10 +4.023012427968502e-10 +2.2427204843237042e-10 +4.285852894154949e-10 +2.7438151967949997e-10 +4.990725722952413e-10 +3.24233733037425e-10 +6.365790489375267e-10 +1.8786461752037693e-10 +9.36934851555115e-10 +1.9328729420874646e-10 +2.1302900967163764e-09 +1.9184434624295806e-10 +1.839058810801874e-10 +3.1045038304739125e-08 +2.033627397720737e-10 +1.965179362792721e-09 +3.9014568630621037e-10 +9.629991995490913e-10 +3.8529492862465446e-10 +6.543016210883198e-10 +3.1023232285992586e-10 +5.203524431666233e-10 +2.443053484937026e-10 +4.814394103716646e-10 +1.9839047821553417e-10 +2.29157081595439e-10 +1.6697733108860693e-10 +2.2885043298472609e-10 +1.4439699240241691e-10 +2.231817349184844e-10 +7.996844380007978e-07 +7.95878555840714e-07 +-6.161782990947841e-09 +-6.174783045271923e-09 +-6.180473110458713e-09 +-6.1838001759594465e-09 +-6.180473110458713e-09 +-6.183800175957144e-09 +-1.3264604647361279e-14 +-1.3437580361963064e-14 +-1.381614108205247e-14 +-1.3724139850276759e-14 +-1.381614108205247e-14 +-1.3724139850276584e-14 +-1.3264604647361279e-14 +-1.3437580361963064e-14 +-1.381614108205247e-14 +-1.3724139850276759e-14 +-1.381614108205247e-14 +-1.3724139850276584e-14 +-1.3264604647357383e-14 +-1.3264604647357383e-14 +-1.258629585661237e-14 +-1.2586303131773045e-14 +-1.2586307639008801e-14 +-1.2586311120145482e-14 +-1.2586314285443517e-14 +-1.258631748040718e-14 +-1.2586321221671653e-14 +-1.2741959563395428e-14 +-1.2741955464025058e-14 +-1.2741952925774324e-14 +-1.2741950138083889e-14 +-1.2741945491635486e-14 +-1.274193825746462e-14 +-1.3437580361959015e-14 +-1.3437580361959015e-14 +-1.3437580361959015e-14 +-1.3816141082048241e-14 +-1.3816141082048241e-14 +-1.3081851406508949e-14 +-1.308185926540242e-14 +-1.3081864134282786e-14 +-1.3081867894733614e-14 +-1.308187131400409e-14 +-1.308187476532053e-14 +-1.3081878806771144e-14 +-1.2999353684840647e-14 +-1.299934941829921e-14 +-1.2999346776539415e-14 +-1.2999343875167873e-14 +-1.2999339039238868e-14 +-1.2999331510061096e-14 +-1.3724139850272537e-14 +-1.3724139850272537e-14 +-1.3724139850272537e-14 +-1.3816141082048243e-14 +-1.3816141082048243e-14 +-1.3081851406508949e-14 +-1.3081859265402422e-14 +-1.3081864134282784e-14 +-1.3081867894733614e-14 +-1.308187131400409e-14 +-1.308187476532053e-14 +-1.3081878806771145e-14 +-1.299935368484049e-14 +-1.2999349418299049e-14 +-1.2999346776539257e-14 +-1.2999343875167712e-14 +-1.299933903923871e-14 +-1.2999331510060935e-14 +-1.3724139850272359e-14 +-1.3724139850272359e-14 +-1.3724139850272359e-14 +-0.39647376852165084 +-0.4455844823264693 +-0.3964737698727394 +-0.4455844904349083 +-0.04058112126213324 +-2.37392784926522e-13 +-0.04058112126182639 +-2.3739125313713354e-13 +-2.3738581599973924e-13 +-2.3739030469186293e-13 +-2.373886019673396e-13 +-2.3738926304868226e-13 +-2.3739032800906814e-13 +-2.373875268840388e-13 +-2.3739166112281285e-13 +-2.373848238523691e-13 +-2.3739287329689576e-13 +-0.04058112126709927 +-2.3739409684312144e-13 +-0.04058112126734901 +-2.3739552961585984e-13 +-0.040581121263560345 +-7.976233462779415e-11 +-8.149038165921345e-11 +-8.149038165921345e-11 +-8.022671984428942e-11 +-8.112229180405433e-11 +-8.112229180405698e-11 +-1.1362727144888948e-10 +-4.545363318183219e-10 +-1.5766054471383136e-10 +-999.9999999987843 +2.0239864420785628e-10 +3.6952311802810024e-10 +2.123373938372435e-10 +2.804864327332228e-10 +1.346149969721881e-10 +2.2070281853153174e-10 +1.3486437441647496e-10 +1.837701666832909e-10 +1.3214731344936636e-10 +1.59848684557641e-10 +1.2663217798563007e-10 +1.4670685236091518e-10 +1.2005152713943525e-10 +2.1846147211317584e-10 +1.1320656639453056e-10 +2.1155957764572616e-10 +1.0602947953081767e-10 +2.1331568061293854e-10 +2.2406981587244565e-10 +1.0144323269437438e-10 +2.0067712609010725e-10 +1.0647572138657723e-10 +1.3628795523686926e-10 +1.1283736217061156e-10 +1.3689006597815967e-10 +1.1944117806753888e-10 +1.4976540231691364e-10 +1.2533138246033542e-10 +1.7219937613078787e-10 +1.2782000199367948e-10 +2.0576625901474408e-10 +1.8061506448741275e-10 +2.5564782647515365e-10 +1.8080595589290967e-10 +3.3611540082361537e-10 +1.8450853640157845e-10 +-999.9999999992634 +500.00000267889834 +3700.000036997707 +3700.00003699796 +3700.000036997707 +3700.00003699796 +3700.000036977598 +3700.000036977598 +11.65620349374497 +11.697892989049905 +11.723721175743378 +11.743669409189184 +11.761807757832353 +11.780116092441125 +11.801554922843986 +11.760485435103986 +11.737564481489017 +11.723372263570411 +11.70778533743834 +11.68180544764916 +11.64135667458445 +3700.000036977598 +3700.000036977598 +3700.000036977598 +0.3151184672323908 +0.32392866804605874 +0.34244076638380455 +0.33803566597697493 +0.34244076638380455 +0.3380356659769663 +0.27110063090377123 +0.2699297687440479 +0.2929786728909554 +0.29344480424126584 +0.28838393432428394 +0.2893992806145764 +0.2710728789062779 +0.26993404119945896 +0.2934152392453943 +0.29361001971947676 +0.2884212793214469 +0.28944447549328195 +0.2710728789062779 +0.2699340411994531 +0.29341523924539437 +0.29361001971947087 +0.28842127932144684 +0.2894444754932388 +0.5508615869879336 +0.15398873818985254 +0.6718832432569866 +0.17589826345513584 +0.5247189958883286 +0.18810973351399282 +0.6259675738420305 +0.20533542867213556 +0.7121098490801165 +0.23131269225729922 +0.7821527320463884 +0.28037348913556315 +0.8428067559035302 +0.5838840489481971 +0.8970272395501521 +0.6703093152878702 +0.94267886174376 +0.7738465562949745 +0.8177198430399907 +0.9786900926762641 +0.6704296542151029 +0.9210489338249574 +0.3564282839324347 +0.8691777702202935 +0.2593618184144545 +0.8137154539828636 +0.21644752420062746 +0.7494805564573437 +0.1955192721716388 +0.6636009115148781 +0.1816326651938952 +0.7714724374833359 +0.16783059150769936 +0.6720038647474075 +0.15295832306009652 +0.5820927246947017 +0 +5.999999940000606 +3.2342062150876796 +9.747775650827162 +objno 0 200 +suffix 4 60 13 0 0 +ipopt_zU_out +22 -1.327369555645263e-09 +23 -1.3446671271054377e-09 +24 -1.382523199114386e-09 +25 -1.373323075936809e-09 +26 -1.382523199114386e-09 +27 -1.3733230759367915e-09 +28 -1.2472104315043693e-09 +29 -1.2452101972496192e-09 +30 -1.2858040647227637e-09 +31 -1.2866523403876923e-09 +32 -1.2775019286011434e-09 +33 -1.2793272952136163e-09 +34 -1.2471629472231613e-09 +35 -1.2452174844060395e-09 +36 -1.2865985041388369e-09 +37 -1.2869532717202986e-09 +38 -1.2775689743171436e-09 +39 -1.2794086668147935e-09 +40 -1.2471629472231613e-09 +41 -1.2452174844060298e-09 +42 -1.2865985041388369e-09 +43 -1.2869532717202878e-09 +44 -1.2775689743171434e-09 +45 -1.2794086668147155e-09 +46 -2.0240773556752306e-09 +47 -1.0745612255836558e-09 +48 -2.770632290509263e-09 +49 -1.103129453565228e-09 +50 -1.9127440056903688e-09 +51 -1.1197213910483093e-09 +52 -2.430513566198766e-09 +53 -1.1439932412498466e-09 +54 -3.1577699873109563e-09 +55 -1.182653712929702e-09 +56 -4.173065268467735e-09 +57 -1.2632815552706913e-09 +58 -5.783269227344645e-09 +59 -2.1847056932251413e-09 +60 -8.828459262787896e-09 +61 -2.7574054223382863e-09 +62 -1.5860201572267072e-08 +63 -4.019796745114287e-09 +64 -4.987327799213503e-09 +65 -4.128677327837785e-08 +66 -2.7584122571707027e-09 +67 -1.1514963264478648e-08 +68 -1.4125712376227499e-09 +69 -6.9490543282105264e-09 +70 -1.2274426584743552e-09 +71 -4.880119585077116e-09 +72 -1.160216995366489e-09 +73 -3.628823630675873e-09 +74 -1.13003440308759e-09 +75 -2.7024178093492304e-09 +76 -1.1108592195439713e-09 +77 -3.978035995523888e-09 +78 -1.0924348929579286e-09 +79 -2.7716511991201962e-09 +80 -1.073254036073809e-09 +81 -2.175341139896496e-09 +suffix 4 86 13 0 0 +ipopt_zL_out +0 2.457002432427315e-13 +1 2.457002432427147e-13 +2 2.457002432427315e-13 +3 2.457002432427147e-13 +4 2.457002432440668e-13 +5 2.457002432440668e-13 +6 7.799202448711829e-11 +7 7.771407288173584e-11 +8 7.754286328443318e-11 +9 7.741114609420585e-11 +10 7.72917673061454e-11 +11 7.717164255304123e-11 +12 7.703145172513595e-11 +13 7.730045781990877e-11 +14 7.7451409084917e-11 +15 7.754517112285163e-11 +16 7.76484093372809e-11 +17 7.782109643810629e-11 +18 7.809149171545744e-11 +19 2.457002432440668e-13 +20 2.457002432440668e-13 +21 2.457002432440668e-13 +22 2.88491781594494e-09 +23 2.806453922602062e-09 +24 2.6547390725285084e-09 +25 2.6893342144319893e-09 +26 2.6547390725285084e-09 +27 2.6893342144320575e-09 +28 3.3533336782625715e-09 +29 3.367879281546927e-09 +30 3.1029251008167857e-09 +31 3.0979961649984553e-09 +32 3.152363115331538e-09 +33 3.1413031705213295e-09 +34 3.353676987058653e-09 +35 3.3678259755079893e-09 +36 3.0983083240635833e-09 +37 3.096252910785026e-09 +38 3.1519549450665203e-09 +39 3.1408126764021113e-09 +40 3.353676987058653e-09 +41 3.367825975508062e-09 +42 3.0983083240635824e-09 +43 3.0962529107850877e-09 +44 3.151954945066521e-09 +45 3.140812676402579e-09 +46 1.6503072927322882e-09 +47 5.903619062223097e-09 +48 1.3530489183372102e-09 +49 5.168276510428202e-09 +50 1.7325290303934247e-09 +51 4.8327689212818915e-09 +52 1.4522971044995076e-09 +53 4.4273454737645e-09 +54 1.276616097383978e-09 +55 3.930138360770138e-09 +56 1.1622933223262232e-09 +57 3.242428123819113e-09 +58 1.0786469044524248e-09 +59 1.556971619947646e-09 +60 1.0134484872637181e-09 +61 1.356225961423535e-09 +62 9.643698375125132e-10 +63 1.174768939146355e-09 +64 1.1117388275802617e-09 +65 9.288986889801197e-10 +66 1.3559825252250914e-09 +67 9.870172368223874e-10 +68 2.55055764727633e-09 +69 1.0459205566343963e-09 +70 3.5051068618760334e-09 +71 1.1172098225860037e-09 +72 4.2000521577056155e-09 +73 1.212961283078632e-09 +74 4.649622902405193e-09 +75 1.3699361786951016e-09 +76 5.005106744564875e-09 +77 1.1783841562800436e-09 +78 5.416717299785639e-09 +79 1.3528060526165563e-09 +80 5.943389257560972e-09 +81 1.561763024323873e-09 +82 500.00000026951534 +83 1.515151527777625e-10 +84 2.8108595681091103e-10 +85 9.326135918021712e-11 diff --git a/pyomo/contrib/solver/tests/unit/sol_files/infeasible2.sol b/pyomo/contrib/solver/tests/unit/sol_files/infeasible2.sol new file mode 100644 index 00000000000..6fddb053745 --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/sol_files/infeasible2.sol @@ -0,0 +1,13 @@ + + Couenne (C:\Users\SASCHA~1\AppData\Local\Temp\tmpvcmknhw0.pyomo.nl May 18 2015): Infeasible + +Options +3 +0 +1 +0 +242 +0 +86 +0 +objno 0 220 diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py new file mode 100644 index 00000000000..b52f96ba903 --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -0,0 +1,374 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import os + +from pyomo.common import unittest +from pyomo.common.config import ConfigDict +from pyomo.contrib.solver import base + + +class TestSolverBase(unittest.TestCase): + def test_abstract_member_list(self): + expected_list = ['solve', 'available', 'version'] + member_list = list(base.SolverBase.__abstractmethods__) + self.assertEqual(sorted(expected_list), sorted(member_list)) + + def test_class_method_list(self): + expected_list = [ + 'Availability', + 'CONFIG', + 'available', + 'is_persistent', + 'solve', + 'version', + ] + method_list = [ + method for method in dir(base.SolverBase) if method.startswith('_') is False + ] + self.assertEqual(sorted(expected_list), sorted(method_list)) + + @unittest.mock.patch.multiple(base.SolverBase, __abstractmethods__=set()) + def test_init(self): + self.instance = base.SolverBase() + self.assertFalse(self.instance.is_persistent()) + self.assertEqual(self.instance.version(), None) + self.assertEqual(self.instance.name, 'solverbase') + self.assertEqual(self.instance.CONFIG, self.instance.config) + self.assertEqual(self.instance.solve(None), None) + self.assertEqual(self.instance.available(), None) + + @unittest.mock.patch.multiple(base.SolverBase, __abstractmethods__=set()) + def test_context_manager(self): + with base.SolverBase() as self.instance: + self.assertFalse(self.instance.is_persistent()) + self.assertEqual(self.instance.version(), None) + self.assertEqual(self.instance.name, 'solverbase') + self.assertEqual(self.instance.CONFIG, self.instance.config) + self.assertEqual(self.instance.solve(None), None) + self.assertEqual(self.instance.available(), None) + + @unittest.mock.patch.multiple(base.SolverBase, __abstractmethods__=set()) + def test_config_kwds(self): + self.instance = base.SolverBase(tee=True) + self.assertTrue(self.instance.config.tee) + + @unittest.mock.patch.multiple(base.SolverBase, __abstractmethods__=set()) + def test_solver_availability(self): + self.instance = base.SolverBase() + self.instance.Availability._value_ = 1 + self.assertTrue(self.instance.Availability.__bool__(self.instance.Availability)) + self.instance.Availability._value_ = -1 + self.assertFalse( + self.instance.Availability.__bool__(self.instance.Availability) + ) + + @unittest.mock.patch.multiple(base.SolverBase, __abstractmethods__=set()) + def test_custom_solver_name(self): + self.instance = base.SolverBase(name='my_unique_name') + self.assertEqual(self.instance.name, 'my_unique_name') + + +class TestPersistentSolverBase(unittest.TestCase): + def test_abstract_member_list(self): + expected_list = [ + 'remove_parameters', + 'version', + 'update_variables', + 'remove_variables', + 'add_constraints', + '_get_primals', + 'set_instance', + 'set_objective', + 'update_parameters', + 'remove_block', + 'add_block', + 'available', + 'add_parameters', + 'remove_constraints', + 'add_variables', + 'solve', + ] + member_list = list(base.PersistentSolverBase.__abstractmethods__) + self.assertEqual(sorted(expected_list), sorted(member_list)) + + def test_class_method_list(self): + expected_list = [ + 'Availability', + 'CONFIG', + '_get_duals', + '_get_primals', + '_get_reduced_costs', + '_load_vars', + 'add_block', + 'add_constraints', + 'add_parameters', + 'add_variables', + 'available', + 'is_persistent', + 'remove_block', + 'remove_constraints', + 'remove_parameters', + 'remove_variables', + 'set_instance', + 'set_objective', + 'solve', + 'update_parameters', + 'update_variables', + 'version', + ] + method_list = [ + method + for method in dir(base.PersistentSolverBase) + if (method.startswith('__') or method.startswith('_abc')) is False + ] + self.assertEqual(sorted(expected_list), sorted(method_list)) + + @unittest.mock.patch.multiple(base.PersistentSolverBase, __abstractmethods__=set()) + def test_init(self): + self.instance = base.PersistentSolverBase() + self.assertTrue(self.instance.is_persistent()) + self.assertEqual(self.instance.set_instance(None), None) + self.assertEqual(self.instance.add_variables(None), None) + self.assertEqual(self.instance.add_parameters(None), None) + self.assertEqual(self.instance.add_constraints(None), None) + self.assertEqual(self.instance.add_block(None), None) + self.assertEqual(self.instance.remove_variables(None), None) + self.assertEqual(self.instance.remove_parameters(None), None) + self.assertEqual(self.instance.remove_constraints(None), None) + self.assertEqual(self.instance.remove_block(None), None) + self.assertEqual(self.instance.set_objective(None), None) + self.assertEqual(self.instance.update_variables(None), None) + self.assertEqual(self.instance.update_parameters(), None) + + with self.assertRaises(NotImplementedError): + self.instance._get_primals() + + with self.assertRaises(NotImplementedError): + self.instance._get_duals() + + with self.assertRaises(NotImplementedError): + self.instance._get_reduced_costs() + + @unittest.mock.patch.multiple(base.PersistentSolverBase, __abstractmethods__=set()) + def test_context_manager(self): + with base.PersistentSolverBase() as self.instance: + self.assertTrue(self.instance.is_persistent()) + self.assertEqual(self.instance.set_instance(None), None) + self.assertEqual(self.instance.add_variables(None), None) + self.assertEqual(self.instance.add_parameters(None), None) + self.assertEqual(self.instance.add_constraints(None), None) + self.assertEqual(self.instance.add_block(None), None) + self.assertEqual(self.instance.remove_variables(None), None) + self.assertEqual(self.instance.remove_parameters(None), None) + self.assertEqual(self.instance.remove_constraints(None), None) + self.assertEqual(self.instance.remove_block(None), None) + self.assertEqual(self.instance.set_objective(None), None) + self.assertEqual(self.instance.update_variables(None), None) + self.assertEqual(self.instance.update_parameters(), None) + + +class TestLegacySolverWrapper(unittest.TestCase): + def test_class_method_list(self): + expected_list = [ + 'available', + 'config_block', + 'license_is_valid', + 'set_options', + 'solve', + ] + method_list = [ + method + for method in dir(base.LegacySolverWrapper) + if method.startswith('_') is False + ] + self.assertEqual(sorted(expected_list), sorted(method_list)) + + def test_context_manager(self): + with base.LegacySolverWrapper() as instance: + with self.assertRaises(AttributeError): + instance.available() + + def test_map_config(self): + # Create a fake/empty config structure that can be added to an empty + # instance of LegacySolverWrapper + self.config = ConfigDict(implicit=True) + self.config.declare( + 'solver_options', + ConfigDict(implicit=True, description="Options to pass to the solver."), + ) + instance = base.LegacySolverWrapper() + instance.config = self.config + instance._map_config( + True, False, False, 20, True, False, None, None, None, False, None, None + ) + self.assertTrue(instance.config.tee) + self.assertFalse(instance.config.load_solutions) + self.assertEqual(instance.config.time_limit, 20) + self.assertEqual(instance.config.report_timing, True) + # Keepfiles should not be created because we did not declare keepfiles on + # the original config + with self.assertRaises(AttributeError): + print(instance.config.keepfiles) + # We haven't implemented solver_io, suffixes, or logfile + with self.assertRaises(NotImplementedError): + instance._map_config( + False, + False, + False, + 20, + False, + False, + None, + None, + '/path/to/bogus/file', + False, + None, + None, + ) + with self.assertRaises(NotImplementedError): + instance._map_config( + False, + False, + False, + 20, + False, + False, + None, + '/path/to/bogus/file', + None, + False, + None, + None, + ) + with self.assertRaises(NotImplementedError): + instance._map_config( + False, + False, + False, + 20, + False, + False, + '/path/to/bogus/file', + None, + None, + False, + None, + None, + ) + # If they ask for keepfiles, we redirect them to working_dir + instance._map_config( + False, False, False, 20, False, False, None, None, None, True, None, None + ) + self.assertEqual(instance.config.working_dir, os.getcwd()) + with self.assertRaises(AttributeError): + print(instance.config.keepfiles) + + def test_solver_options_behavior(self): + # options can work in multiple ways (set from instantiation, set + # after instantiation, set during solve). + # Test case 1: Set at instantiation + solver = base.LegacySolverWrapper(options={'max_iter': 6}) + self.assertEqual(solver.options, {'max_iter': 6}) + + # Test case 2: Set later + solver = base.LegacySolverWrapper() + solver.options = {'max_iter': 4, 'foo': 'bar'} + self.assertEqual(solver.options, {'max_iter': 4, 'foo': 'bar'}) + + # Test case 3: pass some options to the mapping (aka, 'solve' command) + solver = base.LegacySolverWrapper() + config = ConfigDict(implicit=True) + config.declare( + 'solver_options', + ConfigDict(implicit=True, description="Options to pass to the solver."), + ) + solver.config = config + solver._map_config(options={'max_iter': 4}) + self.assertEqual(solver.config.solver_options, {'max_iter': 4}) + + # Test case 4: Set at instantiation and override during 'solve' call + solver = base.LegacySolverWrapper(options={'max_iter': 6}) + config = ConfigDict(implicit=True) + config.declare( + 'solver_options', + ConfigDict(implicit=True, description="Options to pass to the solver."), + ) + solver.config = config + solver._map_config(options={'max_iter': 4}) + self.assertEqual(solver.config.solver_options, {'max_iter': 4}) + self.assertEqual(solver.options, {'max_iter': 6}) + + # solver_options are also supported + # Test case 1: set at instantiation + solver = base.LegacySolverWrapper(solver_options={'max_iter': 6}) + self.assertEqual(solver.options, {'max_iter': 6}) + + # Test case 2: pass some solver_options to the mapping (aka, 'solve' command) + solver = base.LegacySolverWrapper() + config = ConfigDict(implicit=True) + config.declare( + 'solver_options', + ConfigDict(implicit=True, description="Options to pass to the solver."), + ) + solver.config = config + solver._map_config(solver_options={'max_iter': 4}) + self.assertEqual(solver.config.solver_options, {'max_iter': 4}) + + # Test case 3: Set at instantiation and override during 'solve' call + solver = base.LegacySolverWrapper(solver_options={'max_iter': 6}) + config = ConfigDict(implicit=True) + config.declare( + 'solver_options', + ConfigDict(implicit=True, description="Options to pass to the solver."), + ) + solver.config = config + solver._map_config(solver_options={'max_iter': 4}) + self.assertEqual(solver.config.solver_options, {'max_iter': 4}) + self.assertEqual(solver.options, {'max_iter': 6}) + + # users can mix... sort of + # Test case 1: Initialize with options, solve with solver_options + solver = base.LegacySolverWrapper(options={'max_iter': 6}) + config = ConfigDict(implicit=True) + config.declare( + 'solver_options', + ConfigDict(implicit=True, description="Options to pass to the solver."), + ) + solver.config = config + solver._map_config(solver_options={'max_iter': 4}) + self.assertEqual(solver.config.solver_options, {'max_iter': 4}) + + # users CANNOT initialize both values at the same time, because how + # do we know what to do with it then? + # Test case 1: Class instance + with self.assertRaises(ValueError): + solver = base.LegacySolverWrapper( + options={'max_iter': 6}, solver_options={'max_iter': 4} + ) + # Test case 2: Passing to `solve` + solver = base.LegacySolverWrapper() + config = ConfigDict(implicit=True) + config.declare( + 'solver_options', + ConfigDict(implicit=True, description="Options to pass to the solver."), + ) + solver.config = config + with self.assertRaises(ValueError): + solver._map_config(solver_options={'max_iter': 4}, options={'max_iter': 6}) + + def test_map_results(self): + # Unclear how to test this + pass + + def test_solution_handler(self): + # Unclear how to test this + pass diff --git a/pyomo/contrib/solver/tests/unit/test_config.py b/pyomo/contrib/solver/tests/unit/test_config.py new file mode 100644 index 00000000000..354cfd8a37a --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/test_config.py @@ -0,0 +1,120 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common import unittest +from pyomo.contrib.solver.config import ( + SolverConfig, + BranchAndBoundConfig, + AutoUpdateConfig, + PersistentSolverConfig, +) + + +class TestSolverConfig(unittest.TestCase): + def test_interface_default_instantiation(self): + config = SolverConfig() + self.assertIsNone(config._description) + self.assertEqual(config._visibility, 0) + self.assertFalse(config.tee) + self.assertTrue(config.load_solutions) + self.assertTrue(config.raise_exception_on_nonoptimal_result) + self.assertFalse(config.symbolic_solver_labels) + self.assertIsNone(config.timer) + self.assertIsNone(config.threads) + self.assertIsNone(config.time_limit) + + def test_interface_custom_instantiation(self): + config = SolverConfig(description="A description") + config.tee = True + self.assertTrue(config.tee) + self.assertEqual(config._description, "A description") + self.assertFalse(config.time_limit) + config.time_limit = 1.0 + self.assertEqual(config.time_limit, 1.0) + self.assertIsInstance(config.time_limit, float) + + +class TestBranchAndBoundConfig(unittest.TestCase): + def test_interface_default_instantiation(self): + config = BranchAndBoundConfig() + self.assertIsNone(config._description) + self.assertEqual(config._visibility, 0) + self.assertFalse(config.tee) + self.assertTrue(config.load_solutions) + self.assertFalse(config.symbolic_solver_labels) + self.assertIsNone(config.rel_gap) + self.assertIsNone(config.abs_gap) + + def test_interface_custom_instantiation(self): + config = BranchAndBoundConfig(description="A description") + config.tee = True + self.assertTrue(config.tee) + self.assertEqual(config._description, "A description") + self.assertFalse(config.time_limit) + config.time_limit = 1.0 + self.assertEqual(config.time_limit, 1.0) + self.assertIsInstance(config.time_limit, float) + config.rel_gap = 2.5 + self.assertEqual(config.rel_gap, 2.5) + + +class TestAutoUpdateConfig(unittest.TestCase): + def test_interface_default_instantiation(self): + config = AutoUpdateConfig() + self.assertTrue(config.check_for_new_or_removed_constraints) + self.assertTrue(config.check_for_new_or_removed_vars) + self.assertTrue(config.check_for_new_or_removed_params) + self.assertTrue(config.check_for_new_objective) + self.assertTrue(config.update_constraints) + self.assertTrue(config.update_vars) + self.assertTrue(config.update_named_expressions) + self.assertTrue(config.update_objective) + self.assertTrue(config.update_objective) + self.assertTrue(config.treat_fixed_vars_as_params) + + def test_interface_custom_instantiation(self): + config = AutoUpdateConfig(description="A description") + config.check_for_new_objective = False + self.assertEqual(config._description, "A description") + self.assertTrue(config.check_for_new_or_removed_constraints) + self.assertFalse(config.check_for_new_objective) + + +class TestPersistentSolverConfig(unittest.TestCase): + def test_interface_default_instantiation(self): + config = PersistentSolverConfig() + self.assertIsNone(config._description) + self.assertEqual(config._visibility, 0) + self.assertFalse(config.tee) + self.assertTrue(config.load_solutions) + self.assertTrue(config.raise_exception_on_nonoptimal_result) + self.assertFalse(config.symbolic_solver_labels) + self.assertIsNone(config.timer) + self.assertIsNone(config.threads) + self.assertIsNone(config.time_limit) + self.assertTrue(config.auto_updates.check_for_new_or_removed_constraints) + self.assertTrue(config.auto_updates.check_for_new_or_removed_vars) + self.assertTrue(config.auto_updates.check_for_new_or_removed_params) + self.assertTrue(config.auto_updates.check_for_new_objective) + self.assertTrue(config.auto_updates.update_constraints) + self.assertTrue(config.auto_updates.update_vars) + self.assertTrue(config.auto_updates.update_named_expressions) + self.assertTrue(config.auto_updates.update_objective) + self.assertTrue(config.auto_updates.update_objective) + self.assertTrue(config.auto_updates.treat_fixed_vars_as_params) + + def test_interface_custom_instantiation(self): + config = PersistentSolverConfig(description="A description") + config.tee = True + config.auto_updates.check_for_new_objective = False + self.assertTrue(config.tee) + self.assertEqual(config._description, "A description") + self.assertFalse(config.auto_updates.check_for_new_objective) diff --git a/pyomo/contrib/solver/tests/unit/test_ipopt.py b/pyomo/contrib/solver/tests/unit/test_ipopt.py new file mode 100644 index 00000000000..cc459245506 --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/test_ipopt.py @@ -0,0 +1,225 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import os + +from pyomo.common import unittest, Executable +from pyomo.common.errors import DeveloperError +from pyomo.common.tempfiles import TempfileManager +from pyomo.repn.plugins.nl_writer import NLWriter +from pyomo.contrib.solver import ipopt + + +ipopt_available = ipopt.Ipopt().available() + + +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestIpoptSolverConfig(unittest.TestCase): + def test_default_instantiation(self): + config = ipopt.IpoptConfig() + # Should be inherited + self.assertIsNone(config._description) + self.assertEqual(config._visibility, 0) + self.assertFalse(config.tee) + self.assertTrue(config.load_solutions) + self.assertTrue(config.raise_exception_on_nonoptimal_result) + self.assertFalse(config.symbolic_solver_labels) + self.assertIsNone(config.timer) + self.assertIsNone(config.threads) + self.assertIsNone(config.time_limit) + # Unique to this object + self.assertIsInstance(config.executable, type(Executable('path'))) + self.assertIsInstance(config.writer_config, type(NLWriter.CONFIG())) + + def test_custom_instantiation(self): + config = ipopt.IpoptConfig(description="A description") + config.tee = True + self.assertTrue(config.tee) + self.assertEqual(config._description, "A description") + self.assertIsNone(config.time_limit) + # Default should be `ipopt` + self.assertIsNotNone(str(config.executable)) + self.assertIn('ipopt', str(config.executable)) + # Set to a totally bogus path + config.executable = Executable('/bogus/path') + self.assertIsNone(config.executable.executable) + self.assertFalse(config.executable.available()) + + +class TestIpoptSolutionLoader(unittest.TestCase): + def test_get_reduced_costs_error(self): + loader = ipopt.IpoptSolutionLoader(None, None) + with self.assertRaises(RuntimeError): + loader.get_reduced_costs() + + # Set _nl_info to something completely bogus but is not None + class NLInfo: + pass + + loader._nl_info = NLInfo() + loader._nl_info.eliminated_vars = [1, 2, 3] + with self.assertRaises(NotImplementedError): + loader.get_reduced_costs() + # Reset _nl_info so we can ensure we get an error + # when _sol_data is None + loader._nl_info.eliminated_vars = [] + with self.assertRaises(DeveloperError): + loader.get_reduced_costs() + + +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestIpoptInterface(unittest.TestCase): + def test_class_member_list(self): + opt = ipopt.Ipopt() + expected_list = [ + 'Availability', + 'CONFIG', + 'config', + 'available', + 'is_persistent', + 'solve', + 'version', + 'name', + ] + method_list = [method for method in dir(opt) if method.startswith('_') is False] + self.assertEqual(sorted(expected_list), sorted(method_list)) + + def test_default_instantiation(self): + opt = ipopt.Ipopt() + self.assertFalse(opt.is_persistent()) + self.assertIsNotNone(opt.version()) + self.assertEqual(opt.name, 'ipopt') + self.assertEqual(opt.CONFIG, opt.config) + self.assertTrue(opt.available()) + + def test_context_manager(self): + with ipopt.Ipopt() as opt: + self.assertFalse(opt.is_persistent()) + self.assertIsNotNone(opt.version()) + self.assertEqual(opt.name, 'ipopt') + self.assertEqual(opt.CONFIG, opt.config) + self.assertTrue(opt.available()) + + def test_available_cache(self): + opt = ipopt.Ipopt() + opt.available() + self.assertTrue(opt._available_cache[1]) + self.assertIsNotNone(opt._available_cache[0]) + # Now we will try with a custom config that has a fake path + config = ipopt.IpoptConfig() + config.executable = Executable('/a/bogus/path') + opt.available(config=config) + self.assertFalse(opt._available_cache[1]) + self.assertIsNone(opt._available_cache[0]) + + def test_version_cache(self): + opt = ipopt.Ipopt() + opt.version() + self.assertIsNotNone(opt._version_cache[0]) + self.assertIsNotNone(opt._version_cache[1]) + # Now we will try with a custom config that has a fake path + config = ipopt.IpoptConfig() + config.executable = Executable('/a/bogus/path') + opt.version(config=config) + self.assertIsNone(opt._version_cache[0]) + self.assertIsNone(opt._version_cache[1]) + + def test_write_options_file(self): + # If we have no options, we should get false back + opt = ipopt.Ipopt() + result = opt._write_options_file('fakename', None) + self.assertFalse(result) + # Pass it some options that ARE on the command line + opt = ipopt.Ipopt(solver_options={'max_iter': 4}) + result = opt._write_options_file('myfile', opt.config.solver_options) + self.assertFalse(result) + self.assertFalse(os.path.isfile('myfile.opt')) + # Now we are going to actually pass it some options that are NOT on + # the command line + opt = ipopt.Ipopt(solver_options={'custom_option': 4}) + with TempfileManager.new_context() as temp: + dname = temp.mkdtemp() + if not os.path.exists(dname): + os.mkdir(dname) + filename = os.path.join(dname, 'myfile') + result = opt._write_options_file(filename, opt.config.solver_options) + self.assertTrue(result) + self.assertTrue(os.path.isfile(filename + '.opt')) + # Make sure all options are writing to the file + opt = ipopt.Ipopt(solver_options={'custom_option_1': 4, 'custom_option_2': 3}) + with TempfileManager.new_context() as temp: + dname = temp.mkdtemp() + if not os.path.exists(dname): + os.mkdir(dname) + filename = os.path.join(dname, 'myfile') + result = opt._write_options_file(filename, opt.config.solver_options) + self.assertTrue(result) + self.assertTrue(os.path.isfile(filename + '.opt')) + with open(filename + '.opt', 'r') as f: + data = f.readlines() + self.assertEqual(len(data), len(list(opt.config.solver_options.keys()))) + + def test_create_command_line(self): + opt = ipopt.Ipopt() + # No custom options, no file created. Plain and simple. + result = opt._create_command_line('myfile', opt.config, False) + self.assertEqual(result, [str(opt.config.executable), 'myfile.nl', '-AMPL']) + # Custom command line options + opt = ipopt.Ipopt(solver_options={'max_iter': 4}) + result = opt._create_command_line('myfile', opt.config, False) + self.assertEqual( + result, [str(opt.config.executable), 'myfile.nl', '-AMPL', 'max_iter=4'] + ) + # Let's see if we correctly parse config.time_limit + opt = ipopt.Ipopt(solver_options={'max_iter': 4}, time_limit=10) + result = opt._create_command_line('myfile', opt.config, False) + self.assertEqual( + result, + [ + str(opt.config.executable), + 'myfile.nl', + '-AMPL', + 'max_iter=4', + 'max_cpu_time=10.0', + ], + ) + # Now let's do multiple command line options + opt = ipopt.Ipopt(solver_options={'max_iter': 4, 'max_cpu_time': 10}) + result = opt._create_command_line('myfile', opt.config, False) + self.assertEqual( + result, + [ + str(opt.config.executable), + 'myfile.nl', + '-AMPL', + 'max_cpu_time=10', + 'max_iter=4', + ], + ) + # Let's now include if we "have" an options file + result = opt._create_command_line('myfile', opt.config, True) + self.assertEqual( + result, + [ + str(opt.config.executable), + 'myfile.nl', + '-AMPL', + 'option_file_name=myfile.opt', + 'max_cpu_time=10', + 'max_iter=4', + ], + ) + # Finally, let's make sure it errors if someone tries to pass option_file_name + opt = ipopt.Ipopt( + solver_options={'max_iter': 4, 'option_file_name': 'myfile.opt'} + ) + with self.assertRaises(ValueError): + result = opt._create_command_line('myfile', opt.config, False) diff --git a/pyomo/contrib/solver/tests/unit/test_results.py b/pyomo/contrib/solver/tests/unit/test_results.py new file mode 100644 index 00000000000..a15c9b87253 --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/test_results.py @@ -0,0 +1,261 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from io import StringIO +from typing import Sequence, Dict, Optional, Mapping, MutableMapping + + +from pyomo.common import unittest +from pyomo.common.config import ConfigDict +from pyomo.core.base.constraint import ConstraintData +from pyomo.core.base.var import VarData +from pyomo.common.collections import ComponentMap +from pyomo.contrib.solver import results +from pyomo.contrib.solver import solution +import pyomo.environ as pyo +from pyomo.core.base.var import Var + + +class SolutionLoaderExample(solution.SolutionLoaderBase): + """ + This is an example instantiation of a SolutionLoader that is used for + testing generated results. + """ + + def __init__( + self, + primals: Optional[MutableMapping], + duals: Optional[MutableMapping], + reduced_costs: Optional[MutableMapping], + ): + """ + Parameters + ---------- + primals: dict + maps id(Var) to (var, value) + duals: dict + maps Constraint to dual value + reduced_costs: dict + maps id(Var) to (var, reduced_cost) + """ + self._primals = primals + self._duals = duals + self._reduced_costs = reduced_costs + + def get_primals( + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: + if self._primals is None: + raise RuntimeError( + 'Solution loader does not currently have a valid solution. Please ' + 'check the termination condition.' + ) + if vars_to_load is None: + return ComponentMap(self._primals.values()) + else: + primals = ComponentMap() + for v in vars_to_load: + primals[v] = self._primals[id(v)][1] + return primals + + def get_duals( + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: + if self._duals is None: + raise RuntimeError( + 'Solution loader does not currently have valid duals. Please ' + 'check the termination condition and ensure the solver returns duals ' + 'for the given problem type.' + ) + if cons_to_load is None: + duals = dict(self._duals) + else: + duals = {} + for c in cons_to_load: + duals[c] = self._duals[c] + return duals + + def get_reduced_costs( + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: + if self._reduced_costs is None: + raise RuntimeError( + 'Solution loader does not currently have valid reduced costs. Please ' + 'check the termination condition and ensure the solver returns reduced ' + 'costs for the given problem type.' + ) + if vars_to_load is None: + rc = ComponentMap(self._reduced_costs.values()) + else: + rc = ComponentMap() + for v in vars_to_load: + rc[v] = self._reduced_costs[id(v)][1] + return rc + + +class TestTerminationCondition(unittest.TestCase): + def test_member_list(self): + member_list = results.TerminationCondition._member_names_ + expected_list = [ + 'unknown', + 'convergenceCriteriaSatisfied', + 'maxTimeLimit', + 'iterationLimit', + 'objectiveLimit', + 'minStepLength', + 'unbounded', + 'provenInfeasible', + 'locallyInfeasible', + 'infeasibleOrUnbounded', + 'error', + 'interrupted', + 'licensingProblems', + ] + self.assertEqual(member_list.sort(), expected_list.sort()) + + def test_codes(self): + self.assertEqual(results.TerminationCondition.unknown.value, 42) + self.assertEqual( + results.TerminationCondition.convergenceCriteriaSatisfied.value, 0 + ) + self.assertEqual(results.TerminationCondition.maxTimeLimit.value, 1) + self.assertEqual(results.TerminationCondition.iterationLimit.value, 2) + self.assertEqual(results.TerminationCondition.objectiveLimit.value, 3) + self.assertEqual(results.TerminationCondition.minStepLength.value, 4) + self.assertEqual(results.TerminationCondition.unbounded.value, 5) + self.assertEqual(results.TerminationCondition.provenInfeasible.value, 6) + self.assertEqual(results.TerminationCondition.locallyInfeasible.value, 7) + self.assertEqual(results.TerminationCondition.infeasibleOrUnbounded.value, 8) + self.assertEqual(results.TerminationCondition.error.value, 9) + self.assertEqual(results.TerminationCondition.interrupted.value, 10) + self.assertEqual(results.TerminationCondition.licensingProblems.value, 11) + + +class TestSolutionStatus(unittest.TestCase): + def test_member_list(self): + member_list = results.SolutionStatus._member_names_ + expected_list = ['noSolution', 'infeasible', 'feasible', 'optimal'] + self.assertEqual(member_list, expected_list) + + def test_codes(self): + self.assertEqual(results.SolutionStatus.noSolution.value, 0) + self.assertEqual(results.SolutionStatus.infeasible.value, 10) + self.assertEqual(results.SolutionStatus.feasible.value, 20) + self.assertEqual(results.SolutionStatus.optimal.value, 30) + + +class TestResults(unittest.TestCase): + def test_member_list(self): + res = results.Results() + expected_declared = { + 'extra_info', + 'incumbent_objective', + 'iteration_count', + 'objective_bound', + 'solution_loader', + 'solution_status', + 'solver_name', + 'solver_version', + 'termination_condition', + 'timing_info', + 'solver_log', + 'solver_configuration', + } + actual_declared = res._declared + self.assertEqual(expected_declared, actual_declared) + + def test_default_initialization(self): + res = results.Results() + self.assertIsNone(res.solution_loader) + self.assertIsNone(res.incumbent_objective) + self.assertIsNone(res.objective_bound) + self.assertEqual( + res.termination_condition, results.TerminationCondition.unknown + ) + self.assertEqual(res.solution_status, results.SolutionStatus.noSolution) + self.assertIsNone(res.solver_name) + self.assertIsNone(res.solver_version) + self.assertIsNone(res.iteration_count) + self.assertIsInstance(res.timing_info, ConfigDict) + self.assertIsInstance(res.extra_info, ConfigDict) + self.assertIsNone(res.timing_info.start_timestamp) + self.assertIsNone(res.timing_info.wall_time) + + def test_display(self): + res = results.Results() + stream = StringIO() + res.display(ostream=stream) + expected_print = """solution_loader: None +termination_condition: TerminationCondition.unknown +solution_status: SolutionStatus.noSolution +incumbent_objective: None +objective_bound: None +solver_name: None +solver_version: None +iteration_count: None +timing_info: + start_timestamp: None + wall_time: None +extra_info: +""" + out = stream.getvalue() + if 'null' in out: + out = out.replace('null', 'None') + self.assertEqual(expected_print, out) + + def test_generated_results(self): + m = pyo.ConcreteModel() + m.x = Var() + m.y = Var() + m.c1 = pyo.Constraint(expr=m.x == 1) + m.c2 = pyo.Constraint(expr=m.y == 2) + + primals = {} + primals[id(m.x)] = (m.x, 1) + primals[id(m.y)] = (m.y, 2) + duals = {} + duals[m.c1] = 3 + duals[m.c2] = 4 + rc = {} + rc[id(m.x)] = (m.x, 5) + rc[id(m.y)] = (m.y, 6) + + res = results.Results() + res.solution_loader = SolutionLoaderExample( + primals=primals, duals=duals, reduced_costs=rc + ) + + res.solution_loader.load_vars() + self.assertAlmostEqual(m.x.value, 1) + self.assertAlmostEqual(m.y.value, 2) + + m.x.value = None + m.y.value = None + + res.solution_loader.load_vars([m.y]) + self.assertIsNone(m.x.value) + self.assertAlmostEqual(m.y.value, 2) + + duals2 = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], duals2[m.c1]) + self.assertAlmostEqual(duals[m.c2], duals2[m.c2]) + + duals2 = res.solution_loader.get_duals([m.c2]) + self.assertNotIn(m.c1, duals2) + self.assertAlmostEqual(duals[m.c2], duals2[m.c2]) + + rc2 = res.solution_loader.get_reduced_costs() + self.assertAlmostEqual(rc[id(m.x)][1], rc2[m.x]) + self.assertAlmostEqual(rc[id(m.y)][1], rc2[m.y]) + + rc2 = res.solution_loader.get_reduced_costs([m.y]) + self.assertNotIn(m.x, rc2) + self.assertAlmostEqual(rc[id(m.y)][1], rc2[m.y]) diff --git a/pyomo/contrib/solver/tests/unit/test_sol_reader.py b/pyomo/contrib/solver/tests/unit/test_sol_reader.py new file mode 100644 index 00000000000..d5602945e07 --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/test_sol_reader.py @@ -0,0 +1,51 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common import unittest +from pyomo.common.fileutils import this_file_dir +from pyomo.common.tempfiles import TempfileManager +from pyomo.contrib.solver.sol_reader import parse_sol_file, SolFileData + +currdir = this_file_dir() + + +class TestSolFileData(unittest.TestCase): + def test_default_instantiation(self): + instance = SolFileData() + self.assertIsInstance(instance.primals, list) + self.assertIsInstance(instance.duals, list) + self.assertIsInstance(instance.var_suffixes, dict) + self.assertIsInstance(instance.con_suffixes, dict) + self.assertIsInstance(instance.obj_suffixes, dict) + self.assertIsInstance(instance.problem_suffixes, dict) + self.assertIsInstance(instance.other, list) + + +class TestSolParser(unittest.TestCase): + # I am not sure how to write these tests best since the sol parser requires + # not only a file but also the nl_info and results objects. + def setUp(self): + TempfileManager.push() + + def tearDown(self): + TempfileManager.pop(remove=True) + + def test_default_behavior(self): + pass + + def test_custom_behavior(self): + pass + + def test_infeasible1(self): + pass + + def test_infeasible2(self): + pass diff --git a/pyomo/contrib/solver/tests/unit/test_solution.py b/pyomo/contrib/solver/tests/unit/test_solution.py new file mode 100644 index 00000000000..a5ee8a9e391 --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/test_solution.py @@ -0,0 +1,88 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common import unittest +from pyomo.contrib.solver.solution import SolutionLoaderBase, PersistentSolutionLoader + + +class TestSolutionLoaderBase(unittest.TestCase): + def test_abstract_member_list(self): + expected_list = ['get_primals'] + member_list = list(SolutionLoaderBase.__abstractmethods__) + self.assertEqual(sorted(expected_list), sorted(member_list)) + + def test_member_list(self): + expected_list = ['load_vars', 'get_primals', 'get_duals', 'get_reduced_costs'] + method_list = [ + method + for method in dir(SolutionLoaderBase) + if method.startswith('_') is False + ] + self.assertEqual(sorted(expected_list), sorted(method_list)) + + @unittest.mock.patch.multiple(SolutionLoaderBase, __abstractmethods__=set()) + def test_solution_loader_base(self): + self.instance = SolutionLoaderBase() + self.assertEqual(self.instance.get_primals(), None) + with self.assertRaises(NotImplementedError): + self.instance.get_duals() + with self.assertRaises(NotImplementedError): + self.instance.get_reduced_costs() + + +class TestSolSolutionLoader(unittest.TestCase): + # I am currently unsure how to test this further because it relies heavily on + # SolFileData and NLWriterInfo + def test_member_list(self): + expected_list = ['load_vars', 'get_primals', 'get_duals', 'get_reduced_costs'] + method_list = [ + method + for method in dir(SolutionLoaderBase) + if method.startswith('_') is False + ] + self.assertEqual(sorted(expected_list), sorted(method_list)) + + +class TestPersistentSolutionLoader(unittest.TestCase): + def test_abstract_member_list(self): + # We expect no abstract members at this point because it's a real-life + # instantiation of SolutionLoaderBase + member_list = list(PersistentSolutionLoader('ipopt').__abstractmethods__) + self.assertEqual(member_list, []) + + def test_member_list(self): + expected_list = [ + 'load_vars', + 'get_primals', + 'get_duals', + 'get_reduced_costs', + 'invalidate', + ] + method_list = [ + method + for method in dir(PersistentSolutionLoader) + if method.startswith('_') is False + ] + self.assertEqual(sorted(expected_list), sorted(method_list)) + + def test_default_initialization(self): + # Realistically, a solver object should be passed into this. + # However, it works with a string. It'll just error loudly if you + # try to run get_primals, etc. + self.instance = PersistentSolutionLoader('ipopt') + self.assertTrue(self.instance._valid) + self.assertEqual(self.instance._solver, 'ipopt') + + def test_invalid(self): + self.instance = PersistentSolutionLoader('ipopt') + self.instance.invalidate() + with self.assertRaises(RuntimeError): + self.instance.get_primals() diff --git a/pyomo/contrib/solver/tests/unit/test_util.py b/pyomo/contrib/solver/tests/unit/test_util.py new file mode 100644 index 00000000000..f2e8ee707f4 --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/test_util.py @@ -0,0 +1,142 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common import unittest +import pyomo.environ as pyo +from pyomo.contrib.solver.util import ( + collect_vars_and_named_exprs, + get_objective, + check_optimal_termination, + assert_optimal_termination, + SolverStatus, + LegacyTerminationCondition, +) +from pyomo.contrib.solver.results import Results, SolutionStatus, TerminationCondition +from typing import Callable +from pyomo.common.gsl import find_GSL +from pyomo.opt.results import SolverResults + + +class TestGenericUtils(unittest.TestCase): + def basics_helper(self, collector: Callable, *args): + m = pyo.ConcreteModel() + m.x = pyo.Var() + m.y = pyo.Var() + m.z = pyo.Var() + m.E = pyo.Expression(expr=2 * m.z + 1) + m.y.fix(3) + e = m.x * m.y + m.x * m.E + named_exprs, var_list, fixed_vars, external_funcs = collector(e, *args) + self.assertEqual([m.E], named_exprs) + self.assertEqual([m.x, m.y, m.z], var_list) + self.assertEqual([m.y], fixed_vars) + self.assertEqual([], external_funcs) + + def test_collect_vars_basics(self): + self.basics_helper(collect_vars_and_named_exprs) + + def external_func_helper(self, collector: Callable, *args): + DLL = find_GSL() + if not DLL: + self.skipTest('Could not find amplgsl.dll library') + + m = pyo.ConcreteModel() + m.x = pyo.Var() + m.y = pyo.Var() + m.z = pyo.Var() + m.hypot = pyo.ExternalFunction(library=DLL, function='gsl_hypot') + func = m.hypot(m.x, m.x * m.y) + m.E = pyo.Expression(expr=2 * func) + m.y.fix(3) + e = m.z + m.x * m.E + named_exprs, var_list, fixed_vars, external_funcs = collector(e, *args) + self.assertEqual([m.E], named_exprs) + self.assertEqual([m.z, m.x, m.y], var_list) + self.assertEqual([m.y], fixed_vars) + self.assertEqual([func], external_funcs) + + def test_collect_vars_external(self): + self.external_func_helper(collect_vars_and_named_exprs) + + def simple_model(self): + model = pyo.ConcreteModel() + model.x = pyo.Var([1, 2], domain=pyo.NonNegativeReals) + model.OBJ = pyo.Objective(expr=2 * model.x[1] + 3 * model.x[2]) + model.Constraint1 = pyo.Constraint(expr=3 * model.x[1] + 4 * model.x[2] >= 1) + return model + + def test_get_objective_success(self): + model = self.simple_model() + self.assertEqual(model.OBJ, get_objective(model)) + + def test_get_objective_raise(self): + model = self.simple_model() + model.OBJ2 = pyo.Objective(expr=model.x[1] - 4 * model.x[2]) + with self.assertRaises(ValueError): + get_objective(model) + + def test_check_optimal_termination_new_interface(self): + results = Results() + results.solution_status = SolutionStatus.optimal + results.termination_condition = ( + TerminationCondition.convergenceCriteriaSatisfied + ) + # Both items satisfied + self.assertTrue(check_optimal_termination(results)) + # Termination condition not satisfied + results.termination_condition = TerminationCondition.iterationLimit + self.assertFalse(check_optimal_termination(results)) + # Both not satisfied + results.solution_status = SolutionStatus.noSolution + self.assertFalse(check_optimal_termination(results)) + + def test_check_optimal_termination_condition_legacy_interface(self): + results = SolverResults() + results.solver.status = SolverStatus.ok + results.solver.termination_condition = LegacyTerminationCondition.optimal + # Both items satisfied + self.assertTrue(check_optimal_termination(results)) + # Termination condition not satisfied + results.solver.termination_condition = LegacyTerminationCondition.unknown + self.assertFalse(check_optimal_termination(results)) + # Both not satisfied + results.solver.termination_condition = SolverStatus.aborted + self.assertFalse(check_optimal_termination(results)) + + def test_assert_optimal_termination_new_interface(self): + results = Results() + results.solution_status = SolutionStatus.optimal + results.termination_condition = ( + TerminationCondition.convergenceCriteriaSatisfied + ) + assert_optimal_termination(results) + # Termination condition not satisfied + results.termination_condition = TerminationCondition.iterationLimit + with self.assertRaises(RuntimeError): + assert_optimal_termination(results) + # Both not satisfied + results.solution_status = SolutionStatus.noSolution + with self.assertRaises(RuntimeError): + assert_optimal_termination(results) + + def test_assert_optimal_termination_legacy_interface(self): + results = SolverResults() + results.solver.status = SolverStatus.ok + results.solver.termination_condition = LegacyTerminationCondition.optimal + assert_optimal_termination(results) + # Termination condition not satisfied + results.solver.termination_condition = LegacyTerminationCondition.unknown + with self.assertRaises(RuntimeError): + assert_optimal_termination(results) + # Both not satisfied + results.solver.termination_condition = SolverStatus.aborted + with self.assertRaises(RuntimeError): + assert_optimal_termination(results) diff --git a/pyomo/contrib/solver/util.py b/pyomo/contrib/solver/util.py new file mode 100644 index 00000000000..c6bbfbd22ad --- /dev/null +++ b/pyomo/contrib/solver/util.py @@ -0,0 +1,143 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.core.expr.visitor import ExpressionValueVisitor, nonpyomo_leaf_types +import pyomo.core.expr as EXPR +from pyomo.core.base.objective import Objective +from pyomo.opt.results.solver import ( + SolverStatus, + TerminationCondition as LegacyTerminationCondition, +) + + +from pyomo.contrib.solver.results import TerminationCondition, SolutionStatus + + +def get_objective(block): + """ + Get current active objective on a block. If there is more than one active, + return an error. + """ + obj = None + for o in block.component_data_objects( + Objective, descend_into=True, active=True, sort=True + ): + if obj is not None: + raise ValueError('Multiple active objectives found') + obj = o + return obj + + +def check_optimal_termination(results): + """ + This function returns True if the termination condition for the solver + is 'optimal'. + + Parameters + ---------- + results : Pyomo Results object returned from solver.solve + + Returns + ------- + `bool` + """ + if hasattr(results, 'solution_status'): + if results.solution_status == SolutionStatus.optimal and ( + results.termination_condition + == TerminationCondition.convergenceCriteriaSatisfied + ): + return True + else: + if results.solver.status == SolverStatus.ok and ( + results.solver.termination_condition == LegacyTerminationCondition.optimal + or results.solver.termination_condition + == LegacyTerminationCondition.locallyOptimal + or results.solver.termination_condition + == LegacyTerminationCondition.globallyOptimal + ): + return True + return False + + +def assert_optimal_termination(results): + """ + This function checks if the termination condition for the solver + is 'optimal', 'locallyOptimal', or 'globallyOptimal', and the status is 'ok' + and it raises a RuntimeError exception if this is not true. + + Parameters + ---------- + results : Pyomo Results object returned from solver.solve + """ + if not check_optimal_termination(results): + if hasattr(results, 'solution_status'): + msg = ( + 'Solver failed to return an optimal solution. ' + 'Solution status: {}, Termination condition: {}'.format( + results.solution_status, results.termination_condition + ) + ) + else: + msg = ( + 'Solver failed to return an optimal solution. ' + 'Solver status: {}, Termination condition: {}'.format( + results.solver.status, results.solver.termination_condition + ) + ) + raise RuntimeError(msg) + + +class _VarAndNamedExprCollector(ExpressionValueVisitor): + def __init__(self): + self.named_expressions = {} + self.variables = {} + self.fixed_vars = {} + self._external_functions = {} + + def visit(self, node, values): + pass + + def visiting_potential_leaf(self, node): + if type(node) in nonpyomo_leaf_types: + return True, None + + if node.is_variable_type(): + self.variables[id(node)] = node + if node.is_fixed(): + self.fixed_vars[id(node)] = node + return True, None + + if node.is_named_expression_type(): + self.named_expressions[id(node)] = node + return False, None + + if type(node) is EXPR.ExternalFunctionExpression: + self._external_functions[id(node)] = node + return False, None + + if node.is_expression_type(): + return False, None + + return True, None + + +_visitor = _VarAndNamedExprCollector() + + +def collect_vars_and_named_exprs(expr): + _visitor.__init__() + _visitor.dfs_postorder_stack(expr) + return ( + list(_visitor.named_expressions.values()), + list(_visitor.variables.values()), + list(_visitor.fixed_vars.values()), + list(_visitor._external_functions.values()), + ) diff --git a/pyomo/contrib/trustregion/TRF.py b/pyomo/contrib/trustregion/TRF.py index 45e60df7658..ea3a8c746a4 100644 --- a/pyomo/contrib/trustregion/TRF.py +++ b/pyomo/contrib/trustregion/TRF.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -35,7 +35,7 @@ logger = logging.getLogger('pyomo.contrib.trustregion') -__version__ = '0.2.0' +__version__ = (0, 2, 0) def trust_region_method(model, decision_variables, ext_fcn_surrogate_map_rule, config): diff --git a/pyomo/contrib/trustregion/__init__.py b/pyomo/contrib/trustregion/__init__.py index 62ba0892686..38b30839be3 100644 --- a/pyomo/contrib/trustregion/__init__.py +++ b/pyomo/contrib/trustregion/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/trustregion/examples/__init__.py b/pyomo/contrib/trustregion/examples/__init__.py index 62ba0892686..38b30839be3 100644 --- a/pyomo/contrib/trustregion/examples/__init__.py +++ b/pyomo/contrib/trustregion/examples/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/trustregion/examples/example1.py b/pyomo/contrib/trustregion/examples/example1.py index 19965ff1cb2..66df26d143f 100755 --- a/pyomo/contrib/trustregion/examples/example1.py +++ b/pyomo/contrib/trustregion/examples/example1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/trustregion/examples/example2.py b/pyomo/contrib/trustregion/examples/example2.py index 0c506eb6891..ad648855410 100644 --- a/pyomo/contrib/trustregion/examples/example2.py +++ b/pyomo/contrib/trustregion/examples/example2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/trustregion/filter.py b/pyomo/contrib/trustregion/filter.py index 2f0b20ee8f8..7e647a7f0c5 100644 --- a/pyomo/contrib/trustregion/filter.py +++ b/pyomo/contrib/trustregion/filter.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/trustregion/interface.py b/pyomo/contrib/trustregion/interface.py index f68f2fdb308..b459e7cfa17 100644 --- a/pyomo/contrib/trustregion/interface.py +++ b/pyomo/contrib/trustregion/interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/trustregion/plugins.py b/pyomo/contrib/trustregion/plugins.py index 59a11986f3c..d4ed22b9d2f 100644 --- a/pyomo/contrib/trustregion/plugins.py +++ b/pyomo/contrib/trustregion/plugins.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/trustregion/tests/__init__.py b/pyomo/contrib/trustregion/tests/__init__.py index 62ba0892686..38b30839be3 100644 --- a/pyomo/contrib/trustregion/tests/__init__.py +++ b/pyomo/contrib/trustregion/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/trustregion/tests/test_TRF.py b/pyomo/contrib/trustregion/tests/test_TRF.py index e14a784b4af..e2b2b2b64ad 100644 --- a/pyomo/contrib/trustregion/tests/test_TRF.py +++ b/pyomo/contrib/trustregion/tests/test_TRF.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/trustregion/tests/test_examples.py b/pyomo/contrib/trustregion/tests/test_examples.py index a954b0851c7..5451cca5961 100644 --- a/pyomo/contrib/trustregion/tests/test_examples.py +++ b/pyomo/contrib/trustregion/tests/test_examples.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/trustregion/tests/test_filter.py b/pyomo/contrib/trustregion/tests/test_filter.py index 1b89d8d5cd1..18e833685f8 100644 --- a/pyomo/contrib/trustregion/tests/test_filter.py +++ b/pyomo/contrib/trustregion/tests/test_filter.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/trustregion/tests/test_interface.py b/pyomo/contrib/trustregion/tests/test_interface.py index 24517041b2c..0922ccf950b 100644 --- a/pyomo/contrib/trustregion/tests/test_interface.py +++ b/pyomo/contrib/trustregion/tests/test_interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -33,7 +33,7 @@ cos, SolverFactory, ) -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import VarData from pyomo.core.expr.numeric_expr import ExternalFunctionExpression from pyomo.core.expr.visitor import identify_variables from pyomo.contrib.trustregion.interface import TRFInterface @@ -107,7 +107,7 @@ def test_replaceRF(self): expr = self.interface.model.c1.expr new_expr = self.interface.replaceEF(expr) self.assertIsNot(expr, new_expr) - self.assertEquals( + self.assertEqual( str(new_expr), 'x[0]*z[0]**2 + trf_data.ef_outputs[1] == 2.8284271247461903', ) @@ -158,7 +158,7 @@ def test_replaceExternalFunctionsWithVariables(self): self.assertIsInstance(k, ExternalFunctionExpression) self.assertIn(str(self.interface.model.x[0]), str(k)) self.assertIn(str(self.interface.model.x[1]), str(k)) - self.assertIsInstance(i, _GeneralVarData) + self.assertIsInstance(i, VarData) self.assertEqual(i, self.interface.data.ef_outputs[1]) for i, k in self.interface.data.basis_expressions.items(): self.assertEqual(k, 0) @@ -382,17 +382,17 @@ def test_solveModel(self): self.interface.data.value_of_ef_inputs[...] = 0 # Run the solve objective, step_norm, feasibility = self.interface.solveModel() - self.assertEqual(objective, 5.150744273013601) - self.assertEqual(step_norm, 3.393437471478297) - self.assertEqual(feasibility, 0.09569982275514467) + self.assertAlmostEqual(objective, 5.150744273013601) + self.assertAlmostEqual(step_norm, 3.393437471478297) + self.assertAlmostEqual(feasibility, 0.09569982275514467) self.interface.data.basis_constraint.deactivate() # Change the constraint and update the surrogate model self.interface.updateSurrogateModel() self.interface.data.sm_constraint_basis.activate() objective, step_norm, feasibility = self.interface.solveModel() - self.assertEqual(objective, 5.15065981284333) - self.assertEqual(step_norm, 0.0017225116628372117) - self.assertEqual(feasibility, 0.00014665023773349772) + self.assertAlmostEqual(objective, 5.15065981284333) + self.assertAlmostEqual(step_norm, 0.0017225116628372117) + self.assertAlmostEqual(feasibility, 0.00014665023773349772) @unittest.skipIf( not SolverFactory('ipopt').available(False), "The IPOPT solver is not available" @@ -407,8 +407,8 @@ def test_initializeProblem(self): self.assertEqual( self.interface.initial_decision_bounds[var.name], [var.lb, var.ub] ) - self.assertEqual(objective, 5.150744273013601) - self.assertEqual(feasibility, 0.09569982275514467) + self.assertAlmostEqual(objective, 5.150744273013601) + self.assertAlmostEqual(feasibility, 0.09569982275514467) self.assertTrue(self.interface.data.sm_constraint_basis.active) self.assertFalse(self.interface.data.basis_constraint.active) diff --git a/pyomo/contrib/trustregion/tests/test_util.py b/pyomo/contrib/trustregion/tests/test_util.py index 3054c2c2bd5..bdc91744e61 100644 --- a/pyomo/contrib/trustregion/tests/test_util.py +++ b/pyomo/contrib/trustregion/tests/test_util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/trustregion/util.py b/pyomo/contrib/trustregion/util.py index f27420a2bee..ff3f218fc27 100644 --- a/pyomo/contrib/trustregion/util.py +++ b/pyomo/contrib/trustregion/util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/viewer/__init__.py b/pyomo/contrib/viewer/__init__.py index 8b137891791..a4a626013c4 100644 --- a/pyomo/contrib/viewer/__init__.py +++ b/pyomo/contrib/viewer/__init__.py @@ -1 +1,10 @@ - +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/viewer/model_browser.py b/pyomo/contrib/viewer/model_browser.py index 8379518a4cf..91dc946c55d 100644 --- a/pyomo/contrib/viewer/model_browser.py +++ b/pyomo/contrib/viewer/model_browser.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -33,7 +33,7 @@ import pyomo.contrib.viewer.qt as myqt from pyomo.contrib.viewer.report import value_no_exception, get_residual -from pyomo.core.base.param import _ParamData +from pyomo.core.base.param import ParamData from pyomo.environ import ( Block, BooleanVar, @@ -243,7 +243,7 @@ def _get_expr_callback(self): return None def _get_value_callback(self): - if isinstance(self.data, _ParamData): + if isinstance(self.data, ParamData): v = value_no_exception(self.data, div0="divide_by_0") # Check the param value for numpy float and int, sometimes numpy # values can sneak in especially if you set parameters from data @@ -295,7 +295,7 @@ def _get_residual_callback(self): def _get_units_callback(self): if isinstance(self.data, (Var, Var._ComponentDataClass)): return str(units.get_units(self.data)) - if isinstance(self.data, (Param, _ParamData)): + if isinstance(self.data, (Param, ParamData)): return str(units.get_units(self.data)) return self._cache_units @@ -320,7 +320,7 @@ def _set_value_callback(self, val): o.value = val except: return - elif isinstance(self.data, _ParamData): + elif isinstance(self.data, ParamData): if not self.data.parent_component().mutable: return try: diff --git a/pyomo/contrib/viewer/model_select.py b/pyomo/contrib/viewer/model_select.py index 3c6c4ccdf17..e9c82740708 100644 --- a/pyomo/contrib/viewer/model_select.py +++ b/pyomo/contrib/viewer/model_select.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/viewer/pyomo_viewer.py b/pyomo/contrib/viewer/pyomo_viewer.py index a8fec745af4..6a24e12aa61 100644 --- a/pyomo/contrib/viewer/pyomo_viewer.py +++ b/pyomo/contrib/viewer/pyomo_viewer.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/viewer/qt.py b/pyomo/contrib/viewer/qt.py index 150fa3560f6..2715d275758 100644 --- a/pyomo/contrib/viewer/qt.py +++ b/pyomo/contrib/viewer/qt.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/viewer/report.py b/pyomo/contrib/viewer/report.py index 6f212b2fbc3..a28e0082212 100644 --- a/pyomo/contrib/viewer/report.py +++ b/pyomo/contrib/viewer/report.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -50,7 +50,7 @@ def get_residual(ui_data, c): values of the constraint body. This function uses the cached values and will not trigger recalculation. If variable values have changed, this may not yield accurate results. - c(_ConstraintData): a constraint or constraint data + c(ConstraintData): a constraint or constraint data Returns: (float) residual """ @@ -149,7 +149,7 @@ def degrees_of_freedom(blk): Return the degrees of freedom. Args: - blk (Block or _BlockData): Block to count degrees of freedom in + blk (Block or BlockData): Block to count degrees of freedom in Returns: (int): Number of degrees of freedom """ diff --git a/pyomo/contrib/viewer/residual_table.py b/pyomo/contrib/viewer/residual_table.py index 46a86adbce6..94e8902848f 100644 --- a/pyomo/contrib/viewer/residual_table.py +++ b/pyomo/contrib/viewer/residual_table.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -102,10 +102,12 @@ def _inactive_to_back(c): self._items.sort( key=lambda o: ( o is None, - get_residual(self.ui_data, o) - if get_residual(self.ui_data, o) is not None - and not isinstance(get_residual(self.ui_data, o), str) - else _inactive_to_back(o), + ( + get_residual(self.ui_data, o) + if get_residual(self.ui_data, o) is not None + and not isinstance(get_residual(self.ui_data, o), str) + else _inactive_to_back(o) + ), ), reverse=True, ) diff --git a/pyomo/contrib/viewer/tests/__init__.py b/pyomo/contrib/viewer/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/viewer/tests/__init__.py +++ b/pyomo/contrib/viewer/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/viewer/tests/test_data_model_item.py b/pyomo/contrib/viewer/tests/test_data_model_item.py index f3e7aaf9513..781ca25508a 100644 --- a/pyomo/contrib/viewer/tests/test_data_model_item.py +++ b/pyomo/contrib/viewer/tests/test_data_model_item.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/viewer/tests/test_data_model_tree.py b/pyomo/contrib/viewer/tests/test_data_model_tree.py index db745aee9ca..d517c91b353 100644 --- a/pyomo/contrib/viewer/tests/test_data_model_tree.py +++ b/pyomo/contrib/viewer/tests/test_data_model_tree.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/viewer/tests/test_qt.py b/pyomo/contrib/viewer/tests/test_qt.py index 38a022b6668..e71921500f9 100644 --- a/pyomo/contrib/viewer/tests/test_qt.py +++ b/pyomo/contrib/viewer/tests/test_qt.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/viewer/tests/test_report.py b/pyomo/contrib/viewer/tests/test_report.py index b496e2294ff..88044490a77 100644 --- a/pyomo/contrib/viewer/tests/test_report.py +++ b/pyomo/contrib/viewer/tests/test_report.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/viewer/ui.py b/pyomo/contrib/viewer/ui.py index 8a621534b31..374af8a26f0 100644 --- a/pyomo/contrib/viewer/ui.py +++ b/pyomo/contrib/viewer/ui.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/viewer/ui_data.py b/pyomo/contrib/viewer/ui_data.py index 8bbaac14e13..c716cfeedf6 100644 --- a/pyomo/contrib/viewer/ui_data.py +++ b/pyomo/contrib/viewer/ui_data.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/__init__.py b/pyomo/core/__init__.py index 5cbebcee9ec..f0d168d98f9 100644 --- a/pyomo/core/__init__.py +++ b/pyomo/core/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -33,6 +33,8 @@ exactly, atleast, atmost, + all_different, + count_if, implies, lnot, xor, @@ -99,7 +101,7 @@ BooleanValue, native_logical_values, ) -from pyomo.core.kernel.objective import minimize, maximize +from pyomo.core.base import minimize, maximize from pyomo.core.base.config import PyomoOptions from pyomo.core.base.expression import Expression diff --git a/pyomo/core/base/PyomoModel.py b/pyomo/core/base/PyomoModel.py index f8b2710b9f2..22bbc5fa02b 100644 --- a/pyomo/core/base/PyomoModel.py +++ b/pyomo/core/base/PyomoModel.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['Model', 'ConcreteModel', 'AbstractModel', 'global_option'] - import logging import sys from weakref import ref as weakref_ref @@ -20,7 +18,7 @@ from pyomo.common import timing from pyomo.common.collections import Bunch from pyomo.common.dependencies import pympler, pympler_available -from pyomo.common.deprecation import deprecated, deprecation_warning +from pyomo.common.deprecation import deprecated from pyomo.common.gc_manager import PauseGC from pyomo.common.log import is_debug_set from pyomo.common.numeric_types import value @@ -34,12 +32,12 @@ from pyomo.core.base.block import ScalarBlock from pyomo.core.base.set import Set from pyomo.core.base.componentuid import ComponentUID -from pyomo.core.base.transformation import TransformationFactory from pyomo.core.base.label import CNameLabeler, CuidLabeler from pyomo.dataportal.DataPortal import DataPortal -from pyomo.opt.results import SolverResults, Solution, SolverStatus, UndefinedData +from pyomo.opt.results import Solution, SolverStatus, UndefinedData +from contextlib import nullcontext from io import StringIO logger = logging.getLogger('pyomo.core') @@ -691,9 +689,6 @@ def create_instance( if self.is_constructed(): return self.clone() - if report_timing: - timing.report_timing() - if name is None: # Preserve only the local name (not the FQ name, as that may # have been quoted or otherwise escaped) @@ -709,42 +704,44 @@ def create_instance( if data is None: data = {} - # - # Clone the model and load the data - # - instance = self.clone() + reporting_context = timing.report_timing if report_timing else nullcontext + with reporting_context(): + # + # Clone the model and load the data + # + instance = self.clone() - if name is not None: - instance._name = name + if name is not None: + instance._name = name - # If someone passed a rule for creating the instance, fire the - # rule before constructing the components. - if instance._rule is not None: - instance._rule(instance, next(iter(self.index_set()))) + # If someone passed a rule for creating the instance, fire the + # rule before constructing the components. + if instance._rule is not None: + instance._rule(instance, next(iter(self.index_set()))) - if namespaces: - _namespaces = list(namespaces) - else: - _namespaces = [] - if namespace is not None: - _namespaces.append(namespace) - if None not in _namespaces: - _namespaces.append(None) + if namespaces: + _namespaces = list(namespaces) + else: + _namespaces = [] + if namespace is not None: + _namespaces.append(namespace) + if None not in _namespaces: + _namespaces.append(None) - instance.load(data, namespaces=_namespaces, profile_memory=profile_memory) + instance.load(data, namespaces=_namespaces, profile_memory=profile_memory) - # - # Indicate that the model is concrete/constructed - # - instance._constructed = True - # - # Change this class from "Abstract" to "Concrete". It is - # absolutely crazy that this is allowed in Python, but since the - # AbstractModel and ConcreteModel are basically identical, we - # can "reassign" the new concrete instance to be an instance of - # ConcreteModel - # - instance.__class__ = ConcreteModel + # + # Indicate that the model is concrete/constructed + # + instance._constructed = True + # + # Change this class from "Abstract" to "Concrete". It is + # absolutely crazy that this is allowed in Python, but since the + # AbstractModel and ConcreteModel are basically identical, we + # can "reassign" the new concrete instance to be an instance of + # ConcreteModel + # + instance.__class__ = ConcreteModel return instance @deprecated( @@ -789,7 +786,7 @@ def _load_model_data(self, modeldata, namespaces, **kwds): profile_memory = kwds.get('profile_memory', 0) if profile_memory >= 2 and pympler_available: - mem_used = pympler.muppy.get_size(muppy.get_objects()) + mem_used = pympler.muppy.get_size(pympler.muppy.get_objects()) print("") print( " Total memory = %d bytes prior to model " @@ -798,7 +795,7 @@ def _load_model_data(self, modeldata, namespaces, **kwds): if profile_memory >= 3: gc.collect() - mem_used = pympler.muppy.get_size(muppy.get_objects()) + mem_used = pympler.muppy.get_size(pympler.muppy.get_objects()) print( " Total memory = %d bytes prior to model " "construction (after garbage collection)" % mem_used @@ -877,6 +874,7 @@ def _initialize_component( str(data).strip(), type(err).__name__, err, + extra={'cleandoc': False}, ) raise diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index f7815f1676b..6b295196864 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -12,6 +12,7 @@ # TODO: this import is for historical backwards compatibility and should # probably be removed from pyomo.common.collections import ComponentMap +from pyomo.common.enums import minimize, maximize from pyomo.core.expr.symbol_map import SymbolMap from pyomo.core.expr.numvalue import ( @@ -33,10 +34,11 @@ BooleanValue, native_logical_values, ) -from pyomo.core.kernel.objective import minimize, maximize -from pyomo.core.base.config import PyomoOptions -from pyomo.core.base.expression import Expression, _ExpressionData +from pyomo.core.base.component import name, Component, ModelComponentFactory +from pyomo.core.base.componentuid import ComponentUID +from pyomo.core.base.config import PyomoOptions +from pyomo.core.base.enums import SortComponents, TraversalStrategy from pyomo.core.base.label import ( CuidLabeler, CounterLabeler, @@ -47,56 +49,73 @@ NameLabeler, ShortNameLabeler, ) +from pyomo.core.base.misc import display +from pyomo.core.base.reference import Reference +from pyomo.core.base.symbol_map import symbol_map_from_instance +from pyomo.core.base.transformation import ( + Transformation, + TransformationFactory, + ReverseTransformationToken, +) + +from pyomo.core.base.PyomoModel import ( + global_option, + ModelSolution, + ModelSolutions, + Model, + ConcreteModel, + AbstractModel, +) # # Components # -from pyomo.core.base.component import name, Component, ModelComponentFactory -from pyomo.core.base.componentuid import ComponentUID from pyomo.core.base.action import BuildAction -from pyomo.core.base.check import BuildCheck -from pyomo.core.base.set import Set, SetOf, simple_set_rule, RangeSet -from pyomo.core.base.param import Param -from pyomo.core.base.var import Var, _VarData, _GeneralVarData, ScalarVar, VarList +from pyomo.core.base.block import ( + Block, + BlockData, + ScalarBlock, + active_components, + components, + active_components_data, + components_data, +) from pyomo.core.base.boolean_var import ( BooleanVar, - _BooleanVarData, - _GeneralBooleanVarData, + BooleanVarData, BooleanVarList, ScalarBooleanVar, ) +from pyomo.core.base.check import BuildCheck +from pyomo.core.base.connector import Connector, ConnectorData from pyomo.core.base.constraint import ( simple_constraint_rule, simple_constraintlist_rule, ConstraintList, Constraint, - _ConstraintData, + ConstraintData, ) +from pyomo.core.base.expression import Expression, NamedExpressionData, ExpressionData +from pyomo.core.base.external import ExternalFunction from pyomo.core.base.logical_constraint import ( LogicalConstraint, LogicalConstraintList, - _LogicalConstraintData, + LogicalConstraintData, ) from pyomo.core.base.objective import ( simple_objective_rule, simple_objectivelist_rule, Objective, ObjectiveList, - _ObjectiveData, -) -from pyomo.core.base.connector import Connector -from pyomo.core.base.sos import SOSConstraint -from pyomo.core.base.piecewise import Piecewise -from pyomo.core.base.suffix import ( - active_export_suffix_generator, - active_import_suffix_generator, - Suffix, + ObjectiveData, ) -from pyomo.core.base.external import ExternalFunction -from pyomo.core.base.symbol_map import symbol_map_from_instance -from pyomo.core.base.reference import Reference - +from pyomo.core.base.param import Param, ParamData +from pyomo.core.base.piecewise import Piecewise, PiecewiseData from pyomo.core.base.set import ( + Set, + SetData, + SetOf, + RangeSet, Reals, PositiveReals, NonPositiveReals, @@ -116,34 +135,21 @@ PercentFraction, RealInterval, IntegerInterval, + simple_set_rule, ) -from pyomo.core.base.misc import display -from pyomo.core.base.block import ( - Block, - ScalarBlock, - active_components, - components, - active_components_data, - components_data, -) -from pyomo.core.base.enums import SortComponents, TraversalStrategy -from pyomo.core.base.PyomoModel import ( - global_option, - ModelSolution, - ModelSolutions, - Model, - ConcreteModel, - AbstractModel, -) -from pyomo.core.base.transformation import ( - Transformation, - TransformationFactory, - ReverseTransformationToken, +from pyomo.core.base.sos import SOSConstraint, SOSConstraintData +from pyomo.core.base.suffix import ( + active_export_suffix_generator, + active_import_suffix_generator, + Suffix, ) +from pyomo.core.base.var import Var, VarData, ScalarVar, VarList from pyomo.core.base.instance2dat import instance2dat +# # These APIs are deprecated and should be removed in the near future +# from pyomo.core.base.set import set_options, RealSet, IntegerSet, BooleanSet from pyomo.common.deprecation import relocated_module_attribute @@ -155,4 +161,25 @@ relocated_module_attribute( 'SimpleBooleanVar', 'pyomo.core.base.boolean_var.SimpleBooleanVar', version='6.0' ) +# Historically, only a subset of "private" component data classes were imported here +relocated_module_attribute( + f'_GeneralVarData', f'pyomo.core.base.VarData', version='6.7.2' +) +relocated_module_attribute( + f'_GeneralBooleanVarData', f'pyomo.core.base.BooleanVarData', version='6.7.2' +) +relocated_module_attribute( + f'_ExpressionData', f'pyomo.core.base.NamedExpressionData', version='6.7.2' +) +for _cdata in ( + 'ConstraintData', + 'LogicalConstraintData', + 'VarData', + 'BooleanVarData', + 'ObjectiveData', +): + relocated_module_attribute( + f'_{_cdata}', f'pyomo.core.base.{_cdata}', version='6.7.2' + ) +del _cdata del relocated_module_attribute diff --git a/pyomo/core/base/action.py b/pyomo/core/base/action.py index b54beab8584..d24d94fe05a 100644 --- a/pyomo/core/base/action.py +++ b/pyomo/core/base/action.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['BuildAction'] - import logging import types @@ -24,7 +22,8 @@ @ModelComponentFactory.register( - "A component that performs arbitrary actions during model construction. The action rule is applied to every index value." + "A component that performs arbitrary actions during model construction. " + "The action rule is applied to every index value." ) class BuildAction(IndexedComponent): """A build action, which executes a rule for all valid indices. diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index dd043de86c3..44d7e45dcce 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,31 +9,20 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = [ - 'Block', - 'TraversalStrategy', - 'SortComponents', - 'active_components', - 'components', - 'active_components_data', - 'components_data', - 'SimpleBlock', - 'ScalarBlock', -] - +from __future__ import annotations import copy -import enum import logging import sys import weakref import textwrap -from contextlib import contextmanager -from inspect import isclass +from collections import defaultdict +from contextlib import contextmanager +from inspect import isclass, currentframe +from io import StringIO from itertools import filterfalse, chain from operator import itemgetter, attrgetter -from io import StringIO -from pyomo.common.pyomo_typing import overload +from typing import Union, Any, Type from pyomo.common.autoslots import AutoSlots from pyomo.common.collections import Mapping @@ -41,7 +30,7 @@ from pyomo.common.formatting import StreamIndenter from pyomo.common.gc_manager import PauseGC from pyomo.common.log import is_debug_set -from pyomo.common.sorting import sorted_robust +from pyomo.common.pyomo_typing import overload from pyomo.common.timing import ConstructionTimer from pyomo.core.base.component import ( Component, @@ -51,12 +40,13 @@ from pyomo.core.base.enums import SortComponents, TraversalStrategy from pyomo.core.base.global_set import UnindexedComponent_index from pyomo.core.base.componentuid import ComponentUID -from pyomo.core.base.set import Any, GlobalSetBase, _SetDataBase +from pyomo.core.base.set import Any from pyomo.core.base.var import Var from pyomo.core.base.initializer import Initializer from pyomo.core.base.indexed_component import ( ActiveIndexedComponent, UnindexedComponent_set, + IndexedComponent, ) from pyomo.opt.base import ProblemFormat, guess_format @@ -170,13 +160,13 @@ def __init__(self): self.seen_data = set() def unique(self, comp, items, are_values): - """Returns generator that filters duplicate _ComponentData objects from items + """Returns generator that filters duplicate ComponentData objects from items Parameters ---------- comp: ComponentBase The Component (indexed or scalar) that contains all - _ComponentData returned by the `items` generator. `comp` may + ComponentData returned by the `items` generator. `comp` may be an IndexedComponent generated by :py:func:`Reference` (and hence may not own the component datas in `items`) @@ -185,8 +175,8 @@ def unique(self, comp, items, are_values): `comp` Component. are_values: bool - If `True`, `items` yields _ComponentData objects, otherwise, - `items` yields `(index, _ComponentData)` tuples. + If `True`, `items` yields ComponentData objects, otherwise, + `items` yields `(index, ComponentData)` tuples. """ if comp.is_reference(): @@ -264,7 +254,7 @@ class _BlockConstruction(object): class PseudoMap(AutoSlots.Mixin): """ This class presents a "mock" dict interface to the internal - _BlockData data structures. We return this object to the + BlockData data structures. We return this object to the user to preserve the historical "{ctype : {name : obj}}" interface without actually regenerating that dict-of-dicts data structure. @@ -497,7 +487,7 @@ def iteritems(self): return self.items() -class _BlockData(ActiveComponentData): +class BlockData(ActiveComponentData): """ This class holds the fundamental block data. """ @@ -547,11 +537,12 @@ def __init__(self, component): # _ctypes: { ctype -> [1st idx, last idx, count] } # _decl: { name -> idx } # _decl_order: list( tuples( obj, next_type_idx ) ) - super(_BlockData, self).__setattr__('_ctypes', {}) - super(_BlockData, self).__setattr__('_decl', {}) - super(_BlockData, self).__setattr__('_decl_order', []) + super(BlockData, self).__setattr__('_ctypes', {}) + super(BlockData, self).__setattr__('_decl', {}) + super(BlockData, self).__setattr__('_decl_order', []) + self._private_data = None - def __getattr__(self, val): + def __getattr__(self, val) -> Union[Component, IndexedComponent, Any]: if val in ModelComponentFactory: return _component_decorator(self, ModelComponentFactory.get_class(val)) # Since the base classes don't support getattr, we can just @@ -560,7 +551,7 @@ def __getattr__(self, val): "'%s' object has no attribute '%s'" % (self.__class__.__name__, val) ) - def __setattr__(self, name, val): + def __setattr__(self, name: str, val: Union[Component, IndexedComponent, Any]): """ Set an attribute of a block data object. """ @@ -583,7 +574,7 @@ def __setattr__(self, name, val): # Other Python objects are added with the standard __setattr__ # method. # - super(_BlockData, self).__setattr__(name, val) + super(BlockData, self).__setattr__(name, val) # # Case 2. The attribute exists and it is a component in the # list of declarations in this block. We will use the @@ -637,11 +628,11 @@ def __setattr__(self, name, val): # else: # - # NB: This is important: the _BlockData is either a scalar + # NB: This is important: the BlockData is either a scalar # Block (where _parent and _component are defined) or a # single block within an Indexed Block (where only # _component is defined). Regardless, the - # _BlockData.__init__() method declares these methods and + # BlockData.__init__() method declares these methods and # sets them either to None or a weakref. Thus, we will # never have a problem converting these objects from # weakrefs into Blocks and back (when pickling); the @@ -656,23 +647,23 @@ def __setattr__(self, name, val): # return True, this shouldn't be too inefficient. # if name == '_parent': - if val is not None and not isinstance(val(), _BlockData): + if val is not None and not isinstance(val(), BlockData): raise ValueError( "Cannot set the '_parent' attribute of Block '%s' " "to a non-Block object (with type=%s); Did you " "try to create a model component named '_parent'?" % (self.name, type(val)) ) - super(_BlockData, self).__setattr__(name, val) + super(BlockData, self).__setattr__(name, val) elif name == '_component': - if val is not None and not isinstance(val(), _BlockData): + if val is not None and not isinstance(val(), BlockData): raise ValueError( "Cannot set the '_component' attribute of Block '%s' " "to a non-Block object (with type=%s); Did you " "try to create a model component named '_component'?" % (self.name, type(val)) ) - super(_BlockData, self).__setattr__(name, val) + super(BlockData, self).__setattr__(name, val) # # At this point, we should only be seeing non-component data # the user is hanging on the blocks (uncommon) or the @@ -689,7 +680,7 @@ def __setattr__(self, name, val): delattr(self, name) self.add_component(name, val) else: - super(_BlockData, self).__setattr__(name, val) + super(BlockData, self).__setattr__(name, val) def __delattr__(self, name): """ @@ -712,7 +703,7 @@ def __delattr__(self, name): # Other Python objects are removed with the standard __detattr__ # method. # - super(_BlockData, self).__delattr__(name) + super(BlockData, self).__delattr__(name) def _compact_decl_storage(self): idxMap = {} @@ -784,11 +775,11 @@ def transfer_attributes_from(self, src): Parameters ---------- - src: _BlockData or dict + src: BlockData or dict The Block or mapping that contains the new attributes to assign to this block. """ - if isinstance(src, _BlockData): + if isinstance(src, BlockData): # There is a special case where assigning a parent block to # this block creates a circular hierarchy if src is self: @@ -797,7 +788,7 @@ def transfer_attributes_from(self, src): while p_block is not None: if p_block is src: raise ValueError( - "_BlockData.transfer_attributes_from(): Cannot set a " + "BlockData.transfer_attributes_from(): Cannot set a " "sub-block (%s) to a parent block (%s): creates a " "circular hierarchy" % (self, src) ) @@ -813,7 +804,7 @@ def transfer_attributes_from(self, src): del_src_comp = lambda x: None else: raise ValueError( - "_BlockData.transfer_attributes_from(): expected a " + "BlockData.transfer_attributes_from(): expected a " "Block or dict; received %s" % (type(src).__name__,) ) @@ -846,47 +837,6 @@ def transfer_attributes_from(self, src): ): setattr(self, k, v) - def _add_implicit_sets(self, val): - """TODO: This method has known issues (see tickets) and needs to be - reviewed. [JDS 9/2014]""" - - _component_sets = getattr(val, '_implicit_subsets', None) - # - # FIXME: The name attribute should begin with "_", and None - # should replace "_unknown_" - # - if _component_sets is not None: - for ctr, tset in enumerate(_component_sets): - if tset.parent_component().parent_block() is None and not isinstance( - tset.parent_component(), GlobalSetBase - ): - self.add_component("%s_index_%d" % (val.local_name, ctr), tset) - if ( - getattr(val, '_index_set', None) is not None - and isinstance(val._index_set, _SetDataBase) - and val._index_set.parent_component().parent_block() is None - and not isinstance(val._index_set.parent_component(), GlobalSetBase) - ): - self.add_component( - "%s_index" % (val.local_name,), val._index_set.parent_component() - ) - if ( - getattr(val, 'initialize', None) is not None - and isinstance(val.initialize, _SetDataBase) - and val.initialize.parent_component().parent_block() is None - and not isinstance(val.initialize.parent_component(), GlobalSetBase) - ): - self.add_component( - "%s_index_init" % (val.local_name,), val.initialize.parent_component() - ) - if ( - getattr(val, 'domain', None) is not None - and isinstance(val.domain, _SetDataBase) - and val.domain.parent_block() is None - and not isinstance(val.domain, GlobalSetBase) - ): - self.add_component("%s_domain" % (val.local_name,), val.domain) - def collect_ctypes(self, active=None, descend_into=True): """ Count all component types stored on or under this @@ -928,7 +878,7 @@ def collect_ctypes(self, active=None, descend_into=True): def model(self): # - # Special case: the "Model" is always the top-level _BlockData, + # Special case: the "Model" is always the top-level BlockData, # so if this is the top-level block, it must be the model # # Also note the interesting and intentional characteristic for @@ -1066,16 +1016,11 @@ def add_component(self, name, val): val._parent = weakref.ref(self) val._name = name # - # We want to add the temporary / implicit sets first so that - # they get constructed before this component - # - # FIXME: This is sloppy and wasteful (most components trigger - # this, even when there is no need for it). We should - # reconsider the whole _implicit_subsets logic to defer this - # kind of thing to an "update_parent()" method on the - # components. + # Update the context of any anonymous sets # - self._add_implicit_sets(val) + if getattr(val, '_anonymous_sets', None) is not None: + for _set in val._anonymous_sets: + _set._parent = val._parent # # Add the component to the underlying Component store # @@ -1090,7 +1035,7 @@ def add_component(self, name, val): # is inappropriate here. The correct way to add the attribute # is to delegate the work to the next class up the MRO. # - super(_BlockData, self).__setattr__(name, val) + super(BlockData, self).__setattr__(name, val) # # Update the ctype linked lists # @@ -1112,26 +1057,13 @@ def add_component(self, name, val): # Error, for disabled support implicit rule names # if '_rule' in val.__dict__ and val._rule is None: - _found = False try: _test = val.local_name + '_rule' for i in (1, 2): frame = sys._getframe(i) - _found |= _test in frame.f_locals except: pass - if _found: - # JDS: Do not blindly reformat this message. The - # formatter inserts arbitrarily-long names(), which can - # cause the resulting logged message to be very poorly - # formatted due to long lines. - logger.warning( - """As of Pyomo 4.0, Pyomo components no longer support implicit rules. -You defined a component (%s) that appears -to rely on an implicit rule (%s). -Components must now specify their rules explicitly using 'rule=' keywords.""" - % (val.name, _test) - ) + # # Don't reconstruct if this component has already been constructed. # This allows a user to move a component from one block to @@ -1148,9 +1080,8 @@ def add_component(self, name, val): # added to the class by Block.__init__() # if getattr(_component, '_constructed', False): - # NB: we don't have to construct the temporary / implicit - # sets here: if necessary, that happens when - # _add_implicit_sets() calls add_component(). + # NB: we don't have to construct the anonymous sets here: if + # necessary, that happens in component.construct() if _BlockConstruction.data: data = _BlockConstruction.data.get(id(self), None) if data is not None: @@ -1162,7 +1093,7 @@ def add_component(self, name, val): # This is tricky: If we are in the middle of # constructing an indexed block, the block component # already has _constructed=True. Now, if the - # _BlockData.__init__() defines any local variables + # BlockData.__init__() defines any local variables # (like pyomo.gdp.Disjunct's indicator_var), name(True) # will fail: this block data exists and has a parent(), # but it has not yet been added to the parent's _data @@ -1186,11 +1117,12 @@ def add_component(self, name, val): except: err = sys.exc_info()[1] logger.error( - "Constructing component '%s' from data=%s failed:\n%s: %s", + "Constructing component '%s' from data=%s failed:\n %s: %s", str(val.name), str(data).strip(), type(err).__name__, err, + extra={'cleandoc': False}, ) raise if generate_debug_messages: @@ -1236,6 +1168,10 @@ def del_component(self, name_or_object): # Clear the _parent attribute obj._parent = None + # Update the context of any anonymous sets + if getattr(obj, '_anonymous_sets', None) is not None: + for _set in obj._anonymous_sets: + _set._parent = None # Now that this component is not in the _decl map, we can call # delattr as usual. @@ -1245,7 +1181,7 @@ def del_component(self, name_or_object): # Note: 'del self.__dict__[name]' is inappropriate here. The # correct way to add the attribute is to delegate the work to # the next class up the MRO. - super(_BlockData, self).__delattr__(name) + super(BlockData, self).__delattr__(name) def reclassify_component_type( self, name_or_object, new_ctype, preserve_declaration_order=True @@ -1450,7 +1386,7 @@ def _component_data_iteritems(self, ctype, active, sort, dedup): Generator that returns a nested 2-tuple of - ((component name, index value), _ComponentData) + ((component name, index value), ComponentData) for every component data in the block matching the specified ctype(s). @@ -1467,7 +1403,7 @@ def _component_data_iteritems(self, ctype, active, sort, dedup): Iterate over the components in a specified sorted order dedup: _DeduplicateInfo - Deduplicator to prevent returning the same _ComponentData twice + Deduplicator to prevent returning the same ComponentData twice """ for name, comp in PseudoMap(self, ctype, active, sort).items(): # NOTE: Suffix has a dict interface (something other derived @@ -1503,7 +1439,7 @@ def _component_data_iteritems(self, ctype, active, sort, dedup): yield from dedup.unique(comp, _items, False) def _component_data_itervalues(self, ctype, active, sort, dedup): - """Generator that returns the _ComponentData for every component data + """Generator that returns the ComponentData for every component data in the block. Parameters @@ -1518,7 +1454,7 @@ def _component_data_itervalues(self, ctype, active, sort, dedup): Iterate over the components in a specified sorted order dedup: _DeduplicateInfo - Deduplicator to prevent returning the same _ComponentData twice + Deduplicator to prevent returning the same ComponentData twice """ for comp in PseudoMap(self, ctype, active, sort).values(): # NOTE: Suffix has a dict interface (something other derived @@ -1624,7 +1560,7 @@ def component_data_iterindex( generator recursively descends into sub-blocks. The tuple is - ((component name, index value), _ComponentData) + ((component name, index value), ComponentData) """ dedup = _DeduplicateInfo() @@ -1934,7 +1870,14 @@ def valid_problem_types(self): Model object.""" return [ProblemFormat.pyomo] - def write(self, filename=None, format=None, solver_capability=None, io_options={}): + def write( + self, + filename=None, + format=None, + solver_capability=None, + io_options={}, + int_marker=False, + ): """ Write the model to a file, with a given format. """ @@ -1968,7 +1911,8 @@ def write(self, filename=None, format=None, solver_capability=None, io_options={ "Filename '%s' likely does not match specified " "file format (%s)" % (filename, format) ) - problem_writer = WriterFactory(format) + int_marker_kwds = {"int_marker": int_marker} if int_marker else {} + problem_writer = WriterFactory(format, **int_marker_kwds) if problem_writer is None: raise ValueError( "Cannot write model in format '%s': no model " @@ -2083,6 +2027,28 @@ def _create_objects_for_deepcopy(self, memo, component_list): comp._create_objects_for_deepcopy(memo, component_list) return _ans + def private_data(self, scope=None): + mod = currentframe().f_back.f_globals['__name__'] + if scope is None: + scope = mod + elif not mod.startswith(scope): + raise ValueError( + "All keys in the 'private_data' dictionary must " + "be substrings of the caller's module name. " + "Received '%s' when calling private_data on Block " + "'%s'." % (scope, self.name) + ) + if self._private_data is None: + self._private_data = {} + if scope not in self._private_data: + self._private_data[scope] = Block._private_data_initializers[scope]() + return self._private_data[scope] + + +class _BlockData(metaclass=RenamedClass): + __renamed__new_class__ = BlockData + __renamed__version__ = '6.7.2' + @ModelComponentFactory.register( "A component that contains one or more model components." @@ -2097,7 +2063,19 @@ class Block(ActiveIndexedComponent): is deferred. """ - _ComponentDataClass = _BlockData + _ComponentDataClass = BlockData + _private_data_initializers = defaultdict(lambda: dict) + + @overload + def __new__( + cls: Type[Block], *args, **kwds + ) -> Union[ScalarBlock, IndexedBlock]: ... + + @overload + def __new__(cls: Type[ScalarBlock], *args, **kwds) -> ScalarBlock: ... + + @overload + def __new__(cls: Type[IndexedBlock], *args, **kwds) -> IndexedBlock: ... def __new__(cls, *args, **kwds): if cls != Block: @@ -2111,8 +2089,7 @@ def __new__(cls, *args, **kwds): @overload def __init__( self, *indexes, rule=None, concrete=False, dense=True, name=None, doc=None - ): - ... + ): ... def __init__(self, *args, **kwargs): """Constructor""" @@ -2179,7 +2156,7 @@ def _getitem_when_not_present(self, idx): # components declared by the rule have the opportunity # to be initialized with data from # _BlockConstruction.data as they are transferred over. - if obj is not _block and isinstance(obj, _BlockData): + if obj is not _block and isinstance(obj, BlockData): _block.transfer_attributes_from(obj) finally: if data is not None and _block is not self: @@ -2194,6 +2171,11 @@ def construct(self, data=None): """ Initialize the block """ + if self._constructed: + return + self._constructed = True + + timer = ConstructionTimer(self) if is_debug_set(logger): logger.debug( "Constructing %s '%s', from data=%s", @@ -2201,10 +2183,10 @@ def construct(self, data=None): self.name, str(data), ) - if self._constructed: - return - timer = ConstructionTimer(self) - self._constructed = True + + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() # Constructing blocks is tricky. Scalar blocks are already # partially constructed (they have _data[None] == self) in order @@ -2295,12 +2277,29 @@ def display(self, filename=None, ostream=None, prefix=""): ostream = sys.stdout for key in sorted(self): - _BlockData.display(self[key], filename, ostream, prefix) + BlockData.display(self[key], filename, ostream, prefix) + + @staticmethod + def register_private_data_initializer(initializer, scope=None): + mod = currentframe().f_back.f_globals['__name__'] + if scope is None: + scope = mod + elif not mod.startswith(scope): + raise ValueError( + "'private_data' scope must be substrings of the caller's module name. " + f"Received '{scope}' when calling register_private_data_initializer()." + ) + if scope in Block._private_data_initializers: + raise RuntimeError( + "Duplicate initializer registration for 'private_data' dictionary " + f"(scope={scope})" + ) + Block._private_data_initializers[scope] = initializer -class ScalarBlock(_BlockData, Block): +class ScalarBlock(BlockData, Block): def __init__(self, *args, **kwds): - _BlockData.__init__(self, component=self) + BlockData.__init__(self, component=self) Block.__init__(self, *args, **kwds) # Initialize the data dict so that (abstract) attribute # assignment will work. Note that we do not trigger @@ -2322,6 +2321,11 @@ class IndexedBlock(Block): def __init__(self, *args, **kwds): Block.__init__(self, *args, **kwds) + @overload + def __getitem__(self, index) -> BlockData: ... + + __getitem__ = IndexedComponent.__getitem__ # type: ignore + # # Deprecated functions. @@ -2377,101 +2381,120 @@ def components_data(block, ctype, sort=None, sort_by_keys=False, sort_by_names=F # Create a Block and record all the default attributes, methods, etc. # These will be assumed to be the set of illegal component names. # -_BlockData._Block_reserved_words = set(dir(Block())) +BlockData._Block_reserved_words = set(dir(Block())) -class _IndexedCustomBlockMeta(type): - """Metaclass for creating an indexed custom block.""" - - pass - - -class _ScalarCustomBlockMeta(type): - """Metaclass for creating a scalar custom block.""" - - def __new__(meta, name, bases, dct): - def __init__(self, *args, **kwargs): - # bases[0] is the custom block data object - bases[0].__init__(self, component=self) - # bases[1] is the custom block object that - # is used for declaration - bases[1].__init__(self, *args, **kwargs) - - dct["__init__"] = __init__ - return type.__new__(meta, name, bases, dct) +class ScalarCustomBlockMixin(object): + def __init__(self, *args, **kwargs): + # __bases__ for the ScalarCustomBlock is + # + # (ScalarCustomBlockMixin, {custom_data}, {custom_block}) + # + # Unfortunately, we cannot guarantee that this is being called + # from the ScalarCustomBlock (someone could have inherited from + # that class to make another scalar class). We will walk up the + # MRO to find the Scalar class (which should be the only class + # that has this Mixin as the first base class) + for cls in self.__class__.__mro__: + if cls.__bases__[0] is ScalarCustomBlockMixin: + _mixin, _data, _block = cls.__bases__ + _data.__init__(self, component=self) + _block.__init__(self, *args, **kwargs) + break class CustomBlock(Block): """The base class used by instances of custom block components""" - def __init__(self, *args, **kwds): + def __init__(self, *args, **kwargs): if self._default_ctype is not None: - kwds.setdefault('ctype', self._default_ctype) - Block.__init__(self, *args, **kwds) - - def __new__(cls, *args, **kwds): - if cls.__name__.startswith('_Indexed') or cls.__name__.startswith('_Scalar'): - # we are entering here the second time (recursive) - # therefore, we need to create what we have - return super(CustomBlock, cls).__new__(cls) + kwargs.setdefault('ctype', self._default_ctype) + Block.__init__(self, *args, **kwargs) + + def __new__(cls, *args, **kwargs): + if cls.__bases__[0] is not CustomBlock: + # we are creating a class other than the "generic" derived + # custom block class. We can assume that the routing of the + # generic block class to the specific Scalar or Indexed + # subclass has already occurred and we can pass control up + # to (toward) object.__new__() + return super().__new__(cls, *args, **kwargs) + # If the first base class is this CustomBlock class, then the + # user is attempting to create the "generic" block class. + # Depending on the arguments, we need to map this to either the + # Scalar or Indexed block subclass. if not args or (args[0] is UnindexedComponent_set and len(args) == 1): - n = _ScalarCustomBlockMeta( - "_Scalar%s" % (cls.__name__,), (cls._ComponentDataClass, cls), {} - ) - return n.__new__(n) + return super().__new__(cls._scalar_custom_block, *args, **kwargs) else: - n = _IndexedCustomBlockMeta("_Indexed%s" % (cls.__name__,), (cls,), {}) - return n.__new__(n) + return super().__new__(cls._indexed_custom_block, *args, **kwargs) def declare_custom_block(name, new_ctype=None): """Decorator to declare components for a custom block data class - >>> @declare_custom_block(name=FooBlock) - ... class FooBlockData(_BlockData): + >>> @declare_custom_block(name="FooBlock") + ... class FooBlockData(BlockData): ... # custom block data class ... pass """ - def proc_dec(cls): - # this is the decorator function that - # creates the block component class + def block_data_decorator(block_data): + # this is the decorator function that creates the block + # component classes - # Default (derived) Block attributes - clsbody = { - "__module__": cls.__module__, # magic to fix the module - # Default IndexedComponent data object is the decorated class: - "_ComponentDataClass": cls, - # By default this new block does not declare a new ctype - "_default_ctype": None, - } - - c = type( + # Declare the new Block component (derived from CustomBlock) + # corresponding to the BlockData that we are decorating + # + # Note the use of `type(CustomBlock)` to pick up the metaclass + # that was used to create the CustomBlock (in general, it should + # be `type`) + comp = type(CustomBlock)( name, # name of new class (CustomBlock,), # base classes - clsbody, # class body definitions (will populate __dict__) + # class body definitions (populate the new class' __dict__) + { + # ensure the created class is associated with the calling module + "__module__": block_data.__module__, + # Default IndexedComponent data object is the decorated class: + "_ComponentDataClass": block_data, + # By default this new block does not declare a new ctype + "_default_ctype": None, + }, ) if new_ctype is not None: if new_ctype is True: - c._default_ctype = c - elif type(new_ctype) is type: - c._default_ctype = new_ctype + comp._default_ctype = comp + elif isinstance(new_ctype, type): + comp._default_ctype = new_ctype else: raise ValueError( "Expected new_ctype to be either type " "or 'True'; received: %s" % (new_ctype,) ) - # Register the new Block type in the same module as the BlockData - setattr(sys.modules[cls.__module__], name, c) - # TODO: can we also register concrete Indexed* and Scalar* - # classes into the original BlockData module (instead of relying - # on metaclasses)? + # Declare Indexed and Scalar versions of the custom block. We + # will register them both with the calling module scope, and + # with the CustomBlock (so that CustomBlock.__new__ can route + # the object creation to the correct class) + comp._indexed_custom_block = type(comp)( + "Indexed" + name, + (comp,), + { # ensure the created class is associated with the calling module + "__module__": block_data.__module__ + }, + ) + comp._scalar_custom_block = type(comp)( + "Scalar" + name, + (ScalarCustomBlockMixin, block_data, comp), + { # ensure the created class is associated with the calling module + "__module__": block_data.__module__ + }, + ) - # are these necessary? - setattr(cls, '_orig_name', name) - setattr(cls, '_orig_module', cls.__module__) - return cls + # Register the new Block types in the same module as the BlockData + for _cls in (comp, comp._indexed_custom_block, comp._scalar_custom_block): + setattr(sys.modules[block_data.__module__], _cls.__name__, _cls) + return block_data - return proc_dec + return block_data_decorator diff --git a/pyomo/core/base/blockutil.py b/pyomo/core/base/blockutil.py index 21e6ac4db90..d91a5c85ac2 100644 --- a/pyomo/core/base/blockutil.py +++ b/pyomo/core/base/blockutil.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -12,8 +12,6 @@ # the purpose of this file is to collect all utility methods that compute # attributes of blocks, based on their contents. -__all__ = ['has_discrete_variables'] - from pyomo.common import deprecated from pyomo.core.base import Var diff --git a/pyomo/core/base/boolean_var.py b/pyomo/core/base/boolean_var.py index e2aebb4e466..db9a41fceda 100644 --- a/pyomo/core/base/boolean_var.py +++ b/pyomo/core/base/boolean_var.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -68,27 +68,65 @@ def __setstate__(self, state): self._boolvar = weakref_ref(state) -class _BooleanVarData(ComponentData, BooleanValue): - """ - This class defines the data for a single variable. - - Constructor Arguments: - component The BooleanVar object that owns this data. - Public Class Attributes: - fixed If True, then this variable is treated as a - fixed constant in the model. - stale A Boolean indicating whether the value of this variable is - legitimate. This value is true if the value should - be considered legitimate for purposes of reporting or - other interrogation. - value The numeric value of this variable. +def _associated_binary_mapper(encode, val): + if val is None: + return None + if encode: + if val.__class__ is not _DeprecatedImplicitAssociatedBinaryVariable: + return val() + else: + if val.__class__ is not _DeprecatedImplicitAssociatedBinaryVariable: + return weakref_ref(val) + return val + + +class BooleanVarData(ComponentData, BooleanValue): + """This class defines the data for a single Boolean variable. + + Parameters + ---------- + component: Component + The BooleanVar object that owns this data. + + Attributes + ---------- + domain: SetData + The domain of this variable. + + fixed: bool + If True, then this variable is treated as a fixed constant in + the model. + + stale: bool + A Boolean indicating whether the value of this variable is + Consistent with the most recent solve. `True` indicates that + this variable's value was set prior to the most recent solve and + was not updated by the results returned by the solve. + + value: bool + The value of this variable. """ - __slots__ = () + __slots__ = ('_value', 'fixed', '_stale', '_associated_binary') + __autoslot_mappers__ = { + '_associated_binary': _associated_binary_mapper, + '_stale': StaleFlagManager.stale_mapper, + } def __init__(self, component=None): + # + # These lines represent in-lining of the + # following constructors: + # - BooleanVarData + # - ComponentData + # - BooleanValue self._component = weakref_ref(component) if (component is not None) else None self._index = NOTSET + self._value = None + self.fixed = False + self._stale = 0 # True + + self._associated_binary = None def is_fixed(self): """Returns True if this variable is fixed, otherwise returns False.""" @@ -132,113 +170,6 @@ def __call__(self, exception=True): """Compute the value of this variable.""" return self.value - @property - def value(self): - """Return the value for this variable.""" - raise NotImplementedError - - @property - def domain(self): - """Return the domain for this variable.""" - raise NotImplementedError - - @property - def fixed(self): - """Return the fixed indicator for this variable.""" - raise NotImplementedError - - @property - def stale(self): - """Return the stale indicator for this variable.""" - raise NotImplementedError - - def fix(self, value=NOTSET, skip_validation=False): - """Fix the value of this variable (treat as nonvariable) - - This sets the `fixed` indicator to True. If ``value`` is - provided, the value (and the ``skip_validation`` flag) are first - passed to :py:meth:`set_value()`. - - """ - self.fixed = True - if value is not NOTSET: - self.set_value(value, skip_validation) - - def unfix(self): - """Unfix this variable (treat as variable) - - This sets the `fixed` indicator to False. - - """ - self.fixed = False - - def free(self): - """Alias for :py:meth:`unfix`""" - return self.unfix() - - -def _associated_binary_mapper(encode, val): - if val is None: - return None - if encode: - if val.__class__ is not _DeprecatedImplicitAssociatedBinaryVariable: - return val() - else: - if val.__class__ is not _DeprecatedImplicitAssociatedBinaryVariable: - return weakref_ref(val) - return val - - -class _GeneralBooleanVarData(_BooleanVarData): - """ - This class defines the data for a single Boolean variable. - - Constructor Arguments: - component The BooleanVar object that owns this data. - - Public Class Attributes: - domain The domain of this variable. - fixed If True, then this variable is treated as a - fixed constant in the model. - stale A Boolean indicating whether the value of this variable is - legitimiate. This value is true if the value should - be considered legitimate for purposes of reporting or - other interrogation. - value The numeric value of this variable. - - The domain attribute is a property because it is - too widely accessed directly to enforce explicit getter/setter - methods and we need to deter directly modifying or accessing - these attributes in certain cases. - """ - - __slots__ = ('_value', 'fixed', '_stale', '_associated_binary') - __autoslot_mappers__ = { - '_associated_binary': _associated_binary_mapper, - '_stale': StaleFlagManager.stale_mapper, - } - - def __init__(self, component=None): - # - # These lines represent in-lining of the - # following constructors: - # - _BooleanVarData - # - ComponentData - # - BooleanValue - self._component = weakref_ref(component) if (component is not None) else None - self._index = NOTSET - self._value = None - self.fixed = False - self._stale = 0 # True - - self._associated_binary = None - - # - # Abstract Interface - # - - # value is an attribute - @property def value(self): """Return (or set) the value for this variable.""" @@ -265,14 +196,14 @@ def stale(self, val): self._stale = StaleFlagManager.get_flag(0) def get_associated_binary(self): - """Get the binary _VarData associated with this - _GeneralBooleanVarData""" + """Get the binary VarData associated with this + BooleanVarData""" return ( self._associated_binary() if self._associated_binary is not None else None ) def associate_binary_var(self, binary_var): - """Associate a binary _VarData to this _GeneralBooleanVarData""" + """Associate a binary VarData to this BooleanVarData""" if ( self._associated_binary is not None and type(self._associated_binary) @@ -283,15 +214,51 @@ def associate_binary_var(self, binary_var): "with '%s') with '%s' is not allowed" % ( self.name, - self._associated_binary().name - if self._associated_binary is not None - else None, + ( + self._associated_binary().name + if self._associated_binary is not None + else None + ), binary_var.name if binary_var is not None else None, ) ) if binary_var is not None: self._associated_binary = weakref_ref(binary_var) + def fix(self, value=NOTSET, skip_validation=False): + """Fix the value of this variable (treat as nonvariable) + + This sets the `fixed` indicator to True. If ``value`` is + provided, the value (and the ``skip_validation`` flag) are first + passed to :py:meth:`set_value()`. + + """ + self.fixed = True + if value is not NOTSET: + self.set_value(value, skip_validation) + + def unfix(self): + """Unfix this variable (treat as variable) + + This sets the `fixed` indicator to False. + + """ + self.fixed = False + + def free(self): + """Alias for :py:meth:`unfix`""" + return self.unfix() + + +class _BooleanVarData(metaclass=RenamedClass): + __renamed__new_class__ = BooleanVarData + __renamed__version__ = '6.7.2' + + +class _GeneralBooleanVarData(metaclass=RenamedClass): + __renamed__new_class__ = BooleanVarData + __renamed__version__ = '6.7.2' + @ModelComponentFactory.register("Logical decision variables.") class BooleanVar(IndexedComponent): @@ -307,7 +274,7 @@ class BooleanVar(IndexedComponent): to True. """ - _ComponentDataClass = _GeneralBooleanVarData + _ComponentDataClass = BooleanVarData def __new__(cls, *args, **kwds): if cls != BooleanVar: @@ -383,8 +350,12 @@ def construct(self, data=None): timer = ConstructionTimer(self) self._constructed = True + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() + # - # Construct _BooleanVarData objects for all index values + # Construct BooleanVarData objects for all index values # if not self.is_indexed(): self._data[None] = self @@ -495,12 +466,11 @@ def _pprint(self): ) -class ScalarBooleanVar(_GeneralBooleanVarData, BooleanVar): - +class ScalarBooleanVar(BooleanVarData, BooleanVar): """A single variable.""" def __init__(self, *args, **kwd): - _GeneralBooleanVarData.__init__(self, component=self) + BooleanVarData.__init__(self, component=self) BooleanVar.__init__(self, *args, **kwd) self._index = UnindexedComponent_index @@ -516,7 +486,7 @@ def __init__(self, *args, **kwd): def value(self): """Return the value for this variable.""" if self._constructed: - return _GeneralBooleanVarData.value.fget(self) + return BooleanVarData.value.fget(self) raise ValueError( "Accessing the value of variable '%s' " "before the Var has been constructed (there " @@ -527,7 +497,7 @@ def value(self): def value(self, val): """Set the value for this variable.""" if self._constructed: - return _GeneralBooleanVarData.value.fset(self, val) + return BooleanVarData.value.fset(self, val) raise ValueError( "Setting the value of variable '%s' " "before the Var has been constructed (there " @@ -536,7 +506,7 @@ def value(self, val): @property def domain(self): - return _GeneralBooleanVarData.domain.fget(self) + return BooleanVarData.domain.fget(self) def fix(self, value=NOTSET, skip_validation=False): """ @@ -544,7 +514,7 @@ def fix(self, value=NOTSET, skip_validation=False): indicating the variable should be fixed at its current value. """ if self._constructed: - return _GeneralBooleanVarData.fix(self, value, skip_validation) + return BooleanVarData.fix(self, value, skip_validation) raise ValueError( "Fixing variable '%s' " "before the Var has been constructed (there " @@ -554,7 +524,7 @@ def fix(self, value=NOTSET, skip_validation=False): def unfix(self): """Sets the fixed indicator to False.""" if self._constructed: - return _GeneralBooleanVarData.unfix(self) + return BooleanVarData.unfix(self) raise ValueError( "Freeing variable '%s' " "before the Var has been constructed (there " diff --git a/pyomo/core/base/check.py b/pyomo/core/base/check.py index 0e9d8e889b2..485d1a73b6b 100644 --- a/pyomo/core/base/check.py +++ b/pyomo/core/base/check.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['BuildCheck'] - import logging import types diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index a8550f8f469..966ce8c0737 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -20,6 +20,7 @@ from pyomo.common.autoslots import AutoSlots, fast_deepcopy from pyomo.common.collections import OrderedDict from pyomo.common.deprecation import ( + RenamedClass, deprecated, deprecation_warning, relocated_module_attribute, @@ -79,7 +80,7 @@ class CloneError(pyomo.common.errors.PyomoException): pass -class _ComponentBase(PyomoObject): +class ComponentBase(PyomoObject): """A base class for Component and ComponentData This class defines some fundamental methods and properties that are @@ -368,7 +369,7 @@ def pprint(self, ostream=None, verbose=False, prefix=""): @property def name(self): - """Get the fully qualifed component name.""" + """Get the fully qualified component name.""" return self.getname(fully_qualified=True) # Adding a setter here to help users adapt to the new @@ -474,7 +475,12 @@ def _pprint_base_impl( ostream.write(_data) -class Component(_ComponentBase): +class _ComponentBase(metaclass=RenamedClass): + __renamed__new_class__ = ComponentBase + __renamed__version__ = '6.7.2' + + +class Component(ComponentBase): """ This is the base class for all Pyomo modeling components. @@ -501,7 +507,7 @@ def __init__(self, **kwds): # self._ctype = kwds.pop('ctype', None) self.doc = kwds.pop('doc', None) - self._name = kwds.pop('name', str(type(self).__name__)) + self._name = kwds.pop('name', None) if kwds: raise ValueError( "Unexpected keyword options found while constructing '%s':\n\t%s" @@ -625,6 +631,8 @@ def getname(self, fully_qualified=False, name_buffer=None, relative_to=None): Generate fully_qualified names relative to the specified block. """ local_name = self._name + if local_name is None: + local_name = type(self).__name__ if fully_qualified: pb = self.parent_block() if relative_to is None: @@ -655,14 +663,14 @@ def getname(self, fully_qualified=False, name_buffer=None, relative_to=None): "use of this argument poses risks if the buffer contains " "names relative to different Blocks in the model hierarchy or " "a mixture of local and fully_qualified names.", - version='TODO', + version='6.4.1', ) name_buffer[id(self)] = ans return ans @property def name(self): - """Get the fully qualifed component name.""" + """Get the fully qualified component name.""" return self.getname(fully_qualified=True) # Allow setting a component's name if it is not owned by a parent @@ -723,6 +731,27 @@ def get_suffix_value(self, suffix_or_name, default=None): else: return suffix_or_name.get(self, default) + def _pop_from_kwargs(self, name, kwargs, namelist, notset=None): + args = [ + arg + for arg in (kwargs.pop(name, notset) for name in namelist) + if arg is not notset + ] + if len(args) == 1: + return args[0] + elif not args: + return notset + else: + argnames = "%s%s '%s='" % ( + ', '.join("'%s='" % _ for _ in namelist[:-1]), + ',' if len(namelist) > 2 else '', + namelist[-1], + ) + raise ValueError( + "Duplicate initialization: %s() only accepts one of %s" + % (name, argnames) + ) + class ActiveComponent(Component): """A Component that makes semantic sense to activate or deactivate @@ -756,7 +785,7 @@ def deactivate(self): self._active = False -class ComponentData(_ComponentBase): +class ComponentData(ComponentBase): """ This is the base class for the component data used in Pyomo modeling components. Subclasses of ComponentData are @@ -779,11 +808,11 @@ class ComponentData(_ComponentBase): __autoslot_mappers__ = {'_component': AutoSlots.weakref_mapper} # NOTE: This constructor is in-lined in the constructors for the following - # classes: _BooleanVarData, _ConnectorData, _ConstraintData, - # _GeneralExpressionData, _LogicalConstraintData, - # _GeneralLogicalConstraintData, _GeneralObjectiveData, - # _ParamData,_GeneralVarData, _GeneralBooleanVarData, _DisjunctionData, - # _ArcData, _PortData, _LinearConstraintData, and + # classes: BooleanVarData, ConnectorData, ConstraintData, + # ExpressionData, LogicalConstraintData, + # LogicalConstraintData, ObjectiveData, + # ParamData,VarData, BooleanVarData, DisjunctionData, + # ArcData, PortData, _LinearConstraintData, and # _LinearMatrixConstraintData. Changes made here need to be made in those # constructors as well! def __init__(self, component): @@ -893,7 +922,7 @@ def getname(self, fully_qualified=False, name_buffer=None, relative_to=None): "use of this argument poses risks if the buffer contains " "names relative to different Blocks in the model hierarchy or " "a mixture of local and fully_qualified names.", - version='TODO', + version='6.4.1', ) if id(self) in name_buffer: # Return the name if it is in the buffer diff --git a/pyomo/core/base/component_namer.py b/pyomo/core/base/component_namer.py index 17d46c12fae..c2fa01f6ad5 100644 --- a/pyomo/core/base/component_namer.py +++ b/pyomo/core/base/component_namer.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/component_order.py b/pyomo/core/base/component_order.py index 0685571ccb0..9244828cbe5 100644 --- a/pyomo/core/base/component_order.py +++ b/pyomo/core/base/component_order.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,9 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ - -__all__ = ['items', 'display_items', 'display_name'] - from pyomo.core.base.set import Set, RangeSet from pyomo.core.base.param import Param from pyomo.core.base.var import Var diff --git a/pyomo/core/base/componentuid.py b/pyomo/core/base/componentuid.py index 0ab57d1c253..2075aa197dc 100644 --- a/pyomo/core/base/componentuid.py +++ b/pyomo/core/base/componentuid.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -144,9 +144,11 @@ def __hash__(self): ( name, tuple( - (slice, x.start, x.stop, x.step) - if x.__class__ is slice - else x + ( + (slice, x.start, x.stop, x.step) + if x.__class__ is slice + else x + ) for x in idx ), ) diff --git a/pyomo/core/base/config.py b/pyomo/core/base/config.py index 4c6cc06f90c..14c00522673 100644 --- a/pyomo/core/base/config.py +++ b/pyomo/core/base/config.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/connector.py b/pyomo/core/base/connector.py index f3d4833b837..1363f5abd65 100644 --- a/pyomo/core/base/connector.py +++ b/pyomo/core/base/connector.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['Connector'] - import logging import sys from weakref import ref as weakref_ref @@ -26,12 +24,11 @@ from pyomo.core.base.global_set import UnindexedComponent_index from pyomo.core.base.indexed_component import IndexedComponent from pyomo.core.base.misc import apply_indexed_rule -from pyomo.core.base.transformation import TransformationFactory logger = logging.getLogger('pyomo.core') -class _ConnectorData(ComponentData, NumericValue): +class ConnectorData(ComponentData, NumericValue): """Holds the actual connector information""" __slots__ = ('vars', 'aggregators') @@ -108,6 +105,11 @@ def _iter_vars(self): yield v +class _ConnectorData(metaclass=RenamedClass): + __renamed__new_class__ = ConnectorData + __renamed__version__ = '6.7.2' + + @ModelComponentFactory.register( "A bundle of variables that can be manipulated together." ) @@ -160,7 +162,7 @@ def __init__(self, *args, **kwd): # IndexedComponent # def _getitem_when_not_present(self, idx): - _conval = self._data[idx] = _ConnectorData(component=self) + _conval = self._data[idx] = ConnectorData(component=self) return _conval def construct(self, data=None): @@ -173,7 +175,7 @@ def construct(self, data=None): timer = ConstructionTimer(self) self._constructed = True # - # Construct _ConnectorData objects for all index values + # Construct ConnectorData objects for all index values # if self.is_indexed(): self._initialize_members(self._index_set) @@ -261,9 +263,9 @@ def _line_generator(k, v): ) -class ScalarConnector(Connector, _ConnectorData): +class ScalarConnector(Connector, ConnectorData): def __init__(self, *args, **kwd): - _ConnectorData.__init__(self, component=self) + ConnectorData.__init__(self, component=self) Connector.__init__(self, *args, **kwd) self._index = UnindexedComponent_index diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index 53afa35c70c..e12860991c2 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,20 +9,12 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = [ - 'Constraint', - '_ConstraintData', - 'ConstraintList', - 'simple_constraint_rule', - 'simple_constraintlist_rule', -] - -import io +from __future__ import annotations import sys import logging -import math from weakref import ref as weakref_ref from pyomo.common.pyomo_typing import overload +from typing import Union, Type from pyomo.common.deprecation import RenamedClass from pyomo.common.errors import DeveloperError @@ -37,6 +29,7 @@ as_numeric, is_fixed, native_numeric_types, + native_logical_types, native_types, ) from pyomo.core.expr import ( @@ -51,6 +44,7 @@ ActiveIndexedComponent, UnindexedComponent_set, rule_wrapper, + IndexedComponent, ) from pyomo.core.base.set import Set from pyomo.core.base.disable_methods import disable_methods @@ -94,14 +88,15 @@ def C_rule(model, i, j): model.c = Constraint(rule=simple_constraint_rule(...)) """ - return rule_wrapper( - rule, - { - None: Constraint.Skip, - True: Constraint.Feasible, - False: Constraint.Infeasible, - }, - ) + map_types = set([type(None)]) | native_logical_types + result_map = {None: Constraint.Skip} + for l_type in native_logical_types: + result_map[l_type(True)] = Constraint.Feasible + result_map[l_type(False)] = Constraint.Infeasible + # Note: some logical types hash the same as bool (e.g., np.bool_), so + # we will pass the set of all logical types in addition to the + # result_map + return rule_wrapper(rule, result_map, map_types=map_types) def simple_constraintlist_rule(rule): @@ -119,27 +114,24 @@ def C_rule(model, i, j): model.c = ConstraintList(expr=simple_constraintlist_rule(...)) """ - return rule_wrapper( - rule, - { - None: ConstraintList.End, - True: Constraint.Feasible, - False: Constraint.Infeasible, - }, - ) - - -# -# This class is a pure interface -# - - -class _ConstraintData(ActiveComponentData): + map_types = set([type(None)]) | native_logical_types + result_map = {None: ConstraintList.End} + for l_type in native_logical_types: + result_map[l_type(True)] = Constraint.Feasible + result_map[l_type(False)] = Constraint.Infeasible + # Note: some logical types hash the same as bool (e.g., np.bool_), so + # we will pass the set of all logical types in addition to the + # result_map + return rule_wrapper(rule, result_map, map_types=map_types) + + +class ConstraintData(ActiveComponentData): """ - This class defines the data for a single constraint. + This class defines the data for a single algebraic constraint. Constructor arguments: component The Constraint object that owns this data. + expr The Pyomo expression stored in this constraint. Public class attributes: active A boolean that is true if this constraint is @@ -159,164 +151,17 @@ class _ConstraintData(ActiveComponentData): _active A boolean that indicates whether this data is active """ - __slots__ = () + __slots__ = ('_body', '_lower', '_upper', '_expr') # Set to true when a constraint class stores its expression # in linear canonical form _linear_canonical_form = False - def __init__(self, component=None): - # - # These lines represent in-lining of the - # following constructors: - # - _ConstraintData, - # - ActiveComponentData - # - ComponentData - self._component = weakref_ref(component) if (component is not None) else None - self._index = NOTSET - self._active = True - - # - # Interface - # - - def __call__(self, exception=True): - """Compute the value of the body of this constraint.""" - return value(self.body, exception=exception) - - def has_lb(self): - """Returns :const:`False` when the lower bound is - :const:`None` or negative infinity""" - return self.lb is not None - - def has_ub(self): - """Returns :const:`False` when the upper bound is - :const:`None` or positive infinity""" - return self.ub is not None - - def lslack(self): - """ - Returns the value of f(x)-L for constraints of the form: - L <= f(x) (<= U) - (U >=) f(x) >= L - """ - lb = self.lb - if lb is None: - return _inf - else: - return value(self.body) - lb - - def uslack(self): - """ - Returns the value of U-f(x) for constraints of the form: - (L <=) f(x) <= U - U >= f(x) (>= L) - """ - ub = self.ub - if ub is None: - return _inf - else: - return ub - value(self.body) - - def slack(self): - """ - Returns the smaller of lslack and uslack values - """ - lb = self.lb - ub = self.ub - body = value(self.body) - if lb is None: - return ub - body - elif ub is None: - return body - lb - return min(ub - body, body - lb) - - # - # Abstract Interface - # - - @property - def body(self): - """Access the body of a constraint expression.""" - raise NotImplementedError - - @property - def lower(self): - """Access the lower bound of a constraint expression.""" - raise NotImplementedError - - @property - def upper(self): - """Access the upper bound of a constraint expression.""" - raise NotImplementedError - - @property - def lb(self): - """Access the value of the lower bound of a constraint expression.""" - raise NotImplementedError - - @property - def ub(self): - """Access the value of the upper bound of a constraint expression.""" - raise NotImplementedError - - @property - def equality(self): - """A boolean indicating whether this is an equality constraint.""" - raise NotImplementedError - - @property - def strict_lower(self): - """True if this constraint has a strict lower bound.""" - raise NotImplementedError - - @property - def strict_upper(self): - """True if this constraint has a strict upper bound.""" - raise NotImplementedError - - def set_value(self, expr): - """Set the expression on this constraint.""" - raise NotImplementedError - - def get_value(self): - """Get the expression on this constraint.""" - raise NotImplementedError - - -class _GeneralConstraintData(_ConstraintData): - """ - This class defines the data for a single general constraint. - - Constructor arguments: - component The Constraint object that owns this data. - expr The Pyomo expression stored in this constraint. - - Public class attributes: - active A boolean that is true if this constraint is - active in the model. - body The Pyomo expression for this constraint - lower The Pyomo expression for the lower bound - upper The Pyomo expression for the upper bound - equality A boolean that indicates whether this is an - equality constraint - strict_lower A boolean that indicates whether this - constraint uses a strict lower bound - strict_upper A boolean that indicates whether this - constraint uses a strict upper bound - - Private class attributes: - _component The objective component. - _active A boolean that indicates whether this data is active - """ - - __slots__ = ('_body', '_lower', '_upper', '_expr') - def __init__(self, expr=None, component=None): # # These lines represent in-lining of the # following constructors: - # - _ConstraintData, + # - ConstraintData, # - ActiveComponentData # - ComponentData self._component = weakref_ref(component) if (component is not None) else None @@ -329,9 +174,9 @@ def __init__(self, expr=None, component=None): if expr is not None: self.set_value(expr) - # - # Abstract Interface - # + def __call__(self, exception=True): + """Compute the value of the body of this constraint.""" + return value(self.body, exception=exception) @property def body(self): @@ -455,6 +300,16 @@ def strict_upper(self): """True if this constraint has a strict upper bound.""" return False + def has_lb(self): + """Returns :const:`False` when the lower bound is + :const:`None` or negative infinity""" + return self.lb is not None + + def has_ub(self): + """Returns :const:`False` when the upper bound is + :const:`None` or positive infinity""" + return self.ub is not None + @property def expr(self): """Return the expression associated with this constraint.""" @@ -682,6 +537,53 @@ def set_value(self, expr): "upper bound (%s)." % (self.name, self._upper) ) + def lslack(self): + """ + Returns the value of f(x)-L for constraints of the form: + L <= f(x) (<= U) + (U >=) f(x) >= L + """ + lb = self.lb + if lb is None: + return _inf + else: + return value(self.body) - lb + + def uslack(self): + """ + Returns the value of U-f(x) for constraints of the form: + (L <=) f(x) <= U + U >= f(x) (>= L) + """ + ub = self.ub + if ub is None: + return _inf + else: + return ub - value(self.body) + + def slack(self): + """ + Returns the smaller of lslack and uslack values + """ + lb = self.lb + ub = self.ub + body = value(self.body) + if lb is None: + return ub - body + elif ub is None: + return body - lb + return min(ub - body, body - lb) + + +class _ConstraintData(metaclass=RenamedClass): + __renamed__new_class__ = ConstraintData + __renamed__version__ = '6.7.2' + + +class _GeneralConstraintData(metaclass=RenamedClass): + __renamed__new_class__ = ConstraintData + __renamed__version__ = '6.7.2' + @ModelComponentFactory.register("General constraint expressions.") class Constraint(ActiveIndexedComponent): @@ -717,8 +619,6 @@ class Constraint(ActiveIndexedComponent): A dictionary from the index set to component data objects _index The set of valid indices - _implicit_subsets - A tuple of set objects that represents the index set _model A weakref to the model that owns this component _parent @@ -727,7 +627,7 @@ class Constraint(ActiveIndexedComponent): The class type for the derived subclass """ - _ComponentDataClass = _GeneralConstraintData + _ComponentDataClass = ConstraintData class Infeasible(object): pass @@ -737,6 +637,17 @@ class Infeasible(object): Violated = Infeasible Satisfied = Feasible + @overload + def __new__( + cls: Type[Constraint], *args, **kwds + ) -> Union[ScalarConstraint, IndexedConstraint]: ... + + @overload + def __new__(cls: Type[ScalarConstraint], *args, **kwds) -> ScalarConstraint: ... + + @overload + def __new__(cls: Type[IndexedConstraint], *args, **kwds) -> IndexedConstraint: ... + def __new__(cls, *args, **kwds): if cls != Constraint: return super(Constraint, cls).__new__(cls) @@ -746,8 +657,7 @@ def __new__(cls, *args, **kwds): return super(Constraint, cls).__new__(IndexedConstraint) @overload - def __init__(self, *indexes, expr=None, rule=None, name=None, doc=None): - ... + def __init__(self, *indexes, expr=None, rule=None, name=None, doc=None): ... def __init__(self, *args, **kwargs): _init = self._pop_from_kwargs('Constraint', kwargs, ('rule', 'expr'), None) @@ -772,6 +682,10 @@ def construct(self, data=None): if is_debug_set(logger): logger.debug("Constructing constraint %s" % (self.name)) + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() + rule = self.rule try: # We do not (currently) accept data for constructing Constraints @@ -871,14 +785,14 @@ def display(self, prefix="", ostream=None): ) -class ScalarConstraint(_GeneralConstraintData, Constraint): +class ScalarConstraint(ConstraintData, Constraint): """ ScalarConstraint is the implementation representing a single, non-indexed constraint. """ def __init__(self, *args, **kwds): - _GeneralConstraintData.__init__(self, component=self, expr=None) + ConstraintData.__init__(self, component=self, expr=None) Constraint.__init__(self, *args, **kwds) self._index = UnindexedComponent_index @@ -889,7 +803,7 @@ def __init__(self, *args, **kwds): # currently in place). So during initialization only, we will # treat them as "indexed" objects where things like # Constraint.Skip are managed. But after that they will behave - # like _ConstraintData objects where set_value does not handle + # like ConstraintData objects where set_value does not handle # Constraint.Skip but expects a valid expression or None. # @property @@ -902,7 +816,7 @@ def body(self): "an expression. There is currently " "nothing to access." % (self.name) ) - return _GeneralConstraintData.body.fget(self) + return ConstraintData.body.fget(self) @property def lower(self): @@ -914,7 +828,7 @@ def lower(self): "an expression. There is currently " "nothing to access." % (self.name) ) - return _GeneralConstraintData.lower.fget(self) + return ConstraintData.lower.fget(self) @property def upper(self): @@ -926,7 +840,7 @@ def upper(self): "an expression. There is currently " "nothing to access." % (self.name) ) - return _GeneralConstraintData.upper.fget(self) + return ConstraintData.upper.fget(self) @property def equality(self): @@ -938,7 +852,7 @@ def equality(self): "an expression. There is currently " "nothing to access." % (self.name) ) - return _GeneralConstraintData.equality.fget(self) + return ConstraintData.equality.fget(self) @property def strict_lower(self): @@ -950,7 +864,7 @@ def strict_lower(self): "an expression. There is currently " "nothing to access." % (self.name) ) - return _GeneralConstraintData.strict_lower.fget(self) + return ConstraintData.strict_lower.fget(self) @property def strict_upper(self): @@ -962,7 +876,7 @@ def strict_upper(self): "an expression. There is currently " "nothing to access." % (self.name) ) - return _GeneralConstraintData.strict_upper.fget(self) + return ConstraintData.strict_upper.fget(self) def clear(self): self._data = {} @@ -1026,6 +940,11 @@ def add(self, index, expr): """Add a constraint with a given index.""" return self.__setitem__(index, expr) + @overload + def __getitem__(self, index) -> ConstraintData: ... + + __getitem__ = IndexedComponent.__getitem__ # type: ignore + @ModelComponentFactory.register("A list of constraint expressions.") class ConstraintList(IndexedConstraint): @@ -1045,8 +964,7 @@ def __init__(self, **kwargs): _rule = kwargs.pop('rule', None) self._starting_index = kwargs.pop('starting_index', 1) - args = (Set(dimen=1),) - super(ConstraintList, self).__init__(*args, **kwargs) + super(ConstraintList, self).__init__(Set(dimen=1), **kwargs) self.rule = Initializer( _rule, treat_sequences_as_mappings=False, allow_generators=True @@ -1068,7 +986,9 @@ def construct(self, data=None): if is_debug_set(logger): logger.debug("Constructing constraint list %s" % (self.name)) - self.index_set().construct() + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() if self.rule is not None: _rule = self.rule(self.parent_block(), ()) diff --git a/pyomo/core/base/disable_methods.py b/pyomo/core/base/disable_methods.py index 61d63d0a385..ff8eb98487a 100644 --- a/pyomo/core/base/disable_methods.py +++ b/pyomo/core/base/disable_methods.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/enums.py b/pyomo/core/base/enums.py index 35cca4e2ac4..31f2212a661 100644 --- a/pyomo/core/base/enums.py +++ b/pyomo/core/base/enums.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -33,7 +33,6 @@ class TraversalStrategy(enum.Enum, **strictEnum): class SortComponents(enum.Flag, **strictEnum): - """ This class is a convenient wrapper for specifying various sort ordering. We pass these objects to the "sort" argument to various @@ -59,7 +58,7 @@ class SortComponents(enum.Flag, **strictEnum): alphabeticalOrder = alphaOrder alphabetical = alphaOrder # both alpha and decl orders are deterministic, so only must sort indices - deterministic = indices + deterministic = ORDERED_INDICES sortBoth = indices | alphabeticalOrder # Same as True alphabetizeComponentAndIndex = sortBoth diff --git a/pyomo/core/base/expression.py b/pyomo/core/base/expression.py index df9abf0a5a5..a5120759236 100644 --- a/pyomo/core/base/expression.py +++ b/pyomo/core/base/expression.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,15 +9,13 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['Expression', '_ExpressionData'] - import sys import logging from weakref import ref as weakref_ref from pyomo.common.pyomo_typing import overload from pyomo.common.log import is_debug_set -from pyomo.common.deprecation import deprecated, RenamedClass +from pyomo.common.deprecation import RenamedClass from pyomo.common.modeling import NOTSET from pyomo.common.formatting import tabular_writer from pyomo.common.timing import ConstructionTimer @@ -32,31 +30,30 @@ from pyomo.core.base.component import ComponentData, ModelComponentFactory from pyomo.core.base.global_set import UnindexedComponent_index from pyomo.core.base.indexed_component import IndexedComponent, UnindexedComponent_set -from pyomo.core.base.misc import apply_indexed_rule from pyomo.core.expr.numvalue import as_numeric from pyomo.core.base.initializer import Initializer logger = logging.getLogger('pyomo.core') -class _ExpressionData(numeric_expr.NumericValue): - """ - An object that defines a named expression. +class NamedExpressionData(numeric_expr.NumericValue): + """An object that defines a generic "named expression". + + This is the base class for both :py:class:`ExpressionData` and + :py:class:`ObjectiveData`. Public Class Attributes expr The expression owned by this data. + """ + # Note: derived classes are expected to declare the _args_ slot __slots__ = () EXPRESSION_SYSTEM = EXPR.ExpressionType.NUMERIC PRECEDENCE = 0 ASSOCIATIVITY = EXPR.OperatorAssociativity.NON_ASSOCIATIVE - # - # Interface - # - def __call__(self, exception=True): """Compute the value of this expression.""" (arg,) = self._args_ @@ -65,6 +62,18 @@ def __call__(self, exception=True): return arg return arg(exception=exception) + def create_node_with_local_data(self, values): + """ + Construct a simple expression after constructing the + contained expression. + + This class provides a consistent interface for constructing a + node, which is used in tree visitor scripts. + """ + obj = self.__class__() + obj._args_ = values + return obj + def is_named_expression_type(self): """A boolean indicating whether this in a named expression.""" return True @@ -113,9 +122,10 @@ def _compute_polynomial_degree(self, result): def _is_fixed(self, values): return values[0] - # - # Abstract Interface - # + # NamedExpressionData should never return False because + # they can store subexpressions that contain variables + def is_potentially_variable(self): + return True @property def expr(self): @@ -128,58 +138,6 @@ def expr(self): def expr(self, value): self.set_value(value) - def set_value(self, expr): - """Set the expression on this expression.""" - raise NotImplementedError - - def is_constant(self): - """A boolean indicating whether this expression is constant.""" - raise NotImplementedError - - def is_fixed(self): - """A boolean indicating whether this expression is fixed.""" - raise NotImplementedError - - # _ExpressionData should never return False because - # they can store subexpressions that contain variables - def is_potentially_variable(self): - return True - - -class _GeneralExpressionDataImpl(_ExpressionData): - """ - An object that defines an expression that is never cloned - - Constructor Arguments - expr The Pyomo expression stored in this expression. - component The Expression object that owns this data. - - Public Class Attributes - expr The expression owned by this data. - """ - - __slots__ = () - - def __init__(self, expr=None): - self._args_ = (expr,) - - def create_node_with_local_data(self, values): - """ - Construct a simple expression after constructing the - contained expression. - - This class provides a consistent interface for constructing a - node, which is used in tree visitor scripts. - """ - obj = ScalarExpression() - obj.construct() - obj._args_ = values - return obj - - # - # Abstract Interface - # - def set_value(self, expr): """Set the expression on this expression.""" if expr is None or expr.__class__ in native_numeric_types: @@ -238,7 +196,17 @@ def __ipow__(self, other): return numeric_expr._pow_dispatcher[e.__class__, other.__class__](e, other) -class _GeneralExpressionData(_GeneralExpressionDataImpl, ComponentData): +class _ExpressionData(metaclass=RenamedClass): + __renamed__new_class__ = NamedExpressionData + __renamed__version__ = '6.7.2' + + +class _GeneralExpressionDataImpl(metaclass=RenamedClass): + __renamed__new_class__ = NamedExpressionData + __renamed__version__ = '6.7.2' + + +class ExpressionData(NamedExpressionData, ComponentData): """ An object that defines an expression that is never cloned @@ -256,12 +224,16 @@ class _GeneralExpressionData(_GeneralExpressionDataImpl, ComponentData): __slots__ = ('_args_',) def __init__(self, expr=None, component=None): - _GeneralExpressionDataImpl.__init__(self, expr) - # Inlining ComponentData.__init__ + self._args_ = (expr,) self._component = weakref_ref(component) if (component is not None) else None self._index = NOTSET +class _GeneralExpressionData(metaclass=RenamedClass): + __renamed__new_class__ = ExpressionData + __renamed__version__ = '6.7.2' + + @ModelComponentFactory.register( "Named expressions that can be used in other expressions." ) @@ -278,7 +250,7 @@ class Expression(IndexedComponent): doc Text describing this component. """ - _ComponentDataClass = _GeneralExpressionData + _ComponentDataClass = ExpressionData # This seems like a copy-paste error, and should be renamed/removed NoConstraint = IndexedComponent.Skip @@ -293,8 +265,7 @@ def __new__(cls, *args, **kwds): @overload def __init__( self, *indexes, rule=None, expr=None, initialize=None, name=None, doc=None - ): - ... + ): ... def __init__(self, *args, **kwds): _init = self._pop_from_kwargs( @@ -394,6 +365,10 @@ def construct(self, data=None): % (self.name, str(data)) ) + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() + try: # We do not (currently) accept data for constructing Constraints assert data is None @@ -402,9 +377,9 @@ def construct(self, data=None): timer.report() -class ScalarExpression(_GeneralExpressionData, Expression): +class ScalarExpression(ExpressionData, Expression): def __init__(self, *args, **kwds): - _GeneralExpressionData.__init__(self, expr=None, component=self) + ExpressionData.__init__(self, expr=None, component=self) Expression.__init__(self, *args, **kwds) self._index = UnindexedComponent_index @@ -427,7 +402,7 @@ def __call__(self, exception=True): def expr(self): """Return expression on this expression.""" if self._constructed: - return _GeneralExpressionData.expr.fget(self) + return ExpressionData.expr.fget(self) raise ValueError( "Accessing the expression of Expression '%s' " "before the Expression has been constructed (there " @@ -445,7 +420,7 @@ def clear(self): def set_value(self, expr): """Set the expression on this expression.""" if self._constructed: - return _GeneralExpressionData.set_value(self, expr) + return ExpressionData.set_value(self, expr) raise ValueError( "Setting the expression of Expression '%s' " "before the Expression has been constructed (there " @@ -455,7 +430,7 @@ def set_value(self, expr): def is_constant(self): """A boolean indicating whether this expression is constant.""" if self._constructed: - return _GeneralExpressionData.is_constant(self) + return ExpressionData.is_constant(self) raise ValueError( "Accessing the is_constant flag of Expression '%s' " "before the Expression has been constructed (there " @@ -465,7 +440,7 @@ def is_constant(self): def is_fixed(self): """A boolean indicating whether this expression is fixed.""" if self._constructed: - return _GeneralExpressionData.is_fixed(self) + return ExpressionData.is_fixed(self) raise ValueError( "Accessing the is_fixed flag of Expression '%s' " "before the Expression has been constructed (there " @@ -509,6 +484,6 @@ def add(self, index, expr): """Add an expression with a given index.""" if (type(expr) is tuple) and (expr == Expression.Skip): return None - cdata = _GeneralExpressionData(expr, component=self) + cdata = ExpressionData(expr, component=self) self._data[index] = cdata return cdata diff --git a/pyomo/core/base/external.py b/pyomo/core/base/external.py index 8157ca4badb..0fda004b664 100644 --- a/pyomo/core/base/external.py +++ b/pyomo/core/base/external.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -31,20 +31,18 @@ from pyomo.common.autoslots import AutoSlots from pyomo.common.fileutils import find_library -from pyomo.core.expr.numvalue import ( +from pyomo.common.numeric_types import ( + check_if_native_type, native_types, native_numeric_types, - pyomo_constant_types, - NonNumericValue, - NumericConstant, value, + _pyomo_constant_types, ) +from pyomo.core.expr.numvalue import NonNumericValue, NumericConstant import pyomo.core.expr as EXPR from pyomo.core.base.component import Component from pyomo.core.base.units_container import units -__all__ = ('ExternalFunction',) - logger = logging.getLogger('pyomo.core') nan = float('nan') @@ -81,12 +79,10 @@ def __new__(cls, *args, **kwargs): return super().__new__(AMPLExternalFunction) @overload - def __init__(self, function=None, gradient=None, hessian=None, *, fgh=None): - ... + def __init__(self, function=None, gradient=None, hessian=None, *, fgh=None): ... @overload - def __init__(self, *, library: str, function: str): - ... + def __init__(self, *, library: str, function: str): ... def __init__(self, *args, **kwargs): """Construct a reference to an external function. @@ -201,14 +197,15 @@ def __call__(self, *args): pv = False for i, arg in enumerate(args_): try: - # Q: Is there a better way to test if a value is an object - # not in native_types and not a standard expression type? if arg.__class__ in native_types: continue if arg.is_potentially_variable(): pv = True + continue except AttributeError: - args_[i] = NonNumericValue(arg) + if check_if_native_type(arg): + continue + args_[i] = NonNumericValue(arg) # if pv: return EXPR.ExternalFunctionExpression(args_, self) @@ -457,9 +454,11 @@ def _pprint(self): ('units', str(self._units)), ( 'arg_units', - [str(u) for u in self._arg_units] - if self._arg_units is not None - else None, + ( + [str(u) for u in self._arg_units] + if self._arg_units is not None + else None + ), ), ], (), @@ -493,7 +492,7 @@ def is_constant(self): return False -pyomo_constant_types.add(_PythonCallbackFunctionID) +_pyomo_constant_types.add(_PythonCallbackFunctionID) class PythonCallbackFunction(ExternalFunction): @@ -609,9 +608,11 @@ def _pprint(self): ('units', str(self._units)), ( 'arg_units', - [str(u) for u in self._arg_units[:-1]] - if self._arg_units is not None - else None, + ( + [str(u) for u in self._arg_units[:-1]] + if self._arg_units is not None + else None + ), ), ], (), diff --git a/pyomo/core/base/global_set.py b/pyomo/core/base/global_set.py index f4d97403308..b1bb98abee0 100644 --- a/pyomo/core/base/global_set.py +++ b/pyomo/core/base/global_set.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -72,8 +72,11 @@ def _parent(self, val): class _UnindexedComponent_set(GlobalSetBase): local_name = 'UnindexedComponent_set' + _anonymous_sets = GlobalSetBase + def __init__(self, name): self.name = name + self._constructed = True def __contains__(self, val): return val is None @@ -180,6 +183,12 @@ def prev(self, item, step=1): def prevw(self, item, step=1): return self.nextw(item, -step) + def parent_block(self): + return None + + def parent_component(self): + return self + UnindexedComponent_set = _UnindexedComponent_set('UnindexedComponent_set') GlobalSets[UnindexedComponent_set.local_name] = UnindexedComponent_set diff --git a/pyomo/core/base/indexed_component.py b/pyomo/core/base/indexed_component.py index 6e356a8304e..37a62e5c4d7 100644 --- a/pyomo/core/base/indexed_component.py +++ b/pyomo/core/base/indexed_component.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,32 +9,28 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['IndexedComponent', 'ActiveIndexedComponent'] - -import enum import inspect import logging import sys import textwrap -from copy import deepcopy - import pyomo.core.expr as EXPR -from pyomo.core.expr.numeric_expr import NumericNDArray -from pyomo.core.expr.numvalue import native_types +import pyomo.core.base as BASE from pyomo.core.base.indexed_component_slice import IndexedComponent_slice from pyomo.core.base.initializer import Initializer -from pyomo.core.base.component import Component, ActiveComponent +from pyomo.core.base.component import Component, ActiveComponent, ComponentData from pyomo.core.base.config import PyomoOptions from pyomo.core.base.enums import SortComponents from pyomo.core.base.global_set import UnindexedComponent_set +from pyomo.core.expr.numeric_expr import _ndarray from pyomo.core.pyomoobject import PyomoObject from pyomo.common import DeveloperError from pyomo.common.autoslots import fast_deepcopy -from pyomo.common.dependencies import numpy as np, numpy_available +from pyomo.common.collections import ComponentSet from pyomo.common.deprecation import deprecated, deprecation_warning -from pyomo.common.errors import DeveloperError, TemplateExpressionError +from pyomo.common.errors import TemplateExpressionError from pyomo.common.modeling import NOTSET +from pyomo.common.numeric_types import native_types from pyomo.common.sorting import sorted_robust from collections.abc import Sequence @@ -42,6 +38,7 @@ logger = logging.getLogger('pyomo.core') sequence_types = {tuple, list} +slicer_types = {slice, Ellipsis.__class__, IndexedComponent_slice} def normalize_index(x): @@ -163,9 +160,12 @@ def _get_indexed_component_data_name(component, index): """ -def rule_result_substituter(result_map): +def rule_result_substituter(result_map, map_types): _map = result_map - _map_types = set(type(key) for key in result_map) + if map_types is None: + _map_types = set(type(key) for key in result_map) + else: + _map_types = map_types def rule_result_substituter_impl(rule, *args, **kwargs): if rule.__class__ in _map_types: @@ -206,7 +206,7 @@ def rule_result_substituter_impl(rule, *args, **kwargs): """ -def rule_wrapper(rule, wrapping_fcn, positional_arg_map=None): +def rule_wrapper(rule, wrapping_fcn, positional_arg_map=None, map_types=None): """Wrap a rule with another function This utility method provides a way to wrap a function (rule) with @@ -233,7 +233,7 @@ def rule_wrapper(rule, wrapping_fcn, positional_arg_map=None): """ if isinstance(wrapping_fcn, dict): - wrapping_fcn = rule_result_substituter(wrapping_fcn) + wrapping_fcn = rule_result_substituter(wrapping_fcn, map_types) if not inspect.isfunction(rule): return wrapping_fcn(rule) # Because some of our processing of initializer functions relies on @@ -253,8 +253,7 @@ def rule_wrapper(rule, wrapping_fcn, positional_arg_map=None): class IndexedComponent(Component): - """ - This is the base class for all indexed modeling components. + """This is the base class for all indexed modeling components. This class stores a dictionary, self._data, that maps indices to component data objects. The object self._index_set defines valid keys for this dictionary, and the dictionary keys may be a @@ -276,11 +275,16 @@ class IndexedComponent(Component): doc A text string describing this component Private class attributes: - _data A dictionary from the index set to - component data objects - _index_set The set of valid indices - _implicit_subsets A temporary data element that stores - sets that are transferred to the model + + _data: A dictionary from the index set to component data objects + + _index_set: The set of valid indices + + _anonymous_sets: A ComponentSet of "anonymous" sets used by this + component. Anonymous sets are Set / SetOperator / RangeSet + that compose attributes like _index_set, but are not + themselves explicitly assigned (and named) on any Block + """ class Skip(object): @@ -296,45 +300,39 @@ class Skip(object): _DEFAULT_INDEX_CHECKING_ENABLED = True def __init__(self, *args, **kwds): - from pyomo.core.base.set import process_setarg - # kwds.pop('noruleinit', None) Component.__init__(self, **kwds) # self._data = {} # - if len(args) == 0 or (len(args) == 1 and args[0] is UnindexedComponent_set): + if len(args) == 0 or (args[0] is UnindexedComponent_set and len(args) == 1): # # If no indexing sets are provided, generate a dummy index # - self._implicit_subsets = None self._index_set = UnindexedComponent_set + self._anonymous_sets = None elif len(args) == 1: # # If a single indexing set is provided, just process it. # - self._implicit_subsets = None - self._index_set = process_setarg(args[0]) + self._index_set, self._anonymous_sets = BASE.set.process_setarg(args[0]) else: # # If multiple indexing sets are provided, process them all, - # and store the cross-product of these sets. The individual - # sets need to stored in the Pyomo model, so the - # _implicit_subsets class data is used for this temporary - # storage. + # and store the cross-product of these sets. # - # Example: Pyomo allows things like - # "Param([1,2,3], range(100), initialize=0)". This - # needs to create *3* sets: two SetOf components and then - # the SetProduct. That means that the component needs to - # hold on to the implicit SetOf objects until the component - # is assigned to a model (where the implicit subsets can be - # "transferred" to the model). + # Example: Pyomo allows things like "Param([1,2,3], + # range(100), initialize=0)". This needs to create *3* + # sets: two SetOf components and then the SetProduct. As + # the user declined to name any of these sets, we will not + # make up names and instead store them on the model as + # "anonymous components" # - tmp = [process_setarg(x) for x in args] - self._implicit_subsets = tmp - self._index_set = tmp[0].cross(*tmp[1:]) + self._index_set = BASE.set.SetProduct(*args) + self._anonymous_sets = ComponentSet((self._index_set,)) + if self._index_set._anonymous_sets is not None: + self._anonymous_sets.update(self._index_set._anonymous_sets) def _create_objects_for_deepcopy(self, memo, component_list): _new = self.__class__.__new__(self.__class__) @@ -608,7 +606,7 @@ def iteritems(self): """Return a list (index,data) tuples from the dictionary""" return self.items() - def __getitem__(self, index): + def __getitem__(self, index) -> ComponentData: """ This method returns the data corresponding to the given index. """ @@ -733,7 +731,7 @@ def __delitem__(self, index): # this supports "del m.x[:,1]" through a simple recursive call if index.__class__ is IndexedComponent_slice: - # Assert that this slice ws just generated + # Assert that this slice was just generated assert len(index._call_stack) == 1 # Make a copy of the slicer items *before* we start # iterating over it (since we will be removing items!). @@ -746,27 +744,6 @@ def __delitem__(self, index): self._data[index]._component = None del self._data[index] - def _pop_from_kwargs(self, name, kwargs, namelist, notset=None): - args = [ - arg - for arg in (kwargs.pop(name, notset) for name in namelist) - if arg is not notset - ] - if len(args) == 1: - return args[0] - elif not args: - return notset - else: - argnames = "%s%s '%s='" % ( - ', '.join("'%s='" % _ for _ in namelist[:-1]), - ',' if len(namelist) > 2 else '', - namelist[-1], - ) - raise ValueError( - "Duplicate initialization: %s() only accepts one of %s" - % (name, argnames) - ) - def _construct_from_rule_using_setitem(self): if self._rule is None: return @@ -833,16 +810,23 @@ def _validate_index(self, idx): return idx # This is only called through __{get,set,del}item__, which has - # already trapped unhashable objects. - validated_idx = self._index_set.get(idx, _NotFound) - if validated_idx is not _NotFound: - # If the index is in the underlying index set, then return it - # Note: This check is potentially expensive (e.g., when the - # indexing set is a complex set operation)! - return validated_idx - - if idx.__class__ is IndexedComponent_slice: - return idx + # already trapped unhashable objects. Unfortunately, Python + # 3.12 made slices hashable. This means that slices will get + # here and potentially be looked up in the index_set. This will + # cause problems with Any, where Any will happily return the + # index as a valid set. We will only validate the index for + # non-Any sets. Any will pass through so that normalize_index + # can be called (which can generate the TypeError for slices) + _any = isinstance(self._index_set, BASE.set._AnySet) + if _any: + validated_idx = _NotFound + else: + validated_idx = self._index_set.get(idx, _NotFound) + if validated_idx is not _NotFound: + # If the index is in the underlying index set, then return it + # Note: This check is potentially expensive (e.g., when the + # indexing set is a complex set operation)! + return validated_idx if normalize_index.flatten: # Now we normalize the index and check again. Usually, @@ -850,16 +834,24 @@ def _validate_index(self, idx): # "automatic" call to normalize_index until now for the # sake of efficiency. normalized_idx = normalize_index(idx) - if normalized_idx is not idx: - idx = normalized_idx - if idx in self._data: - return idx - if idx in self._index_set: - return idx + if normalized_idx is not idx and not _any: + if normalized_idx in self._data: + return normalized_idx + if normalized_idx in self._index_set: + return normalized_idx + else: + normalized_idx = idx + # There is the chance that the index contains an Ellipsis, # so we should generate a slicer - if idx is Ellipsis or idx.__class__ is tuple and Ellipsis in idx: - return self._processUnhashableIndex(idx) + if ( + normalized_idx.__class__ in slicer_types + or normalized_idx.__class__ is tuple + and any(_.__class__ in slicer_types for _ in normalized_idx) + ): + return self._processUnhashableIndex(normalized_idx) + if _any: + return idx # # Generate different errors, depending on the state of the index. # @@ -872,7 +864,8 @@ def _validate_index(self, idx): # Raise an exception # raise KeyError( - "Index '%s' is not valid for indexed component '%s'" % (idx, self.name) + "Index '%s' is not valid for indexed component '%s'" + % (normalized_idx, self.name) ) def _processUnhashableIndex(self, idx): @@ -881,7 +874,7 @@ def _processUnhashableIndex(self, idx): There are three basic ways to get here: 1) the index contains one or more slices or ellipsis 2) the index contains an unhashable type (e.g., a Pyomo - (Scalar)Component + (Scalar)Component) 3) the index contains an IndexTemplate """ # @@ -988,11 +981,13 @@ def _processUnhashableIndex(self, idx): slice_dim -= 1 if normalize_index.flatten: set_dim = self.dim() - elif self._implicit_subsets is None: + elif not self.is_indexed(): # Scalar component. set_dim = 0 else: - set_dim = len(self._implicit_subsets) + set_dim = self.index_set().dimen + if set_dim is None: + set_dim = 1 structurally_valid = False if slice_dim == set_dim or set_dim is None: @@ -1200,7 +1195,7 @@ class IndexedComponent_NDArrayMixin(object): def __array__(self, dtype=None): if not self.is_indexed(): - ans = NumericNDArray(shape=(1,), dtype=object) + ans = _ndarray.NumericNDArray(shape=(1,), dtype=object) ans[0] = self return ans @@ -1220,10 +1215,12 @@ def __array__(self, dtype=None): % (self, bounds[0], bounds[1]) ) shape = tuple(b + 1 for b in bounds[1]) - ans = NumericNDArray(shape=shape, dtype=object) + ans = _ndarray.NumericNDArray(shape=shape, dtype=object) for k, v in self.items(): ans[k] = v return ans def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): - return NumericNDArray.__array_ufunc__(None, ufunc, method, *inputs, **kwargs) + return _ndarray.NumericNDArray.__array_ufunc__( + None, ufunc, method, *inputs, **kwargs + ) diff --git a/pyomo/core/base/indexed_component_slice.py b/pyomo/core/base/indexed_component_slice.py index 9779711a19b..37b3c452433 100644 --- a/pyomo/core/base/indexed_component_slice.py +++ b/pyomo/core/base/indexed_component_slice.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -402,8 +402,7 @@ def __init__(self, component, fixed, sliced, ellipsis, iter_over_index, sort): self.last_index = () self.tuplize_unflattened_index = ( - self.component._implicit_subsets is None - or len(self.component._implicit_subsets) == 1 + len(list(self.component.index_set().subsets())) <= 1 ) if fixed is None and sliced is None and ellipsis is None: diff --git a/pyomo/core/base/initializer.py b/pyomo/core/base/initializer.py index 991feb0450d..c87a4236abe 100644 --- a/pyomo/core/base/initializer.py +++ b/pyomo/core/base/initializer.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/instance2dat.py b/pyomo/core/base/instance2dat.py index b11c0c18e11..5cd690b7ece 100644 --- a/pyomo/core/base/instance2dat.py +++ b/pyomo/core/base/instance2dat.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['instance2dat'] - import types from pyomo.core.base import Set, Param, value diff --git a/pyomo/core/base/label.py b/pyomo/core/base/label.py index b642b834146..e22c1283138 100644 --- a/pyomo/core/base/label.py +++ b/pyomo/core/base/label.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,17 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = [ - 'CounterLabeler', - 'NumericLabeler', - 'CNameLabeler', - 'TextLabeler', - 'AlphaNumericTextLabeler', - 'NameLabeler', - 'CuidLabeler', - 'ShortNameLabeler', -] - import re from pyomo.common.deprecation import deprecated diff --git a/pyomo/core/base/logical_constraint.py b/pyomo/core/base/logical_constraint.py index 6d553c66fed..cc0780fd9bd 100644 --- a/pyomo/core/base/logical_constraint.py +++ b/pyomo/core/base/logical_constraint.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['LogicalConstraint', '_LogicalConstraintData', 'LogicalConstraintList'] - import inspect import sys import logging @@ -22,7 +20,6 @@ from pyomo.common.modeling import NOTSET from pyomo.common.timing import ConstructionTimer -from pyomo.core.base.constraint import Constraint from pyomo.core.expr.boolean_value import as_boolean, BooleanConstant from pyomo.core.expr.numvalue import native_types, native_logical_types from pyomo.core.base.component import ActiveComponentData, ModelComponentFactory @@ -45,64 +42,7 @@ """ -class _LogicalConstraintData(ActiveComponentData): - """ - This class defines the data for a single logical constraint. - - It functions as a pure interface. - - Constructor arguments: - component The LogicalConstraint object that owns this data. - - Public class attributes: - active A boolean that is true if this statement is - active in the model. - body The Pyomo logical expression for this statement - - Private class attributes: - _component The statement component. - _active A boolean that indicates whether this data is active - """ - - __slots__ = () - - def __init__(self, component=None): - # - # These lines represent in-lining of the - # following constructors: - # - ActiveComponentData - # - ComponentData - self._component = weakref_ref(component) if (component is not None) else None - self._index = NOTSET - self._active = True - - # - # Interface - # - def __call__(self, exception=True): - """Compute the value of the body of this logical constraint.""" - if self.body is None: - return None - return self.body(exception=exception) - - # - # Abstract Interface - # - @property - def expr(self): - """Get the expression on this logical constraint.""" - raise NotImplementedError - - def set_value(self, expr): - """Set the expression on this logical constraint.""" - raise NotImplementedError - - def get_value(self): - """Get the expression on this logical constraint.""" - raise NotImplementedError - - -class _GeneralLogicalConstraintData(_LogicalConstraintData): +class LogicalConstraintData(ActiveComponentData): """ This class defines the data for a single general logical constraint. @@ -126,7 +66,7 @@ def __init__(self, expr=None, component=None): # # These lines represent in-lining of the # following constructors: - # - _LogicalConstraintData, + # - LogicalConstraintData, # - ActiveComponentData # - ComponentData self._component = weakref_ref(component) if (component is not None) else None @@ -137,6 +77,12 @@ def __init__(self, expr=None, component=None): if expr is not None: self.set_value(expr) + def __call__(self, exception=True): + """Compute the value of the body of this logical constraint.""" + if self.body is None: + return None + return self.body(exception=exception) + # # Abstract Interface # @@ -176,6 +122,16 @@ def get_value(self): return self._expr +class _LogicalConstraintData(metaclass=RenamedClass): + __renamed__new_class__ = LogicalConstraintData + __renamed__version__ = '6.7.2' + + +class _GeneralLogicalConstraintData(metaclass=RenamedClass): + __renamed__new_class__ = LogicalConstraintData + __renamed__version__ = '6.7.2' + + @ModelComponentFactory.register("General logical constraints.") class LogicalConstraint(ActiveIndexedComponent): """ @@ -210,8 +166,6 @@ class LogicalConstraint(ActiveIndexedComponent): A dictionary from the index set to component data objects _index_set The set of valid indices - _implicit_subsets - A tuple of set objects that represents the index set _model A weakref to the model that owns this component _parent @@ -220,7 +174,7 @@ class LogicalConstraint(ActiveIndexedComponent): The class type for the derived subclass """ - _ComponentDataClass = _GeneralLogicalConstraintData + _ComponentDataClass = LogicalConstraintData class Infeasible(object): pass @@ -280,6 +234,10 @@ def construct(self, data=None): timer = ConstructionTimer(self) self._constructed = True + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() + _init_expr = self._init_expr _init_rule = self.rule # @@ -374,7 +332,7 @@ def display(self, prefix="", ostream=None): # # Checks flags like Constraint.Skip, etc. before actually creating a - # constraint object. Returns the _ConstraintData object when it should be + # constraint object. Returns the ConstraintData object when it should be # added to the _data dict; otherwise, None is returned or an exception # is raised. # @@ -410,14 +368,14 @@ def _check_skip_add(self, index, expr): return expr -class ScalarLogicalConstraint(_GeneralLogicalConstraintData, LogicalConstraint): +class ScalarLogicalConstraint(LogicalConstraintData, LogicalConstraint): """ ScalarLogicalConstraint is the implementation representing a single, non-indexed logical constraint. """ def __init__(self, *args, **kwds): - _GeneralLogicalConstraintData.__init__(self, component=self, expr=None) + LogicalConstraintData.__init__(self, component=self, expr=None) LogicalConstraint.__init__(self, *args, **kwds) self._index = UnindexedComponent_index @@ -437,7 +395,7 @@ def body(self): "an expression. There is currently " "nothing to access." % self.name ) - return _GeneralLogicalConstraintData.body.fget(self) + return LogicalConstraintData.body.fget(self) raise ValueError( "Accessing the body of logical constraint '%s' " "before the LogicalConstraint has been constructed (there " @@ -451,7 +409,7 @@ def body(self): # currently in place). So during initialization only, we will # treat them as "indexed" objects where things like # True are managed. But after that they will behave - # like _LogicalConstraintData objects where set_value expects + # like LogicalConstraintData objects where set_value expects # a valid expression or None. # @@ -516,22 +474,25 @@ class LogicalConstraintList(IndexedLogicalConstraint): def __init__(self, **kwargs): """Constructor""" - args = (Set(),) if 'expr' in kwargs: raise ValueError("LogicalConstraintList does not accept the 'expr' keyword") - LogicalConstraint.__init__(self, *args, **kwargs) + LogicalConstraint.__init__(self, Set(dimen=1), **kwargs) def construct(self, data=None): """ Construct the expression(s) for this logical constraint. """ + if self._constructed: + return + self._constructed = True + generate_debug_messages = is_debug_set(logger) if generate_debug_messages: logger.debug("Constructing logical constraint list %s" % self.name) - if self._constructed: - return - self._constructed = True + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() assert self._init_expr is None _init_rule = self.rule diff --git a/pyomo/core/base/matrix_constraint.py b/pyomo/core/base/matrix_constraint.py index 0c55dbc15d3..8dac7c3d24b 100644 --- a/pyomo/core/base/matrix_constraint.py +++ b/pyomo/core/base/matrix_constraint.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -19,7 +19,7 @@ from pyomo.core.expr.numvalue import value from pyomo.core.expr.numeric_expr import LinearExpression from pyomo.core.base.component import ModelComponentFactory -from pyomo.core.base.constraint import IndexedConstraint, _ConstraintData +from pyomo.core.base.constraint import IndexedConstraint, ConstraintData from pyomo.repn.standard_repn import StandardRepn from collections.abc import Mapping @@ -28,7 +28,7 @@ logger = logging.getLogger('pyomo.core') -class _MatrixConstraintData(_ConstraintData): +class _MatrixConstraintData(ConstraintData): """ This class defines the data for a single linear constraint derived from a canonical form Ax=b constraint. @@ -104,7 +104,7 @@ def __init__(self, index, component_ref): # # These lines represent in-lining of the # following constructors: - # - _ConstraintData, + # - ConstraintData, # - ActiveComponentData # - ComponentData self._component = component_ref @@ -209,7 +209,7 @@ def index(self): return self._index # - # Abstract Interface (_ConstraintData) + # Abstract Interface (ConstraintData) # @property diff --git a/pyomo/core/base/misc.py b/pyomo/core/base/misc.py index cf37ad48fea..456a4531e30 100644 --- a/pyomo/core/base/misc.py +++ b/pyomo/core/base/misc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,14 +9,10 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['display'] - import logging import sys -import types from pyomo.common.deprecation import relocated_module_attribute -from pyomo.core.expr import native_numeric_types logger = logging.getLogger('pyomo.core') diff --git a/pyomo/core/base/numvalue.py b/pyomo/core/base/numvalue.py index 11d45228bf5..75bceef7ebb 100644 --- a/pyomo/core/base/numvalue.py +++ b/pyomo/core/base/numvalue.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/objective.py b/pyomo/core/base/objective.py index 3c625d81c2d..f1204f2a09c 100644 --- a/pyomo/core/base/objective.py +++ b/pyomo/core/base/objective.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,22 +9,13 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ( - 'Objective', - 'simple_objective_rule', - '_ObjectiveData', - 'minimize', - 'maximize', - 'simple_objectivelist_rule', - 'ObjectiveList', -) - import sys import logging from weakref import ref as weakref_ref from pyomo.common.pyomo_typing import overload from pyomo.common.deprecation import RenamedClass +from pyomo.common.enums import ObjectiveSense, minimize, maximize from pyomo.common.log import is_debug_set from pyomo.common.modeling import NOTSET from pyomo.common.formatting import tabular_writer @@ -38,14 +29,13 @@ UnindexedComponent_set, rule_wrapper, ) -from pyomo.core.base.expression import _ExpressionData, _GeneralExpressionDataImpl +from pyomo.core.base.expression import NamedExpressionData from pyomo.core.base.set import Set from pyomo.core.base.initializer import ( Initializer, IndexedCallInitializer, CountedCallInitializer, ) -from pyomo.core.base import minimize, maximize logger = logging.getLogger('pyomo.core') @@ -91,47 +81,7 @@ def O_rule(model, i, j): return rule_wrapper(rule, {None: ObjectiveList.End}) -# -# This class is a pure interface -# - - -class _ObjectiveData(_ExpressionData): - """ - This class defines the data for a single objective. - - Public class attributes: - expr The Pyomo expression for this objective - sense The direction for this objective. - """ - - __slots__ = () - - # - # Interface - # - - def is_minimizing(self): - """Return True if this is a minimization objective.""" - return self.sense == minimize - - # - # Abstract Interface - # - - @property - def sense(self): - """Access sense (direction) of this objective.""" - raise NotImplementedError - - def set_sense(self, sense): - """Set the sense (direction) of this objective.""" - raise NotImplementedError - - -class _GeneralObjectiveData( - _GeneralExpressionDataImpl, _ObjectiveData, ActiveComponentData -): +class ObjectiveData(NamedExpressionData, ActiveComponentData): """ This class defines the data for a single objective. @@ -154,22 +104,20 @@ class _GeneralObjectiveData( _active A boolean that indicates whether this data is active """ - __slots__ = ("_sense", "_args_") + __slots__ = ("_args_", "_sense") def __init__(self, expr=None, sense=minimize, component=None): - _GeneralExpressionDataImpl.__init__(self, expr) + # Inlining NamedExpressionData.__init__ + self._args_ = (expr,) # Inlining ActiveComponentData.__init__ self._component = weakref_ref(component) if (component is not None) else None self._index = NOTSET self._active = True - self._sense = sense + self._sense = ObjectiveSense(sense) - if (self._sense != minimize) and (self._sense != maximize): - raise ValueError( - "Objective sense must be set to one of " - "'minimize' (%s) or 'maximize' (%s). Invalid " - "value: %s'" % (minimize, maximize, sense) - ) + def is_minimizing(self): + """Return True if this is a minimization objective.""" + return self.sense == minimize def set_value(self, expr): if expr is None: @@ -192,14 +140,17 @@ def sense(self, sense): def set_sense(self, sense): """Set the sense (direction) of this objective.""" - if sense in {minimize, maximize}: - self._sense = sense - else: - raise ValueError( - "Objective sense must be set to one of " - "'minimize' (%s) or 'maximize' (%s). Invalid " - "value: %s'" % (minimize, maximize, sense) - ) + self._sense = ObjectiveSense(sense) + + +class _ObjectiveData(metaclass=RenamedClass): + __renamed__new_class__ = ObjectiveData + __renamed__version__ = '6.7.2' + + +class _GeneralObjectiveData(metaclass=RenamedClass): + __renamed__new_class__ = ObjectiveData + __renamed__version__ = '6.7.2' @ModelComponentFactory.register("Expressions that are minimized or maximized.") @@ -242,8 +193,6 @@ class Objective(ActiveIndexedComponent): A dictionary from the index set to component data objects _index The set of valid indices - _implicit_subsets - A tuple of set objects that represents the index set _model A weakref to the model that owns this component _parent @@ -252,7 +201,7 @@ class Objective(ActiveIndexedComponent): The class type for the derived subclass """ - _ComponentDataClass = _GeneralObjectiveData + _ComponentDataClass = ObjectiveData NoObjective = ActiveIndexedComponent.Skip def __new__(cls, *args, **kwds): @@ -266,8 +215,7 @@ def __new__(cls, *args, **kwds): @overload def __init__( self, *indexes, expr=None, rule=None, sense=minimize, name=None, doc=None - ): - ... + ): ... def __init__(self, *args, **kwargs): _sense = kwargs.pop('sense', minimize) @@ -291,6 +239,10 @@ def construct(self, data=None): if is_debug_set(logger): logger.debug("Constructing objective %s" % (self.name)) + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() + rule = self.rule try: # We do not (currently) accept data for constructing Objectives @@ -362,11 +314,7 @@ def _pprint(self): ], self._data.items(), ("Active", "Sense", "Expression"), - lambda k, v: [ - v.active, - ("minimize" if (v.sense == minimize) else "maximize"), - v.expr, - ], + lambda k, v: [v.active, v.sense, v.expr], ) def display(self, prefix="", ostream=None): @@ -398,14 +346,14 @@ def display(self, prefix="", ostream=None): ) -class ScalarObjective(_GeneralObjectiveData, Objective): +class ScalarObjective(ObjectiveData, Objective): """ ScalarObjective is the implementation representing a single, non-indexed objective. """ def __init__(self, *args, **kwd): - _GeneralObjectiveData.__init__(self, expr=None, component=self) + ObjectiveData.__init__(self, expr=None, component=self) Objective.__init__(self, *args, **kwd) self._index = UnindexedComponent_index @@ -441,7 +389,7 @@ def expr(self): "a sense or expression (there is currently " "no value to return)." % (self.name) ) - return _GeneralObjectiveData.expr.fget(self) + return ObjectiveData.expr.fget(self) raise ValueError( "Accessing the expression of objective '%s' " "before the Objective has been constructed (there " @@ -464,7 +412,7 @@ def sense(self): "a sense or expression (there is currently " "no value to return)." % (self.name) ) - return _GeneralObjectiveData.sense.fget(self) + return ObjectiveData.sense.fget(self) raise ValueError( "Accessing the sense of objective '%s' " "before the Objective has been constructed (there " @@ -483,7 +431,7 @@ def sense(self, sense): # currently in place). So during initialization only, we will # treat them as "indexed" objects where things like # Objective.Skip are managed. But after that they will behave - # like _ObjectiveData objects where set_value does not handle + # like ObjectiveData objects where set_value does not handle # Objective.Skip but expects a valid expression or None # @@ -507,7 +455,7 @@ def set_sense(self, sense): if self._constructed: if len(self._data) == 0: self._data[None] = self - return _GeneralObjectiveData.set_sense(self, sense) + return ObjectiveData.set_sense(self, sense) raise ValueError( "Setting the sense of objective '%s' " "before the Objective has been constructed (there " @@ -565,8 +513,7 @@ def __init__(self, **kwargs): _rule = kwargs.pop('rule', None) self._starting_index = kwargs.pop('starting_index', 1) - args = (Set(dimen=1),) - super().__init__(*args, **kwargs) + super().__init__(Set(dimen=1), **kwargs) self.rule = Initializer(_rule, allow_generators=True) # HACK to make the "counted call" syntax work. We wait until @@ -586,7 +533,9 @@ def construct(self, data=None): if is_debug_set(logger): logger.debug("Constructing objective list %s" % (self.name)) - self.index_set().construct() + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() if self.rule is not None: _rule = self.rule(self.parent_block(), ()) diff --git a/pyomo/core/base/param.py b/pyomo/core/base/param.py index a6b893ec2c9..45de3286589 100644 --- a/pyomo/core/base/param.py +++ b/pyomo/core/base/param.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,13 +9,13 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['Param'] - +from __future__ import annotations import sys import types import logging from weakref import ref as weakref_ref from pyomo.common.pyomo_typing import overload +from typing import Union, Type from pyomo.common.autoslots import AutoSlots from pyomo.common.deprecation import deprecation_warning, RenamedClass @@ -118,7 +118,7 @@ def _parent(self, val): pass -class _ParamData(ComponentData, NumericValue): +class ParamData(ComponentData, NumericValue): """ This class defines the data for a mutable parameter. @@ -164,16 +164,31 @@ def set_value(self, value, idx=NOTSET): # required to be mutable. # _comp = self.parent_component() - if type(value) in native_types: + if value.__class__ in native_types: # TODO: warn/error: check if this Param has units: assigning # a dimensionless value to a united param should be an error pass elif _comp._units is not None: _src_magnitude = expr_value(value) - _src_units = units.get_units(value) - value = units.convert_value( - num_value=_src_magnitude, from_units=_src_units, to_units=_comp._units - ) + # Note: expr_value() could have just registered a new numeric type + if value.__class__ in native_types: + value = _src_magnitude + else: + _src_units = units.get_units(value) + value = units.convert_value( + num_value=_src_magnitude, + from_units=_src_units, + to_units=_comp._units, + ) + # FIXME: we should call value() here [to ensure types get + # registered], but doing so breaks non-numeric Params (which we + # allow). The real fix will be to follow the precedent from + # GetItemExpression and have separate types based on which + # expression "system" the Param should participate in (numeric, + # logical, or structural). + # + # else: + # value = expr_value(value) old_value, self._value = self._value, value try: @@ -237,6 +252,11 @@ def _compute_polynomial_degree(self, result): return 0 +class _ParamData(metaclass=RenamedClass): + __renamed__new_class__ = ParamData + __renamed__version__ = '6.7.2' + + @ModelComponentFactory.register( "Parameter data that is used to define a model instance." ) @@ -270,7 +290,7 @@ class Param(IndexedComponent, IndexedComponent_NDArrayMixin): """ DefaultMutable = False - _ComponentDataClass = _ParamData + _ComponentDataClass = ParamData class NoValue(object): """A dummy type that is pickle-safe that we can use as the default @@ -278,6 +298,17 @@ class NoValue(object): pass + @overload + def __new__( + cls: Type[Param], *args, **kwds + ) -> Union[ScalarParam, IndexedParam]: ... + + @overload + def __new__(cls: Type[ScalarParam], *args, **kwds) -> ScalarParam: ... + + @overload + def __new__(cls: Type[IndexedParam], *args, **kwds) -> IndexedParam: ... + def __new__(cls, *args, **kwds): if cls != Param: return super(Param, cls).__new__(cls) @@ -301,8 +332,7 @@ def __init__( units=None, name=None, doc=None, - ): - ... + ): ... def __init__(self, *args, **kwd): _init = self._pop_from_kwargs('Param', kwd, ('rule', 'initialize'), NOTSET) @@ -331,7 +361,7 @@ def __init__(self, *args, **kwd): if _domain_rule is None: self.domain = _ImplicitAny(owner=self, name='Any') else: - self.domain = SetInitializer(_domain_rule)(self.parent_block(), None) + self.domain = SetInitializer(_domain_rule)(self.parent_block(), None, self) # After IndexedComponent.__init__ so we can call is_indexed(). self._rule = Initializer( _init, @@ -498,14 +528,14 @@ def store_values(self, new_values, check=True): # instead of incurring the penalty of checking. for index, new_value in new_values.items(): if index not in self._data: - self._data[index] = _ParamData(self) + self._data[index] = ParamData(self) self._data[index]._value = new_value else: # For scalars, we will choose an approach based on # how "dense" the Param is if not self._data: # empty for index in self._index_set: - p = self._data[index] = _ParamData(self) + p = self._data[index] = ParamData(self) p._value = new_values elif len(self._data) == len(self._index_set): for index in self._index_set: @@ -513,7 +543,7 @@ def store_values(self, new_values, check=True): else: for index in self._index_set: if index not in self._data: - self._data[index] = _ParamData(self) + self._data[index] = ParamData(self) self._data[index]._value = new_values else: # @@ -576,9 +606,9 @@ def _getitem_when_not_present(self, index): # a default value, as long as *solving* a model without # reasonable values produces an informative error. if self._mutable: - # Note: _ParamData defaults to Param.NoValue + # Note: ParamData defaults to Param.NoValue if self.is_indexed(): - ans = self._data[index] = _ParamData(self) + ans = self._data[index] = ParamData(self) else: ans = self._data[index] = self ans._index = index @@ -673,8 +703,8 @@ def _setitem_impl(self, index, obj, value): return obj else: old_value, self._data[index] = self._data[index], value - # Because we do not have a _ParamData, we cannot rely on the - # validation that occurs in _ParamData.set_value() + # Because we do not have a ParamData, we cannot rely on the + # validation that occurs in ParamData.set_value() try: self._validate_value(index, value) return value @@ -711,14 +741,14 @@ def _setitem_when_not_present(self, index, value, _check_domain=True): self._index = UnindexedComponent_index return self elif self._mutable: - obj = self._data[index] = _ParamData(self) + obj = self._data[index] = ParamData(self) obj.set_value(value, index) obj._index = index return obj else: self._data[index] = value - # Because we do not have a _ParamData, we cannot rely on the - # validation that occurs in _ParamData.set_value() + # Because we do not have a ParamData, we cannot rely on the + # validation that occurs in ParamData.set_value() self._validate_value(index, value, _check_domain) return value except: @@ -784,6 +814,10 @@ def construct(self, data=None): ) self._mutable = True + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() + try: # # If the default value is a simple type, we check it versus @@ -872,9 +906,9 @@ def _pprint(self): return (headers, self.sparse_iteritems(), ("Value",), dataGen) -class ScalarParam(_ParamData, Param): +class ScalarParam(ParamData, Param): def __init__(self, *args, **kwds): - _ParamData.__init__(self, component=self) + ParamData.__init__(self, component=self) Param.__init__(self, *args, **kwds) self._index = UnindexedComponent_index @@ -967,7 +1001,7 @@ def _create_objects_for_deepcopy(self, memo, component_list): # between potentially variable GetItemExpression objects and # "constant" GetItemExpression objects. That will need to wait for # the expression rework [JDS; Nov 22]. - def __getitem__(self, args): + def __getitem__(self, args) -> ParamData: try: return super().__getitem__(args) except: diff --git a/pyomo/core/base/piecewise.py b/pyomo/core/base/piecewise.py index 8ab6ce38ca5..8c5f34d2b53 100644 --- a/pyomo/core/base/piecewise.py +++ b/pyomo/core/base/piecewise.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -32,13 +32,6 @@ *) piecewise for functions of the form y = f(x1,x2,...) """ -# ****** NOTE: Nothing in this file relies on integer division ******* -# I predict this will save numerous headaches as -# well as gratuitous calls to float() in this code -from __future__ import division - -__all__ = ['Piecewise'] - import logging import math import itertools @@ -47,14 +40,14 @@ import enum from pyomo.common.log import is_debug_set -from pyomo.common.deprecation import deprecation_warning +from pyomo.common.deprecation import RenamedClass, deprecation_warning from pyomo.common.numeric_types import value from pyomo.common.timing import ConstructionTimer -from pyomo.core.base.block import Block, _BlockData +from pyomo.core.base.block import Block, BlockData from pyomo.core.base.component import ModelComponentFactory from pyomo.core.base.constraint import Constraint, ConstraintList from pyomo.core.base.sos import SOSConstraint -from pyomo.core.base.var import Var, _VarData, IndexedVar +from pyomo.core.base.var import Var, VarData, IndexedVar from pyomo.core.base.set_types import PositiveReals, NonNegativeReals, Binary from pyomo.core.base.util import flatten_tuple @@ -151,8 +144,6 @@ def _characterize_function(name, tol, f_rule, model, points, *index): # expression generation errors in the checks below points = [value(_p) for _p in points] - # we use future division to protect against the case where - # the user supplies integer type points for return values if isinstance(f_rule, types.FunctionType): values = [f_rule(model, *flatten_tuple((index, x))) for x in points] elif f_rule.__class__ is dict: @@ -178,9 +169,11 @@ def _characterize_function(name, tol, f_rule, model, points, *index): # we have a step function step = True slopes = [ - (None) - if (points[i] == points[i - 1]) - else ((values[i] - values[i - 1]) / (points[i] - points[i - 1])) + ( + (None) + if (points[i] == points[i - 1]) + else ((values[i] - values[i - 1]) / (points[i] - points[i - 1])) + ) for i in range(1, len(points)) ] @@ -193,9 +186,9 @@ def _characterize_function(name, tol, f_rule, model, points, *index): # to send this warning through Pyomo if not all( itertools.starmap( - lambda x1, x2: (True) - if ((x1 is None) or (x2 is None)) - else (abs(x1 - x2) > tol), + lambda x1, x2: ( + (True) if ((x1 is None) or (x2 is None)) else (abs(x1 - x2) > tol) + ), zip(slopes, itertools.islice(slopes, 1, None)), ) ): @@ -221,14 +214,14 @@ def _characterize_function(name, tol, f_rule, model, points, *index): return 0, values, False -class _PiecewiseData(_BlockData): +class PiecewiseData(BlockData): """ This class defines the base class for all linearization and piecewise constraint generators.. """ def __init__(self, parent): - _BlockData.__init__(self, parent) + BlockData.__init__(self, parent) self._constructed = True self._bound_type = None self._domain_pts = None @@ -270,7 +263,6 @@ def __call__(self, x): yU = self._range_pts[i + 1] if xL == xU: # a step function return yU - # using future division return yL + ((yU - yL) / (xU - xL)) * (x - xL) raise ValueError( "The point %s is outside the list of domain " @@ -280,6 +272,11 @@ def __call__(self, x): ) +class _PiecewiseData(metaclass=RenamedClass): + __renamed__new_class__ = PiecewiseData + __renamed__version__ = '6.7.2' + + class _SimpleSinglePiecewise(object): """ Called when the piecewise points list has only two points @@ -297,7 +294,6 @@ def construct(self, pblock, x_var, y_var): # create a single linear constraint LHS = y_var F_AT_XO = y_pts[0] - # using future division dF_AT_XO = (y_pts[1] - y_pts[0]) / (x_pts[1] - x_pts[0]) X_MINUS_XO = x_var - x_pts[0] if bound_type == Bound.Upper: @@ -737,7 +733,7 @@ def construct(self, pblock, x_var, y_var): # create indexers polytopes = range(1, len_x_pts) - # create constants (using future division) + # create constants SLOPE = { p: (y_pts[p] - y_pts[p - 1]) / (x_pts[p] - x_pts[p - 1]) for p in polytopes } @@ -906,7 +902,6 @@ def con1_rule(model, i): rhs *= 0.0 else: rhs *= OPT_M['UB'][i] * (1 - bigm_y[i]) - # using future division return ( y_var - y_pts[i - 1] @@ -920,7 +915,6 @@ def con1_rule(model, i): rhs *= 0.0 else: rhs *= OPT_M['LB'][i] * (1 - bigm_y[i]) - # using future division return ( y_var - y_pts[i - 1] @@ -942,7 +936,6 @@ def conAFF_rule(model, i): rhs *= 0.0 else: rhs *= OPT_M['LB'][i] * (1 - bigm_y[i]) - # using future division return ( y_var - y_pts[i - 1] @@ -972,7 +965,6 @@ def conAFF_rule(model, i): pblock.bigm_domain_constraint_upper = Constraint(expr=x_var <= x_pts[-1]) def _M_func(self, a, Fa, b, Fb, c, Fc): - # using future division return Fa - Fb - ((a - b) * ((Fc - Fb) / (c - b))) def _find_M(self, x_pts, y_pts, bound_type): @@ -1138,7 +1130,7 @@ def f(model,j,x): not be modified. """ - _ComponentDataClass = _PiecewiseData + _ComponentDataClass = PiecewiseData def __new__(cls, *args, **kwds): if cls != Piecewise: @@ -1248,7 +1240,7 @@ def __init__(self, *args, **kwds): # Check that the variables args are actually Pyomo Vars if not ( - isinstance(self._domain_var, _VarData) + isinstance(self._domain_var, VarData) or isinstance(self._domain_var, IndexedVar) ): msg = ( @@ -1257,7 +1249,7 @@ def __init__(self, *args, **kwds): ) raise TypeError(msg % (repr(self._domain_var),)) if not ( - isinstance(self._range_var, _VarData) + isinstance(self._range_var, VarData) or isinstance(self._range_var, IndexedVar) ): msg = ( @@ -1367,22 +1359,22 @@ def add(self, index, _is_indexed=None): _self_yvar = None _self_domain_pts_index = None if not _is_indexed: - # allows one to mix Var and _VarData as input to + # allows one to mix Var and VarData as input to # non-indexed Piecewise, index would be None in this case - # so for Var elements Var[None] is Var, but _VarData[None] would fail + # so for Var elements Var[None] is Var, but VarData[None] would fail _self_xvar = self._domain_var _self_yvar = self._range_var _self_domain_pts_index = self._domain_points[index] else: - # The following allows one to specify a Var or _VarData + # The following allows one to specify a Var or VarData # object even with an indexed Piecewise component. # The most common situation will most likely be a VarArray, # so we try this first. - if not isinstance(self._domain_var, _VarData): + if not isinstance(self._domain_var, VarData): _self_xvar = self._domain_var[index] else: _self_xvar = self._domain_var - if not isinstance(self._range_var, _VarData): + if not isinstance(self._range_var, VarData): _self_yvar = self._range_var[index] else: _self_yvar = self._range_var @@ -1554,7 +1546,7 @@ def add(self, index, _is_indexed=None): raise ValueError(msg % (self.name, index, self._pw_rep)) if _is_indexed: - comp = _PiecewiseData(self) + comp = PiecewiseData(self) else: comp = self self._data[index] = comp @@ -1564,9 +1556,9 @@ def add(self, index, _is_indexed=None): comp.build_constraints(func, _self_xvar, _self_yvar) -class SimplePiecewise(_PiecewiseData, Piecewise): +class SimplePiecewise(PiecewiseData, Piecewise): def __init__(self, *args, **kwds): - _PiecewiseData.__init__(self, self) + PiecewiseData.__init__(self, self) Piecewise.__init__(self, *args, **kwds) diff --git a/pyomo/core/base/plugin.py b/pyomo/core/base/plugin.py index 4ecb12d86a6..062e9f9fb85 100644 --- a/pyomo/core/base/plugin.py +++ b/pyomo/core/base/plugin.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -21,30 +21,6 @@ calling_frame=inspect.currentframe().f_back, ) -__all__ = [ - 'pyomo_callback', - 'IPyomoExpression', - 'ExpressionFactory', - 'ExpressionRegistration', - 'IPyomoPresolver', - 'IPyomoPresolveAction', - 'IParamRepresentation', - 'ParamRepresentationFactory', - 'IPyomoScriptPreprocess', - 'IPyomoScriptCreateModel', - 'IPyomoScriptCreateDataPortal', - 'IPyomoScriptModifyInstance', - 'IPyomoScriptPrintModel', - 'IPyomoScriptPrintInstance', - 'IPyomoScriptSaveInstance', - 'IPyomoScriptPrintResults', - 'IPyomoScriptSaveResults', - 'IPyomoScriptPostprocess', - 'ModelComponentFactory', - 'Transformation', - 'TransformationFactory', -] - from pyomo.core.base.component import ModelComponentFactory from pyomo.core.base.transformation import ( Transformation, diff --git a/pyomo/core/base/range.py b/pyomo/core/base/range.py index b0863f11207..acb004e3b10 100644 --- a/pyomo/core/base/range.py +++ b/pyomo/core/base/range.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -12,6 +12,8 @@ import math from collections.abc import Sequence +from pyomo.common.numeric_types import check_if_numeric_type + try: from math import remainder except ImportError: @@ -180,35 +182,40 @@ def __ne__(self, other): def __contains__(self, value): # NumericRanges must hold items that are comparable to ints if value.__class__ not in self._types_comparable_to_int: - # Special case: because numpy is fond of returning scalars - # as length-1 ndarrays, we will include a special case that - # will unpack things that look like single element arrays. - try: - # Note: trap "value[0] is not value" to catch things like - # single-character strings - if ( - hasattr(value, '__len__') - and hasattr(value, '__getitem__') - and len(value) == 1 - and value[0] is not value - ): - return value[0] in self - except: - pass - # See if this class behaves like a "normal" number: both - # comparable and creatable - try: - if not (bool(value - 0 > 0) ^ bool(value - 0 <= 0)): - return False - elif value.__class__(0) != 0 or not value.__class__(0) == 0: + # Build on numeric_type.check_if_numeric_type to cleanly + # handle numpy registrations + if check_if_numeric_type(value): + self._types_comparable_to_int.add(value.__class__) + else: + # Special case: because numpy is fond of returning scalars + # as length-1 ndarrays, we will include a special case that + # will unpack things that look like single element arrays. + try: + # Note: trap "value[0] is not value" to catch things like + # single-character strings + if ( + hasattr(value, '__len__') + and hasattr(value, '__getitem__') + and len(value) == 1 + and value[0] is not value + ): + return value[0] in self + except: + pass + # See if this class behaves like a "normal" number: both + # comparable and creatable + try: + if not (bool(value - 0 > 0) ^ bool(value - 0 <= 0)): + return False + elif value.__class__(0) != 0 or not value.__class__(0) == 0: + return False + else: + self._types_comparable_to_int.add(value.__class__) + except: return False - else: - self._types_comparable_to_int.add(value.__class__) - except: - return False if self.step: - _dir = math.copysign(1, self.step) + _dir = int(math.copysign(1, self.step)) _from_start = value - self.start return ( 0 <= _dir * _from_start <= _dir * (self.end - self.start) @@ -411,14 +418,13 @@ def _split_ranges(cnr, new_step): assert new_step >= abs(cnr.step) assert new_step % cnr.step == 0 - _dir = math.copysign(1, cnr.step) + _dir = int(math.copysign(1, cnr.step)) _subranges = [] for i in range(int(abs(new_step // cnr.step))): if _dir * (cnr.start + i * cnr.step) > _dir * cnr.end: # Once we walk past the end of the range, we are done # (all remaining offsets will be farther past the end) break - _subranges.append( NumericRange(cnr.start + i * cnr.step, cnr.end, _dir * new_step) ) @@ -458,7 +464,7 @@ def _step_lcm(self, other_ranges): else: # one of the steps was 0: add to preserve the non-zero step a += b - return abs(a) + return int(abs(a)) def _push_to_discrete_element(self, val, push_to_next_larger_value): if not self.step or val in _infinite: @@ -557,9 +563,14 @@ def range_difference(self, other_ranges): NumericRange(t.start, start, 0, (t.closed[0], False)) ) if s.step: # i.e., not a single point - for i in range(int(start // s.step), int(end // s.step)): + for i in range(int((end - start) // s.step)): _new_subranges.append( - NumericRange(i * s.step, (i + 1) * s.step, 0, '()') + NumericRange( + start + i * s.step, + start + (i + 1) * s.step, + 0, + '()', + ) ) if t.end > end: _new_subranges.append( @@ -605,7 +616,7 @@ def range_difference(self, other_ranges): ) elif t_max == s_max and t_c[1] and not s_c[1]: _new_subranges.append(NumericRange(t_max, t_max, 0)) - _this = _new_subranges + _this = _new_subranges return _this def range_intersection(self, other_ranges): diff --git a/pyomo/core/base/rangeset.py b/pyomo/core/base/rangeset.py index 18dedb84c34..32e41698aab 100644 --- a/pyomo/core/base/rangeset.py +++ b/pyomo/core/base/rangeset.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['RangeSet'] - from .set import RangeSet from pyomo.common.deprecation import deprecation_warning diff --git a/pyomo/core/base/reference.py b/pyomo/core/base/reference.py index 79ae83b97be..558ced64f1b 100644 --- a/pyomo/core/base/reference.py +++ b/pyomo/core/base/reference.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -18,7 +18,7 @@ Sequence, ) from pyomo.common.modeling import NOTSET -from pyomo.core.base.set import DeclareGlobalSet, Set, SetOf, OrderedSetOf, _SetDataBase +from pyomo.core.base.set import DeclareGlobalSet, Set, SetOf, OrderedSetOf, SetData from pyomo.core.base.component import Component, ComponentData from pyomo.core.base.global_set import UnindexedComponent_set from pyomo.core.base.enums import SortComponents @@ -579,7 +579,7 @@ def Reference(reference, ctype=NOTSET): :py:class:`IndexedComponent`. If the indices associated with wildcards in the component slice all - refer to the same :py:class:`Set` objects for all data identifed by + refer to the same :py:class:`Set` objects for all data identified by the slice, then the resulting indexed component will be indexed by the product of those sets. However, if all data do not share common set objects, or only a subset of indices in a multidimentional set @@ -612,7 +612,7 @@ def Reference(reference, ctype=NOTSET): ... >>> m.r1 = Reference(m.b[:,:].x) >>> m.r1.pprint() - r1 : Size=4, Index=r1_index, ReferenceTo=b[:, :].x + r1 : Size=4, Index={1, 2}*{3, 4}, ReferenceTo=b[:, :].x Key : Lower : Value : Upper : Fixed : Stale : Domain (1, 3) : 1 : None : 3 : False : True : Reals (1, 4) : 1 : None : 4 : False : True : Reals @@ -625,7 +625,7 @@ def Reference(reference, ctype=NOTSET): >>> m.r2 = Reference(m.b[:,3].x) >>> m.r2.pprint() - r2 : Size=2, Index=b_index_0, ReferenceTo=b[:, 3].x + r2 : Size=2, Index={1, 2}, ReferenceTo=b[:, 3].x Key : Lower : Value : Upper : Fixed : Stale : Domain 1 : 1 : None : 3 : False : True : Reals 2 : 2 : None : 3 : False : True : Reals @@ -642,7 +642,7 @@ def Reference(reference, ctype=NOTSET): ... >>> m.r3 = Reference(m.b[:].x[:]) >>> m.r3.pprint() - r3 : Size=4, Index=r3_index, ReferenceTo=b[:].x[:] + r3 : Size=4, Index=ReferenceSet(b[:].x[:]), ReferenceTo=b[:].x[:] Key : Lower : Value : Upper : Fixed : Stale : Domain (1, 3) : 1 : None : None : False : True : Reals (1, 4) : 1 : None : None : False : True : Reals @@ -657,7 +657,7 @@ def Reference(reference, ctype=NOTSET): >>> m.r3[1,4] = 10 >>> m.b[1].x.pprint() - x : Size=2, Index=b[1].x_index + x : Size=2, Index={3, 4} Key : Lower : Value : Upper : Fixed : Stale : Domain 3 : 1 : None : None : False : True : Reals 4 : 1 : 10 : None : False : False : Reals @@ -774,10 +774,10 @@ def Reference(reference, ctype=NOTSET): # is that within the subsets list, and set is a wildcard set. index = wildcards[0][1] # index is the first wildcard set. - if not isinstance(index, _SetDataBase): + if not isinstance(index, SetData): index = SetOf(index) for lvl, idx in wildcards[1:]: - if not isinstance(idx, _SetDataBase): + if not isinstance(idx, SetData): idx = SetOf(idx) index = index * idx # index is now either a single Set, or a SetProduct of the diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 051922c4aaa..8b7c2a246d6 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,6 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from __future__ import annotations import inspect import itertools import logging @@ -16,7 +17,10 @@ import sys import weakref from pyomo.common.pyomo_typing import overload +from typing import Union, Type, Any as typingAny +from collections.abc import Iterator +from pyomo.common.collections import ComponentSet from pyomo.common.deprecation import deprecated, deprecation_warning, RenamedClass from pyomo.common.errors import DeveloperError, PyomoException from pyomo.common.log import is_debug_set @@ -46,7 +50,7 @@ RangeDifferenceError, ) from pyomo.core.base.component import ( - _ComponentBase, + ComponentBase, Component, ComponentData, ModelComponentFactory, @@ -80,10 +84,7 @@ All Sets implement one of the following APIs: -0. `class _SetDataBase(ComponentData)` - *(pure virtual interface)* - -1. `class _SetData(_SetDataBase)` +1. `class SetData(ComponentData)` *(base class for all AML Sets)* 2. `class _FiniteSetMixin(object)` @@ -98,7 +99,7 @@ bounded continuous ranges as well as unbounded discrete ranges). As there are an infinite number of values, iteration is *not* supported. The base class also implements all Python set operations. -Note that `_SetData` does *not* implement `len()`, as Python requires +Note that `SetData` does *not* implement `len()`, as Python requires `len()` to return a positive integer. Finite sets add iteration and support for `len()`. In addition, they @@ -124,9 +125,19 @@ def process_setarg(arg): - if isinstance(arg, _SetDataBase): - return arg - elif isinstance(arg, _ComponentBase): + if isinstance(arg, SetData): + if ( + getattr(arg, '_parent', None) is not None + or getattr(arg, '_anonymous_sets', None) is GlobalSetBase + or arg.parent_component()._parent is not None + ): + return arg, None + _anonymous = ComponentSet((arg,)) + if getattr(arg, '_anonymous_sets', None) is not None: + _anonymous.update(arg._anonymous_sets) + return arg, _anonymous + + elif isinstance(arg, ComponentBase): if isinstance(arg, IndexedComponent) and arg.is_indexed(): raise TypeError( "Cannot apply a Set operator to an " @@ -168,7 +179,7 @@ def process_setarg(arg): ) ): ans.construct() - return ans + return process_setarg(ans) # TBD: should lists/tuples be copied into Sets, or # should we preserve the reference using SetOf? @@ -188,19 +199,20 @@ def process_setarg(arg): # create the Set: # _defer_construct = False - if inspect.isgenerator(arg): - _ordered = True - _defer_construct = True - elif inspect.isfunction(arg): - _ordered = True - _defer_construct = True - elif not hasattr(arg, '__contains__'): - raise TypeError( - "Cannot create a Set from data that does not support " - "__contains__. Expected set-like object supporting " - "collections.abc.Collection interface, but received '%s'." - % (type(arg).__name__,) - ) + if not hasattr(arg, '__contains__'): + if inspect.isgenerator(arg): + _ordered = True + _defer_construct = True + elif inspect.isfunction(arg): + _ordered = True + _defer_construct = True + else: + raise TypeError( + "Cannot create a Set from data that does not support " + "__contains__. Expected set-like object supporting " + "collections.abc.Collection interface, but received '%s'." + % (type(arg).__name__,) + ) elif arg.__class__ is type: # This catches the (deprecated) RealSet API. return process_setarg(arg()) @@ -221,7 +233,10 @@ def process_setarg(arg): # Or we can do the simple thing and just use SetOf: # # ans = SetOf(arg) - return ans + _anonymous = ComponentSet((ans,)) + if getattr(ans, '_anonymous_sets', None) is not None: + _anonymous.update(_anonymous_sets) + return ans, _anonymous @deprecated( @@ -308,11 +323,22 @@ def intersect(self, other): else: self._set = SetIntersectInitializer(self._set, other) - def __call__(self, parent, idx): + def __call__(self, parent, idx, obj): if self._set is None: return Any - else: - return process_setarg(self._set(parent, idx)) + _ans, _anonymous = process_setarg(self._set(parent, idx)) + if _anonymous: + pc = obj.parent_component() + if getattr(pc, '_anonymous_sets', None) is None: + pc._anonymous_sets = _anonymous + else: + pc._anonymous_sets.update(_anonymous) + for _set in _anonymous: + _set._parent = pc._parent + if pc._constructed: + for _set in _anonymous: + _set.construct() + return _ans def constant(self): return self._set is None or self._set.constant() @@ -483,16 +509,8 @@ class _NotFound(object): pass -# A trivial class that we can use to test if an object is a "legitimate" -# set (either ScalarSet, or a member of an IndexedSet) -class _SetDataBase(ComponentData): - """The base for all objects that can be used as a component indexing set.""" - - __slots__ = () - - -class _SetData(_SetDataBase): - """The base for all Pyomo AML objects that can be used as a component +class SetData(ComponentData): + """The base for all Pyomo objects that can be used as a component indexing set. Derived versions of this class can be used as the Index for any @@ -505,13 +523,13 @@ def __contains__(self, value): ans = self.get(value, _NotFound) except TypeError: # In Python 3.x, Sets are unhashable - if isinstance(value, _SetData): + if isinstance(value, SetData): ans = _NotFound else: raise if ans is _NotFound: - if isinstance(value, _SetData): + if isinstance(value, SetData): deprecation_warning( "Testing for set subsets with 'a in b' is deprecated. " "Use 'a.issubset(b)'.", @@ -543,7 +561,7 @@ def isordered(self): def subsets(self, expand_all_set_operators=None): return iter((self,)) - def __iter__(self): + def __iter__(self) -> Iterator[typingAny]: """Iterate over the set members Raises AttributeError for non-finite sets. This must be @@ -563,6 +581,8 @@ def __eq__(self, other): # ranges (or no ranges). We will re-generate non-finite sets to # make sure we get an accurate "finiteness" flag. if hasattr(other, 'isfinite'): + if not other.parent_component().is_constructed(): + return False other_isfinite = other.isfinite() if not other_isfinite: try: @@ -863,7 +883,7 @@ def _get_continuous_interval(self): @property @deprecated("The 'virtual' attribute is no longer supported", version='5.7') def virtual(self): - return isinstance(self, (_AnySet, SetOperator, _InfiniteRangeSetData)) + return isinstance(self, (_AnySet, SetOperator, InfiniteRangeSetData)) @virtual.setter def virtual(self, value): @@ -1126,33 +1146,23 @@ def cross(self, *args): def __ror__(self, other): # See the discussion of Set vs SetOf in process_setarg above - # - # return SetOf(other) | self - return process_setarg(other) | self + return SetUnion(other, self) def __rand__(self, other): # See the discussion of Set vs SetOf in process_setarg above - # - # return SetOf(other) & self - return process_setarg(other) & self + return SetIntersection(other, self) def __rsub__(self, other): # See the discussion of Set vs SetOf in process_setarg above - # - # return SetOf(other) - self - return process_setarg(other) - self + return SetDifference(other, self) def __rxor__(self, other): # See the discussion of Set vs SetOf in process_setarg above - # - # return SetOf(other) ^ self - return process_setarg(other) ^ self + return SetSymmetricDifference(other, self) def __rmul__(self, other): # See the discussion of Set vs SetOf in process_setarg above - # - # return SetOf(other) * self - return process_setarg(other) * self + return SetProduct(other, self) def __lt__(self, other): """ @@ -1167,6 +1177,16 @@ def __gt__(self, other): return self >= other and not self == other +class _SetData(metaclass=RenamedClass): + __renamed__new_class__ = SetData + __renamed__version__ = '6.7.2' + + +class _SetDataBase(metaclass=RenamedClass): + __renamed__new_class__ = SetData + __renamed__version__ = '6.7.2' + + class _FiniteSetMixin(object): __slots__ = () @@ -1273,14 +1293,14 @@ def ranges(self): yield NonNumericRange(i) -class _FiniteSetData(_FiniteSetMixin, _SetData): +class FiniteSetData(_FiniteSetMixin, SetData): """A general unordered iterable Set""" __slots__ = ('_values', '_domain', '_validate', '_filter', '_dimen') def __init__(self, component): - _SetData.__init__(self, component=component) - # Derived classes (like _OrderedSetData) may want to change the + SetData.__init__(self, component=component) + # Derived classes (like OrderedSetData) may want to change the # storage if not hasattr(self, '_values'): self._values = set() @@ -1318,7 +1338,7 @@ def __len__(self): return len(self._values) def __str__(self): - if self.parent_block() is not None: + if self.parent_component()._name is not None: return self.name if not self.parent_component()._constructed: return type(self).__name__ @@ -1356,8 +1376,10 @@ def add(self, *values): else: # If we are not normalizing indices, then we cannot reliably # infer the set dimen + _d = 1 + if isinstance(value, Sequence) and self.dimen != 1: + _d = len(value) _value = value - _d = None if _value not in self._domain: raise ValueError( "Cannot add value %s to Set %s.\n" @@ -1447,6 +1469,11 @@ def pop(self): return self._values.pop() +class _FiniteSetData(metaclass=RenamedClass): + __renamed__new_class__ = FiniteSetData + __renamed__version__ = '6.7.2' + + class _ScalarOrderedSetMixin(object): # This mixin is required because scalar ordered sets implement # __getitem__() as an alias of at() @@ -1584,41 +1611,39 @@ def _to_0_based_index(self, item): # implementation does not guarantee that the index is valid (it # could be outside of abs(i) <= len(self)). try: - if item != int(item): - raise IndexError( - "%s indices must be integers, not %s" - % (self.name, type(item).__name__) - ) - item = int(item) + _item = int(item) + if item != _item: + raise IndexError() except: raise IndexError( - "%s indices must be integers, not %s" % (self.name, type(item).__name__) - ) - - if item >= 1: - return item - 1 - elif item < 0: - item += len(self) - if item < 0: - raise IndexError("%s index out of range" % (self.name,)) - return item + f"Set '{self.name}' positional indices must be integers, " + f"not {type(item).__name__}" + ) from None + + if _item >= 1: + return _item - 1 + elif _item < 0: + _item += len(self) + if _item < 0: + raise IndexError(f"{self.name} index out of range") + return _item else: raise IndexError( - "Pyomo Sets are 1-indexed: valid index values for Sets are " - "[1 .. len(Set)] or [-1 .. -len(Set)]" + "Accessing Pyomo Sets by position is 1-based: valid Set positional " + "index values are [1 .. len(Set)] or [-1 .. -len(Set)]" ) -class _OrderedSetData(_OrderedSetMixin, _FiniteSetData): +class OrderedSetData(_OrderedSetMixin, FiniteSetData): """ This class defines the base class for an ordered set of concrete data. In older Pyomo terms, this defines a "concrete" ordered set - that is, a set that "owns" the list of set members. While this class actually implements a set ordered by insertion order, we make the "official" - _InsertionOrderSetData an empty derivative class, so that + InsertionOrderSetData an empty derivative class, so that - issubclass(_SortedSetData, _InsertionOrderSetData) == False + issubclass(SortedSetData, InsertionOrderSetData) == False Constructor Arguments: component The Set object that owns this data. @@ -1631,7 +1656,7 @@ class _OrderedSetData(_OrderedSetMixin, _FiniteSetData): def __init__(self, component): self._values = {} self._ordered_values = [] - _FiniteSetData.__init__(self, component=component) + FiniteSetData.__init__(self, component=component) def _iter_impl(self): """ @@ -1683,7 +1708,7 @@ def at(self, index): try: return self._ordered_values[i] except IndexError: - raise IndexError("%s index out of range" % (self.name)) + raise IndexError(f"{self.name} index out of range") from None def ord(self, item): """ @@ -1709,7 +1734,12 @@ def ord(self, item): raise ValueError("%s.ord(x): x not in %s" % (self.name, self.name)) -class _InsertionOrderSetData(_OrderedSetData): +class _OrderedSetData(metaclass=RenamedClass): + __renamed__new_class__ = OrderedSetData + __renamed__version__ = '6.7.2' + + +class InsertionOrderSetData(OrderedSetData): """ This class defines the data for a ordered set where the items are ordered in insertion order (similar to Python's OrderedSet. @@ -1730,7 +1760,7 @@ def set_value(self, val): "This WILL potentially lead to nondeterministic behavior " "in Pyomo" % (type(val).__name__,) ) - super(_InsertionOrderSetData, self).set_value(val) + super(InsertionOrderSetData, self).set_value(val) def update(self, values): if type(values) in Set._UnorderedInitializers: @@ -1740,7 +1770,12 @@ def update(self, values): "This WILL potentially lead to nondeterministic behavior " "in Pyomo" % (type(values).__name__,) ) - super(_InsertionOrderSetData, self).update(values) + super(InsertionOrderSetData, self).update(values) + + +class _InsertionOrderSetData(metaclass=RenamedClass): + __renamed__new_class__ = InsertionOrderSetData + __renamed__version__ = '6.7.2' class _SortedSetMixin(object): @@ -1755,7 +1790,7 @@ def sorted_iter(self): return iter(self) -class _SortedSetData(_SortedSetMixin, _OrderedSetData): +class SortedSetData(_SortedSetMixin, OrderedSetData): """ This class defines the data for a sorted set. @@ -1770,7 +1805,7 @@ class _SortedSetData(_SortedSetMixin, _OrderedSetData): def __init__(self, component): # An empty set is sorted... self._is_sorted = True - _OrderedSetData.__init__(self, component=component) + OrderedSetData.__init__(self, component=component) def _iter_impl(self): """ @@ -1778,12 +1813,12 @@ def _iter_impl(self): """ if not self._is_sorted: self._sort() - return super(_SortedSetData, self)._iter_impl() + return super(SortedSetData, self)._iter_impl() def __reversed__(self): if not self._is_sorted: self._sort() - return super(_SortedSetData, self).__reversed__() + return super(SortedSetData, self).__reversed__() def _add_impl(self, value): # Note that the sorted status has no bearing on insertion, @@ -1797,7 +1832,7 @@ def _add_impl(self, value): # def discard(self, val): def clear(self): - super(_SortedSetData, self).clear() + super(SortedSetData, self).clear() self._is_sorted = True def at(self, index): @@ -1809,7 +1844,7 @@ def at(self, index): """ if not self._is_sorted: self._sort() - return super(_SortedSetData, self).at(index) + return super(SortedSetData, self).at(index) def ord(self, item): """ @@ -1821,7 +1856,7 @@ def ord(self, item): """ if not self._is_sorted: self._sort() - return super(_SortedSetData, self).ord(item) + return super(SortedSetData, self).ord(item) def sorted_data(self): return self.data() @@ -1834,6 +1869,11 @@ def _sort(self): self._is_sorted = True +class _SortedSetData(metaclass=RenamedClass): + __renamed__new_class__ = SortedSetData + __renamed__version__ = '6.7.2' + + ############################################################################ _SET_API = (('__contains__', 'test membership in'), 'get', 'ranges', 'bounds') @@ -1949,6 +1989,12 @@ class SortedOrder(object): _ValidOrderedAuguments = {True, False, InsertionOrder, SortedOrder} _UnorderedInitializers = {set} + @overload + def __new__(cls: Type[Set], *args, **kwds) -> Union[SetData, IndexedSet]: ... + + @overload + def __new__(cls: Type[OrderedScalarSet], *args, **kwds) -> OrderedScalarSet: ... + def __new__(cls, *args, **kwds): if cls is not Set: return super(Set, cls).__new__(cls) @@ -1958,7 +2004,7 @@ def __new__(cls, *args, **kwds): # Many things are easier by forcing it to be consistent across # the set (namely, the _ComponentDataClass is constant). # However, it is a bit off that 'ordered' it the only arg NOT - # processed by Initializer. We can mock up a _SortedSetData + # processed by Initializer. We can mock up a SortedSetData # sort function that preserves Insertion Order (lambda x: x), but # the unsorted is harder (it would effectively be insertion # order, but ordered() may not be deterministic based on how the @@ -2003,11 +2049,11 @@ def __new__(cls, *args, **kwds): else: newObj = super(Set, cls).__new__(IndexedSet) if ordered is Set.InsertionOrder: - newObj._ComponentDataClass = _InsertionOrderSetData + newObj._ComponentDataClass = InsertionOrderSetData elif ordered is Set.SortedOrder: - newObj._ComponentDataClass = _SortedSetData + newObj._ComponentDataClass = SortedSetData else: - newObj._ComponentDataClass = _FiniteSetData + newObj._ComponentDataClass = FiniteSetData return newObj @overload @@ -2023,9 +2069,8 @@ def __init__( filter=None, validate=None, name=None, - doc=None - ): - ... + doc=None, + ): ... def __init__(self, *args, **kwds): kwds.setdefault('ctype', Set) @@ -2091,7 +2136,7 @@ def __init__(self, *args, **kwds): # order to correctly parse the data stream. if not self.is_indexed(): if self._init_domain.constant(): - self._domain = self._init_domain(self.parent_block(), None) + self._domain = self._init_domain(self.parent_block(), None, self) if self._init_dimen.constant(): self._dimen = self._init_dimen(self.parent_block(), None) @@ -2107,10 +2152,16 @@ def check_values(self): def construct(self, data=None): if self._constructed: return + self._constructed = True + timer = ConstructionTimer(self) if is_debug_set(logger): - logger.debug("Constructing Set, name=%s, from data=%r" % (self.name, data)) - self._constructed = True + logger.debug("Constructing Set, name=%s, from data=%r" % (self, data)) + + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() + if data is not None: # Data supplied to construct() should override data provided # to the constructor @@ -2146,7 +2197,7 @@ def _getitem_when_not_present(self, index): """Returns the default component data value.""" # Because we allow sets within an IndexedSet to have different # dimen, we have moved the tuplization logic from PyomoModel - # into Set (because we cannot know the dimen of a _SetData until + # into Set (because we cannot know the dimen of a SetData until # we are actually constructing that index). This also means # that we need to potentially communicate the dimen to the # (wrapped) value initializer. So, we will get the dimen first, @@ -2165,7 +2216,9 @@ def _getitem_when_not_present(self, index): ) _d = None - domain = self._init_domain(_block, index) + domain = self._init_domain(_block, index, self) + if domain is not None: + domain.construct() if _d is UnknownSetDimen and domain is not None and domain.dimen is not None: _d = domain.dimen @@ -2189,11 +2242,9 @@ def _getitem_when_not_present(self, index): else: obj = self._data[index] = self._ComponentDataClass(component=self) obj._index = index + obj._domain = domain if _d is not UnknownSetDimen: obj._dimen = _d - if domain is not None: - obj._domain = domain - domain.parent_component().construct() if self._init_validate is not None: try: obj._validate = Initializer(self._init_validate(_block, index)) @@ -2240,9 +2291,11 @@ def _getitem_when_not_present(self, index): % ( self.name, ("[%s]" % (index,) if self.is_indexed() else ""), - _values - if _values.__class__ is type - else type(_values).__name__, + ( + _values + if _values.__class__ is type + else type(_values).__name__ + ), ) ) raise @@ -2304,7 +2357,7 @@ def _pprint(self): # else: # return '{' + str(ans)[1:-1] + "}" - # TBD: In the current design, we force all _SetData within an + # TBD: In the current design, we force all SetData within an # indexed Set to have the same isordered value, so we will only # print it once in the header. Is this a good design? try: @@ -2324,7 +2377,7 @@ def _pprint(self): _ordered = "Sorted" else: _ordered = "{user}" - elif issubclass(_refClass, _InsertionOrderSetData): + elif issubclass(_refClass, InsertionOrderSetData): _ordered = "Insertion" return ( [ @@ -2348,10 +2401,15 @@ def data(self): "Return a dict containing the data() of each Set in this IndexedSet" return {k: v.data() for k, v in self.items()} + @overload + def __getitem__(self, index) -> SetData: ... + + __getitem__ = IndexedComponent.__getitem__ # type: ignore -class FiniteScalarSet(_FiniteSetData, Set): + +class FiniteScalarSet(FiniteSetData, Set): def __init__(self, **kwds): - _FiniteSetData.__init__(self, component=self) + FiniteSetData.__init__(self, component=self) Set.__init__(self, **kwds) self._index = UnindexedComponent_index @@ -2361,13 +2419,13 @@ class FiniteSimpleSet(metaclass=RenamedClass): __renamed__version__ = '6.0' -class OrderedScalarSet(_ScalarOrderedSetMixin, _InsertionOrderSetData, Set): +class OrderedScalarSet(_ScalarOrderedSetMixin, InsertionOrderSetData, Set): def __init__(self, **kwds): # In case someone inherits from us, we will provide a rational # default for the "ordered" flag kwds.setdefault('ordered', Set.InsertionOrder) - _InsertionOrderSetData.__init__(self, component=self) + InsertionOrderSetData.__init__(self, component=self) Set.__init__(self, **kwds) @@ -2376,13 +2434,13 @@ class OrderedSimpleSet(metaclass=RenamedClass): __renamed__version__ = '6.0' -class SortedScalarSet(_ScalarOrderedSetMixin, _SortedSetData, Set): +class SortedScalarSet(_ScalarOrderedSetMixin, SortedSetData, Set): def __init__(self, **kwds): # In case someone inherits from us, we will provide a rational # default for the "ordered" flag kwds.setdefault('ordered', Set.SortedOrder) - _SortedSetData.__init__(self, component=self) + SortedSetData.__init__(self, component=self) Set.__init__(self, **kwds) self._index = UnindexedComponent_index @@ -2425,14 +2483,14 @@ class AbstractSortedSimpleSet(metaclass=RenamedClass): ############################################################################ -class SetOf(_SetData, Component): +class SetOf(SetData, Component): """""" def __new__(cls, *args, **kwds): if cls is not SetOf: return super(SetOf, cls).__new__(cls) (reference,) = args - if isinstance(reference, (_SetData, GlobalSetBase)): + if isinstance(reference, (SetData, GlobalSetBase)): if reference.isfinite(): if reference.isordered(): return super(SetOf, cls).__new__(OrderedSetOf) @@ -2446,30 +2504,30 @@ def __new__(cls, *args, **kwds): return super(SetOf, cls).__new__(FiniteSetOf) def __init__(self, reference, **kwds): - _SetData.__init__(self, component=self) + SetData.__init__(self, component=self) kwds.setdefault('ctype', SetOf) Component.__init__(self, **kwds) self._ref = reference + self.construct() def __str__(self): - if self.parent_block() is not None: + if self._name is not None: return self.name return str(self._ref) def construct(self, data=None): if self._constructed: return + self._constructed = True + timer = ConstructionTimer(self) if is_debug_set(logger): - logger.debug( - "Constructing SetOf, name=%s, from data=%r" % (self.name, data) - ) - self._constructed = True + logger.debug("Constructing SetOf, name=%s, from data=%r" % (self, data)) timer.report() @property def dimen(self): - if isinstance(self._ref, _SetData): + if isinstance(self._ref, SetData): return self._ref.dimen _iter = iter(self) try: @@ -2545,7 +2603,7 @@ def at(self, index): try: return self._ref[i] except IndexError: - raise IndexError("%s index out of range" % (self.name)) + raise IndexError(f"{self.name} index out of range") from None def ord(self, item): # The bulk of single-value set members are stored as scalars. @@ -2564,7 +2622,7 @@ def ord(self, item): ############################################################################ -class _InfiniteRangeSetData(_SetData): +class InfiniteRangeSetData(SetData): """Data class for a infinite set. This Set implements an interface to an *infinite set* defined by one @@ -2576,7 +2634,7 @@ class _InfiniteRangeSetData(_SetData): __slots__ = ('_ranges',) def __init__(self, component): - _SetData.__init__(self, component=component) + SetData.__init__(self, component=component) self._ranges = None def get(self, value, default=None): @@ -2609,8 +2667,13 @@ def ranges(self): return iter(self._ranges) -class _FiniteRangeSetData( - _SortedSetMixin, _OrderedSetMixin, _FiniteSetMixin, _InfiniteRangeSetData +class _InfiniteRangeSetData(metaclass=RenamedClass): + __renamed__new_class__ = InfiniteRangeSetData + __renamed__version__ = '6.7.2' + + +class FiniteRangeSetData( + _SortedSetMixin, _OrderedSetMixin, _FiniteSetMixin, InfiniteRangeSetData ): __slots__ = () @@ -2633,7 +2696,7 @@ def _iter_impl(self): # iterate over it nIters = len(self._ranges) - 1 if not nIters: - yield from _FiniteRangeSetData._range_gen(self._ranges[0]) + yield from FiniteRangeSetData._range_gen(self._ranges[0]) return # The trick here is that we need to remove any duplicates from @@ -2644,7 +2707,7 @@ def _iter_impl(self): for r in self._ranges: # Note: there should always be at least 1 member in each # NumericRange - i = _FiniteRangeSetData._range_gen(r) + i = FiniteRangeSetData._range_gen(r) iters.append([next(i), i]) iters.sort(reverse=True, key=lambda x: x[0]) @@ -2669,7 +2732,7 @@ def __len__(self): if r.start == r.end: return 1 else: - return (r.end - r.start) // r.step + 1 + return int((r.end - r.start) // r.step) + 1 else: return sum(1 for _ in self) @@ -2686,7 +2749,7 @@ def at(self, index): if not idx: return ans idx -= 1 - raise IndexError("%s index out of range" % (self.name,)) + raise IndexError(f"{self.name} index out of range") def ord(self, item): if len(self._ranges) == 1: @@ -2710,11 +2773,16 @@ def ord(self, item): ) # We must redefine ranges(), bounds(), and domain so that we get the - # _InfiniteRangeSetData version and not the one from + # InfiniteRangeSetData version and not the one from # _FiniteSetMixin. - bounds = _InfiniteRangeSetData.bounds - ranges = _InfiniteRangeSetData.ranges - domain = _InfiniteRangeSetData.domain + bounds = InfiniteRangeSetData.bounds + ranges = InfiniteRangeSetData.ranges + domain = InfiniteRangeSetData.domain + + +class _FiniteRangeSetData(metaclass=RenamedClass): + __renamed__new_class__ = FiniteRangeSetData + __renamed__version__ = '6.7.2' @ModelComponentFactory.register( @@ -2861,9 +2929,8 @@ def __init__( filter=None, validate=None, name=None, - doc=None - ): - ... + doc=None, + ): ... @overload def __init__( @@ -2878,9 +2945,8 @@ def __init__( filter=None, validate=None, name=None, - doc=None - ): - ... + doc=None, + ): ... @overload def __init__( @@ -2892,9 +2958,8 @@ def __init__( filter=None, validate=None, name=None, - doc=None - ): - ... + doc=None, + ): ... def __init__(self, *args, **kwds): # Finite was processed by __new__ @@ -2936,14 +3001,12 @@ def __init__(self, *args, **kwds): pass def __str__(self): - if self.parent_block() is not None: + # Named components should return their name e.g., Reals + if self._name is not None: return self.name # Unconstructed floating components return their type if not self._constructed: return type(self).__name__ - # Named, constructed components should return their name e.g., Reals - if type(self).__name__ != self._name: - return self.name # Floating, unnamed constructed components return their ranges() ans = ' | '.join(str(_) for _ in self.ranges()) if ' | ' in ans: @@ -2956,11 +3019,16 @@ def __str__(self): def construct(self, data=None): if self._constructed: return + timer = ConstructionTimer(self) if is_debug_set(logger): - logger.debug( - "Constructing RangeSet, name=%s, from data=%r" % (self.name, data) - ) + logger.debug("Constructing RangeSet, name=%s, from data=%r" % (self, data)) + # Note: we cannot set the constructed flag until after we have + # generated the debug message: the debug message needs the name, + # which in turn may need ranges(), which has not been + # constructed. + self._constructed = True + if data is not None: raise ValueError( "RangeSet.construct() does not support the data= argument.\n" @@ -2968,19 +3036,9 @@ def construct(self, data=None): "as numbers, constants, or Params to the RangeSet() " "declaration" ) - self._constructed = True args, ranges = self._init_data - if any(not is_constant(arg) for arg in args): - logger.warning( - "Constructing RangeSet '%s' from non-constant data (e.g., " - "Var or mutable Param). The linkage between this RangeSet " - "and the original source data will be broken, so updating " - "the data value in the future will not be reflected in this " - "RangeSet. To suppress this warning, explicitly convert " - "the source data to a constant type (e.g., float, int, or " - "immutable Param)" % (self.name,) - ) + nonconstant_data_warning = any(not is_constant(arg) for arg in args) args = tuple(value(arg) for arg in args) if type(ranges) is not tuple: ranges = tuple(ranges) @@ -3091,7 +3149,7 @@ def construct(self, data=None): old_ranges.reverse() while old_ranges: r = old_ranges.pop() - for i, val in enumerate(_FiniteRangeSetData._range_gen(r)): + for i, val in enumerate(FiniteRangeSetData._range_gen(r)): if not _filter(_block, val): split_r = r.range_difference((NumericRange(val, val, 0),)) if len(split_r) == 2: @@ -3141,6 +3199,22 @@ def construct(self, data=None): "Set %s" % (val, self.name) ) + # Defer the warning about non-constant args until after the + # component has been constructed, so that the conversion of the + # component to a rational string will work (anonymous RangeSets + # will report their ranges, which aren't present until + # construction is over) + if nonconstant_data_warning: + logger.warning( + "Constructing RangeSet '%s' from non-constant data (e.g., " + "Var or mutable Param). The linkage between this RangeSet " + "and the original source data will be broken, so updating " + "the data value in the future will not be reflected in this " + "RangeSet. To suppress this warning, explicitly convert " + "the source data to a constant type (e.g., float, int, or " + "immutable Param)" % (self,) + ) + timer.report() # @@ -3173,9 +3247,9 @@ def _pprint(self): ) -class InfiniteScalarRangeSet(_InfiniteRangeSetData, RangeSet): +class InfiniteScalarRangeSet(InfiniteRangeSetData, RangeSet): def __init__(self, *args, **kwds): - _InfiniteRangeSetData.__init__(self, component=self) + InfiniteRangeSetData.__init__(self, component=self) RangeSet.__init__(self, *args, **kwds) self._index = UnindexedComponent_index @@ -3188,9 +3262,9 @@ class InfiniteSimpleRangeSet(metaclass=RenamedClass): __renamed__version__ = '6.0' -class FiniteScalarRangeSet(_ScalarOrderedSetMixin, _FiniteRangeSetData, RangeSet): +class FiniteScalarRangeSet(_ScalarOrderedSetMixin, FiniteRangeSetData, RangeSet): def __init__(self, *args, **kwds): - _FiniteRangeSetData.__init__(self, component=self) + FiniteRangeSetData.__init__(self, component=self) RangeSet.__init__(self, *args, **kwds) self._index = UnindexedComponent_index @@ -3228,37 +3302,43 @@ class AbstractFiniteSimpleRangeSet(metaclass=RenamedClass): ############################################################################ -class SetOperator(_SetData, Set): +class SetOperator(SetData, Set): __slots__ = ('_sets',) def __init__(self, *args, **kwds): - _SetData.__init__(self, component=self) + SetData.__init__(self, component=self) Set.__init__(self, **kwds) - implicit = [] - sets = [] - for _set in args: - _new_set = process_setarg(_set) - sets.append(_new_set) - if _new_set is not _set or _new_set.parent_block() is None: - implicit.append(_new_set) - self._sets = tuple(sets) - self._implicit_subsets = tuple(implicit) - # We will implicitly construct all set operators if the operands - # are all constructed. + self._sets, _anonymous = zip(*(process_setarg(_set) for _set in args)) + _anonymous = tuple(filter(None, _anonymous)) + if _anonymous: + self._anonymous_sets = ComponentSet() + for _set in _anonymous: + self._anonymous_sets.update(_set) + # We will immediately construct all set operators if the operands + # are all themselves constructed. if all(_.parent_component()._constructed for _ in self._sets): self.construct() def construct(self, data=None): if self._constructed: return + self._constructed = True + timer = ConstructionTimer(self) if is_debug_set(logger): logger.debug( - "Constructing SetOperator, name=%s, from data=%r" % (self.name, data) + "Constructing SetOperator, name=%s, from data=%r" % (self, data) ) - for s in self._sets: - s.parent_component().construct() - super(SetOperator, self).construct() + + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() + + # This ensures backwards compatibility by causing all scalar + # sets (including set operators) to be initialized (and + # potentially empty) after construct(). + self._getitem_when_not_present(None) + if data: deprecation_warning( "Providing construction data to SetOperator objects is " @@ -3279,7 +3359,7 @@ def construct(self, data=None): if fail: raise ValueError( "Constructing SetOperator %s with incompatible data " - "(data=%s}" % (self.name, data) + "(data=%s}" % (self, data) ) timer.report() @@ -3305,7 +3385,7 @@ def __len__(self): ) def __str__(self): - if self.parent_block() is not None: + if self._name is not None: return self.name return self._expression_str() @@ -3410,7 +3490,7 @@ def _domain(self, val): def _checkArgs(*sets): ans = [] for s in sets: - if isinstance(s, _SetDataBase): + if isinstance(s, SetData): ans.append((s.isordered(), s.isfinite())) elif type(s) in {tuple, list}: ans.append((True, True)) @@ -3505,7 +3585,7 @@ def at(self, index): if val not in self._sets[0]: idx -= 1 except StopIteration: - raise IndexError("%s index out of range" % (self.name,)) + raise IndexError(f"{self.name} index out of range") from None return val def ord(self, item): @@ -3642,7 +3722,7 @@ def at(self, index): idx -= 1 return next(_iter) except StopIteration: - raise IndexError("%s index out of range" % (self.name,)) + raise IndexError(f"{self.name} index out of range") from None def ord(self, item): """ @@ -3736,7 +3816,7 @@ def at(self, index): idx -= 1 return next(_iter) except StopIteration: - raise IndexError("%s index out of range" % (self.name,)) + raise IndexError(f"{self.name} index out of range") from None def ord(self, item): """ @@ -3846,7 +3926,7 @@ def at(self, index): idx -= 1 return next(_iter) except StopIteration: - raise IndexError("%s index out of range" % (self.name,)) + raise IndexError(f"{self.name} index out of range") from None def ord(self, item): """ @@ -3899,7 +3979,7 @@ def bounds(self): @property def dimen(self): if not (FLATTEN_CROSS_PRODUCT and normalize_index.flatten): - return None + return len(self._sets) # By convention, "None" trumps UnknownSetDimen. That is, a set # product is "non-dimentioned" if any term is non-dimentioned, # even if we do not yet know the dimentionality of another term. @@ -4128,7 +4208,7 @@ def at(self, index): i -= 1 _ord[i], _idx = _idx % _ord[i], _idx // _ord[i] if _idx: - raise IndexError("%s index out of range" % (self.name,)) + raise IndexError(f"{self.name} index out of range") ans = tuple(s.at(i + 1) for s, i in zip(self._sets, _ord)) if FLATTEN_CROSS_PRODUCT and normalize_index.flatten and self.dimen != len(ans): return self._flatten_product(ans) @@ -4166,9 +4246,9 @@ def ord(self, item): ############################################################################ -class _AnySet(_SetData, Set): +class _AnySet(SetData, Set): def __init__(self, **kwds): - _SetData.__init__(self, component=self) + SetData.__init__(self, component=self) # There is a chicken-and-egg game here: the SetInitializer uses # Any as part of the processing of the domain/within/bounds # domain restrictions. However, Any has not been declared when @@ -4177,6 +4257,7 @@ def __init__(self, **kwds): # accept (and ignore) this value. kwds.setdefault('domain', self) Set.__init__(self, **kwds) + self.construct() def get(self, val, default=None): return val if val is not Ellipsis else default @@ -4204,7 +4285,7 @@ def domain(self): return Any def __str__(self): - if self.parent_block() is not None: + if self._name is not None: return self.name return type(self).__name__ @@ -4221,10 +4302,11 @@ def get(self, val, default=None): return super(_AnyWithNoneSet, self).get(val, default) -class _EmptySet(_FiniteSetMixin, _SetData, Set): +class _EmptySet(_FiniteSetMixin, SetData, Set): def __init__(self, **kwds): - _SetData.__init__(self, component=self) + SetData.__init__(self, component=self) Set.__init__(self, **kwds) + self.construct() def get(self, val, default=None): return default @@ -4249,7 +4331,7 @@ def domain(self): return EmptySet def __str__(self): - if self.parent_block() is not None: + if self._name is not None: return self.name return type(self).__name__ @@ -4352,7 +4434,11 @@ def __new__(cls, *args, **kwds): name = base_set.name else: name = cls_name - ans = RangeSet(ranges=list(range_init(None, None).ranges()), name=name) + tmp = Set() + ans = RangeSet( + ranges=list(range_init(None, None, tmp).ranges()), name=name + ) + ans._anonymous_sets = tmp._anonymous_sets if name_kwd is None and (cls_name is not None or bounds is not None): ans._name += str(ans.bounds()) else: @@ -4382,6 +4468,9 @@ def get_interval(self): # Cache the set bounds / interval _set._bounds = obj.bounds() _set._interval = obj.get_interval() + # Now that the set is constructed, override the _anonymous_sets to + # mark the set as a global set (used by process_setarg) + _set._anonymous_sets = GlobalSetBase return _set diff --git a/pyomo/core/base/set_types.py b/pyomo/core/base/set_types.py index db9fe0f796c..80c8a41ff2e 100644 --- a/pyomo/core/base/set_types.py +++ b/pyomo/core/base/set_types.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/sets.py b/pyomo/core/base/sets.py index cbaad33c0b8..72d49479dd3 100644 --- a/pyomo/core/base/sets.py +++ b/pyomo/core/base/sets.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -13,14 +13,12 @@ # . rename 'filter' to something else # . confirm that filtering is efficient -__all__ = ['Set', 'set_options', 'simple_set_rule', 'SetOf'] - from .set import ( process_setarg, set_options, simple_set_rule, _SetDataBase, - _SetData, + SetData, Set, SetOf, IndexedSet, diff --git a/pyomo/core/base/sos.py b/pyomo/core/base/sos.py index 98cc9d28c8f..afd52c111bc 100644 --- a/pyomo/core/base/sos.py +++ b/pyomo/core/base/sos.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['SOSConstraint'] - import sys import logging @@ -30,7 +28,7 @@ logger = logging.getLogger('pyomo.core') -class _SOSConstraintData(ActiveComponentData): +class SOSConstraintData(ActiveComponentData): """ This class defines the data for a single special ordered set. @@ -103,6 +101,11 @@ def set_items(self, variables, weights): self._weights.append(w) +class _SOSConstraintData(metaclass=RenamedClass): + __renamed__new_class__ = SOSConstraintData + __renamed__version__ = '6.7.2' + + @ModelComponentFactory.register("SOS constraint expressions.") class SOSConstraint(ActiveIndexedComponent): """ @@ -514,10 +517,10 @@ def add(self, index, variables, weights=None): Add a component data for the specified index. """ if index is None: - # because ScalarSOSConstraint already makes an _SOSConstraintData instance + # because ScalarSOSConstraint already makes an SOSConstraintData instance soscondata = self else: - soscondata = _SOSConstraintData(self) + soscondata = SOSConstraintData(self) self._data[index] = soscondata soscondata._index = index @@ -551,9 +554,9 @@ def pprint(self, ostream=None, verbose=False, prefix=""): ostream.write("\t\t" + str(weight) + ' : ' + var.name + '\n') -class ScalarSOSConstraint(SOSConstraint, _SOSConstraintData): +class ScalarSOSConstraint(SOSConstraint, SOSConstraintData): def __init__(self, *args, **kwd): - _SOSConstraintData.__init__(self, self) + SOSConstraintData.__init__(self, self) SOSConstraint.__init__(self, *args, **kwd) self._index = UnindexedComponent_index diff --git a/pyomo/core/base/suffix.py b/pyomo/core/base/suffix.py index 8806bc7abcd..be2f732650d 100644 --- a/pyomo/core/base/suffix.py +++ b/pyomo/core/base/suffix.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,20 +9,34 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ('Suffix', 'active_export_suffix_generator', 'active_import_suffix_generator') - +import enum import logging -from pyomo.common.pyomo_typing import overload from pyomo.common.collections import ComponentMap +from pyomo.common.config import In +from pyomo.common.deprecation import deprecated from pyomo.common.log import is_debug_set +from pyomo.common.modeling import NOTSET +from pyomo.common.pyomo_typing import overload from pyomo.common.timing import ConstructionTimer from pyomo.core.base.component import ActiveComponent, ModelComponentFactory - -from pyomo.common.deprecation import deprecated +from pyomo.core.base.disable_methods import disable_methods +from pyomo.core.base.initializer import Initializer logger = logging.getLogger('pyomo.core') +_SUFFIX_API = ( + ('__contains__', 'test membership in'), + ('__iter__', 'iterate over'), + '__getitem__', + '__setitem__', + 'set_value', + 'set_all_values', + 'clear_value', + 'clear_all_values', + 'update_values', +) + # A list of convenient suffix generators, including: # - active_export_suffix_generator # **(used by problem writers) @@ -36,102 +50,87 @@ # - suffix_generator -def active_export_suffix_generator(a_block, datatype=False): - if datatype is False: - for name, suffix in a_block.component_map(Suffix, active=True).items(): - if suffix.export_enabled() is True: - yield name, suffix - else: - for name, suffix in a_block.component_map(Suffix, active=True).items(): - if (suffix.export_enabled() is True) and ( - suffix.get_datatype() is datatype - ): - yield name, suffix - - -def export_suffix_generator(a_block, datatype=False): - if datatype is False: - for name, suffix in a_block.component_map(Suffix).items(): - if suffix.export_enabled() is True: - yield name, suffix - else: - for name, suffix in a_block.component_map(Suffix).items(): - if (suffix.export_enabled() is True) and ( - suffix.get_datatype() is datatype - ): - yield name, suffix - - -def active_import_suffix_generator(a_block, datatype=False): - if datatype is False: - for name, suffix in a_block.component_map(Suffix, active=True).items(): - if suffix.import_enabled() is True: - yield name, suffix - else: - for name, suffix in a_block.component_map(Suffix, active=True).items(): - if (suffix.import_enabled() is True) and ( - suffix.get_datatype() is datatype - ): - yield name, suffix - - -def import_suffix_generator(a_block, datatype=False): - if datatype is False: - for name, suffix in a_block.component_map(Suffix).items(): - if suffix.import_enabled() is True: - yield name, suffix - else: - for name, suffix in a_block.component_map(Suffix).items(): - if (suffix.import_enabled() is True) and ( - suffix.get_datatype() is datatype - ): - yield name, suffix - - -def active_local_suffix_generator(a_block, datatype=False): - if datatype is False: - for name, suffix in a_block.component_map(Suffix, active=True).items(): - if suffix.get_direction() is Suffix.LOCAL: - yield name, suffix - else: - for name, suffix in a_block.component_map(Suffix, active=True).items(): - if (suffix.get_direction() is Suffix.LOCAL) and ( - suffix.get_datatype() is datatype - ): - yield name, suffix - - -def local_suffix_generator(a_block, datatype=False): - if datatype is False: - for name, suffix in a_block.component_map(Suffix).items(): - if suffix.get_direction() is Suffix.LOCAL: - yield name, suffix - else: - for name, suffix in a_block.component_map(Suffix).items(): - if (suffix.get_direction() is Suffix.LOCAL) and ( - suffix.get_datatype() is datatype - ): - yield name, suffix - - -def active_suffix_generator(a_block, datatype=False): - if datatype is False: - for name, suffix in a_block.component_map(Suffix, active=True).items(): - yield name, suffix - else: - for name, suffix in a_block.component_map(Suffix, active=True).items(): - if suffix.get_datatype() is datatype: - yield name, suffix - - -def suffix_generator(a_block, datatype=False): - if datatype is False: - for name, suffix in a_block.component_map(Suffix).items(): - yield name, suffix - else: - for name, suffix in a_block.component_map(Suffix).items(): - if suffix.get_datatype() is datatype: - yield name, suffix +def suffix_generator(a_block, datatype=NOTSET, direction=NOTSET, active=None): + _iter = a_block.component_map(Suffix, active=active).items() + if direction is not NOTSET: + direction = _SuffixDirectionDomain(direction) + if not direction: + _iter = filter(lambda item: item[1].direction == direction, _iter) + else: + _iter = filter(lambda item: item[1].direction & direction, _iter) + if datatype is not NOTSET: + _iter = filter(lambda item: item[1].datatype == datatype, _iter) + return _iter + + +def active_export_suffix_generator(a_block, datatype=NOTSET): + return suffix_generator(a_block, datatype, SuffixDirection.EXPORT, True) + + +def export_suffix_generator(a_block, datatype=NOTSET): + return suffix_generator(a_block, datatype, SuffixDirection.EXPORT) + + +def active_import_suffix_generator(a_block, datatype=NOTSET): + return suffix_generator(a_block, datatype, SuffixDirection.IMPORT, True) + + +def import_suffix_generator(a_block, datatype=NOTSET): + return suffix_generator(a_block, datatype, SuffixDirection.IMPORT) + + +def active_local_suffix_generator(a_block, datatype=NOTSET): + return suffix_generator(a_block, datatype, SuffixDirection.LOCAL, True) + + +def local_suffix_generator(a_block, datatype=NOTSET): + return suffix_generator(a_block, datatype, SuffixDirection.LOCAL) + + +def active_suffix_generator(a_block, datatype=NOTSET): + return suffix_generator(a_block, datatype, active=True) + + +class SuffixDataType(enum.IntEnum): + """Suffix data types + + AMPL only supports two data types for Suffixes: int and float. The + numeric values here are specific to the NL file format and should + not be changed without checking/updating the NL writer. + + """ + + INT = 0 + FLOAT = 4 + + +class SuffixDirection(enum.IntEnum): + """Suffix data flow definition. + + This identifies if the specific Suffix is to be sent to the solver, + read from the solver output, both, or neither: + + - LOCAL: Suffix is local to Pyomo and should not be sent to or read + from the solver. + + - EXPORT: Suffix should be sent to the solver as supplemental model + information. + + - IMPORT: Suffix values will be returned from the solver and should + be read from the solver output. + + - IMPORT_EXPORT: The Suffix is both an EXPORT and IMPORT suffix. + + """ + + LOCAL = 0 + EXPORT = 1 + IMPORT = 2 + IMPORT_EXPORT = 3 + + +_SuffixDataTypeDomain = In(SuffixDataType) +_SuffixDirectionDomain = In(SuffixDirection) @ModelComponentFactory.register("Declare a container for extraneous model data") @@ -147,29 +146,37 @@ class Suffix(ComponentMap, ActiveComponent): suffix. """ + # + # The following local (class) aliases are provided for convenience + # and backwards compatibility with The Book, 3rd ed + # + # Suffix Directions: - # If more directions are added be sure to update the error message - # in the setDirection method - # neither sent to solver or received from solver - LOCAL = 0 - # sent to solver or other external location - EXPORT = 1 - # obtained from solver or other external source - IMPORT = 2 - IMPORT_EXPORT = 3 # both - - SuffixDirections = (LOCAL, EXPORT, IMPORT, IMPORT_EXPORT) - SuffixDirectionToStr = { - LOCAL: 'Suffix.LOCAL', - EXPORT: 'Suffix.EXPORT', - IMPORT: 'Suffix.IMPORT', - IMPORT_EXPORT: 'Suffix.IMPORT_EXPORT', - } - # Suffix Datatypes - FLOAT = 4 - INT = 0 - SuffixDatatypes = (FLOAT, INT, None) - SuffixDatatypeToStr = {FLOAT: 'Suffix.FLOAT', INT: 'Suffix.INT', None: str(None)} + # - neither sent to solver or received from solver + LOCAL = SuffixDirection.LOCAL + # - sent to solver or other external location + EXPORT = SuffixDirection.EXPORT + # - obtained from solver or other external source + IMPORT = SuffixDirection.IMPORT + # - both import and export + IMPORT_EXPORT = SuffixDirection.IMPORT_EXPORT + + FLOAT = SuffixDataType.FLOAT + INT = SuffixDataType.INT + + def __new__(cls, *args, **kwargs): + if cls is not Suffix: + return super().__new__(cls) + return super().__new__(AbstractSuffix) + + def __setstate__(self, state): + super().__setstate__(state) + # As the concrete class *is* the "Suffix" base class, the normal + # implementation of deepcopy (through get/setstate) will create + # the new Suffix, and __new__ will map it to AbstractSuffix. We + # need to map constructed Suffixes back to Suffix: + if self._constructed and self.__class__ is AbstractSuffix: + self.__class__ = Suffix @overload def __init__( @@ -180,35 +187,32 @@ def __init__( initialize=None, rule=None, name=None, - doc=None - ): - ... + doc=None, + ): ... - def __init__(self, **kwds): + def __init__(self, **kwargs): # Suffix type information self._direction = None self._datatype = None self._rule = None - # The suffix direction - direction = kwds.pop('direction', Suffix.LOCAL) + # The suffix direction (note the setter performs error checking) + self.direction = kwargs.pop('direction', Suffix.LOCAL) - # The suffix datatype - datatype = kwds.pop('datatype', Suffix.FLOAT) + # The suffix datatype (note the setter performs error checking) + self.datatype = kwargs.pop('datatype', Suffix.FLOAT) # The suffix construction rule # TODO: deprecate the use of 'rule' - self._rule = kwds.pop('rule', None) - self._rule = kwds.pop('initialize', self._rule) - - # Check that keyword values make sense (these function have - # internal error checking). - self.set_direction(direction) - self.set_datatype(datatype) + self._rule = Initializer( + self._pop_from_kwargs('Suffix', kwargs, ('rule', 'initialize'), None), + treat_sequences_as_mappings=False, + allow_generators=True, + ) # Initialize base classes - kwds.setdefault('ctype', Suffix) - ActiveComponent.__init__(self, **kwds) + kwargs.setdefault('ctype', Suffix) + ActiveComponent.__init__(self, **kwargs) ComponentMap.__init__(self) if self._rule is None: @@ -219,7 +223,7 @@ def construct(self, data=None): Constructs this component, applying rule if it exists. """ if is_debug_set(logger): - logger.debug("Constructing suffix %s", self.name) + logger.debug(f"Constructing %s '%s'", self.__class__.__name__, self.name) if self._constructed is True: return @@ -228,7 +232,16 @@ def construct(self, data=None): self._constructed = True if self._rule is not None: - self.update_values(self._rule(self._parent())) + rule = self._rule + if rule.contains_indices(): + # The rule contains explicit indices (e.g., is a dict). + # Iterate over the indices, expand them, and store the + # result + block = self.parent_block() + for index in rule.indices(): + self.set_value(index, rule(block, index), expand=True) + else: + self.update_values(rule(self.parent_block(), None), expand=True) timer.report() @property @@ -239,11 +252,8 @@ def datatype(self): @datatype.setter def datatype(self, datatype): """Set the suffix datatype.""" - if datatype not in self.SuffixDatatypeToStr: - raise ValueError( - "Suffix datatype must be one of: %s. \n" - "Value given: %s" % (list(self.SuffixDatatypeToStr.values()), datatype) - ) + if datatype is not None: + datatype = _SuffixDataTypeDomain(datatype) self._datatype = datatype @property @@ -254,20 +264,7 @@ def direction(self): @direction.setter def direction(self, direction): """Set the suffix direction.""" - if direction not in self.SuffixDirectionToStr: - raise ValueError( - "Suffix direction must be one of: %s. \n" - "Value given: %s" - % (list(self.SuffixDirectionToStr.values()), direction) - ) - self._direction = direction - - @deprecated( - 'Suffix.exportEnabled is replaced with Suffix.export_enabled.', - version='4.1.10486', - ) - def exportEnabled(self): - return self.export_enabled() + self._direction = _SuffixDirectionDomain(direction) def export_enabled(self): """ @@ -276,13 +273,6 @@ def export_enabled(self): """ return bool(self._direction & Suffix.EXPORT) - @deprecated( - 'Suffix.importEnabled is replaced with Suffix.import_enabled.', - version='4.1.10486', - ) - def importEnabled(self): - return self.import_enabled() - def import_enabled(self): """ Returns True when this suffix is enabled for import from @@ -290,13 +280,6 @@ def import_enabled(self): """ return bool(self._direction & Suffix.IMPORT) - @deprecated( - 'Suffix.updateValues is replaced with Suffix.update_values.', - version='4.1.10486', - ) - def updateValues(self, data, expand=True): - return self.update_values(data, expand) - def update_values(self, data, expand=True): """ Updates the suffix data given a list of component,value @@ -316,12 +299,6 @@ def update_values(self, data, expand=True): # As implemented by MutableMapping self.update(data) - @deprecated( - 'Suffix.setValue is replaced with Suffix.set_value.', version='4.1.10486' - ) - def setValue(self, component, value, expand=True): - return self.set_value(component, value, expand) - def set_value(self, component, value, expand=True): """ Sets the value of this suffix on the specified component. @@ -339,13 +316,6 @@ def set_value(self, component, value, expand=True): else: self[component] = value - @deprecated( - 'Suffix.setAllValues is replaced with Suffix.set_all_values.', - version='4.1.10486', - ) - def setAllValues(self, value): - return self.set_all_values(value) - def set_all_values(self, value): """ Sets the value of this suffix on all components. @@ -353,34 +323,15 @@ def set_all_values(self, value): for ndx in self: self[ndx] = value - @deprecated( - 'Suffix.clearValue is replaced with Suffix.clear_value.', version='4.1.10486' - ) - def clearValue(self, component, expand=True): - return self.clear_value(component, expand) - def clear_value(self, component, expand=True): """ Clears suffix information for a component. """ if expand and component.is_indexed(): for component_ in component.values(): - try: - del self[component_] - except KeyError: - pass + self.pop(component_, None) else: - try: - del self[component] - except KeyError: - pass - - @deprecated( - 'Suffix.clearAllValues is replaced with Suffix.clear_all_values.', - version='4.1.10486', - ) - def clearAllValues(self): - return self.clear_all_values() + self.pop(component, None) def clear_all_values(self): """ @@ -389,115 +340,56 @@ def clear_all_values(self): self.clear() @deprecated( - 'Suffix.setDatatype is replaced with Suffix.set_datatype.', version='4.1.10486' + 'Suffix.set_datatype is replaced with the Suffix.datatype property', + version='6.7.1', ) - def setDatatype(self, datatype): - return self.set_datatype(datatype) - def set_datatype(self, datatype): """ Set the suffix datatype. """ - if datatype not in self.SuffixDatatypes: - raise ValueError( - "Suffix datatype must be one of: %s. \n" - "Value given: %s" - % (list(Suffix.SuffixDatatypeToStr.values()), datatype) - ) - self._datatype = datatype + self.datatype = datatype @deprecated( - 'Suffix.getDatatype is replaced with Suffix.get_datatype.', version='4.1.10486' + 'Suffix.get_datatype is replaced with the Suffix.datatype property', + version='6.7.1', ) - def getDatatype(self): - return self.get_datatype() - def get_datatype(self): """ Return the suffix datatype. """ - return self._datatype + return self.datatype @deprecated( - 'Suffix.setDirection is replaced with Suffix.set_direction.', - version='4.1.10486', + 'Suffix.set_direction is replaced with the Suffix.direction property', + version='6.7.1', ) - def setDirection(self, direction): - return self.set_direction(direction) - def set_direction(self, direction): """ Set the suffix direction. """ - if direction not in self.SuffixDirections: - raise ValueError( - "Suffix direction must be one of: %s. \n" - "Value given: %s" - % (list(self.SuffixDirectionToStr.values()), direction) - ) - self._direction = direction + self.direction = direction @deprecated( - 'Suffix.getDirection is replaced with Suffix.get_direction.', - version='4.1.10486', + 'Suffix.get_direction is replaced with the Suffix.direction property', + version='6.7.1', ) - def getDirection(self): - return self.get_direction() - def get_direction(self): """ Return the suffix direction. """ - return self._direction - - def __str__(self): - """ - Return a string representation of the suffix. If the name - attribute is None, then return '' - """ - name = self.name - if name is None: - return '' - return name + return self.direction def _pprint(self): return ( [ - ('Direction', self.SuffixDirectionToStr[self._direction]), - ('Datatype', self.SuffixDatatypeToStr[self._datatype]), + ('Direction', str(self._direction.name)), + ('Datatype', getattr(self._datatype, 'name', 'None')), ], ((str(k), v) for k, v in self._dict.values()), ("Value",), lambda k, v: [v], ) - # TODO: delete - @deprecated( - 'Suffix.getValue is replaced with the dict-interface method Suffix.get.', - version='4.1.10486', - ) - def getValue(self, component, *args): - """ - Returns the current value of this suffix for the specified - component. - """ - # As implemented by MutableMapping - return self.get(component, *args) - - # TODO: delete - @deprecated( - 'Suffix.extractValues() is replaced with ' - 'the dict-interface method Suffix.items().', - version='4.1.10486', - ) - def extractValues(self): - """ - Extract all data stored on this Suffix into a list of - component, value tuples. - """ - # As implemented by MutableMapping - return list(self.items()) - # # Override a few methods to make sure the ActiveComponent versions are # called. We can't just switch the inheritance order due to @@ -510,17 +402,10 @@ def pprint(self, *args, **kwds): def __str__(self): return ActiveComponent.__str__(self) - # - # Override NotImplementedError messages on ComponentMap base class - # - - def __eq__(self, other): - """Not implemented.""" - raise NotImplementedError("Suffix components are not comparable") - def __ne__(self, other): - """Not implemented.""" - raise NotImplementedError("Suffix components are not comparable") +@disable_methods(_SUFFIX_API) +class AbstractSuffix(Suffix): + pass class SuffixFinder(object): @@ -542,8 +427,8 @@ def __init__(self, name, default=None): """ self.name = name self.default = default - self._suffixes_by_block = ComponentMap({None: []}) self.all_suffixes = [] + self._suffixes_by_block = {None: []} def find(self, component_data): """Find suffix value for a given component data object in model tree diff --git a/pyomo/core/base/symbol_map.py b/pyomo/core/base/symbol_map.py index e4e7f9d781c..189cce7646a 100644 --- a/pyomo/core/base/symbol_map.py +++ b/pyomo/core/base/symbol_map.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/symbolic.py b/pyomo/core/base/symbolic.py index 3fa5c168207..c1ee08dd584 100644 --- a/pyomo/core/base/symbolic.py +++ b/pyomo/core/base/symbolic.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/template_expr.py b/pyomo/core/base/template_expr.py index f8ff345a1e5..c3697be7eb0 100644 --- a/pyomo/core/base/template_expr.py +++ b/pyomo/core/base/template_expr.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/transformation.py b/pyomo/core/base/transformation.py index 70d89af3798..31f5a251553 100644 --- a/pyomo/core/base/transformation.py +++ b/pyomo/core/base/transformation.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/units_container.py b/pyomo/core/base/units_container.py index 1e9685188c7..f3dec1e0db1 100644 --- a/pyomo/core/base/units_container.py +++ b/pyomo/core/base/units_container.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -119,7 +119,6 @@ value, native_types, native_numeric_types, - pyomo_constant_types, ) from pyomo.core.expr.template_expr import IndexTemplate from pyomo.core.expr.visitor import ExpressionValueVisitor @@ -127,7 +126,6 @@ pint_module, pint_available = attempt_import( 'pint', - defer_check=True, error_message=( 'The "pint" package failed to import. ' 'This package is necessary to use Pyomo units.' @@ -902,7 +900,7 @@ def initializeWalker(self, expr): def beforeChild(self, node, child, child_idx): ctype = child.__class__ - if ctype in native_types or ctype in pyomo_constant_types: + if ctype in native_types: return False, self._pint_dimensionless if child.is_expression_type(): @@ -917,7 +915,7 @@ def beforeChild(self, node, child, child_idx): pint_unit = self._pyomo_units_container._get_pint_units(pyomo_unit) return False, pint_unit - return True, None + return False, self._pint_dimensionless def exitNode(self, node, data): """Visitor callback when moving up the expression tree. @@ -1514,6 +1512,19 @@ def __getattribute__(self, attr): # present, at which point this instance __class__ will fall back # to PyomoUnitsContainer (where this method is not declared, OR # pint is not available and an ImportError will be raised. + # + # We need special case handling for __class__: gurobipy + # interrogates things by looking at their __class__ during + # python shutdown. Unfortunately, interrogating this + # singleton's __class__ evaluates `pint_available`, which - if + # DASK is installed - imports dask. Importing dask creates + # threading objects. Unfortunately, creating threading objects + # during interpreter shutdown generates a RuntimeError. So, our + # solution is to special-case the resolution of __class__ here + # to avoid accidentally triggering the imports. + if attr == "__class__": + return _DeferredUnitsSingleton + # if pint_available: # If the first thing that is being called is # "units.set_pint_registry(...)", then we will call __init__ diff --git a/pyomo/core/base/util.py b/pyomo/core/base/util.py index 867a303395b..6a3885cedfb 100644 --- a/pyomo/core/base/util.py +++ b/pyomo/core/base/util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/var.py b/pyomo/core/base/var.py index e7e9e4f8f2f..38d1d38a864 100644 --- a/pyomo/core/base/var.py +++ b/pyomo/core/base/var.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,12 +9,12 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['Var', '_VarData', '_GeneralVarData', 'VarList', 'SimpleVar', 'ScalarVar'] - +from __future__ import annotations import logging import sys from pyomo.common.pyomo_typing import overload from weakref import ref as weakref_ref +from typing import Union, Type from pyomo.common.deprecation import RenamedClass from pyomo.common.log import is_debug_set @@ -29,7 +29,6 @@ value, is_potentially_variable, native_numeric_types, - native_types, ) from pyomo.core.base.component import ComponentData, ModelComponentFactory from pyomo.core.base.global_set import UnindexedComponent_index @@ -44,7 +43,6 @@ DefaultInitializer, BoundInitializer, ) -from pyomo.core.base.misc import apply_indexed_rule from pyomo.core.base.set import ( Reals, Binary, @@ -54,7 +52,6 @@ integer_global_set_ids, ) from pyomo.core.base.units_container import units -from pyomo.core.base.util import is_functor logger = logging.getLogger('pyomo.core') @@ -66,8 +63,7 @@ + [(_, False) for _ in integer_global_set_ids] ) _VARDATA_API = ( - # including 'domain' runs afoul of logic in Block._add_implicit_sets() - # 'domain', + 'domain', 'bounds', 'lower', 'upper', @@ -89,241 +85,11 @@ 'value', 'stale', 'fixed', + ('__call__', "access property 'value' on"), ) -class _VarData(ComponentData, NumericValue): - """This class defines the abstract interface for a single variable. - - Note that this "abstract" class is not intended to be directly - instantiated. - - """ - - __slots__ = () - - # - # Interface - # - - def has_lb(self): - """Returns :const:`False` when the lower bound is - :const:`None` or negative infinity""" - return self.lb is not None - - def has_ub(self): - """Returns :const:`False` when the upper bound is - :const:`None` or positive infinity""" - return self.ub is not None - - # TODO: deprecate this? Properties are generally preferred over "set*()" - def setlb(self, val): - """ - Set the lower bound for this variable after validating that - the value is fixed (or None). - """ - self.lower = val - - # TODO: deprecate this? Properties are generally preferred over "set*()" - def setub(self, val): - """ - Set the upper bound for this variable after validating that - the value is fixed (or None). - """ - self.upper = val - - @property - def bounds(self): - """Returns (or set) the tuple (lower bound, upper bound). - - This returns the current (numeric) values of the lower and upper - bounds as a tuple. If there is no bound, returns None (and not - +/-inf) - - """ - return self.lb, self.ub - - @bounds.setter - def bounds(self, val): - self.lower, self.upper = val - - @property - def lb(self): - """Return (or set) the numeric value of the variable lower bound.""" - lb = value(self.lower) - return None if lb == _ninf else lb - - @lb.setter - def lb(self, val): - self.lower = val - - @property - def ub(self): - """Return (or set) the numeric value of the variable upper bound.""" - ub = value(self.upper) - return None if ub == _inf else ub - - @ub.setter - def ub(self, val): - self.upper = val - - def is_integer(self): - """Returns True when the domain is a contiguous integer range.""" - _id = id(self.domain) - if _id in _known_global_real_domains: - return not _known_global_real_domains[_id] - _interval = self.domain.get_interval() - if _interval is None: - return False - # Note: it is not sufficient to just check the step: the - # starting / ending points must be integers (or not specified) - start, stop, step = _interval - return ( - step == 1 - and (start is None or int(start) == start) - and (stop is None or int(stop) == stop) - ) - - def is_binary(self): - """Returns True when the domain is restricted to Binary values.""" - domain = self.domain - if domain is Binary: - return True - if id(domain) in _known_global_real_domains: - return False - return domain.get_interval() == (0, 1, 1) - - def is_continuous(self): - """Returns True when the domain is a continuous real range""" - _id = id(self.domain) - if _id in _known_global_real_domains: - return _known_global_real_domains[_id] - _interval = self.domain.get_interval() - return _interval is not None and _interval[2] == 0 - - def is_fixed(self): - """Returns True if this variable is fixed, otherwise returns False.""" - return self.fixed - - def is_constant(self): - """Returns False because this is not a constant in an expression.""" - return False - - def is_variable_type(self): - """Returns True because this is a variable.""" - return True - - def is_potentially_variable(self): - """Returns True because this is a variable.""" - return True - - def _compute_polynomial_degree(self, result): - """ - If the variable is fixed, it represents a constant - is a polynomial with degree 0. Otherwise, it has - degree 1. This method is used in expressions to - compute polynomial degree. - """ - if self.fixed: - return 0 - return 1 - - def clear(self): - self.value = None - - def __call__(self, exception=True): - """Compute the value of this variable.""" - return self.value - - # - # Abstract Interface - # - - def set_value(self, val, skip_validation=False): - """Set the current variable value.""" - raise NotImplementedError - - @property - def value(self): - """Return (or set) the value for this variable.""" - raise NotImplementedError - - @property - def domain(self): - """Return (or set) the domain for this variable.""" - raise NotImplementedError - - @property - def lower(self): - """Return (or set) an expression for the variable lower bound.""" - raise NotImplementedError - - @property - def upper(self): - """Return (or set) an expression for the variable upper bound.""" - raise NotImplementedError - - @property - def fixed(self): - """Return (or set) the fixed indicator for this variable. - - Alias for :meth:`is_fixed` / :meth:`fix` / :meth:`unfix`. - - """ - raise NotImplementedError - - @property - def stale(self): - """The stale status for this variable. - - Variables are "stale" if their current value was not updated as - part of the most recent model update. A "model update" can be - one of several things: a solver invocation, loading a previous - solution, or manually updating a non-stale :class:`Var` value. - - Returns - ------- - bool - - Notes - ----- - Fixed :class:`Var` objects will be stale after invoking a solver - (as their value was not updated by the solver). - - Updating a stale :class:`Var` value will not cause other - variable values to be come stale. However, updating the first - non-stale :class:`Var` value after a solve or solution load - *will* cause all other variables to be marked as stale - - """ - raise NotImplementedError - - def fix(self, value=NOTSET, skip_validation=False): - """Fix the value of this variable (treat as nonvariable) - - This sets the :attr:`fixed` indicator to True. If ``value`` is - provided, the value (and the ``skip_validation`` flag) are first - passed to :meth:`set_value()`. - - """ - self.fixed = True - if value is not NOTSET: - self.set_value(value, skip_validation) - - def unfix(self): - """Unfix this variable (treat as variable in solver interfaces) - - This sets the :attr:`fixed` indicator to False. - - """ - self.fixed = False - - def free(self): - """Alias for :meth:`unfix`""" - return self.unfix() - - -class _GeneralVarData(_VarData): +class VarData(ComponentData, NumericValue): """This class defines the data for a single variable.""" __slots__ = ('_value', '_lb', '_ub', '_domain', '_fixed', '_stale') @@ -333,7 +99,7 @@ def __init__(self, component=None): # # These lines represent in-lining of the # following constructors: - # - _VarData + # - VarData # - ComponentData # - NumericValue self._component = weakref_ref(component) if (component is not None) else None @@ -364,10 +130,6 @@ def copy(cls, src): self._index = src._index return self - # - # Abstract Interface - # - def set_value(self, val, skip_validation=False): """Set the current variable value. @@ -390,17 +152,22 @@ def set_value(self, val, skip_validation=False): # # Check if this Var has units: assigning dimensionless # values to a variable with units should be an error - if type(val) not in native_numeric_types: - if self.parent_component()._units is not None: - _src_magnitude = value(val) + if val.__class__ in native_numeric_types: + pass + elif self.parent_component()._units is not None: + _src_magnitude = value(val) + # Note: value() could have just registered a new numeric type + if val.__class__ in native_numeric_types: + val = _src_magnitude + else: _src_units = units.get_units(val) val = units.convert_value( num_value=_src_magnitude, from_units=_src_units, to_units=self.parent_component()._units, ) - else: - val = value(val) + else: + val = value(val) if not skip_validation: if val not in self.domain: @@ -423,20 +190,28 @@ def set_value(self, val, skip_validation=False): @property def value(self): + """Return (or set) the value for this variable.""" return self._value @value.setter def value(self, val): self.set_value(val) + def __call__(self, exception=True): + """Compute the value of this variable.""" + return self._value + @property def domain(self): + """Return (or set) the domain for this variable.""" return self._domain @domain.setter def domain(self, domain): try: - self._domain = SetInitializer(domain)(self.parent_block(), self.index()) + self._domain = SetInitializer(domain)( + self.parent_block(), self.index(), self + ) except: logger.error( "%s is not a valid domain. Variable domains must be an " @@ -445,9 +220,42 @@ def domain(self, domain): ) raise - @_VarData.bounds.getter + def has_lb(self): + """Returns :const:`False` when the lower bound is + :const:`None` or negative infinity""" + return self.lb is not None + + def has_ub(self): + """Returns :const:`False` when the upper bound is + :const:`None` or positive infinity""" + return self.ub is not None + + # TODO: deprecate this? Properties are generally preferred over "set*()" + def setlb(self, val): + """ + Set the lower bound for this variable after validating that + the value is fixed (or None). + """ + self.lower = val + + # TODO: deprecate this? Properties are generally preferred over "set*()" + def setub(self, val): + """ + Set the upper bound for this variable after validating that + the value is fixed (or None). + """ + self.upper = val + + @property def bounds(self): - # Custom implementation of _VarData.bounds to avoid unnecessary + """Returns (or set) the tuple (lower bound, upper bound). + + This returns the current (numeric) values of the lower and upper + bounds as a tuple. If there is no bound, returns None (and not + +/-inf) + + """ + # Custom implementation of lb / ub to avoid unnecessary # expression generation and duplicate calls to domain.bounds() domain_lb, domain_ub = self.domain.bounds() # lb is the tighter of the domain and bounds @@ -488,10 +296,14 @@ def bounds(self): ub = min(ub, domain_ub) return lb, ub - @_VarData.lb.getter + @bounds.setter + def bounds(self, val): + self.lower, self.upper = val + + @property def lb(self): - # Custom implementation of _VarData.lb to avoid unnecessary - # expression generation + """Return (or set) the numeric value of the variable lower bound.""" + # Note: Implementation avoids unnecessary expression generation domain_lb, domain_ub = self.domain.bounds() # lb is the tighter of the domain and bounds lb = self._lb @@ -513,10 +325,14 @@ def lb(self): lb = max(lb, domain_lb) return lb - @_VarData.ub.getter + @lb.setter + def lb(self, val): + self.lower = val + + @property def ub(self): - # Custom implementation of _VarData.ub to avoid unnecessary - # expression generation + """Return (or set) the numeric value of the variable upper bound.""" + # Note: implementation avoids unnecessary expression generation domain_lb, domain_ub = self.domain.bounds() # ub is the tighter of the domain and bounds ub = self._ub @@ -538,6 +354,10 @@ def ub(self): ub = min(ub, domain_ub) return ub + @ub.setter + def ub(self, val): + self.upper = val + @property def lower(self): """Return (or set) an expression for the variable lower bound. @@ -594,8 +414,37 @@ def get_units(self): # component if not scalar return self.parent_component()._units + def fix(self, value=NOTSET, skip_validation=False): + """Fix the value of this variable (treat as nonvariable) + + This sets the :attr:`fixed` indicator to True. If ``value`` is + provided, the value (and the ``skip_validation`` flag) are first + passed to :meth:`set_value()`. + + """ + self.fixed = True + if value is not NOTSET: + self.set_value(value, skip_validation) + + def unfix(self): + """Unfix this variable (treat as variable in solver interfaces) + + This sets the :attr:`fixed` indicator to False. + + """ + self.fixed = False + + def free(self): + """Alias for :meth:`unfix`""" + return self.unfix() + @property def fixed(self): + """Return (or set) the fixed indicator for this variable. + + Alias for :meth:`is_fixed` / :meth:`fix` / :meth:`unfix`. + + """ return self._fixed @fixed.setter @@ -604,6 +453,28 @@ def fixed(self, val): @property def stale(self): + """The stale status for this variable. + + Variables are "stale" if their current value was not updated as + part of the most recent model update. A "model update" can be + one of several things: a solver invocation, loading a previous + solution, or manually updating a non-stale :class:`Var` value. + + Returns + ------- + bool + + Notes + ----- + Fixed :class:`Var` objects will be stale after invoking a solver + (as their value was not updated by the solver). + + Updating a stale :class:`Var` value will not cause other + variable values to be come stale. However, updating the first + non-stale :class:`Var` value after a solve or solution load + *will* cause all other variables to be marked as stale + + """ return StaleFlagManager.is_stale(self._stale) @stale.setter @@ -613,11 +484,70 @@ def stale(self, val): else: self._stale = StaleFlagManager.get_flag(0) - # Note: override the base class definition to avoid a call through a - # property + def is_integer(self): + """Returns True when the domain is a contiguous integer range.""" + _id = id(self.domain) + if _id in _known_global_real_domains: + return not _known_global_real_domains[_id] + _interval = self.domain.get_interval() + if _interval is None: + return False + # Note: it is not sufficient to just check the step: the + # starting / ending points must be integers (or not specified) + start, stop, step = _interval + return ( + step == 1 + and (start is None or int(start) == start) + and (stop is None or int(stop) == stop) + ) + + def is_binary(self): + """Returns True when the domain is restricted to Binary values.""" + domain = self.domain + if domain is Binary: + return True + if id(domain) in _known_global_real_domains: + return False + return domain.get_interval() == (0, 1, 1) + + def is_continuous(self): + """Returns True when the domain is a continuous real range""" + _id = id(self.domain) + if _id in _known_global_real_domains: + return _known_global_real_domains[_id] + _interval = self.domain.get_interval() + return _interval is not None and _interval[2] == 0 + def is_fixed(self): + """Returns True if this variable is fixed, otherwise returns False.""" return self._fixed + def is_constant(self): + """Returns False because this is not a constant in an expression.""" + return False + + def is_variable_type(self): + """Returns True because this is a variable.""" + return True + + def is_potentially_variable(self): + """Returns True because this is a variable.""" + return True + + def clear(self): + self.value = None + + def _compute_polynomial_degree(self, result): + """ + If the variable is fixed, it represents a constant + is a polynomial with degree 0. Otherwise, it has + degree 1. This method is used in expressions to + compute polynomial degree. + """ + if self._fixed: + return 0 + return 1 + def _process_bound(self, val, bound_type): if type(val) in native_numeric_types or val is None: # TODO: warn/error: check if this Var has units: assigning @@ -640,6 +570,16 @@ def _process_bound(self, val, bound_type): return val +class _VarData(metaclass=RenamedClass): + __renamed__new_class__ = VarData + __renamed__version__ = '6.7.2' + + +class _GeneralVarData(metaclass=RenamedClass): + __renamed__new_class__ = VarData + __renamed__version__ = '6.7.2' + + @ModelComponentFactory.register("Decision variables.") class Var(IndexedComponent, IndexedComponent_NDArrayMixin): """A numeric variable, which may be defined over an index. @@ -665,7 +605,16 @@ class Var(IndexedComponent, IndexedComponent_NDArrayMixin): doc (str, optional): Text describing this component. """ - _ComponentDataClass = _GeneralVarData + _ComponentDataClass = VarData + + @overload + def __new__(cls: Type[Var], *args, **kwargs) -> Union[ScalarVar, IndexedVar]: ... + + @overload + def __new__(cls: Type[ScalarVar], *args, **kwargs) -> ScalarVar: ... + + @overload + def __new__(cls: Type[IndexedVar], *args, **kwargs) -> IndexedVar: ... def __new__(cls, *args, **kwargs): if cls is not Var: @@ -687,9 +636,8 @@ def __init__( dense=True, units=None, name=None, - doc=None - ): - ... + doc=None, + ): ... def __init__(self, *args, **kwargs): # @@ -764,7 +712,7 @@ def add(self, index): def construct(self, data=None): """ - Construct the _VarData objects for this variable + Construct the VarData objects for this variable """ if self._constructed: return @@ -774,6 +722,10 @@ def construct(self, data=None): if is_debug_set(logger): logger.debug("Constructing Variable %s" % (self.name,)) + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() + # Note: define 'index' to avoid 'variable referenced before # assignment' in the error message generated in the 'except:' # block below. @@ -819,7 +771,7 @@ def construct(self, data=None): # initializers that are constant, we can avoid # re-calling (and re-validating) the inputs in certain # cases. To support this, we will create the first - # _VarData and then use it as a template to initialize + # VarData and then use it as a template to initialize # (constant portions of) every VarData so as to not # repeat all the domain/bounds validation. try: @@ -854,7 +806,7 @@ def construct(self, data=None): # We can directly set the attribute (not the # property) because the SetInitializer ensures # that the value is a proper Set. - obj._domain = self._rule_domain(block, index) + obj._domain = self._rule_domain(block, index, self) if call_bounds_rule: for index, obj in self._data.items(): obj.lower, obj.upper = self._rule_bounds(block, index) @@ -891,7 +843,7 @@ def _getitem_when_not_present(self, index): obj._index = index # We can directly set the attribute (not the property) because # the SetInitializer ensures that the value is a proper Set. - obj._domain = self._rule_domain(parent, index) + obj._domain = self._rule_domain(parent, index, self) if self._rule_bounds is not None: obj.lower, obj.upper = self._rule_bounds(parent, index) if self._rule_init is not None: @@ -937,11 +889,11 @@ def _pprint(self): ) -class ScalarVar(_GeneralVarData, Var): +class ScalarVar(VarData, Var): """A single variable.""" def __init__(self, *args, **kwd): - _GeneralVarData.__init__(self, component=self) + VarData.__init__(self, component=self) Var.__init__(self, *args, **kwd) self._index = UnindexedComponent_index @@ -988,7 +940,7 @@ def fix(self, value=NOTSET, skip_validation=False): def unfix(self): """Unfix all variables in this :class:`IndexedVar` (treat as variable) - This sets the :attr:`_VarData.fixed` indicator to False for + This sets the :attr:`VarData.fixed` indicator to False for every variable in this :class:`IndexedVar`. """ @@ -1013,17 +965,17 @@ def domain(self, domain): try: domain_rule = SetInitializer(domain) if domain_rule.constant(): - domain = domain_rule(self.parent_block(), None) + domain = domain_rule(self.parent_block(), None, self) for vardata in self.values(): vardata._domain = domain elif domain_rule.contains_indices(): parent = self.parent_block() for index in domain_rule.indices(): - self[index]._domain = domain_rule(parent, index) + self[index]._domain = domain_rule(parent, index, self) else: parent = self.parent_block() for index, vardata in self.items(): - vardata._domain = domain_rule(parent, index) + vardata._domain = domain_rule(parent, index, self) except: logger.error( "%s is not a valid domain. Variable domains must be an " @@ -1042,7 +994,7 @@ def domain(self, domain): # between potentially variable GetItemExpression objects and # "constant" GetItemExpression objects. That will need to wait for # the expression rework [JDS; Nov 22]. - def __getitem__(self, args): + def __getitem__(self, args) -> VarData: try: return super().__getitem__(args) except RuntimeError: diff --git a/pyomo/core/beta/__init__.py b/pyomo/core/beta/__init__.py index d07668534c8..a2d51d0b23e 100644 --- a/pyomo/core/beta/__init__.py +++ b/pyomo/core/beta/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/beta/dict_objects.py b/pyomo/core/beta/dict_objects.py index c987d0946a3..eedb3c45bf3 100644 --- a/pyomo/core/beta/dict_objects.py +++ b/pyomo/core/beta/dict_objects.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,17 +9,15 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = () - import logging from weakref import ref as weakref_ref from pyomo.common.log import is_debug_set from pyomo.core.base.set_types import Any -from pyomo.core.base.var import IndexedVar, _VarData -from pyomo.core.base.constraint import IndexedConstraint, _ConstraintData -from pyomo.core.base.objective import IndexedObjective, _ObjectiveData -from pyomo.core.base.expression import IndexedExpression, _ExpressionData +from pyomo.core.base.var import IndexedVar, VarData +from pyomo.core.base.constraint import IndexedConstraint, ConstraintData +from pyomo.core.base.objective import IndexedObjective, ObjectiveData +from pyomo.core.base.expression import IndexedExpression, ExpressionData from collections.abc import MutableMapping from collections.abc import Mapping @@ -186,7 +184,7 @@ def __init__(self, *args, **kwds): # Constructor for ComponentDict needs to # go last in order to handle any initialization # iterable as an argument - ComponentDict.__init__(self, _VarData, *args, **kwds) + ComponentDict.__init__(self, VarData, *args, **kwds) class ConstraintDict(ComponentDict, IndexedConstraint): @@ -195,7 +193,7 @@ def __init__(self, *args, **kwds): # Constructor for ComponentDict needs to # go last in order to handle any initialization # iterable as an argument - ComponentDict.__init__(self, _ConstraintData, *args, **kwds) + ComponentDict.__init__(self, ConstraintData, *args, **kwds) class ObjectiveDict(ComponentDict, IndexedObjective): @@ -204,7 +202,7 @@ def __init__(self, *args, **kwds): # Constructor for ComponentDict needs to # go last in order to handle any initialization # iterable as an argument - ComponentDict.__init__(self, _ObjectiveData, *args, **kwds) + ComponentDict.__init__(self, ObjectiveData, *args, **kwds) class ExpressionDict(ComponentDict, IndexedExpression): @@ -213,4 +211,4 @@ def __init__(self, *args, **kwds): # Constructor for ComponentDict needs to # go last in order to handle any initialization # iterable as an argument - ComponentDict.__init__(self, _ExpressionData, *args, **kwds) + ComponentDict.__init__(self, ExpressionData, *args, **kwds) diff --git a/pyomo/core/beta/list_objects.py b/pyomo/core/beta/list_objects.py index 2c42dfa57c8..005bfc38a1f 100644 --- a/pyomo/core/beta/list_objects.py +++ b/pyomo/core/beta/list_objects.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,17 +9,15 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = () - import logging from weakref import ref as weakref_ref from pyomo.common.log import is_debug_set from pyomo.core.base.set_types import Any -from pyomo.core.base.var import IndexedVar, _VarData -from pyomo.core.base.constraint import IndexedConstraint, _ConstraintData -from pyomo.core.base.objective import IndexedObjective, _ObjectiveData -from pyomo.core.base.expression import IndexedExpression, _ExpressionData +from pyomo.core.base.var import IndexedVar, VarData +from pyomo.core.base.constraint import IndexedConstraint, ConstraintData +from pyomo.core.base.objective import IndexedObjective, ObjectiveData +from pyomo.core.base.expression import IndexedExpression, ExpressionData from collections.abc import MutableSequence @@ -234,7 +232,7 @@ def __init__(self, *args, **kwds): # Constructor for ComponentList needs to # go last in order to handle any initialization # iterable as an argument - ComponentList.__init__(self, _VarData, *args, **kwds) + ComponentList.__init__(self, VarData, *args, **kwds) class XConstraintList(ComponentList, IndexedConstraint): @@ -243,7 +241,7 @@ def __init__(self, *args, **kwds): # Constructor for ComponentList needs to # go last in order to handle any initialization # iterable as an argument - ComponentList.__init__(self, _ConstraintData, *args, **kwds) + ComponentList.__init__(self, ConstraintData, *args, **kwds) class XObjectiveList(ComponentList, IndexedObjective): @@ -252,7 +250,7 @@ def __init__(self, *args, **kwds): # Constructor for ComponentList needs to # go last in order to handle any initialization # iterable as an argument - ComponentList.__init__(self, _ObjectiveData, *args, **kwds) + ComponentList.__init__(self, ObjectiveData, *args, **kwds) class XExpressionList(ComponentList, IndexedExpression): @@ -261,4 +259,4 @@ def __init__(self, *args, **kwds): # Constructor for ComponentList needs to # go last in order to handle any initialization # iterable as an argument - ComponentList.__init__(self, _ExpressionData, *args, **kwds) + ComponentList.__init__(self, ExpressionData, *args, **kwds) diff --git a/pyomo/core/expr/__init__.py b/pyomo/core/expr/__init__.py index 5e30fceeeaa..b0ad2ac4892 100644 --- a/pyomo/core/expr/__init__.py +++ b/pyomo/core/expr/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,14 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -# -# The definition of __all__ is a bit funky here, because we want to -# expose symbols in pyomo.core.expr.current that are not included in -# pyomo.core.expr. The idea is that pyomo.core.expr provides symbols -# that are used by general users, but pyomo.core.expr.current provides -# symbols that are used by developers. -# - from . import ( numvalue, visitor, @@ -56,6 +48,7 @@ # BooleanValue, BooleanConstant, + BooleanExpression, BooleanExpressionBase, # UnaryBooleanExpression, @@ -70,6 +63,8 @@ ExactlyExpression, AtMostExpression, AtLeastExpression, + AllDifferentExpression, + CountIfExpression, # land, lnot, @@ -79,6 +74,8 @@ exactly, atleast, atmost, + all_different, + count_if, implies, ) from .numeric_expr import ( diff --git a/pyomo/core/expr/base.py b/pyomo/core/expr/base.py index b74bbff4e3c..6e2066afcc5 100644 --- a/pyomo/core/expr/base.py +++ b/pyomo/core/expr/base.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -360,7 +360,7 @@ def size(self): """ return visitor.sizeof_expression(self) - def _apply_operation(self, result): # pragma: no cover + def _apply_operation(self, result): """ Compute the values of this node given the values of its children. diff --git a/pyomo/core/expr/boolean_value.py b/pyomo/core/expr/boolean_value.py index b9c8ece29c8..002ec91be9d 100644 --- a/pyomo/core/expr/boolean_value.py +++ b/pyomo/core/expr/boolean_value.py @@ -2,7 +2,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/expr/calculus/__init__.py b/pyomo/core/expr/calculus/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/core/expr/calculus/__init__.py +++ b/pyomo/core/expr/calculus/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/core/expr/calculus/derivatives.py b/pyomo/core/expr/calculus/derivatives.py index c9787b0e309..69fe4969938 100644 --- a/pyomo/core/expr/calculus/derivatives.py +++ b/pyomo/core/expr/calculus/derivatives.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -39,11 +39,11 @@ def differentiate(expr, wrt=None, wrt_list=None, mode=Modes.reverse_numeric): ---------- expr: pyomo.core.expr.numeric_expr.NumericExpression The expression to differentiate - wrt: pyomo.core.base.var._GeneralVarData + wrt: pyomo.core.base.var.VarData If specified, this function will return the derivative with - respect to wrt. wrt is normally a _GeneralVarData, but could - also be a _ParamData. wrt and wrt_list cannot both be specified. - wrt_list: list of pyomo.core.base.var._GeneralVarData + respect to wrt. wrt is normally a VarData, but could + also be a ParamData. wrt and wrt_list cannot both be specified. + wrt_list: list of pyomo.core.base.var.VarData If specified, this function will return the derivative with respect to each element in wrt_list. A list will be returned where the values are the derivatives with respect to the diff --git a/pyomo/core/expr/calculus/diff_with_pyomo.py b/pyomo/core/expr/calculus/diff_with_pyomo.py index 0e3ba3cc2b2..fe3eddf1490 100644 --- a/pyomo/core/expr/calculus/diff_with_pyomo.py +++ b/pyomo/core/expr/calculus/diff_with_pyomo.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/expr/calculus/diff_with_sympy.py b/pyomo/core/expr/calculus/diff_with_sympy.py index 32cf60547ec..ab62fa3c307 100644 --- a/pyomo/core/expr/calculus/diff_with_sympy.py +++ b/pyomo/core/expr/calculus/diff_with_sympy.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/expr/cnf_walker.py b/pyomo/core/expr/cnf_walker.py index a7bf61bef5a..7b2081e5d36 100644 --- a/pyomo/core/expr/cnf_walker.py +++ b/pyomo/core/expr/cnf_walker.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/expr/compare.py b/pyomo/core/expr/compare.py index ec8d56896b8..44d9c4205d7 100644 --- a/pyomo/core/expr/compare.py +++ b/pyomo/core/expr/compare.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -196,7 +196,7 @@ def compare_expressions(expr1, expr2, include_named_exprs=True): ) try: res = pn1 == pn2 - except PyomoException: + except (PyomoException, AttributeError): res = False return res diff --git a/pyomo/core/expr/current.py b/pyomo/core/expr/current.py index 0a2ff01c82a..1209dac0310 100644 --- a/pyomo/core/expr/current.py +++ b/pyomo/core/expr/current.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/expr/expr_common.py b/pyomo/core/expr/expr_common.py index daf86c7afc8..88065e37a2c 100644 --- a/pyomo/core/expr/expr_common.py +++ b/pyomo/core/expr/expr_common.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/expr/expr_errors.py b/pyomo/core/expr/expr_errors.py index e33a6cbbbd7..b0ad816d725 100644 --- a/pyomo/core/expr/expr_errors.py +++ b/pyomo/core/expr/expr_errors.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/expr/logical_expr.py b/pyomo/core/expr/logical_expr.py index e5a2f411a6e..9519b02a43b 100644 --- a/pyomo/core/expr/logical_expr.py +++ b/pyomo/core/expr/logical_expr.py @@ -2,7 +2,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -10,8 +10,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from __future__ import division - import types from itertools import islice @@ -37,6 +35,7 @@ from .base import ExpressionBase from .boolean_value import BooleanValue, BooleanConstant from .expr_common import _and, _or, _equiv, _inv, _xor, _impl, ExpressionType +from .numeric_expr import NumericExpression import operator @@ -184,12 +183,62 @@ def _flattened(args): yield arg +def _flattened_boolean_args(args): + """Flatten any potentially indexed arguments and check that they are + Boolean-valued.""" + for arg in args: + if arg.__class__ in native_types: + myiter = (arg,) + elif isinstance(arg, (types.GeneratorType, list)): + myiter = arg + elif arg.is_indexed(): + myiter = arg.values() + else: + myiter = (arg,) + for _argdata in myiter: + if _argdata.__class__ in native_logical_types: + yield _argdata + elif hasattr(_argdata, 'is_logical_type') and _argdata.is_logical_type(): + yield _argdata + elif isinstance(_argdata, BooleanValue): + yield _argdata + else: + raise ValueError( + "Non-Boolean-valued argument '%s' encountered when constructing " + "expression of Boolean arguments" % arg + ) + + +def _flattened_numeric_args(args): + """Flatten any potentially indexed arguments and check that they are + numeric.""" + for arg in args: + if arg.__class__ in native_types: + myiter = (arg,) + elif isinstance(arg, (types.GeneratorType, list)): + myiter = arg + elif arg.is_indexed(): + myiter = arg.values() + else: + myiter = (arg,) + for _argdata in myiter: + if _argdata.__class__ in native_numeric_types: + yield _argdata + elif hasattr(_argdata, 'is_numeric_type') and _argdata.is_numeric_type(): + yield _argdata + else: + raise ValueError( + "Non-numeric argument '%s' encountered when constructing " + "expression with numeric arguments" % arg + ) + + def land(*args): """ Construct an AndExpression between passed arguments. """ result = AndExpression([]) - for argdata in _flattened(args): + for argdata in _flattened_boolean_args(args): result = result.add(argdata) return result @@ -199,7 +248,7 @@ def lor(*args): Construct an OrExpression between passed arguments. """ result = OrExpression([]) - for argdata in _flattened(args): + for argdata in _flattened_boolean_args(args): result = result.add(argdata) return result @@ -212,7 +261,7 @@ def exactly(n, *args): Usage: exactly(2, m.Y1, m.Y2, m.Y3, ...) """ - result = ExactlyExpression([n] + list(_flattened(args))) + result = ExactlyExpression([n] + list(_flattened_boolean_args(args))) return result @@ -224,7 +273,7 @@ def atmost(n, *args): Usage: atmost(2, m.Y1, m.Y2, m.Y3, ...) """ - result = AtMostExpression([n] + list(_flattened(args))) + result = AtMostExpression([n] + list(_flattened_boolean_args(args))) return result @@ -236,10 +285,30 @@ def atleast(n, *args): Usage: atleast(2, m.Y1, m.Y2, m.Y3, ...) """ - result = AtLeastExpression([n] + list(_flattened(args))) + result = AtLeastExpression([n] + list(_flattened_boolean_args(args))) return result +def all_different(*args): + """Creates a new AllDifferentExpression + + Requires all of the arguments to take on a different value + + Usage: all_different(m.X1, m.X2, ...) + """ + return AllDifferentExpression(list(_flattened_numeric_args(args))) + + +def count_if(*args): + """Creates a new CountIfExpression + + Counts the number of True-valued arguments + + Usage: count_if(m.Y1, m.Y2, ...) + """ + return CountIfExpression(list(_flattened_boolean_args(args))) + + class UnaryBooleanExpression(BooleanExpression): """ Abstract class for single-argument logical expressions. @@ -512,4 +581,54 @@ def _apply_operation(self, result): return sum(result[1:]) >= result[0] +class AllDifferentExpression(NaryBooleanExpression): + """ + Logical expression that all of the N child statements have different values. + All arguments are expected to be discrete-valued. + """ + + __slots__ = () + + PRECEDENCE = None + + def getname(self, *arg, **kwd): + return 'all_different' + + def _to_string(self, values, verbose, smap): + return "all_different(%s)" % (", ".join(values)) + + def _apply_operation(self, result): + last = None + # we know these are integer-valued, so we can just sort them an make + # sure that no adjacent pairs have the same value. + for val in sorted(result): + if last == val: + return False + last = val + return True + + +class CountIfExpression(NumericExpression): + """ + Logical expression that returns the number of True child statements. + All arguments are expected to be Boolean-valued. + """ + + __slots__ = () + PRECEDENCE = None + + # NumericExpression assumes binary operator, so we have to override. + def nargs(self): + return len(self._args_) + + def getname(self, *arg, **kwd): + return 'count_if' + + def _to_string(self, values, verbose, smap): + return "count_if(%s)" % (", ".join(values)) + + def _apply_operation(self, result): + return sum(value(r) for r in result) + + special_boolean_atom_types = {ExactlyExpression, AtMostExpression, AtLeastExpression} diff --git a/pyomo/core/expr/ndarray.py b/pyomo/core/expr/ndarray.py new file mode 100644 index 00000000000..41514c91153 --- /dev/null +++ b/pyomo/core/expr/ndarray.py @@ -0,0 +1,39 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common.dependencies import numpy as np, numpy_available + + +# +# Note: the "if numpy_available" in the class definition also ensures +# that the numpy types are registered if numpy is in fact available +# +class NumericNDArray(np.ndarray if numpy_available else object): + """An ndarray subclass that stores Pyomo numeric expressions""" + + def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): + if method == '__call__': + # Convert all incoming types to ndarray (to prevent recursion) + args = [np.asarray(i) for i in inputs] + # Set the return type to be an 'object'. This prevents the + # logical operators from casting the result to a bool. This + # requires numpy >= 1.6 + kwargs['dtype'] = object + + # Delegate to the base ufunc, but return an instance of this + # class so that additional operators hit this method. + ans = getattr(ufunc, method)(*args, **kwargs) + if isinstance(ans, np.ndarray): + if ans.size == 1: + return ans[0] + return ans.view(NumericNDArray) + else: + return ans diff --git a/pyomo/core/expr/numeric_expr.py b/pyomo/core/expr/numeric_expr.py index 1e3039b5727..21896c63219 100644 --- a/pyomo/core/expr/numeric_expr.py +++ b/pyomo/core/expr/numeric_expr.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -17,14 +17,8 @@ logger = logging.getLogger('pyomo.core') -from math import isclose - -from pyomo.common.dependencies import numpy as np, numpy_available -from pyomo.common.deprecation import ( - deprecated, - deprecation_warning, - relocated_module_attribute, -) +from pyomo.common.dependencies import attempt_import +from pyomo.common.deprecation import deprecated, relocated_module_attribute from pyomo.common.errors import PyomoException, DeveloperError from pyomo.common.formatting import tostr from pyomo.common.numeric_types import ( @@ -47,6 +41,9 @@ # Note: pyggyback on expr.base's use of attempt_import(visitor) from pyomo.core.expr.base import ExpressionBase, NPV_Mixin, visitor + +_ndarray, _ = attempt_import('pyomo.core.expr.ndarray') + relocated_module_attribute( 'is_potentially_variable', 'pyomo.core.expr.numvalue.is_potentially_variable', @@ -634,7 +631,9 @@ def __abs__(self): return _abs_dispatcher[self.__class__](self) def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): - return NumericNDArray.__array_ufunc__(None, ufunc, method, *inputs, **kwargs) + return _ndarray.NumericNDArray.__array_ufunc__( + None, ufunc, method, *inputs, **kwargs + ) def to_string(self, verbose=None, labeler=None, smap=None, compute_values=False): """Return a string representation of the expression tree. @@ -671,35 +670,6 @@ def to_string(self, verbose=None, labeler=None, smap=None, compute_values=False) return str(self) -# -# Note: the "if numpy_available" in the class definition also ensures -# that the numpy types are registered if numpy is in fact available -# -# TODO: Move this to a separate module to support avoiding the numpy -# import if numpy is not actually used. -class NumericNDArray(np.ndarray if numpy_available else object): - """An ndarray subclass that stores Pyomo numeric expressions""" - - def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): - if method == '__call__': - # Convert all incoming types to ndarray (to prevent recursion) - args = [np.asarray(i) for i in inputs] - # Set the return type to be an 'object'. This prevents the - # logical operators from casting the result to a bool. This - # requires numpy >= 1.6 - kwargs['dtype'] = object - - # Delegate to the base ufunc, but return an instance of this - # class so that additional operators hit this method. - ans = getattr(ufunc, method)(*args, **kwargs) - if isinstance(ans, np.ndarray): - if ans.size == 1: - return ans[0] - return ans.view(NumericNDArray) - else: - return ans - - # ------------------------------------------------------- # # Expression classes @@ -752,7 +722,7 @@ def args(self): @deprecated( 'The implicit recasting of a "not potentially variable" ' 'expression node to a potentially variable one is no ' - 'longer supported (this violates that immutability ' + 'longer supported (this violates the immutability ' 'promise for Pyomo5 expression trees).', version='6.4.3', ) @@ -1186,13 +1156,30 @@ def nargs(self): @property def args(self): - if len(self._args_) != self._nargs: - self._args_ = self._args_[: self._nargs] - return self._args_ + # We unconditionally make a copy of the args to isolate the user + # from future possible updates to the underlying list + return self._args_[: self._nargs] def getname(self, *args, **kwds): return 'sum' + def _trunc_append(self, other): + _args = self._args_ + if len(_args) > self._nargs: + _args = _args[: self._nargs] + _args.append(other) + return self.__class__(_args) + + def _trunc_extend(self, other): + _args = self._args_ + if len(_args) > self._nargs: + _args = _args[: self._nargs] + if len(other._args_) == other._nargs: + _args.extend(other._args_) + else: + _args.extend(other._args_[: other._nargs]) + return self.__class__(_args) + def _apply_operation(self, result): return sum(result) @@ -1247,9 +1234,11 @@ class LinearExpression(SumExpression): """An expression object for linear polynomials. This is a derived :py:class`SumExpression` that guarantees all - arguments are either not potentially variable (e.g., native types, - Params, or NPV expressions) OR :py:class:`MonomialTermExpression` - objects. + arguments are one of the following types: + + - not potentially variable (e.g., native types, Params, or NPV expressions) + - :py:class:`MonomialTermExpression` + - :py:class:`VarData` Args: args (tuple): Children nodes @@ -1266,7 +1255,7 @@ def __init__(self, args=None, constant=None, linear_coefs=None, linear_vars=None You can specify `args` OR (`constant`, `linear_coefs`, and `linear_vars`). If `args` is provided, it should be a list that - contains only constants, NPV objects/expressions, or + contains only constants, NPV objects/expressions, variables, or :py:class:`MonomialTermExpression` objects. Alternatively, you can specify the constant, the list of linear_coefs and the list of linear_vars separately. Note that these lists are NOT @@ -1311,8 +1300,14 @@ def _build_cache(self): if arg.__class__ is MonomialTermExpression: coef.append(arg._args_[0]) var.append(arg._args_[1]) - else: + elif arg.__class__ in native_numeric_types: const += arg + elif not arg.is_potentially_variable(): + const += arg + else: + assert arg.is_potentially_variable() + coef.append(1) + var.append(arg) LinearExpression._cache = (self, const, coef, var) @property @@ -1338,7 +1333,7 @@ def create_node_with_local_data(self, args, classtype=None): classtype = self.__class__ if type(args) is not list: args = list(args) - for i, arg in enumerate(args): + for arg in args: if arg.__class__ in self._allowable_linear_expr_arg_types: # 99% of the time, the arg type hasn't changed continue @@ -1349,8 +1344,7 @@ def create_node_with_local_data(self, args, classtype=None): # NPV expressions are OK pass elif arg.is_variable_type(): - # vars are OK, but need to be mapped to monomial terms - args[i] = MonomialTermExpression((1, arg)) + # vars are OK continue else: # For anything else, convert this to a general sum @@ -1833,7 +1827,7 @@ def _add_native_param(a, b): def _add_native_var(a, b): if not a: return b - return LinearExpression([a, MonomialTermExpression((1, b))]) + return LinearExpression([a, b]) def _add_native_monomial(a, b): @@ -1845,17 +1839,13 @@ def _add_native_monomial(a, b): def _add_native_linear(a, b): if not a: return b - args = b.args - args.append(a) - return b.__class__(args) + return b._trunc_append(a) def _add_native_sum(a, b): if not a: return b - args = b.args - args.append(a) - return b.__class__(args) + return b._trunc_append(a) def _add_native_other(a, b): @@ -1888,7 +1878,7 @@ def _add_npv_param(a, b): def _add_npv_var(a, b): - return LinearExpression([a, MonomialTermExpression((1, b))]) + return LinearExpression([a, b]) def _add_npv_monomial(a, b): @@ -1896,15 +1886,11 @@ def _add_npv_monomial(a, b): def _add_npv_linear(a, b): - args = b.args - args.append(a) - return b.__class__(args) + return b._trunc_append(a) def _add_npv_sum(a, b): - args = b.args - args.append(a) - return b.__class__(args) + return b._trunc_append(a) def _add_npv_other(a, b): @@ -1950,7 +1936,7 @@ def _add_param_var(a, b): a = a.value if not a: return b - return LinearExpression([a, MonomialTermExpression((1, b))]) + return LinearExpression([a, b]) def _add_param_monomial(a, b): @@ -1966,9 +1952,7 @@ def _add_param_linear(a, b): a = a.value if not a: return b - args = b.args - args.append(a) - return b.__class__(args) + return b._trunc_append(a) def _add_param_sum(a, b): @@ -1976,9 +1960,7 @@ def _add_param_sum(a, b): a = value(a) if not a: return b - args = b.args - args.append(a) - return b.__class__(args) + return b._trunc_append(a) def _add_param_other(a, b): @@ -1997,11 +1979,11 @@ def _add_param_other(a, b): def _add_var_native(a, b): if not b: return a - return LinearExpression([MonomialTermExpression((1, a)), b]) + return LinearExpression([a, b]) def _add_var_npv(a, b): - return LinearExpression([MonomialTermExpression((1, a)), b]) + return LinearExpression([a, b]) def _add_var_param(a, b): @@ -2009,29 +1991,23 @@ def _add_var_param(a, b): b = b.value if not b: return a - return LinearExpression([MonomialTermExpression((1, a)), b]) + return LinearExpression([a, b]) def _add_var_var(a, b): - return LinearExpression( - [MonomialTermExpression((1, a)), MonomialTermExpression((1, b))] - ) + return LinearExpression([a, b]) def _add_var_monomial(a, b): - return LinearExpression([MonomialTermExpression((1, a)), b]) + return LinearExpression([a, b]) def _add_var_linear(a, b): - args = b.args - args.append(MonomialTermExpression((1, a))) - return b.__class__(args) + return b._trunc_append(a) def _add_var_sum(a, b): - args = b.args - args.append(a) - return b.__class__(args) + return b._trunc_append(a) def _add_var_other(a, b): @@ -2062,7 +2038,7 @@ def _add_monomial_param(a, b): def _add_monomial_var(a, b): - return LinearExpression([a, MonomialTermExpression((1, b))]) + return LinearExpression([a, b]) def _add_monomial_monomial(a, b): @@ -2070,15 +2046,11 @@ def _add_monomial_monomial(a, b): def _add_monomial_linear(a, b): - args = b.args - args.append(a) - return b.__class__(args) + return b._trunc_append(a) def _add_monomial_sum(a, b): - args = b.args - args.append(a) - return b.__class__(args) + return b._trunc_append(a) def _add_monomial_other(a, b): @@ -2093,15 +2065,11 @@ def _add_monomial_other(a, b): def _add_linear_native(a, b): if not b: return a - args = a.args - args.append(b) - return a.__class__(args) + return a._trunc_append(b) def _add_linear_npv(a, b): - args = a.args - args.append(b) - return a.__class__(args) + return a._trunc_append(b) def _add_linear_param(a, b): @@ -2109,33 +2077,23 @@ def _add_linear_param(a, b): b = b.value if not b: return a - args = a.args - args.append(b) - return a.__class__(args) + return a._trunc_append(b) def _add_linear_var(a, b): - args = a.args - args.append(MonomialTermExpression((1, b))) - return a.__class__(args) + return a._trunc_append(b) def _add_linear_monomial(a, b): - args = a.args - args.append(b) - return a.__class__(args) + return a._trunc_append(b) def _add_linear_linear(a, b): - args = a.args - args.extend(b.args) - return a.__class__(args) + return a._trunc_extend(b) def _add_linear_sum(a, b): - args = b.args - args.append(a) - return b.__class__(args) + return b._trunc_append(a) def _add_linear_other(a, b): @@ -2150,15 +2108,11 @@ def _add_linear_other(a, b): def _add_sum_native(a, b): if not b: return a - args = a.args - args.append(b) - return a.__class__(args) + return a._trunc_append(b) def _add_sum_npv(a, b): - args = a.args - args.append(b) - return a.__class__(args) + return a._trunc_append(b) def _add_sum_param(a, b): @@ -2166,39 +2120,27 @@ def _add_sum_param(a, b): b = b.value if not b: return a - args = a.args - args.append(b) - return a.__class__(args) + return a._trunc_append(b) def _add_sum_var(a, b): - args = a.args - args.append(b) - return a.__class__(args) + return a._trunc_append(b) def _add_sum_monomial(a, b): - args = a.args - args.append(b) - return a.__class__(args) + return a._trunc_append(b) def _add_sum_linear(a, b): - args = a.args - args.append(b) - return a.__class__(args) + return a._trunc_append(b) def _add_sum_sum(a, b): - args = a.args - args.extend(b.args) - return a.__class__(args) + return a._trunc_extend(b) def _add_sum_other(a, b): - args = a.args - args.append(b) - return a.__class__(args) + return a._trunc_append(b) # @@ -2237,9 +2179,7 @@ def _add_other_linear(a, b): def _add_other_sum(a, b): - args = b.args - args.append(a) - return b.__class__(args) + return b._trunc_append(a) def _add_other_other(a, b): @@ -2348,8 +2288,11 @@ def _iadd_mutablenpvsum_mutable(a, b): def _iadd_mutablenpvsum_native(a, b): if not b: return a - a._args_.append(b) - a._nargs += 1 + if a._args_ and a._args_[-1].__class__ in native_numeric_types: + a._args_[-1] += b + else: + a._args_.append(b) + a._nargs += 1 return a @@ -2361,9 +2304,7 @@ def _iadd_mutablenpvsum_npv(a, b): def _iadd_mutablenpvsum_param(a, b): if b.is_constant(): - b = b.value - if not b: - return a + return _iadd_mutablesum_native(a, b.value) a._args_.append(b) a._nargs += 1 return a @@ -2414,9 +2355,9 @@ def _register_new_iadd_mutablenpvsum_handler(a, b): # Retrieve the appropriate handler, record it in the main # _iadd_mutablenpvsum_dispatcher dict (so this method is not called a second time for # these types) - _iadd_mutablenpvsum_dispatcher[ - b.__class__ - ] = handler = _iadd_mutablenpvsum_type_handler_mapping[types[0]] + _iadd_mutablenpvsum_dispatcher[b.__class__] = handler = ( + _iadd_mutablenpvsum_type_handler_mapping[types[0]] + ) # Call the appropriate handler return handler(a, b) @@ -2444,8 +2385,11 @@ def _iadd_mutablelinear_mutable(a, b): def _iadd_mutablelinear_native(a, b): if not b: return a - a._args_.append(b) - a._nargs += 1 + if a._args_ and a._args_[-1].__class__ in native_numeric_types: + a._args_[-1] += b + else: + a._args_.append(b) + a._nargs += 1 return a @@ -2457,16 +2401,14 @@ def _iadd_mutablelinear_npv(a, b): def _iadd_mutablelinear_param(a, b): if b.is_constant(): - b = b.value - if not b: - return a + return _iadd_mutablesum_native(a, b.value) a._args_.append(b) a._nargs += 1 return a def _iadd_mutablelinear_var(a, b): - a._args_.append(MonomialTermExpression((1, b))) + a._args_.append(b) a._nargs += 1 return a @@ -2513,9 +2455,9 @@ def _register_new_iadd_mutablelinear_handler(a, b): # Retrieve the appropriate handler, record it in the main # _iadd_mutablelinear_dispatcher dict (so this method is not called a second time for # these types) - _iadd_mutablelinear_dispatcher[ - b.__class__ - ] = handler = _iadd_mutablelinear_type_handler_mapping[types[0]] + _iadd_mutablelinear_dispatcher[b.__class__] = handler = ( + _iadd_mutablelinear_type_handler_mapping[types[0]] + ) # Call the appropriate handler return handler(a, b) @@ -2543,8 +2485,11 @@ def _iadd_mutablesum_mutable(a, b): def _iadd_mutablesum_native(a, b): if not b: return a - a._args_.append(b) - a._nargs += 1 + if a._args_ and a._args_[-1].__class__ in native_numeric_types: + a._args_[-1] += b + else: + a._args_.append(b) + a._nargs += 1 return a @@ -2556,9 +2501,7 @@ def _iadd_mutablesum_npv(a, b): def _iadd_mutablesum_param(a, b): if b.is_constant(): - b = b.value - if not b: - return a + return _iadd_mutablesum_native(a, b.value) a._args_.append(b) a._nargs += 1 return a @@ -2614,9 +2557,9 @@ def _register_new_iadd_mutablesum_handler(a, b): # Retrieve the appropriate handler, record it in the main # _iadd_mutablesum_dispatcher dict (so this method is not called a # second time for these types) - _iadd_mutablesum_dispatcher[ - b.__class__ - ] = handler = _iadd_mutablesum_type_handler_mapping[types[0]] + _iadd_mutablesum_dispatcher[b.__class__] = handler = ( + _iadd_mutablesum_type_handler_mapping[types[0]] + ) # Call the appropriate handler return handler(a, b) @@ -2652,8 +2595,8 @@ def _neg_var(a): def _neg_monomial(a): - args = a.args - return MonomialTermExpression((-args[0], args[1])) + coef, var = a.args + return MonomialTermExpression((-coef, var)) def _neg_sum(a): diff --git a/pyomo/core/expr/numvalue.py b/pyomo/core/expr/numvalue.py index 305391daffa..96e2f50b3f8 100644 --- a/pyomo/core/expr/numvalue.py +++ b/pyomo/core/expr/numvalue.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,21 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ( - 'value', - 'is_constant', - 'is_fixed', - 'is_variable_type', - 'is_potentially_variable', - 'NumericValue', - 'ZeroConstant', - 'native_numeric_types', - 'native_types', - 'nonpyomo_leaf_types', - 'polynomial_degree', -) - -import collections import sys import logging @@ -34,7 +19,6 @@ ) from pyomo.core.expr.expr_common import ExpressionType from pyomo.core.expr.numeric_expr import NumericValue -import pyomo.common.numeric_types as _numeric_types # TODO: update Pyomo to import these objects from common.numeric_types # (and not from here) @@ -44,7 +28,7 @@ native_numeric_types, native_integer_types, native_logical_types, - pyomo_constant_types, + _pyomo_constant_types, check_if_numeric_type, value, ) @@ -60,6 +44,16 @@ "be treated as if they were bool (as was the case for the other " "native_*_types sets). Users likely should use native_logical_types.", ) +relocated_module_attribute( + 'pyomo_constant_types', + 'pyomo.common.numeric_types._pyomo_constant_types', + version='6.7.2', + f_globals=globals(), + msg="The pyomo_constant_types set will be removed in the future: the set " + "contained only NumericConstant and _PythonCallbackFunctionID, and provided " + "no meaningful value to clients or walkers. Users should likely handle " + "these types in the same manner as immutable Params.", +) relocated_module_attribute( 'RegisterNumericType', 'pyomo.common.numeric_types.RegisterNumericType', @@ -101,7 +95,7 @@ ##------------------------------------------------------------------------ -class NonNumericValue(object): +class NonNumericValue(PyomoObject): """An object that contains a non-numeric value Constructor Arguments: @@ -116,6 +110,9 @@ def __init__(self, value): def __str__(self): return str(self.value) + def __call__(self, exception=None): + return self.value + nonpyomo_leaf_types.add(NonNumericValue) @@ -259,11 +256,6 @@ def polynomial_degree(obj): ) -# -# It is very common to have only a few constants in a model, but those -# constants get repeated many times. KnownConstants lets us re-use / -# share constants we have seen before. -# # Note: # For now, all constants are coerced to floats. This avoids integer # division in Python 2.x. (At least some of the time.) @@ -273,6 +265,10 @@ def polynomial_degree(obj): # need to index KnownConstants by both the class type and value, since # INT, FLOAT and LONG values sometimes hash the same. # +# It is very common to have only a few constants in a model, but those +# constants get repeated many times. KnownConstants lets us re-use / +# share constants we have seen before. +# _KnownConstants = {} @@ -427,7 +423,7 @@ def pprint(self, ostream=None, verbose=False): ostream.write(str(self)) -pyomo_constant_types.add(NumericConstant) +_pyomo_constant_types.add(NumericConstant) # We use as_numeric() so that the constant is also in the cache ZeroConstant = as_numeric(0) diff --git a/pyomo/core/expr/relational_expr.py b/pyomo/core/expr/relational_expr.py index 6e4831d5c0c..c80fdd4930a 100644 --- a/pyomo/core/expr/relational_expr.py +++ b/pyomo/core/expr/relational_expr.py @@ -2,7 +2,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/expr/symbol_map.py b/pyomo/core/expr/symbol_map.py index ab497c217a8..ebcf9b2953e 100644 --- a/pyomo/core/expr/symbol_map.py +++ b/pyomo/core/expr/symbol_map.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/expr/sympy_tools.py b/pyomo/core/expr/sympy_tools.py index 7b494a610cd..6c184f0e4c4 100644 --- a/pyomo/core/expr/sympy_tools.py +++ b/pyomo/core/expr/sympy_tools.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -175,10 +175,11 @@ def sympyVars(self): class Pyomo2SympyVisitor(EXPR.StreamBasedExpressionVisitor): - def __init__(self, object_map): + def __init__(self, object_map, keep_mutable_parameters=False): sympy.Add # this ensures _configure_sympy gets run super(Pyomo2SympyVisitor, self).__init__() self.object_map = object_map + self.keep_mutable_parameters = keep_mutable_parameters def initializeWalker(self, expr): return self.beforeChild(None, expr, None) @@ -212,6 +213,8 @@ def beforeChild(self, node, child, child_idx): # # Everything else is a constant... # + if self.keep_mutable_parameters and child.is_parameter_type() and child.mutable: + return False, self.object_map.getSympySymbol(child) return False, value(child) @@ -245,13 +248,15 @@ def beforeChild(self, node, child, child_idx): return True, None -def sympyify_expression(expr): +def sympyify_expression(expr, keep_mutable_parameters=False): """Convert a Pyomo expression to a Sympy expression""" # # Create the visitor and call it. # object_map = PyomoSympyBimap() - visitor = Pyomo2SympyVisitor(object_map) + visitor = Pyomo2SympyVisitor( + object_map, keep_mutable_parameters=keep_mutable_parameters + ) return object_map, visitor.walk_expression(expr) diff --git a/pyomo/core/expr/taylor_series.py b/pyomo/core/expr/taylor_series.py index 2c72f8bcfbc..2658dd36ff5 100644 --- a/pyomo/core/expr/taylor_series.py +++ b/pyomo/core/expr/taylor_series.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core.expr import identify_variables, value, differentiate import logging import math diff --git a/pyomo/core/expr/template_expr.py b/pyomo/core/expr/template_expr.py index 21e26038771..d30046e9d82 100644 --- a/pyomo/core/expr/template_expr.py +++ b/pyomo/core/expr/template_expr.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -19,11 +19,12 @@ from pyomo.core.expr.base import ExpressionBase, ExpressionArgs_Mixin, NPV_Mixin from pyomo.core.expr.logical_expr import BooleanExpression from pyomo.core.expr.numeric_expr import ( + ARG_TYPE, NumericExpression, - SumExpression, Numeric_NPV_Mixin, + SumExpression, + mutable_expression, register_arg_type, - ARG_TYPE, _balanced_parens, ) from pyomo.core.expr.numvalue import ( @@ -116,16 +117,10 @@ def _to_string(self, values, verbose, smap): return "%s[%s]" % (values[0], ','.join(values[1:])) def _resolve_template(self, args): - return args[0].__getitem__(tuple(args[1:])) + return args[0].__getitem__(args[1:]) def _apply_operation(self, result): - args = tuple( - arg - if arg.__class__ in native_types or not arg.is_numeric_type() - else value(arg) - for arg in result[1:] - ) - return result[0].__getitem__(tuple(result[1:])) + return result[0].__getitem__(result[1:]) class Numeric_GetItemExpression(GetItemExpression, NumericExpression): @@ -256,8 +251,8 @@ def nargs(self): return 2 def _apply_operation(self, result): - assert len(result) == 2 - return getattr(result[0], result[1]) + obj, attr = result + return getattr(obj, attr) def _to_string(self, values, verbose, smap): assert len(values) == 2 @@ -271,7 +266,7 @@ def _to_string(self, values, verbose, smap): return "%s.%s" % (values[0], attr) def _resolve_template(self, args): - return getattr(*tuple(args)) + return getattr(*args) class Numeric_GetAttrExpression(GetAttrExpression, NumericExpression): @@ -519,7 +514,15 @@ def _to_string(self, values, verbose, smap): return 'SUM(%s %s)' % (val, iterStr) def _resolve_template(self, args): - return SumExpression(args) + with mutable_expression() as e: + for arg in args: + e += arg + if e.nargs() > 1: + return e + elif not e.nargs(): + return 0 + else: + return e.arg(0) class IndexTemplate(NumericValue): diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py index c8f22ba1d3a..08015f8b42c 100644 --- a/pyomo/core/expr/visitor.py +++ b/pyomo/core/expr/visitor.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,7 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from __future__ import division import inspect import logging @@ -82,11 +81,13 @@ class RevertToNonrecursive(Exception): class StreamBasedExpressionVisitor(object): """This class implements a generic stream-based expression walker. - This visitor walks an expression tree using a depth-first strategy - and generates a full event stream similar to other tree visitors - (e.g., the expat XML parser). The following events are triggered - through callback functions as the traversal enters and leaves nodes - in the tree: + This visitor walks an expression tree using a depth-first strategy + and generates a full event stream similar to other tree visitors + (e.g., the expat XML parser). The following events are triggered + through callback functions as the traversal enters and leaves nodes + in the tree: + + :: initializeWalker(expr) -> walk, result enterNode(N1) -> args, data @@ -100,7 +101,7 @@ class StreamBasedExpressionVisitor(object): exitNode(N1, data) -> N1_result finalizeWalker(result) -> result - Individual event callbacks match the following signatures: + Individual event callbacks match the following signatures: walk, result = initializeWalker(self, expr): @@ -123,7 +124,7 @@ class StreamBasedExpressionVisitor(object): not defined, the default behavior is equivalent to returning (None, []). - node_result = exitNode(self, node, data): + node_result = exitNode(self, node, data): exitNode() is called after the node is completely processed (as the walker returns up the tree to the parent node). It is @@ -133,7 +134,7 @@ class StreamBasedExpressionVisitor(object): this node. If not specified, the default action is to return the data object from enterNode(). - descend, child_result = beforeChild(self, node, child, child_idx): + descend, child_result = beforeChild(self, node, child, child_idx): beforeChild() is called by a node for every child before entering the child node. The node, child node, and child index @@ -145,7 +146,7 @@ class StreamBasedExpressionVisitor(object): equivalent to (True, None). The default behavior if not specified is equivalent to (True, None). - data = acceptChildResult(self, node, data, child_result, child_idx): + data = acceptChildResult(self, node, data, child_result, child_idx): acceptChildResult() is called for each child result being returned to a node. This callback is responsible for recording @@ -156,7 +157,7 @@ class StreamBasedExpressionVisitor(object): returned. If acceptChildResult is not specified, it does nothing if data is None, otherwise it calls data.append(result). - afterChild(self, node, child, child_idx): + afterChild(self, node, child, child_idx): afterChild() is called by a node for every child node immediately after processing the node is complete before control @@ -165,7 +166,7 @@ class StreamBasedExpressionVisitor(object): are passed, and nothing is returned. If afterChild is not specified, no action takes place. - finalizeResult(self, result): + finalizeResult(self, result): finalizeResult() is called once after the entire expression tree has been walked. It is passed the result returned by the root @@ -173,10 +174,10 @@ class StreamBasedExpressionVisitor(object): the walker returns the result obtained from the exitNode callback on the root node. - Clients interact with this class by either deriving from it and - implementing the necessary callbacks (see above), assigning callable - functions to an instance of this class, or passing the callback - functions as arguments to this class' constructor. + Clients interact with this class by either deriving from it and + implementing the necessary callbacks (see above), assigning callable + functions to an instance of this class, or passing the callback + functions as arguments to this class' constructor. """ @@ -254,7 +255,14 @@ def wrapper(*args): ) def walk_expression(self, expr): - """Walk an expression, calling registered callbacks.""" + """Walk an expression, calling registered callbacks. + + This is the standard interface for running the visitor. It + defaults to using an efficient recursive implementation of the + visitor, falling back on :py:meth:`walk_expression_nonrecursive` + if the recursion stack gets too deep. + + """ if self.initializeWalker is not None: walk, root = self.initializeWalker(expr) if not walk: @@ -496,7 +504,13 @@ def _recursive_frame_to_nonrecursive_stack(self, local): ) def walk_expression_nonrecursive(self, expr): - """Walk an expression, calling registered callbacks.""" + """Nonrecursively walk an expression, calling registered callbacks. + + This routine is safer than the recursive walkers for deep (or + unbalanced) trees. It is, however, slightly slower than the + recursive implementations. + + """ # # This walker uses a linked list to store the stack (instead of # an array). The nodes of the linked list are 6-member tuples: @@ -666,7 +680,6 @@ def _nonrecursive_walker_loop(self, ptr): class SimpleExpressionVisitor(object): - """ Note: This class is a customization of the PyUtilib :class:`SimpleVisitor @@ -1360,22 +1373,125 @@ def identify_components(expr, component_types): # ===================================================== -class _VariableVisitor(SimpleExpressionVisitor): - def __init__(self): - self.seen = set() +class _VariableVisitor(StreamBasedExpressionVisitor): + def __init__(self, include_fixed=False, named_expression_cache=None): + """Visitor that collects all unique variables participating in an + expression - def visit(self, node): - if node.__class__ in nonpyomo_leaf_types: - return + Args: + include_fixed (bool): Whether to include fixed variables + named_expression_cache (optional, dict): Dict mapping ids of named + expressions to a tuple of the list of all variables and the + set of all variable ids contained in the named expression. - if node.is_variable_type(): - if id(node) in self.seen: - return - self.seen.add(id(node)) - return node + """ + super().__init__() + self._include_fixed = include_fixed + if named_expression_cache is None: + # This cache will map named expression ids to the + # tuple: ([variables], {variable ids}) + named_expression_cache = {} + self._named_expression_cache = named_expression_cache + # Stack of active named expressions. This holds the id of + # expressions we are currently in. + self._active_named_expressions = [] + def initializeWalker(self, expr): + if expr.__class__ in native_types: + return False, [] + elif expr.is_named_expression_type(): + eid = id(expr) + if eid in self._named_expression_cache: + # If we were given a named expression that is already cached, + # just do nothing and return the expression's variables + variables, var_set = self._named_expression_cache[eid] + return False, variables + else: + # We were given a named expression that is not cached. + # Initialize data structures and add this expression to the + # stack. This expression will get popped in exitNode. + self._variables = [] + self._seen = set() + self._named_expression_cache[eid] = [], set() + self._active_named_expressions.append(eid) + return True, expr + elif expr.is_variable_type(): + return False, [expr] + else: + self._variables = [] + self._seen = set() + return True, expr + + def beforeChild(self, parent, child, index): + if child.__class__ in native_types: + return False, None + elif child.is_named_expression_type(): + eid = id(child) + if eid in self._named_expression_cache: + # We have already encountered this named expression. We just add + # the cached variables to our list and don't descend. + if self._active_named_expressions: + # If we are in another named expression, we update the + # parent expression's cache. We don't need to update the + # global list as we will do this when we exit the active + # named expression. + parent_eid = self._active_named_expressions[-1] + variables, var_set = self._named_expression_cache[parent_eid] + else: + # If we are not in a named expression, we update the global + # list. + variables = self._variables + var_set = self._seen + for var in self._named_expression_cache[eid][0]: + if id(var) not in var_set: + var_set.add(id(var)) + variables.append(var) + return False, None + else: + # If we are descending into a new named expression, initialize + # a cache to store the expression's local variables. + self._named_expression_cache[id(child)] = ([], set()) + self._active_named_expressions.append(id(child)) + return True, None + elif child.is_variable_type() and (self._include_fixed or not child.fixed): + if self._active_named_expressions: + # If we are in a named expression, add new variables to the cache. + eid = self._active_named_expressions[-1] + variables, var_set = self._named_expression_cache[eid] + else: + variables = self._variables + var_set = self._seen + if id(child) not in var_set: + var_set.add(id(child)) + variables.append(child) + return False, None + else: + return True, None -def identify_variables(expr, include_fixed=True): + def exitNode(self, node, data): + if node.is_named_expression_type(): + # If we are returning from a named expression, we have at least one + # active named expression. We must make sure that we properly + # handle the variables for the named expression we just exited. + eid = self._active_named_expressions.pop() + if self._active_named_expressions: + # If we still are in a named expression, we update that expression's + # cache with any new variables encountered. + parent_eid = self._active_named_expressions[-1] + variables, var_set = self._named_expression_cache[parent_eid] + else: + variables = self._variables + var_set = self._seen + for var in self._named_expression_cache[eid][0]: + if id(var) not in var_set: + var_set.add(id(var)) + variables.append(var) + + def finalizeResult(self, result): + return self._variables + + +def identify_variables(expr, include_fixed=True, named_expression_cache=None): """ A generator that yields a sequence of variables in an expression tree. @@ -1389,22 +1505,13 @@ def identify_variables(expr, include_fixed=True): Yields: Each variable that is found. """ - visitor = _VariableVisitor() - if include_fixed: - for v in visitor.xbfs_yield_leaves(expr): - if isinstance(v, tuple): - yield from v - else: - yield v - else: - for v in visitor.xbfs_yield_leaves(expr): - if isinstance(v, tuple): - for v_i in v: - if not v_i.is_fixed(): - yield v_i - else: - if not v.is_fixed(): - yield v + if named_expression_cache is None: + named_expression_cache = {} + visitor = _VariableVisitor( + named_expression_cache=named_expression_cache, include_fixed=include_fixed + ) + variables = visitor.walk_expression(expr) + yield from variables # ===================================================== diff --git a/pyomo/core/kernel/__init__.py b/pyomo/core/kernel/__init__.py index 28a329109fc..ffe0beee080 100644 --- a/pyomo/core/kernel/__init__.py +++ b/pyomo/core/kernel/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/base.py b/pyomo/core/kernel/base.py index 2c0af56bc10..d599c76f6a1 100644 --- a/pyomo/core/kernel/base.py +++ b/pyomo/core/kernel/base.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/block.py b/pyomo/core/kernel/block.py index fd779578fc4..8ba332e5545 100644 --- a/pyomo/core/kernel/block.py +++ b/pyomo/core/kernel/block.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/component_map.py b/pyomo/core/kernel/component_map.py index 501854ad972..5b5b6e9a6f2 100644 --- a/pyomo/core/kernel/component_map.py +++ b/pyomo/core/kernel/component_map.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/component_set.py b/pyomo/core/kernel/component_set.py index b0eb3507347..969b8b86372 100644 --- a/pyomo/core/kernel/component_set.py +++ b/pyomo/core/kernel/component_set.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/conic.py b/pyomo/core/kernel/conic.py index 730c072d1b7..1bb5f1b6ce8 100644 --- a/pyomo/core/kernel/conic.py +++ b/pyomo/core/kernel/conic.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/constraint.py b/pyomo/core/kernel/constraint.py index 7c7969cb025..6aa4abc4bfe 100644 --- a/pyomo/core/kernel/constraint.py +++ b/pyomo/core/kernel/constraint.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/container_utils.py b/pyomo/core/kernel/container_utils.py index 7f3329aadb3..e197d0162b5 100644 --- a/pyomo/core/kernel/container_utils.py +++ b/pyomo/core/kernel/container_utils.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/dict_container.py b/pyomo/core/kernel/dict_container.py index b86d9c5b8f2..ae23044f8ed 100644 --- a/pyomo/core/kernel/dict_container.py +++ b/pyomo/core/kernel/dict_container.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/expression.py b/pyomo/core/kernel/expression.py index b375a6a89fc..a477ff9d0e3 100644 --- a/pyomo/core/kernel/expression.py +++ b/pyomo/core/kernel/expression.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/heterogeneous_container.py b/pyomo/core/kernel/heterogeneous_container.py index 43846673838..4783a2d3ec6 100644 --- a/pyomo/core/kernel/heterogeneous_container.py +++ b/pyomo/core/kernel/heterogeneous_container.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/homogeneous_container.py b/pyomo/core/kernel/homogeneous_container.py index 22a70e1edff..edec98e9736 100644 --- a/pyomo/core/kernel/homogeneous_container.py +++ b/pyomo/core/kernel/homogeneous_container.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/list_container.py b/pyomo/core/kernel/list_container.py index 05116797f3a..d60b0c7678d 100644 --- a/pyomo/core/kernel/list_container.py +++ b/pyomo/core/kernel/list_container.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/matrix_constraint.py b/pyomo/core/kernel/matrix_constraint.py index 1dc0fa7ddc3..ac0ec8e832d 100644 --- a/pyomo/core/kernel/matrix_constraint.py +++ b/pyomo/core/kernel/matrix_constraint.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/objective.py b/pyomo/core/kernel/objective.py index c25c86d3c09..ac6f22d07d3 100644 --- a/pyomo/core/kernel/objective.py +++ b/pyomo/core/kernel/objective.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,15 +9,12 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from pyomo.common.enums import ObjectiveSense, minimize, maximize from pyomo.core.expr.numvalue import as_numeric from pyomo.core.kernel.base import _abstract_readwrite_property from pyomo.core.kernel.container_utils import define_simple_containers from pyomo.core.kernel.expression import IExpression -# Constants used to define the optimization sense -minimize = 1 -maximize = -1 - class IObjective(IExpression): """ @@ -84,14 +81,7 @@ def sense(self): @sense.setter def sense(self, sense): """Set the sense (direction) of this objective.""" - if (sense == minimize) or (sense == maximize): - self._sense = sense - else: - raise ValueError( - "Objective sense must be set to one of: " - "[minimize (%s), maximize (%s)]. Invalid " - "value: %s'" % (minimize, maximize, sense) - ) + self._sense = ObjectiveSense(sense) # inserts class definitions for simple _tuple, _list, and diff --git a/pyomo/core/kernel/parameter.py b/pyomo/core/kernel/parameter.py index 1d22072435d..d4dd6336c69 100644 --- a/pyomo/core/kernel/parameter.py +++ b/pyomo/core/kernel/parameter.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/piecewise_library/__init__.py b/pyomo/core/kernel/piecewise_library/__init__.py index d275b52367e..c4d2a751632 100644 --- a/pyomo/core/kernel/piecewise_library/__init__.py +++ b/pyomo/core/kernel/piecewise_library/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/piecewise_library/transforms.py b/pyomo/core/kernel/piecewise_library/transforms.py index f00e57c199d..bc6cb0f51ad 100644 --- a/pyomo/core/kernel/piecewise_library/transforms.py +++ b/pyomo/core/kernel/piecewise_library/transforms.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/piecewise_library/transforms_nd.py b/pyomo/core/kernel/piecewise_library/transforms_nd.py index f1ea67e8d4b..2c4c8a1f1f2 100644 --- a/pyomo/core/kernel/piecewise_library/transforms_nd.py +++ b/pyomo/core/kernel/piecewise_library/transforms_nd.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/piecewise_library/util.py b/pyomo/core/kernel/piecewise_library/util.py index e65502b1a12..23975d87596 100644 --- a/pyomo/core/kernel/piecewise_library/util.py +++ b/pyomo/core/kernel/piecewise_library/util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/register_numpy_types.py b/pyomo/core/kernel/register_numpy_types.py index b9205930512..86877be2230 100644 --- a/pyomo/core/kernel/register_numpy_types.py +++ b/pyomo/core/kernel/register_numpy_types.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -17,10 +17,11 @@ version='6.1', ) -from pyomo.core.expr.numvalue import ( +from pyomo.common.numeric_types import ( RegisterNumericType, RegisterIntegerType, RegisterBooleanType, + native_complex_types, native_numeric_types, native_integer_types, native_boolean_types, @@ -37,6 +38,8 @@ numpy_float = [] numpy_bool_names = [] numpy_bool = [] +numpy_complex_names = [] +numpy_complex = [] if _has_numpy: # Historically, the lists included several numpy aliases @@ -44,6 +47,8 @@ numpy_int.extend((numpy.int_, numpy.intc, numpy.intp)) numpy_float_names.append('float_') numpy_float.append(numpy.float_) + numpy_complex_names.append('complex_') + numpy_complex.append(numpy.complex_) # Re-build the old numpy_* lists for t in native_boolean_types: @@ -63,13 +68,8 @@ # Complex -numpy_complex_names = [] -numpy_complex = [] -if _has_numpy: - numpy_complex_names.extend(('complex_', 'complex64', 'complex128')) - for _type_name in numpy_complex_names: - try: - _type = getattr(numpy, _type_name) - numpy_complex.append(_type) - except: # pragma:nocover - pass +for t in native_complex_types: + if t.__module__ == 'numpy': + if t.__name__ not in numpy_complex_names: + numpy_complex.append(t) + numpy_complex_names.append(t.__name__) diff --git a/pyomo/core/kernel/set_types.py b/pyomo/core/kernel/set_types.py index efe5965946a..5915f0d64b3 100644 --- a/pyomo/core/kernel/set_types.py +++ b/pyomo/core/kernel/set_types.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/sos.py b/pyomo/core/kernel/sos.py index cb8d8ea4930..1845343f526 100644 --- a/pyomo/core/kernel/sos.py +++ b/pyomo/core/kernel/sos.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/suffix.py b/pyomo/core/kernel/suffix.py index 77079364703..56e13a371a3 100644 --- a/pyomo/core/kernel/suffix.py +++ b/pyomo/core/kernel/suffix.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/tuple_container.py b/pyomo/core/kernel/tuple_container.py index f717fe0350a..83aab49e5db 100644 --- a/pyomo/core/kernel/tuple_container.py +++ b/pyomo/core/kernel/tuple_container.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/variable.py b/pyomo/core/kernel/variable.py index ff54bcb2fca..61324b3dc0f 100644 --- a/pyomo/core/kernel/variable.py +++ b/pyomo/core/kernel/variable.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/plugins/__init__.py b/pyomo/core/plugins/__init__.py index f763881c50c..23407cd77ef 100644 --- a/pyomo/core/plugins/__init__.py +++ b/pyomo/core/plugins/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/plugins/transform/__init__.py b/pyomo/core/plugins/transform/__init__.py index 7d37c706542..21e762047ca 100644 --- a/pyomo/core/plugins/transform/__init__.py +++ b/pyomo/core/plugins/transform/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/plugins/transform/add_slack_vars.py b/pyomo/core/plugins/transform/add_slack_vars.py index 6906b033aab..39903384729 100644 --- a/pyomo/core/plugins/transform/add_slack_vars.py +++ b/pyomo/core/plugins/transform/add_slack_vars.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -23,7 +23,6 @@ from pyomo.core.plugins.transform.hierarchy import NonIsomorphicTransformation from pyomo.common.config import ConfigBlock, ConfigValue from pyomo.core.base import ComponentUID -from pyomo.core.base.constraint import _ConstraintData from pyomo.common.deprecation import deprecation_warning @@ -42,7 +41,7 @@ def target_list(x): # [ESJ 07/15/2020] We have to just pass it through because we need the # instance in order to be able to do anything about it... return [x] - elif isinstance(x, (Constraint, _ConstraintData)): + elif getattr(x, 'ctype', None) is Constraint: return [x] elif hasattr(x, '__iter__'): ans = [] @@ -53,7 +52,7 @@ def target_list(x): deprecation_msg = None # same as above... ans.append(i) - elif isinstance(i, (Constraint, _ConstraintData)): + elif getattr(i, 'ctype', None) is Constraint: ans.append(i) else: raise ValueError( diff --git a/pyomo/core/plugins/transform/discrete_vars.py b/pyomo/core/plugins/transform/discrete_vars.py index cfb1c5e144f..35729e76517 100644 --- a/pyomo/core/plugins/transform/discrete_vars.py +++ b/pyomo/core/plugins/transform/discrete_vars.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/plugins/transform/eliminate_fixed_vars.py b/pyomo/core/plugins/transform/eliminate_fixed_vars.py index 1048b957e08..934228afd7c 100644 --- a/pyomo/core/plugins/transform/eliminate_fixed_vars.py +++ b/pyomo/core/plugins/transform/eliminate_fixed_vars.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -11,7 +11,7 @@ from pyomo.core.expr import ExpressionBase, as_numeric from pyomo.core import Constraint, Objective, TransformationFactory -from pyomo.core.base.var import Var, _VarData +from pyomo.core.base.var import Var, VarData from pyomo.core.util import sequence from pyomo.core.plugins.transform.hierarchy import IsomorphicTransformation @@ -77,7 +77,7 @@ def _fix_vars(self, expr, model): if isinstance(expr._args[i], ExpressionBase): _args.append(self._fix_vars(expr._args[i], model)) elif ( - isinstance(expr._args[i], Var) or isinstance(expr._args[i], _VarData) + isinstance(expr._args[i], Var) or isinstance(expr._args[i], VarData) ) and expr._args[i].fixed: if expr._args[i].value != 0.0: _args.append(as_numeric(expr._args[i].value)) diff --git a/pyomo/core/plugins/transform/equality_transform.py b/pyomo/core/plugins/transform/equality_transform.py index e0cc463e238..99291c2227c 100644 --- a/pyomo/core/plugins/transform/equality_transform.py +++ b/pyomo/core/plugins/transform/equality_transform.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -66,7 +66,7 @@ def _create_using(self, model, **kwds): con = equality.__getattribute__(con_name) # - # Get all _ConstraintData objects + # Get all ConstraintData objects # # We need to get the keys ahead of time because we are modifying # con._data on-the-fly. @@ -104,7 +104,7 @@ def _create_using(self, model, **kwds): con.add(ub_name, new_expr) # Since we explicitly `continue` for equality constraints, we - # can safely remove the old _ConstraintData object + # can safely remove the old ConstraintData object del con._data[ndx] return equality.create() diff --git a/pyomo/core/plugins/transform/expand_connectors.py b/pyomo/core/plugins/transform/expand_connectors.py index bf1b517c1b0..82ec546e593 100644 --- a/pyomo/core/plugins/transform/expand_connectors.py +++ b/pyomo/core/plugins/transform/expand_connectors.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -25,7 +25,7 @@ Var, SortComponents, ) -from pyomo.core.base.connector import _ConnectorData, ScalarConnector +from pyomo.core.base.connector import ConnectorData, ScalarConnector @TransformationFactory.register( @@ -69,7 +69,7 @@ def _apply_to(self, instance, **kwds): # The set of connectors found in the current constraint found = ComponentSet() - connector_types = set([ScalarConnector, _ConnectorData]) + connector_types = set([ScalarConnector, ConnectorData]) for constraint in instance.component_data_objects( Constraint, sort=SortComponents.deterministic ): @@ -180,9 +180,11 @@ def _validate_and_expand_connector_set(self, connectors): # -3 if v is None else -2 if k in c.aggregators - else -1 - if not hasattr(v, 'is_indexed') or not v.is_indexed() - else len(v) + else ( + -1 + if not hasattr(v, 'is_indexed') or not v.is_indexed() + else len(v) + ) ) ref[k] = (v, _len, c) @@ -220,11 +222,15 @@ def _validate_and_expand_connector_set(self, connectors): _len = ( -3 if _v is None - else -2 - if k in c.aggregators - else -1 - if not hasattr(_v, 'is_indexed') or not _v.is_indexed() - else len(_v) + else ( + -2 + if k in c.aggregators + else ( + -1 + if not hasattr(_v, 'is_indexed') or not _v.is_indexed() + else len(_v) + ) + ) ) if (_len >= 0) ^ (v[1] >= 0): raise ValueError( diff --git a/pyomo/core/plugins/transform/hierarchy.py b/pyomo/core/plugins/transform/hierarchy.py index a7667fc028a..86338d17f88 100644 --- a/pyomo/core/plugins/transform/hierarchy.py +++ b/pyomo/core/plugins/transform/hierarchy.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/plugins/transform/logical_to_linear.py b/pyomo/core/plugins/transform/logical_to_linear.py index e6554e0ed38..da69ca113bd 100644 --- a/pyomo/core/plugins/transform/logical_to_linear.py +++ b/pyomo/core/plugins/transform/logical_to_linear.py @@ -1,5 +1,17 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Transformation from BooleanVar and LogicalConstraint to Binary and Constraints.""" + from pyomo.common.collections import ComponentMap from pyomo.common.errors import MouseTrap, DeveloperError from pyomo.common.modeling import unique_component_name @@ -17,7 +29,7 @@ BooleanVarList, SortComponents, ) -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.base.boolean_var import _DeprecatedImplicitAssociatedBinaryVariable from pyomo.core.expr.cnf_walker import to_cnf from pyomo.core.expr import ( @@ -88,7 +100,7 @@ def _apply_to(self, model, **kwds): # the GDP will be solved, and it would be wrong to assume that a GDP # will *necessarily* be solved as an algebraic model. The star # example of not doing so being GDPopt.) - if t.ctype is Block or isinstance(t, _BlockData): + if t.ctype is Block or isinstance(t, BlockData): self._transform_block(t, model, new_var_lists, transBlocks) elif t.ctype is LogicalConstraint: if t.is_indexed(): @@ -273,7 +285,7 @@ class CnfToLinearVisitor(StreamBasedExpressionVisitor): """Convert CNF logical constraint to linear constraints. Expected expression node types: AndExpression, OrExpression, NotExpression, - AtLeastExpression, AtMostExpression, ExactlyExpression, _BooleanVarData + AtLeastExpression, AtMostExpression, ExactlyExpression, BooleanVarData """ @@ -360,7 +372,7 @@ def beforeChild(self, node, child, child_idx): if child.is_expression_type(): return True, None - # Only thing left should be _BooleanVarData + # Only thing left should be BooleanVarData # # TODO: After the expr_multiple_dispatch is merged, this should # be switched to using as_numeric. diff --git a/pyomo/core/plugins/transform/model.py b/pyomo/core/plugins/transform/model.py index 99c1d21c9a0..7ee268a4292 100644 --- a/pyomo/core/plugins/transform/model.py +++ b/pyomo/core/plugins/transform/model.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -55,8 +55,8 @@ def to_standard_form(self): # N.B. Structure hierarchy: # # active_components: {class: {attr_name: object}} - # object -> Constraint: ._data: {ndx: _ConstraintData} - # _ConstraintData: .lower, .body, .upper + # object -> Constraint: ._data: {ndx: ConstraintData} + # ConstraintData: .lower, .body, .upper # # So, altogether, we access a lower bound via # diff --git a/pyomo/core/plugins/transform/nonnegative_transform.py b/pyomo/core/plugins/transform/nonnegative_transform.py index b32b7b1efc0..d123e68cb2e 100644 --- a/pyomo/core/plugins/transform/nonnegative_transform.py +++ b/pyomo/core/plugins/transform/nonnegative_transform.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/plugins/transform/radix_linearization.py b/pyomo/core/plugins/transform/radix_linearization.py index 0d77a342147..92270655f31 100644 --- a/pyomo/core/plugins/transform/radix_linearization.py +++ b/pyomo/core/plugins/transform/radix_linearization.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -21,7 +21,7 @@ Block, RangeSet, ) -from pyomo.core.base.var import _VarData +from pyomo.core.base.var import VarData import logging @@ -237,10 +237,7 @@ def _discretize_bilinear(self, b, v, v_idx, u, u_idx): K = max(b.DISCRETIZATION) _dw = Var( - bounds=( - min(0, _lb * 2**-K, _ub * 2**-K), - max(0, _lb * 2**-K, _ub * 2**-K), - ) + bounds=(min(0, _lb * 2**-K, _ub * 2**-K), max(0, _lb * 2**-K, _ub * 2**-K)) ) b.add_component("dw%s_v%s" % (u_idx, v_idx), _dw) @@ -271,8 +268,8 @@ def _collect_bilinear(self, expr, bilin, quad): self._collect_bilinear(e, bilin, quad) # No need to check denominator, as this is poly_degree==2 return - if not isinstance(expr._numerator[0], _VarData) or not isinstance( - expr._numerator[1], _VarData + if not isinstance(expr._numerator[0], VarData) or not isinstance( + expr._numerator[1], VarData ): raise RuntimeError("Cannot yet handle complex subexpressions") if expr._numerator[0] is expr._numerator[1]: diff --git a/pyomo/core/plugins/transform/relax_integrality.py b/pyomo/core/plugins/transform/relax_integrality.py index 06dd2faba77..40cf74ddbcc 100644 --- a/pyomo/core/plugins/transform/relax_integrality.py +++ b/pyomo/core/plugins/transform/relax_integrality.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/plugins/transform/scaling.py b/pyomo/core/plugins/transform/scaling.py index 0883455f9de..11d4ac8c493 100644 --- a/pyomo/core/plugins/transform/scaling.py +++ b/pyomo/core/plugins/transform/scaling.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -10,16 +10,7 @@ # ___________________________________________________________________________ from pyomo.common.collections import ComponentMap -from pyomo.core.base import ( - Block, - Var, - Constraint, - Objective, - _ConstraintData, - _ObjectiveData, - Suffix, - value, -) +from pyomo.core.base import Block, Var, Constraint, Objective, Suffix, value from pyomo.core.plugins.transform.hierarchy import Transformation from pyomo.core.base import TransformationFactory from pyomo.core.base.suffix import SuffixFinder @@ -197,7 +188,7 @@ def _apply_to(self, model, rename=True): already_scaled.add(id(c)) # perform the constraint/objective scaling and variable sub scaling_factor = component_scaling_factor_map[c] - if isinstance(c, _ConstraintData): + if c.ctype is Constraint: body = scaling_factor * replace_expressions( expr=c.body, substitution_map=variable_substitution_dict, @@ -226,7 +217,7 @@ def _apply_to(self, model, rename=True): else: c.set_value((lower, body, upper)) - elif isinstance(c, _ObjectiveData): + elif c.ctype is Objective: c.expr = scaling_factor * replace_expressions( expr=c.expr, substitution_map=variable_substitution_dict, diff --git a/pyomo/core/plugins/transform/standard_form.py b/pyomo/core/plugins/transform/standard_form.py index 54df13fc49d..ffc382a2cf7 100644 --- a/pyomo/core/plugins/transform/standard_form.py +++ b/pyomo/core/plugins/transform/standard_form.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/plugins/transform/util.py b/pyomo/core/plugins/transform/util.py index bba8adfbc0f..9719b1f38d9 100644 --- a/pyomo/core/plugins/transform/util.py +++ b/pyomo/core/plugins/transform/util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/pyomoobject.py b/pyomo/core/pyomoobject.py index 692db444f84..3bf6de37489 100644 --- a/pyomo/core/pyomoobject.py +++ b/pyomo/core/pyomoobject.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/staleflag.py b/pyomo/core/staleflag.py index 7d0dddef0dd..da90032a03c 100644 --- a/pyomo/core/staleflag.py +++ b/pyomo/core/staleflag.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/__init__.py b/pyomo/core/tests/__init__.py index 0dc08cc5aea..761a6e6c44c 100644 --- a/pyomo/core/tests/__init__.py +++ b/pyomo/core/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/data/__init__.py b/pyomo/core/tests/data/__init__.py index 21b3abf0760..a73865ee112 100644 --- a/pyomo/core/tests/data/__init__.py +++ b/pyomo/core/tests/data/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/data/test_odbc_ini.py b/pyomo/core/tests/data/test_odbc_ini.py index e7152181645..43584fe3ca9 100644 --- a/pyomo/core/tests/data/test_odbc_ini.py +++ b/pyomo/core/tests/data/test_odbc_ini.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/diet/__init__.py b/pyomo/core/tests/diet/__init__.py index 3e98344ba07..717247051c4 100644 --- a/pyomo/core/tests/diet/__init__.py +++ b/pyomo/core/tests/diet/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/diet/test_diet.py b/pyomo/core/tests/diet/test_diet.py index d92f0a024ba..9e11907179e 100644 --- a/pyomo/core/tests/diet/test_diet.py +++ b/pyomo/core/tests/diet/test_diet.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/examples/__init__.py b/pyomo/core/tests/examples/__init__.py index 602516fcb56..c5ecc4ee437 100644 --- a/pyomo/core/tests/examples/__init__.py +++ b/pyomo/core/tests/examples/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/examples/pmedian.py b/pyomo/core/tests/examples/pmedian.py index 5176f8bad18..c476f01bd17 100644 --- a/pyomo/core/tests/examples/pmedian.py +++ b/pyomo/core/tests/examples/pmedian.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/examples/pmedian1.py b/pyomo/core/tests/examples/pmedian1.py index 5aeec502f7c..8e11383116b 100644 --- a/pyomo/core/tests/examples/pmedian1.py +++ b/pyomo/core/tests/examples/pmedian1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/examples/pmedian2.py b/pyomo/core/tests/examples/pmedian2.py index 8a908f7d661..88a9666fe41 100644 --- a/pyomo/core/tests/examples/pmedian2.py +++ b/pyomo/core/tests/examples/pmedian2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/examples/pmedian4.py b/pyomo/core/tests/examples/pmedian4.py index 98dd90f3e8f..101ee3e7c46 100644 --- a/pyomo/core/tests/examples/pmedian4.py +++ b/pyomo/core/tests/examples/pmedian4.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/examples/pmedian_concrete.py b/pyomo/core/tests/examples/pmedian_concrete.py new file mode 100644 index 00000000000..a6a1859df23 --- /dev/null +++ b/pyomo/core/tests/examples/pmedian_concrete.py @@ -0,0 +1,70 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import math +from pyomo.environ import ( + ConcreteModel, + Param, + RangeSet, + Var, + Reals, + Binary, + PositiveIntegers, +) + + +def _cost_rule(model, n, m): + # We will assume costs are an arbitrary function of the indices + return math.sin(n * 2.33333 + m * 7.99999) + + +def create_model(n=3, m=3, p=2): + model = ConcreteModel(name="M1") + + model.N = Param(initialize=n, within=PositiveIntegers) + model.M = Param(initialize=m, within=PositiveIntegers) + model.P = Param(initialize=p, within=RangeSet(1, model.N), mutable=True) + + model.Locations = RangeSet(1, model.N) + model.Customers = RangeSet(1, model.M) + + model.cost = Param( + model.Locations, model.Customers, initialize=_cost_rule, within=Reals + ) + model.serve_customer_from_location = Var( + model.Locations, model.Customers, bounds=(0.0, 1.0) + ) + model.select_location = Var(model.Locations, within=Binary) + + @model.Objective() + def obj(model): + return sum( + model.cost[n, m] * model.serve_customer_from_location[n, m] + for n in model.Locations + for m in model.Customers + ) + + @model.Constraint(model.Customers) + def single_x(model, m): + return ( + sum(model.serve_customer_from_location[n, m] for n in model.Locations) + == 1.0 + ) + + @model.Constraint(model.Locations, model.Customers) + def bound_y(model, n, m): + return model.serve_customer_from_location[n, m] <= model.select_location[n] + + @model.Constraint() + def num_facilities(model): + return sum(model.select_location[n] for n in model.Locations) == model.P + + return model diff --git a/pyomo/core/tests/examples/test_amplbook2.py b/pyomo/core/tests/examples/test_amplbook2.py index fdb9cc571bf..72e3d2b4599 100644 --- a/pyomo/core/tests/examples/test_amplbook2.py +++ b/pyomo/core/tests/examples/test_amplbook2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/examples/test_kernel_examples.py b/pyomo/core/tests/examples/test_kernel_examples.py index 7039f457f84..61d0fa2527d 100644 --- a/pyomo/core/tests/examples/test_kernel_examples.py +++ b/pyomo/core/tests/examples/test_kernel_examples.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -44,10 +44,10 @@ def setUpModule(): global testing_solvers import pyomo.environ - from pyomo.solvers.tests.solvers import test_solver_cases + from pyomo.solvers.tests.solvers import test_solver_cases as _test_solver_cases - for _solver, _io in test_solver_cases(): - if (_solver, _io) in testing_solvers and test_solver_cases( + for _solver, _io in _test_solver_cases(): + if (_solver, _io) in testing_solvers and _test_solver_cases( _solver, _io ).available: testing_solvers[_solver, _io] = True diff --git a/pyomo/core/tests/examples/test_pyomo.py b/pyomo/core/tests/examples/test_pyomo.py index 64c195c0ab4..2d3a39ebdda 100644 --- a/pyomo/core/tests/examples/test_pyomo.py +++ b/pyomo/core/tests/examples/test_pyomo.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/examples/test_tutorials.py b/pyomo/core/tests/examples/test_tutorials.py index 3a74c1ca142..c8de003007e 100644 --- a/pyomo/core/tests/examples/test_tutorials.py +++ b/pyomo/core/tests/examples/test_tutorials.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/transform/__init__.py b/pyomo/core/tests/transform/__init__.py index df59aa21988..f34c7624e25 100644 --- a/pyomo/core/tests/transform/__init__.py +++ b/pyomo/core/tests/transform/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/transform/test_add_slacks.py b/pyomo/core/tests/transform/test_add_slacks.py index a3698b7d529..b395237b8e4 100644 --- a/pyomo/core/tests/transform/test_add_slacks.py +++ b/pyomo/core/tests/transform/test_add_slacks.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -102,10 +102,7 @@ def checkRule1(self, m): self, cons.body, EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, m.x)), - EXPR.MonomialTermExpression((-1, transBlock._slack_minus_rule1)), - ] + [m.x, EXPR.MonomialTermExpression((-1, transBlock._slack_minus_rule1))] ), ) @@ -118,14 +115,7 @@ def checkRule3(self, m): self.assertEqual(cons.lower, 0.1) assertExpressionsEqual( - self, - cons.body, - EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, m.x)), - EXPR.MonomialTermExpression((1, transBlock._slack_plus_rule3)), - ] - ), + self, cons.body, EXPR.LinearExpression([m.x, transBlock._slack_plus_rule3]) ) def test_ub_constraint_modified(self): @@ -154,8 +144,8 @@ def test_both_bounds_constraint_modified(self): cons.body, EXPR.LinearExpression( [ - EXPR.MonomialTermExpression((1, m.y)), - EXPR.MonomialTermExpression((1, transBlock._slack_plus_rule2)), + m.y, + transBlock._slack_plus_rule2, EXPR.MonomialTermExpression((-1, transBlock._slack_minus_rule2)), ] ), @@ -184,10 +174,10 @@ def test_new_obj_created(self): obj.expr, EXPR.LinearExpression( [ - EXPR.MonomialTermExpression((1, transBlock._slack_minus_rule1)), - EXPR.MonomialTermExpression((1, transBlock._slack_plus_rule2)), - EXPR.MonomialTermExpression((1, transBlock._slack_minus_rule2)), - EXPR.MonomialTermExpression((1, transBlock._slack_plus_rule3)), + transBlock._slack_minus_rule1, + transBlock._slack_plus_rule2, + transBlock._slack_minus_rule2, + transBlock._slack_plus_rule3, ] ), ) @@ -302,10 +292,7 @@ def checkTargetsObj(self, m): self, obj.expr, EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, transBlock._slack_minus_rule1)), - EXPR.MonomialTermExpression((1, transBlock._slack_plus_rule3)), - ] + [transBlock._slack_minus_rule1, transBlock._slack_plus_rule3] ), ) @@ -343,7 +330,7 @@ def test_error_for_non_constraint_noniterable_target(self): self.assertRaisesRegex( ValueError, "Expected Constraint or list of Constraints.\n\tReceived " - "", + "", TransformationFactory('core.add_slack_variables').apply_to, m, targets=m.indexedVar[1], @@ -423,9 +410,9 @@ def test_transformed_constraints_sumexpression_body(self): c.body, EXPR.LinearExpression( [ - EXPR.MonomialTermExpression((1, m.x)), + m.x, EXPR.MonomialTermExpression((-2, m.y)), - EXPR.MonomialTermExpression((1, transBlock._slack_plus_rule4)), + transBlock._slack_plus_rule4, EXPR.MonomialTermExpression((-1, transBlock._slack_minus_rule4)), ] ), @@ -518,15 +505,9 @@ def checkTargetObj(self, m): obj.expr, EXPR.LinearExpression( [ - EXPR.MonomialTermExpression( - (1, transBlock.component("_slack_plus_rule1[1]")) - ), - EXPR.MonomialTermExpression( - (1, transBlock.component("_slack_plus_rule1[2]")) - ), - EXPR.MonomialTermExpression( - (1, transBlock.component("_slack_plus_rule1[3]")) - ), + transBlock.component("_slack_plus_rule1[1]"), + transBlock.component("_slack_plus_rule1[2]"), + transBlock.component("_slack_plus_rule1[3]"), ] ), ) @@ -558,14 +539,7 @@ def checkTransformedRule1(self, m, i): EXPR.LinearExpression( [ EXPR.MonomialTermExpression((2, m.x[i])), - EXPR.MonomialTermExpression( - ( - 1, - m._core_add_slack_variables.component( - "_slack_plus_rule1[%s]" % i - ), - ) - ), + m._core_add_slack_variables.component("_slack_plus_rule1[%s]" % i), ] ), ) diff --git a/pyomo/core/tests/transform/test_scaling.py b/pyomo/core/tests/transform/test_scaling.py index 1cb4e886956..d0fbfab61bd 100644 --- a/pyomo/core/tests/transform/test_scaling.py +++ b/pyomo/core/tests/transform/test_scaling.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/transform/test_transform.py b/pyomo/core/tests/transform/test_transform.py index 7c3f17fcfec..cd1f26417a7 100644 --- a/pyomo/core/tests/transform/test_transform.py +++ b/pyomo/core/tests/transform/test_transform.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/__init__.py b/pyomo/core/tests/unit/__init__.py index 65e82b81c0c..85ece8d8cd5 100644 --- a/pyomo/core/tests/unit/__init__.py +++ b/pyomo/core/tests/unit/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/kernel/__init__.py b/pyomo/core/tests/unit/kernel/__init__.py index ff387efbd03..e5231e0f859 100644 --- a/pyomo/core/tests/unit/kernel/__init__.py +++ b/pyomo/core/tests/unit/kernel/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/kernel/test_block.py b/pyomo/core/tests/unit/kernel/test_block.py index 5d1ecc33f06..b21771653bb 100644 --- a/pyomo/core/tests/unit/kernel/test_block.py +++ b/pyomo/core/tests/unit/kernel/test_block.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -1646,13 +1646,15 @@ def test_components_no_descend_active_True(self): ctype=IBlock, active=True, descend_into=False ) ), - sorted( - str(_b) - for _b in self._components_no_descend[obj][IBlock] - if _b.active - ) - if getattr(obj, 'active', True) - else [], + ( + sorted( + str(_b) + for _b in self._components_no_descend[obj][IBlock] + if _b.active + ) + if getattr(obj, 'active', True) + else [] + ), ) self.assertEqual( set( @@ -1661,13 +1663,15 @@ def test_components_no_descend_active_True(self): ctype=IBlock, active=True, descend_into=False ) ), - set( - id(_b) - for _b in self._components_no_descend[obj][IBlock] - if _b.active - ) - if getattr(obj, 'active', True) - else set(), + ( + set( + id(_b) + for _b in self._components_no_descend[obj][IBlock] + if _b.active + ) + if getattr(obj, 'active', True) + else set() + ), ) # test ctype=IVariable self.assertEqual( @@ -1677,9 +1681,13 @@ def test_components_no_descend_active_True(self): ctype=IVariable, active=True, descend_into=False ) ), - sorted(str(_v) for _v in self._components_no_descend[obj][IVariable]) - if getattr(obj, 'active', True) - else [], + ( + sorted( + str(_v) for _v in self._components_no_descend[obj][IVariable] + ) + if getattr(obj, 'active', True) + else [] + ), ) self.assertEqual( set( @@ -1688,34 +1696,40 @@ def test_components_no_descend_active_True(self): ctype=IVariable, active=True, descend_into=False ) ), - set(id(_v) for _v in self._components_no_descend[obj][IVariable]) - if getattr(obj, 'active', True) - else set(), + ( + set(id(_v) for _v in self._components_no_descend[obj][IVariable]) + if getattr(obj, 'active', True) + else set() + ), ) # test no ctype self.assertEqual( sorted( str(_c) for _c in obj.components(active=True, descend_into=False) ), - sorted( - str(_c) - for ctype in self._components_no_descend[obj] - for _c in self._components_no_descend[obj][ctype] - if getattr(_c, "active", True) - ) - if getattr(obj, 'active', True) - else [], + ( + sorted( + str(_c) + for ctype in self._components_no_descend[obj] + for _c in self._components_no_descend[obj][ctype] + if getattr(_c, "active", True) + ) + if getattr(obj, 'active', True) + else [] + ), ) self.assertEqual( set(id(_c) for _c in obj.components(active=True, descend_into=False)), - set( - id(_c) - for ctype in self._components_no_descend[obj] - for _c in self._components_no_descend[obj][ctype] - if getattr(_c, "active", True) - ) - if getattr(obj, 'active', True) - else set(), + ( + set( + id(_c) + for ctype in self._components_no_descend[obj] + for _c in self._components_no_descend[obj][ctype] + if getattr(_c, "active", True) + ) + if getattr(obj, 'active', True) + else set() + ), ) def test_components_active_None(self): @@ -1794,9 +1808,11 @@ def test_components_active_True(self): ctype=IBlock, active=True, descend_into=True ) ), - sorted(str(_b) for _b in self._components[obj][IBlock] if _b.active) - if getattr(obj, 'active', True) - else [], + ( + sorted(str(_b) for _b in self._components[obj][IBlock] if _b.active) + if getattr(obj, 'active', True) + else [] + ), ) self.assertEqual( set( @@ -1805,9 +1821,11 @@ def test_components_active_True(self): ctype=IBlock, active=True, descend_into=True ) ), - set(id(_b) for _b in self._components[obj][IBlock] if _b.active) - if getattr(obj, 'active', True) - else set(), + ( + set(id(_b) for _b in self._components[obj][IBlock] if _b.active) + if getattr(obj, 'active', True) + else set() + ), ) # test ctype=IVariable self.assertEqual( @@ -1817,13 +1835,15 @@ def test_components_active_True(self): ctype=IVariable, active=True, descend_into=True ) ), - sorted( - str(_v) - for _v in self._components[obj][IVariable] - if _active_path_to_object_exists(obj, _v) - ) - if getattr(obj, 'active', True) - else [], + ( + sorted( + str(_v) + for _v in self._components[obj][IVariable] + if _active_path_to_object_exists(obj, _v) + ) + if getattr(obj, 'active', True) + else [] + ), ) self.assertEqual( set( @@ -1832,38 +1852,44 @@ def test_components_active_True(self): ctype=IVariable, active=True, descend_into=True ) ), - set( - id(_v) - for _v in self._components[obj][IVariable] - if _active_path_to_object_exists(obj, _v) - ) - if getattr(obj, 'active', True) - else set(), + ( + set( + id(_v) + for _v in self._components[obj][IVariable] + if _active_path_to_object_exists(obj, _v) + ) + if getattr(obj, 'active', True) + else set() + ), ) # test no ctype self.assertEqual( sorted( str(_c) for _c in obj.components(active=True, descend_into=True) ), - sorted( - str(_c) - for ctype in self._components[obj] - for _c in self._components[obj][ctype] - if _active_path_to_object_exists(obj, _c) - ) - if getattr(obj, 'active', True) - else [], + ( + sorted( + str(_c) + for ctype in self._components[obj] + for _c in self._components[obj][ctype] + if _active_path_to_object_exists(obj, _c) + ) + if getattr(obj, 'active', True) + else [] + ), ) self.assertEqual( set(id(_c) for _c in obj.components(active=True, descend_into=True)), - set( - id(_c) - for ctype in self._components[obj] - for _c in self._components[obj][ctype] - if _active_path_to_object_exists(obj, _c) - ) - if getattr(obj, 'active', True) - else set(), + ( + set( + id(_c) + for ctype in self._components[obj] + for _c in self._components[obj][ctype] + if _active_path_to_object_exists(obj, _c) + ) + if getattr(obj, 'active', True) + else set() + ), ) diff --git a/pyomo/core/tests/unit/kernel/test_component_map.py b/pyomo/core/tests/unit/kernel/test_component_map.py index 64ba700895e..3fb8b99a9a3 100644 --- a/pyomo/core/tests/unit/kernel/test_component_map.py +++ b/pyomo/core/tests/unit/kernel/test_component_map.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -234,6 +234,20 @@ def test_eq(self): self.assertTrue(cmap1 != cmap2) self.assertNotEqual(cmap1, cmap2) + cmap2 = ComponentMap(self._components) + o = objective() + cmap1[o] = 10 + cmap2[o] = 10 + self.assertEqual(cmap1, cmap2) + cmap2[o] = 20 + self.assertNotEqual(cmap1, cmap2) + cmap2[o] = 10 + self.assertEqual(cmap1, cmap2) + del cmap2[o] + self.assertNotEqual(cmap1, cmap2) + cmap2[objective()] = 10 + self.assertNotEqual(cmap1, cmap2) + if __name__ == "__main__": unittest.main() diff --git a/pyomo/core/tests/unit/kernel/test_component_set.py b/pyomo/core/tests/unit/kernel/test_component_set.py index 10a7b27e59e..38f17a702c1 100644 --- a/pyomo/core/tests/unit/kernel/test_component_set.py +++ b/pyomo/core/tests/unit/kernel/test_component_set.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -264,6 +264,14 @@ def test_eq(self): self.assertTrue(cset1 != cset2) self.assertNotEqual(cset1, cset2) + cset2.add(variable()) + self.assertFalse(cset2 == cset1) + self.assertTrue(cset2 != cset1) + self.assertNotEqual(cset2, cset1) + self.assertFalse(cset1 == cset2) + self.assertTrue(cset1 != cset2) + self.assertNotEqual(cset1, cset2) + if __name__ == "__main__": unittest.main() diff --git a/pyomo/core/tests/unit/kernel/test_conic.py b/pyomo/core/tests/unit/kernel/test_conic.py index e7416210b8a..ccfbcca7e1f 100644 --- a/pyomo/core/tests/unit/kernel/test_conic.py +++ b/pyomo/core/tests/unit/kernel/test_conic.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -700,8 +700,7 @@ def test_expression(self): c.x[0].value = 1.2 c.x[1].value = -5.3 val = round( - (1.2**2 + (-5.3) ** 2) ** 0.5 - - ((2.7 / 0.4) ** 0.4) * ((3.7 / 0.6) ** 0.6), + (1.2**2 + (-5.3) ** 2) ** 0.5 - ((2.7 / 0.4) ** 0.4) * ((3.7 / 0.6) ** 0.6), 9, ) self.assertEqual(round(c(), 9), val) diff --git a/pyomo/core/tests/unit/kernel/test_constraint.py b/pyomo/core/tests/unit/kernel/test_constraint.py index f2f219cc66f..97832dd8bca 100644 --- a/pyomo/core/tests/unit/kernel/test_constraint.py +++ b/pyomo/core/tests/unit/kernel/test_constraint.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/kernel/test_dict_container.py b/pyomo/core/tests/unit/kernel/test_dict_container.py index e6b6f8d7aab..6ae25362bb2 100644 --- a/pyomo/core/tests/unit/kernel/test_dict_container.py +++ b/pyomo/core/tests/unit/kernel/test_dict_container.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/kernel/test_expression.py b/pyomo/core/tests/unit/kernel/test_expression.py index 85f8c331a46..39d3eaa463c 100644 --- a/pyomo/core/tests/unit/kernel/test_expression.py +++ b/pyomo/core/tests/unit/kernel/test_expression.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/kernel/test_kernel.py b/pyomo/core/tests/unit/kernel/test_kernel.py index fbff295881a..b34bcdeaadb 100644 --- a/pyomo/core/tests/unit/kernel/test_kernel.py +++ b/pyomo/core/tests/unit/kernel/test_kernel.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/kernel/test_list_container.py b/pyomo/core/tests/unit/kernel/test_list_container.py index 9e3ada739b2..a4641f83295 100644 --- a/pyomo/core/tests/unit/kernel/test_list_container.py +++ b/pyomo/core/tests/unit/kernel/test_list_container.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/kernel/test_matrix_constraint.py b/pyomo/core/tests/unit/kernel/test_matrix_constraint.py index c986e5eda96..24a2915f224 100644 --- a/pyomo/core/tests/unit/kernel/test_matrix_constraint.py +++ b/pyomo/core/tests/unit/kernel/test_matrix_constraint.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/kernel/test_objective.py b/pyomo/core/tests/unit/kernel/test_objective.py index f60ff9bdb49..810218f1dc2 100644 --- a/pyomo/core/tests/unit/kernel/test_objective.py +++ b/pyomo/core/tests/unit/kernel/test_objective.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/kernel/test_parameter.py b/pyomo/core/tests/unit/kernel/test_parameter.py index 04dc08f095f..469ed9fbe8c 100644 --- a/pyomo/core/tests/unit/kernel/test_parameter.py +++ b/pyomo/core/tests/unit/kernel/test_parameter.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/kernel/test_piecewise.py b/pyomo/core/tests/unit/kernel/test_piecewise.py index 2c236c0dd12..3d9cf66e39c 100644 --- a/pyomo/core/tests/unit/kernel/test_piecewise.py +++ b/pyomo/core/tests/unit/kernel/test_piecewise.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/kernel/test_sos.py b/pyomo/core/tests/unit/kernel/test_sos.py index 9410425d405..b1cb67a96f8 100644 --- a/pyomo/core/tests/unit/kernel/test_sos.py +++ b/pyomo/core/tests/unit/kernel/test_sos.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/kernel/test_suffix.py b/pyomo/core/tests/unit/kernel/test_suffix.py index c4c75278d50..2a73888c2d3 100644 --- a/pyomo/core/tests/unit/kernel/test_suffix.py +++ b/pyomo/core/tests/unit/kernel/test_suffix.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/kernel/test_tuple_container.py b/pyomo/core/tests/unit/kernel/test_tuple_container.py index 0b45c36b299..c016c5fc789 100644 --- a/pyomo/core/tests/unit/kernel/test_tuple_container.py +++ b/pyomo/core/tests/unit/kernel/test_tuple_container.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/kernel/test_variable.py b/pyomo/core/tests/unit/kernel/test_variable.py index e360240f3b2..181eb15c972 100644 --- a/pyomo/core/tests/unit/kernel/test_variable.py +++ b/pyomo/core/tests/unit/kernel/test_variable.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_action.py b/pyomo/core/tests/unit/test_action.py index 5db6f165854..3481c90a021 100644 --- a/pyomo/core/tests/unit/test_action.py +++ b/pyomo/core/tests/unit/test_action.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_block.py b/pyomo/core/tests/unit/test_block.py index f68850d9421..3d578f7dc88 100644 --- a/pyomo/core/tests/unit/test_block.py +++ b/pyomo/core/tests/unit/test_block.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -13,6 +13,7 @@ # from io import StringIO +import logging import os import sys import types @@ -54,7 +55,7 @@ from pyomo.core.base.block import ( ScalarBlock, SubclassOf, - _BlockData, + BlockData, declare_custom_block, ) import pyomo.core.expr as EXPR @@ -851,7 +852,7 @@ class DerivedBlock(ScalarBlock): _Block_reserved_words = None DerivedBlock._Block_reserved_words = ( - set(['a', 'b', 'c']) | _BlockData._Block_reserved_words + set(['a', 'b', 'c']) | BlockData._Block_reserved_words ) m = ConcreteModel() @@ -965,7 +966,7 @@ def __init__(self, *args, **kwds): b.c.d.e = Block() with self.assertRaisesRegex( ValueError, - r'_BlockData.transfer_attributes_from\(\): ' + r'BlockData.transfer_attributes_from\(\): ' r'Cannot set a sub-block \(c.d.e\) to a parent block \(c\):', ): b.c.d.e.transfer_attributes_from(b.c) @@ -974,7 +975,7 @@ def __init__(self, *args, **kwds): b = Block(concrete=True) with self.assertRaisesRegex( ValueError, - r'_BlockData.transfer_attributes_from\(\): expected a Block ' + r'BlockData.transfer_attributes_from\(\): expected a Block ' 'or dict; received str', ): b.transfer_attributes_from('foo') @@ -2626,19 +2627,16 @@ def test_pprint(self): m = HierarchicalModel().model buf = StringIO() m.pprint(ostream=buf) - ref = """3 Set Declarations + ref = """2 Set Declarations a1_IDX : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 2 : {5, 4} a3_IDX : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 2 : {6, 7} - a_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {1, 2, 3} 3 Block Declarations - a : Size=3, Index=a_index, Active=True + a : Size=3, Index={1, 2, 3}, Active=True a[1] : Active=True 2 Block Declarations c : Size=2, Index=a1_IDX, Active=True @@ -2668,9 +2666,8 @@ def test_pprint(self): c : Size=1, Index=None, Active=True 0 Declarations: -6 Declarations: a1_IDX a3_IDX c a_index a b +5 Declarations: a1_IDX a3_IDX c a b """ - print(buf.getvalue()) self.assertEqual(ref, buf.getvalue()) @unittest.skipIf(not 'glpk' in solvers, "glpk solver is not available") @@ -2979,9 +2976,70 @@ def test_write_exceptions(self): with self.assertRaisesRegex(ValueError, ".*Cannot write model in format"): m.write(format="bogus") - def test_override_pprint(self): + def test_custom_block(self): + @declare_custom_block('TestingBlock') + class TestingBlockData(BlockData): + def __init__(self, component): + BlockData.__init__(self, component) + logging.getLogger(__name__).warning("TestingBlockData.__init__") + + self.assertIn('TestingBlock', globals()) + self.assertIn('ScalarTestingBlock', globals()) + self.assertIn('IndexedTestingBlock', globals()) + self.assertIs(TestingBlock.__module__, __name__) + self.assertIs(ScalarTestingBlock.__module__, __name__) + self.assertIs(IndexedTestingBlock.__module__, __name__) + + with LoggingIntercept() as LOG: + obj = TestingBlock() + self.assertIs(type(obj), ScalarTestingBlock) + self.assertEqual(LOG.getvalue().strip(), "TestingBlockData.__init__") + + with LoggingIntercept() as LOG: + obj = TestingBlock([1, 2]) + self.assertIs(type(obj), IndexedTestingBlock) + self.assertEqual(LOG.getvalue(), "") + + # Test that we can derive from a ScalarCustomBlock + class DerivedScalarTestingBlock(ScalarTestingBlock): + pass + + with LoggingIntercept() as LOG: + obj = DerivedScalarTestingBlock() + self.assertIs(type(obj), DerivedScalarTestingBlock) + self.assertEqual(LOG.getvalue().strip(), "TestingBlockData.__init__") + + def test_custom_block_ctypes(self): + @declare_custom_block('TestingBlock') + class TestingBlockData(BlockData): + pass + + self.assertIs(TestingBlock().ctype, Block) + + @declare_custom_block('TestingBlock', True) + class TestingBlockData(BlockData): + pass + + self.assertIs(TestingBlock().ctype, TestingBlock) + + @declare_custom_block('TestingBlock', Constraint) + class TestingBlockData(BlockData): + pass + + self.assertIs(TestingBlock().ctype, Constraint) + + with self.assertRaisesRegex( + ValueError, + r"Expected new_ctype to be either type or 'True'; received: \[\]", + ): + + @declare_custom_block('TestingBlock', []) + class TestingBlockData(BlockData): + pass + + def test_custom_block_override_pprint(self): @declare_custom_block('TempBlock') - class TempBlockData(_BlockData): + class TempBlockData(BlockData): def pprint(self, ostream=None, verbose=False, prefix=""): ostream.write('Testing pprint of a custom block.') @@ -3056,9 +3114,9 @@ def test_derived_block_construction(self): class ConcreteBlock(Block): pass - class ScalarConcreteBlock(_BlockData, ConcreteBlock): + class ScalarConcreteBlock(BlockData, ConcreteBlock): def __init__(self, *args, **kwds): - _BlockData.__init__(self, component=self) + BlockData.__init__(self, component=self) ConcreteBlock.__init__(self, *args, **kwds) _buf = [] @@ -3407,6 +3465,97 @@ def test_deduplicate_component_data_iterindex(self): ], ) + def test_private_data(self): + m = ConcreteModel() + m.b = Block() + m.b.b = Block([1, 2]) + + mfe = m.private_data() + self.assertIsInstance(mfe, dict) + self.assertEqual(len(mfe), 0) + self.assertEqual(len(m._private_data), 1) + self.assertIn('pyomo.core.tests.unit.test_block', m._private_data) + self.assertIs(mfe, m._private_data['pyomo.core.tests.unit.test_block']) + + with self.assertRaisesRegex( + ValueError, + "All keys in the 'private_data' dictionary must " + "be substrings of the caller's module name. " + "Received 'no mice here' when calling private_data on Block " + "'b'.", + ): + mfe2 = m.b.private_data('no mice here') + + mfe3 = m.b.b[1].private_data('pyomo.core.tests') + self.assertIsInstance(mfe3, dict) + self.assertEqual(len(mfe3), 0) + self.assertIsInstance(m.b.b[1]._private_data, dict) + self.assertEqual(len(m.b.b[1]._private_data), 1) + self.assertIn('pyomo.core.tests', m.b.b[1]._private_data) + self.assertIs(mfe3, m.b.b[1]._private_data['pyomo.core.tests']) + mfe3['there are cookies'] = 'but no mice' + + mfe4 = m.b.b[1].private_data('pyomo.core.tests') + self.assertIs(mfe4, mfe3) + + def test_register_private_data(self): + _save = Block._private_data_initializers + + Block._private_data_initializers = pdi = _save.copy() + pdi.clear() + try: + self.assertEqual(len(pdi), 0) + b = Block(concrete=True) + ps = b.private_data() + self.assertEqual(ps, {}) + self.assertEqual(len(pdi), 1) + finally: + Block._private_data_initializers = _save + + def init(): + return {'a': None, 'b': 1} + + Block._private_data_initializers = pdi = _save.copy() + pdi.clear() + try: + self.assertEqual(len(pdi), 0) + Block.register_private_data_initializer(init) + self.assertEqual(len(pdi), 1) + + b = Block(concrete=True) + ps = b.private_data() + self.assertEqual(ps, {'a': None, 'b': 1}) + self.assertEqual(len(pdi), 1) + finally: + Block._private_data_initializers = _save + + Block._private_data_initializers = pdi = _save.copy() + pdi.clear() + try: + Block.register_private_data_initializer(init) + self.assertEqual(len(pdi), 1) + Block.register_private_data_initializer(init, 'pyomo') + self.assertEqual(len(pdi), 2) + + with self.assertRaisesRegex( + RuntimeError, + r"Duplicate initializer registration for 'private_data' " + r"dictionary \(scope=pyomo.core.tests.unit.test_block\)", + ): + Block.register_private_data_initializer(init) + + with self.assertRaisesRegex( + ValueError, + r"'private_data' scope must be substrings of the caller's " + r"module name. Received 'invalid' when calling " + r"register_private_data_initializer\(\).", + ): + Block.register_private_data_initializer(init, 'invalid') + + self.assertEqual(len(pdi), 2) + finally: + Block._private_data_initializers = _save + if __name__ == "__main__": unittest.main() diff --git a/pyomo/core/tests/unit/test_block_model.py b/pyomo/core/tests/unit/test_block_model.py index ed751e96fc5..b4cf34e7516 100644 --- a/pyomo/core/tests/unit/test_block_model.py +++ b/pyomo/core/tests/unit/test_block_model.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_bounds.py b/pyomo/core/tests/unit/test_bounds.py index c2c6a69bdd2..23554f555c9 100644 --- a/pyomo/core/tests/unit/test_bounds.py +++ b/pyomo/core/tests/unit/test_bounds.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_check.py b/pyomo/core/tests/unit/test_check.py index 5b2d5408fd5..e61e3998fb7 100644 --- a/pyomo/core/tests/unit/test_check.py +++ b/pyomo/core/tests/unit/test_check.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_compare.py b/pyomo/core/tests/unit/test_compare.py index 8b8538a8656..7c3536bc084 100644 --- a/pyomo/core/tests/unit/test_compare.py +++ b/pyomo/core/tests/unit/test_compare.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -165,17 +165,11 @@ def test_expr_if(self): 0, (EqualityExpression, 2), (LinearExpression, 2), - (MonomialTermExpression, 2), - 1, m.y, - (MonomialTermExpression, 2), - 1, m.x, 0, (EqualityExpression, 2), (LinearExpression, 2), - (MonomialTermExpression, 2), - 1, m.y, (MonomialTermExpression, 2), -1, diff --git a/pyomo/core/tests/unit/test_component.py b/pyomo/core/tests/unit/test_component.py index b4408fe8c54..b12db9af047 100644 --- a/pyomo/core/tests/unit/test_component.py +++ b/pyomo/core/tests/unit/test_component.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -66,19 +66,17 @@ def test_getname(self): ) m.b[2]._component = None - self.assertEqual( - m.b[2].getname(fully_qualified=True), "[Unattached _BlockData]" - ) + self.assertEqual(m.b[2].getname(fully_qualified=True), "[Unattached BlockData]") # I think that getname() should do this: # self.assertEqual(m.b[2].c[2,4].getname(fully_qualified=True), - # "[Unattached _BlockData].c[2,4]") + # "[Unattached BlockData].c[2,4]") # but it doesn't match current behavior. I will file a PEP to # propose changing the behavior later and proceed to test # current behavior. self.assertEqual(m.b[2].c[2, 4].getname(fully_qualified=True), "c[2,4]") self.assertEqual( - m.b[2].getname(fully_qualified=False), "[Unattached _BlockData]" + m.b[2].getname(fully_qualified=False), "[Unattached BlockData]" ) self.assertEqual(m.b[2].c[2, 4].getname(fully_qualified=False), "c[2,4]") diff --git a/pyomo/core/tests/unit/test_componentuid.py b/pyomo/core/tests/unit/test_componentuid.py index 1c9b3c444bf..5808bedb7fd 100644 --- a/pyomo/core/tests/unit/test_componentuid.py +++ b/pyomo/core/tests/unit/test_componentuid.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -601,31 +601,26 @@ def test_generate_cuid_string_map(self): ComponentUID.generate_cuid_string_map(model, repr_version=1), ComponentUID.generate_cuid_string_map(model), ) - self.assertEqual(len(cuids[0]), 29) - self.assertEqual(len(cuids[1]), 29) + self.assertEqual(len(cuids[0]), 24) + self.assertEqual(len(cuids[1]), 24) for obj in [ model, model.x, model.y, - model.y_index, model.y[1], model.y[2], model.V, - model.V_index, model.V['a', 'b'], model.V[1, '2'], model.V[3, 4], model.b, model.b.z, - model.b.z_index, model.b.z[1], model.b.z['2'], getattr(model.b, '.H'), - getattr(model.b, '.H_index'), getattr(model.b, '.H')['a'], getattr(model.b, '.H')[2], model.B, - model.B_index, model.B['a'], getattr(model.B['a'], '.k'), model.B[2], @@ -642,23 +637,20 @@ def test_generate_cuid_string_map(self): ), ComponentUID.generate_cuid_string_map(model, descend_into=False), ) - self.assertEqual(len(cuids[0]), 18) - self.assertEqual(len(cuids[1]), 18) + self.assertEqual(len(cuids[0]), 15) + self.assertEqual(len(cuids[1]), 15) for obj in [ model, model.x, model.y, - model.y_index, model.y[1], model.y[2], model.V, - model.V_index, model.V['a', 'b'], model.V[1, '2'], model.V[3, 4], model.b, model.B, - model.B_index, model.B['a'], model.B[2], model.component('c tuple')[(1,)], diff --git a/pyomo/core/tests/unit/test_con.py b/pyomo/core/tests/unit/test_con.py index bd90972fee2..15f190e281e 100644 --- a/pyomo/core/tests/unit/test_con.py +++ b/pyomo/core/tests/unit/test_con.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -44,7 +44,7 @@ InequalityExpression, RangedExpression, ) -from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.constraint import ConstraintData class TestConstraintCreation(unittest.TestCase): @@ -1074,7 +1074,7 @@ def test_setitem(self): m.c[2] = m.x**2 <= 4 self.assertEqual(len(m.c), 1) self.assertEqual(list(m.c.keys()), [2]) - self.assertIsInstance(m.c[2], _GeneralConstraintData) + self.assertIsInstance(m.c[2], ConstraintData) self.assertEqual(m.c[2].upper, 4) m.c[3] = Constraint.Skip @@ -1388,7 +1388,7 @@ def test_empty_singleton(self): # Even though we construct a ScalarConstraint, # if it is not initialized that means it is "empty" # and we should encounter errors when trying to access the - # _ConstraintData interface methods until we assign + # ConstraintData interface methods until we assign # something to the constraint. # self.assertEqual(a._constructed, True) diff --git a/pyomo/core/tests/unit/test_concrete.py b/pyomo/core/tests/unit/test_concrete.py index a9bd75f05c7..9083c5cf7f9 100644 --- a/pyomo/core/tests/unit/test_concrete.py +++ b/pyomo/core/tests/unit/test_concrete.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_connector.py b/pyomo/core/tests/unit/test_connector.py index 1dde9f3af24..3871f5f372a 100644 --- a/pyomo/core/tests/unit/test_connector.py +++ b/pyomo/core/tests/unit/test_connector.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -301,7 +301,7 @@ def test_expand_single_scalar(self): m.component('c.expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """c.expanded : Size=1, Index='c.expanded_index', Active=True + """c.expanded : Size=1, Index={1}, Active=True Key : Lower : Body : Upper : Active 1 : 1.0 : x : 1.0 : True """, @@ -336,7 +336,7 @@ def test_expand_scalar(self): m.component('c.expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """c.expanded : Size=2, Index='c.expanded_index', Active=True + """c.expanded : Size=2, Index={1, 2}, Active=True Key : Lower : Body : Upper : Active 1 : 1.0 : x : 1.0 : True 2 : 1.0 : y : 1.0 : True @@ -372,7 +372,7 @@ def test_expand_expression(self): m.component('c.expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """c.expanded : Size=2, Index='c.expanded_index', Active=True + """c.expanded : Size=2, Index={1, 2}, Active=True Key : Lower : Body : Upper : Active 1 : 1.0 : - x : 1.0 : True 2 : 1.0 : 1 + y : 1.0 : True @@ -408,7 +408,7 @@ def test_expand_indexed(self): m.component('c.expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """c.expanded : Size=3, Index='c.expanded_index', Active=True + """c.expanded : Size=3, Index={1, 2, 3}, Active=True Key : Lower : Body : Upper : Active 1 : 1.0 : x[1] : 1.0 : True 2 : 1.0 : x[2] : 1.0 : True @@ -451,7 +451,7 @@ def test_expand_empty_scalar(self): m.component('c.expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """c.expanded : Size=2, Index='c.expanded_index', Active=True + """c.expanded : Size=2, Index={1, 2}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : x - 'ECON.auto.x' : 0.0 : True 2 : 0.0 : y - 'ECON.auto.y' : 0.0 : True @@ -488,7 +488,7 @@ def test_expand_empty_expression(self): m.component('c.expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """c.expanded : Size=2, Index='c.expanded_index', Active=True + """c.expanded : Size=2, Index={1, 2}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : - x - 'ECON.auto.x' : 0.0 : True 2 : 0.0 : 1 + y - 'ECON.auto.y' : 0.0 : True @@ -533,7 +533,7 @@ def test_expand_empty_indexed(self): m.component('c.expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """c.expanded : Size=3, Index='c.expanded_index', Active=True + """c.expanded : Size=3, Index={1, 2, 3}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : x[1] - 'ECON.auto.x'[1] : 0.0 : True 2 : 0.0 : x[2] - 'ECON.auto.x'[2] : 0.0 : True @@ -590,7 +590,7 @@ def test_expand_multiple_empty_indexed(self): m.component('c.expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """c.expanded : Size=3, Index='c.expanded_index', Active=True + """c.expanded : Size=3, Index={1, 2, 3}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : x[1] - 'ECON1.auto.x'[1] : 0.0 : True 2 : 0.0 : x[2] - 'ECON1.auto.x'[2] : 0.0 : True @@ -602,7 +602,7 @@ def test_expand_multiple_empty_indexed(self): m.component('d.expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """d.expanded : Size=3, Index='d.expanded_index', Active=True + """d.expanded : Size=3, Index={1, 2, 3}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : 'ECON2.auto.x'[1] - 'ECON1.auto.x'[1] : 0.0 : True 2 : 0.0 : 'ECON2.auto.x'[2] - 'ECON1.auto.x'[2] : 0.0 : True @@ -653,7 +653,7 @@ def test_expand_multiple_indexed(self): m.component('c.expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """c.expanded : Size=3, Index='c.expanded_index', Active=True + """c.expanded : Size=3, Index={1, 2, 3}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : x[1] - a2[1] : 0.0 : True 2 : 0.0 : x[2] - a2[2] : 0.0 : True @@ -665,7 +665,7 @@ def test_expand_multiple_indexed(self): m.component('d.expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """d.expanded : Size=3, Index='d.expanded_index', Active=True + """d.expanded : Size=3, Index={1, 2, 3}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : a1[1] - a2[1] : 0.0 : True 2 : 0.0 : a1[2] - a2[2] : 0.0 : True @@ -734,7 +734,7 @@ def test_expand_implicit_indexed(self): m.component('c.expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """c.expanded : Size=3, Index='c.expanded_index', Active=True + """c.expanded : Size=3, Index={1, 2, 3}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : x[1] - a2[1] : 0.0 : True 2 : 0.0 : x[2] - a2[2] : 0.0 : True @@ -746,7 +746,7 @@ def test_expand_implicit_indexed(self): m.component('d.expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """d.expanded : Size=3, Index='d.expanded_index', Active=True + """d.expanded : Size=3, Index={1, 2, 3}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : 'ECON2.auto.x'[1] - x[1] : 0.0 : True 2 : 0.0 : 'ECON2.auto.x'[2] - x[2] : 0.0 : True @@ -789,7 +789,7 @@ def test_varlist_aggregator(self): m.component('c.expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """c.expanded : Size=2, Index='c.expanded_index', Active=True + """c.expanded : Size=2, Index={1, 2}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : flow[1] - 'ECON1.auto.flow' : 0.0 : True 2 : 0.0 : phase - 'ECON1.auto.phase' : 0.0 : True @@ -800,7 +800,7 @@ def test_varlist_aggregator(self): m.component('d.expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """d.expanded : Size=2, Index='d.expanded_index', Active=True + """d.expanded : Size=2, Index={1, 2}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : 'ECON2.auto.flow' - flow[2] : 0.0 : True 2 : 0.0 : 'ECON2.auto.phase' - phase : 0.0 : True @@ -844,7 +844,7 @@ def test_indexed_connector(self): m.component('eq.expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """eq.expanded : Size=1, Index='eq.expanded_index', Active=True + """eq.expanded : Size=1, Index={1}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : x - y : 0.0 : True """, diff --git a/pyomo/core/tests/unit/test_deprecation.py b/pyomo/core/tests/unit/test_deprecation.py index 9adf2de26cd..7d718a4bd2a 100644 --- a/pyomo/core/tests/unit/test_deprecation.py +++ b/pyomo/core/tests/unit/test_deprecation.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_derivs.py b/pyomo/core/tests/unit/test_derivs.py index 23a5a8bc7d1..6a4fc6814b3 100644 --- a/pyomo/core/tests/unit/test_derivs.py +++ b/pyomo/core/tests/unit/test_derivs.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -322,6 +322,18 @@ def test_nested_named_expressions(self): self.assertAlmostEqual(derivs[m.y], pyo.value(symbolic[m.y]), tol + 3) self.assertAlmostEqual(derivs[m.y], approx_deriv(e, m.y), tol) + def test_linear_exprs_issue_3096(self): + m = pyo.ConcreteModel() + m.y1 = pyo.Var(initialize=10) + m.y2 = pyo.Var(initialize=100) + e = (m.y1 - 0.5) * (m.y1 - 0.5) + (m.y2 - 0.5) * (m.y2 - 0.5) + derivs = reverse_ad(e) + self.assertEqual(derivs[m.y1], 19) + self.assertEqual(derivs[m.y2], 199) + symbolic = reverse_sd(e) + self.assertExpressionsEqual(symbolic[m.y1], m.y1 - 0.5 + m.y1 - 0.5) + self.assertExpressionsEqual(symbolic[m.y2], m.y2 - 0.5 + m.y2 - 0.5) + class TestDifferentiate(unittest.TestCase): @unittest.skipUnless(sympy_available, "test requires sympy") diff --git a/pyomo/core/tests/unit/test_dict_objects.py b/pyomo/core/tests/unit/test_dict_objects.py index 7d3244f4d86..ef9f330bfff 100644 --- a/pyomo/core/tests/unit/test_dict_objects.py +++ b/pyomo/core/tests/unit/test_dict_objects.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -17,10 +17,10 @@ ObjectiveDict, ExpressionDict, ) -from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.constraint import _GeneralConstraintData -from pyomo.core.base.objective import _GeneralObjectiveData -from pyomo.core.base.expression import _GeneralExpressionData +from pyomo.core.base.var import VarData +from pyomo.core.base.constraint import ConstraintData +from pyomo.core.base.objective import ObjectiveData +from pyomo.core.base.expression import ExpressionData class _TestComponentDictBase(object): @@ -348,10 +348,10 @@ def test_active(self): class TestVarDict(_TestComponentDictBase, unittest.TestCase): - # Note: the updated _GeneralVarData class only takes an optional + # Note: the updated VarData class only takes an optional # parent argument (you no longer pass the domain in) _ctype = VarDict - _cdatatype = lambda self, arg: _GeneralVarData() + _cdatatype = lambda self, arg: VarData() def setUp(self): _TestComponentDictBase.setUp(self) @@ -360,7 +360,7 @@ def setUp(self): class TestExpressionDict(_TestComponentDictBase, unittest.TestCase): _ctype = ExpressionDict - _cdatatype = _GeneralExpressionData + _cdatatype = ExpressionData def setUp(self): _TestComponentDictBase.setUp(self) @@ -375,7 +375,7 @@ def setUp(self): class TestConstraintDict(_TestActiveComponentDictBase, unittest.TestCase): _ctype = ConstraintDict - _cdatatype = _GeneralConstraintData + _cdatatype = ConstraintData def setUp(self): _TestComponentDictBase.setUp(self) @@ -384,7 +384,7 @@ def setUp(self): class TestObjectiveDict(_TestActiveComponentDictBase, unittest.TestCase): _ctype = ObjectiveDict - _cdatatype = _GeneralObjectiveData + _cdatatype = ObjectiveData def setUp(self): _TestComponentDictBase.setUp(self) diff --git a/pyomo/core/tests/unit/test_disable_methods.py b/pyomo/core/tests/unit/test_disable_methods.py index 4d6595e5fe8..618752aee85 100644 --- a/pyomo/core/tests/unit/test_disable_methods.py +++ b/pyomo/core/tests/unit/test_disable_methods.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_enums.py b/pyomo/core/tests/unit/test_enums.py index 8f342e55188..cce908a87de 100644 --- a/pyomo/core/tests/unit/test_enums.py +++ b/pyomo/core/tests/unit/test_enums.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_expr5.txt b/pyomo/core/tests/unit/test_expr5.txt index a5fc934bd77..2bf78cb4985 100644 --- a/pyomo/core/tests/unit/test_expr5.txt +++ b/pyomo/core/tests/unit/test_expr5.txt @@ -1,11 +1,8 @@ -2 Set Declarations +1 Set Declarations A : set A Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {1, 2, 3} - c3_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 1 : {1,} 2 Param Declarations B : param B @@ -49,8 +46,8 @@ 2 : -Inf : B[2]*x[2] : 1.0 : True 3 : -Inf : B[3]*x[3] : 1.0 : True c3 : con c3 - Size=1, Index=c3_index, Active=True + Size=1, Index={1}, Active=True Key : Lower : Body : Upper : Active 1 : -Inf : y : 0.0 : True -10 Declarations: A B C x y o c1 c2 c3_index c3 +9 Declarations: A B C x y o c1 c2 c3 diff --git a/pyomo/core/tests/unit/test_expr_misc.py b/pyomo/core/tests/unit/test_expr_misc.py index 4ec53521d6b..f4fd7556117 100644 --- a/pyomo/core/tests/unit/test_expr_misc.py +++ b/pyomo/core/tests/unit/test_expr_misc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_expression.py b/pyomo/core/tests/unit/test_expression.py index 8dca0062dd0..eb16f7c6142 100644 --- a/pyomo/core/tests/unit/test_expression.py +++ b/pyomo/core/tests/unit/test_expression.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -29,7 +29,7 @@ value, sum_product, ) -from pyomo.core.base.expression import _GeneralExpressionData +from pyomo.core.base.expression import ExpressionData from pyomo.core.expr.compare import compare_expressions, assertExpressionsEqual from pyomo.common.tee import capture_output @@ -515,10 +515,10 @@ def test_implicit_definition(self): model.E = Expression(model.idx) self.assertEqual(len(model.E), 3) expr = model.E[1] - self.assertIs(type(expr), _GeneralExpressionData) + self.assertIs(type(expr), ExpressionData) model.E[1] = None self.assertIs(expr, model.E[1]) - self.assertIs(type(expr), _GeneralExpressionData) + self.assertIs(type(expr), ExpressionData) self.assertIs(expr.expr, None) model.E[1] = 5 self.assertIs(expr, model.E[1]) @@ -537,7 +537,7 @@ def test_explicit_skip_definition(self): model.E[1] = None expr = model.E[1] - self.assertIs(type(expr), _GeneralExpressionData) + self.assertIs(type(expr), ExpressionData) self.assertIs(expr.expr, None) model.E[1] = 5 self.assertIs(expr, model.E[1]) @@ -738,11 +738,11 @@ def test_pprint_oldStyle(self): expr = model.e * model.x**2 + model.E[1] output = """\ -sum(prod(e{sum(mon(1, x), 2)}, pow(x, 2)), E[1]{sum(pow(x, 2), 1)}) +sum(prod(e{sum(x, 2)}, pow(x, 2)), E[1]{sum(pow(x, 2), 1)}) e : Size=1, Index=None Key : Expression - None : sum(mon(1, x), 2) -E : Size=2, Index=E_index + None : sum(x, 2) +E : Size=2, Index={1, 2} Key : Expression 1 : sum(pow(x, 2), 1) 2 : sum(pow(x, 2), 1) @@ -761,7 +761,7 @@ def test_pprint_oldStyle(self): e : Size=1, Index=None Key : Expression None : 1.0 -E : Size=2, Index=E_index +E : Size=2, Index={1, 2} Key : Expression 1 : 2.0 2 : sum(pow(x, 2), 1) @@ -780,7 +780,7 @@ def test_pprint_oldStyle(self): e : Size=1, Index=None Key : Expression None : Undefined -E : Size=2, Index=E_index +E : Size=2, Index={1, 2} Key : Expression 1 : Undefined 2 : sum(pow(x, 2), 1) @@ -806,7 +806,7 @@ def test_pprint_newStyle(self): e : Size=1, Index=None Key : Expression None : x + 2 -E : Size=2, Index=E_index +E : Size=2, Index={1, 2} Key : Expression 1 : x**2 + 1 2 : x**2 + 1 @@ -830,7 +830,7 @@ def test_pprint_newStyle(self): e : Size=1, Index=None Key : Expression None : 1.0 -E : Size=2, Index=E_index +E : Size=2, Index={1, 2} Key : Expression 1 : 2.0 2 : x**2 + 1 @@ -849,7 +849,7 @@ def test_pprint_newStyle(self): e : Size=1, Index=None Key : Expression None : Undefined -E : Size=2, Index=E_index +E : Size=2, Index={1, 2} Key : Expression 1 : Undefined 2 : x**2 + 1 @@ -951,12 +951,7 @@ def test_isub(self): assertExpressionsEqual( self, m.e.expr, - EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, m.x)), - EXPR.MonomialTermExpression((-1, m.y)), - ] - ), + EXPR.LinearExpression([m.x, EXPR.MonomialTermExpression((-1, m.y))]), ) self.assertTrue(compare_expressions(m.e.expr, m.x - m.y)) diff --git a/pyomo/core/tests/unit/test_external.py b/pyomo/core/tests/unit/test_external.py index 96c05b6b0b8..1d4a59647c1 100644 --- a/pyomo/core/tests/unit/test_external.py +++ b/pyomo/core/tests/unit/test_external.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_indexed.py b/pyomo/core/tests/unit/test_indexed.py index 29bf22ceeb1..3480b653ea5 100644 --- a/pyomo/core/tests/unit/test_indexed.py +++ b/pyomo/core/tests/unit/test_indexed.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_indexed_slice.py b/pyomo/core/tests/unit/test_indexed_slice.py index e89c48a6061..40aaad9fec9 100644 --- a/pyomo/core/tests/unit/test_indexed_slice.py +++ b/pyomo/core/tests/unit/test_indexed_slice.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -17,7 +17,7 @@ import pyomo.common.unittest as unittest from pyomo.environ import Var, Block, ConcreteModel, RangeSet, Set, Any -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.base.indexed_component_slice import IndexedComponent_slice from pyomo.core.base.set import normalize_index @@ -64,7 +64,7 @@ def tearDown(self): self.m = None def test_simple_getitem(self): - self.assertIsInstance(self.m.b[1, 4], _BlockData) + self.assertIsInstance(self.m.b[1, 4], BlockData) def test_simple_getslice(self): _slicer = self.m.b[:, 4] diff --git a/pyomo/core/tests/unit/test_initializer.py b/pyomo/core/tests/unit/test_initializer.py index 5767406bdf7..c0f9ddc9565 100644 --- a/pyomo/core/tests/unit/test_initializer.py +++ b/pyomo/core/tests/unit/test_initializer.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -11,6 +11,8 @@ import functools import pickle +import platform +import sys import types import pyomo.common.unittest as unittest @@ -37,6 +39,9 @@ from pyomo.environ import ConcreteModel, Var +is_pypy = platform.python_implementation().lower().startswith("pypy") + + def _init_scalar(m): return 1 @@ -561,12 +566,20 @@ def test_no_argspec(self): self.assertFalse(a.contains_indices()) self.assertEqual(a('111', 2), 7) - # Special case: getfullargspec fails on int, so we assume it is - # always an IndexedCallInitializer + # Special case: getfullargspec fails for int under CPython and + # PyPy<7.3.14, so we assume it is an IndexedCallInitializer. basetwo = functools.partial(int, '101', base=2) a = Initializer(basetwo) - self.assertIs(type(a), IndexedCallInitializer) - self.assertFalse(a.constant()) + if is_pypy and sys.pypy_version_info[:3] >= (7, 3, 14): + # PyPy behavior diverged from CPython in 7.3.14. Arguably + # this is "more correct", so we will allow the difference to + # persist through Pyomo's Initializer handling (and not + # special case it there) + self.assertIs(type(a), ScalarCallInitializer) + self.assertTrue(a.constant()) + else: + self.assertIs(type(a), IndexedCallInitializer) + self.assertFalse(a.constant()) self.assertFalse(a.verified) self.assertFalse(a.contains_indices()) # but this is not callable, as int won't accept the 'model' diff --git a/pyomo/core/tests/unit/test_kernel_register_numpy_types.py b/pyomo/core/tests/unit/test_kernel_register_numpy_types.py index 0a9e3ab08f9..91a0f571881 100644 --- a/pyomo/core/tests/unit/test_kernel_register_numpy_types.py +++ b/pyomo/core/tests/unit/test_kernel_register_numpy_types.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -10,7 +10,7 @@ # ___________________________________________________________________________ import pyomo.common.unittest as unittest -from pyomo.common.dependencies import numpy_available +from pyomo.common.dependencies import numpy, numpy_available from pyomo.common.log import LoggingIntercept # Boolean @@ -38,12 +38,22 @@ numpy_float_names.append('float16') numpy_float_names.append('float32') numpy_float_names.append('float64') + if hasattr(numpy, 'float96'): + numpy_float_names.append('float96') + if hasattr(numpy, 'float128'): + # On some numpy builds, the name of float128 is longdouble + numpy_float_names.append(numpy.float128.__name__) # Complex numpy_complex_names = [] if numpy_available: numpy_complex_names.append('complex_') numpy_complex_names.append('complex64') numpy_complex_names.append('complex128') + if hasattr(numpy, 'complex192'): + numpy_complex_names.append('complex192') + if hasattr(numpy, 'complex256'): + # On some numpy builds, the name of complex256 is clongdouble + numpy_complex_names.append(numpy.complex256.__name__) class TestNumpyRegistration(unittest.TestCase): diff --git a/pyomo/core/tests/unit/test_labelers.py b/pyomo/core/tests/unit/test_labelers.py index 15c56b5390d..579abfd8b52 100644 --- a/pyomo/core/tests/unit/test_labelers.py +++ b/pyomo/core/tests/unit/test_labelers.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_list_objects.py b/pyomo/core/tests/unit/test_list_objects.py index 442fa97b6d1..671a8429e06 100644 --- a/pyomo/core/tests/unit/test_list_objects.py +++ b/pyomo/core/tests/unit/test_list_objects.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -17,10 +17,10 @@ XObjectiveList, XExpressionList, ) -from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.constraint import _GeneralConstraintData -from pyomo.core.base.objective import _GeneralObjectiveData -from pyomo.core.base.expression import _GeneralExpressionData +from pyomo.core.base.var import VarData +from pyomo.core.base.constraint import ConstraintData +from pyomo.core.base.objective import ObjectiveData +from pyomo.core.base.expression import ExpressionData class _TestComponentListBase(object): @@ -365,10 +365,10 @@ def test_active(self): class TestVarList(_TestComponentListBase, unittest.TestCase): - # Note: the updated _GeneralVarData class only takes an optional + # Note: the updated VarData class only takes an optional # parent argument (you no longer pass the domain in) _ctype = XVarList - _cdatatype = lambda self, arg: _GeneralVarData() + _cdatatype = lambda self, arg: VarData() def setUp(self): _TestComponentListBase.setUp(self) @@ -377,7 +377,7 @@ def setUp(self): class TestExpressionList(_TestComponentListBase, unittest.TestCase): _ctype = XExpressionList - _cdatatype = _GeneralExpressionData + _cdatatype = ExpressionData def setUp(self): _TestComponentListBase.setUp(self) @@ -392,7 +392,7 @@ def setUp(self): class TestConstraintList(_TestActiveComponentListBase, unittest.TestCase): _ctype = XConstraintList - _cdatatype = _GeneralConstraintData + _cdatatype = ConstraintData def setUp(self): _TestComponentListBase.setUp(self) @@ -401,7 +401,7 @@ def setUp(self): class TestObjectiveList(_TestActiveComponentListBase, unittest.TestCase): _ctype = XObjectiveList - _cdatatype = _GeneralObjectiveData + _cdatatype = ObjectiveData def setUp(self): _TestComponentListBase.setUp(self) diff --git a/pyomo/core/tests/unit/test_logical_constraint.py b/pyomo/core/tests/unit/test_logical_constraint.py index ed8120da935..b1f37996018 100644 --- a/pyomo/core/tests/unit/test_logical_constraint.py +++ b/pyomo/core/tests/unit/test_logical_constraint.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.common.unittest as unittest from pyomo.core.expr.sympy_tools import sympy_available diff --git a/pyomo/core/tests/unit/test_logical_expr_expanded.py b/pyomo/core/tests/unit/test_logical_expr_expanded.py index f5b86d59cbd..6468a21e336 100644 --- a/pyomo/core/tests/unit/test_logical_expr_expanded.py +++ b/pyomo/core/tests/unit/test_logical_expr_expanded.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -13,9 +13,9 @@ """ Testing for the logical expression system """ -from __future__ import division + import operator -from itertools import product +from itertools import permutations, product import pyomo.common.unittest as unittest @@ -23,6 +23,8 @@ from pyomo.core.expr.sympy_tools import sympy_available from pyomo.core.expr.visitor import identify_variables from pyomo.environ import ( + all_different, + count_if, land, atleast, atmost, @@ -39,6 +41,8 @@ BooleanVar, lnot, xor, + Var, + Integers, ) @@ -234,12 +238,50 @@ def test_nary_atleast(self): ) self.assertEqual(value(atleast(ntrue, m.Y)), correct_value) + def test_nary_all_diff(self): + m = ConcreteModel() + m.x = Var(range(4), domain=Integers, bounds=(0, 3)) + for vals in permutations(range(4)): + self.assertTrue(value(all_different(*vals))) + for i, v in enumerate(vals): + m.x[i] = v + self.assertTrue(value(all_different(m.x))) + self.assertFalse(value(all_different(1, 1, 2, 3))) + m.x[0] = 1 + m.x[1] = 1 + m.x[2] = 2 + m.x[3] = 3 + self.assertFalse(value(all_different(m.x))) + + def test_count_if(self): + nargs = 3 + m = ConcreteModel() + m.s = RangeSet(nargs) + m.Y = BooleanVar(m.s) + m.x = Var(domain=Integers, bounds=(0, 3)) + for truth_combination in _generate_possible_truth_inputs(nargs): + for ntrue in range(nargs + 1): + m.Y.set_values(dict(enumerate(truth_combination, 1))) + correct_value = sum(truth_combination) + self.assertEqual(value(count_if(*(m.Y[i] for i in m.s))), correct_value) + self.assertEqual(value(count_if(m.Y)), correct_value) + m.x = 2 + self.assertEqual( + value(count_if([m.Y[i] for i in m.s] + [m.x == 3])), correct_value + ) + m.x = 3 + self.assertEqual( + value(count_if([m.Y[i] for i in m.s] + [m.x == 3])), correct_value + 1 + ) + def test_to_string(self): m = ConcreteModel() m.Y1 = BooleanVar() m.Y2 = BooleanVar() m.Y3 = BooleanVar() m.Y4 = BooleanVar() + m.int1 = Var(domain=Integers) + m.int2 = Var(domain=Integers) self.assertEqual(str(land(m.Y1, m.Y2, m.Y3)), "Y1 ∧ Y2 ∧ Y3") self.assertEqual(str(lor(m.Y1, m.Y2, m.Y3)), "Y1 ∨ Y2 ∨ Y3") @@ -249,6 +291,10 @@ def test_to_string(self): self.assertEqual(str(atleast(1, m.Y1, m.Y2)), "atleast(1: [Y1, Y2])") self.assertEqual(str(atmost(1, m.Y1, m.Y2)), "atmost(1: [Y1, Y2])") self.assertEqual(str(exactly(1, m.Y1, m.Y2)), "exactly(1: [Y1, Y2])") + self.assertEqual( + str(all_different(m.int1, m.int2)), "all_different(int1, int2)" + ) + self.assertEqual(str(count_if(m.Y1, m.Y2)), "count_if(Y1, Y2)") # Precedence checks self.assertEqual(str(m.Y1.implies(m.Y2).lor(m.Y3)), "(Y1 --> Y2) ∨ Y3") @@ -266,11 +312,16 @@ def test_node_types(self): m.Y1 = BooleanVar() m.Y2 = BooleanVar() m.Y3 = BooleanVar() + m.int1 = Var(domain=Integers) + m.int2 = Var(domain=Integers) + m.int3 = Var(domain=Integers) self.assertFalse(m.Y1.is_expression_type()) self.assertTrue(lnot(m.Y1).is_expression_type()) self.assertTrue(equivalent(m.Y1, m.Y2).is_expression_type()) self.assertTrue(atmost(1, [m.Y1, m.Y2, m.Y3]).is_expression_type()) + self.assertTrue(all_different(m.int1, m.int2, m.int3).is_expression_type()) + self.assertTrue(count_if(m.Y1, m.Y2, m.Y3).is_expression_type()) def test_numeric_invalid(self): m = ConcreteModel() diff --git a/pyomo/core/tests/unit/test_logical_to_linear.py b/pyomo/core/tests/unit/test_logical_to_linear.py index 22133f22ba2..e777259f8ce 100644 --- a/pyomo/core/tests/unit/test_logical_to_linear.py +++ b/pyomo/core/tests/unit/test_logical_to_linear.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_matrix_constraint.py b/pyomo/core/tests/unit/test_matrix_constraint.py index d9b51de7bf6..993e2a18eb3 100644 --- a/pyomo/core/tests/unit/test_matrix_constraint.py +++ b/pyomo/core/tests/unit/test_matrix_constraint.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_misc.py b/pyomo/core/tests/unit/test_misc.py index 261c94d96bd..440c8807358 100644 --- a/pyomo/core/tests/unit/test_misc.py +++ b/pyomo/core/tests/unit/test_misc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_model.py b/pyomo/core/tests/unit/test_model.py index 95ad17e97f4..9016f9937c0 100644 --- a/pyomo/core/tests/unit/test_model.py +++ b/pyomo/core/tests/unit/test_model.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_mutable.py b/pyomo/core/tests/unit/test_mutable.py index 933ef1fe3dc..d10622d84c0 100644 --- a/pyomo/core/tests/unit/test_mutable.py +++ b/pyomo/core/tests/unit/test_mutable.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_numeric_expr.py b/pyomo/core/tests/unit/test_numeric_expr.py index 13af5adc9bb..efb01e6d6ce 100644 --- a/pyomo/core/tests/unit/test_numeric_expr.py +++ b/pyomo/core/tests/unit/test_numeric_expr.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -104,10 +104,6 @@ MinExpression, _balanced_parens, ) -from pyomo.core.expr.compare import ( - assertExpressionsEqual, - assertExpressionsStructurallyEqual, -) from pyomo.core.expr.relational_expr import RelationalExpression, EqualityExpression from pyomo.core.expr.relational_expr import RelationalExpression, EqualityExpression from pyomo.common.errors import PyomoException @@ -116,7 +112,7 @@ from pyomo.core.base.label import NumericLabeler from pyomo.core.expr.template_expr import IndexTemplate from pyomo.core.expr import expr_common -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import VarData from pyomo.repn import generate_standard_repn from pyomo.core.expr.numvalue import NumericValue @@ -298,7 +294,7 @@ def value_check(self, exp, val): class TestExpression_EvaluateVarData(TestExpression_EvaluateNumericValue): def create(self, val, domain): - tmp = _GeneralVarData() + tmp = VarData() tmp.domain = domain tmp.value = val return tmp @@ -642,13 +638,7 @@ def test_simpleSum(self): m.b = Var() e = m.a + m.b # - assertExpressionsEqual( - self, - e, - LinearExpression( - [MonomialTermExpression((1, m.a)), MonomialTermExpression((1, m.b))] - ), - ) + self.assertExpressionsEqual(e, LinearExpression([m.a, m.b])) self.assertRaises(KeyError, e.arg, 3) @@ -658,16 +648,8 @@ def test_simpleSum_API(self): m.b = Var() e = m.a + m.b e += 2 * m.a - assertExpressionsEqual( - self, - e, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - MonomialTermExpression((2, m.a)), - ] - ), + self.assertExpressionsEqual( + e, LinearExpression([m.a, m.b, MonomialTermExpression((2, m.a))]) ) def test_constSum(self): @@ -675,13 +657,9 @@ def test_constSum(self): m = AbstractModel() m.a = Var() # - assertExpressionsEqual( - self, m.a + 5, LinearExpression([MonomialTermExpression((1, m.a)), 5]) - ) + self.assertExpressionsEqual(m.a + 5, LinearExpression([m.a, 5])) - assertExpressionsEqual( - self, 5 + m.a, LinearExpression([5, MonomialTermExpression((1, m.a))]) - ) + self.assertExpressionsEqual(5 + m.a, LinearExpression([5, m.a])) def test_nestedSum(self): # @@ -702,13 +680,7 @@ def test_nestedSum(self): # a b e1 = m.a + m.b e = e1 + 5 - assertExpressionsEqual( - self, - e, - LinearExpression( - [MonomialTermExpression((1, m.a)), MonomialTermExpression((1, m.b)), 5] - ), - ) + self.assertExpressionsEqual(e, LinearExpression([m.a, m.b, 5])) # + # / \ @@ -717,13 +689,7 @@ def test_nestedSum(self): # a b e1 = m.a + m.b e = 5 + e1 - assertExpressionsEqual( - self, - e, - LinearExpression( - [MonomialTermExpression((1, m.a)), MonomialTermExpression((1, m.b)), 5] - ), - ) + self.assertExpressionsEqual(e, LinearExpression([m.a, m.b, 5])) # + # / \ @@ -732,17 +698,7 @@ def test_nestedSum(self): # a b e1 = m.a + m.b e = e1 + m.c - assertExpressionsEqual( - self, - e, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.c)), - ] - ), - ) + self.assertExpressionsEqual(e, LinearExpression([m.a, m.b, m.c])) # + # / \ @@ -751,17 +707,7 @@ def test_nestedSum(self): # a b e1 = m.a + m.b e = m.c + e1 - assertExpressionsEqual( - self, - e, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.c)), - ] - ), - ) + self.assertExpressionsEqual(e, LinearExpression([m.a, m.b, m.c])) # + # / \ @@ -772,18 +718,7 @@ def test_nestedSum(self): e2 = m.c + m.d e = e1 + e2 # - assertExpressionsEqual( - self, - e, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.c)), - MonomialTermExpression((1, m.d)), - ] - ), - ) + self.assertExpressionsEqual(e, LinearExpression([m.a, m.b, m.c, m.d])) def test_nestedSum2(self): # @@ -807,25 +742,9 @@ def test_nestedSum2(self): e1 = m.a + m.b e = 2 * e1 + m.c - assertExpressionsEqual( - self, + self.assertExpressionsEqual( e, - SumExpression( - [ - ProductExpression( - ( - 2, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - ] - ), - ) - ), - m.c, - ] - ), + SumExpression([ProductExpression((2, LinearExpression([m.a, m.b]))), m.c]), ) # * @@ -840,27 +759,13 @@ def test_nestedSum2(self): e1 = m.a + m.b e = 3 * (2 * e1 + m.c) - assertExpressionsEqual( - self, + self.assertExpressionsEqual( e, ProductExpression( ( 3, SumExpression( - [ - ProductExpression( - ( - 2, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - ] - ), - ) - ), - m.c, - ] + [ProductExpression((2, LinearExpression([m.a, m.b]))), m.c] ), ) ), @@ -903,12 +808,8 @@ def test_sumOf_nestedTrivialProduct(self): e1 = m.a * 5 e = e1 + m.b # - assertExpressionsEqual( - self, - e, - LinearExpression( - [MonomialTermExpression((5, m.a)), MonomialTermExpression((1, m.b))] - ), + self.assertExpressionsEqual( + e, LinearExpression([MonomialTermExpression((5, m.a)), m.b]) ) # + @@ -918,12 +819,8 @@ def test_sumOf_nestedTrivialProduct(self): # a 5 e = m.b + e1 # - assertExpressionsEqual( - self, - e, - LinearExpression( - [MonomialTermExpression((1, m.b)), MonomialTermExpression((5, m.a))] - ), + self.assertExpressionsEqual( + e, LinearExpression([m.b, MonomialTermExpression((5, m.a))]) ) # + @@ -934,16 +831,8 @@ def test_sumOf_nestedTrivialProduct(self): e2 = m.b + m.c e = e1 + e2 # - assertExpressionsEqual( - self, - e, - LinearExpression( - [ - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.c)), - MonomialTermExpression((5, m.a)), - ] - ), + self.assertExpressionsEqual( + e, LinearExpression([m.b, m.c, MonomialTermExpression((5, m.a))]) ) # + @@ -954,16 +843,8 @@ def test_sumOf_nestedTrivialProduct(self): e2 = m.b + m.c e = e2 + e1 # - assertExpressionsEqual( - self, - e, - LinearExpression( - [ - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.c)), - MonomialTermExpression((5, m.a)), - ] - ), + self.assertExpressionsEqual( + e, LinearExpression([m.b, m.c, MonomialTermExpression((5, m.a))]) ) def test_simpleDiff(self): @@ -978,12 +859,8 @@ def test_simpleDiff(self): # / \ # a b e = m.a - m.b - assertExpressionsEqual( - self, - e, - LinearExpression( - [MonomialTermExpression((1, m.a)), MonomialTermExpression((-1, m.b))] - ), + self.assertExpressionsEqual( + e, LinearExpression([m.a, MonomialTermExpression((-1, m.b))]) ) def test_constDiff(self): @@ -996,15 +873,13 @@ def test_constDiff(self): # - # / \ # a 5 - assertExpressionsEqual( - self, m.a - 5, LinearExpression([MonomialTermExpression((1, m.a)), -5]) - ) + self.assertExpressionsEqual(m.a - 5, LinearExpression([m.a, -5])) # - # / \ # 5 a - assertExpressionsEqual( - self, 5 - m.a, LinearExpression([5, MonomialTermExpression((-1, m.a))]) + self.assertExpressionsEqual( + 5 - m.a, LinearExpression([5, MonomialTermExpression((-1, m.a))]) ) def test_paramDiff(self): @@ -1019,20 +894,16 @@ def test_paramDiff(self): # / \ # a p e = m.a - m.p - assertExpressionsEqual( - self, - e, - LinearExpression( - [MonomialTermExpression((1, m.a)), NPV_NegationExpression((m.p,))] - ), + self.assertExpressionsEqual( + e, LinearExpression([m.a, NPV_NegationExpression((m.p,))]) ) # - # / \ # m.p a e = m.p - m.a - assertExpressionsEqual( - self, e, LinearExpression([m.p, MonomialTermExpression((-1, m.a))]) + self.assertExpressionsEqual( + e, LinearExpression([m.p, MonomialTermExpression((-1, m.a))]) ) def test_constparamDiff(self): @@ -1076,8 +947,8 @@ def test_termDiff(self): e = 5 - 2 * m.a - assertExpressionsEqual( - self, e, LinearExpression([5, MonomialTermExpression((-2, m.a))]) + self.assertExpressionsEqual( + e, LinearExpression([5, MonomialTermExpression((-2, m.a))]) ) def test_nestedDiff(self): @@ -1097,16 +968,8 @@ def test_nestedDiff(self): # a b e1 = m.a - m.b e = e1 - 5 - assertExpressionsEqual( - self, - e, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((-1, m.b)), - -5, - ] - ), + self.assertExpressionsEqual( + e, LinearExpression([m.a, MonomialTermExpression((-1, m.b)), -5]) ) # - @@ -1116,21 +979,13 @@ def test_nestedDiff(self): # a b e1 = m.a - m.b e = 5 - e1 - assertExpressionsEqual( - self, + self.assertExpressionsEqual( e, SumExpression( [ 5, NegationExpression( - ( - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((-1, m.b)), - ] - ), - ) + (LinearExpression([m.a, MonomialTermExpression((-1, m.b))]),) ), ] ), @@ -1143,12 +998,11 @@ def test_nestedDiff(self): # a b e1 = m.a - m.b e = e1 - m.c - assertExpressionsEqual( - self, + self.assertExpressionsEqual( e, LinearExpression( [ - MonomialTermExpression((1, m.a)), + m.a, MonomialTermExpression((-1, m.b)), MonomialTermExpression((-1, m.c)), ] @@ -1162,21 +1016,13 @@ def test_nestedDiff(self): # a b e1 = m.a - m.b e = m.c - e1 - assertExpressionsEqual( - self, + self.assertExpressionsEqual( e, SumExpression( [ m.c, NegationExpression( - ( - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((-1, m.b)), - ] - ), - ) + (LinearExpression([m.a, MonomialTermExpression((-1, m.b))]),) ), ] ), @@ -1190,26 +1036,13 @@ def test_nestedDiff(self): e1 = m.a - m.b e2 = m.c - m.d e = e1 - e2 - assertExpressionsEqual( - self, + self.assertExpressionsEqual( e, SumExpression( [ - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((-1, m.b)), - ] - ), + LinearExpression([m.a, MonomialTermExpression((-1, m.b))]), NegationExpression( - ( - LinearExpression( - [ - MonomialTermExpression((1, m.c)), - MonomialTermExpression((-1, m.d)), - ] - ), - ) + (LinearExpression([m.c, MonomialTermExpression((-1, m.d))]),) ), ] ), @@ -1233,8 +1066,8 @@ def test_negation_mutableparam(self): m = AbstractModel() m.p = Param(mutable=True, initialize=1.0) e = -m.p - assertExpressionsEqual(self, e, NPV_NegationExpression((m.p,))) - assertExpressionsEqual(self, -e, m.p) + self.assertExpressionsEqual(e, NPV_NegationExpression((m.p,))) + self.assertExpressionsEqual(-e, m.p) def test_negation_terms(self): # @@ -1244,15 +1077,15 @@ def test_negation_terms(self): m.v = Var() m.p = Param(mutable=True, initialize=1.0) e = -m.p * m.v - assertExpressionsEqual( - self, e, MonomialTermExpression((NPV_NegationExpression((m.p,)), m.v)) + self.assertExpressionsEqual( + e, MonomialTermExpression((NPV_NegationExpression((m.p,)), m.v)) ) - assertExpressionsEqual(self, -e, MonomialTermExpression((m.p, m.v))) + self.assertExpressionsEqual(-e, MonomialTermExpression((m.p, m.v))) # e = -5 * m.v - assertExpressionsEqual(self, e, MonomialTermExpression((-5, m.v))) - assertExpressionsEqual(self, -e, MonomialTermExpression((5, m.v))) + self.assertExpressionsEqual(e, MonomialTermExpression((-5, m.v))) + self.assertExpressionsEqual(-e, MonomialTermExpression((5, m.v))) def test_trivialDiff(self): # @@ -1389,8 +1222,7 @@ def test_sumOf_nestedTrivialProduct2(self): # a 5 e1 = m.a * m.p e = e1 - m.b - assertExpressionsEqual( - self, + self.assertExpressionsEqual( e, LinearExpression( [MonomialTermExpression((m.p, m.a)), MonomialTermExpression((-1, m.b))] @@ -1404,14 +1236,10 @@ def test_sumOf_nestedTrivialProduct2(self): # a 5 e1 = m.a * m.p e = m.b - e1 - assertExpressionsEqual( - self, + self.assertExpressionsEqual( e, LinearExpression( - [ - MonomialTermExpression((1, m.b)), - MonomialTermExpression((NPV_NegationExpression((m.p,)), m.a)), - ] + [m.b, MonomialTermExpression((NPV_NegationExpression((m.p,)), m.a))] ), ) @@ -1423,21 +1251,13 @@ def test_sumOf_nestedTrivialProduct2(self): e1 = m.a * m.p e2 = m.b - m.c e = e1 - e2 - assertExpressionsEqual( - self, + self.assertExpressionsEqual( e, SumExpression( [ MonomialTermExpression((m.p, m.a)), NegationExpression( - ( - LinearExpression( - [ - MonomialTermExpression((1, m.b)), - MonomialTermExpression((-1, m.c)), - ] - ), - ) + (LinearExpression([m.b, MonomialTermExpression((-1, m.c))]),) ), ] ), @@ -1451,13 +1271,11 @@ def test_sumOf_nestedTrivialProduct2(self): e1 = m.a * m.p e2 = m.b - m.c e = e2 - e1 - self.maxDiff = None - assertExpressionsEqual( - self, + self.assertExpressionsEqual( e, LinearExpression( [ - MonomialTermExpression((1, m.b)), + m.b, MonomialTermExpression((-1, m.c)), MonomialTermExpression((NPV_NegationExpression((m.p,)), m.a)), ] @@ -1624,32 +1442,16 @@ def test_nestedProduct2(self): e3 = e1 + m.d e = e2 * e3 - assertExpressionsEqual( - self, + self.assertExpressionsEqual( e, ProductExpression( - ( - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.c)), - ] - ), - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.d)), - ] - ), - ) + (LinearExpression([m.a, m.b, m.c]), LinearExpression([m.a, m.b, m.d])) ), ) # Verify shared args... - self.assertIsNot(e1._args_, e2._args_) - self.assertIs(e1._args_, e3._args_) - self.assertIs(e1._args_, e.arg(1)._args_) + self.assertIs(e1._args_, e2._args_) + self.assertIsNot(e1._args_, e3._args_) + self.assertIs(e1._args_, e.arg(0)._args_) self.assertIs(e.arg(0).arg(0), e.arg(1).arg(0)) self.assertIs(e.arg(0).arg(1), e.arg(1).arg(1)) @@ -1668,11 +1470,8 @@ def test_nestedProduct2(self): e3 = e1 * m.d e = e2 * e3 # - inner = LinearExpression( - [MonomialTermExpression((1, m.a)), MonomialTermExpression((1, m.b))] - ) - assertExpressionsEqual( - self, + inner = LinearExpression([m.a, m.b]) + self.assertExpressionsEqual( e, ProductExpression( (ProductExpression((m.c, inner)), ProductExpression((inner, m.d))) @@ -1711,8 +1510,8 @@ def test_nestedProduct3(self): # a b e1 = m.a * m.b e = e1 * 5 - assertExpressionsEqual( - self, e, MonomialTermExpression((NPV_ProductExpression((m.a, 5)), m.b)) + self.assertExpressionsEqual( + e, MonomialTermExpression((NPV_ProductExpression((m.a, 5)), m.b)) ) # * @@ -1801,37 +1600,37 @@ def test_trivialProduct(self): m.q = Param(initialize=1) e = m.a * 0 - assertExpressionsEqual(self, e, MonomialTermExpression((0, m.a))) + self.assertExpressionsEqual(e, MonomialTermExpression((0, m.a))) e = 0 * m.a - assertExpressionsEqual(self, e, MonomialTermExpression((0, m.a))) + self.assertExpressionsEqual(e, MonomialTermExpression((0, m.a))) e = m.a * m.p - assertExpressionsEqual(self, e, MonomialTermExpression((0, m.a))) + self.assertExpressionsEqual(e, MonomialTermExpression((0, m.a))) e = m.p * m.a - assertExpressionsEqual(self, e, MonomialTermExpression((0, m.a))) + self.assertExpressionsEqual(e, MonomialTermExpression((0, m.a))) # # Check that multiplying by one gives the original expression # e = m.a * 1 - assertExpressionsEqual(self, e, m.a) + self.assertExpressionsEqual(e, m.a) e = 1 * m.a - assertExpressionsEqual(self, e, m.a) + self.assertExpressionsEqual(e, m.a) e = m.a * m.q - assertExpressionsEqual(self, e, m.a) + self.assertExpressionsEqual(e, m.a) e = m.q * m.a - assertExpressionsEqual(self, e, m.a) + self.assertExpressionsEqual(e, m.a) # # Check that numeric constants are simply muliplied out # e = NumericConstant(3) * NumericConstant(2) - assertExpressionsEqual(self, e, 6) + self.assertExpressionsEqual(e, 6) self.assertIs(type(e), int) self.assertEqual(e, 6) @@ -1996,19 +1795,19 @@ def test_trivialDivision(self): # Check that dividing zero by anything non-zero gives zero # e = 0 / m.a - assertExpressionsEqual(self, e, DivisionExpression((0, m.a))) + self.assertExpressionsEqual(e, DivisionExpression((0, m.a))) # # Check that dividing by one 1 gives the original expression # e = m.a / 1 - assertExpressionsEqual(self, e, m.a) + self.assertExpressionsEqual(e, m.a) # # Check the structure dividing 1 by an expression # e = 1 / m.a - assertExpressionsEqual(self, e, DivisionExpression((1, m.a))) + self.assertExpressionsEqual(e, DivisionExpression((1, m.a))) # # Check the structure dividing 1 by an expression @@ -2065,10 +1864,10 @@ def test_sum(self): model.p = Param(mutable=True) expr = 5 + model.a + model.a - self.assertEqual("sum(5, mon(1, a), mon(1, a))", str(expr)) + self.assertEqual("sum(5, a, a)", str(expr)) expr += 5 - self.assertEqual("sum(5, mon(1, a), mon(1, a), 5)", str(expr)) + self.assertEqual("sum(5, a, a, 5)", str(expr)) expr = 2 + model.p self.assertEqual("sum(2, p)", str(expr)) @@ -2084,24 +1883,18 @@ def test_linearsum(self): expr = quicksum(i * model.a[i] for i in A) self.assertEqual( - "sum(mon(0, a[0]), mon(1, a[1]), mon(2, a[2]), mon(3, a[3]), " - "mon(4, a[4]))", + "sum(mon(0, a[0]), a[1], mon(2, a[2]), mon(3, a[3]), " "mon(4, a[4]))", str(expr), ) expr = quicksum((i - 2) * model.a[i] for i in A) self.assertEqual( - "sum(mon(-2, a[0]), mon(-1, a[1]), mon(0, a[2]), mon(1, a[3]), " - "mon(2, a[4]))", + "sum(mon(-2, a[0]), mon(-1, a[1]), mon(0, a[2]), a[3], " "mon(2, a[4]))", str(expr), ) expr = quicksum(model.a[i] for i in A) - self.assertEqual( - "sum(mon(1, a[0]), mon(1, a[1]), mon(1, a[2]), mon(1, a[3]), " - "mon(1, a[4]))", - str(expr), - ) + self.assertEqual("sum(a[0], a[1], a[2], a[3], a[4])", str(expr)) model.p[1].value = 0 model.p[3].value = 3 @@ -2169,10 +1962,10 @@ def test_inequality(self): self.assertEqual("5 <= a < 10", str(expr)) expr = 5 <= model.a + 5 - self.assertEqual("5 <= sum(mon(1, a), 5)", str(expr)) + self.assertEqual("5 <= sum(a, 5)", str(expr)) expr = expr < 10 - self.assertEqual("5 <= sum(mon(1, a), 5) < 10", str(expr)) + self.assertEqual("5 <= sum(a, 5) < 10", str(expr)) def test_equality(self): # @@ -2197,10 +1990,10 @@ def test_equality(self): self.assertEqual("a == 10", str(expr)) expr = 5 == model.a + 5 - self.assertEqual("sum(mon(1, a), 5) == 5", str(expr)) + self.assertEqual("sum(a, 5) == 5", str(expr)) expr = model.a + 5 == 5 - self.assertEqual("sum(mon(1, a), 5) == 5", str(expr)) + self.assertEqual("sum(a, 5) == 5", str(expr)) def test_getitem(self): m = ConcreteModel() @@ -2237,7 +2030,7 @@ def test_small_expression(self): expr = abs(expr) self.assertEqual( "abs(neg(pow(2, div(2, prod(2, sum(1, neg(pow(div(prod(sum(" - "mon(1, a), 1, -1), a), a), b)), 1))))))", + "a, 1, -1), a), a), b)), 1))))))", str(expr), ) @@ -3782,24 +3575,16 @@ def tearDown(self): def test_summation1(self): e = sum_product(self.m.a) - assertExpressionsEqual( - self, + self.assertExpressionsEqual( e, LinearExpression( - [ - MonomialTermExpression((1, self.m.a[1])), - MonomialTermExpression((1, self.m.a[2])), - MonomialTermExpression((1, self.m.a[3])), - MonomialTermExpression((1, self.m.a[4])), - MonomialTermExpression((1, self.m.a[5])), - ] + [self.m.a[1], self.m.a[2], self.m.a[3], self.m.a[4], self.m.a[5]] ), ) def test_summation2(self): e = sum_product(self.m.p, self.m.a) - assertExpressionsEqual( - self, + self.assertExpressionsEqual( e, LinearExpression( [ @@ -3814,8 +3599,7 @@ def test_summation2(self): def test_summation3(self): e = sum_product(self.m.q, self.m.a) - assertExpressionsEqual( - self, + self.assertExpressionsEqual( e, LinearExpression( [ @@ -3830,8 +3614,7 @@ def test_summation3(self): def test_summation4(self): e = sum_product(self.m.a, self.m.b) - assertExpressionsEqual( - self, + self.assertExpressionsEqual( e, SumExpression( [ @@ -3846,8 +3629,7 @@ def test_summation4(self): def test_summation5(self): e = sum_product(self.m.b, denom=self.m.a) - assertExpressionsEqual( - self, + self.assertExpressionsEqual( e, SumExpression( [ @@ -3862,8 +3644,7 @@ def test_summation5(self): def test_summation6(self): e = sum_product(self.m.a, denom=self.m.p) - assertExpressionsEqual( - self, + self.assertExpressionsEqual( e, LinearExpression( [ @@ -3888,8 +3669,7 @@ def test_summation6(self): def test_summation7(self): e = sum_product(self.m.p, self.m.q, index=self.m.I) - assertExpressionsEqual( - self, + self.assertExpressionsEqual( e, NPV_SumExpression( [ @@ -3906,21 +3686,20 @@ def test_summation_compression(self): e1 = sum_product(self.m.a) e2 = sum_product(self.m.b) e = e1 + e2 - assertExpressionsEqual( - self, + self.assertExpressionsEqual( e, LinearExpression( [ - MonomialTermExpression((1, self.m.a[1])), - MonomialTermExpression((1, self.m.a[2])), - MonomialTermExpression((1, self.m.a[3])), - MonomialTermExpression((1, self.m.a[4])), - MonomialTermExpression((1, self.m.a[5])), - MonomialTermExpression((1, self.m.b[1])), - MonomialTermExpression((1, self.m.b[2])), - MonomialTermExpression((1, self.m.b[3])), - MonomialTermExpression((1, self.m.b[4])), - MonomialTermExpression((1, self.m.b[5])), + self.m.a[1], + self.m.a[2], + self.m.a[3], + self.m.a[4], + self.m.a[5], + self.m.b[1], + self.m.b[2], + self.m.b[3], + self.m.b[4], + self.m.b[5], ] ), ) @@ -3948,42 +3727,27 @@ def test_deprecation(self): r"DEPRECATED: The quicksum\(linear=...\) argument is deprecated " r"and ignored.", ) - assertExpressionsEqual( - self, + self.assertExpressionsEqual( e, LinearExpression( - [ - MonomialTermExpression((1, self.m.a[1])), - MonomialTermExpression((1, self.m.a[2])), - MonomialTermExpression((1, self.m.a[3])), - MonomialTermExpression((1, self.m.a[4])), - MonomialTermExpression((1, self.m.a[5])), - ] + [self.m.a[1], self.m.a[2], self.m.a[3], self.m.a[4], self.m.a[5]] ), ) def test_summation1(self): e = quicksum((self.m.a[i] for i in self.m.a)) self.assertEqual(e(), 25) - assertExpressionsEqual( - self, + self.assertExpressionsEqual( e, LinearExpression( - [ - MonomialTermExpression((1, self.m.a[1])), - MonomialTermExpression((1, self.m.a[2])), - MonomialTermExpression((1, self.m.a[3])), - MonomialTermExpression((1, self.m.a[4])), - MonomialTermExpression((1, self.m.a[5])), - ] + [self.m.a[1], self.m.a[2], self.m.a[3], self.m.a[4], self.m.a[5]] ), ) def test_summation2(self): e = quicksum(self.m.p[i] * self.m.a[i] for i in self.m.a) self.assertEqual(e(), 25) - assertExpressionsEqual( - self, + self.assertExpressionsEqual( e, LinearExpression( [ @@ -3999,8 +3763,7 @@ def test_summation2(self): def test_summation3(self): e = quicksum(self.m.q[i] * self.m.a[i] for i in self.m.a) self.assertEqual(e(), 75) - assertExpressionsEqual( - self, + self.assertExpressionsEqual( e, LinearExpression( [ @@ -4016,8 +3779,7 @@ def test_summation3(self): def test_summation4(self): e = quicksum(self.m.a[i] * self.m.b[i] for i in self.m.a) self.assertEqual(e(), 250) - assertExpressionsEqual( - self, + self.assertExpressionsEqual( e, SumExpression( [ @@ -4033,8 +3795,7 @@ def test_summation4(self): def test_summation5(self): e = quicksum(self.m.b[i] / self.m.a[i] for i in self.m.a) self.assertEqual(e(), 10) - assertExpressionsEqual( - self, + self.assertExpressionsEqual( e, SumExpression( [ @@ -4050,8 +3811,7 @@ def test_summation5(self): def test_summation6(self): e = quicksum(self.m.a[i] / self.m.p[i] for i in self.m.a) self.assertEqual(e(), 25) - assertExpressionsEqual( - self, + self.assertExpressionsEqual( e, LinearExpression( [ @@ -4077,8 +3837,7 @@ def test_summation6(self): def test_summation7(self): e = quicksum((self.m.p[i] * self.m.q[i] for i in self.m.I), linear=False) self.assertEqual(e(), 15) - assertExpressionsEqual( - self, + self.assertExpressionsEqual( e, NPV_SumExpression( [ @@ -4203,15 +3962,15 @@ def test_SumExpression(self): self.assertEqual(expr2(), 15) self.assertNotEqual(id(expr1), id(expr2)) self.assertNotEqual(id(expr1._args_), id(expr2._args_)) - self.assertIs(expr1.arg(0).arg(1), expr2.arg(0).arg(1)) - self.assertIs(expr1.arg(1).arg(1), expr2.arg(1).arg(1)) + self.assertIs(expr1.arg(0), expr2.arg(0)) + self.assertIs(expr1.arg(1), expr2.arg(1)) expr1 += self.m.b self.assertEqual(expr1(), 25) self.assertEqual(expr2(), 15) self.assertNotEqual(id(expr1), id(expr2)) self.assertNotEqual(id(expr1._args_), id(expr2._args_)) - self.assertIs(expr1.arg(0).arg(1), expr2.arg(0).arg(1)) - self.assertIs(expr1.arg(1).arg(1), expr2.arg(1).arg(1)) + self.assertIs(expr1.arg(0), expr2.arg(0)) + self.assertIs(expr1.arg(1), expr2.arg(1)) # total = counter.count - start self.assertEqual(total, 1) @@ -4388,9 +4147,9 @@ def test_productOfExpressions(self): self.assertEqual(expr1.arg(1).nargs(), 2) self.assertEqual(expr2.arg(1).nargs(), 2) - self.assertIs(expr1.arg(0).arg(0).arg(1), expr2.arg(0).arg(0).arg(1)) - self.assertIs(expr1.arg(0).arg(1).arg(1), expr2.arg(0).arg(1).arg(1)) - self.assertIs(expr1.arg(1).arg(0).arg(1), expr2.arg(1).arg(0).arg(1)) + self.assertIs(expr1.arg(0).arg(0), expr2.arg(0).arg(0)) + self.assertIs(expr1.arg(0).arg(1), expr2.arg(0).arg(1)) + self.assertIs(expr1.arg(1).arg(0), expr2.arg(1).arg(0)) expr1 *= self.m.b self.assertEqual(expr1(), 1500) @@ -4429,8 +4188,8 @@ def test_productOfExpressions_div(self): self.assertEqual(expr1.arg(1).nargs(), 2) self.assertEqual(expr2.arg(1).nargs(), 2) - self.assertIs(expr1.arg(0).arg(0).arg(1), expr2.arg(0).arg(0).arg(1)) - self.assertIs(expr1.arg(0).arg(1).arg(1), expr2.arg(0).arg(1).arg(1)) + self.assertIs(expr1.arg(0).arg(0), expr2.arg(0).arg(0)) + self.assertIs(expr1.arg(0).arg(1), expr2.arg(0).arg(1)) expr1 /= self.m.b self.assertAlmostEqual(expr1(), 0.15) @@ -4453,7 +4212,7 @@ def test_Expr_if(self): # expr1 = Expr_if(IF=self.m.a + self.m.b < 20, THEN=self.m.a, ELSE=self.m.b) expr2 = expr1.clone() - assertExpressionsStructurallyEqual(self, expr1, expr2) + self.assertExpressionsStructurallyEqual(expr1, expr2) self.assertIsNot(expr1, expr2) self.assertIsNot(expr1.arg(0), expr2.arg(0)) @@ -5029,7 +4788,7 @@ def test_init(self): self.assertEqual(e.linear_vars, [m.x, m.y]) self.assertEqual(e.linear_coefs, [2, 3]) - assertExpressionsEqual(self, e, f) + self.assertExpressionsEqual(e, f) args = [10, MonomialTermExpression((4, m.y)), MonomialTermExpression((5, m.x))] with LoggingIntercept() as OUT: @@ -5116,7 +4875,7 @@ def test_sum_other(self): with linear_expression() as e: e = e - arg - assertExpressionsEqual(self, e, -arg) + self.assertExpressionsEqual(e, -arg) def test_mul_other(self): m = ConcreteModel() @@ -5255,25 +5014,13 @@ def test_pow_other(self): with linear_expression() as e: e += m.p e = 2**e - assertExpressionsEqual(self, e, NPV_PowExpression((2, m.p))) + self.assertExpressionsEqual(e, NPV_PowExpression((2, m.p))) with linear_expression() as e: e += m.v[0] + m.v[1] e = m.v[0] ** e - assertExpressionsEqual( - self, - e, - PowExpression( - ( - m.v[0], - LinearExpression( - [ - MonomialTermExpression((1, m.v[0])), - MonomialTermExpression((1, m.v[1])), - ] - ), - ) - ), + self.assertExpressionsEqual( + e, PowExpression((m.v[0], LinearExpression([m.v[0], m.v[1]]))) ) @@ -5545,7 +5292,7 @@ def test_simple(self): s = pickle.dumps(e) e_ = pickle.loads(s) self.assertIsNot(e, e_) - assertExpressionsStructurallyEqual(self, e, e_) + self.assertExpressionsStructurallyEqual(e, e_) def test_sum(self): M = ConcreteModel() @@ -5556,7 +5303,7 @@ def test_sum(self): s = pickle.dumps(e) e_ = pickle.loads(s) self.assertIsNot(e, e_) - assertExpressionsStructurallyEqual(self, e, e_) + self.assertExpressionsStructurallyEqual(e, e_) def Xtest_Sum(self): M = ConcreteModel() @@ -5566,7 +5313,7 @@ def Xtest_Sum(self): s = pickle.dumps(e) e_ = pickle.loads(s) self.assertIsNot(e, e_) - assertExpressionsStructurallyEqual(self, e, e_) + self.assertExpressionsStructurallyEqual(e, e_) def test_prod(self): M = ConcreteModel() @@ -5577,7 +5324,7 @@ def test_prod(self): s = pickle.dumps(e) e_ = pickle.loads(s) self.assertIsNot(e, e_) - assertExpressionsStructurallyEqual(self, e, e_) + self.assertExpressionsStructurallyEqual(e, e_) def test_negation(self): M = ConcreteModel() @@ -5586,7 +5333,7 @@ def test_negation(self): s = pickle.dumps(e) e_ = pickle.loads(s) self.assertIsNot(e, e_) - assertExpressionsStructurallyEqual(self, e, e_) + self.assertExpressionsStructurallyEqual(e, e_) def test_reciprocal(self): M = ConcreteModel() @@ -5597,7 +5344,7 @@ def test_reciprocal(self): s = pickle.dumps(e) e_ = pickle.loads(s) self.assertIsNot(e, e_) - assertExpressionsStructurallyEqual(self, e, e_) + self.assertExpressionsStructurallyEqual(e, e_) def test_multisum(self): M = ConcreteModel() @@ -5608,7 +5355,7 @@ def test_multisum(self): s = pickle.dumps(e) e_ = pickle.loads(s) self.assertIsNot(e, e_) - assertExpressionsStructurallyEqual(self, e, e_) + self.assertExpressionsStructurallyEqual(e, e_) def test_linear(self): M = ConcreteModel() @@ -5621,7 +5368,7 @@ def test_linear(self): s = pickle.dumps(e) e_ = pickle.loads(s) self.assertIsNot(e, e_) - assertExpressionsStructurallyEqual(self, e, e_) + self.assertExpressionsStructurallyEqual(e, e_) def test_linear_context(self): M = ConcreteModel() @@ -5634,7 +5381,7 @@ def test_linear_context(self): s = pickle.dumps(e) e_ = pickle.loads(s) self.assertIsNot(e, e_) - assertExpressionsStructurallyEqual(self, e, e_) + self.assertExpressionsStructurallyEqual(e, e_) def test_ExprIf(self): M = ConcreteModel() @@ -5643,7 +5390,7 @@ def test_ExprIf(self): s = pickle.dumps(e) e_ = pickle.loads(s) self.assertIsNot(e, e_) - assertExpressionsStructurallyEqual(self, e, e_) + self.assertExpressionsStructurallyEqual(e, e_) def test_getitem(self): m = ConcreteModel() @@ -5656,7 +5403,7 @@ def test_getitem(self): s = pickle.dumps(e) e_ = pickle.loads(s) self.assertIsNot(e, e_) - assertExpressionsStructurallyEqual(self, e, e_) + self.assertExpressionsStructurallyEqual(e, e_) self.assertEqual("x[{I} + P[{I} + 1]] + 3", str(e)) def test_abs(self): @@ -5666,7 +5413,7 @@ def test_abs(self): s = pickle.dumps(e) e_ = pickle.loads(s) self.assertIsNot(e, e_) - assertExpressionsStructurallyEqual(self, e, e_) + self.assertExpressionsStructurallyEqual(e, e_) self.assertEqual(str(e), str(e_)) def test_sin(self): @@ -5676,7 +5423,7 @@ def test_sin(self): s = pickle.dumps(e) e_ = pickle.loads(s) self.assertIsNot(e, e_) - assertExpressionsStructurallyEqual(self, e, e_) + self.assertExpressionsStructurallyEqual(e, e_) self.assertEqual(str(e), str(e_)) def test_external_fcn(self): @@ -5687,7 +5434,7 @@ def test_external_fcn(self): s = pickle.dumps(e) e_ = pickle.loads(s) self.assertIsNot(e, e_) - assertExpressionsStructurallyEqual(self, e, e_) + self.assertExpressionsStructurallyEqual(e, e_) # diff --git a/pyomo/core/tests/unit/test_numeric_expr_api.py b/pyomo/core/tests/unit/test_numeric_expr_api.py index 0d85e959fa0..923f78af1be 100644 --- a/pyomo/core/tests/unit/test_numeric_expr_api.py +++ b/pyomo/core/tests/unit/test_numeric_expr_api.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -223,7 +223,7 @@ def test_negation(self): self.assertEqual(is_fixed(e), False) self.assertEqual(value(e), -15) self.assertEqual(str(e), "- (x + 2*x)") - self.assertEqual(e.to_string(verbose=True), "neg(sum(mon(1, x), mon(2, x)))") + self.assertEqual(e.to_string(verbose=True), "neg(sum(x, mon(2, x)))") # This can't occur through operator overloading, but could # through expression substitution @@ -634,8 +634,7 @@ def test_linear(self): self.assertEqual(value(e), 1 + 4 + 5 + 2) self.assertEqual(str(e), "0*x[0] + x[1] + 2*x[2] + 5 + y - 3") self.assertEqual( - e.to_string(verbose=True), - "sum(mon(0, x[0]), mon(1, x[1]), mon(2, x[2]), 5, mon(1, y), -3)", + e.to_string(verbose=True), "sum(mon(0, x[0]), x[1], mon(2, x[2]), 5, y, -3)" ) self.assertIs(type(e), LinearExpression) @@ -701,7 +700,7 @@ def test_expr_if(self): ) self.assertEqual( e.to_string(verbose=True), - "Expr_if( ( 5 <= y ), then=( sum(mon(1, x[0]), 5) ), else=( pow(x[1], 2) ) )", + "Expr_if( ( 5 <= y ), then=( sum(x[0], 5) ), else=( pow(x[1], 2) ) )", ) m.y.fix() @@ -950,7 +949,14 @@ def test_sum(self): f = e.create_node_with_local_data(e.args) self.assertIsNot(f, e) self.assertIs(type(f), type(e)) - self.assertIs(f.args, e.args) + self.assertIsNot(f._args_, e._args_) + self.assertIsNot(f.args, e.args) + + f = e.create_node_with_local_data(e._args_) + self.assertIsNot(f, e) + self.assertIs(type(f), type(e)) + self.assertIs(f._args_, e._args_) + self.assertIsNot(f.args, e.args) f = e.create_node_with_local_data((m.x, 2, 3)) self.assertIsNot(f, e) @@ -965,9 +971,7 @@ def test_sum(self): f = e.create_node_with_local_data((m.p, m.x)) self.assertIsNot(f, e) self.assertIs(type(f), LinearExpression) - assertExpressionsStructurallyEqual( - self, f.args, [m.p, MonomialTermExpression((1, m.x))] - ) + assertExpressionsStructurallyEqual(self, f.args, [m.p, m.x]) f = e.create_node_with_local_data((m.p, m.x**2)) self.assertIsNot(f, e) diff --git a/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py b/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py index 3e9e160b1b1..bb7a291e67d 100644 --- a/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py +++ b/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -123,8 +123,6 @@ def setUp(self): self.mutable_l3 = _MutableNPVSumExpression([self.npv]) # often repeated reference expressions - self.mon_bin = MonomialTermExpression((1, self.bin)) - self.mon_var = MonomialTermExpression((1, self.var)) self.minus_bin = MonomialTermExpression((-1, self.bin)) self.minus_npv = NPV_NegationExpression((self.npv,)) self.minus_param_mut = NPV_NegationExpression((self.param_mut,)) @@ -368,38 +366,34 @@ def test_add_asbinary(self): # BooleanVar objects do not support addition (self.asbinary, self.asbinary, NotImplemented), (self.asbinary, self.zero, self.bin), - (self.asbinary, self.one, LinearExpression([self.mon_bin, 1])), + (self.asbinary, self.one, LinearExpression([self.bin, 1])), # 4: - (self.asbinary, self.native, LinearExpression([self.mon_bin, 5])), - (self.asbinary, self.npv, LinearExpression([self.mon_bin, self.npv])), - (self.asbinary, self.param, LinearExpression([self.mon_bin, 6])), + (self.asbinary, self.native, LinearExpression([self.bin, 5])), + (self.asbinary, self.npv, LinearExpression([self.bin, self.npv])), + (self.asbinary, self.param, LinearExpression([self.bin, 6])), ( self.asbinary, self.param_mut, - LinearExpression([self.mon_bin, self.param_mut]), + LinearExpression([self.bin, self.param_mut]), ), # 8: - (self.asbinary, self.var, LinearExpression([self.mon_bin, self.mon_var])), + (self.asbinary, self.var, LinearExpression([self.bin, self.var])), ( self.asbinary, self.mon_native, - LinearExpression([self.mon_bin, self.mon_native]), + LinearExpression([self.bin, self.mon_native]), ), ( self.asbinary, self.mon_param, - LinearExpression([self.mon_bin, self.mon_param]), - ), - ( - self.asbinary, - self.mon_npv, - LinearExpression([self.mon_bin, self.mon_npv]), + LinearExpression([self.bin, self.mon_param]), ), + (self.asbinary, self.mon_npv, LinearExpression([self.bin, self.mon_npv])), # 12: ( self.asbinary, self.linear, - LinearExpression(self.linear.args + [self.mon_bin]), + LinearExpression(self.linear.args + [self.bin]), ), (self.asbinary, self.sum, SumExpression(self.sum.args + [self.bin])), (self.asbinary, self.other, SumExpression([self.bin, self.other])), @@ -408,7 +402,7 @@ def test_add_asbinary(self): ( self.asbinary, self.mutable_l1, - LinearExpression([self.mon_bin, self.mon_npv]), + LinearExpression([self.bin, self.mon_npv]), ), ( self.asbinary, @@ -416,13 +410,9 @@ def test_add_asbinary(self): SumExpression(self.mutable_l2.args + [self.bin]), ), (self.asbinary, self.param0, self.bin), - (self.asbinary, self.param1, LinearExpression([self.mon_bin, 1])), + (self.asbinary, self.param1, LinearExpression([self.bin, 1])), # 20: - ( - self.asbinary, - self.mutable_l3, - LinearExpression([self.mon_bin, self.npv]), - ), + (self.asbinary, self.mutable_l3, LinearExpression([self.bin, self.npv])), ] self._run_cases(tests, operator.add) self._run_cases(tests, operator.iadd) @@ -462,7 +452,7 @@ def test_add_zero(self): def test_add_one(self): tests = [ (self.one, self.invalid, NotImplemented), - (self.one, self.asbinary, LinearExpression([1, self.mon_bin])), + (self.one, self.asbinary, LinearExpression([1, self.bin])), (self.one, self.zero, 1), (self.one, self.one, 2), # 4: @@ -471,7 +461,7 @@ def test_add_one(self): (self.one, self.param, 7), (self.one, self.param_mut, NPV_SumExpression([1, self.param_mut])), # 8: - (self.one, self.var, LinearExpression([1, self.mon_var])), + (self.one, self.var, LinearExpression([1, self.var])), (self.one, self.mon_native, LinearExpression([1, self.mon_native])), (self.one, self.mon_param, LinearExpression([1, self.mon_param])), (self.one, self.mon_npv, LinearExpression([1, self.mon_npv])), @@ -494,7 +484,7 @@ def test_add_one(self): def test_add_native(self): tests = [ (self.native, self.invalid, NotImplemented), - (self.native, self.asbinary, LinearExpression([5, self.mon_bin])), + (self.native, self.asbinary, LinearExpression([5, self.bin])), (self.native, self.zero, 5), (self.native, self.one, 6), # 4: @@ -503,7 +493,7 @@ def test_add_native(self): (self.native, self.param, 11), (self.native, self.param_mut, NPV_SumExpression([5, self.param_mut])), # 8: - (self.native, self.var, LinearExpression([5, self.mon_var])), + (self.native, self.var, LinearExpression([5, self.var])), (self.native, self.mon_native, LinearExpression([5, self.mon_native])), (self.native, self.mon_param, LinearExpression([5, self.mon_param])), (self.native, self.mon_npv, LinearExpression([5, self.mon_npv])), @@ -530,7 +520,7 @@ def test_add_native(self): def test_add_npv(self): tests = [ (self.npv, self.invalid, NotImplemented), - (self.npv, self.asbinary, LinearExpression([self.npv, self.mon_bin])), + (self.npv, self.asbinary, LinearExpression([self.npv, self.bin])), (self.npv, self.zero, self.npv), (self.npv, self.one, NPV_SumExpression([self.npv, 1])), # 4: @@ -539,7 +529,7 @@ def test_add_npv(self): (self.npv, self.param, NPV_SumExpression([self.npv, 6])), (self.npv, self.param_mut, NPV_SumExpression([self.npv, self.param_mut])), # 8: - (self.npv, self.var, LinearExpression([self.npv, self.mon_var])), + (self.npv, self.var, LinearExpression([self.npv, self.var])), (self.npv, self.mon_native, LinearExpression([self.npv, self.mon_native])), (self.npv, self.mon_param, LinearExpression([self.npv, self.mon_param])), (self.npv, self.mon_npv, LinearExpression([self.npv, self.mon_npv])), @@ -570,7 +560,7 @@ def test_add_npv(self): def test_add_param(self): tests = [ (self.param, self.invalid, NotImplemented), - (self.param, self.asbinary, LinearExpression([6, self.mon_bin])), + (self.param, self.asbinary, LinearExpression([6, self.bin])), (self.param, self.zero, 6), (self.param, self.one, 7), # 4: @@ -579,7 +569,7 @@ def test_add_param(self): (self.param, self.param, 12), (self.param, self.param_mut, NPV_SumExpression([6, self.param_mut])), # 8: - (self.param, self.var, LinearExpression([6, self.mon_var])), + (self.param, self.var, LinearExpression([6, self.var])), (self.param, self.mon_native, LinearExpression([6, self.mon_native])), (self.param, self.mon_param, LinearExpression([6, self.mon_param])), (self.param, self.mon_npv, LinearExpression([6, self.mon_npv])), @@ -605,7 +595,7 @@ def test_add_param_mut(self): ( self.param_mut, self.asbinary, - LinearExpression([self.param_mut, self.mon_bin]), + LinearExpression([self.param_mut, self.bin]), ), (self.param_mut, self.zero, self.param_mut), (self.param_mut, self.one, NPV_SumExpression([self.param_mut, 1])), @@ -619,11 +609,7 @@ def test_add_param_mut(self): NPV_SumExpression([self.param_mut, self.param_mut]), ), # 8: - ( - self.param_mut, - self.var, - LinearExpression([self.param_mut, self.mon_var]), - ), + (self.param_mut, self.var, LinearExpression([self.param_mut, self.var])), ( self.param_mut, self.mon_native, @@ -674,37 +660,21 @@ def test_add_param_mut(self): def test_add_var(self): tests = [ (self.var, self.invalid, NotImplemented), - (self.var, self.asbinary, LinearExpression([self.mon_var, self.mon_bin])), + (self.var, self.asbinary, LinearExpression([self.var, self.bin])), (self.var, self.zero, self.var), - (self.var, self.one, LinearExpression([self.mon_var, 1])), + (self.var, self.one, LinearExpression([self.var, 1])), # 4: - (self.var, self.native, LinearExpression([self.mon_var, 5])), - (self.var, self.npv, LinearExpression([self.mon_var, self.npv])), - (self.var, self.param, LinearExpression([self.mon_var, 6])), - ( - self.var, - self.param_mut, - LinearExpression([self.mon_var, self.param_mut]), - ), + (self.var, self.native, LinearExpression([self.var, 5])), + (self.var, self.npv, LinearExpression([self.var, self.npv])), + (self.var, self.param, LinearExpression([self.var, 6])), + (self.var, self.param_mut, LinearExpression([self.var, self.param_mut])), # 8: - (self.var, self.var, LinearExpression([self.mon_var, self.mon_var])), - ( - self.var, - self.mon_native, - LinearExpression([self.mon_var, self.mon_native]), - ), - ( - self.var, - self.mon_param, - LinearExpression([self.mon_var, self.mon_param]), - ), - (self.var, self.mon_npv, LinearExpression([self.mon_var, self.mon_npv])), + (self.var, self.var, LinearExpression([self.var, self.var])), + (self.var, self.mon_native, LinearExpression([self.var, self.mon_native])), + (self.var, self.mon_param, LinearExpression([self.var, self.mon_param])), + (self.var, self.mon_npv, LinearExpression([self.var, self.mon_npv])), # 12: - ( - self.var, - self.linear, - LinearExpression(self.linear.args + [self.mon_var]), - ), + (self.var, self.linear, LinearExpression(self.linear.args + [self.var])), (self.var, self.sum, SumExpression(self.sum.args + [self.var])), (self.var, self.other, SumExpression([self.var, self.other])), (self.var, self.mutable_l0, self.var), @@ -712,7 +682,7 @@ def test_add_var(self): ( self.var, self.mutable_l1, - LinearExpression([self.mon_var] + self.mutable_l1.args), + LinearExpression([self.var] + self.mutable_l1.args), ), ( self.var, @@ -720,13 +690,9 @@ def test_add_var(self): SumExpression(self.mutable_l2.args + [self.var]), ), (self.var, self.param0, self.var), - (self.var, self.param1, LinearExpression([self.mon_var, 1])), + (self.var, self.param1, LinearExpression([self.var, 1])), # 20: - ( - self.var, - self.mutable_l3, - LinearExpression([MonomialTermExpression((1, self.var)), self.npv]), - ), + (self.var, self.mutable_l3, LinearExpression([self.var, self.npv])), ] self._run_cases(tests, operator.add) self._run_cases(tests, operator.iadd) @@ -737,7 +703,7 @@ def test_add_mon_native(self): ( self.mon_native, self.asbinary, - LinearExpression([self.mon_native, self.mon_bin]), + LinearExpression([self.mon_native, self.bin]), ), (self.mon_native, self.zero, self.mon_native), (self.mon_native, self.one, LinearExpression([self.mon_native, 1])), @@ -751,11 +717,7 @@ def test_add_mon_native(self): LinearExpression([self.mon_native, self.param_mut]), ), # 8: - ( - self.mon_native, - self.var, - LinearExpression([self.mon_native, self.mon_var]), - ), + (self.mon_native, self.var, LinearExpression([self.mon_native, self.var])), ( self.mon_native, self.mon_native, @@ -813,7 +775,7 @@ def test_add_mon_param(self): ( self.mon_param, self.asbinary, - LinearExpression([self.mon_param, self.mon_bin]), + LinearExpression([self.mon_param, self.bin]), ), (self.mon_param, self.zero, self.mon_param), (self.mon_param, self.one, LinearExpression([self.mon_param, 1])), @@ -827,11 +789,7 @@ def test_add_mon_param(self): LinearExpression([self.mon_param, self.param_mut]), ), # 8: - ( - self.mon_param, - self.var, - LinearExpression([self.mon_param, self.mon_var]), - ), + (self.mon_param, self.var, LinearExpression([self.mon_param, self.var])), ( self.mon_param, self.mon_native, @@ -882,11 +840,7 @@ def test_add_mon_param(self): def test_add_mon_npv(self): tests = [ (self.mon_npv, self.invalid, NotImplemented), - ( - self.mon_npv, - self.asbinary, - LinearExpression([self.mon_npv, self.mon_bin]), - ), + (self.mon_npv, self.asbinary, LinearExpression([self.mon_npv, self.bin])), (self.mon_npv, self.zero, self.mon_npv), (self.mon_npv, self.one, LinearExpression([self.mon_npv, 1])), # 4: @@ -899,7 +853,7 @@ def test_add_mon_npv(self): LinearExpression([self.mon_npv, self.param_mut]), ), # 8: - (self.mon_npv, self.var, LinearExpression([self.mon_npv, self.mon_var])), + (self.mon_npv, self.var, LinearExpression([self.mon_npv, self.var])), ( self.mon_npv, self.mon_native, @@ -949,7 +903,7 @@ def test_add_linear(self): ( self.linear, self.asbinary, - LinearExpression(self.linear.args + [self.mon_bin]), + LinearExpression(self.linear.args + [self.bin]), ), (self.linear, self.zero, self.linear), (self.linear, self.one, LinearExpression(self.linear.args + [1])), @@ -963,11 +917,7 @@ def test_add_linear(self): LinearExpression(self.linear.args + [self.param_mut]), ), # 8: - ( - self.linear, - self.var, - LinearExpression(self.linear.args + [self.mon_var]), - ), + (self.linear, self.var, LinearExpression(self.linear.args + [self.var])), ( self.linear, self.mon_native, @@ -1134,7 +1084,7 @@ def test_add_mutable_l1(self): ( self.mutable_l1, self.asbinary, - LinearExpression(self.mutable_l1.args + [self.mon_bin]), + LinearExpression(self.mutable_l1.args + [self.bin]), ), (self.mutable_l1, self.zero, self.mon_npv), (self.mutable_l1, self.one, LinearExpression(self.mutable_l1.args + [1])), @@ -1159,7 +1109,7 @@ def test_add_mutable_l1(self): ( self.mutable_l1, self.var, - LinearExpression(self.mutable_l1.args + [self.mon_var]), + LinearExpression(self.mutable_l1.args + [self.var]), ), ( self.mutable_l1, @@ -1341,7 +1291,7 @@ def test_add_param0(self): def test_add_param1(self): tests = [ (self.param1, self.invalid, NotImplemented), - (self.param1, self.asbinary, LinearExpression([1, self.mon_bin])), + (self.param1, self.asbinary, LinearExpression([1, self.bin])), (self.param1, self.zero, 1), (self.param1, self.one, 2), # 4: @@ -1350,7 +1300,7 @@ def test_add_param1(self): (self.param1, self.param, 7), (self.param1, self.param_mut, NPV_SumExpression([1, self.param_mut])), # 8: - (self.param1, self.var, LinearExpression([1, self.mon_var])), + (self.param1, self.var, LinearExpression([1, self.var])), (self.param1, self.mon_native, LinearExpression([1, self.mon_native])), (self.param1, self.mon_param, LinearExpression([1, self.mon_param])), (self.param1, self.mon_npv, LinearExpression([1, self.mon_npv])), @@ -1380,7 +1330,7 @@ def test_add_mutable_l3(self): ( self.mutable_l3, self.asbinary, - LinearExpression(self.mutable_l3.args + [self.mon_bin]), + LinearExpression(self.mutable_l3.args + [self.bin]), ), (self.mutable_l3, self.zero, self.npv), (self.mutable_l3, self.one, NPV_SumExpression(self.mutable_l3.args + [1])), @@ -1409,7 +1359,7 @@ def test_add_mutable_l3(self): ( self.mutable_l3, self.var, - LinearExpression(self.mutable_l3.args + [self.mon_var]), + LinearExpression(self.mutable_l3.args + [self.var]), ), ( self.mutable_l3, @@ -1515,32 +1465,32 @@ def test_sub_asbinary(self): # BooleanVar objects do not support addition (self.asbinary, self.asbinary, NotImplemented), (self.asbinary, self.zero, self.bin), - (self.asbinary, self.one, LinearExpression([self.mon_bin, -1])), + (self.asbinary, self.one, LinearExpression([self.bin, -1])), # 4: - (self.asbinary, self.native, LinearExpression([self.mon_bin, -5])), - (self.asbinary, self.npv, LinearExpression([self.mon_bin, self.minus_npv])), - (self.asbinary, self.param, LinearExpression([self.mon_bin, -6])), + (self.asbinary, self.native, LinearExpression([self.bin, -5])), + (self.asbinary, self.npv, LinearExpression([self.bin, self.minus_npv])), + (self.asbinary, self.param, LinearExpression([self.bin, -6])), ( self.asbinary, self.param_mut, - LinearExpression([self.mon_bin, self.minus_param_mut]), + LinearExpression([self.bin, self.minus_param_mut]), ), # 8: - (self.asbinary, self.var, LinearExpression([self.mon_bin, self.minus_var])), + (self.asbinary, self.var, LinearExpression([self.bin, self.minus_var])), ( self.asbinary, self.mon_native, - LinearExpression([self.mon_bin, self.minus_mon_native]), + LinearExpression([self.bin, self.minus_mon_native]), ), ( self.asbinary, self.mon_param, - LinearExpression([self.mon_bin, self.minus_mon_param]), + LinearExpression([self.bin, self.minus_mon_param]), ), ( self.asbinary, self.mon_npv, - LinearExpression([self.mon_bin, self.minus_mon_npv]), + LinearExpression([self.bin, self.minus_mon_npv]), ), # 12: (self.asbinary, self.linear, SumExpression([self.bin, self.minus_linear])), @@ -1551,7 +1501,7 @@ def test_sub_asbinary(self): ( self.asbinary, self.mutable_l1, - LinearExpression([self.mon_bin, self.minus_mon_npv]), + LinearExpression([self.bin, self.minus_mon_npv]), ), ( self.asbinary, @@ -1559,12 +1509,12 @@ def test_sub_asbinary(self): SumExpression([self.bin, self.minus_mutable_l2]), ), (self.asbinary, self.param0, self.bin), - (self.asbinary, self.param1, LinearExpression([self.mon_bin, -1])), + (self.asbinary, self.param1, LinearExpression([self.bin, -1])), # 20: ( self.asbinary, self.mutable_l3, - LinearExpression([self.mon_bin, self.minus_npv]), + LinearExpression([self.bin, self.minus_npv]), ), ] self._run_cases(tests, operator.sub) @@ -1837,35 +1787,31 @@ def test_sub_param_mut(self): def test_sub_var(self): tests = [ (self.var, self.invalid, NotImplemented), - (self.var, self.asbinary, LinearExpression([self.mon_var, self.minus_bin])), + (self.var, self.asbinary, LinearExpression([self.var, self.minus_bin])), (self.var, self.zero, self.var), - (self.var, self.one, LinearExpression([self.mon_var, -1])), + (self.var, self.one, LinearExpression([self.var, -1])), # 4: - (self.var, self.native, LinearExpression([self.mon_var, -5])), - (self.var, self.npv, LinearExpression([self.mon_var, self.minus_npv])), - (self.var, self.param, LinearExpression([self.mon_var, -6])), + (self.var, self.native, LinearExpression([self.var, -5])), + (self.var, self.npv, LinearExpression([self.var, self.minus_npv])), + (self.var, self.param, LinearExpression([self.var, -6])), ( self.var, self.param_mut, - LinearExpression([self.mon_var, self.minus_param_mut]), + LinearExpression([self.var, self.minus_param_mut]), ), # 8: - (self.var, self.var, LinearExpression([self.mon_var, self.minus_var])), + (self.var, self.var, LinearExpression([self.var, self.minus_var])), ( self.var, self.mon_native, - LinearExpression([self.mon_var, self.minus_mon_native]), + LinearExpression([self.var, self.minus_mon_native]), ), ( self.var, self.mon_param, - LinearExpression([self.mon_var, self.minus_mon_param]), - ), - ( - self.var, - self.mon_npv, - LinearExpression([self.mon_var, self.minus_mon_npv]), + LinearExpression([self.var, self.minus_mon_param]), ), + (self.var, self.mon_npv, LinearExpression([self.var, self.minus_mon_npv])), # 12: ( self.var, @@ -1879,7 +1825,7 @@ def test_sub_var(self): ( self.var, self.mutable_l1, - LinearExpression([self.mon_var, self.minus_mon_npv]), + LinearExpression([self.var, self.minus_mon_npv]), ), ( self.var, @@ -1887,13 +1833,9 @@ def test_sub_var(self): SumExpression([self.var, self.minus_mutable_l2]), ), (self.var, self.param0, self.var), - (self.var, self.param1, LinearExpression([self.mon_var, -1])), + (self.var, self.param1, LinearExpression([self.var, -1])), # 20: - ( - self.var, - self.mutable_l3, - LinearExpression([self.mon_var, self.minus_npv]), - ), + (self.var, self.mutable_l3, LinearExpression([self.var, self.minus_npv])), ] self._run_cases(tests, operator.sub) self._run_cases(tests, operator.isub) @@ -6511,7 +6453,7 @@ def test_mutable_nvp_iadd(self): mutable_npv = _MutableNPVSumExpression([]) tests = [ (mutable_npv, self.invalid, NotImplemented), - (mutable_npv, self.asbinary, _MutableLinearExpression([self.mon_bin])), + (mutable_npv, self.asbinary, _MutableLinearExpression([self.bin])), (mutable_npv, self.zero, _MutableNPVSumExpression([])), (mutable_npv, self.one, _MutableNPVSumExpression([1])), # 4: @@ -6520,7 +6462,7 @@ def test_mutable_nvp_iadd(self): (mutable_npv, self.param, _MutableNPVSumExpression([6])), (mutable_npv, self.param_mut, _MutableNPVSumExpression([self.param_mut])), # 8: - (mutable_npv, self.var, _MutableLinearExpression([self.mon_var])), + (mutable_npv, self.var, _MutableLinearExpression([self.var])), (mutable_npv, self.mon_native, _MutableLinearExpression([self.mon_native])), (mutable_npv, self.mon_param, _MutableLinearExpression([self.mon_param])), (mutable_npv, self.mon_npv, _MutableLinearExpression([self.mon_npv])), @@ -6546,20 +6488,20 @@ def test_mutable_nvp_iadd(self): mutable_npv = _MutableNPVSumExpression([10]) tests = [ (mutable_npv, self.invalid, NotImplemented), - (mutable_npv, self.asbinary, _MutableLinearExpression([10, self.mon_bin])), + (mutable_npv, self.asbinary, _MutableLinearExpression([10, self.bin])), (mutable_npv, self.zero, _MutableNPVSumExpression([10])), - (mutable_npv, self.one, _MutableNPVSumExpression([10, 1])), + (mutable_npv, self.one, _MutableNPVSumExpression([11])), # 4: - (mutable_npv, self.native, _MutableNPVSumExpression([10, 5])), + (mutable_npv, self.native, _MutableNPVSumExpression([15])), (mutable_npv, self.npv, _MutableNPVSumExpression([10, self.npv])), - (mutable_npv, self.param, _MutableNPVSumExpression([10, 6])), + (mutable_npv, self.param, _MutableNPVSumExpression([16])), ( mutable_npv, self.param_mut, _MutableNPVSumExpression([10, self.param_mut]), ), # 8: - (mutable_npv, self.var, _MutableLinearExpression([10, self.mon_var])), + (mutable_npv, self.var, _MutableLinearExpression([10, self.var])), ( mutable_npv, self.mon_native, @@ -6592,7 +6534,7 @@ def test_mutable_nvp_iadd(self): _MutableSumExpression([10] + self.mutable_l2.args), ), (mutable_npv, self.param0, _MutableNPVSumExpression([10])), - (mutable_npv, self.param1, _MutableNPVSumExpression([10, 1])), + (mutable_npv, self.param1, _MutableNPVSumExpression([11])), # 20: (mutable_npv, self.mutable_l3, _MutableNPVSumExpression([10, self.npv])), ] @@ -6602,7 +6544,7 @@ def test_mutable_lin_iadd(self): mutable_lin = _MutableLinearExpression([]) tests = [ (mutable_lin, self.invalid, NotImplemented), - (mutable_lin, self.asbinary, _MutableLinearExpression([self.mon_bin])), + (mutable_lin, self.asbinary, _MutableLinearExpression([self.bin])), (mutable_lin, self.zero, _MutableLinearExpression([])), (mutable_lin, self.one, _MutableLinearExpression([1])), # 4: @@ -6611,7 +6553,7 @@ def test_mutable_lin_iadd(self): (mutable_lin, self.param, _MutableLinearExpression([6])), (mutable_lin, self.param_mut, _MutableLinearExpression([self.param_mut])), # 8: - (mutable_lin, self.var, _MutableLinearExpression([self.mon_var])), + (mutable_lin, self.var, _MutableLinearExpression([self.var])), (mutable_lin, self.mon_native, _MutableLinearExpression([self.mon_native])), (mutable_lin, self.mon_param, _MutableLinearExpression([self.mon_param])), (mutable_lin, self.mon_npv, _MutableLinearExpression([self.mon_npv])), @@ -6634,81 +6576,69 @@ def test_mutable_lin_iadd(self): ] self._run_iadd_cases(tests, operator.iadd) - mutable_lin = _MutableLinearExpression([self.mon_bin]) + mutable_lin = _MutableLinearExpression([self.bin]) tests = [ (mutable_lin, self.invalid, NotImplemented), ( mutable_lin, self.asbinary, - _MutableLinearExpression([self.mon_bin, self.mon_bin]), + _MutableLinearExpression([self.bin, self.bin]), ), - (mutable_lin, self.zero, _MutableLinearExpression([self.mon_bin])), - (mutable_lin, self.one, _MutableLinearExpression([self.mon_bin, 1])), + (mutable_lin, self.zero, _MutableLinearExpression([self.bin])), + (mutable_lin, self.one, _MutableLinearExpression([self.bin, 1])), # 4: - (mutable_lin, self.native, _MutableLinearExpression([self.mon_bin, 5])), - (mutable_lin, self.npv, _MutableLinearExpression([self.mon_bin, self.npv])), - (mutable_lin, self.param, _MutableLinearExpression([self.mon_bin, 6])), + (mutable_lin, self.native, _MutableLinearExpression([self.bin, 5])), + (mutable_lin, self.npv, _MutableLinearExpression([self.bin, self.npv])), + (mutable_lin, self.param, _MutableLinearExpression([self.bin, 6])), ( mutable_lin, self.param_mut, - _MutableLinearExpression([self.mon_bin, self.param_mut]), + _MutableLinearExpression([self.bin, self.param_mut]), ), # 8: - ( - mutable_lin, - self.var, - _MutableLinearExpression([self.mon_bin, self.mon_var]), - ), + (mutable_lin, self.var, _MutableLinearExpression([self.bin, self.var])), ( mutable_lin, self.mon_native, - _MutableLinearExpression([self.mon_bin, self.mon_native]), + _MutableLinearExpression([self.bin, self.mon_native]), ), ( mutable_lin, self.mon_param, - _MutableLinearExpression([self.mon_bin, self.mon_param]), + _MutableLinearExpression([self.bin, self.mon_param]), ), ( mutable_lin, self.mon_npv, - _MutableLinearExpression([self.mon_bin, self.mon_npv]), + _MutableLinearExpression([self.bin, self.mon_npv]), ), # 12: ( mutable_lin, self.linear, - _MutableLinearExpression([self.mon_bin] + self.linear.args), - ), - ( - mutable_lin, - self.sum, - _MutableSumExpression([self.mon_bin] + self.sum.args), - ), - ( - mutable_lin, - self.other, - _MutableSumExpression([self.mon_bin, self.other]), + _MutableLinearExpression([self.bin] + self.linear.args), ), - (mutable_lin, self.mutable_l0, _MutableLinearExpression([self.mon_bin])), + (mutable_lin, self.sum, _MutableSumExpression([self.bin] + self.sum.args)), + (mutable_lin, self.other, _MutableSumExpression([self.bin, self.other])), + (mutable_lin, self.mutable_l0, _MutableLinearExpression([self.bin])), # 16: ( mutable_lin, self.mutable_l1, - _MutableLinearExpression([self.mon_bin] + self.mutable_l1.args), + _MutableLinearExpression([self.bin] + self.mutable_l1.args), ), ( mutable_lin, self.mutable_l2, - _MutableSumExpression([self.mon_bin] + self.mutable_l2.args), + _MutableSumExpression([self.bin] + self.mutable_l2.args), ), - (mutable_lin, self.param0, _MutableLinearExpression([self.mon_bin])), - (mutable_lin, self.param1, _MutableLinearExpression([self.mon_bin, 1])), + (mutable_lin, self.param0, _MutableLinearExpression([self.bin])), + (mutable_lin, self.param1, _MutableLinearExpression([self.bin, 1])), # 20: ( mutable_lin, self.mutable_l3, - _MutableLinearExpression([self.mon_bin, self.npv]), + _MutableLinearExpression([self.bin, self.npv]), ), ] self._run_iadd_cases(tests, operator.iadd) @@ -6854,7 +6784,7 @@ def as_numeric(self): assertExpressionsEqual(self, PowExpression((self.var, 2)), e) e = obj + obj - assertExpressionsEqual(self, LinearExpression((self.mon_var, self.mon_var)), e) + assertExpressionsEqual(self, LinearExpression((self.var, self.var)), e) def test_categorize_arg_type(self): class CustomAsNumeric(NumericValue): diff --git a/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py b/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py index 3000f644e80..19968640a21 100644 --- a/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py +++ b/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -102,38 +102,34 @@ def test_add_asbinary(self): # BooleanVar objects do not support addition (self.asbinary, self.asbinary, NotImplemented), (self.asbinary, self.zero, self.bin), - (self.asbinary, self.one, LinearExpression([self.mon_bin, 1])), + (self.asbinary, self.one, LinearExpression([self.bin, 1])), # 4: - (self.asbinary, self.native, LinearExpression([self.mon_bin, 5])), - (self.asbinary, self.npv, LinearExpression([self.mon_bin, self.npv])), - (self.asbinary, self.param, LinearExpression([self.mon_bin, 6])), + (self.asbinary, self.native, LinearExpression([self.bin, 5])), + (self.asbinary, self.npv, LinearExpression([self.bin, self.npv])), + (self.asbinary, self.param, LinearExpression([self.bin, 6])), ( self.asbinary, self.param_mut, - LinearExpression([self.mon_bin, self.param_mut]), + LinearExpression([self.bin, self.param_mut]), ), # 8: - (self.asbinary, self.var, LinearExpression([self.mon_bin, self.mon_var])), + (self.asbinary, self.var, LinearExpression([self.bin, self.var])), ( self.asbinary, self.mon_native, - LinearExpression([self.mon_bin, self.mon_native]), + LinearExpression([self.bin, self.mon_native]), ), ( self.asbinary, self.mon_param, - LinearExpression([self.mon_bin, self.mon_param]), - ), - ( - self.asbinary, - self.mon_npv, - LinearExpression([self.mon_bin, self.mon_npv]), + LinearExpression([self.bin, self.mon_param]), ), + (self.asbinary, self.mon_npv, LinearExpression([self.bin, self.mon_npv])), # 12: ( self.asbinary, self.linear, - LinearExpression(self.linear.args + [self.mon_bin]), + LinearExpression(self.linear.args + [self.bin]), ), (self.asbinary, self.sum, SumExpression(self.sum.args + [self.bin])), (self.asbinary, self.other, SumExpression([self.bin, self.other])), @@ -142,7 +138,7 @@ def test_add_asbinary(self): ( self.asbinary, self.mutable_l1, - LinearExpression([self.mon_bin, self.mon_npv]), + LinearExpression([self.bin, self.mon_npv]), ), ( self.asbinary, @@ -150,13 +146,9 @@ def test_add_asbinary(self): SumExpression(self.mutable_l2.args + [self.bin]), ), (self.asbinary, self.param0, self.bin), - (self.asbinary, self.param1, LinearExpression([self.mon_bin, 1])), + (self.asbinary, self.param1, LinearExpression([self.bin, 1])), # 20: - ( - self.asbinary, - self.mutable_l3, - LinearExpression([self.mon_bin, self.npv]), - ), + (self.asbinary, self.mutable_l3, LinearExpression([self.bin, self.npv])), ] self._run_cases(tests, operator.add) self._run_cases(tests, operator.iadd) @@ -196,7 +188,7 @@ def test_add_zero(self): def test_add_one(self): tests = [ (self.one, self.invalid, NotImplemented), - (self.one, self.asbinary, LinearExpression([1, self.mon_bin])), + (self.one, self.asbinary, LinearExpression([1, self.bin])), (self.one, self.zero, 1), (self.one, self.one, 2), # 4: @@ -205,7 +197,7 @@ def test_add_one(self): (self.one, self.param, 7), (self.one, self.param_mut, NPV_SumExpression([1, self.param_mut])), # 8: - (self.one, self.var, LinearExpression([1, self.mon_var])), + (self.one, self.var, LinearExpression([1, self.var])), (self.one, self.mon_native, LinearExpression([1, self.mon_native])), (self.one, self.mon_param, LinearExpression([1, self.mon_param])), (self.one, self.mon_npv, LinearExpression([1, self.mon_npv])), @@ -228,7 +220,7 @@ def test_add_one(self): def test_add_native(self): tests = [ (self.native, self.invalid, NotImplemented), - (self.native, self.asbinary, LinearExpression([5, self.mon_bin])), + (self.native, self.asbinary, LinearExpression([5, self.bin])), (self.native, self.zero, 5), (self.native, self.one, 6), # 4: @@ -237,7 +229,7 @@ def test_add_native(self): (self.native, self.param, 11), (self.native, self.param_mut, NPV_SumExpression([5, self.param_mut])), # 8: - (self.native, self.var, LinearExpression([5, self.mon_var])), + (self.native, self.var, LinearExpression([5, self.var])), (self.native, self.mon_native, LinearExpression([5, self.mon_native])), (self.native, self.mon_param, LinearExpression([5, self.mon_param])), (self.native, self.mon_npv, LinearExpression([5, self.mon_npv])), @@ -264,7 +256,7 @@ def test_add_native(self): def test_add_npv(self): tests = [ (self.npv, self.invalid, NotImplemented), - (self.npv, self.asbinary, LinearExpression([self.npv, self.mon_bin])), + (self.npv, self.asbinary, LinearExpression([self.npv, self.bin])), (self.npv, self.zero, self.npv), (self.npv, self.one, NPV_SumExpression([self.npv, 1])), # 4: @@ -273,7 +265,7 @@ def test_add_npv(self): (self.npv, self.param, NPV_SumExpression([self.npv, 6])), (self.npv, self.param_mut, NPV_SumExpression([self.npv, self.param_mut])), # 8: - (self.npv, self.var, LinearExpression([self.npv, self.mon_var])), + (self.npv, self.var, LinearExpression([self.npv, self.var])), (self.npv, self.mon_native, LinearExpression([self.npv, self.mon_native])), (self.npv, self.mon_param, LinearExpression([self.npv, self.mon_param])), (self.npv, self.mon_npv, LinearExpression([self.npv, self.mon_npv])), @@ -304,7 +296,7 @@ def test_add_npv(self): def test_add_param(self): tests = [ (self.param, self.invalid, NotImplemented), - (self.param, self.asbinary, LinearExpression([6, self.mon_bin])), + (self.param, self.asbinary, LinearExpression([6, self.bin])), (self.param, self.zero, 6), (self.param, self.one, 7), # 4: @@ -313,7 +305,7 @@ def test_add_param(self): (self.param, self.param, 12), (self.param, self.param_mut, NPV_SumExpression([6, self.param_mut])), # 8: - (self.param, self.var, LinearExpression([6, self.mon_var])), + (self.param, self.var, LinearExpression([6, self.var])), (self.param, self.mon_native, LinearExpression([6, self.mon_native])), (self.param, self.mon_param, LinearExpression([6, self.mon_param])), (self.param, self.mon_npv, LinearExpression([6, self.mon_npv])), @@ -339,7 +331,7 @@ def test_add_param_mut(self): ( self.param_mut, self.asbinary, - LinearExpression([self.param_mut, self.mon_bin]), + LinearExpression([self.param_mut, self.bin]), ), (self.param_mut, self.zero, self.param_mut), (self.param_mut, self.one, NPV_SumExpression([self.param_mut, 1])), @@ -353,11 +345,7 @@ def test_add_param_mut(self): NPV_SumExpression([self.param_mut, self.param_mut]), ), # 8: - ( - self.param_mut, - self.var, - LinearExpression([self.param_mut, self.mon_var]), - ), + (self.param_mut, self.var, LinearExpression([self.param_mut, self.var])), ( self.param_mut, self.mon_native, @@ -408,37 +396,21 @@ def test_add_param_mut(self): def test_add_var(self): tests = [ (self.var, self.invalid, NotImplemented), - (self.var, self.asbinary, LinearExpression([self.mon_var, self.mon_bin])), + (self.var, self.asbinary, LinearExpression([self.var, self.bin])), (self.var, self.zero, self.var), - (self.var, self.one, LinearExpression([self.mon_var, 1])), + (self.var, self.one, LinearExpression([self.var, 1])), # 4: - (self.var, self.native, LinearExpression([self.mon_var, 5])), - (self.var, self.npv, LinearExpression([self.mon_var, self.npv])), - (self.var, self.param, LinearExpression([self.mon_var, 6])), - ( - self.var, - self.param_mut, - LinearExpression([self.mon_var, self.param_mut]), - ), + (self.var, self.native, LinearExpression([self.var, 5])), + (self.var, self.npv, LinearExpression([self.var, self.npv])), + (self.var, self.param, LinearExpression([self.var, 6])), + (self.var, self.param_mut, LinearExpression([self.var, self.param_mut])), # 8: - (self.var, self.var, LinearExpression([self.mon_var, self.mon_var])), - ( - self.var, - self.mon_native, - LinearExpression([self.mon_var, self.mon_native]), - ), - ( - self.var, - self.mon_param, - LinearExpression([self.mon_var, self.mon_param]), - ), - (self.var, self.mon_npv, LinearExpression([self.mon_var, self.mon_npv])), + (self.var, self.var, LinearExpression([self.var, self.var])), + (self.var, self.mon_native, LinearExpression([self.var, self.mon_native])), + (self.var, self.mon_param, LinearExpression([self.var, self.mon_param])), + (self.var, self.mon_npv, LinearExpression([self.var, self.mon_npv])), # 12: - ( - self.var, - self.linear, - LinearExpression(self.linear.args + [self.mon_var]), - ), + (self.var, self.linear, LinearExpression(self.linear.args + [self.var])), (self.var, self.sum, SumExpression(self.sum.args + [self.var])), (self.var, self.other, SumExpression([self.var, self.other])), (self.var, self.mutable_l0, self.var), @@ -446,7 +418,7 @@ def test_add_var(self): ( self.var, self.mutable_l1, - LinearExpression([self.mon_var] + self.mutable_l1.args), + LinearExpression([self.var] + self.mutable_l1.args), ), ( self.var, @@ -454,13 +426,9 @@ def test_add_var(self): SumExpression(self.mutable_l2.args + [self.var]), ), (self.var, self.param0, self.var), - (self.var, self.param1, LinearExpression([self.mon_var, 1])), + (self.var, self.param1, LinearExpression([self.var, 1])), # 20: - ( - self.var, - self.mutable_l3, - LinearExpression([MonomialTermExpression((1, self.var)), self.npv]), - ), + (self.var, self.mutable_l3, LinearExpression([self.var, self.npv])), ] self._run_cases(tests, operator.add) self._run_cases(tests, operator.iadd) @@ -471,7 +439,7 @@ def test_add_mon_native(self): ( self.mon_native, self.asbinary, - LinearExpression([self.mon_native, self.mon_bin]), + LinearExpression([self.mon_native, self.bin]), ), (self.mon_native, self.zero, self.mon_native), (self.mon_native, self.one, LinearExpression([self.mon_native, 1])), @@ -485,11 +453,7 @@ def test_add_mon_native(self): LinearExpression([self.mon_native, self.param_mut]), ), # 8: - ( - self.mon_native, - self.var, - LinearExpression([self.mon_native, self.mon_var]), - ), + (self.mon_native, self.var, LinearExpression([self.mon_native, self.var])), ( self.mon_native, self.mon_native, @@ -547,7 +511,7 @@ def test_add_mon_param(self): ( self.mon_param, self.asbinary, - LinearExpression([self.mon_param, self.mon_bin]), + LinearExpression([self.mon_param, self.bin]), ), (self.mon_param, self.zero, self.mon_param), (self.mon_param, self.one, LinearExpression([self.mon_param, 1])), @@ -561,11 +525,7 @@ def test_add_mon_param(self): LinearExpression([self.mon_param, self.param_mut]), ), # 8: - ( - self.mon_param, - self.var, - LinearExpression([self.mon_param, self.mon_var]), - ), + (self.mon_param, self.var, LinearExpression([self.mon_param, self.var])), ( self.mon_param, self.mon_native, @@ -616,11 +576,7 @@ def test_add_mon_param(self): def test_add_mon_npv(self): tests = [ (self.mon_npv, self.invalid, NotImplemented), - ( - self.mon_npv, - self.asbinary, - LinearExpression([self.mon_npv, self.mon_bin]), - ), + (self.mon_npv, self.asbinary, LinearExpression([self.mon_npv, self.bin])), (self.mon_npv, self.zero, self.mon_npv), (self.mon_npv, self.one, LinearExpression([self.mon_npv, 1])), # 4: @@ -633,7 +589,7 @@ def test_add_mon_npv(self): LinearExpression([self.mon_npv, self.param_mut]), ), # 8: - (self.mon_npv, self.var, LinearExpression([self.mon_npv, self.mon_var])), + (self.mon_npv, self.var, LinearExpression([self.mon_npv, self.var])), ( self.mon_npv, self.mon_native, @@ -683,7 +639,7 @@ def test_add_linear(self): ( self.linear, self.asbinary, - LinearExpression(self.linear.args + [self.mon_bin]), + LinearExpression(self.linear.args + [self.bin]), ), (self.linear, self.zero, self.linear), (self.linear, self.one, LinearExpression(self.linear.args + [1])), @@ -697,11 +653,7 @@ def test_add_linear(self): LinearExpression(self.linear.args + [self.param_mut]), ), # 8: - ( - self.linear, - self.var, - LinearExpression(self.linear.args + [self.mon_var]), - ), + (self.linear, self.var, LinearExpression(self.linear.args + [self.var])), ( self.linear, self.mon_native, @@ -868,7 +820,7 @@ def test_add_mutable_l1(self): ( self.mutable_l1, self.asbinary, - LinearExpression(self.mutable_l1.args + [self.mon_bin]), + LinearExpression(self.mutable_l1.args + [self.bin]), ), (self.mutable_l1, self.zero, self.mon_npv), (self.mutable_l1, self.one, LinearExpression(self.mutable_l1.args + [1])), @@ -893,7 +845,7 @@ def test_add_mutable_l1(self): ( self.mutable_l1, self.var, - LinearExpression(self.mutable_l1.args + [self.mon_var]), + LinearExpression(self.mutable_l1.args + [self.var]), ), ( self.mutable_l1, @@ -1075,7 +1027,7 @@ def test_add_param0(self): def test_add_param1(self): tests = [ (self.param1, self.invalid, NotImplemented), - (self.param1, self.asbinary, LinearExpression([1, self.mon_bin])), + (self.param1, self.asbinary, LinearExpression([1, self.bin])), (self.param1, self.zero, 1), (self.param1, self.one, 2), # 4: @@ -1084,7 +1036,7 @@ def test_add_param1(self): (self.param1, self.param, 7), (self.param1, self.param_mut, NPV_SumExpression([1, self.param_mut])), # 8: - (self.param1, self.var, LinearExpression([1, self.mon_var])), + (self.param1, self.var, LinearExpression([1, self.var])), (self.param1, self.mon_native, LinearExpression([1, self.mon_native])), (self.param1, self.mon_param, LinearExpression([1, self.mon_param])), (self.param1, self.mon_npv, LinearExpression([1, self.mon_npv])), @@ -1114,7 +1066,7 @@ def test_add_mutable_l3(self): ( self.mutable_l3, self.asbinary, - LinearExpression(self.mutable_l3.args + [self.mon_bin]), + LinearExpression(self.mutable_l3.args + [self.bin]), ), (self.mutable_l3, self.zero, self.npv), (self.mutable_l3, self.one, NPV_SumExpression(self.mutable_l3.args + [1])), @@ -1143,7 +1095,7 @@ def test_add_mutable_l3(self): ( self.mutable_l3, self.var, - LinearExpression(self.mutable_l3.args + [self.mon_var]), + LinearExpression(self.mutable_l3.args + [self.var]), ), ( self.mutable_l3, @@ -1249,32 +1201,32 @@ def test_sub_asbinary(self): # BooleanVar objects do not support addition (self.asbinary, self.asbinary, NotImplemented), (self.asbinary, self.zero, self.bin), - (self.asbinary, self.one, LinearExpression([self.mon_bin, -1])), + (self.asbinary, self.one, LinearExpression([self.bin, -1])), # 4: - (self.asbinary, self.native, LinearExpression([self.mon_bin, -5])), - (self.asbinary, self.npv, LinearExpression([self.mon_bin, self.minus_npv])), - (self.asbinary, self.param, LinearExpression([self.mon_bin, -6])), + (self.asbinary, self.native, LinearExpression([self.bin, -5])), + (self.asbinary, self.npv, LinearExpression([self.bin, self.minus_npv])), + (self.asbinary, self.param, LinearExpression([self.bin, -6])), ( self.asbinary, self.param_mut, - LinearExpression([self.mon_bin, self.minus_param_mut]), + LinearExpression([self.bin, self.minus_param_mut]), ), # 8: - (self.asbinary, self.var, LinearExpression([self.mon_bin, self.minus_var])), + (self.asbinary, self.var, LinearExpression([self.bin, self.minus_var])), ( self.asbinary, self.mon_native, - LinearExpression([self.mon_bin, self.minus_mon_native]), + LinearExpression([self.bin, self.minus_mon_native]), ), ( self.asbinary, self.mon_param, - LinearExpression([self.mon_bin, self.minus_mon_param]), + LinearExpression([self.bin, self.minus_mon_param]), ), ( self.asbinary, self.mon_npv, - LinearExpression([self.mon_bin, self.minus_mon_npv]), + LinearExpression([self.bin, self.minus_mon_npv]), ), # 12: (self.asbinary, self.linear, SumExpression([self.bin, self.minus_linear])), @@ -1285,7 +1237,7 @@ def test_sub_asbinary(self): ( self.asbinary, self.mutable_l1, - LinearExpression([self.mon_bin, self.minus_mon_npv]), + LinearExpression([self.bin, self.minus_mon_npv]), ), ( self.asbinary, @@ -1293,12 +1245,12 @@ def test_sub_asbinary(self): SumExpression([self.bin, self.minus_mutable_l2]), ), (self.asbinary, self.param0, self.bin), - (self.asbinary, self.param1, LinearExpression([self.mon_bin, -1])), + (self.asbinary, self.param1, LinearExpression([self.bin, -1])), # 20: ( self.asbinary, self.mutable_l3, - LinearExpression([self.mon_bin, self.minus_npv]), + LinearExpression([self.bin, self.minus_npv]), ), ] self._run_cases(tests, operator.sub) @@ -1571,35 +1523,31 @@ def test_sub_param_mut(self): def test_sub_var(self): tests = [ (self.var, self.invalid, NotImplemented), - (self.var, self.asbinary, LinearExpression([self.mon_var, self.minus_bin])), + (self.var, self.asbinary, LinearExpression([self.var, self.minus_bin])), (self.var, self.zero, self.var), - (self.var, self.one, LinearExpression([self.mon_var, -1])), + (self.var, self.one, LinearExpression([self.var, -1])), # 4: - (self.var, self.native, LinearExpression([self.mon_var, -5])), - (self.var, self.npv, LinearExpression([self.mon_var, self.minus_npv])), - (self.var, self.param, LinearExpression([self.mon_var, -6])), + (self.var, self.native, LinearExpression([self.var, -5])), + (self.var, self.npv, LinearExpression([self.var, self.minus_npv])), + (self.var, self.param, LinearExpression([self.var, -6])), ( self.var, self.param_mut, - LinearExpression([self.mon_var, self.minus_param_mut]), + LinearExpression([self.var, self.minus_param_mut]), ), # 8: - (self.var, self.var, LinearExpression([self.mon_var, self.minus_var])), + (self.var, self.var, LinearExpression([self.var, self.minus_var])), ( self.var, self.mon_native, - LinearExpression([self.mon_var, self.minus_mon_native]), + LinearExpression([self.var, self.minus_mon_native]), ), ( self.var, self.mon_param, - LinearExpression([self.mon_var, self.minus_mon_param]), - ), - ( - self.var, - self.mon_npv, - LinearExpression([self.mon_var, self.minus_mon_npv]), + LinearExpression([self.var, self.minus_mon_param]), ), + (self.var, self.mon_npv, LinearExpression([self.var, self.minus_mon_npv])), # 12: ( self.var, @@ -1613,7 +1561,7 @@ def test_sub_var(self): ( self.var, self.mutable_l1, - LinearExpression([self.mon_var, self.minus_mon_npv]), + LinearExpression([self.var, self.minus_mon_npv]), ), ( self.var, @@ -1621,13 +1569,9 @@ def test_sub_var(self): SumExpression([self.var, self.minus_mutable_l2]), ), (self.var, self.param0, self.var), - (self.var, self.param1, LinearExpression([self.mon_var, -1])), + (self.var, self.param1, LinearExpression([self.var, -1])), # 20: - ( - self.var, - self.mutable_l3, - LinearExpression([self.mon_var, self.minus_npv]), - ), + (self.var, self.mutable_l3, LinearExpression([self.var, self.minus_npv])), ] self._run_cases(tests, operator.sub) self._run_cases(tests, operator.isub) @@ -6039,7 +5983,7 @@ def test_mutable_nvp_iadd(self): mutable_npv = _MutableNPVSumExpression([]) tests = [ (mutable_npv, self.invalid, NotImplemented), - (mutable_npv, self.asbinary, _MutableLinearExpression([self.mon_bin])), + (mutable_npv, self.asbinary, _MutableLinearExpression([self.bin])), (mutable_npv, self.zero, _MutableNPVSumExpression([])), (mutable_npv, self.one, _MutableNPVSumExpression([1])), # 4: @@ -6048,7 +5992,7 @@ def test_mutable_nvp_iadd(self): (mutable_npv, self.param, _MutableNPVSumExpression([6])), (mutable_npv, self.param_mut, _MutableNPVSumExpression([self.param_mut])), # 8: - (mutable_npv, self.var, _MutableLinearExpression([self.mon_var])), + (mutable_npv, self.var, _MutableLinearExpression([self.var])), (mutable_npv, self.mon_native, _MutableLinearExpression([self.mon_native])), (mutable_npv, self.mon_param, _MutableLinearExpression([self.mon_param])), (mutable_npv, self.mon_npv, _MutableLinearExpression([self.mon_npv])), @@ -6074,20 +6018,20 @@ def test_mutable_nvp_iadd(self): mutable_npv = _MutableNPVSumExpression([10]) tests = [ (mutable_npv, self.invalid, NotImplemented), - (mutable_npv, self.asbinary, _MutableLinearExpression([10, self.mon_bin])), + (mutable_npv, self.asbinary, _MutableLinearExpression([10, self.bin])), (mutable_npv, self.zero, _MutableNPVSumExpression([10])), - (mutable_npv, self.one, _MutableNPVSumExpression([10, 1])), + (mutable_npv, self.one, _MutableNPVSumExpression([11])), # 4: - (mutable_npv, self.native, _MutableNPVSumExpression([10, 5])), + (mutable_npv, self.native, _MutableNPVSumExpression([15])), (mutable_npv, self.npv, _MutableNPVSumExpression([10, self.npv])), - (mutable_npv, self.param, _MutableNPVSumExpression([10, 6])), + (mutable_npv, self.param, _MutableNPVSumExpression([16])), ( mutable_npv, self.param_mut, _MutableNPVSumExpression([10, self.param_mut]), ), # 8: - (mutable_npv, self.var, _MutableLinearExpression([10, self.mon_var])), + (mutable_npv, self.var, _MutableLinearExpression([10, self.var])), ( mutable_npv, self.mon_native, @@ -6120,7 +6064,7 @@ def test_mutable_nvp_iadd(self): _MutableSumExpression([10] + self.mutable_l2.args), ), (mutable_npv, self.param0, _MutableNPVSumExpression([10])), - (mutable_npv, self.param1, _MutableNPVSumExpression([10, 1])), + (mutable_npv, self.param1, _MutableNPVSumExpression([11])), # 20: (mutable_npv, self.mutable_l3, _MutableNPVSumExpression([10, self.npv])), ] @@ -6130,7 +6074,7 @@ def test_mutable_lin_iadd(self): mutable_lin = _MutableLinearExpression([]) tests = [ (mutable_lin, self.invalid, NotImplemented), - (mutable_lin, self.asbinary, _MutableLinearExpression([self.mon_bin])), + (mutable_lin, self.asbinary, _MutableLinearExpression([self.bin])), (mutable_lin, self.zero, _MutableLinearExpression([])), (mutable_lin, self.one, _MutableLinearExpression([1])), # 4: @@ -6139,7 +6083,7 @@ def test_mutable_lin_iadd(self): (mutable_lin, self.param, _MutableLinearExpression([6])), (mutable_lin, self.param_mut, _MutableLinearExpression([self.param_mut])), # 8: - (mutable_lin, self.var, _MutableLinearExpression([self.mon_var])), + (mutable_lin, self.var, _MutableLinearExpression([self.var])), (mutable_lin, self.mon_native, _MutableLinearExpression([self.mon_native])), (mutable_lin, self.mon_param, _MutableLinearExpression([self.mon_param])), (mutable_lin, self.mon_npv, _MutableLinearExpression([self.mon_npv])), @@ -6162,81 +6106,69 @@ def test_mutable_lin_iadd(self): ] self._run_iadd_cases(tests, operator.iadd) - mutable_lin = _MutableLinearExpression([self.mon_bin]) + mutable_lin = _MutableLinearExpression([self.bin]) tests = [ (mutable_lin, self.invalid, NotImplemented), ( mutable_lin, self.asbinary, - _MutableLinearExpression([self.mon_bin, self.mon_bin]), + _MutableLinearExpression([self.bin, self.bin]), ), - (mutable_lin, self.zero, _MutableLinearExpression([self.mon_bin])), - (mutable_lin, self.one, _MutableLinearExpression([self.mon_bin, 1])), + (mutable_lin, self.zero, _MutableLinearExpression([self.bin])), + (mutable_lin, self.one, _MutableLinearExpression([self.bin, 1])), # 4: - (mutable_lin, self.native, _MutableLinearExpression([self.mon_bin, 5])), - (mutable_lin, self.npv, _MutableLinearExpression([self.mon_bin, self.npv])), - (mutable_lin, self.param, _MutableLinearExpression([self.mon_bin, 6])), + (mutable_lin, self.native, _MutableLinearExpression([self.bin, 5])), + (mutable_lin, self.npv, _MutableLinearExpression([self.bin, self.npv])), + (mutable_lin, self.param, _MutableLinearExpression([self.bin, 6])), ( mutable_lin, self.param_mut, - _MutableLinearExpression([self.mon_bin, self.param_mut]), + _MutableLinearExpression([self.bin, self.param_mut]), ), # 8: - ( - mutable_lin, - self.var, - _MutableLinearExpression([self.mon_bin, self.mon_var]), - ), + (mutable_lin, self.var, _MutableLinearExpression([self.bin, self.var])), ( mutable_lin, self.mon_native, - _MutableLinearExpression([self.mon_bin, self.mon_native]), + _MutableLinearExpression([self.bin, self.mon_native]), ), ( mutable_lin, self.mon_param, - _MutableLinearExpression([self.mon_bin, self.mon_param]), + _MutableLinearExpression([self.bin, self.mon_param]), ), ( mutable_lin, self.mon_npv, - _MutableLinearExpression([self.mon_bin, self.mon_npv]), + _MutableLinearExpression([self.bin, self.mon_npv]), ), # 12: ( mutable_lin, self.linear, - _MutableLinearExpression([self.mon_bin] + self.linear.args), - ), - ( - mutable_lin, - self.sum, - _MutableSumExpression([self.mon_bin] + self.sum.args), - ), - ( - mutable_lin, - self.other, - _MutableSumExpression([self.mon_bin, self.other]), + _MutableLinearExpression([self.bin] + self.linear.args), ), - (mutable_lin, self.mutable_l0, _MutableLinearExpression([self.mon_bin])), + (mutable_lin, self.sum, _MutableSumExpression([self.bin] + self.sum.args)), + (mutable_lin, self.other, _MutableSumExpression([self.bin, self.other])), + (mutable_lin, self.mutable_l0, _MutableLinearExpression([self.bin])), # 16: ( mutable_lin, self.mutable_l1, - _MutableLinearExpression([self.mon_bin] + self.mutable_l1.args), + _MutableLinearExpression([self.bin] + self.mutable_l1.args), ), ( mutable_lin, self.mutable_l2, - _MutableSumExpression([self.mon_bin] + self.mutable_l2.args), + _MutableSumExpression([self.bin] + self.mutable_l2.args), ), - (mutable_lin, self.param0, _MutableLinearExpression([self.mon_bin])), - (mutable_lin, self.param1, _MutableLinearExpression([self.mon_bin, 1])), + (mutable_lin, self.param0, _MutableLinearExpression([self.bin])), + (mutable_lin, self.param1, _MutableLinearExpression([self.bin, 1])), # 20: ( mutable_lin, self.mutable_l3, - _MutableLinearExpression([self.mon_bin, self.npv]), + _MutableLinearExpression([self.bin, self.npv]), ), ] self._run_iadd_cases(tests, operator.iadd) diff --git a/pyomo/core/tests/unit/test_numpy_expr.py b/pyomo/core/tests/unit/test_numpy_expr.py index df20f30f9b4..fb81dfe809f 100644 --- a/pyomo/core/tests/unit/test_numpy_expr.py +++ b/pyomo/core/tests/unit/test_numpy_expr.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -29,7 +29,7 @@ Reals, ) from pyomo.core.expr import MonomialTermExpression -from pyomo.core.expr.numeric_expr import NumericNDArray +from pyomo.core.expr.ndarray import NumericNDArray from pyomo.core.expr.numvalue import as_numeric from pyomo.core.expr.compare import compare_expressions from pyomo.core.expr.relational_expr import InequalityExpression diff --git a/pyomo/core/tests/unit/test_numvalue.py b/pyomo/core/tests/unit/test_numvalue.py index 0f9e42f552a..1cccd3863ea 100644 --- a/pyomo/core/tests/unit/test_numvalue.py +++ b/pyomo/core/tests/unit/test_numvalue.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -12,8 +12,13 @@ # Unit Tests for Python numeric values # +import subprocess +import sys from math import nan, inf + import pyomo.common.unittest as unittest +from pyomo.common.dependencies import numpy, numpy_available +from pyomo.core.base.units_container import pint_available from pyomo.environ import ( value, @@ -38,13 +43,6 @@ ) from pyomo.common.numeric_types import _native_boolean_types -try: - import numpy - - numpy_available = True -except: - numpy_available = False - class MyBogusType(object): def __init__(self, val=0): @@ -53,7 +51,16 @@ def __init__(self, val=0): class MyBogusNumericType(MyBogusType): def __add__(self, other): - return MyBogusNumericType(self.val + float(other)) + if other.__class__ in native_numeric_types: + return MyBogusNumericType(self.val + float(other)) + else: + return NotImplemented + + def __le__(self, other): + if other.__class__ in native_numeric_types: + return self.val <= float(other) + else: + return NotImplemented def __lt__(self, other): return self.val < float(other) @@ -537,34 +544,109 @@ def test_unknownNumericType(self): try: val = as_numeric(ref) self.assertEqual(val().val, 42.0) + self.assertIn(MyBogusNumericType, native_numeric_types) + self.assertIn(MyBogusNumericType, native_types) finally: native_numeric_types.remove(MyBogusNumericType) native_types.remove(MyBogusNumericType) + @unittest.skipUnless(numpy_available, "This test requires NumPy") def test_numpy_basic_float_registration(self): - if not numpy_available: - self.skipTest("This test requires NumPy") self.assertIn(numpy.float_, native_numeric_types) self.assertNotIn(numpy.float_, native_integer_types) self.assertIn(numpy.float_, _native_boolean_types) self.assertIn(numpy.float_, native_types) + @unittest.skipUnless(numpy_available, "This test requires NumPy") def test_numpy_basic_int_registration(self): - if not numpy_available: - self.skipTest("This test requires NumPy") self.assertIn(numpy.int_, native_numeric_types) self.assertIn(numpy.int_, native_integer_types) self.assertIn(numpy.int_, _native_boolean_types) self.assertIn(numpy.int_, native_types) + @unittest.skipUnless(numpy_available, "This test requires NumPy") def test_numpy_basic_bool_registration(self): - if not numpy_available: - self.skipTest("This test requires NumPy") self.assertNotIn(numpy.bool_, native_numeric_types) self.assertNotIn(numpy.bool_, native_integer_types) self.assertIn(numpy.bool_, _native_boolean_types) self.assertIn(numpy.bool_, native_types) + @unittest.skipUnless(numpy_available, "This test requires NumPy") + def test_automatic_numpy_registration(self): + cmd = ( + 'from pyomo.common.numeric_types import native_numeric_types as nnt; ' + 'print("float64" in [_.__name__ for _ in nnt]); ' + 'import numpy; ' + 'print("float64" in [_.__name__ for _ in nnt])' + ) + + rc = subprocess.run( + [sys.executable, '-c', cmd], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + self.assertEqual((rc.returncode, rc.stdout), (0, "False\nTrue\n")) + + cmd = ( + 'import numpy; ' + 'from pyomo.common.numeric_types import native_numeric_types as nnt; ' + 'print("float64" in [_.__name__ for _ in nnt])' + ) + + rc = subprocess.run( + [sys.executable, '-c', cmd], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + self.assertEqual((rc.returncode, rc.stdout), (0, "True\n")) + + def test_unknownNumericType_expr_registration(self): + cmd = ( + 'import pyomo; ' + 'from pyomo.core.base import Var, Param; ' + 'from pyomo.core.base.units_container import units; ' + 'from pyomo.common.numeric_types import native_numeric_types as nnt; ' + f'from {__name__} import MyBogusNumericType; ' + 'ref = MyBogusNumericType(42); ' + 'print(MyBogusNumericType in nnt); %s; print(MyBogusNumericType in nnt); ' + ) + + def _tester(expr): + rc = subprocess.run( + [sys.executable, '-c', cmd % expr], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + self.assertEqual( + (rc.returncode, rc.stdout), + ( + 0, + '''False +WARNING: Dynamically registering the following numeric type: + pyomo.core.tests.unit.test_numvalue.MyBogusNumericType + Dynamic registration is supported for convenience, but there are known + limitations to this approach. We recommend explicitly registering numeric + types using RegisterNumericType() or RegisterIntegerType(). +True +''', + ), + ) + + _tester('Var() <= ref') + _tester('ref <= Var()') + _tester('ref + Var()') + _tester('Var() + ref') + _tester('v = Var(); v.construct(); v.value = ref') + _tester('p = Param(mutable=True); p.construct(); p.value = ref') + if pint_available: + _tester('v = Var(units=units.m); v.construct(); v.value = ref') + _tester( + 'p = Param(mutable=True, units=units.m); p.construct(); p.value = ref' + ) + if __name__ == "__main__": unittest.main() diff --git a/pyomo/core/tests/unit/test_obj.py b/pyomo/core/tests/unit/test_obj.py index d73bf7d6dfd..dc2e320e63b 100644 --- a/pyomo/core/tests/unit/test_obj.py +++ b/pyomo/core/tests/unit/test_obj.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -78,7 +78,7 @@ def test_empty_singleton(self): # Even though we construct a ScalarObjective, # if it is not initialized that means it is "empty" # and we should encounter errors when trying to access the - # _ObjectiveData interface methods until we assign + # ObjectiveData interface methods until we assign # something to the objective. # self.assertEqual(a._constructed, True) diff --git a/pyomo/core/tests/unit/test_param.py b/pyomo/core/tests/unit/test_param.py index 6ba1163e3c3..f22674b6bf7 100644 --- a/pyomo/core/tests/unit/test_param.py +++ b/pyomo/core/tests/unit/test_param.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -65,8 +65,8 @@ from pyomo.common.errors import PyomoException from pyomo.common.log import LoggingIntercept from pyomo.common.tempfiles import TempfileManager -from pyomo.core.base.param import _ParamData -from pyomo.core.base.set import _SetData +from pyomo.core.base.param import ParamData +from pyomo.core.base.set import SetData from pyomo.core.base.units_container import units, pint_available, UnitsError from io import StringIO @@ -181,7 +181,7 @@ def test_setitem_preexisting(self): idx = sorted(keys)[0] self.assertEqual(value(self.instance.A[idx]), self.data[idx]) if self.instance.A.mutable: - self.assertTrue(isinstance(self.instance.A[idx], _ParamData)) + self.assertTrue(isinstance(self.instance.A[idx], ParamData)) else: self.assertEqual(type(self.instance.A[idx]), float) @@ -190,7 +190,7 @@ def test_setitem_preexisting(self): if not self.instance.A.mutable: self.fail("Expected setitem[%s] to fail for immutable Params" % (idx,)) self.assertEqual(value(self.instance.A[idx]), 4.3) - self.assertTrue(isinstance(self.instance.A[idx], _ParamData)) + self.assertTrue(isinstance(self.instance.A[idx], ParamData)) except TypeError: # immutable Params should raise a TypeError exception if self.instance.A.mutable: @@ -249,7 +249,7 @@ def test_setitem_default_override(self): self.assertEqual(value(self.instance.A[idx]), self.instance.A._default_val) if self.instance.A.mutable: - self.assertIsInstance(self.instance.A[idx], _ParamData) + self.assertIsInstance(self.instance.A[idx], ParamData) else: self.assertEqual( type(self.instance.A[idx]), type(value(self.instance.A._default_val)) @@ -260,7 +260,7 @@ def test_setitem_default_override(self): if not self.instance.A.mutable: self.fail("Expected setitem[%s] to fail for immutable Params" % (idx,)) self.assertEqual(self.instance.A[idx].value, 4.3) - self.assertIsInstance(self.instance.A[idx], _ParamData) + self.assertIsInstance(self.instance.A[idx], ParamData) except TypeError: # immutable Params should raise a TypeError exception if self.instance.A.mutable: @@ -1487,7 +1487,7 @@ def test_domain_set_initializer(self): m.I = Set(initialize=[1, 2, 3]) param_vals = {1: 1, 2: 1, 3: -1} m.p = Param(m.I, initialize=param_vals, domain={-1, 1}) - self.assertIsInstance(m.p.domain, _SetData) + self.assertIsInstance(m.p.domain, SetData) @unittest.skipUnless(pint_available, "units test requires pint module") def test_set_value_units(self): diff --git a/pyomo/core/tests/unit/test_pickle.py b/pyomo/core/tests/unit/test_pickle.py index 808db2e45f3..fccc92bbfa2 100644 --- a/pyomo/core/tests/unit/test_pickle.py +++ b/pyomo/core/tests/unit/test_pickle.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -35,7 +35,7 @@ ) -using_pypy = platform.python_implementation() == "PyPy" +is_pypy = platform.python_implementation().lower().startswith("pypy") def obj_rule(model): @@ -322,7 +322,7 @@ def rule2(model, i): model.con = Constraint(rule=rule1) model.con2 = Constraint(model.a, rule=rule2) instance = model.create_instance() - if using_pypy: + if is_pypy: str_ = pickle.dumps(instance) tmp_ = pickle.loads(str_) else: diff --git a/pyomo/core/tests/unit/test_piecewise.py b/pyomo/core/tests/unit/test_piecewise.py index aeb02b82624..7b8e01e6a45 100644 --- a/pyomo/core/tests/unit/test_piecewise.py +++ b/pyomo/core/tests/unit/test_piecewise.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -104,7 +104,7 @@ def test_indexed_with_nonindexed_vars(self): model.con3 = Piecewise(*args, **keywords) # test that nonindexed Piecewise can handle - # _VarData (e.g model.x[1] + # VarData (e.g model.x[1] def test_nonindexed_with_indexed_vars(self): model = ConcreteModel() model.range = Var([1]) diff --git a/pyomo/core/tests/unit/test_preprocess.py b/pyomo/core/tests/unit/test_preprocess.py index d4c5ae75bb0..ce7924f3ac5 100644 --- a/pyomo/core/tests/unit/test_preprocess.py +++ b/pyomo/core/tests/unit/test_preprocess.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_range.py b/pyomo/core/tests/unit/test_range.py index 8cd1e7ce46c..4b489f50d44 100644 --- a/pyomo/core/tests/unit/test_range.py +++ b/pyomo/core/tests/unit/test_range.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_reference.py b/pyomo/core/tests/unit/test_reference.py index a7a470b1a3b..7370881612f 100644 --- a/pyomo/core/tests/unit/test_reference.py +++ b/pyomo/core/tests/unit/test_reference.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -729,7 +729,6 @@ def test_component_data_reference(self): self.assertIs(m.r.ctype, Var) self.assertIsNot(m.r.index_set(), m.y.index_set()) - self.assertIs(m.y.index_set(), m.y_index) self.assertIs(m.r.index_set(), UnindexedComponent_ReferenceSet) self.assertEqual(len(m.r), 1) self.assertTrue(m.r.is_reference()) @@ -773,7 +772,7 @@ def test_reference_var_pprint(self): m.r.pprint(ostream=buf) self.assertEqual( buf.getvalue(), - """r : Size=2, Index=x_index, ReferenceTo=x + """r : Size=2, Index={1, 2}, ReferenceTo=x Key : Lower : Value : Upper : Fixed : Stale : Domain 1 : None : 4 : None : False : False : Reals 2 : None : 8 : None : False : False : Reals @@ -784,7 +783,7 @@ def test_reference_var_pprint(self): m.s.pprint(ostream=buf) self.assertEqual( buf.getvalue(), - """s : Size=2, Index=x_index, ReferenceTo=x[:, ...] + """s : Size=2, Index={1, 2}, ReferenceTo=x[:, ...] Key : Lower : Value : Upper : Fixed : Stale : Domain 1 : None : 4 : None : False : False : Reals 2 : None : 8 : None : False : False : Reals @@ -799,10 +798,10 @@ def test_reference_indexedcomponent_pprint(self): m.r.pprint(ostream=buf) self.assertEqual( buf.getvalue(), - """r : Size=2, Index=x_index, ReferenceTo=x + """r : Size=2, Index={1, 2}, ReferenceTo=x Key : Object - 1 : - 2 : + 1 : + 2 : """, ) m.s = Reference(m.x[:, ...], ctype=IndexedComponent) @@ -810,10 +809,10 @@ def test_reference_indexedcomponent_pprint(self): m.s.pprint(ostream=buf) self.assertEqual( buf.getvalue(), - """s : Size=2, Index=x_index, ReferenceTo=x[:, ...] + """s : Size=2, Index={1, 2}, ReferenceTo=x[:, ...] Key : Object - 1 : - 2 : + 1 : + 2 : """, ) @@ -1281,7 +1280,6 @@ def test_contains_with_nonflattened(self): normalize_index.flatten = _old_flatten def test_pprint_nonfinite_sets(self): - self.maxDiff = None m = ConcreteModel() m.v = Var(NonNegativeIntegers, dense=False) m.ref = Reference(m.v) @@ -1323,7 +1321,6 @@ def test_pprint_nonfinite_sets(self): def test_pprint_nonfinite_sets_ctypeNone(self): # test issue #2039 - self.maxDiff = None m = ConcreteModel() m.v = Var(NonNegativeIntegers, dense=False) m.ref = Reference(m.v, ctype=None) @@ -1360,8 +1357,8 @@ def test_pprint_nonfinite_sets_ctypeNone(self): 1 IndexedComponent Declarations ref : Size=2, Index=NonNegativeIntegers, ReferenceTo=v Key : Object - 3 : - 5 : + 3 : + 5 : 2 Declarations: v ref """.strip(), @@ -1380,7 +1377,7 @@ def b(b, i): self.assertEqual( buf.getvalue().strip(), """ -r : Size=4, Index=r_index, ReferenceTo=b[:].x[:] +r : Size=4, Index=ReferenceSet(b[:].x[:]), ReferenceTo=b[:].x[:] Key : Lower : Value : Upper : Fixed : Stale : Domain (1, 3) : 1 : None : None : False : True : Reals (1, 4) : 1 : None : None : False : True : Reals diff --git a/pyomo/core/tests/unit/test_relational_expr.py b/pyomo/core/tests/unit/test_relational_expr.py index f55bfff108c..d361bfcc83c 100644 --- a/pyomo/core/tests/unit/test_relational_expr.py +++ b/pyomo/core/tests/unit/test_relational_expr.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index 4263bdef153..f62589a6873 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -60,8 +60,8 @@ FiniteSetOf, InfiniteSetOf, RangeSet, - _FiniteRangeSetData, - _InfiniteRangeSetData, + FiniteRangeSetData, + InfiniteRangeSetData, FiniteScalarRangeSet, InfiniteScalarRangeSet, AbstractFiniteScalarRangeSet, @@ -81,10 +81,10 @@ SetProduct_InfiniteSet, SetProduct_FiniteSet, SetProduct_OrderedSet, - _SetData, - _FiniteSetData, - _InsertionOrderSetData, - _SortedSetData, + SetData, + FiniteSetData, + InsertionOrderSetData, + SortedSetData, _FiniteSetMixin, _OrderedSetMixin, SetInitializer, @@ -112,17 +112,19 @@ class Test_SetInitializer(unittest.TestCase): def test_single_set(self): + tmp = Set() # a placeholder to accumulate _anonymous_sets references + a = SetInitializer(None) self.assertIs(type(a), SetInitializer) self.assertIsNone(a._set) - self.assertIs(a(None, None), Any) + self.assertIs(a(None, None, tmp), Any) self.assertTrue(a.constant()) self.assertFalse(a.verified) a = SetInitializer(Reals) self.assertIs(type(a), SetInitializer) self.assertIs(type(a._set), ConstantInitializer) - self.assertIs(a(None, None), Reals) + self.assertIs(a(None, None, tmp), Reals) self.assertIs(a._set.val, Reals) self.assertTrue(a.constant()) self.assertFalse(a.verified) @@ -130,18 +132,20 @@ def test_single_set(self): a = SetInitializer({1: Reals}) self.assertIs(type(a), SetInitializer) self.assertIs(type(a._set), ItemInitializer) - self.assertIs(a(None, 1), Reals) + self.assertIs(a(None, 1, tmp), Reals) self.assertFalse(a.constant()) self.assertFalse(a.verified) def test_intersect(self): + tmp = Set() # a placeholder to accumulate _anonymous_sets references + a = SetInitializer(None) a.intersect(SetInitializer(None)) self.assertIs(type(a), SetInitializer) self.assertIsNone(a._set) self.assertTrue(a.constant()) self.assertFalse(a.verified) - self.assertIs(a(None, None), Any) + self.assertIs(a(None, None, tmp), Any) a = SetInitializer(None) a.intersect(SetInitializer(Reals)) @@ -150,7 +154,7 @@ def test_intersect(self): self.assertIs(a._set.val, Reals) self.assertTrue(a.constant()) self.assertFalse(a.verified) - self.assertIs(a(None, None), Reals) + self.assertIs(a(None, None, tmp), Reals) a = SetInitializer(None) a.intersect(BoundsInitializer(5, default_step=1)) @@ -158,7 +162,7 @@ def test_intersect(self): self.assertIs(type(a._set), BoundsInitializer) self.assertTrue(a.constant()) self.assertFalse(a.verified) - self.assertEqual(a(None, None), RangeSet(5)) + self.assertEqual(a(None, None, tmp), RangeSet(5)) a = SetInitializer(Reals) a.intersect(SetInitializer(None)) @@ -167,7 +171,7 @@ def test_intersect(self): self.assertIs(a._set.val, Reals) self.assertTrue(a.constant()) self.assertFalse(a.verified) - self.assertIs(a(None, None), Reals) + self.assertIs(a(None, None, tmp), Reals) a = SetInitializer(Reals) a.intersect(SetInitializer(Integers)) @@ -179,7 +183,7 @@ def test_intersect(self): self.assertIs(a._set._B.val, Integers) self.assertTrue(a.constant()) self.assertFalse(a.verified) - s = a(None, None) + s = a(None, None, tmp) self.assertIs(type(s), SetIntersection_InfiniteSet) self.assertIs(s._sets[0], Reals) self.assertIs(s._sets[1], Integers) @@ -195,7 +199,7 @@ def test_intersect(self): self.assertIs(a._set._A._B.val, Integers) self.assertTrue(a.constant()) self.assertFalse(a.verified) - s = a(None, None) + s = a(None, None, tmp) self.assertIs(type(s), SetIntersection_OrderedSet) self.assertIs(type(s._sets[0]), SetIntersection_InfiniteSet) self.assertIsInstance(s._sets[1], RangeSet) @@ -212,7 +216,7 @@ def test_intersect(self): self.assertIs(a._set._A._B.val, Integers) self.assertTrue(a.constant()) self.assertFalse(a.verified) - s = a(None, None) + s = a(None, None, tmp) self.assertIs(type(s), SetIntersection_InfiniteSet) p.construct() s.construct() @@ -236,8 +240,8 @@ def test_intersect(self): self.assertFalse(a.constant()) self.assertFalse(a.verified) with self.assertRaises(KeyError): - a(None, None) - s = a(None, 1) + a(None, None, tmp) + s = a(None, 1, tmp) self.assertIs(type(s), SetIntersection_InfiniteSet) p.construct() s.construct() @@ -304,15 +308,17 @@ def test_boundsinit(self): self.assertEqual(s, RangeSet(0, 5)) def test_setdefault(self): + tmp = Set() # a placeholder to accumulate _anonymous_sets references + a = SetInitializer(None) - self.assertIs(a(None, None), Any) + self.assertIs(a(None, None, tmp), Any) a.setdefault(Reals) - self.assertIs(a(None, None), Reals) + self.assertIs(a(None, None, tmp), Reals) a = SetInitializer(Integers) - self.assertIs(a(None, None), Integers) + self.assertIs(a(None, None, tmp), Integers) a.setdefault(Reals) - self.assertIs(a(None, None), Integers) + self.assertIs(a(None, None, tmp), Integers) a = BoundsInitializer(5, default_step=1) self.assertEqual(a(None, None), RangeSet(5)) @@ -321,9 +327,9 @@ def test_setdefault(self): a = SetInitializer(Reals) a.intersect(SetInitializer(Integers)) - self.assertIs(type(a(None, None)), SetIntersection_InfiniteSet) + self.assertIs(type(a(None, None, tmp)), SetIntersection_InfiniteSet) a.setdefault(RangeSet(5)) - self.assertIs(type(a(None, None)), SetIntersection_InfiniteSet) + self.assertIs(type(a(None, None, tmp)), SetIntersection_InfiniteSet) def test_indices(self): a = SetInitializer(None) @@ -993,9 +999,7 @@ def __ge__(self, other): output = StringIO() with LoggingIntercept(output, 'pyomo.core', logging.DEBUG): i = SetOf([1, 2, 3]) - self.assertEqual(output.getvalue(), "") - i.construct() - ref = 'Constructing SetOf, name=OrderedSetOf, from data=None\n' + ref = 'Constructing SetOf, name=[1, 2, 3], from data=None\n' self.assertEqual(output.getvalue(), ref) # Calling construct() twice bypasses construction the second # time around @@ -1238,6 +1242,10 @@ def __len__(self): # Test types that cannot be case to set self.assertNotEqual(SetOf({3}), 3) + # Test floats + self.assertEqual(RangeSet(0.0, 2.0), RangeSet(0.0, 2.0)) + self.assertEqual(RangeSet(0.0, 2.0), RangeSet(0, 2)) + def test_inequality(self): self.assertTrue(SetOf([1, 2, 3]) <= SetOf({1, 2, 3})) self.assertFalse(SetOf([1, 2, 3]) < SetOf({1, 2, 3})) @@ -1277,19 +1285,19 @@ def test_is_functions(self): self.assertTrue(i.isdiscrete()) self.assertTrue(i.isfinite()) self.assertTrue(i.isordered()) - self.assertIsInstance(i, _FiniteRangeSetData) + self.assertIsInstance(i, FiniteRangeSetData) i = RangeSet(1, 3) self.assertTrue(i.isdiscrete()) self.assertTrue(i.isfinite()) self.assertTrue(i.isordered()) - self.assertIsInstance(i, _FiniteRangeSetData) + self.assertIsInstance(i, FiniteRangeSetData) i = RangeSet(1, 3, 0) self.assertFalse(i.isdiscrete()) self.assertFalse(i.isfinite()) self.assertFalse(i.isordered()) - self.assertIsInstance(i, _InfiniteRangeSetData) + self.assertIsInstance(i, InfiniteRangeSetData) def test_pprint(self): m = ConcreteModel() @@ -1530,8 +1538,8 @@ def test_ordered_setof(self): self.assertEqual(i[-1], 0) with self.assertRaisesRegex( IndexError, - "valid index values for Sets are " - r"\[1 .. len\(Set\)\] or \[-1 .. -len\(Set\)\]", + "Accessing Pyomo Sets by position is 1-based: valid Set positional " + r"index values are \[1 .. len\(Set\)\] or \[-1 .. -len\(Set\)\]", ): i[0] with self.assertRaisesRegex(IndexError, "OrderedSetOf index out of range"): @@ -1589,8 +1597,8 @@ def test_ordered_setof(self): self.assertEqual(i[-1], 0) with self.assertRaisesRegex( IndexError, - "valid index values for Sets are " - r"\[1 .. len\(Set\)\] or \[-1 .. -len\(Set\)\]", + "Accessing Pyomo Sets by position is 1-based: valid Set positional " + r"index values are \[1 .. len\(Set\)\] or \[-1 .. -len\(Set\)\]", ): i[0] with self.assertRaisesRegex(IndexError, "OrderedSetOf index out of range"): @@ -1752,8 +1760,8 @@ def test_ord_index(self): self.assertEqual(r[i + 1], v) with self.assertRaisesRegex( IndexError, - "valid index values for Sets are " - r"\[1 .. len\(Set\)\] or \[-1 .. -len\(Set\)\]", + "Accessing Pyomo Sets by position is 1-based: valid Set positional " + r"index values are \[1 .. len\(Set\)\] or \[-1 .. -len\(Set\)\]", ): r[0] with self.assertRaisesRegex( @@ -1769,8 +1777,8 @@ def test_ord_index(self): self.assertEqual(r[i + 1], v) with self.assertRaisesRegex( IndexError, - "valid index values for Sets are " - r"\[1 .. len\(Set\)\] or \[-1 .. -len\(Set\)\]", + "Accessing Pyomo Sets by position is 1-based: valid Set positional " + r"index values are \[1 .. len\(Set\)\] or \[-1 .. -len\(Set\)\]", ): r[0] with self.assertRaisesRegex( @@ -1811,7 +1819,7 @@ def test_check_values(self): class Test_SetOperator(unittest.TestCase): def test_construct(self): p = Param(initialize=3) - a = RangeSet(p) + a = RangeSet(p, name='a') output = StringIO() with LoggingIntercept(output, 'pyomo.core', logging.DEBUG): i = a * a @@ -1820,12 +1828,8 @@ def test_construct(self): with LoggingIntercept(output, 'pyomo.core', logging.DEBUG): i.construct() ref = ( - 'Constructing SetOperator, name=SetProduct_OrderedSet, ' - 'from data=None\n' - 'Constructing RangeSet, name=FiniteScalarRangeSet, ' - 'from data=None\n' - 'Constructing Set, name=SetProduct_OrderedSet, ' - 'from data=None\n' + 'Constructing SetOperator, name=a*a, from data=None\n' + 'Constructing RangeSet, name=a, from data=None\n' ) self.assertEqual(output.getvalue(), ref) # Calling construct() twice bypasses construction the second @@ -1937,8 +1941,8 @@ def test_domain_and_pprint(self): m.A.pprint(ostream=output) ref = """ A : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 1 : I | A_index_0 : 4 : {1, 2, 3, 4} + Key : Dimen : Domain : Size : Members + None : 1 : I | {3, 4} : 4 : {1, 2, 3, 4} """.strip() self.assertEqual(output.getvalue().strip(), ref) @@ -2213,8 +2217,8 @@ def test_domain_and_pprint(self): m.A.pprint(ostream=output) ref = """ A : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 1 : I & A_index_0 : 0 : {} + Key : Dimen : Domain : Size : Members + None : 1 : I & {3, 4} : 0 : {} """.strip() self.assertEqual(output.getvalue().strip(), ref) @@ -2491,8 +2495,8 @@ def test_domain_and_pprint(self): m.A.pprint(ostream=output) ref = """ A : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 1 : I - A_index_0 : 2 : {1, 2} + Key : Dimen : Domain : Size : Members + None : 1 : I - {3, 4} : 2 : {1, 2} """.strip() self.assertEqual(output.getvalue().strip(), ref) @@ -2647,6 +2651,34 @@ def test_infinite_setdifference(self): list(RangeSet(ranges=[NR(0, 2, 0, (True, False))]).ranges()), ) + x = RangeSet(0, 6, 0) - RangeSet(1, 5, 2) + self.assertIs(type(x), SetDifference_InfiniteSet) + self.assertFalse(x.isfinite()) + self.assertFalse(x.isordered()) + + self.assertIn(0, x) + self.assertNotIn(1, x) + self.assertIn(2, x) + self.assertNotIn(3, x) + self.assertIn(4, x) + self.assertNotIn(5, x) + self.assertIn(6, x) + self.assertNotIn(7, x) + + self.assertEqual( + list(x.ranges()), + list( + RangeSet( + ranges=[ + NR(0, 1, 0, (True, False)), + NR(1, 3, 0, (False, False)), + NR(3, 5, 0, (False, False)), + NR(5, 6, 0, (False, True)), + ] + ).ranges() + ), + ) + class TestSetSymmetricDifference(unittest.TestCase): def test_pickle(self): @@ -2692,8 +2724,8 @@ def test_domain_and_pprint(self): m.A.pprint(ostream=output) ref = """ A : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 1 : I ^ A_index_0 : 4 : {1, 2, 3, 4} + Key : Dimen : Domain : Size : Members + None : 1 : I ^ {3, 4} : 4 : {1, 2, 3, 4} """.strip() self.assertEqual(output.getvalue().strip(), ref) @@ -2954,8 +2986,8 @@ def test_domain_and_pprint(self): m.A.pprint(ostream=output) ref = """ A : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : I*A_index_0 : 4 : {(1, 3), (1, 4), (2, 3), (2, 4)} + Key : Dimen : Domain : Size : Members + None : 2 : I*{3, 4} : 4 : {(1, 3), (1, 4), (2, 3), (2, 4)} """.strip() self.assertEqual(output.getvalue().strip(), ref) @@ -3070,7 +3102,7 @@ def test_no_normalize_index(self): x = I * J normalize_index.flatten = False - self.assertIs(x.dimen, None) + self.assertIs(x.dimen, 2) self.assertIn(((1, 2), 3), x) self.assertIn((1, (2, 3)), x) # if we are not flattening, then lookup must match the @@ -3245,7 +3277,7 @@ def test_ordered_multidim_setproduct(self): ((3, 4), (7, 8)), ] self.assertEqual(list(x), ref) - self.assertEqual(x.dimen, None) + self.assertEqual(x.dimen, 2) finally: SetModule.FLATTEN_CROSS_PRODUCT = origFlattenCross @@ -3289,7 +3321,7 @@ def test_ordered_nondim_setproduct(self): (1, (2, 3), 5), ] self.assertEqual(list(x), ref) - self.assertEqual(x.dimen, None) + self.assertEqual(x.dimen, 3) finally: SetModule.FLATTEN_CROSS_PRODUCT = origFlattenCross @@ -3341,7 +3373,7 @@ def test_ordered_nondim_setproduct(self): self.assertEqual(list(x), ref) for i, v in enumerate(ref): self.assertEqual(x[i + 1], v) - self.assertEqual(x.dimen, None) + self.assertEqual(x.dimen, 4) finally: SetModule.FLATTEN_CROSS_PRODUCT = origFlattenCross @@ -4105,9 +4137,9 @@ def test_indexed_set(self): self.assertFalse(m.I[1].isordered()) self.assertFalse(m.I[2].isordered()) self.assertFalse(m.I[3].isordered()) - self.assertIs(type(m.I[1]), _FiniteSetData) - self.assertIs(type(m.I[2]), _FiniteSetData) - self.assertIs(type(m.I[3]), _FiniteSetData) + self.assertIs(type(m.I[1]), FiniteSetData) + self.assertIs(type(m.I[2]), FiniteSetData) + self.assertIs(type(m.I[3]), FiniteSetData) self.assertEqual(m.I.data(), {1: (1,), 2: (2,), 3: (4,)}) # Explicit (constant) construction @@ -4123,9 +4155,9 @@ def test_indexed_set(self): self.assertTrue(m.I[1].isordered()) self.assertTrue(m.I[2].isordered()) self.assertTrue(m.I[3].isordered()) - self.assertIs(type(m.I[1]), _InsertionOrderSetData) - self.assertIs(type(m.I[2]), _InsertionOrderSetData) - self.assertIs(type(m.I[3]), _InsertionOrderSetData) + self.assertIs(type(m.I[1]), InsertionOrderSetData) + self.assertIs(type(m.I[2]), InsertionOrderSetData) + self.assertIs(type(m.I[3]), InsertionOrderSetData) self.assertEqual(m.I.data(), {1: (4, 2, 5), 2: (4, 2, 5), 3: (4, 2, 5)}) # Explicit (constant) construction @@ -4141,9 +4173,9 @@ def test_indexed_set(self): self.assertTrue(m.I[1].isordered()) self.assertTrue(m.I[2].isordered()) self.assertTrue(m.I[3].isordered()) - self.assertIs(type(m.I[1]), _SortedSetData) - self.assertIs(type(m.I[2]), _SortedSetData) - self.assertIs(type(m.I[3]), _SortedSetData) + self.assertIs(type(m.I[1]), SortedSetData) + self.assertIs(type(m.I[2]), SortedSetData) + self.assertIs(type(m.I[3]), SortedSetData) self.assertEqual(m.I.data(), {1: (2, 4, 5), 2: (2, 4, 5), 3: (2, 4, 5)}) # Explicit (procedural) construction @@ -4191,10 +4223,12 @@ def test_indexing(self): m.I = [1, 3, 2] self.assertEqual(m.I[2], 3) with self.assertRaisesRegex( - IndexError, "I indices must be integers, not float" + IndexError, "Set 'I' positional indices must be integers, not float" ): m.I[2.5] - with self.assertRaisesRegex(IndexError, "I indices must be integers, not str"): + with self.assertRaisesRegex( + IndexError, "Set 'I' positional indices must be integers, not str" + ): m.I['a'] def test_add_filter_validate(self): @@ -4266,7 +4300,7 @@ def _l_tri(model, i, j): # This tests a filter that matches the dimentionality of the # component. construct() needs to recognize that the filter is # returning a constant in construct() and re-assign it to be the - # _filter for each _SetData + # _filter for each SetData def _lt_3(model, i): self.assertIs(model, m) return i < 3 @@ -4376,17 +4410,17 @@ def test_domain(self): self.assertEqual(list(m.I), [0, 2.0, 4]) with self.assertRaisesRegex( ValueError, - 'The value is not in the domain ' r'\(Integers & I_domain_index_0_index_1', + r'The value is not in the domain \(Integers & \[0:inf:2\]\) & \[0..9\]', ): m.I.add(1.5) with self.assertRaisesRegex( ValueError, - 'The value is not in the domain ' r'\(Integers & I_domain_index_0_index_1', + r'The value is not in the domain \(Integers & \[0:inf:2\]\) & \[0..9\]', ): m.I.add(1) with self.assertRaisesRegex( ValueError, - 'The value is not in the domain ' r'\(Integers & I_domain_index_0_index_1', + r'The value is not in the domain \(Integers & \[0:inf:2\]\) & \[0..9\]', ): m.I.add(10) @@ -4424,8 +4458,8 @@ def myFcn(x): Key : Dimen : Domain : Size : Members None : 2 : Any : 2 : {(3, 4), (1, 2)} M : Size=1, Index=None, Ordered=False - Key : Dimen : Domain : Size : Members - None : 1 : Reals - M_index_1 : Inf : ([-inf..0) | (0..inf]) + Key : Dimen : Domain : Size : Members + None : 1 : Reals - [0] : Inf : ([-inf..0) | (0..inf]) N : Size=1, Index=None, Ordered=False Key : Dimen : Domain : Size : Members None : 1 : Integers - Reals : Inf : [] @@ -4435,12 +4469,7 @@ def myFcn(x): Key : Finite : Members None : True : [1:3] -1 SetOf Declarations - M_index_1 : Dimen=1, Size=1, Bounds=(0, 0) - Key : Ordered : Members - None : True : [0] - -8 Declarations: I_index I J K L M_index_1 M N""".strip(), +7 Declarations: I_index I J K L M N""".strip(), ) def test_pickle(self): @@ -4526,11 +4555,11 @@ def test_construction(self): ref = """ I : Size=0, Index=None, Ordered=Insertion Not constructed -II : Size=0, Index=II_index, Ordered=Insertion +II : Size=0, Index={1, 2, 3}, Ordered=Insertion Not constructed J : Size=0, Index=None, Ordered=Insertion Not constructed -JJ : Size=0, Index=JJ_index, Ordered=Insertion +JJ : Size=0, Index={1, 2, 3}, Ordered=Insertion Not constructed""".strip() self.assertEqual(output.getvalue().strip(), ref) @@ -4797,7 +4826,7 @@ def _i_init(m, i): output = StringIO() m.I.pprint(ostream=output) ref = """ -I : Size=2, Index=I_index, Ordered=Insertion +I : Size=2, Index={1, 2, 3, 4, 5}, Ordered=Insertion Key : Dimen : Domain : Size : Members 2 : 1 : Any : 2 : {0, 1} 4 : 1 : Any : 4 : {0, 1, 2, 3} @@ -5227,7 +5256,7 @@ def test_no_normalize_index(self): m.I = Set() self.assertIs(m.I._dimen, UnknownSetDimen) self.assertTrue(m.I.add((1, (2, 3)))) - self.assertIs(m.I._dimen, None) + self.assertIs(m.I._dimen, 2) self.assertNotIn(((1, 2), 3), m.I) self.assertIn((1, (2, 3)), m.I) self.assertNotIn((1, 2, 3), m.I) @@ -5268,15 +5297,15 @@ def test_no_normalize_index(self): class TestAbstractSetAPI(unittest.TestCase): - def test_SetData(self): + def testSetData(self): # This tests an anstract non-finite set API m = ConcreteModel() m.I = Set(initialize=[1]) - s = _SetData(m.I) + s = SetData(m.I) # - # _SetData API + # SetData API # with self.assertRaises(DeveloperError): @@ -5366,7 +5395,7 @@ def test_SetData(self): def test_FiniteMixin(self): # This tests an anstract finite set API - class FiniteMixin(_FiniteSetMixin, _SetData): + class FiniteMixin(_FiniteSetMixin, SetData): pass m = ConcreteModel() @@ -5374,7 +5403,7 @@ class FiniteMixin(_FiniteSetMixin, _SetData): s = FiniteMixin(m.I) # - # _SetData API + # SetData API # with self.assertRaises(DeveloperError): @@ -5491,7 +5520,7 @@ class FiniteMixin(_FiniteSetMixin, _SetData): def test_OrderedMixin(self): # This tests an anstract ordered set API - class OrderedMixin(_OrderedSetMixin, _FiniteSetMixin, _SetData): + class OrderedMixin(_OrderedSetMixin, _FiniteSetMixin, SetData): pass m = ConcreteModel() @@ -5499,7 +5528,7 @@ class OrderedMixin(_OrderedSetMixin, _FiniteSetMixin, _SetData): s = OrderedMixin(m.I) # - # _SetData API + # SetData API # with self.assertRaises(DeveloperError): @@ -6238,7 +6267,6 @@ def test_issue_835(self): @unittest.skipIf(NamedTuple is None, "typing module not available") def test_issue_938(self): - self.maxDiff = None NodeKey = NamedTuple('NodeKey', [('id', int)]) ArcKey = NamedTuple('ArcKey', [('node_from', NodeKey), ('node_to', NodeKey)]) @@ -6271,14 +6299,11 @@ def objective_rule(model_arg): output = StringIO() m.pprint(ostream=output) ref = """ -3 Set Declarations +2 Set Declarations arc_keys : Set of arcs Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 2 : arc_keys_domain : 2 : {(0, 0), (0, 1)} - arc_keys_domain : Size=1, Index=None, Ordered=True Key : Dimen : Domain : Size : Members - None : 2 : node_keys*node_keys : 4 : {(0, 0), (0, 1), (1, 0), (1, 1)} + None : 2 : node_keys*node_keys : 2 : {(0, 0), (0, 1)} node_keys : Set of nodes Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members @@ -6295,7 +6320,7 @@ def objective_rule(model_arg): Key : Active : Sense : Expression None : True : minimize : arc_variables[0,0] + arc_variables[0,1] -5 Declarations: node_keys arc_keys_domain arc_keys arc_variables obj +4 Declarations: node_keys arc_keys arc_variables obj """.strip() self.assertEqual(output.getvalue().strip(), ref) @@ -6304,18 +6329,15 @@ def objective_rule(model_arg): output = StringIO() m.pprint(ostream=output) ref = """ -3 Set Declarations +2 Set Declarations arc_keys : Set of arcs Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : None : arc_keys_domain : 2 : {ArcKey(node_from=NodeKey(id=0), node_to=NodeKey(id=0)), ArcKey(node_from=NodeKey(id=0), node_to=NodeKey(id=1))} - arc_keys_domain : Size=1, Index=None, Ordered=True Key : Dimen : Domain : Size : Members - None : None : node_keys*node_keys : 4 : {(NodeKey(id=0), NodeKey(id=0)), (NodeKey(id=0), NodeKey(id=1)), (NodeKey(id=1), NodeKey(id=0)), (NodeKey(id=1), NodeKey(id=1))} + None : 2 : node_keys*node_keys : 2 : {ArcKey(node_from=NodeKey(id=0), node_to=NodeKey(id=0)), ArcKey(node_from=NodeKey(id=0), node_to=NodeKey(id=1))} node_keys : Set of nodes Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members - None : None : Any : 2 : {NodeKey(id=0), NodeKey(id=1)} + None : 1 : Any : 2 : {NodeKey(id=0), NodeKey(id=1)} 1 Var Declarations arc_variables : Size=2, Index=arc_keys @@ -6328,7 +6350,7 @@ def objective_rule(model_arg): Key : Active : Sense : Expression None : True : minimize : arc_variables[ArcKey(node_from=NodeKey(id=0), node_to=NodeKey(id=0))] + arc_variables[ArcKey(node_from=NodeKey(id=0), node_to=NodeKey(id=1))] -5 Declarations: node_keys arc_keys_domain arc_keys arc_variables obj +4 Declarations: node_keys arc_keys arc_variables obj """.strip() self.assertEqual(output.getvalue().strip(), ref) diff --git a/pyomo/core/tests/unit/test_sets.py b/pyomo/core/tests/unit/test_sets.py index 47cc14ce181..48869397aae 100644 --- a/pyomo/core/tests/unit/test_sets.py +++ b/pyomo/core/tests/unit/test_sets.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -3395,7 +3395,9 @@ def test_getitem(self): with self.assertRaisesRegex(RuntimeError, ".*before it has been constructed"): a[0] a.construct() - with self.assertRaisesRegex(IndexError, "Pyomo Sets are 1-indexed"): + with self.assertRaisesRegex( + IndexError, "Accessing Pyomo Sets by position is 1-based" + ): a[0] self.assertEqual(a[1], 2) diff --git a/pyomo/core/tests/unit/test_smap.py b/pyomo/core/tests/unit/test_smap.py index 2b9d2f192c0..69448916a04 100644 --- a/pyomo/core/tests/unit/test_smap.py +++ b/pyomo/core/tests/unit/test_smap.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_sos.py b/pyomo/core/tests/unit/test_sos.py index 92a8a5eabaa..cacfcdf5d42 100644 --- a/pyomo/core/tests/unit/test_sos.py +++ b/pyomo/core/tests/unit/test_sos.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_sos_v2.py b/pyomo/core/tests/unit/test_sos_v2.py index 8b6fab549a2..996dd10829d 100644 --- a/pyomo/core/tests/unit/test_sos_v2.py +++ b/pyomo/core/tests/unit/test_sos_v2.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ***************************************************************************** # ***************************************************************************** diff --git a/pyomo/core/tests/unit/test_suffix.py b/pyomo/core/tests/unit/test_suffix.py index 131e2054284..d2e861cceb5 100644 --- a/pyomo/core/tests/unit/test_suffix.py +++ b/pyomo/core/tests/unit/test_suffix.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -14,12 +14,15 @@ import os import itertools +import logging import pickle from os.path import abspath, dirname currdir = dirname(abspath(__file__)) + os.sep import pyomo.common.unittest as unittest +from pyomo.common.collections import ComponentMap +from pyomo.common.log import LoggingIntercept from pyomo.core.base.suffix import ( active_export_suffix_generator, export_suffix_generator, @@ -55,49 +58,137 @@ def simple_obj_rule(model, i): class TestSuffixMethods(unittest.TestCase): - # test __init__ - def test_init(self): - model = ConcreteModel() - # no keywords - model.junk = Suffix() - model.del_component('junk') + def test_suffix_debug(self): + with LoggingIntercept(level=logging.DEBUG) as OUT: + m = ConcreteModel() + m.s = Suffix() + m.foo = Suffix(rule=[]) + print(OUT.getvalue()) + self.assertEqual( + OUT.getvalue(), + "Constructing ConcreteModel 'ConcreteModel', from data=None\n" + "Constructing Suffix 'Suffix'\n" + "Constructing AbstractSuffix 'foo' on [Model] from data=None\n" + "Constructing Suffix 'foo'\n" + "Constructed component ''[Model].foo'':\n" + "foo : Direction=LOCAL, Datatype=FLOAT\n" + " Key : Value\n\n", + ) - for direction, datatype in itertools.product( - Suffix.SuffixDirections, Suffix.SuffixDatatypes - ): - model.junk = Suffix(direction=direction, datatype=datatype) - model.del_component('junk') + def test_suffix_rule(self): + m = ConcreteModel() + m.I = Set(initialize=[1, 2, 3]) + m.x = Var(m.I) + m.y = Var(m.I) + m.c = Constraint(m.I, rule=lambda m, i: m.x[i] >= i) + m.d = Constraint(m.I, rule=lambda m, i: m.x[i] <= -i) + + _dict = {m.c[1]: 10, m.c[2]: 20, m.c[3]: 30, m.d: 100} + m.suffix_dict = Suffix(initialize=_dict) + self.assertEqual(len(m.suffix_dict), 6) + self.assertEqual(m.suffix_dict[m.c[1]], 10) + self.assertEqual(m.suffix_dict[m.c[2]], 20) + self.assertEqual(m.suffix_dict[m.c[3]], 30) + self.assertEqual(m.suffix_dict[m.d[1]], 100) + self.assertEqual(m.suffix_dict[m.d[2]], 100) + self.assertEqual(m.suffix_dict[m.d[3]], 100) + + # check double-construction + _dict[m.c[1]] = 1000 + m.suffix_dict.construct() + self.assertEqual(len(m.suffix_dict), 6) + self.assertEqual(m.suffix_dict[m.c[1]], 10) + + m.suffix_cmap = Suffix( + initialize=ComponentMap( + [(m.x[1], 10), (m.x[2], 20), (m.x[3], 30), (m.y, 100)] + ) + ) + self.assertEqual(len(m.suffix_dict), 6) + self.assertEqual(m.suffix_cmap[m.x[1]], 10) + self.assertEqual(m.suffix_cmap[m.x[2]], 20) + self.assertEqual(m.suffix_cmap[m.x[3]], 30) + self.assertEqual(m.suffix_cmap[m.y[1]], 100) + self.assertEqual(m.suffix_cmap[m.y[2]], 100) + self.assertEqual(m.suffix_cmap[m.y[3]], 100) + + m.suffix_list = Suffix( + initialize=[(m.x[1], 10), (m.x[2], 20), (m.x[3], 30), (m.y, 100)] + ) + self.assertEqual(len(m.suffix_dict), 6) + self.assertEqual(m.suffix_list[m.x[1]], 10) + self.assertEqual(m.suffix_list[m.x[2]], 20) + self.assertEqual(m.suffix_list[m.x[3]], 30) + self.assertEqual(m.suffix_list[m.y[1]], 100) + self.assertEqual(m.suffix_list[m.y[2]], 100) + self.assertEqual(m.suffix_list[m.y[3]], 100) + + def gen_init(): + yield (m.x[1], 10) + yield (m.x[2], 20) + yield (m.x[3], 30) + yield (m.y, 100) + + m.suffix_generator = Suffix(initialize=gen_init()) + self.assertEqual(len(m.suffix_dict), 6) + self.assertEqual(m.suffix_generator[m.x[1]], 10) + self.assertEqual(m.suffix_generator[m.x[2]], 20) + self.assertEqual(m.suffix_generator[m.x[3]], 30) + self.assertEqual(m.suffix_generator[m.y[1]], 100) + self.assertEqual(m.suffix_generator[m.y[2]], 100) + self.assertEqual(m.suffix_generator[m.y[3]], 100) + + def genfcn_init(m, i): + yield (m.x[1], 10) + yield (m.x[2], 20) + yield (m.x[3], 30) + yield (m.y, 100) + + m.suffix_generator_fcn = Suffix(initialize=genfcn_init) + self.assertEqual(len(m.suffix_dict), 6) + self.assertEqual(m.suffix_generator_fcn[m.x[1]], 10) + self.assertEqual(m.suffix_generator_fcn[m.x[2]], 20) + self.assertEqual(m.suffix_generator_fcn[m.x[3]], 30) + self.assertEqual(m.suffix_generator_fcn[m.y[1]], 100) + self.assertEqual(m.suffix_generator_fcn[m.y[2]], 100) + self.assertEqual(m.suffix_generator_fcn[m.y[3]], 100) # test import_enabled def test_import_enabled(self): model = ConcreteModel() + model.test_implicit = Suffix() + self.assertFalse(model.test_implicit.import_enabled()) + model.test_local = Suffix(direction=Suffix.LOCAL) - self.assertTrue(model.test_local.import_enabled() is False) + self.assertFalse(model.test_local.import_enabled()) model.test_out = Suffix(direction=Suffix.IMPORT) - self.assertTrue(model.test_out.import_enabled() is True) + self.assertTrue(model.test_out.import_enabled()) model.test_in = Suffix(direction=Suffix.EXPORT) - self.assertTrue(model.test_in.import_enabled() is False) + self.assertFalse(model.test_in.import_enabled()) model.test_inout = Suffix(direction=Suffix.IMPORT_EXPORT) - self.assertTrue(model.test_inout.import_enabled() is True) + self.assertTrue(model.test_inout.import_enabled()) # test export_enabled def test_export_enabled(self): model = ConcreteModel() + model.test_implicit = Suffix() + self.assertFalse(model.test_implicit.export_enabled()) + model.test_local = Suffix(direction=Suffix.LOCAL) - self.assertTrue(model.test_local.export_enabled() is False) + self.assertFalse(model.test_local.export_enabled()) model.test_out = Suffix(direction=Suffix.IMPORT) - self.assertTrue(model.test_out.export_enabled() is False) + self.assertFalse(model.test_out.export_enabled()) model.test_in = Suffix(direction=Suffix.EXPORT) - self.assertTrue(model.test_in.export_enabled() is True) + self.assertTrue(model.test_in.export_enabled()) model.test_inout = Suffix(direction=Suffix.IMPORT_EXPORT) - self.assertTrue(model.test_inout.export_enabled() is True) + self.assertTrue(model.test_inout.export_enabled()) # test set_value and getValue # and if Var arrays are correctly expanded @@ -773,20 +864,33 @@ def test_set_all_values3(self): self.assertEqual(model.z[1].get_suffix_value(model.junk), 3.0) # test update_values - def test_update_values1(self): + def test_update_values(self): model = ConcreteModel() model.junk = Suffix() model.x = Var() model.y = Var() - model.z = Var() + model.z = Var([1, 2]) model.junk.set_value(model.x, 0.0) self.assertEqual(model.junk.get(model.x), 0.0) self.assertEqual(model.junk.get(model.y), None) self.assertEqual(model.junk.get(model.z), None) + self.assertEqual(model.junk.get(model.z[1]), None) + self.assertEqual(model.junk.get(model.z[2]), None) model.junk.update_values([(model.x, 1.0), (model.y, 2.0), (model.z, 3.0)]) self.assertEqual(model.junk.get(model.x), 1.0) self.assertEqual(model.junk.get(model.y), 2.0) + self.assertEqual(model.junk.get(model.z), None) + self.assertEqual(model.junk.get(model.z[1]), 3.0) + self.assertEqual(model.junk.get(model.z[2]), 3.0) + model.junk.clear() + model.junk.update_values( + [(model.x, 1.0), (model.y, 2.0), (model.z, 3.0)], expand=False + ) + self.assertEqual(model.junk.get(model.x), 1.0) + self.assertEqual(model.junk.get(model.y), 2.0) self.assertEqual(model.junk.get(model.z), 3.0) + self.assertEqual(model.junk.get(model.z[1]), None) + self.assertEqual(model.junk.get(model.z[2]), None) # test clear_value def test_clear_value(self): @@ -802,26 +906,66 @@ def test_clear_value(self): model.junk.set_value(model.z, 2.0) model.junk.set_value(model.z[1], 4.0) - self.assertTrue(model.junk.get(model.x) == -1.0) - self.assertTrue(model.junk.get(model.y) == None) - self.assertTrue(model.junk.get(model.y[1]) == -2.0) + self.assertEqual(model.junk.get(model.x), -1.0) + self.assertEqual(model.junk.get(model.y), None) + self.assertEqual(model.junk.get(model.y[1]), -2.0) self.assertEqual(model.junk.get(model.y[2]), 1.0) self.assertEqual(model.junk.get(model.z), None) self.assertEqual(model.junk.get(model.z[2]), 2.0) self.assertEqual(model.junk.get(model.z[1]), 4.0) model.junk.clear_value(model.y) + + self.assertEqual(model.junk.get(model.x), -1.0) + self.assertEqual(model.junk.get(model.y), None) + self.assertEqual(model.junk.get(model.y[1]), None) + self.assertEqual(model.junk.get(model.y[2]), None) + self.assertEqual(model.junk.get(model.z), None) + self.assertEqual(model.junk.get(model.z[2]), 2.0) + self.assertEqual(model.junk.get(model.z[1]), 4.0) + + model.junk.clear_value(model.x) + + self.assertEqual(model.junk.get(model.x), None) + self.assertEqual(model.junk.get(model.y), None) + self.assertEqual(model.junk.get(model.y[1]), None) + self.assertEqual(model.junk.get(model.y[2]), None) + self.assertEqual(model.junk.get(model.z), None) + self.assertEqual(model.junk.get(model.z[2]), 2.0) + self.assertEqual(model.junk.get(model.z[1]), 4.0) + + # Clearing a scalar that is not there does not raise an error model.junk.clear_value(model.x) + + self.assertEqual(model.junk.get(model.x), None) + self.assertEqual(model.junk.get(model.y), None) + self.assertEqual(model.junk.get(model.y[1]), None) + self.assertEqual(model.junk.get(model.y[2]), None) + self.assertEqual(model.junk.get(model.z), None) + self.assertEqual(model.junk.get(model.z[2]), 2.0) + self.assertEqual(model.junk.get(model.z[1]), 4.0) + model.junk.clear_value(model.z[1]) - self.assertTrue(model.junk.get(model.x) is None) - self.assertTrue(model.junk.get(model.y) is None) - self.assertTrue(model.junk.get(model.y[1]) is None) + self.assertEqual(model.junk.get(model.x), None) + self.assertEqual(model.junk.get(model.y), None) + self.assertEqual(model.junk.get(model.y[1]), None) self.assertEqual(model.junk.get(model.y[2]), None) self.assertEqual(model.junk.get(model.z), None) self.assertEqual(model.junk.get(model.z[2]), 2.0) self.assertEqual(model.junk.get(model.z[1]), None) + # Clearing an indexed component with missing indices does not raise an error + model.junk.clear_value(model.z) + + self.assertEqual(model.junk.get(model.x), None) + self.assertEqual(model.junk.get(model.y), None) + self.assertEqual(model.junk.get(model.y[1]), None) + self.assertEqual(model.junk.get(model.y[2]), None) + self.assertEqual(model.junk.get(model.z), None) + self.assertEqual(model.junk.get(model.z[2]), None) + self.assertEqual(model.junk.get(model.z[1]), None) + # test clear_value no args def test_clear_all_values(self): model = ConcreteModel() @@ -853,45 +997,76 @@ def test_clear_all_values(self): def test_set_datatype_get_datatype(self): model = ConcreteModel() model.junk = Suffix(datatype=Suffix.FLOAT) - self.assertTrue(model.junk.get_datatype() is Suffix.FLOAT) - model.junk.set_datatype(Suffix.INT) - self.assertTrue(model.junk.get_datatype() is Suffix.INT) - model.junk.set_datatype(None) - self.assertTrue(model.junk.get_datatype() is None) + self.assertEqual(model.junk.datatype, Suffix.FLOAT) + model.junk.datatype = Suffix.INT + self.assertEqual(model.junk.datatype, Suffix.INT) + model.junk.datatype = None + self.assertEqual(model.junk.datatype, None) + model.junk.datatype = 'FLOAT' + self.assertEqual(model.junk.datatype, Suffix.FLOAT) + model.junk.datatype = 'INT' + self.assertEqual(model.junk.datatype, Suffix.INT) + model.junk.datatype = 4 + self.assertEqual(model.junk.datatype, Suffix.FLOAT) + model.junk.datatype = 0 + self.assertEqual(model.junk.datatype, Suffix.INT) + + with LoggingIntercept() as LOG: + model.junk.set_datatype(None) + self.assertEqual(model.junk.datatype, None) + self.assertRegex( + LOG.getvalue().replace("\n", " "), + "^DEPRECATED: Suffix.set_datatype is replaced with the " + "Suffix.datatype property", + ) - # test that calling set_datatype with a bad value fails - def test_set_datatype_badvalue(self): - model = ConcreteModel() - model.junk = Suffix() - try: - model.junk.set_datatype(1.0) - except ValueError: - pass - else: - self.fail("Calling set_datatype with a bad type should fail.") + model.junk.datatype = 'FLOAT' + with LoggingIntercept() as LOG: + self.assertEqual(model.junk.get_datatype(), Suffix.FLOAT) + self.assertRegex( + LOG.getvalue().replace("\n", " "), + "^DEPRECATED: Suffix.get_datatype is replaced with the " + "Suffix.datatype property", + ) + + with self.assertRaisesRegex(ValueError, "1.0 is not a valid SuffixDataType"): + model.junk.datatype = 1.0 # test set_direction and get_direction def test_set_direction_get_direction(self): model = ConcreteModel() model.junk = Suffix(direction=Suffix.LOCAL) - self.assertTrue(model.junk.get_direction() is Suffix.LOCAL) - model.junk.set_direction(Suffix.EXPORT) - self.assertTrue(model.junk.get_direction() is Suffix.EXPORT) - model.junk.set_direction(Suffix.IMPORT) - self.assertTrue(model.junk.get_direction() is Suffix.IMPORT) - model.junk.set_direction(Suffix.IMPORT_EXPORT) - self.assertTrue(model.junk.get_direction() is Suffix.IMPORT_EXPORT) + self.assertEqual(model.junk.direction, Suffix.LOCAL) + model.junk.direction = Suffix.EXPORT + self.assertEqual(model.junk.direction, Suffix.EXPORT) + model.junk.direction = Suffix.IMPORT + self.assertEqual(model.junk.direction, Suffix.IMPORT) + model.junk.direction = Suffix.IMPORT_EXPORT + self.assertEqual(model.junk.direction, Suffix.IMPORT_EXPORT) + + with LoggingIntercept() as LOG: + model.junk.set_direction(1) + self.assertEqual(model.junk.direction, Suffix.EXPORT) + self.assertRegex( + LOG.getvalue().replace("\n", " "), + "^DEPRECATED: Suffix.set_direction is replaced with the " + "Suffix.direction property", + ) - # test that calling set_direction with a bad value fails - def test_set_direction_badvalue(self): - model = ConcreteModel() - model.junk = Suffix() - try: - model.junk.set_direction('a') - except ValueError: - pass - else: - self.fail("Calling set_datatype with a bad type should fail.") + model.junk.direction = 'IMPORT' + with LoggingIntercept() as LOG: + self.assertEqual(model.junk.get_direction(), Suffix.IMPORT) + self.assertRegex( + LOG.getvalue().replace("\n", " "), + "^DEPRECATED: Suffix.get_direction is replaced with the " + "Suffix.direction property", + ) + + with self.assertRaisesRegex(ValueError, "'a' is not a valid SuffixDirection"): + model.junk.direction = 'a' + # None is allowed for datatype, but not direction + with self.assertRaisesRegex(ValueError, "None is not a valid SuffixDirection"): + model.junk.direction = None # test __str__ def test_str(self): @@ -905,13 +1080,44 @@ def test_pprint(self): model.junk = Suffix(direction=Suffix.EXPORT) output = StringIO() model.junk.pprint(ostream=output) - model.junk.set_direction(Suffix.IMPORT) + self.assertEqual( + output.getvalue(), + "junk : Direction=EXPORT, Datatype=FLOAT\n Key : Value\n", + ) + model.junk.direction = Suffix.IMPORT + output = StringIO() model.junk.pprint(ostream=output) - model.junk.set_direction(Suffix.LOCAL) + self.assertEqual( + output.getvalue(), + "junk : Direction=IMPORT, Datatype=FLOAT\n Key : Value\n", + ) + model.junk.direction = Suffix.LOCAL + model.junk.datatype = None + output = StringIO() model.junk.pprint(ostream=output) - model.junk.set_direction(Suffix.IMPORT_EXPORT) + self.assertEqual( + output.getvalue(), + "junk : Direction=LOCAL, Datatype=None\n Key : Value\n", + ) + model.junk.direction = Suffix.IMPORT_EXPORT + model.junk.datatype = Suffix.INT + output = StringIO() model.junk.pprint(ostream=output) + self.assertEqual( + output.getvalue(), + "junk : Direction=IMPORT_EXPORT, Datatype=INT\n Key : Value\n", + ) + output = StringIO() model.pprint(ostream=output) + self.assertEqual( + output.getvalue(), + """1 Suffix Declarations + junk : Direction=IMPORT_EXPORT, Datatype=INT + Key : Value + +1 Declarations: junk +""", + ) # test pprint(verbose=True) def test_pprint_verbose(self): @@ -929,7 +1135,16 @@ def test_pprint_verbose(self): output = StringIO() model.junk.pprint(ostream=output, verbose=True) - model.pprint(ostream=output, verbose=True) + self.assertEqual( + output.getvalue(), + """junk : Direction=LOCAL, Datatype=FLOAT + Key : Value + s.B[1] : 2.0 + s.B[2] : 3.0 + s.B[3] : 1.0 + s.b : 3.0 +""", + ) def test_active_export_suffix_generator(self): model = ConcreteModel() diff --git a/pyomo/core/tests/unit/test_symbol_map.py b/pyomo/core/tests/unit/test_symbol_map.py index 5f6416e2c8d..773e6d335f1 100644 --- a/pyomo/core/tests/unit/test_symbol_map.py +++ b/pyomo/core/tests/unit/test_symbol_map.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_symbolic.py b/pyomo/core/tests/unit/test_symbolic.py index bbac4599363..91887f27bb7 100644 --- a/pyomo/core/tests/unit/test_symbolic.py +++ b/pyomo/core/tests/unit/test_symbolic.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_taylor_series.py b/pyomo/core/tests/unit/test_taylor_series.py index d4fe5291b2d..4b36451d222 100644 --- a/pyomo/core/tests/unit/test_taylor_series.py +++ b/pyomo/core/tests/unit/test_taylor_series.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_template_expr.py b/pyomo/core/tests/unit/test_template_expr.py index 4b4ea494b0e..80f5d90b60e 100644 --- a/pyomo/core/tests/unit/test_template_expr.py +++ b/pyomo/core/tests/unit/test_template_expr.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -127,7 +127,7 @@ def test_template_scalar_with_set(self): # Note that structural expressions do not implement polynomial_degree with self.assertRaisesRegex( AttributeError, - "'_InsertionOrderSetData' object has " "no attribute 'polynomial_degree'", + "'InsertionOrderSetData' object has " "no attribute 'polynomial_degree'", ): e.polynomial_degree() self.assertEqual(str(e), "s[{I}]") @@ -490,14 +490,14 @@ def c(m): self.assertEqual( str(resolve_template(template)), 'x[1,1,10] + ' - '(x[2,1,10] + x[2,1,20]) + ' - '(x[3,1,10] + x[3,1,20] + x[3,1,30]) + ' - '(x[1,2,10]) + ' - '(x[2,2,10] + x[2,2,20]) + ' - '(x[3,2,10] + x[3,2,20] + x[3,2,30]) + ' - '(x[1,3,10]) + ' - '(x[2,3,10] + x[2,3,20]) + ' - '(x[3,3,10] + x[3,3,20] + x[3,3,30]) <= 0', + 'x[2,1,10] + x[2,1,20] + ' + 'x[3,1,10] + x[3,1,20] + x[3,1,30] + ' + 'x[1,2,10] + ' + 'x[2,2,10] + x[2,2,20] + ' + 'x[3,2,10] + x[3,2,20] + x[3,2,30] + ' + 'x[1,3,10] + ' + 'x[2,3,10] + x[2,3,20] + ' + 'x[3,3,10] + x[3,3,20] + x[3,3,30] <= 0', ) def test_multidim_nested_sum_rule(self): @@ -566,14 +566,14 @@ def c(m): self.assertEqual( str(resolve_template(template)), 'x[1,1,10] + ' - '(x[2,1,10] + x[2,1,20]) + ' - '(x[3,1,10] + x[3,1,20] + x[3,1,30]) + ' - '(x[1,2,10]) + ' - '(x[2,2,10] + x[2,2,20]) + ' - '(x[3,2,10] + x[3,2,20] + x[3,2,30]) + ' - '(x[1,3,10]) + ' - '(x[2,3,10] + x[2,3,20]) + ' - '(x[3,3,10] + x[3,3,20] + x[3,3,30]) <= 0', + 'x[2,1,10] + x[2,1,20] + ' + 'x[3,1,10] + x[3,1,20] + x[3,1,30] + ' + 'x[1,2,10] + ' + 'x[2,2,10] + x[2,2,20] + ' + 'x[3,2,10] + x[3,2,20] + x[3,2,30] + ' + 'x[1,3,10] + ' + 'x[2,3,10] + x[2,3,20] + ' + 'x[3,3,10] + x[3,3,20] + x[3,3,30] <= 0', ) def test_multidim_nested_getattr_sum_rule(self): @@ -609,14 +609,14 @@ def c(m): self.assertEqual( str(resolve_template(template)), 'x[1,1,10] + ' - '(x[2,1,10] + x[2,1,20]) + ' - '(x[3,1,10] + x[3,1,20] + x[3,1,30]) + ' - '(x[1,2,10]) + ' - '(x[2,2,10] + x[2,2,20]) + ' - '(x[3,2,10] + x[3,2,20] + x[3,2,30]) + ' - '(x[1,3,10]) + ' - '(x[2,3,10] + x[2,3,20]) + ' - '(x[3,3,10] + x[3,3,20] + x[3,3,30]) <= 0', + 'x[2,1,10] + x[2,1,20] + ' + 'x[3,1,10] + x[3,1,20] + x[3,1,30] + ' + 'x[1,2,10] + ' + 'x[2,2,10] + x[2,2,20] + ' + 'x[3,2,10] + x[3,2,20] + x[3,2,30] + ' + 'x[1,3,10] + ' + 'x[2,3,10] + x[2,3,20] + ' + 'x[3,3,10] + x[3,3,20] + x[3,3,30] <= 0', ) def test_eval_getattr(self): diff --git a/pyomo/core/tests/unit/test_units.py b/pyomo/core/tests/unit/test_units.py index 8ec83fe1a73..bda62835711 100644 --- a/pyomo/core/tests/unit/test_units.py +++ b/pyomo/core/tests/unit/test_units.py @@ -2,7 +2,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -920,8 +920,7 @@ def test_module_example(self): model = ConcreteModel() model.acc = Var() model.obj = Objective( - expr=(model.acc * units.m / units.s**2 - 9.81 * units.m / units.s**2) - ** 2 + expr=(model.acc * units.m / units.s**2 - 9.81 * units.m / units.s**2) ** 2 ) self.assertEqual('m**2/s**4', str(units.get_units(model.obj.expr))) diff --git a/pyomo/core/tests/unit/test_var.py b/pyomo/core/tests/unit/test_var.py index 33e46a79e9b..6b2e92be832 100644 --- a/pyomo/core/tests/unit/test_var.py +++ b/pyomo/core/tests/unit/test_var.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_var_set_bounds.py b/pyomo/core/tests/unit/test_var_set_bounds.py index eb969c2ca73..1686ba4f1c6 100644 --- a/pyomo/core/tests/unit/test_var_set_bounds.py +++ b/pyomo/core/tests/unit/test_var_set_bounds.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -36,7 +36,7 @@ # GAH: These tests been temporarily disabled. It is no longer the job of Var # to validate its domain at the time of construction. It only needs to # ensure that whatever object is passed as its domain is suitable for -# interacting with the _VarData interface (e.g., has a bounds method) +# interacting with the VarData interface (e.g., has a bounds method) # The plan is to start adding functionality to the solver interfaces # that will support custom domains. diff --git a/pyomo/core/tests/unit/test_visitor.py b/pyomo/core/tests/unit/test_visitor.py index b70996a13dc..5733710ab46 100644 --- a/pyomo/core/tests/unit/test_visitor.py +++ b/pyomo/core/tests/unit/test_visitor.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -72,7 +72,7 @@ RECURSION_LIMIT, get_stack_depth, ) -from pyomo.core.base.param import _ParamData, ScalarParam +from pyomo.core.base.param import ParamData, ScalarParam from pyomo.core.expr.template_expr import IndexTemplate from pyomo.common.collections import ComponentSet from pyomo.common.errors import TemplateExpressionError @@ -145,7 +145,8 @@ def test_identify_vars_vars(self): self.assertEqual(list(identify_variables(m.a + m.b[1])), [m.a, m.b[1]]) self.assertEqual(list(identify_variables(m.a ** m.b[1])), [m.a, m.b[1]]) self.assertEqual( - list(identify_variables(m.a ** m.b[1] + m.b[2])), [m.b[2], m.a, m.b[1]] + ComponentSet(identify_variables(m.a ** m.b[1] + m.b[2])), + ComponentSet([m.b[2], m.a, m.b[1]]), ) self.assertEqual( list(identify_variables(m.a ** m.b[1] + m.b[2] * m.b[3] * m.b[2])), @@ -159,14 +160,20 @@ def test_identify_vars_vars(self): # Identify variables in the arguments to functions # self.assertEqual( - list(identify_variables(m.x(m.a, 'string_param', 1, []) * m.b[1])), - [m.b[1], m.a], + ComponentSet(identify_variables(m.x(m.a, 'string_param', 1, []) * m.b[1])), + ComponentSet([m.b[1], m.a]), ) self.assertEqual( list(identify_variables(m.x(m.p, 'string_param', 1, []) * m.b[1])), [m.b[1]] ) - self.assertEqual(list(identify_variables(tanh(m.a) * m.b[1])), [m.b[1], m.a]) - self.assertEqual(list(identify_variables(abs(m.a) * m.b[1])), [m.b[1], m.a]) + self.assertEqual( + ComponentSet(identify_variables(tanh(m.a) * m.b[1])), + ComponentSet([m.b[1], m.a]), + ) + self.assertEqual( + ComponentSet(identify_variables(abs(m.a) * m.b[1])), + ComponentSet([m.b[1], m.a]), + ) # # Check logic for allowing duplicates # @@ -405,7 +412,6 @@ def test_replacement_walker0(self): ) del M.w - del M.w_index M.w = VarList() e = 2 * sum_product(M.z, M.x) walker = ReplacementWalkerTest1(M) @@ -438,9 +444,7 @@ def test_replacement_linear_expression_with_constant(self): sub_map = dict() sub_map[id(m.x)] = 5 e2 = replace_expressions(e, sub_map) - assertExpressionsEqual( - self, e2, LinearExpression([10, MonomialTermExpression((1, m.y))]) - ) + assertExpressionsEqual(self, e2, LinearExpression([10, m.y])) e = LinearExpression(linear_coefs=[2, 3], linear_vars=[m.x, m.y]) sub_map = dict() @@ -688,7 +692,7 @@ def __init__(self, model): self.model = model def visiting_potential_leaf(self, node): - if node.__class__ in (_ParamData, ScalarParam): + if node.__class__ in (ParamData, ScalarParam): if id(node) in self.substitute: return True, self.substitute[id(node)] self.substitute[id(node)] = 2 * self.model.w.add() @@ -887,20 +891,7 @@ def test_replace(self): assertExpressionsEqual( self, SumExpression( - [ - LinearExpression( - [ - MonomialTermExpression((1, m.y[1])), - MonomialTermExpression((1, m.y[2])), - ] - ), - LinearExpression( - [ - MonomialTermExpression((1, m.y[2])), - MonomialTermExpression((1, m.y[3])), - ] - ), - ] + [LinearExpression([m.y[1], m.y[2]]), LinearExpression([m.y[2], m.y[3]])] ) == 0, f, @@ -931,9 +922,7 @@ def test_npv_sum(self): e3 = replace_expressions(e1, {id(m.p1): m.x}) assertExpressionsEqual(self, e2, m.p2 + 2) - assertExpressionsEqual( - self, e3, LinearExpression([MonomialTermExpression((1, m.x)), 2]) - ) + assertExpressionsEqual(self, e3, LinearExpression([m.x, 2])) def test_npv_negation(self): m = ConcreteModel() @@ -1822,8 +1811,9 @@ def run_walker(self, walker): cases = [] else: # 3 sufficed through Python 3.10, but appeared to need to be - # raised to 5 for recent 3.11 builds (3.11.2) - cases = [(0, ""), (5, warn_msg)] + # raised to 5 for Python 3.11 builds (3.11.2), and again to + # 10 for Python 3.12 builds (3.12.0) + cases = [(0, ""), (10, warn_msg)] head_room = sys.getrecursionlimit() - get_stack_depth() for n, msg in cases: diff --git a/pyomo/core/tests/unit/test_xfrm_discrete_vars.py b/pyomo/core/tests/unit/test_xfrm_discrete_vars.py index ae630586480..d0e74c8cae3 100644 --- a/pyomo/core/tests/unit/test_xfrm_discrete_vars.py +++ b/pyomo/core/tests/unit/test_xfrm_discrete_vars.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/uninstantiated_model_linear.py b/pyomo/core/tests/unit/uninstantiated_model_linear.py index 387444b7bc5..417f7763d87 100644 --- a/pyomo/core/tests/unit/uninstantiated_model_linear.py +++ b/pyomo/core/tests/unit/uninstantiated_model_linear.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/uninstantiated_model_quadratic.py b/pyomo/core/tests/unit/uninstantiated_model_quadratic.py index 572c6a43a14..350d96a85bb 100644 --- a/pyomo/core/tests/unit/uninstantiated_model_quadratic.py +++ b/pyomo/core/tests/unit/uninstantiated_model_quadratic.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/varpprint.txt b/pyomo/core/tests/unit/varpprint.txt index bd49b881417..a8c33c6b007 100644 --- a/pyomo/core/tests/unit/varpprint.txt +++ b/pyomo/core/tests/unit/varpprint.txt @@ -1,13 +1,7 @@ -3 Set Declarations +1 Set Declarations a : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {1, 2, 3} - cl_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 10 : {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} - o3_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : a*a : 9 : {(1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3), (3, 1), (3, 2), (3, 3)} 2 Param Declarations A : Size=1, Index=None, Domain=Any, Default=-1, Mutable=True @@ -37,7 +31,7 @@ 1 : True : minimize : b[1] 2 : True : minimize : b[2] 3 : True : minimize : b[3] - o3 : Size=0, Index=o3_index, Active=True + o3 : Size=0, Index=a*a, Active=True Key : Active : Sense : Expression 19 Constraint Declarations @@ -97,7 +91,7 @@ c9b : Size=1, Index=None, Active=True Key : Lower : Body : Upper : Active None : -Inf : c : A + A : True - cl : Size=10, Index=cl_index, Active=True + cl : Size=10, Index={1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, Active=True Key : Lower : Body : Upper : Active 1 : -Inf : d - c : 0.0 : True 2 : -Inf : d - 2*c : 0.0 : True @@ -110,4 +104,4 @@ 9 : -Inf : d - 9*c : 0.0 : True 10 : -Inf : d - 10*c : 0.0 : True -30 Declarations: a b c d e A B o2 o3_index o3 c1 c2 c3 c4 c5 c6a c7a c7b c8 c9a c9b c10a c11 c15a c16a c12 c13a c14a cl_index cl +28 Declarations: a b c d e A B o2 o3 c1 c2 c3 c4 c5 c6a c7a c7b c8 c9a c9b c10a c11 c15a c16a c12 c13a c14a cl diff --git a/pyomo/core/util.py b/pyomo/core/util.py index 3f8a136e07d..4b6cc8f3320 100644 --- a/pyomo/core/util.py +++ b/pyomo/core/util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -13,27 +13,12 @@ # Utility functions # -__all__ = [ - 'sum_product', - 'summation', - 'dot_product', - 'sequence', - 'prod', - 'quicksum', - 'target_list', -] - from pyomo.common.deprecation import deprecation_warning from pyomo.core.expr.numvalue import native_numeric_types -from pyomo.core.expr.numeric_expr import ( - mutable_expression, - nonlinear_expression, - NPV_SumExpression, -) -import pyomo.core.expr as EXPR +from pyomo.core.expr.numeric_expr import mutable_expression, NPV_SumExpression from pyomo.core.base.var import Var from pyomo.core.base.expression import Expression -from pyomo.core.base.component import _ComponentBase +from pyomo.core.base.component import ComponentBase import logging logger = logging.getLogger(__name__) @@ -253,12 +238,12 @@ def sequence(*args): def target_list(x): - if isinstance(x, _ComponentBase): + if isinstance(x, ComponentBase): return [x] elif hasattr(x, '__iter__'): ans = [] for i in x: - if isinstance(i, _ComponentBase): + if isinstance(i, ComponentBase): ans.append(i) else: raise ValueError( diff --git a/pyomo/dae/__init__.py b/pyomo/dae/__init__.py index 8d07b184336..5860a129aa2 100644 --- a/pyomo/dae/__init__.py +++ b/pyomo/dae/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/contset.py b/pyomo/dae/contset.py index ee4c9f79e89..9b4f11714df 100644 --- a/pyomo/dae/contset.py +++ b/pyomo/dae/contset.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -17,7 +17,6 @@ from pyomo.core.base.component import ModelComponentFactory logger = logging.getLogger('pyomo.dae') -__all__ = ['ContinuousSet'] @ModelComponentFactory.register( diff --git a/pyomo/dae/diffvar.py b/pyomo/dae/diffvar.py index 8d75b9ae148..b921107957f 100644 --- a/pyomo/dae/diffvar.py +++ b/pyomo/dae/diffvar.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -16,8 +16,6 @@ from pyomo.core.base.var import Var from pyomo.dae.contset import ContinuousSet -__all__ = ('DerivativeVar', 'DAE_Error') - def create_access_function(var): """ diff --git a/pyomo/dae/flatten.py b/pyomo/dae/flatten.py index 595f90b3dc7..3d90cc443c1 100644 --- a/pyomo/dae/flatten.py +++ b/pyomo/dae/flatten.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -200,8 +200,28 @@ def slice_component_along_sets(component, sets, context_slice=None, normalize=No # # Note that c_slice is not necessarily a slice. # We enter this loop even if no sets need slicing. - temp_slice = c_slice.duplicate() - next(iter(temp_slice)) + try: + next(iter(c_slice.duplicate())) + except IndexError: + if normalize_index.flatten: + raise + # There is an edge case where when we are not + # flattening indices the dimensionality of an + # index can change between a SetProduct and the + # member Sets: the member set can have dimen>1 + # (or even None!), but the dimen of that portion + # of the SetProduct is always 1. Since we are + # just checking that the c_slice isn't + # completely empty, we will allow matching with + # an Ellipsis + _empty = True + try: + next(iter(base_component[...])) + _empty = False + except: + pass + if _empty: + raise if (normalize is None and normalize_index.flatten) or normalize: # Most users probably want this index to be normalized, # so they can more conveniently use it as a key in a @@ -239,7 +259,7 @@ def generate_sliced_components( Parameters ---------- - b: _BlockData + b: BlockData Block whose components will be sliced index_stack: list @@ -247,7 +267,7 @@ def generate_sliced_components( component, that have been sliced. This is necessary to return the sets that have been sliced. - slice_: IndexedComponent_slice or _BlockData + slice_: IndexedComponent_slice or BlockData Slice generated so far. This function will yield extensions to this slice at the current level of the block hierarchy. @@ -423,7 +443,7 @@ def flatten_components_along_sets(m, sets, ctype, indices=None, active=None): Parameters ---------- - m: _BlockData + m: BlockData Block whose components (and their sub-components) will be partitioned @@ -526,7 +546,7 @@ def flatten_dae_components(model, time, ctype, indices=None, active=None): Parameters ---------- - model: _BlockData + model: BlockData Block whose components are partitioned time: Set diff --git a/pyomo/dae/initialization.py b/pyomo/dae/initialization.py index c10ccb023d1..97928026de2 100644 --- a/pyomo/dae/initialization.py +++ b/pyomo/dae/initialization.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/integral.py b/pyomo/dae/integral.py index 302e50a007d..8c9512d98dd 100644 --- a/pyomo/dae/integral.py +++ b/pyomo/dae/integral.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -14,15 +14,13 @@ from pyomo.core.base.indexed_component import rule_wrapper from pyomo.core.base.expression import ( Expression, - _GeneralExpressionData, + ExpressionData, ScalarExpression, IndexedExpression, ) from pyomo.dae.contset import ContinuousSet from pyomo.dae.diffvar import DAE_Error -__all__ = ('Integral',) - @ModelComponentFactory.register("Integral Expression in a DAE model.") class Integral(Expression): @@ -153,7 +151,7 @@ class ScalarIntegral(ScalarExpression, Integral): """ def __init__(self, *args, **kwds): - _GeneralExpressionData.__init__(self, None, component=self) + ExpressionData.__init__(self, None, component=self) Integral.__init__(self, *args, **kwds) def clear(self): diff --git a/pyomo/dae/misc.py b/pyomo/dae/misc.py index 9b867bcfff4..dcb73f60c9e 100644 --- a/pyomo/dae/misc.py +++ b/pyomo/dae/misc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -263,7 +263,7 @@ def _update_var(v): # Note: This is not required it is handled by the _default method on # Var (which is now a IndexedComponent). However, it # would be much slower to rely on that method to generate new - # _VarData for a large number of new indices. + # VarData for a large number of new indices. new_indices = set(v.index_set()) - set(v._data.keys()) for index in new_indices: v.add(index) diff --git a/pyomo/dae/plugins/__init__.py b/pyomo/dae/plugins/__init__.py index 96ab91b0ac0..681112dd970 100644 --- a/pyomo/dae/plugins/__init__.py +++ b/pyomo/dae/plugins/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/plugins/colloc.py b/pyomo/dae/plugins/colloc.py index c95d4b1a672..81f1e4dd7ea 100644 --- a/pyomo/dae/plugins/colloc.py +++ b/pyomo/dae/plugins/colloc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -10,6 +10,7 @@ # ___________________________________________________________________________ import logging +import math # If the user has numpy then the collocation points and the a matrix for # the Runge-Kutta basis formulation will be calculated as needed. @@ -156,7 +157,7 @@ def conv(a, b): def calc_cp(alpha, beta, k): gamma = [] - factorial = numpy.math.factorial + factorial = math.factorial for i in range(k + 1): num = factorial(alpha + k) * factorial(alpha + beta + k + i) diff --git a/pyomo/dae/plugins/finitedifference.py b/pyomo/dae/plugins/finitedifference.py index 71bb2ffc9b6..6557a14e562 100644 --- a/pyomo/dae/plugins/finitedifference.py +++ b/pyomo/dae/plugins/finitedifference.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/set_utils.py b/pyomo/dae/set_utils.py index 96a7489261e..d7a1d9517d9 100644 --- a/pyomo/dae/set_utils.py +++ b/pyomo/dae/set_utils.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -164,8 +164,8 @@ def get_indices_of_projection(index_set, *sets): info['set_except'] = [None] # index_getter returns an index corresponding to the values passed to # it, re-ordered according to order of indexing sets in component. - info['index_getter'] = ( - lambda incomplete_index, *newvals: newvals[0] + info['index_getter'] = lambda incomplete_index, *newvals: ( + newvals[0] if len(newvals) <= 1 else tuple([newvals[location[i]] for i in location]) ) diff --git a/pyomo/dae/simulator.py b/pyomo/dae/simulator.py index b869592553a..72ba0c7331d 100644 --- a/pyomo/dae/simulator.py +++ b/pyomo/dae/simulator.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # _________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects @@ -6,20 +17,14 @@ # the U.S. Government retains certain rights in this software. # This software is distributed under the BSD License. # _________________________________________________________________________ -from pyomo.core.base import Constraint, Param, value, Suffix, Block +import logging +from pyomo.core.base import Constraint, Param, value, Suffix, Block from pyomo.dae import ContinuousSet, DerivativeVar from pyomo.dae.diffvar import DAE_Error - import pyomo.core.expr as EXPR from pyomo.core.expr.numvalue import native_numeric_types from pyomo.core.expr.template_expr import IndexTemplate, _GetItemIndexer - -import logging - -__all__ = ('Simulator',) -logger = logging.getLogger('pyomo.core') - from pyomo.common.dependencies import ( numpy as np, numpy_available, @@ -28,6 +33,8 @@ attempt_import, ) +logger = logging.getLogger('pyomo.core') + casadi_intrinsic = {} diff --git a/pyomo/dae/tests/__init__.py b/pyomo/dae/tests/__init__.py index 12bdccd0ef4..4638923595a 100644 --- a/pyomo/dae/tests/__init__.py +++ b/pyomo/dae/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/tests/test_colloc.py b/pyomo/dae/tests/test_colloc.py index dda928110ae..e7e6b20d660 100644 --- a/pyomo/dae/tests/test_colloc.py +++ b/pyomo/dae/tests/test_colloc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,7 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from __future__ import print_function + import pyomo.common.unittest as unittest from pyomo.environ import Var, Set, ConcreteModel, TransformationFactory, pyomo diff --git a/pyomo/dae/tests/test_contset.py b/pyomo/dae/tests/test_contset.py index ce13d53dfd5..e5f11b90e27 100644 --- a/pyomo/dae/tests/test_contset.py +++ b/pyomo/dae/tests/test_contset.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/tests/test_diffvar.py b/pyomo/dae/tests/test_diffvar.py index 718781d5916..414e9341e19 100644 --- a/pyomo/dae/tests/test_diffvar.py +++ b/pyomo/dae/tests/test_diffvar.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -69,7 +69,6 @@ def test_valid(self): del m.dv del m.dv2 del m.v - del m.v_index m.v = Var(m.x, m.t) m.dv = DerivativeVar(m.v, wrt=m.x) diff --git a/pyomo/dae/tests/test_finite_diff.py b/pyomo/dae/tests/test_finite_diff.py index 9ae7ecdea91..a1b842feccf 100644 --- a/pyomo/dae/tests/test_finite_diff.py +++ b/pyomo/dae/tests/test_finite_diff.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,7 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from __future__ import print_function + import pyomo.common.unittest as unittest from pyomo.environ import Var, Set, ConcreteModel, TransformationFactory diff --git a/pyomo/dae/tests/test_flatten.py b/pyomo/dae/tests/test_flatten.py index a6ea824c3ef..1fc28f66bdf 100644 --- a/pyomo/dae/tests/test_flatten.py +++ b/pyomo/dae/tests/test_flatten.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -49,6 +49,12 @@ class TestAssumedBehavior(unittest.TestCase): immediately obvious would be the case. """ + def setUp(self): + self._orig_flatten = normalize_index.flatten + + def tearDown(self): + normalize_index.flatten = self._orig_flatten + def test_cross(self): m = ConcreteModel() m.s1 = Set(initialize=[1, 2]) @@ -313,6 +319,12 @@ def c_rule(m, t): class TestFlatten(_TestFlattenBase, unittest.TestCase): + def setUp(self): + self._orig_flatten = normalize_index.flatten + + def tearDown(self): + normalize_index.flatten = self._orig_flatten + def _model1_1d_sets(self): # One-dimensional sets, no skipping. m = ConcreteModel() diff --git a/pyomo/dae/tests/test_initialization.py b/pyomo/dae/tests/test_initialization.py index 390b6ecc59e..8407ad2b2a4 100644 --- a/pyomo/dae/tests/test_initialization.py +++ b/pyomo/dae/tests/test_initialization.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/tests/test_integral.py b/pyomo/dae/tests/test_integral.py index 77d6d4dd8a9..933bd97d7b4 100644 --- a/pyomo/dae/tests/test_integral.py +++ b/pyomo/dae/tests/test_integral.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/tests/test_misc.py b/pyomo/dae/tests/test_misc.py index 11c4e44b7b0..48c1e48418d 100644 --- a/pyomo/dae/tests/test_misc.py +++ b/pyomo/dae/tests/test_misc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/tests/test_set_utils.py b/pyomo/dae/tests/test_set_utils.py index fa592e05181..8877dadf798 100644 --- a/pyomo/dae/tests/test_set_utils.py +++ b/pyomo/dae/tests/test_set_utils.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/tests/test_simulator.py b/pyomo/dae/tests/test_simulator.py index b3003bb5a0d..76316b5571e 100644 --- a/pyomo/dae/tests/test_simulator.py +++ b/pyomo/dae/tests/test_simulator.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,7 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from __future__ import print_function import json import pyomo.common.unittest as unittest diff --git a/pyomo/dae/utilities.py b/pyomo/dae/utilities.py index e48c66e003d..ae4018a122e 100644 --- a/pyomo/dae/utilities.py +++ b/pyomo/dae/utilities.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dataportal/DataPortal.py b/pyomo/dataportal/DataPortal.py index 8eb577af013..457bb1aacee 100644 --- a/pyomo/dataportal/DataPortal.py +++ b/pyomo/dataportal/DataPortal.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['DataPortal'] - import logging from pyomo.common.log import is_debug_set from pyomo.dataportal.factory import DataManagerFactory, UnknownDataManager diff --git a/pyomo/dataportal/TableData.py b/pyomo/dataportal/TableData.py index 1d428967449..f1500d09f9b 100644 --- a/pyomo/dataportal/TableData.py +++ b/pyomo/dataportal/TableData.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['TableData'] - from pyomo.common.collections import Bunch from pyomo.dataportal.process_data import _process_data diff --git a/pyomo/dataportal/__init__.py b/pyomo/dataportal/__init__.py index ca82614ef2a..ece0ac039f6 100644 --- a/pyomo/dataportal/__init__.py +++ b/pyomo/dataportal/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dataportal/factory.py b/pyomo/dataportal/factory.py index f1c18dc05c9..479769137e2 100644 --- a/pyomo/dataportal/factory.py +++ b/pyomo/dataportal/factory.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['DataManagerFactory', 'UnknownDataManager'] - import logging from pyomo.common import Factory from pyomo.common.plugin_base import PluginError diff --git a/pyomo/dataportal/parse_datacmds.py b/pyomo/dataportal/parse_datacmds.py index be363fdb64b..60e2f2c0acb 100644 --- a/pyomo/dataportal/parse_datacmds.py +++ b/pyomo/dataportal/parse_datacmds.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['parse_data_commands'] - import bisect import sys import logging diff --git a/pyomo/dataportal/plugins/__init__.py b/pyomo/dataportal/plugins/__init__.py index e861233dc01..3a356ee9da8 100644 --- a/pyomo/dataportal/plugins/__init__.py +++ b/pyomo/dataportal/plugins/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from pyomo.common.dependencies import pyutilib, pyutilib_available - def load(): import pyomo.dataportal.plugins.csv_table @@ -19,6 +17,4 @@ def load(): import pyomo.dataportal.plugins.json_dict import pyomo.dataportal.plugins.text import pyomo.dataportal.plugins.xml_table - - if pyutilib_available: - import pyomo.dataportal.plugins.sheet + import pyomo.dataportal.plugins.sheet diff --git a/pyomo/dataportal/plugins/csv_table.py b/pyomo/dataportal/plugins/csv_table.py index 6563a89df10..a52c8227695 100644 --- a/pyomo/dataportal/plugins/csv_table.py +++ b/pyomo/dataportal/plugins/csv_table.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dataportal/plugins/datacommands.py b/pyomo/dataportal/plugins/datacommands.py index 068a551d8d2..2da0d44f048 100644 --- a/pyomo/dataportal/plugins/datacommands.py +++ b/pyomo/dataportal/plugins/datacommands.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dataportal/plugins/db_table.py b/pyomo/dataportal/plugins/db_table.py index 682b87ab13e..a39705a6058 100644 --- a/pyomo/dataportal/plugins/db_table.py +++ b/pyomo/dataportal/plugins/db_table.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dataportal/plugins/json_dict.py b/pyomo/dataportal/plugins/json_dict.py index e42c040ad0b..8b41e9a1c7b 100644 --- a/pyomo/dataportal/plugins/json_dict.py +++ b/pyomo/dataportal/plugins/json_dict.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dataportal/plugins/sheet.py b/pyomo/dataportal/plugins/sheet.py index bc7e4d06952..773cce81116 100644 --- a/pyomo/dataportal/plugins/sheet.py +++ b/pyomo/dataportal/plugins/sheet.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -18,9 +18,18 @@ # ) from pyomo.dataportal.factory import DataManagerFactory from pyomo.common.errors import ApplicationError -from pyomo.common.dependencies import attempt_import +from pyomo.common.dependencies import attempt_import, importlib, pyutilib -spreadsheet, spreadsheet_available = attempt_import('pyutilib.excel.spreadsheet') + +def _spreadsheet_importer(): + # verify pyutilib imported correctly the first time + pyutilib.component + return importlib.import_module('pyutilib.excel.spreadsheet') + + +spreadsheet, spreadsheet_available = attempt_import( + 'pyutilib.excel.spreadsheet', importer=_spreadsheet_importer +) def _attempt_open_excel(): diff --git a/pyomo/dataportal/plugins/text.py b/pyomo/dataportal/plugins/text.py index a9b169e27bd..9a86fd4481b 100644 --- a/pyomo/dataportal/plugins/text.py +++ b/pyomo/dataportal/plugins/text.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dataportal/plugins/xml_table.py b/pyomo/dataportal/plugins/xml_table.py index 79245c6d24a..7e10b96312e 100644 --- a/pyomo/dataportal/plugins/xml_table.py +++ b/pyomo/dataportal/plugins/xml_table.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dataportal/process_data.py b/pyomo/dataportal/process_data.py index 5eb15269e0c..f6f20d69f67 100644 --- a/pyomo/dataportal/process_data.py +++ b/pyomo/dataportal/process_data.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dataportal/tests/__init__.py b/pyomo/dataportal/tests/__init__.py index 65e82b81c0c..85ece8d8cd5 100644 --- a/pyomo/dataportal/tests/__init__.py +++ b/pyomo/dataportal/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dataportal/tests/test_dat_parser.py b/pyomo/dataportal/tests/test_dat_parser.py index 0663279875d..43bf216525c 100644 --- a/pyomo/dataportal/tests/test_dat_parser.py +++ b/pyomo/dataportal/tests/test_dat_parser.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dataportal/tests/test_dataportal.py b/pyomo/dataportal/tests/test_dataportal.py index db9423abff6..8496a8fa3f8 100644 --- a/pyomo/dataportal/tests/test_dataportal.py +++ b/pyomo/dataportal/tests/test_dataportal.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -772,22 +772,22 @@ def test_data_namespace(self): self.assertEqual( sorted( md.values(), - key=lambda x: tuple(sorted(x) + [0]) - if type(x) is list - else tuple(sorted(x.values())) - if not type(x) is int - else (x,), + key=lambda x: ( + tuple(sorted(x) + [0]) + if type(x) is list + else tuple(sorted(x.values())) if not type(x) is int else (x,) + ), ), [-4, -3, -2, -1, [1, 3, 5], {1: 10, 3: 30, 5: 50}], ) self.assertEqual( sorted( md.values('ns1'), - key=lambda x: tuple(sorted(x) + [0]) - if type(x) is list - else tuple(sorted(x.values())) - if not type(x) is int - else (x,), + key=lambda x: ( + tuple(sorted(x) + [0]) + if type(x) is list + else tuple(sorted(x.values())) if not type(x) is int else (x,) + ), ), [1, [7, 9, 11], {7: 70, 9: 90, 11: 110}], ) diff --git a/pyomo/duality/__init__.py b/pyomo/duality/__init__.py index 7f1c869670d..a08ca813ff8 100644 --- a/pyomo/duality/__init__.py +++ b/pyomo/duality/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/duality/collect.py b/pyomo/duality/collect.py index a8b62cb8dfe..350ca058f82 100644 --- a/pyomo/duality/collect.py +++ b/pyomo/duality/collect.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/duality/lagrangian_dual.py b/pyomo/duality/lagrangian_dual.py index 1b27a3f93d4..96bc3f4a95e 100644 --- a/pyomo/duality/lagrangian_dual.py +++ b/pyomo/duality/lagrangian_dual.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/duality/plugins.py b/pyomo/duality/plugins.py index 9a8e10b4cfc..0e89857ded1 100644 --- a/pyomo/duality/plugins.py +++ b/pyomo/duality/plugins.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -87,16 +87,9 @@ def _dualize(self, block, unfixed=[]): # # Collect linear terms from the block # - ( - A, - b_coef, - c_rhs, - c_sense, - d_sense, - vnames, - cnames, - v_domain, - ) = collect_linear_terms(block, unfixed) + (A, b_coef, c_rhs, c_sense, d_sense, vnames, cnames, v_domain) = ( + collect_linear_terms(block, unfixed) + ) ##print(A) ##print(vnames) ##print(cnames) diff --git a/pyomo/duality/tests/__init__.py b/pyomo/duality/tests/__init__.py index 0dc08cc5aea..761a6e6c44c 100644 --- a/pyomo/duality/tests/__init__.py +++ b/pyomo/duality/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/duality/tests/test_linear_dual.py b/pyomo/duality/tests/test_linear_dual.py index ba3554bdc50..da8ba7a370c 100644 --- a/pyomo/duality/tests/test_linear_dual.py +++ b/pyomo/duality/tests/test_linear_dual.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/environ/__init__.py b/pyomo/environ/__init__.py index 51c68449247..07b3dfad680 100644 --- a/pyomo/environ/__init__.py +++ b/pyomo/environ/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -50,6 +50,8 @@ def _do_import(pkg_name): 'pyomo.contrib.multistart', 'pyomo.contrib.preprocessing', 'pyomo.contrib.pynumero', + 'pyomo.contrib.simplification', + 'pyomo.contrib.solver', 'pyomo.contrib.trustregion', ] @@ -114,6 +116,8 @@ def _import_packages(): exactly, atleast, atmost, + all_different, + count_if, implies, lnot, xor, diff --git a/pyomo/environ/tests/__init__.py b/pyomo/environ/tests/__init__.py index b1d721839c7..61e159c169b 100644 --- a/pyomo/environ/tests/__init__.py +++ b/pyomo/environ/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/environ/tests/standalone_minimal_pyomo_driver.py b/pyomo/environ/tests/standalone_minimal_pyomo_driver.py index 88f8e9f8651..80fb5d15121 100644 --- a/pyomo/environ/tests/standalone_minimal_pyomo_driver.py +++ b/pyomo/environ/tests/standalone_minimal_pyomo_driver.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/environ/tests/test_environ.py b/pyomo/environ/tests/test_environ.py index 27a9f10cc08..9811b412af7 100644 --- a/pyomo/environ/tests/test_environ.py +++ b/pyomo/environ/tests/test_environ.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -16,14 +16,8 @@ import sys import subprocess -from collections import namedtuple - import pyomo.common.unittest as unittest -from pyomo.common.dependencies import numpy_available, attempt_import - -pyro4, pyro4_available = attempt_import('Pyro4') - class ImportData(object): def __init__(self): @@ -145,21 +139,22 @@ def test_tpl_import_time(self): 'base64', # Imported on Windows 'cPickle', 'csv', - 'ctypes', + 'ctypes', # mandatory import in core/base/external.py; TODO: fix this + 'datetime', # imported by contrib.solver 'decimal', 'gc', # Imported on MacOS, Windows; Linux in 3.10 'glob', 'heapq', # Added in Python 3.10 - 'importlib', # Imported on Windows + 'importlib', 'inspect', 'json', # Imported on Windows 'locale', # Added in Python 3.9 'logging', 'pickle', 'platform', - 'random', # Imported on MacOS, Windows 'shlex', 'socket', # Imported on MacOS, Windows; Linux in 3.10 + 'subprocess', 'tempfile', # Imported on MacOS, Windows 'textwrap', 'typing', @@ -168,9 +163,6 @@ def test_tpl_import_time(self): } # Non-standard-library TPLs that Pyomo will load unconditionally ref.add('ply') - ref.add('pyutilib') - if numpy_available: - ref.add('numpy') diff = set(_[0] for _ in tpl_by_time[-5:]).difference(ref) self.assertEqual( diff, set(), "Unexpected module found in 5 slowest-loading TPL modules" diff --git a/pyomo/environ/tests/test_package_layout.py b/pyomo/environ/tests/test_package_layout.py index 0bc8c55113a..47c6422a879 100644 --- a/pyomo/environ/tests/test_package_layout.py +++ b/pyomo/environ/tests/test_package_layout.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -38,6 +38,7 @@ _NON_MODULE_DIRS = { join('contrib', 'ampl_function_demo', 'src'), join('contrib', 'appsi', 'cmodel', 'src'), + join('contrib', 'simplification', 'ginac', 'src'), join('contrib', 'pynumero', 'src'), join('core', 'tests', 'data', 'baselines'), join('core', 'tests', 'diet', 'baselines'), diff --git a/pyomo/gdp/__init__.py b/pyomo/gdp/__init__.py index 6fc2d4b7351..d204369cdba 100644 --- a/pyomo/gdp/__init__.py +++ b/pyomo/gdp/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,7 +9,13 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from pyomo.gdp.disjunct import GDP_Error, Disjunct, Disjunction +from pyomo.gdp.disjunct import ( + GDP_Error, + Disjunct, + DisjunctData, + Disjunction, + DisjunctionData, +) # Do not import these files: importing them registers the transformation # plugins with the pyomo script so that they get automatically invoked. diff --git a/pyomo/gdp/basic_step.py b/pyomo/gdp/basic_step.py index 69313ac2b1b..56a19e2a0f2 100644 --- a/pyomo/gdp/basic_step.py +++ b/pyomo/gdp/basic_step.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/disjunct.py b/pyomo/gdp/disjunct.py index b95ce252536..637f55cbed1 100644 --- a/pyomo/gdp/disjunct.py +++ b/pyomo/gdp/disjunct.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -41,7 +41,7 @@ ComponentData, ) from pyomo.core.base.global_set import UnindexedComponent_index -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.base.misc import apply_indexed_rule from pyomo.core.base.indexed_component import ActiveIndexedComponent from pyomo.core.expr.expr_common import ExpressionType @@ -412,7 +412,7 @@ def process(arg): return (_Initializer.deferred_value, arg) -class _DisjunctData(_BlockData): +class DisjunctData(BlockData): __autoslot_mappers__ = {'_transformation_block': AutoSlots.weakref_mapper} _Block_reserved_words = set() @@ -424,7 +424,7 @@ def transformation_block(self): ) def __init__(self, component): - _BlockData.__init__(self, component) + BlockData.__init__(self, component) with self._declare_reserved_components(): self.indicator_var = AutoLinkedBooleanVar() self.binary_indicator_var = AutoLinkedBinaryVar(self.indicator_var) @@ -434,23 +434,28 @@ def __init__(self, component): self._transformation_block = None def activate(self): - super(_DisjunctData, self).activate() + super(DisjunctData, self).activate() self.indicator_var.unfix() def deactivate(self): - super(_DisjunctData, self).deactivate() + super(DisjunctData, self).deactivate() self.indicator_var.fix(False) def _deactivate_without_fixing_indicator(self): - super(_DisjunctData, self).deactivate() + super(DisjunctData, self).deactivate() def _activate_without_unfixing_indicator(self): - super(_DisjunctData, self).activate() + super(DisjunctData, self).activate() + + +class _DisjunctData(metaclass=RenamedClass): + __renamed__new_class__ = DisjunctData + __renamed__version__ = '6.7.2' @ModelComponentFactory.register("Disjunctive blocks.") class Disjunct(Block): - _ComponentDataClass = _DisjunctData + _ComponentDataClass = DisjunctData def __new__(cls, *args, **kwds): if cls != Disjunct: @@ -475,7 +480,7 @@ def __init__(self, *args, **kwargs): # def _deactivate_without_fixing_indicator(self): # # Ideally, this would be a super call from this class. However, # # doing that would trigger a call to deactivate() on all the - # # _DisjunctData objects (exactly what we want to avoid!) + # # DisjunctData objects (exactly what we want to avoid!) # # # # For the time being, we will do something bad and directly call # # the base class method from where we would otherwise want to @@ -484,7 +489,7 @@ def __init__(self, *args, **kwargs): def _activate_without_unfixing_indicator(self): # Ideally, this would be a super call from this class. However, # doing that would trigger a call to deactivate() on all the - # _DisjunctData objects (exactly what we want to avoid!) + # DisjunctData objects (exactly what we want to avoid!) # # For the time being, we will do something bad and directly call # the base class method from where we would otherwise want to @@ -495,15 +500,15 @@ def _activate_without_unfixing_indicator(self): component_data._activate_without_unfixing_indicator() -class ScalarDisjunct(_DisjunctData, Disjunct): +class ScalarDisjunct(DisjunctData, Disjunct): def __init__(self, *args, **kwds): ## FIXME: This is a HACK to get around a chicken-and-egg issue - ## where _BlockData creates the indicator_var *before* + ## where BlockData creates the indicator_var *before* ## Block.__init__ declares the _defer_construction flag. self._defer_construction = True self._suppress_ctypes = set() - _DisjunctData.__init__(self, self) + DisjunctData.__init__(self, self) Disjunct.__init__(self, *args, **kwds) self._data[None] = self self._index = UnindexedComponent_index @@ -524,10 +529,10 @@ def active(self): return any(d.active for d in self._data.values()) -_DisjunctData._Block_reserved_words = set(dir(Disjunct())) +DisjunctData._Block_reserved_words = set(dir(Disjunct())) -class _DisjunctionData(ActiveComponentData): +class DisjunctionData(ActiveComponentData): __slots__ = ('disjuncts', 'xor', '_algebraic_constraint', '_transformation_map') __autoslot_mappers__ = {'_algebraic_constraint': AutoSlots.weakref_mapper} _NoArgument = (0,) @@ -542,7 +547,7 @@ def __init__(self, component=None): # # These lines represent in-lining of the # following constructors: - # - _ConstraintData, + # - ConstraintData, # - ActiveComponentData # - ComponentData self._component = weakref_ref(component) if (component is not None) else None @@ -564,16 +569,18 @@ def set_value(self, expr): # IndexedDisjunct indexed by Any which has already been transformed, # the new Disjuncts are Blocks already. This catches them for who # they are anyway. - if isinstance(e, _DisjunctData): - self.disjuncts.append(e) - continue - # The user was lazy and gave us a single constraint - # expression or an iterable of expressions - expressions = [] - if hasattr(e, '__iter__'): + if hasattr(e, 'is_component_type') and e.is_component_type(): + if e.ctype == Disjunct and not e.is_indexed(): + self.disjuncts.append(e) + continue + e_iter = [e] + elif hasattr(e, '__iter__'): e_iter = e else: e_iter = [e] + # The user was lazy and gave us a single constraint + # expression or an iterable of expressions + expressions = [] for _tmpe in e_iter: try: if _tmpe.is_expression_type(): @@ -581,13 +588,12 @@ def set_value(self, expr): continue except AttributeError: pass - msg = "\n\tin %s" % (type(e),) if e_iter is e else "" + msg = " in '%s'" % (type(e).__name__,) if e_iter is e else "" raise ValueError( - "Unexpected term for Disjunction %s.\n" - "\tExpected a Disjunct object, relational expression, " - "or iterable of\n" - "\trelational expressions but got %s%s" - % (self.name, type(_tmpe), msg) + "Unexpected term for Disjunction '%s'.\n" + " Expected a Disjunct object, relational expression, " + "or iterable of\n relational expressions but got '%s'%s" + % (self.name, type(_tmpe).__name__, msg) ) comp = self.parent_component() @@ -619,9 +625,14 @@ def set_value(self, expr): self.disjuncts.append(disjunct) +class _DisjunctionData(metaclass=RenamedClass): + __renamed__new_class__ = DisjunctionData + __renamed__version__ = '6.7.2' + + @ModelComponentFactory.register("Disjunction expressions.") class Disjunction(ActiveIndexedComponent): - _ComponentDataClass = _DisjunctionData + _ComponentDataClass = DisjunctionData def __new__(cls, *args, **kwds): if cls != Disjunction: @@ -699,6 +710,10 @@ def construct(self, data=None): timer = ConstructionTimer(self) self._constructed = True + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() + _self_parent = self.parent_block() if not self.is_indexed(): if self._init_rule is not None: @@ -758,9 +773,9 @@ def _pprint(self): ) -class ScalarDisjunction(_DisjunctionData, Disjunction): +class ScalarDisjunction(DisjunctionData, Disjunction): def __init__(self, *args, **kwds): - _DisjunctionData.__init__(self, component=self) + DisjunctionData.__init__(self, component=self) Disjunction.__init__(self, *args, **kwds) self._index = UnindexedComponent_index @@ -771,7 +786,7 @@ def __init__(self, *args, **kwds): # currently in place). So during initialization only, we will # treat them as "indexed" objects where things like # Constraint.Skip are managed. But after that they will behave - # like _DisjunctionData objects where set_value does not handle + # like DisjunctionData objects where set_value does not handle # Disjunction.Skip but expects a valid expression or None. # diff --git a/pyomo/gdp/plugins/__init__.py b/pyomo/gdp/plugins/__init__.py index 1222ce500f1..875e47e6cc1 100644 --- a/pyomo/gdp/plugins/__init__.py +++ b/pyomo/gdp/plugins/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -22,3 +22,4 @@ def load(): import pyomo.gdp.plugins.multiple_bigm import pyomo.gdp.plugins.transform_current_disjunctive_state import pyomo.gdp.plugins.bound_pretransformation + import pyomo.gdp.plugins.binary_multiplication diff --git a/pyomo/gdp/plugins/between_steps.py b/pyomo/gdp/plugins/between_steps.py index fad783d595d..8f57164334e 100644 --- a/pyomo/gdp/plugins/between_steps.py +++ b/pyomo/gdp/plugins/between_steps.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/plugins/bigm.py b/pyomo/gdp/plugins/bigm.py index c3f65a7ad70..d715d913db8 100644 --- a/pyomo/gdp/plugins/bigm.py +++ b/pyomo/gdp/plugins/bigm.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -13,8 +13,10 @@ import logging +from pyomo.common.autoslots import AutoSlots from pyomo.common.collections import ComponentMap from pyomo.common.config import ConfigDict, ConfigValue +from pyomo.common.gc_manager import PauseGC from pyomo.common.modeling import unique_component_name from pyomo.common.deprecation import deprecated, deprecation_warning from pyomo.contrib.cp.transform.logical_to_disjunctive_program import ( @@ -57,6 +59,26 @@ logger = logging.getLogger('pyomo.gdp.bigm') +class _BigMData(AutoSlots.Mixin): + __slots__ = ('bigm_src',) + + def __init__(self): + # we will keep a map of constraints (hashable, ha!) to a tuple to + # indicate what their M value is and where it came from, of the form: + # ((lower_value, lower_source, lower_key), (upper_value, upper_source, + # upper_key)), where the first tuple is the information for the lower M, + # the second tuple is the info for the upper M, source is the Suffix or + # argument dictionary and None if the value was calculated, and key is + # the key in the Suffix or argument dictionary, and None if it was + # calculated. (Note that it is possible the lower or upper is + # user-specified and the other is not, hence the need to store + # information for both.) + self.bigm_src = {} + + +Block.register_private_data_initializer(_BigMData) + + @TransformationFactory.register( 'gdp.bigm', doc="Relax disjunctive model using big-M terms." ) @@ -93,15 +115,8 @@ class BigM_Transformation(GDP_to_MIP_Transformation, _BigM_MixIn): name beginning "_pyomo_gdp_bigm_reformulation". That Block will contain an indexed Block named "relaxedDisjuncts", which will hold the relaxed disjuncts. This block is indexed by an integer - indicating the order in which the disjuncts were relaxed. - Each block has a dictionary "_constraintMap": - - 'srcConstraints': ComponentMap(: - ) - 'transformedConstraints': ComponentMap(: - ) - - All transformed Disjuncts will have a pointer to the block their transformed + indicating the order in which the disjuncts were relaxed. All + transformed Disjuncts will have a pointer to the block their transformed constraints are on, and all transformed Disjunctions will have a pointer to the corresponding 'Or' or 'ExactlyOne' constraint. @@ -161,6 +176,7 @@ class BigM_Transformation(GDP_to_MIP_Transformation, _BigM_MixIn): def __init__(self): super().__init__(logger) + self._set_up_expr_bound_visitor() def _apply_to(self, instance, **kwds): self.used_args = ComponentMap() # If everything was sure to go well, @@ -169,14 +185,19 @@ def _apply_to(self, instance, **kwds): # as a key in bigMargs, I need the error # not to be when I try to put it into # this map! - try: - self._apply_to_impl(instance, **kwds) - finally: - self._restore_state() - self.used_args.clear() + with PauseGC(): + try: + self._apply_to_impl(instance, **kwds) + finally: + self._restore_state() + self.used_args.clear() + self._expr_bound_visitor.leaf_bounds.clear() + self._expr_bound_visitor.use_fixed_var_values_as_bounds = False def _apply_to_impl(self, instance, **kwds): self._process_arguments(instance, **kwds) + if self._config.assume_fixed_vars_permanent: + self._expr_bound_visitor.use_fixed_var_values_as_bounds = True # filter out inactive targets and handle case where targets aren't # specified. @@ -192,21 +213,15 @@ def _apply_to_impl(self, instance, **kwds): bigM = self._config.bigM for t in preprocessed_targets: if t.ctype is Disjunction: - self._transform_disjunctionData( - t, - t.index(), - bigM, - parent_disjunct=gdp_tree.parent(t), - root_disjunct=gdp_tree.root_disjunct(t), - ) + self._transform_disjunctionData(t, t.index(), bigM, gdp_tree) # issue warnings about anything that was in the bigM args dict that we # didn't use _warn_for_unused_bigM_args(bigM, self.used_args, logger) - def _transform_disjunctionData( - self, obj, index, bigM, parent_disjunct=None, root_disjunct=None - ): + def _transform_disjunctionData(self, obj, index, bigM, gdp_tree): + parent_disjunct = gdp_tree.parent(obj) + root_disjunct = gdp_tree.root_disjunct(obj) (transBlock, xorConstraint) = self._setup_transform_disjunctionData( obj, root_disjunct ) @@ -215,13 +230,12 @@ def _transform_disjunctionData( or_expr = 0 for disjunct in obj.disjuncts: or_expr += disjunct.binary_indicator_var - self._transform_disjunct(disjunct, bigM, transBlock) + self._transform_disjunct(disjunct, bigM, transBlock, gdp_tree) - rhs = 1 if parent_disjunct is None else parent_disjunct.binary_indicator_var if obj.xor: - xorConstraint[index] = or_expr == rhs + xorConstraint[index] = or_expr == 1 else: - xorConstraint[index] = or_expr >= rhs + xorConstraint[index] = or_expr >= 1 # Mark the DisjunctionData as transformed by mapping it to its XOR # constraint. obj._algebraic_constraint = weakref_ref(xorConstraint[index]) @@ -229,7 +243,7 @@ def _transform_disjunctionData( # and deactivate for the writers obj.deactivate() - def _transform_disjunct(self, obj, bigM, transBlock): + def _transform_disjunct(self, obj, bigM, transBlock, gdp_tree): # We're not using the preprocessed list here, so this could be # inactive. We've already done the error checking in preprocessing, so # we just skip it here. @@ -241,17 +255,11 @@ def _transform_disjunct(self, obj, bigM, transBlock): relaxationBlock = self._get_disjunct_transformation_block(obj, transBlock) - # we will keep a map of constraints (hashable, ha!) to a tuple to - # indicate what their M value is and where it came from, of the form: - # ((lower_value, lower_source, lower_key), (upper_value, upper_source, - # upper_key)), where the first tuple is the information for the lower M, - # the second tuple is the info for the upper M, source is the Suffix or - # argument dictionary and None if the value was calculated, and key is - # the key in the Suffix or argument dictionary, and None if it was - # calculated. (Note that it is possible the lower or upper is - # user-specified and the other is not, hence the need to store - # information for both.) - relaxationBlock.bigm_src = {} + indicator_expression = 0 + node = obj + while node is not None: + indicator_expression += 1 - node.binary_indicator_var + node = gdp_tree.parent_disjunct(node) # This is crazy, but if the disjunction has been previously # relaxed, the disjunct *could* be deactivated. This is a big @@ -262,18 +270,26 @@ def _transform_disjunct(self, obj, bigM, transBlock): # comparing the two relaxations. # # Transform each component within this disjunct - self._transform_block_components(obj, obj, bigM, arg_list, suffix_list) + self._transform_block_components( + obj, obj, bigM, arg_list, suffix_list, indicator_expression + ) # deactivate disjunct to keep the writers happy obj._deactivate_without_fixing_indicator() def _transform_constraint( - self, obj, disjunct, bigMargs, arg_list, disjunct_suffix_list + self, + obj, + disjunct, + bigMargs, + arg_list, + disjunct_suffix_list, + indicator_expression, ): # add constraint to the transformation block, we'll transform it there. transBlock = disjunct._transformation_block() - bigm_src = transBlock.bigm_src - constraintMap = transBlock._constraintMap + bigm_src = transBlock.private_data().bigm_src + constraint_map = transBlock.private_data('pyomo.gdp') disjunctionRelaxationBlock = transBlock.parent_block() @@ -340,7 +356,13 @@ def _transform_constraint( bigm_src[c] = (lower, upper) self._add_constraint_expressions( - c, i, M, disjunct.binary_indicator_var, newConstraint, constraintMap + c, + i, + M, + disjunct.binary_indicator_var, + newConstraint, + constraint_map, + indicator_expression=indicator_expression, ) # deactivate because we relaxed @@ -402,10 +424,9 @@ def _update_M_from_suffixes(self, constraint, suffix_list, lower, upper): ) def get_m_value_src(self, constraint): transBlock = _get_constraint_transBlock(constraint) - ( - (lower_val, lower_source, lower_key), - (upper_val, upper_source, upper_key), - ) = transBlock.bigm_src[constraint] + ((lower_val, lower_source, lower_key), (upper_val, upper_source, upper_key)) = ( + transBlock.private_data().bigm_src[constraint] + ) if ( constraint.lower is not None @@ -459,7 +480,7 @@ def get_M_value_src(self, constraint): transBlock = _get_constraint_transBlock(constraint) # This is a KeyError if it fails, but it is also my fault if it # fails... (That is, it's a bug in the mapping.) - return transBlock.bigm_src[constraint] + return transBlock.private_data().bigm_src[constraint] def get_M_value(self, constraint): """Returns the M values used to transform constraint. Return is a tuple: @@ -474,7 +495,7 @@ def get_M_value(self, constraint): transBlock = _get_constraint_transBlock(constraint) # This is a KeyError if it fails, but it is also my fault if it # fails... (That is, it's a bug in the mapping.) - lower, upper = transBlock.bigm_src[constraint] + lower, upper = transBlock.private_data().bigm_src[constraint] return (lower[0], upper[0]) def get_all_M_values_by_constraint(self, model): @@ -494,9 +515,8 @@ def get_all_M_values_by_constraint(self, model): # First check if it was transformed at all. if transBlock is not None: # If it was transformed with BigM, we get the M values. - if hasattr(transBlock, 'bigm_src'): - for cons in transBlock.bigm_src: - m_values[cons] = self.get_M_value(cons) + for cons in transBlock.private_data().bigm_src: + m_values[cons] = self.get_M_value(cons) return m_values def get_largest_M_value(self, model): diff --git a/pyomo/gdp/plugins/bigm_mixin.py b/pyomo/gdp/plugins/bigm_mixin.py index ba25dfeffd0..1c3fcb2c64a 100644 --- a/pyomo/gdp/plugins/bigm_mixin.py +++ b/pyomo/gdp/plugins/bigm_mixin.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -11,7 +11,8 @@ from pyomo.gdp import GDP_Error from pyomo.common.collections import ComponentSet -from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr +from pyomo.contrib.fbbt.expression_bounds_walker import ExpressionBoundsVisitor +import pyomo.contrib.fbbt.interval as interval from pyomo.core import Suffix @@ -103,6 +104,13 @@ def _get_bigM_arg_list(self, bigm_args, block): block = block.parent_block() return arg_list + def _set_up_expr_bound_visitor(self): + # we assume the default config arg for 'assume_fixed_vars_permanent,` + # and we will change it during apply_to if we need to + self._expr_bound_visitor = ExpressionBoundsVisitor( + use_fixed_var_values_as_bounds=False + ) + def _process_M_value( self, m, @@ -210,10 +218,8 @@ def _get_M_from_args(self, constraint, bigMargs, arg_list, lower, upper): return lower, upper def _estimate_M(self, expr, constraint): - expr_lb, expr_ub = compute_bounds_on_expr( - expr, ignore_fixed=not self._config.assume_fixed_vars_permanent - ) - if expr_lb is None or expr_ub is None: + expr_lb, expr_ub = self._expr_bound_visitor.walk_expression(expr) + if expr_lb == -interval.inf or expr_ub == interval.inf: raise GDP_Error( "Cannot estimate M for unbounded " "expressions.\n\t(found while processing " @@ -226,7 +232,14 @@ def _estimate_M(self, expr, constraint): return tuple(M) def _add_constraint_expressions( - self, c, i, M, indicator_var, newConstraint, constraintMap + self, + c, + i, + M, + indicator_var, + newConstraint, + constraint_map, + indicator_expression=None, ): # Since we are both combining components from multiple blocks and using # local names, we need to make sure that the first index for @@ -238,6 +251,8 @@ def _add_constraint_expressions( # over the constraint indices, but I don't think it matters a lot.) unique = len(newConstraint) name = c.local_name + "_%s" % unique + if indicator_expression is None: + indicator_expression = 1 - indicator_var if c.lower is not None: if M[0] is None: @@ -245,25 +260,21 @@ def _add_constraint_expressions( "Cannot relax disjunctive constraint '%s' " "because M is not defined." % name ) - M_expr = M[0] * (1 - indicator_var) + M_expr = M[0] * indicator_expression newConstraint.add((name, i, 'lb'), c.lower <= c.body - M_expr) - constraintMap['transformedConstraints'][c] = [newConstraint[name, i, 'lb']] - constraintMap['srcConstraints'][newConstraint[name, i, 'lb']] = c + constraint_map.transformed_constraints[c].append( + newConstraint[name, i, 'lb'] + ) + constraint_map.src_constraint[newConstraint[name, i, 'lb']] = c if c.upper is not None: if M[1] is None: raise GDP_Error( "Cannot relax disjunctive constraint '%s' " "because M is not defined." % name ) - M_expr = M[1] * (1 - indicator_var) + M_expr = M[1] * indicator_expression newConstraint.add((name, i, 'ub'), c.body - M_expr <= c.upper) - transformed = constraintMap['transformedConstraints'].get(c) - if transformed is not None: - constraintMap['transformedConstraints'][c].append( - newConstraint[name, i, 'ub'] - ) - else: - constraintMap['transformedConstraints'][c] = [ - newConstraint[name, i, 'ub'] - ] - constraintMap['srcConstraints'][newConstraint[name, i, 'ub']] = c + constraint_map.transformed_constraints[c].append( + newConstraint[name, i, 'ub'] + ) + constraint_map.src_constraint[newConstraint[name, i, 'ub']] = c diff --git a/pyomo/gdp/plugins/bilinear.py b/pyomo/gdp/plugins/bilinear.py index feacaaddefc..67390801348 100644 --- a/pyomo/gdp/plugins/bilinear.py +++ b/pyomo/gdp/plugins/bilinear.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/plugins/binary_multiplication.py b/pyomo/gdp/plugins/binary_multiplication.py new file mode 100644 index 00000000000..bea33580ed6 --- /dev/null +++ b/pyomo/gdp/plugins/binary_multiplication.py @@ -0,0 +1,176 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from .gdp_to_mip_transformation import GDP_to_MIP_Transformation +from pyomo.common.config import ConfigDict, ConfigValue +from pyomo.core.base import TransformationFactory +from pyomo.core.util import target_list +from pyomo.gdp import Disjunction +from weakref import ref as weakref_ref +import logging + + +logger = logging.getLogger(__name__) + + +@TransformationFactory.register( + 'gdp.binary_multiplication', + doc="Reformulate the GDP as an MINLP by multiplying f(x) <= 0 by y to get " + "f(x) * y <= 0 where y is the binary corresponding to the Boolean indicator " + "var of the Disjunct containing f(x) <= 0.", +) +class GDPBinaryMultiplicationTransformation(GDP_to_MIP_Transformation): + CONFIG = ConfigDict("gdp.binary_multiplication") + CONFIG.declare( + 'targets', + ConfigValue( + default=None, + domain=target_list, + description="target or list of targets that will be transformed", + doc=""" + + This specifies the list of components to transform. If None (default), the + entire model is transformed. Note that if the transformation is done out + of place, the list of targets should be attached to the model before it + is cloned, and the list will specify the targets on the cloned + instance.""", + ), + ) + + transformation_name = 'binary_multiplication' + + def __init__(self): + super().__init__(logger) + + def _apply_to(self, instance, **kwds): + try: + self._apply_to_impl(instance, **kwds) + finally: + self._restore_state() + + def _apply_to_impl(self, instance, **kwds): + self._process_arguments(instance, **kwds) + + # filter out inactive targets and handle case where targets aren't + # specified. + targets = self._filter_targets(instance) + # transform logical constraints based on targets + self._transform_logical_constraints(instance, targets) + # we need to preprocess targets to make sure that if there are any + # disjunctions in targets that their disjuncts appear before them in + # the list. + gdp_tree = self._get_gdp_tree_from_targets(instance, targets) + preprocessed_targets = gdp_tree.reverse_topological_sort() + + for t in preprocessed_targets: + if t.ctype is Disjunction: + self._transform_disjunctionData( + t, + t.index(), + parent_disjunct=gdp_tree.parent(t), + root_disjunct=gdp_tree.root_disjunct(t), + ) + + def _transform_disjunctionData( + self, obj, index, parent_disjunct=None, root_disjunct=None + ): + (transBlock, xorConstraint) = self._setup_transform_disjunctionData( + obj, root_disjunct + ) + + # add or (or xor) constraint + or_expr = 0 + for disjunct in obj.disjuncts: + or_expr += disjunct.binary_indicator_var + self._transform_disjunct(disjunct, transBlock) + + if obj.xor: + xorConstraint[index] = or_expr == 1 + else: + xorConstraint[index] = or_expr >= 1 + # Mark the DisjunctionData as transformed by mapping it to its XOR + # constraint. + obj._algebraic_constraint = weakref_ref(xorConstraint[index]) + + # and deactivate for the writers + obj.deactivate() + + def _transform_disjunct(self, obj, transBlock): + # We're not using the preprocessed list here, so this could be + # inactive. We've already done the error checking in preprocessing, so + # we just skip it here. + if not obj.active: + return + + relaxationBlock = self._get_disjunct_transformation_block(obj, transBlock) + + # Transform each component within this disjunct + self._transform_block_components(obj, obj) + + # deactivate disjunct to keep the writers happy + obj._deactivate_without_fixing_indicator() + + def _transform_constraint(self, obj, disjunct): + # add constraint to the transformation block, we'll transform it there. + transBlock = disjunct._transformation_block() + constraint_map = transBlock.private_data('pyomo.gdp') + + disjunctionRelaxationBlock = transBlock.parent_block() + + # We will make indexes from ({obj.local_name} x obj.index_set() x ['lb', + # 'ub']), but don't bother construct that set here, as taking Cartesian + # products is kind of expensive (and redundant since we have the + # original model) + newConstraint = transBlock.transformedConstraints + + for i in sorted(obj.keys()): + c = obj[i] + if not c.active: + continue + + self._add_constraint_expressions( + c, i, disjunct.binary_indicator_var, newConstraint, constraint_map + ) + + # deactivate because we relaxed + c.deactivate() + + def _add_constraint_expressions( + self, c, i, indicator_var, newConstraint, constraint_map + ): + # Since we are both combining components from multiple blocks and using + # local names, we need to make sure that the first index for + # transformedConstraints is guaranteed to be unique. We just grab the + # current length of the list here since that will be monotonically + # increasing and hence unique. We'll append it to the + # slightly-more-human-readable constraint name for something familiar + # but unique. (Note that we really could do this outside of the loop + # over the constraint indices, but I don't think it matters a lot.) + unique = len(newConstraint) + name = c.local_name + "_%s" % unique + transformed = constraint_map.transformed_constraints[c] + + lb, ub = c.lower, c.upper + if (c.equality or lb is ub) and lb is not None: + # equality + newConstraint.add((name, i, 'eq'), (c.body - lb) * indicator_var == 0) + transformed.append(newConstraint[name, i, 'eq']) + constraint_map.src_constraint[newConstraint[name, i, 'eq']] = c + else: + # inequality + if lb is not None: + newConstraint.add((name, i, 'lb'), 0 <= (c.body - lb) * indicator_var) + transformed.append(newConstraint[name, i, 'lb']) + constraint_map.src_constraint[newConstraint[name, i, 'lb']] = c + if ub is not None: + newConstraint.add((name, i, 'ub'), (c.body - ub) * indicator_var <= 0) + transformed.append(newConstraint[name, i, 'ub']) + constraint_map.src_constraint[newConstraint[name, i, 'ub']] = c diff --git a/pyomo/gdp/plugins/bound_pretransformation.py b/pyomo/gdp/plugins/bound_pretransformation.py index 0d6b14a4b80..7c90c24d869 100644 --- a/pyomo/gdp/plugins/bound_pretransformation.py +++ b/pyomo/gdp/plugins/bound_pretransformation.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -244,9 +244,9 @@ def _create_transformation_constraints( disjunction, transformation_blocks ) if self.transformation_name not in disjunction._transformation_map: - disjunction._transformation_map[ - self.transformation_name - ] = ComponentMap() + disjunction._transformation_map[self.transformation_name] = ( + ComponentMap() + ) trans_map = disjunction._transformation_map[self.transformation_name] for disj in disjunction.disjuncts: diff --git a/pyomo/gdp/plugins/chull.py b/pyomo/gdp/plugins/chull.py index d226c57aae7..c11d8ea0729 100644 --- a/pyomo/gdp/plugins/chull.py +++ b/pyomo/gdp/plugins/chull.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/plugins/cuttingplane.py b/pyomo/gdp/plugins/cuttingplane.py index 49d984a0712..6c77a582987 100644 --- a/pyomo/gdp/plugins/cuttingplane.py +++ b/pyomo/gdp/plugins/cuttingplane.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -15,7 +15,7 @@ Implements a general cutting plane-based reformulation for linear and convex GDPs. """ -from __future__ import division + from pyomo.common.config import ( ConfigBlock, @@ -808,13 +808,9 @@ def _apply_to(self, instance, bigM=None, **kwds): else: self.verbose = False - ( - instance_rBigM, - cuts_obj, - instance_rHull, - var_info, - transBlockName, - ) = self._setup_subproblems(instance, bigM, self._config.tighten_relaxation) + (instance_rBigM, cuts_obj, instance_rHull, var_info, transBlockName) = ( + self._setup_subproblems(instance, bigM, self._config.tighten_relaxation) + ) self._generate_cuttingplanes( instance_rBigM, cuts_obj, instance_rHull, var_info, transBlockName diff --git a/pyomo/gdp/plugins/fix_disjuncts.py b/pyomo/gdp/plugins/fix_disjuncts.py index d0f59ce87ce..172363caab7 100644 --- a/pyomo/gdp/plugins/fix_disjuncts.py +++ b/pyomo/gdp/plugins/fix_disjuncts.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -52,7 +52,7 @@ class GDP_Disjunct_Fixer(Transformation): This reclassifies all disjuncts in the passed model instance as ctype Block and deactivates the constraints and disjunctions within inactive disjuncts. - In addition, it transforms relvant LogicalConstraints and BooleanVars so + In addition, it transforms relevant LogicalConstraints and BooleanVars so that the resulting model is a (MI)(N)LP (where it is only mixed-integer if the model contains integer-domain Vars or BooleanVars which were not indicator_vars of Disjuncs. diff --git a/pyomo/gdp/plugins/gdp_to_mip_transformation.py b/pyomo/gdp/plugins/gdp_to_mip_transformation.py index 0aa5ec163b6..8dcd22b292a 100644 --- a/pyomo/gdp/plugins/gdp_to_mip_transformation.py +++ b/pyomo/gdp/plugins/gdp_to_mip_transformation.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -11,7 +11,8 @@ from functools import wraps -from pyomo.common.collections import ComponentMap +from pyomo.common.autoslots import AutoSlots +from pyomo.common.collections import ComponentMap, DefaultComponentMap from pyomo.common.log import is_debug_set from pyomo.common.modeling import unique_component_name @@ -48,6 +49,17 @@ from weakref import ref as weakref_ref +class _GDPTransformationData(AutoSlots.Mixin): + __slots__ = ('src_constraint', 'transformed_constraints') + + def __init__(self): + self.src_constraint = ComponentMap() + self.transformed_constraints = DefaultComponentMap(list) + + +Block.register_private_data_initializer(_GDPTransformationData, scope='pyomo.gdp') + + class GDP_to_MIP_Transformation(Transformation): """ Base class for transformations from GDP to MIP @@ -213,21 +225,26 @@ def _setup_transform_disjunctionData(self, obj, root_disjunct): "likely indicative of a modeling error." % obj.name ) - # Create or fetch the transformation block + # We always need to create or fetch a transformation block on the parent block. + trans_block, new_block = self._add_transformation_block(obj.parent_block()) + # This is where we put exactly_one/or constraint + algebraic_constraint = self._add_xor_constraint( + obj.parent_component(), trans_block + ) + + # If requested, create or fetch the transformation block above the + # nested hierarchy if root_disjunct is not None: - # We want to put all the transformed things on the root - # Disjunct's parent's block so that they do not get - # re-transformed - transBlock, new_block = self._add_transformation_block( + # We want to put some transformed things on the root Disjunct's + # parent's block so that they do not get re-transformed. (Note this + # is never true for hull, but it calls this method with + # root_disjunct=None. BigM can't put the exactly-one constraint up + # here, but it can put everything else.) + trans_block, new_block = self._add_transformation_block( root_disjunct.parent_block() ) - else: - # This isn't nested--just put it on the parent block. - transBlock, new_block = self._add_transformation_block(obj.parent_block()) - - xorConstraint = self._add_xor_constraint(obj.parent_component(), transBlock) - return transBlock, xorConstraint + return trans_block, algebraic_constraint def _get_disjunct_transformation_block(self, disjunct, transBlock): if disjunct.transformation_block is not None: @@ -238,14 +255,7 @@ def _get_disjunct_transformation_block(self, disjunct, transBlock): relaxationBlock = relaxedDisjuncts[len(relaxedDisjuncts)] relaxationBlock.transformedConstraints = Constraint(Any) - relaxationBlock.localVarReferences = Block() - # add the map that will link back and forth between transformed - # constraints and their originals. - relaxationBlock._constraintMap = { - 'srcConstraints': ComponentMap(), - 'transformedConstraints': ComponentMap(), - } # add mappings to source disjunct (so we'll know we've relaxed) disjunct._transformation_block = weakref_ref(relaxationBlock) diff --git a/pyomo/gdp/plugins/gdp_var_mover.py b/pyomo/gdp/plugins/gdp_var_mover.py index df659670bf4..7b1df0bb68f 100644 --- a/pyomo/gdp/plugins/gdp_var_mover.py +++ b/pyomo/gdp/plugins/gdp_var_mover.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -115,7 +115,7 @@ def _apply_to(self, instance, **kwds): disjunct_component, Block ) # HACK: activate the block, but do not activate the - # _BlockData objects + # BlockData objects super(ActiveIndexedComponent, disjunct_component).activate() # Deactivate all constraints. Note that we only need to diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index b8e2b3e3699..854366c0cf0 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -11,9 +11,12 @@ import logging +from collections import defaultdict + +from pyomo.common.autoslots import AutoSlots import pyomo.common.config as cfg from pyomo.common import deprecated -from pyomo.common.collections import ComponentMap, ComponentSet +from pyomo.common.collections import ComponentMap, ComponentSet, DefaultComponentMap from pyomo.common.modeling import unique_component_name from pyomo.core.expr.numvalue import ZeroConstant import pyomo.core.expr as EXPR @@ -39,6 +42,7 @@ Binary, ) from pyomo.gdp import Disjunct, Disjunction, GDP_Error +from pyomo.gdp.disjunct import DisjunctData from pyomo.gdp.plugins.gdp_to_mip_transformation import GDP_to_MIP_Transformation from pyomo.gdp.transformed_disjunct import _TransformedDisjunct from pyomo.gdp.util import ( @@ -47,11 +51,30 @@ _warn_for_active_disjunct, ) from pyomo.core.util import target_list +from pyomo.util.vars_from_expressions import get_vars_from_components from weakref import ref as weakref_ref logger = logging.getLogger('pyomo.gdp.hull') +class _HullTransformationData(AutoSlots.Mixin): + __slots__ = ( + 'disaggregated_var_map', + 'original_var_map', + 'bigm_constraint_map', + 'disaggregation_constraint_map', + ) + + def __init__(self): + self.disaggregated_var_map = DefaultComponentMap(ComponentMap) + self.original_var_map = ComponentMap() + self.bigm_constraint_map = DefaultComponentMap(ComponentMap) + self.disaggregation_constraint_map = DefaultComponentMap(ComponentMap) + + +Block.register_private_data_initializer(_HullTransformationData) + + @TransformationFactory.register( 'gdp.hull', doc="Relax disjunctive model by forming the hull reformulation." ) @@ -76,35 +99,13 @@ class Hull_Reformulation(GDP_to_MIP_Transformation): list of blocks and Disjunctions [default: the instance] The transformation will create a new Block with a unique - name beginning "_pyomo_gdp_hull_reformulation". - The block will have a dictionary "_disaggregatedVarMap: - 'srcVar': ComponentMap(:), - 'disaggregatedVar': ComponentMap(:) - - It will also have a ComponentMap "_bigMConstraintMap": - - : - - Last, it will contain an indexed Block named "relaxedDisjuncts", - which will hold the relaxed disjuncts. This block is indexed by - an integer indicating the order in which the disjuncts were relaxed. - Each block has a dictionary "_constraintMap": - - 'srcConstraints': ComponentMap(: - ), - 'transformedConstraints': - ComponentMap( : - , - : []) - - All transformed Disjuncts will have a pointer to the block their transformed - constraints are on, and all transformed Disjunctions will have a - pointer to the corresponding OR or XOR constraint. - - The _pyomo_gdp_hull_reformulation block will have a ComponentMap - "_disaggregationConstraintMap": - :ComponentMap(: ) - + name beginning "_pyomo_gdp_hull_reformulation". It will contain an + indexed Block named "relaxedDisjuncts" that will hold the relaxed + disjuncts. This block is indexed by an integer indicating the order + in which the disjuncts were relaxed. All transformed Disjuncts will + have a pointer to the block their transformed constraints are on, + and all transformed Disjunctions will have a pointer to the + corresponding OR or XOR constraint. """ CONFIG = cfg.ConfigDict('gdp.hull') @@ -204,33 +205,40 @@ def __init__(self): super().__init__(logger) self._targets = set() - def _add_local_vars(self, block, local_var_dict): + def _collect_local_vars_from_block(self, block, local_var_dict): localVars = block.component('LocalVars') - if type(localVars) is Suffix: + if localVars is not None and localVars.ctype is Suffix: for disj, var_list in localVars.items(): - if local_var_dict.get(disj) is None: - local_var_dict[disj] = ComponentSet(var_list) - else: - local_var_dict[disj].update(var_list) - - def _get_local_var_suffixes(self, block, local_var_dict): - # You can specify suffixes on any block (disjuncts included). This - # method starts from a Disjunct (presumably) and checks for a LocalVar - # suffixes going both up and down the tree, adding them into the - # dictionary that is the second argument. - - # first look beneath where we are (there could be Blocks on this - # disjunct) - for b in block.component_data_objects( - Block, descend_into=(Block), active=True, sort=SortComponents.deterministic - ): - self._add_local_vars(b, local_var_dict) - # now traverse upwards and get what's above - while block is not None: - self._add_local_vars(block, local_var_dict) - block = block.parent_block() - - return local_var_dict + local_var_dict[disj].update(var_list) + + def _get_user_defined_local_vars(self, targets): + user_defined_local_vars = defaultdict(ComponentSet) + seen_blocks = set() + # we go through the targets looking both up and down the hierarchy, but + # we cache what Blocks/Disjuncts we've already looked on so that we + # don't duplicate effort. + for t in targets: + if t.ctype is Disjunct: + # first look beneath where we are (there could be Blocks on this + # disjunct) + for b in t.component_data_objects( + Block, + descend_into=Block, + active=True, + sort=SortComponents.deterministic, + ): + if b not in seen_blocks: + self._collect_local_vars_from_block(b, user_defined_local_vars) + seen_blocks.add(b) + # now look up in the tree + blk = t + while blk is not None: + if blk in seen_blocks: + break + self._collect_local_vars_from_block(blk, user_defined_local_vars) + seen_blocks.add(blk) + blk = blk.parent_block() + return user_defined_local_vars def _apply_to(self, instance, **kwds): try: @@ -239,7 +247,6 @@ def _apply_to(self, instance, **kwds): self._restore_state() self._transformation_blocks.clear() self._algebraic_constraints.clear() - self._targets_set = set() def _apply_to_impl(self, instance, **kwds): self._process_arguments(instance, **kwds) @@ -253,16 +260,17 @@ def _apply_to_impl(self, instance, **kwds): # Preprocess in order to find what disjunctive components need # transformation gdp_tree = self._get_gdp_tree_from_targets(instance, targets) - preprocessed_targets = gdp_tree.topological_sort() - self._targets_set = set(preprocessed_targets) + # Transform from leaf to root: This is important for hull because for + # nested GDPs, we will introduce variables that need disaggregating into + # parent Disjuncts as we transform their child Disjunctions. + preprocessed_targets = gdp_tree.reverse_topological_sort() + # Get all LocalVars from Suffixes ahead of time + local_vars_by_disjunct = self._get_user_defined_local_vars(preprocessed_targets) for t in preprocessed_targets: if t.ctype is Disjunction: self._transform_disjunctionData( - t, - t.index(), - parent_disjunct=gdp_tree.parent(t), - root_disjunct=gdp_tree.root_disjunct(t), + t, t.index(), gdp_tree.parent(t), local_vars_by_disjunct ) # We skip disjuncts now, because we need information from the # disjunctions to transform them (which variables to disaggregate), @@ -274,23 +282,11 @@ def _add_transformation_block(self, to_block): return transBlock, new_block transBlock.lbub = Set(initialize=['lb', 'ub', 'eq']) - # Map between disaggregated variables and their - # originals - transBlock._disaggregatedVarMap = { - 'srcVar': ComponentMap(), - 'disaggregatedVar': ComponentMap(), - } - # Map between disaggregated variables and their lb*indicator <= var <= - # ub*indicator constraints - transBlock._bigMConstraintMap = ComponentMap() + # We will store all of the disaggregation constraints for any # Disjunctions we transform onto this block here. transBlock.disaggregationConstraints = Constraint(NonNegativeIntegers) - # This will map from srcVar to a map of srcDisjunction to the - # disaggregation constraint corresponding to srcDisjunction - transBlock._disaggregationConstraintMap = ComponentMap() - # we are going to store some of the disaggregated vars directly here # when we have vars that don't appear in every disjunct transBlock._disaggregatedVars = Var(NonNegativeIntegers, dense=False) @@ -299,46 +295,55 @@ def _add_transformation_block(self, to_block): return transBlock, True def _transform_disjunctionData( - self, obj, index, parent_disjunct=None, root_disjunct=None + self, obj, index, parent_disjunct, local_vars_by_disjunct ): # Hull reformulation doesn't work if this is an OR constraint. So if # xor is false, give up if not obj.xor: raise GDP_Error( "Cannot do hull reformulation for " - "Disjunction '%s' with OR constraint. " + "Disjunction '%s' with OR constraint. " "Must be an XOR!" % obj.name ) - + # collect the Disjuncts we are going to transform now because we will + # change their active status when we transform them, but we still need + # this list after the fact. + active_disjuncts = [disj for disj in obj.disjuncts if disj.active] + + # We put *all* transformed things on the parent Block of this + # disjunction. We'll mark the disaggregated Vars as local, but beyond + # that, we actually need everything to get transformed again as we go up + # the nested hierarchy (if there is one) transBlock, xorConstraint = self._setup_transform_disjunctionData( - obj, root_disjunct + obj, root_disjunct=None ) disaggregationConstraint = transBlock.disaggregationConstraints - disaggregationConstraintMap = transBlock._disaggregationConstraintMap + disaggregationConstraintMap = ( + transBlock.private_data().disaggregation_constraint_map + ) disaggregatedVars = transBlock._disaggregatedVars disaggregated_var_bounds = transBlock._boundsConstraints - # We first go through and collect all the variables that we - # are going to disaggregate. - varOrder_set = ComponentSet() - varOrder = [] - varsByDisjunct = ComponentMap() - localVarsByDisjunct = ComponentMap() - include_fixed_vars = not self._config.assume_fixed_vars_permanent - for disjunct in obj.disjuncts: - if not disjunct.active: - continue - disjunctVars = varsByDisjunct[disjunct] = ComponentSet() + # We first go through and collect all the variables that we are going to + # disaggregate. We do this in its own pass because we want to know all + # the Disjuncts that each Var appears in since that will tell us exactly + # which diaggregated variables we need. + var_order = ComponentSet() + disjuncts_var_appears_in = ComponentMap() + # For each disjunct in the disjunction, we will store a list of Vars + # that need a disaggregated counterpart in that disjunct. + disjunct_disaggregated_var_map = {} + for disjunct in active_disjuncts: # create the key for each disjunct now - transBlock._disaggregatedVarMap['disaggregatedVar'][ - disjunct - ] = ComponentMap() - for cons in disjunct.component_data_objects( + disjunct_disaggregated_var_map[disjunct] = ComponentMap() + for var in get_vars_from_components( + disjunct, Constraint, + include_fixed=not self._config.assume_fixed_vars_permanent, active=True, sort=SortComponents.deterministic, - descend_into=(Block, Disjunct), + descend_into=Block, ): # [ESJ 02/14/2020] By default, we disaggregate fixed variables # on the philosophy that fixing is not a promise for the future @@ -347,189 +352,159 @@ def _transform_disjunctionData( # with their transformed model. However, the user may have set # assume_fixed_vars_permanent to True in which case we will skip # them - for var in EXPR.identify_variables( - cons.body, include_fixed=include_fixed_vars - ): - # Note the use of a list so that we will - # eventually disaggregate the vars in a - # deterministic order (the order that we found - # them) - disjunctVars.add(var) - if not var in varOrder_set: - varOrder.append(var) - varOrder_set.add(var) - - # check for LocalVars Suffix - localVarsByDisjunct = self._get_local_var_suffixes( - disjunct, localVarsByDisjunct - ) - # We will disaggregate all variables that are not explicitly declared as - # being local. Since we transform from leaf to root, we are implicitly - # treating our own disaggregated variables as local, so they will not be + # Note that, because ComponentSets are ordered, we will + # eventually disaggregate the vars in a deterministic order + # (the order that we found them) + if var not in var_order: + var_order.add(var) + disjuncts_var_appears_in[var] = ComponentSet([disjunct]) + else: + disjuncts_var_appears_in[var].add(disjunct) + + # Now, we will disaggregate all variables that are not explicitly + # declared as being local. If we are moving up in a nested tree, we have + # marked our own disaggregated variables as local, so they will not be # re-disaggregated. - varSet = [] - varSet = {disj: [] for disj in obj.disjuncts} - # Note that variables are local with respect to a Disjunct. We deal with - # them here to do some error checking (if something is obviously not - # local since it is used in multiple Disjuncts in this Disjunction) and - # also to get a deterministic order in which to process them when we - # transform the Disjuncts: Values of localVarsByDisjunct are - # ComponentSets, so we need this for determinism (we iterate through the - # localVars of a Disjunct later) - localVars = ComponentMap() - varsToDisaggregate = [] - disjunctsVarAppearsIn = ComponentMap() - for var in varOrder: - disjuncts = disjunctsVarAppearsIn[var] = [ - d for d in varsByDisjunct if var in varsByDisjunct[d] - ] + vars_to_disaggregate = {disj: ComponentSet() for disj in obj.disjuncts} + all_vars_to_disaggregate = ComponentSet() + # We will ignore variables declared as local in a Disjunct that don't + # actually appear in any Constraints on that Disjunct, but in order to + # do this, we will explicitly collect the set of local_vars in this + # loop. + local_vars = defaultdict(ComponentSet) + for var in var_order: + disjuncts = disjuncts_var_appears_in[var] # clearly not local if used in more than one disjunct if len(disjuncts) > 1: if self._generate_debug_messages: logger.debug( "Assuming '%s' is not a local var since it is" - "used in multiple disjuncts." - % var.getname(fully_qualified=True) + "used in multiple disjuncts." % var.name ) for disj in disjuncts: - varSet[disj].append(var) - varsToDisaggregate.append(var) - # disjuncts is a list of length 1 - elif localVarsByDisjunct.get(disjuncts[0]) is not None: - if var in localVarsByDisjunct[disjuncts[0]]: - localVars_thisDisjunct = localVars.get(disjuncts[0]) - if localVars_thisDisjunct is not None: - localVars[disjuncts[0]].append(var) - else: - localVars[disjuncts[0]] = [var] - else: - # It's not local to this Disjunct - varSet[disjuncts[0]].append(var) - varsToDisaggregate.append(var) - else: - # We don't even have have any local vars for this Disjunct. - varSet[disjuncts[0]].append(var) - varsToDisaggregate.append(var) + vars_to_disaggregate[disj].add(var) + all_vars_to_disaggregate.add(var) + else: # var only appears in one disjunct + disjunct = next(iter(disjuncts)) + # We check if the user declared it as local + if disjunct in local_vars_by_disjunct: + if var in local_vars_by_disjunct[disjunct]: + local_vars[disjunct].add(var) + continue + # It's not declared local to this Disjunct, so we + # disaggregate + vars_to_disaggregate[disjunct].add(var) + all_vars_to_disaggregate.add(var) # Now that we know who we need to disaggregate, we will do it # while we also transform the disjuncts. - local_var_set = self._get_local_var_set(obj) + + # Get the list of local variables for the parent Disjunct so that we can + # add the disaggregated variables we're about to make to it: + parent_local_var_list = self._get_local_var_list(parent_disjunct) or_expr = 0 for disjunct in obj.disjuncts: or_expr += disjunct.indicator_var.get_associated_binary() - self._transform_disjunct( - disjunct, - transBlock, - varSet[disjunct], - localVars.get(disjunct, []), - local_var_set, - ) - rhs = 1 if parent_disjunct is None else parent_disjunct.binary_indicator_var - xorConstraint.add(index, (or_expr, rhs)) + if disjunct.active: + self._transform_disjunct( + obj=disjunct, + transBlock=transBlock, + vars_to_disaggregate=vars_to_disaggregate[disjunct], + local_vars=local_vars[disjunct], + parent_local_var_suffix=parent_local_var_list, + parent_disjunct_local_vars=local_vars_by_disjunct[parent_disjunct], + disjunct_disaggregated_var_map=disjunct_disaggregated_var_map, + ) + xorConstraint.add(index, (or_expr, 1)) # map the DisjunctionData to its XOR constraint to mark it as # transformed obj._algebraic_constraint = weakref_ref(xorConstraint[index]) - # add the reaggregation constraints - for i, var in enumerate(varsToDisaggregate): + # Now add the reaggregation constraints + for var in all_vars_to_disaggregate: # There are two cases here: Either the var appeared in every # disjunct in the disjunction, or it didn't. If it did, there's # nothing special to do: All of the disaggregated variables have # been created, and we can just proceed and make this constraint. If # it didn't, we need one more disaggregated variable, correctly # defined. And then we can make the constraint. - if len(disjunctsVarAppearsIn[var]) < len(obj.disjuncts): + if len(disjuncts_var_appears_in[var]) < len(active_disjuncts): # create one more disaggregated var idx = len(disaggregatedVars) disaggregated_var = disaggregatedVars[idx] - # mark this as local because we won't re-disaggregate if this is - # a nested disjunction - if local_var_set is not None: - local_var_set.append(disaggregated_var) + # mark this as local because we won't re-disaggregate it if this + # is a nested disjunction + if parent_local_var_list is not None: + parent_local_var_list.append(disaggregated_var) + local_vars_by_disjunct[parent_disjunct].add(disaggregated_var) var_free = 1 - sum( disj.indicator_var.get_associated_binary() - for disj in disjunctsVarAppearsIn[var] + for disj in disjuncts_var_appears_in[var] ) self._declare_disaggregated_var_bounds( - var, - disaggregated_var, - obj, - disaggregated_var_bounds, - (idx, 'lb'), - (idx, 'ub'), - var_free, + original_var=var, + disaggregatedVar=disaggregated_var, + disjunct=obj, + bigmConstraint=disaggregated_var_bounds, + lb_idx=(idx, 'lb'), + ub_idx=(idx, 'ub'), + var_free_indicator=var_free, + ) + # Update mappings: + var_info = var.parent_block().private_data() + disaggregated_var_map = var_info.disaggregated_var_map + dis_var_info = disaggregated_var.parent_block().private_data() + + dis_var_info.bigm_constraint_map[disaggregated_var][obj] = Reference( + disaggregated_var_bounds[idx, :] ) - # maintain the mappings - for disj in obj.disjuncts: + dis_var_info.original_var_map[disaggregated_var] = var + + # For every Disjunct the Var does not appear in, we want to map + # that this new variable is its disaggreggated variable. + for disj in active_disjuncts: # Because we called _transform_disjunct above, we know that # if this isn't transformed it is because it was cleanly # deactivated, and we can just skip it. if ( disj._transformation_block is not None - and disj not in disjunctsVarAppearsIn[var] + and disj not in disjuncts_var_appears_in[var] ): - relaxationBlock = disj._transformation_block().parent_block() - relaxationBlock._bigMConstraintMap[ - disaggregated_var - ] = Reference(disaggregated_var_bounds[idx, :]) - relaxationBlock._disaggregatedVarMap['srcVar'][ - disaggregated_var - ] = var - relaxationBlock._disaggregatedVarMap['disaggregatedVar'][disj][ - var - ] = disaggregated_var + disaggregated_var_map[disj][var] = disaggregated_var + # start the expression for the reaggregation constraint with + # this var disaggregatedExpr = disaggregated_var else: disaggregatedExpr = 0 - for disjunct in disjunctsVarAppearsIn[var]: - if disjunct._transformation_block is None: - # Because we called _transform_disjunct above, we know that - # if this isn't transformed it is because it was cleanly - # deactivated, and we can just skip it. - continue + for disjunct in disjuncts_var_appears_in[var]: + disaggregatedExpr += disjunct_disaggregated_var_map[disjunct][var] - disaggregatedVar = ( - disjunct._transformation_block() - .parent_block() - ._disaggregatedVarMap['disaggregatedVar'][disjunct][var] - ) - disaggregatedExpr += disaggregatedVar - - # We equate the sum of the disaggregated vars to var (the original) - # if parent_disjunct is None, else it needs to be the disaggregated - # var corresponding to var on the parent disjunct. This is the - # reason we transform from root to leaf: This constraint is now - # correct regardless of how nested something may have been. - parent_var = ( - var - if parent_disjunct is None - else self.get_disaggregated_var(var, parent_disjunct) - ) cons_idx = len(disaggregationConstraint) - disaggregationConstraint.add(cons_idx, parent_var == disaggregatedExpr) + # We always aggregate to the original var. If this is nested, this + # constraint will be transformed again. (And if it turns out + # everything in it is local, then that transformation won't actually + # change the mathematical expression, so it's okay. + disaggregationConstraint.add(cons_idx, var == disaggregatedExpr) # and update the map so that we can find this later. We index by # variable and the particular disjunction because there is a # different one for each disjunction - if disaggregationConstraintMap.get(var) is not None: - disaggregationConstraintMap[var][obj] = disaggregationConstraint[ - cons_idx - ] - else: - thismap = disaggregationConstraintMap[var] = ComponentMap() - thismap[obj] = disaggregationConstraint[cons_idx] + disaggregationConstraintMap[var][obj] = disaggregationConstraint[cons_idx] # deactivate for the writers obj.deactivate() - def _transform_disjunct(self, obj, transBlock, varSet, localVars, local_var_set): - # We're not using the preprocessed list here, so this could be - # inactive. We've already done the error checking in preprocessing, so - # we just skip it here. - if not obj.active: - return - + def _transform_disjunct( + self, + obj, + transBlock, + vars_to_disaggregate, + local_vars, + parent_local_var_suffix, + parent_disjunct_local_vars, + disjunct_disaggregated_var_map, + ): relaxationBlock = self._get_disjunct_transformation_block(obj, transBlock) # Put the disaggregated variables all on their own block so that we can @@ -539,7 +514,7 @@ def _transform_disjunct(self, obj, transBlock, varSet, localVars, local_var_set) # add the disaggregated variables and their bigm constraints # to the relaxationBlock - for var in varSet: + for var in vars_to_disaggregate: disaggregatedVar = Var(within=Reals, initialize=var.value) # naming conflicts are possible here since this is a bunch # of variables from different blocks coming together, so we @@ -550,10 +525,13 @@ def _transform_disjunct(self, obj, transBlock, varSet, localVars, local_var_set) relaxationBlock.disaggregatedVars.add_component( disaggregatedVarName, disaggregatedVar ) - # mark this as local because we won't re-disaggregate if this is a - # nested disjunction - if local_var_set is not None: - local_var_set.append(disaggregatedVar) + # mark this as local via the Suffix in case this is a partial + # transformation: + if parent_local_var_suffix is not None: + parent_local_var_suffix.append(disaggregatedVar) + # Record that it's local for our own bookkeeping in case we're in a + # nested tree in *this* transformation + parent_disjunct_local_vars.add(disaggregatedVar) # add the bigm constraint bigmConstraint = Constraint(transBlock.lbub) @@ -562,19 +540,22 @@ def _transform_disjunct(self, obj, transBlock, varSet, localVars, local_var_set) ) self._declare_disaggregated_var_bounds( - var, - disaggregatedVar, - obj, - bigmConstraint, - 'lb', - 'ub', - obj.indicator_var.get_associated_binary(), - transBlock, + original_var=var, + disaggregatedVar=disaggregatedVar, + disjunct=obj, + bigmConstraint=bigmConstraint, + lb_idx='lb', + ub_idx='ub', + var_free_indicator=obj.indicator_var.get_associated_binary(), ) + # update the bigm constraint mappings + data_dict = disaggregatedVar.parent_block().private_data() + data_dict.bigm_constraint_map[disaggregatedVar][obj] = bigmConstraint + disjunct_disaggregated_var_map[obj][var] = disaggregatedVar - for var in localVars: - # we don't need to disaggregated, we can use this Var, but we do - # need to set up its bounds constraints. + for var in local_vars: + # we don't need to disaggregate, i.e., we can use this Var, but we + # do need to set up its bounds constraints. # naming conflicts are possible here since this is a bunch # of variables from different blocks coming together, so we @@ -585,36 +566,38 @@ def _transform_disjunct(self, obj, transBlock, varSet, localVars, local_var_set) bigmConstraint = Constraint(transBlock.lbub) relaxationBlock.add_component(conName, bigmConstraint) + parent_block = var.parent_block() + self._declare_disaggregated_var_bounds( - var, - var, - obj, - bigmConstraint, - 'lb', - 'ub', - obj.indicator_var.get_associated_binary(), - transBlock, + original_var=var, + disaggregatedVar=var, + disjunct=obj, + bigmConstraint=bigmConstraint, + lb_idx='lb', + ub_idx='ub', + var_free_indicator=obj.indicator_var.get_associated_binary(), ) + # update the bigm constraint mappings + data_dict = var.parent_block().private_data() + data_dict.bigm_constraint_map[var][obj] = bigmConstraint + disjunct_disaggregated_var_map[obj][var] = var var_substitute_map = dict( - (id(v), newV) - for v, newV in transBlock._disaggregatedVarMap['disaggregatedVar'][ - obj - ].items() + (id(v), newV) for v, newV in disjunct_disaggregated_var_map[obj].items() ) zero_substitute_map = dict( (id(v), ZeroConstant) - for v, newV in transBlock._disaggregatedVarMap['disaggregatedVar'][ - obj - ].items() + for v, newV in disjunct_disaggregated_var_map[obj].items() ) - zero_substitute_map.update((id(v), ZeroConstant) for v in localVars) # Transform each component within this disjunct self._transform_block_components( obj, obj, var_substitute_map, zero_substitute_map ) + # Anything that was local to this Disjunct is also local to the parent, + # and just got "promoted" up there, so to speak. + parent_disjunct_local_vars.update(local_vars) # deactivate disjunct so writers can be happy obj._deactivate_without_fixing_indicator() @@ -627,10 +610,7 @@ def _declare_disaggregated_var_bounds( lb_idx, ub_idx, var_free_indicator, - transBlock=None, ): - # If transBlock is None then this is a disaggregated variable for - # multiple Disjuncts and we will handle the mappings separately. lb = original_var.lb ub = original_var.ub if lb is None or ub is None: @@ -648,61 +628,39 @@ def _declare_disaggregated_var_bounds( if ub: bigmConstraint.add(ub_idx, disaggregatedVar <= ub * var_free_indicator) + original_var_info = original_var.parent_block().private_data() + disaggregated_var_map = original_var_info.disaggregated_var_map + disaggregated_var_info = disaggregatedVar.parent_block().private_data() + # store the mappings from variables to their disaggregated selves on - # the transformation block. - if transBlock is not None: - transBlock._disaggregatedVarMap['disaggregatedVar'][disjunct][ - original_var - ] = disaggregatedVar - transBlock._disaggregatedVarMap['srcVar'][disaggregatedVar] = original_var - transBlock._bigMConstraintMap[disaggregatedVar] = bigmConstraint - - def _get_local_var_set(self, disjunction): - # add Suffix to the relaxation block that disaggregated variables are - # local (in case this is nested in another Disjunct) - local_var_set = None - parent_disjunct = disjunction.parent_block() - while parent_disjunct is not None: - if parent_disjunct.ctype is Disjunct: - break - parent_disjunct = parent_disjunct.parent_block() + # the transformation block + disaggregated_var_map[disjunct][original_var] = disaggregatedVar + disaggregated_var_info.original_var_map[disaggregatedVar] = original_var + + def _get_local_var_list(self, parent_disjunct): + # Add or retrieve Suffix from parent_disjunct so that, if this is + # nested, we can use it to declare that the disaggregated variables are + # local. We return the list so that we can add to it. + local_var_list = None if parent_disjunct is not None: # This limits the cases that a user is allowed to name something # (other than a Suffix) 'LocalVars' on a Disjunct. But I am assuming # that the Suffix has to be somewhere above the disjunct in the # tree, so I can't put it on a Block that I own. And if I'm coopting # something of theirs, it may as well be here. - self._add_local_var_suffix(parent_disjunct) + self._get_local_var_suffix(parent_disjunct) if parent_disjunct.LocalVars.get(parent_disjunct) is None: parent_disjunct.LocalVars[parent_disjunct] = [] - local_var_set = parent_disjunct.LocalVars[parent_disjunct] + local_var_list = parent_disjunct.LocalVars[parent_disjunct] - return local_var_set - - def _warn_for_active_disjunct( - self, innerdisjunct, outerdisjunct, var_substitute_map, zero_substitute_map - ): - # We override the base class method because in hull, it might just be - # that we haven't gotten here yet. - disjuncts = ( - innerdisjunct.values() if innerdisjunct.is_indexed() else (innerdisjunct,) - ) - for disj in disjuncts: - if disj in self._targets_set: - # We're getting to this, have some patience. - continue - else: - # But if it wasn't in the targets after preprocessing, it - # doesn't belong in an active Disjunction that we are - # transforming and we should be confused. - _warn_for_active_disjunct(innerdisjunct, outerdisjunct) + return local_var_list def _transform_constraint( self, obj, disjunct, var_substitute_map, zero_substitute_map ): # we will put a new transformed constraint on the relaxation block. relaxationBlock = disjunct._transformation_block() - constraintMap = relaxationBlock._constraintMap + constraint_map = relaxationBlock.private_data('pyomo.gdp') # We will make indexes from ({obj.local_name} x obj.index_set() x ['lb', # 'ub']), but don't bother construct that set here, as taking Cartesian @@ -784,32 +742,32 @@ def _transform_constraint( # this variable, so I'm going to return # it. Alternatively we could return an empty list, but I # think I like this better. - constraintMap['transformedConstraints'][c] = [v[0]] + constraint_map.transformed_constraints[c].append(v[0]) # Reverse map also (this is strange) - constraintMap['srcConstraints'][v[0]] = c + constraint_map.src_constraint[v[0]] = c continue newConsExpr = expr - (1 - y) * h_0 == c.lower * y if obj.is_indexed(): newConstraint.add((name, i, 'eq'), newConsExpr) - # map the _ConstraintDatas (we mapped the container above) - constraintMap['transformedConstraints'][c] = [ + # map the ConstraintDatas (we mapped the container above) + constraint_map.transformed_constraints[c].append( newConstraint[name, i, 'eq'] - ] - constraintMap['srcConstraints'][newConstraint[name, i, 'eq']] = c + ) + constraint_map.src_constraint[newConstraint[name, i, 'eq']] = c else: newConstraint.add((name, 'eq'), newConsExpr) - # map to the _ConstraintData (And yes, for + # map to the ConstraintData (And yes, for # ScalarConstraints, this is overwriting the map to the # container we made above, and that is what I want to # happen. ScalarConstraints will map to lists. For # IndexedConstraints, we can map the container to the # container, but more importantly, we are mapping the - # _ConstraintDatas to each other above) - constraintMap['transformedConstraints'][c] = [ + # ConstraintDatas to each other above) + constraint_map.transformed_constraints[c].append( newConstraint[name, 'eq'] - ] - constraintMap['srcConstraints'][newConstraint[name, 'eq']] = c + ) + constraint_map.src_constraint[newConstraint[name, 'eq']] = c continue @@ -824,16 +782,16 @@ def _transform_constraint( if obj.is_indexed(): newConstraint.add((name, i, 'lb'), newConsExpr) - constraintMap['transformedConstraints'][c] = [ + constraint_map.transformed_constraints[c].append( newConstraint[name, i, 'lb'] - ] - constraintMap['srcConstraints'][newConstraint[name, i, 'lb']] = c + ) + constraint_map.src_constraint[newConstraint[name, i, 'lb']] = c else: newConstraint.add((name, 'lb'), newConsExpr) - constraintMap['transformedConstraints'][c] = [ + constraint_map.transformed_constraints[c].append( newConstraint[name, 'lb'] - ] - constraintMap['srcConstraints'][newConstraint[name, 'lb']] = c + ) + constraint_map.src_constraint[newConstraint[name, 'lb']] = c if c.upper is not None: if self._generate_debug_messages: @@ -848,29 +806,21 @@ def _transform_constraint( newConstraint.add((name, i, 'ub'), newConsExpr) # map (have to account for fact we might have created list # above - transformed = constraintMap['transformedConstraints'].get(c) - if transformed is not None: - transformed.append(newConstraint[name, i, 'ub']) - else: - constraintMap['transformedConstraints'][c] = [ - newConstraint[name, i, 'ub'] - ] - constraintMap['srcConstraints'][newConstraint[name, i, 'ub']] = c + constraint_map.transformed_constraints[c].append( + newConstraint[name, i, 'ub'] + ) + constraint_map.src_constraint[newConstraint[name, i, 'ub']] = c else: newConstraint.add((name, 'ub'), newConsExpr) - transformed = constraintMap['transformedConstraints'].get(c) - if transformed is not None: - transformed.append(newConstraint[name, 'ub']) - else: - constraintMap['transformedConstraints'][c] = [ - newConstraint[name, 'ub'] - ] - constraintMap['srcConstraints'][newConstraint[name, 'ub']] = c + constraint_map.transformed_constraints[c].append( + newConstraint[name, 'ub'] + ) + constraint_map.src_constraint[newConstraint[name, 'ub']] = c # deactivate now that we have transformed obj.deactivate() - def _add_local_var_suffix(self, disjunct): + def _get_local_var_suffix(self, disjunct): # If the Suffix is there, we will borrow it. If not, we make it. If it's # something else, we complain. localSuffix = disjunct.component("LocalVars") @@ -885,7 +835,7 @@ def _add_local_var_suffix(self, disjunct): % (disjunct.getname(fully_qualified=True), localSuffix.ctype) ) - def get_disaggregated_var(self, v, disjunct): + def get_disaggregated_var(self, v, disjunct, raise_exception=True): """ Returns the disaggregated variable corresponding to the Var v and the Disjunct disjunct. @@ -899,15 +849,16 @@ def get_disaggregated_var(self, v, disjunct): """ if disjunct._transformation_block is None: raise GDP_Error("Disjunct '%s' has not been transformed" % disjunct.name) - transBlock = disjunct._transformation_block().parent_block() - try: - return transBlock._disaggregatedVarMap['disaggregatedVar'][disjunct][v] - except: - logger.error( - "It does not appear '%s' is a " - "variable that appears in disjunct '%s'" % (v.name, disjunct.name) - ) - raise + msg = ( + "It does not appear '%s' is a " + "variable that appears in disjunct '%s'" % (v.name, disjunct.name) + ) + disaggregated_var_map = v.parent_block().private_data().disaggregated_var_map + if v in disaggregated_var_map[disjunct]: + return disaggregated_var_map[disjunct][v] + else: + if raise_exception: + raise GDP_Error(msg) def get_src_var(self, disaggregated_var): """ @@ -916,35 +867,24 @@ def get_src_var(self, disaggregated_var): Parameters ---------- - disaggregated_var: a Var which was created by the hull + disaggregated_var: a Var that was created by the hull transformation as a disaggregated variable (and so appears on a transformation block of some Disjunct) """ - msg = ( + var_map = disaggregated_var.parent_block().private_data() + if disaggregated_var in var_map.original_var_map: + return var_map.original_var_map[disaggregated_var] + raise GDP_Error( "'%s' does not appear to be a " "disaggregated variable" % disaggregated_var.name ) - # There are two possibilities: It is declared on a Disjunct - # transformation Block, or it is declared on the parent of a Disjunct - # transformation block (if it is a single variable for multiple - # Disjuncts the original doesn't appear in) - transBlock = disaggregated_var.parent_block() - if not hasattr(transBlock, '_disaggregatedVarMap'): - try: - transBlock = transBlock.parent_block().parent_block() - except: - logger.error(msg) - raise - try: - return transBlock._disaggregatedVarMap['srcVar'][disaggregated_var] - except: - logger.error(msg) - raise # retrieves the disaggregation constraint for original_var resulting from # transforming disjunction - def get_disaggregation_constraint(self, original_var, disjunction): + def get_disaggregation_constraint( + self, original_var, disjunction, raise_exception=True + ): """ Returns the disaggregation (re-aggregation?) constraint (which links the disaggregated variables to their original) @@ -957,7 +897,7 @@ def get_disaggregation_constraint(self, original_var, disjunction): disjunction: a transformed Disjunction containing original_var """ for disjunct in disjunction.disjuncts: - transBlock = disjunct._transformation_block + transBlock = disjunct.transformation_block if transBlock is not None: break if transBlock is None: @@ -968,20 +908,25 @@ def get_disaggregation_constraint(self, original_var, disjunction): ) try: - return ( - transBlock() - .parent_block() - ._disaggregationConstraintMap[original_var][disjunction] + cons = ( + transBlock.parent_block() + .private_data() + .disaggregation_constraint_map[original_var][disjunction] ) except: - logger.error( - "It doesn't appear that '%s' is a variable that was " - "disaggregated by Disjunction '%s'" - % (original_var.name, disjunction.name) - ) - raise + if raise_exception: + logger.error( + "It doesn't appear that '%s' is a variable that was " + "disaggregated by Disjunction '%s'" + % (original_var.name, disjunction.name) + ) + raise + return None + while not cons.active: + cons = self.get_transformed_constraints(cons)[0] + return cons - def get_var_bounds_constraint(self, v): + def get_var_bounds_constraint(self, v, disjunct=None): """ Returns the IndexedConstraint which sets a disaggregated variable to be within its bounds when its Disjunct is active and to @@ -990,28 +935,43 @@ def get_var_bounds_constraint(self, v): Parameters ---------- - v: a Var which was created by the hull transformation as a + v: a Var that was created by the hull transformation as a disaggregated variable (and so appears on a transformation block of some Disjunct) + disjunct: (For nested Disjunctions) Which Disjunct in the + hierarchy the bounds Constraint should correspond to. + Optional since for non-nested models this can be inferred. """ - msg = ( + info = v.parent_block().private_data() + if v in info.bigm_constraint_map: + if len(info.bigm_constraint_map[v]) == 1: + # Not nested, or it's at the top layer, so we're fine. + return list(info.bigm_constraint_map[v].values())[0] + elif disjunct is not None: + # This is nested, so we need to walk up to find the active ones + return info.bigm_constraint_map[v][disjunct] + else: + raise ValueError( + "It appears that the variable '%s' appears " + "within a nested GDP hierarchy, and no " + "'disjunct' argument was specified. Please " + "specify for which Disjunct the bounds " + "constraint for '%s' should be returned." % (v, v) + ) + raise GDP_Error( "Either '%s' is not a disaggregated variable, or " "the disjunction that disaggregates it has not " "been properly transformed." % v.name ) - # This can only go well if v is a disaggregated var - transBlock = v.parent_block() - if not hasattr(transBlock, '_bigMConstraintMap'): - try: - transBlock = transBlock.parent_block().parent_block() - except: - logger.error(msg) - raise - try: - return transBlock._bigMConstraintMap[v] - except: - logger.error(msg) - raise + + def get_transformed_constraints(self, cons): + cons = super().get_transformed_constraints(cons) + while not cons[0].active: + transformed_cons = [] + for con in cons: + transformed_cons += super().get_transformed_constraints(con) + cons = transformed_cons + return cons @TransformationFactory.register( diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index ca0762987a8..4dffd4e9f9a 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -14,6 +14,7 @@ from pyomo.common.collections import ComponentMap from pyomo.common.config import ConfigDict, ConfigValue +from pyomo.common.gc_manager import PauseGC from pyomo.common.modeling import unique_component_name from pyomo.core import ( @@ -30,7 +31,6 @@ NonNegativeIntegers, Objective, Param, - RangeSet, Set, SetOf, SortComponents, @@ -59,6 +59,18 @@ logger = logging.getLogger('pyomo.gdp.mbigm') +_trusted_solvers = { + 'gurobi', + 'cplex', + 'cbc', + 'glpk', + 'scip', + 'xpress', + 'mosek', + 'baron', + 'highs', +} + @TransformationFactory.register( 'gdp.mbigm', @@ -200,20 +212,26 @@ class MultipleBigMTransformation(GDP_to_MIP_Transformation, _BigM_MixIn): def __init__(self): super().__init__(logger) - self.handlers[Suffix] = self._warn_for_active_suffix self._arg_list = {} + self._set_up_expr_bound_visitor() + self.handlers[Suffix] = self._warn_for_active_suffix def _apply_to(self, instance, **kwds): self.used_args = ComponentMap() - try: - self._apply_to_impl(instance, **kwds) - finally: - self._restore_state() - self.used_args.clear() - self._arg_list.clear() + with PauseGC(): + try: + self._apply_to_impl(instance, **kwds) + finally: + self._restore_state() + self.used_args.clear() + self._arg_list.clear() + self._expr_bound_visitor.leaf_bounds.clear() + self._expr_bound_visitor.use_fixed_var_values_as_bounds = False def _apply_to_impl(self, instance, **kwds): self._process_arguments(instance, **kwds) + if self._config.assume_fixed_vars_permanent: + self._bound_visitor.use_fixed_var_values_as_bounds = True if ( self._config.only_mbigm_bound_constraints @@ -303,10 +321,10 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, root_disjunct) Ms = arg_Ms if not self._config.only_mbigm_bound_constraints: - Ms = ( - transBlock.calculated_missing_m_values - ) = self._calculate_missing_M_values( - active_disjuncts, arg_Ms, transBlock, transformed_constraints + Ms = transBlock.calculated_missing_m_values = ( + self._calculate_missing_M_values( + active_disjuncts, arg_Ms, transBlock, transformed_constraints + ) ) # Now we can deactivate the constraints we deferred, so that we don't @@ -318,8 +336,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, root_disjunct) for disjunct in active_disjuncts: or_expr += disjunct.indicator_var.get_associated_binary() self._transform_disjunct(disjunct, transBlock, active_disjuncts, Ms) - rhs = 1 if parent_disjunct is None else parent_disjunct.binary_indicator_var - algebraic_constraint.add(index, (or_expr, rhs)) + algebraic_constraint.add(index, or_expr == 1) # map the DisjunctionData to its XOR constraint to mark it as # transformed obj._algebraic_constraint = weakref_ref(algebraic_constraint[index]) @@ -339,17 +356,10 @@ def _transform_disjunct(self, obj, transBlock, active_disjuncts, Ms): # deactivate disjunct so writers can be happy obj._deactivate_without_fixing_indicator() - def _warn_for_active_suffix(self, obj, disjunct, active_disjuncts, Ms): - raise GDP_Error( - "Found active Suffix '{0}' on Disjunct '{1}'. " - "The multiple bigM transformation does not currently " - "support Suffixes.".format(obj.name, disjunct.name) - ) - def _transform_constraint(self, obj, disjunct, active_disjuncts, Ms): # we will put a new transformed constraint on the relaxation block. relaxationBlock = disjunct._transformation_block() - constraintMap = relaxationBlock._constraintMap + constraint_map = relaxationBlock.private_data('pyomo.gdp') transBlock = relaxationBlock.parent_block() # Though rare, it is possible to get naming conflicts here @@ -368,7 +378,7 @@ def _transform_constraint(self, obj, disjunct, active_disjuncts, Ms): continue if not self._config.only_mbigm_bound_constraints: - transformed = [] + transformed = constraint_map.transformed_constraints[c] if c.lower is not None: rhs = sum( Ms[c, disj][0] * disj.indicator_var.get_associated_binary() @@ -387,8 +397,7 @@ def _transform_constraint(self, obj, disjunct, active_disjuncts, Ms): newConstraint.add((i, 'ub'), c.body - c.upper <= rhs) transformed.append(newConstraint[i, 'ub']) for c_new in transformed: - constraintMap['srcConstraints'][c_new] = [c] - constraintMap['transformedConstraints'][c] = transformed + constraint_map.src_constraint[c_new] = [c] else: lower = (None, None, None) upper = (None, None, None) @@ -417,11 +426,11 @@ def _transform_constraint(self, obj, disjunct, active_disjuncts, Ms): M, disjunct.indicator_var.get_associated_binary(), newConstraint, - constraintMap, + constraint_map, ) - # deactivate now that we have transformed - c.deactivate() + # deactivate now that we have transformed + c.deactivate() def _transform_bound_constraints(self, active_disjuncts, transBlock, Ms): # first we're just going to find all of them @@ -486,6 +495,7 @@ def _transform_bound_constraints(self, active_disjuncts, transBlock, Ms): relaxationBlock = self._get_disjunct_transformation_block( disj, transBlock ) + constraint_map = relaxationBlock.private_data('pyomo.gdp') if len(lower_dict) > 0: M = lower_dict.get(disj, None) if M is None: @@ -517,39 +527,24 @@ def _transform_bound_constraints(self, active_disjuncts, transBlock, Ms): idx = i + offset if len(lower_dict) > 0: transformed.add((idx, 'lb'), v >= lower_rhs) - relaxationBlock._constraintMap['srcConstraints'][ - transformed[idx, 'lb'] - ] = [] + constraint_map.src_constraint[transformed[idx, 'lb']] = [] for c, disj in lower_bound_constraints_by_var[v]: - relaxationBlock._constraintMap['srcConstraints'][ - transformed[idx, 'lb'] - ].append(c) - disj.transformation_block._constraintMap['transformedConstraints'][ - c - ] = [transformed[idx, 'lb']] + constraint_map.src_constraint[transformed[idx, 'lb']].append(c) + disj.transformation_block.private_data( + 'pyomo.gdp' + ).transformed_constraints[c].append(transformed[idx, 'lb']) if len(upper_dict) > 0: transformed.add((idx, 'ub'), v <= upper_rhs) - relaxationBlock._constraintMap['srcConstraints'][ - transformed[idx, 'ub'] - ] = [] + constraint_map.src_constraint[transformed[idx, 'ub']] = [] for c, disj in upper_bound_constraints_by_var[v]: - relaxationBlock._constraintMap['srcConstraints'][ - transformed[idx, 'ub'] - ].append(c) + constraint_map.src_constraint[transformed[idx, 'ub']].append(c) # might already be here if it had an upper bound - if ( - c - in disj.transformation_block._constraintMap[ - 'transformedConstraints' - ] - ): - disj.transformation_block._constraintMap[ - 'transformedConstraints' - ][c].append(transformed[idx, 'ub']) - else: - disj.transformation_block._constraintMap[ - 'transformedConstraints' - ][c] = [transformed[idx, 'ub']] + disj_constraint_map = disj.transformation_block.private_data( + 'pyomo.gdp' + ) + disj_constraint_map.transformed_constraints[c].append( + transformed[idx, 'ub'] + ) return transformed_constraints @@ -624,40 +619,28 @@ def _calculate_missing_M_values( self.used_args[constraint, other_disjunct] = (lower_M, upper_M) else: (lower_M, upper_M) = (None, None) + unsuccessful_solve_msg = ( + "Unsuccessful solve to calculate M value to " + "relax constraint '%s' on Disjunct '%s' when " + "Disjunct '%s' is selected." + % (constraint.name, disjunct.name, other_disjunct.name) + ) if constraint.lower is not None and lower_M is None: # last resort: calculate if lower_M is None: scratch.obj.expr = constraint.body - constraint.lower scratch.obj.sense = minimize - results = self._config.solver.solve(other_disjunct) - if ( - results.solver.termination_condition - is not TerminationCondition.optimal - ): - raise GDP_Error( - "Unsuccessful solve to calculate M value to " - "relax constraint '%s' on Disjunct '%s' when " - "Disjunct '%s' is selected." - % (constraint.name, disjunct.name, other_disjunct.name) - ) - lower_M = value(scratch.obj.expr) + lower_M = self._solve_disjunct_for_M( + other_disjunct, scratch, unsuccessful_solve_msg + ) if constraint.upper is not None and upper_M is None: # last resort: calculate if upper_M is None: scratch.obj.expr = constraint.body - constraint.upper scratch.obj.sense = maximize - results = self._config.solver.solve(other_disjunct) - if ( - results.solver.termination_condition - is not TerminationCondition.optimal - ): - raise GDP_Error( - "Unsuccessful solve to calculate M value to " - "relax constraint '%s' on Disjunct '%s' when " - "Disjunct '%s' is selected." - % (constraint.name, disjunct.name, other_disjunct.name) - ) - upper_M = value(scratch.obj.expr) + upper_M = self._solve_disjunct_for_M( + other_disjunct, scratch, unsuccessful_solve_msg + ) arg_Ms[constraint, other_disjunct] = (lower_M, upper_M) transBlock._mbm_values[constraint, other_disjunct] = (lower_M, upper_M) @@ -667,6 +650,60 @@ def _calculate_missing_M_values( return arg_Ms + def _solve_disjunct_for_M( + self, other_disjunct, scratch_block, unsuccessful_solve_msg + ): + solver = self._config.solver + results = solver.solve(other_disjunct, load_solutions=False) + if results.solver.termination_condition is TerminationCondition.infeasible: + # [2/18/24]: TODO: After the solver rewrite is complete, we will not + # need this check since we can actually determine from the + # termination condition whether or not the solver proved + # infeasibility or just terminated at local infeasiblity. For now, + # while this is not complete, it catches most of the solvers we + # trust, and, unless someone is so pathological as to *rename* an + # untrusted solver using a trusted solver name, it will never do the + # *wrong* thing. + if any(s in solver.name for s in _trusted_solvers): + logger.debug( + "Disjunct '%s' is infeasible, deactivating." % other_disjunct.name + ) + other_disjunct.deactivate() + M = 0 + else: + # This is a solver that might report + # 'infeasible' for local infeasibility, so we + # can't deactivate with confidence. To be + # conservative, we'll just complain about + # it. Post-solver-rewrite we will want to change + # this so that we check for 'proven_infeasible' + # and then we can abandon this hack + raise GDP_Error(unsuccessful_solve_msg) + elif results.solver.termination_condition is not TerminationCondition.optimal: + raise GDP_Error(unsuccessful_solve_msg) + else: + other_disjunct.solutions.load_from(results) + M = value(scratch_block.obj.expr) + return M + + def _warn_for_active_suffix(self, suffix, disjunct, active_disjuncts, Ms): + if suffix.local_name == 'BigM': + logger.debug( + "Found active 'BigM' Suffix on '{0}'. " + "The multiple bigM transformation does not currently " + "support specifying M's with Suffixes and is ignoring " + "this Suffix.".format(disjunct.name) + ) + elif suffix.local_name == 'LocalVars': + # This is fine, but this transformation doesn't need anything from it + pass + else: + raise GDP_Error( + "Found active Suffix '{0}' on Disjunct '{1}'. " + "The multiple bigM transformation does not " + "support this Suffix.".format(suffix.name, disjunct.name) + ) + # These are all functions to retrieve transformed components from # original ones and vice versa. diff --git a/pyomo/gdp/plugins/partition_disjuncts.py b/pyomo/gdp/plugins/partition_disjuncts.py index 57cfe1852c3..1a76900047c 100644 --- a/pyomo/gdp/plugins/partition_disjuncts.py +++ b/pyomo/gdp/plugins/partition_disjuncts.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -15,7 +15,7 @@ J. Kronqvist, R. Misener, and C. Tsay, "Between Steps: Intermediate Relaxations between big-M and Convex Hull Reformulations," 2021. """ -from __future__ import division + from pyomo.common.config import ( ConfigBlock, diff --git a/pyomo/gdp/plugins/transform_current_disjunctive_state.py b/pyomo/gdp/plugins/transform_current_disjunctive_state.py index 338f42c68da..3e20224ec3d 100644 --- a/pyomo/gdp/plugins/transform_current_disjunctive_state.py +++ b/pyomo/gdp/plugins/transform_current_disjunctive_state.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/tests/__init__.py b/pyomo/gdp/tests/__init__.py index c5e495e5aa3..a2a2c61779a 100644 --- a/pyomo/gdp/tests/__init__.py +++ b/pyomo/gdp/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/tests/common_tests.py b/pyomo/gdp/tests/common_tests.py index b475334981b..50bc8b05f86 100644 --- a/pyomo/gdp/tests/common_tests.py +++ b/pyomo/gdp/tests/common_tests.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -30,7 +30,7 @@ from pyomo.gdp import Disjunct, Disjunction, GDP_Error from pyomo.core.expr.compare import assertExpressionsEqual from pyomo.core.base import constraint, ComponentUID -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.repn import generate_standard_repn import pyomo.core.expr as EXPR import pyomo.gdp.tests.models as models @@ -58,6 +58,24 @@ def check_linear_coef(self, repn, var, coef): self.assertAlmostEqual(repn.linear_coefs[var_id], coef) +def check_quadratic_coef(self, repn, v1, v2, coef): + if isinstance(v1, BooleanVar): + v1 = v1.get_associated_binary() + if isinstance(v2, BooleanVar): + v2 = v2.get_associated_binary() + + v1id = id(v1) + v2id = id(v2) + + qcoef_map = dict() + for (_v1, _v2), _coef in zip(repn.quadratic_vars, repn.quadratic_coefs): + qcoef_map[id(_v1), id(_v2)] = _coef + qcoef_map[id(_v2), id(_v1)] = _coef + + self.assertIn((v1id, v2id), qcoef_map) + self.assertAlmostEqual(qcoef_map[v1id, v2id], coef) + + def check_squared_term_coef(self, repn, var, coef): var_id = None for i, (v1, v2) in enumerate(repn.quadratic_vars): @@ -407,12 +425,7 @@ def check_two_term_disjunction_xor(self, xor, disj1, disj2): assertExpressionsEqual( self, xor.body, - EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, disj1.binary_indicator_var)), - EXPR.MonomialTermExpression((1, disj2.binary_indicator_var)), - ] - ), + EXPR.LinearExpression([disj1.binary_indicator_var, disj2.binary_indicator_var]), ) self.assertEqual(xor.lower, 1) self.assertEqual(xor.upper, 1) @@ -679,32 +692,29 @@ def check_indexedDisj_only_targets_transformed(self, transformation): trans.get_transformed_constraints(m.disjunct1[1, 0].c)[0] .parent_block() .parent_block(), - disjBlock[2], + disjBlock[0], ) self.assertIs( trans.get_transformed_constraints(m.disjunct1[1, 1].c)[0].parent_block(), - disjBlock[3], + disjBlock[1], ) # In the disaggregated var bounds self.assertIs( trans.get_transformed_constraints(m.disjunct1[2, 0].c)[0] .parent_block() .parent_block(), - disjBlock[0], + disjBlock[2], ) self.assertIs( trans.get_transformed_constraints(m.disjunct1[2, 1].c)[0].parent_block(), - disjBlock[1], + disjBlock[3], ) # This relies on the disjunctions being transformed in the same order # every time. These are the mappings between the indices of the original # disjuncts and the indices on the indexed block on the transformation # block. - if transformation == 'bigm': - pairs = [((1, 0), 0), ((1, 1), 1), ((2, 0), 2), ((2, 1), 3)] - elif transformation == 'hull': - pairs = [((2, 0), 0), ((2, 1), 1), ((1, 0), 2), ((1, 1), 3)] + pairs = [((1, 0), 0), ((1, 1), 1), ((2, 0), 2), ((2, 1), 3)] for i, j in pairs: self.assertIs(trans.get_src_disjunct(disjBlock[j]), m.disjunct1[i]) @@ -942,9 +952,7 @@ def check_disjunction_data_target(self, transformation): transBlock = m.component("_pyomo_gdp_%s_reformulation" % transformation) self.assertIsInstance(transBlock, Block) self.assertIsInstance(transBlock.component("disjunction_xor"), Constraint) - self.assertIsInstance( - transBlock.disjunction_xor[2], constraint._GeneralConstraintData - ) + self.assertIsInstance(transBlock.disjunction_xor[2], constraint.ConstraintData) self.assertIsInstance(transBlock.component("relaxedDisjuncts"), Block) self.assertEqual(len(transBlock.relaxedDisjuncts), 3) @@ -953,7 +961,7 @@ def check_disjunction_data_target(self, transformation): m, targets=[m.disjunction[1]] ) self.assertIsInstance( - m.disjunction[1].algebraic_constraint, constraint._GeneralConstraintData + m.disjunction[1].algebraic_constraint, constraint.ConstraintData ) transBlock = m.component("_pyomo_gdp_%s_reformulation_4" % transformation) self.assertIsInstance(transBlock, Block) @@ -1694,26 +1702,78 @@ def check_all_components_transformed(self, m): # makeNestedDisjunctions_NestedDisjuncts model. self.assertIsInstance(m.disj.algebraic_constraint, Constraint) self.assertIsInstance(m.d1.disj2.algebraic_constraint, Constraint) - self.assertIsInstance(m.d1.transformation_block, _BlockData) - self.assertIsInstance(m.d2.transformation_block, _BlockData) - self.assertIsInstance(m.d1.d3.transformation_block, _BlockData) - self.assertIsInstance(m.d1.d4.transformation_block, _BlockData) + self.assertIsInstance(m.d1.transformation_block, BlockData) + self.assertIsInstance(m.d2.transformation_block, BlockData) + self.assertIsInstance(m.d1.d3.transformation_block, BlockData) + self.assertIsInstance(m.d1.d4.transformation_block, BlockData) def check_transformation_blocks_nestedDisjunctions(self, m, transformation): disjunctionTransBlock = m.disj.algebraic_constraint.parent_block() transBlocks = disjunctionTransBlock.relaxedDisjuncts - self.assertEqual(len(transBlocks), 4) if transformation == 'bigm': + self.assertEqual(len(transBlocks), 4) self.assertIs(transBlocks[0], m.d1.d3.transformation_block) self.assertIs(transBlocks[1], m.d1.d4.transformation_block) self.assertIs(transBlocks[2], m.d1.transformation_block) self.assertIs(transBlocks[3], m.d2.transformation_block) if transformation == 'hull': - self.assertIs(transBlocks[2], m.d1.d3.transformation_block) - self.assertIs(transBlocks[3], m.d1.d4.transformation_block) - self.assertIs(transBlocks[0], m.d1.transformation_block) - self.assertIs(transBlocks[1], m.d2.transformation_block) + # This is a much more comprehensive test that doesn't depend on + # transformation Block structure, so just reuse it: + hull = TransformationFactory('gdp.hull') + d3 = hull.get_disaggregated_var(m.d1.d3.binary_indicator_var, m.d1) + d4 = hull.get_disaggregated_var(m.d1.d4.binary_indicator_var, m.d1) + self.check_transformed_model_nestedDisjuncts(m, d3, d4) + + # Check the 4 constraints that are unique to the case where we didn't + # declare d1.d3 and d1.d4 as local + d32 = hull.get_disaggregated_var(m.d1.d3.binary_indicator_var, m.d2) + d42 = hull.get_disaggregated_var(m.d1.d4.binary_indicator_var, m.d2) + # check the additional disaggregated indicator var bound constraints + cons = hull.get_var_bounds_constraint(d32) + self.assertEqual(len(cons), 1) + check_obj_in_active_tree(self, cons['ub']) + cons_expr = self.simplify_leq_cons(cons['ub']) + # Note that this comes out as d32 <= 1 - d1.ind_var because it's the + # "extra" disaggregated var that gets created when it need to be + # disaggregated for d1, but it's not used in d2 + assertExpressionsEqual( + self, cons_expr, d32 + m.d1.binary_indicator_var - 1 <= 0.0 + ) + + cons = hull.get_var_bounds_constraint(d42) + self.assertEqual(len(cons), 1) + check_obj_in_active_tree(self, cons['ub']) + cons_expr = self.simplify_leq_cons(cons['ub']) + # Note that this comes out as d42 <= 1 - d1.ind_var because it's the + # "extra" disaggregated var that gets created when it need to be + # disaggregated for d1, but it's not used in d2 + assertExpressionsEqual( + self, cons_expr, d42 + m.d1.binary_indicator_var - 1 <= 0.0 + ) + # check the aggregation constraints for the disaggregated indicator vars + cons = hull.get_disaggregation_constraint(m.d1.d3.binary_indicator_var, m.disj) + check_obj_in_active_tree(self, cons) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual( + self, cons_expr, m.d1.d3.binary_indicator_var - d32 - d3 == 0.0 + ) + cons = hull.get_disaggregation_constraint(m.d1.d4.binary_indicator_var, m.disj) + check_obj_in_active_tree(self, cons) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual( + self, cons_expr, m.d1.d4.binary_indicator_var - d42 - d4 == 0.0 + ) + + num_cons = len( + list(m.component_data_objects(Constraint, active=True, descend_into=Block)) + ) + # 30 total constraints in transformed model minus 10 trivial bounds + # (lower bounds of 0) gives us 20 constraints total: + self.assertEqual(num_cons, 20) + # (And this is 4 more than we test in + # self.check_transformed_model_nestedDisjuncts, so that's comforting + # too.) def check_nested_disjunction_target(self, transformation): @@ -1877,3 +1937,17 @@ def check_nested_disjuncts_in_flat_gdp(self, transformation): for t in m.T: self.assertTrue(value(m.disj1[t].indicator_var)) self.assertTrue(value(m.disj1[t].sub1.indicator_var)) + + +def check_do_not_assume_nested_indicators_local(self, transformation): + m = models.why_indicator_vars_are_not_always_local() + TransformationFactory(transformation).apply_to(m) + + results = SolverFactory('gurobi').solve(m) + self.assertEqual(results.solver.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(value(m.obj), 9) + self.assertAlmostEqual(value(m.x), 9) + self.assertTrue(value(m.Y2.indicator_var)) + self.assertFalse(value(m.Y1.indicator_var)) + self.assertTrue(value(m.Z1.indicator_var)) + self.assertTrue(value(m.Z1.indicator_var)) diff --git a/pyomo/gdp/tests/jobshop_large_hull.lp b/pyomo/gdp/tests/jobshop_large_hull.lp index 983770880b7..f0a9d3ccbf0 100644 --- a/pyomo/gdp/tests/jobshop_large_hull.lp +++ b/pyomo/gdp/tests/jobshop_large_hull.lp @@ -42,75 +42,75 @@ c_u_Feas(G)_: <= -17 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(0)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(G)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(1)_: -+1 t(F) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(F)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(2)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(G)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(3)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(E)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(4)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(G)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(5)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(E)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(6)_: -+1 t(F) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(F)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(7)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(E)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(8)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(G)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(9)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(D)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(10)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(G)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(11)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(D)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(12)_: @@ -120,9 +120,9 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(12)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(13)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(D)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(14)_: @@ -132,81 +132,81 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(14)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(15)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(D)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(16)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(E)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(17)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(D)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(18)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(E)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(19)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(D)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(20)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(G)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(21)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(C)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(22)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(G)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(23)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(C)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(24)_: -+1 t(F) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(F)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(25)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(C)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(26)_: -+1 t(F) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(F)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(27)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(C)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(28)_: @@ -216,33 +216,33 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(28)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(29)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(C)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(30)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(D)_ ++1 t(F) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(F)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(31)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(C)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(32)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(D)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(33)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(C)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(34)_: @@ -258,27 +258,27 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(35)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(36)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(G)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(37)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(B)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(38)_: -+1 t(F) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(F)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(39)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(B)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(40)_: @@ -288,81 +288,81 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(40)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(41)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(B)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(42)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(E)_ ++1 t(F) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(F)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(43)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(B)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(44)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(E)_ ++1 t(F) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(F)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(45)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(B)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(46)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(D)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(47)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(B)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(48)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(D)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(49)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(B)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(50)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(C)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(51)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(B)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(52)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(G)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(53)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(A)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(54)_: @@ -372,9 +372,9 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(54)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(55)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(A)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(56)_: @@ -384,166 +384,166 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(56)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(57)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(A)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(58)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(E)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(59)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(A)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(60)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(E)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(61)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(A)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(62)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(D)_ ++1 t(F) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(F)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(63)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(A)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(64)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(C)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(65)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(A)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(66)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(B)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(67)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(A)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(68)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(B)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(69)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(A)_ ++1 t(F) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(F)_ = 0 -c_e__pyomo_gdp_hull_reformulation_disj_xor(F_G_4)_: -+1 NoClash(F_G_4_0)_binary_indicator_var -+1 NoClash(F_G_4_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(A_B_3)_: ++1 NoClash(A_B_3_0)_binary_indicator_var ++1 NoClash(A_B_3_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(E_G_5)_: -+1 NoClash(E_G_5_0)_binary_indicator_var -+1 NoClash(E_G_5_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(A_B_5)_: ++1 NoClash(A_B_5_0)_binary_indicator_var ++1 NoClash(A_B_5_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(E_G_2)_: -+1 NoClash(E_G_2_0)_binary_indicator_var -+1 NoClash(E_G_2_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(A_C_1)_: ++1 NoClash(A_C_1_0)_binary_indicator_var ++1 NoClash(A_C_1_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(E_F_3)_: -+1 NoClash(E_F_3_0)_binary_indicator_var -+1 NoClash(E_F_3_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(A_D_3)_: ++1 NoClash(A_D_3_0)_binary_indicator_var ++1 NoClash(A_D_3_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(D_G_4)_: -+1 NoClash(D_G_4_0)_binary_indicator_var -+1 NoClash(D_G_4_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(A_E_3)_: ++1 NoClash(A_E_3_0)_binary_indicator_var ++1 NoClash(A_E_3_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(D_G_2)_: -+1 NoClash(D_G_2_0)_binary_indicator_var -+1 NoClash(D_G_2_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(A_E_5)_: ++1 NoClash(A_E_5_0)_binary_indicator_var ++1 NoClash(A_E_5_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(D_F_4)_: -+1 NoClash(D_F_4_0)_binary_indicator_var -+1 NoClash(D_F_4_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(A_F_1)_: ++1 NoClash(A_F_1_0)_binary_indicator_var ++1 NoClash(A_F_1_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(D_F_3)_: -+1 NoClash(D_F_3_0)_binary_indicator_var -+1 NoClash(D_F_3_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(A_F_3)_: ++1 NoClash(A_F_3_0)_binary_indicator_var ++1 NoClash(A_F_3_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(D_E_3)_: -+1 NoClash(D_E_3_0)_binary_indicator_var -+1 NoClash(D_E_3_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(A_G_5)_: ++1 NoClash(A_G_5_0)_binary_indicator_var ++1 NoClash(A_G_5_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(D_E_2)_: -+1 NoClash(D_E_2_0)_binary_indicator_var -+1 NoClash(D_E_2_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(B_C_2)_: ++1 NoClash(B_C_2_0)_binary_indicator_var ++1 NoClash(B_C_2_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(C_G_4)_: -+1 NoClash(C_G_4_0)_binary_indicator_var -+1 NoClash(C_G_4_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(B_D_2)_: ++1 NoClash(B_D_2_0)_binary_indicator_var ++1 NoClash(B_D_2_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(C_G_2)_: -+1 NoClash(C_G_2_0)_binary_indicator_var -+1 NoClash(C_G_2_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(B_D_3)_: ++1 NoClash(B_D_3_0)_binary_indicator_var ++1 NoClash(B_D_3_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(C_F_4)_: -+1 NoClash(C_F_4_0)_binary_indicator_var -+1 NoClash(C_F_4_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(B_E_2)_: ++1 NoClash(B_E_2_0)_binary_indicator_var ++1 NoClash(B_E_2_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(C_F_1)_: -+1 NoClash(C_F_1_0)_binary_indicator_var -+1 NoClash(C_F_1_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(B_E_3)_: ++1 NoClash(B_E_3_0)_binary_indicator_var ++1 NoClash(B_E_3_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(C_E_2)_: -+1 NoClash(C_E_2_0)_binary_indicator_var -+1 NoClash(C_E_2_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(B_E_5)_: ++1 NoClash(B_E_5_0)_binary_indicator_var ++1 NoClash(B_E_5_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(C_D_4)_: -+1 NoClash(C_D_4_0)_binary_indicator_var -+1 NoClash(C_D_4_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(B_F_3)_: ++1 NoClash(B_F_3_0)_binary_indicator_var ++1 NoClash(B_F_3_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(C_D_2)_: -+1 NoClash(C_D_2_0)_binary_indicator_var -+1 NoClash(C_D_2_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(B_G_2)_: ++1 NoClash(B_G_2_0)_binary_indicator_var ++1 NoClash(B_G_2_1)_binary_indicator_var = 1 c_e__pyomo_gdp_hull_reformulation_disj_xor(B_G_5)_: @@ -551,632 +551,630 @@ c_e__pyomo_gdp_hull_reformulation_disj_xor(B_G_5)_: +1 NoClash(B_G_5_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(B_G_2)_: -+1 NoClash(B_G_2_0)_binary_indicator_var -+1 NoClash(B_G_2_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(C_D_2)_: ++1 NoClash(C_D_2_0)_binary_indicator_var ++1 NoClash(C_D_2_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(B_F_3)_: -+1 NoClash(B_F_3_0)_binary_indicator_var -+1 NoClash(B_F_3_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(C_D_4)_: ++1 NoClash(C_D_4_0)_binary_indicator_var ++1 NoClash(C_D_4_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(B_E_5)_: -+1 NoClash(B_E_5_0)_binary_indicator_var -+1 NoClash(B_E_5_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(C_E_2)_: ++1 NoClash(C_E_2_0)_binary_indicator_var ++1 NoClash(C_E_2_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(B_E_3)_: -+1 NoClash(B_E_3_0)_binary_indicator_var -+1 NoClash(B_E_3_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(C_F_1)_: ++1 NoClash(C_F_1_0)_binary_indicator_var ++1 NoClash(C_F_1_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(B_E_2)_: -+1 NoClash(B_E_2_0)_binary_indicator_var -+1 NoClash(B_E_2_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(C_F_4)_: ++1 NoClash(C_F_4_0)_binary_indicator_var ++1 NoClash(C_F_4_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(B_D_3)_: -+1 NoClash(B_D_3_0)_binary_indicator_var -+1 NoClash(B_D_3_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(C_G_2)_: ++1 NoClash(C_G_2_0)_binary_indicator_var ++1 NoClash(C_G_2_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(B_D_2)_: -+1 NoClash(B_D_2_0)_binary_indicator_var -+1 NoClash(B_D_2_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(C_G_4)_: ++1 NoClash(C_G_4_0)_binary_indicator_var ++1 NoClash(C_G_4_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(B_C_2)_: -+1 NoClash(B_C_2_0)_binary_indicator_var -+1 NoClash(B_C_2_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(D_E_2)_: ++1 NoClash(D_E_2_0)_binary_indicator_var ++1 NoClash(D_E_2_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(A_G_5)_: -+1 NoClash(A_G_5_0)_binary_indicator_var -+1 NoClash(A_G_5_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(D_E_3)_: ++1 NoClash(D_E_3_0)_binary_indicator_var ++1 NoClash(D_E_3_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(A_F_3)_: -+1 NoClash(A_F_3_0)_binary_indicator_var -+1 NoClash(A_F_3_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(D_F_3)_: ++1 NoClash(D_F_3_0)_binary_indicator_var ++1 NoClash(D_F_3_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(A_F_1)_: -+1 NoClash(A_F_1_0)_binary_indicator_var -+1 NoClash(A_F_1_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(D_F_4)_: ++1 NoClash(D_F_4_0)_binary_indicator_var ++1 NoClash(D_F_4_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(A_E_5)_: -+1 NoClash(A_E_5_0)_binary_indicator_var -+1 NoClash(A_E_5_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(D_G_2)_: ++1 NoClash(D_G_2_0)_binary_indicator_var ++1 NoClash(D_G_2_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(A_E_3)_: -+1 NoClash(A_E_3_0)_binary_indicator_var -+1 NoClash(A_E_3_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(D_G_4)_: ++1 NoClash(D_G_4_0)_binary_indicator_var ++1 NoClash(D_G_4_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(A_D_3)_: -+1 NoClash(A_D_3_0)_binary_indicator_var -+1 NoClash(A_D_3_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(E_F_3)_: ++1 NoClash(E_F_3_0)_binary_indicator_var ++1 NoClash(E_F_3_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(A_C_1)_: -+1 NoClash(A_C_1_0)_binary_indicator_var -+1 NoClash(A_C_1_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(E_G_2)_: ++1 NoClash(E_G_2_0)_binary_indicator_var ++1 NoClash(E_G_2_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(A_B_5)_: -+1 NoClash(A_B_5_0)_binary_indicator_var -+1 NoClash(A_B_5_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(E_G_5)_: ++1 NoClash(E_G_5_0)_binary_indicator_var ++1 NoClash(E_G_5_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(A_B_3)_: -+1 NoClash(A_B_3_0)_binary_indicator_var -+1 NoClash(A_B_3_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(F_G_4)_: ++1 NoClash(F_G_4_0)_binary_indicator_var ++1 NoClash(F_G_4_1)_binary_indicator_var = 1 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(F)_ -+6.0 NoClash(F_G_4_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ ++4.0 NoClash(A_B_3_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(G)_ --92 NoClash(F_G_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ +-92 NoClash(A_B_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(F)_ --92 NoClash(F_G_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ +-92 NoClash(A_B_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(F)_ -+6.0 NoClash(F_G_4_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ ++5.0 NoClash(A_B_3_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(G)_ --92 NoClash(F_G_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ +-92 NoClash(A_B_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(F)_ --92 NoClash(F_G_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ +-92 NoClash(A_B_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(E)_ -+7.0 NoClash(E_G_5_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ ++2.0 NoClash(A_B_5_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(G)_ --92 NoClash(E_G_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(B)_ +-92 NoClash(A_B_5_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(E)_ --92 NoClash(E_G_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ +-92 NoClash(A_B_5_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(E)_ --1 NoClash(E_G_5_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(B)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ ++3.0 NoClash(A_B_5_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(G)_ --92 NoClash(E_G_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(B)_ +-92 NoClash(A_B_5_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(E)_ --92 NoClash(E_G_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ +-92 NoClash(A_B_5_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(E)_ -+8.0 NoClash(E_G_2_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ ++6.0 NoClash(A_C_1_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(G)_ --92 NoClash(E_G_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ +-92 NoClash(A_C_1_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(E)_ --92 NoClash(E_G_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ +-92 NoClash(A_C_1_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(E)_ -+4.0 NoClash(E_G_2_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ ++3.0 NoClash(A_C_1_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(G)_ --92 NoClash(E_G_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ +-92 NoClash(A_C_1_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(E)_ --92 NoClash(E_G_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ +-92 NoClash(A_C_1_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(E)_ -+3.0 NoClash(E_F_3_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(A)_ ++10.0 NoClash(A_D_3_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(F)_ --92 NoClash(E_F_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(D)_ +-92 NoClash(A_D_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(E)_ --92 NoClash(E_F_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(A)_ +-92 NoClash(A_D_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(F)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(E)_ -+8.0 NoClash(E_F_3_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(D)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(A)_ <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(F)_ --92 NoClash(E_F_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(D)_ +-92 NoClash(A_D_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(E)_ --92 NoClash(E_F_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(A)_ +-92 NoClash(A_D_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(D)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(A)_ ++7.0 NoClash(A_E_3_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(G)_ --92 NoClash(D_G_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(E)_ +-92 NoClash(A_E_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(D)_ --92 NoClash(D_G_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(A)_ +-92 NoClash(A_E_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(D)_ -+6.0 NoClash(D_G_4_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(E)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(A)_ ++4.0 NoClash(A_E_3_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(G)_ --92 NoClash(D_G_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(E)_ +-92 NoClash(A_E_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(D)_ --92 NoClash(D_G_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(A)_ +-92 NoClash(A_E_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(D)_ -+8.0 NoClash(D_G_2_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(A)_ ++4.0 NoClash(A_E_5_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(G)_ --92 NoClash(D_G_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(E)_ +-92 NoClash(A_E_5_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(D)_ --92 NoClash(D_G_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(A)_ +-92 NoClash(A_E_5_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(D)_ -+8.0 NoClash(D_G_2_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(E)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(A)_ <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(G)_ --92 NoClash(D_G_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(E)_ +-92 NoClash(A_E_5_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(D)_ --92 NoClash(D_G_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(A)_ +-92 NoClash(A_E_5_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_transformedConstraints(c_0_ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(D)_ -+1 NoClash(D_F_4_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(A)_ ++2.0 NoClash(A_F_1_0)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)__t(F)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(F)_ --92 NoClash(D_F_4_0)_binary_indicator_var +-92 NoClash(A_F_1_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(D)_ --92 NoClash(D_F_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(A)_ +-92 NoClash(A_F_1_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_transformedConstraints(c_0_ub)_: -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(F)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(D)_ -+7.0 NoClash(D_F_4_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(A)_ ++3.0 NoClash(A_F_1_1)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)__t(F)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(F)_ --92 NoClash(D_F_4_1)_binary_indicator_var +-92 NoClash(A_F_1_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(D)_ --92 NoClash(D_F_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(A)_ +-92 NoClash(A_F_1_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_transformedConstraints(c_0_ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(D)_ --1 NoClash(D_F_3_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(A)_ ++4.0 NoClash(A_F_3_0)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)__t(F)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(F)_ --92 NoClash(D_F_3_0)_binary_indicator_var +-92 NoClash(A_F_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(D)_ --92 NoClash(D_F_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(A)_ +-92 NoClash(A_F_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_transformedConstraints(c_0_ub)_: -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(F)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(D)_ -+11.0 NoClash(D_F_3_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(A)_ ++6.0 NoClash(A_F_3_1)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)__t(F)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(F)_ --92 NoClash(D_F_3_1)_binary_indicator_var +-92 NoClash(A_F_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(D)_ --92 NoClash(D_F_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(A)_ +-92 NoClash(A_F_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(D)_ -+2.0 NoClash(D_E_3_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(A)_ ++9.0 NoClash(A_G_5_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(E)_ --92 NoClash(D_E_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(G)_ +-92 NoClash(A_G_5_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(D)_ --92 NoClash(D_E_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(A)_ +-92 NoClash(A_G_5_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(E)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(D)_ -+9.0 NoClash(D_E_3_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(G)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(A)_ +-3.0 NoClash(A_G_5_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(E)_ --92 NoClash(D_E_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(G)_ +-92 NoClash(A_G_5_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(D)_ --92 NoClash(D_E_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(A)_ +-92 NoClash(A_G_5_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(D)_ -+4.0 NoClash(D_E_2_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(B)_ ++9.0 NoClash(B_C_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(E)_ --92 NoClash(D_E_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(C)_ +-92 NoClash(B_C_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(D)_ --92 NoClash(D_E_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(B)_ +-92 NoClash(B_C_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(E)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(D)_ -+8.0 NoClash(D_E_2_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(C)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(B)_ +-3.0 NoClash(B_C_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(E)_ --92 NoClash(D_E_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(C)_ +-92 NoClash(B_C_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(D)_ --92 NoClash(D_E_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(B)_ +-92 NoClash(B_C_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(C)_ -+4.0 NoClash(C_G_4_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(B)_ ++8.0 NoClash(B_D_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(G)_ --92 NoClash(C_G_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(D)_ +-92 NoClash(B_D_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(C)_ --92 NoClash(C_G_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(B)_ +-92 NoClash(B_D_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(C)_ -+7.0 NoClash(C_G_4_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(D)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(B)_ ++3.0 NoClash(B_D_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(G)_ --92 NoClash(C_G_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(D)_ +-92 NoClash(B_D_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(C)_ --92 NoClash(C_G_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(B)_ +-92 NoClash(B_D_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(C)_ -+2.0 NoClash(C_G_2_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(B)_ ++10.0 NoClash(B_D_3_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(G)_ --92 NoClash(C_G_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(D)_ +-92 NoClash(B_D_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(C)_ --92 NoClash(C_G_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(B)_ +-92 NoClash(B_D_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(C)_ -+9.0 NoClash(C_G_2_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(D)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(B)_ +-1 NoClash(B_D_3_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(G)_ --92 NoClash(C_G_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(D)_ +-92 NoClash(B_D_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(C)_ --92 NoClash(C_G_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(B)_ +-92 NoClash(B_D_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(C)_ -+5.0 NoClash(C_F_4_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(B)_ ++4.0 NoClash(B_E_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(F)_ --92 NoClash(C_F_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(E)_ +-92 NoClash(B_E_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(C)_ --92 NoClash(C_F_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(B)_ +-92 NoClash(B_E_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(F)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(C)_ -+8.0 NoClash(C_F_4_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(E)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(B)_ ++3.0 NoClash(B_E_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(F)_ --92 NoClash(C_F_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(E)_ +-92 NoClash(B_E_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(C)_ --92 NoClash(C_F_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(B)_ +-92 NoClash(B_E_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(C)_ -+2.0 NoClash(C_F_1_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(B)_ ++7.0 NoClash(B_E_3_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(F)_ --92 NoClash(C_F_1_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(E)_ +-92 NoClash(B_E_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(C)_ --92 NoClash(C_F_1_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(B)_ +-92 NoClash(B_E_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(F)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(C)_ -+6.0 NoClash(C_F_1_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(E)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(B)_ ++3.0 NoClash(B_E_3_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(F)_ --92 NoClash(C_F_1_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(E)_ +-92 NoClash(B_E_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(C)_ --92 NoClash(C_F_1_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(B)_ +-92 NoClash(B_E_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_transformedConstraints(c_0_ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(C)_ --2.0 NoClash(C_E_2_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(B)_ ++5.0 NoClash(B_E_5_0)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)__t(E)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(E)_ --92 NoClash(C_E_2_0)_binary_indicator_var +-92 NoClash(B_E_5_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(C)_ --92 NoClash(C_E_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(B)_ +-92 NoClash(B_E_5_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_transformedConstraints(c_0_ub)_: -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(E)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(C)_ -+9.0 NoClash(C_E_2_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(B)_ <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)__t(E)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(E)_ --92 NoClash(C_E_2_1)_binary_indicator_var +-92 NoClash(B_E_5_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(C)_ --92 NoClash(C_E_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(B)_ +-92 NoClash(B_E_5_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(C)_ -+5.0 NoClash(C_D_4_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(B)_ ++4.0 NoClash(B_F_3_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(D)_ --92 NoClash(C_D_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(F)_ +-92 NoClash(B_F_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(C)_ --92 NoClash(C_D_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(B)_ +-92 NoClash(B_F_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(D)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(C)_ -+2.0 NoClash(C_D_4_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(F)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(B)_ ++5.0 NoClash(B_F_3_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(D)_ --92 NoClash(C_D_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(F)_ +-92 NoClash(B_F_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(C)_ --92 NoClash(C_D_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(B)_ +-92 NoClash(B_F_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(C)_ -+2.0 NoClash(C_D_2_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(B)_ ++8.0 NoClash(B_G_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(D)_ --92 NoClash(C_D_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(G)_ +-92 NoClash(B_G_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(C)_ --92 NoClash(C_D_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(B)_ +-92 NoClash(B_G_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(D)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(C)_ -+9.0 NoClash(C_D_2_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(G)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(B)_ ++3.0 NoClash(B_G_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(D)_ --92 NoClash(C_D_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(G)_ +-92 NoClash(B_G_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(C)_ --92 NoClash(C_D_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(B)_ +-92 NoClash(B_G_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(34)_transformedConstraints(c_0_ub)_: @@ -1212,544 +1210,546 @@ c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(35)__t(B)_bounds_(ub)_: <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(B)_ -+8.0 NoClash(B_G_2_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(C)_ ++2.0 NoClash(C_D_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(G)_ --92 NoClash(B_G_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(D)_ +-92 NoClash(C_D_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(B)_ --92 NoClash(B_G_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(C)_ +-92 NoClash(C_D_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(B)_ -+3.0 NoClash(B_G_2_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(D)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(C)_ ++9.0 NoClash(C_D_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(G)_ --92 NoClash(B_G_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(D)_ +-92 NoClash(C_D_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(B)_ --92 NoClash(B_G_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(C)_ +-92 NoClash(C_D_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(B)_ -+4.0 NoClash(B_F_3_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(C)_ ++5.0 NoClash(C_D_4_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(F)_ --92 NoClash(B_F_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(D)_ +-92 NoClash(C_D_4_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(B)_ --92 NoClash(B_F_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(C)_ +-92 NoClash(C_D_4_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(F)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(B)_ -+5.0 NoClash(B_F_3_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(D)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(C)_ ++2.0 NoClash(C_D_4_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(F)_ --92 NoClash(B_F_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(D)_ +-92 NoClash(C_D_4_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(B)_ --92 NoClash(B_F_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(C)_ +-92 NoClash(C_D_4_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_transformedConstraints(c_0_ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(B)_ -+5.0 NoClash(B_E_5_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(C)_ +-2.0 NoClash(C_E_2_0)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)__t(E)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(E)_ --92 NoClash(B_E_5_0)_binary_indicator_var +-92 NoClash(C_E_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(B)_ --92 NoClash(B_E_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(C)_ +-92 NoClash(C_E_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_transformedConstraints(c_0_ub)_: -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(E)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(B)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(C)_ ++9.0 NoClash(C_E_2_1)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)__t(E)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(E)_ --92 NoClash(B_E_5_1)_binary_indicator_var +-92 NoClash(C_E_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(B)_ --92 NoClash(B_E_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(C)_ +-92 NoClash(C_E_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(B)_ -+7.0 NoClash(B_E_3_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(C)_ ++2.0 NoClash(C_F_1_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(E)_ --92 NoClash(B_E_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(F)_ +-92 NoClash(C_F_1_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(B)_ --92 NoClash(B_E_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(C)_ +-92 NoClash(C_F_1_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(E)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(B)_ -+3.0 NoClash(B_E_3_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(F)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(C)_ ++6.0 NoClash(C_F_1_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(E)_ --92 NoClash(B_E_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(F)_ +-92 NoClash(C_F_1_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(B)_ --92 NoClash(B_E_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(C)_ +-92 NoClash(C_F_1_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(B)_ -+4.0 NoClash(B_E_2_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(C)_ ++5.0 NoClash(C_F_4_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(E)_ --92 NoClash(B_E_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(F)_ +-92 NoClash(C_F_4_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(B)_ --92 NoClash(B_E_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(C)_ +-92 NoClash(C_F_4_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(E)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(B)_ -+3.0 NoClash(B_E_2_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(F)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(C)_ ++8.0 NoClash(C_F_4_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(E)_ --92 NoClash(B_E_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(F)_ +-92 NoClash(C_F_4_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(B)_ --92 NoClash(B_E_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(C)_ +-92 NoClash(C_F_4_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(B)_ -+10.0 NoClash(B_D_3_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(C)_ ++2.0 NoClash(C_G_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(D)_ --92 NoClash(B_D_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(G)_ +-92 NoClash(C_G_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(B)_ --92 NoClash(B_D_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(C)_ +-92 NoClash(C_G_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(D)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(B)_ --1 NoClash(B_D_3_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(G)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(C)_ ++9.0 NoClash(C_G_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(D)_ --92 NoClash(B_D_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(G)_ +-92 NoClash(C_G_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(B)_ --92 NoClash(B_D_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(C)_ +-92 NoClash(C_G_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(B)_ -+8.0 NoClash(B_D_2_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(C)_ ++4.0 NoClash(C_G_4_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(D)_ --92 NoClash(B_D_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(G)_ +-92 NoClash(C_G_4_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(B)_ --92 NoClash(B_D_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(C)_ +-92 NoClash(C_G_4_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(D)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(B)_ -+3.0 NoClash(B_D_2_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(G)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(C)_ ++7.0 NoClash(C_G_4_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(D)_ --92 NoClash(B_D_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(G)_ +-92 NoClash(C_G_4_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(B)_ --92 NoClash(B_D_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(C)_ +-92 NoClash(C_G_4_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(B)_ -+9.0 NoClash(B_C_2_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(D)_ ++4.0 NoClash(D_E_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(C)_ --92 NoClash(B_C_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(E)_ +-92 NoClash(D_E_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(B)_ --92 NoClash(B_C_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(D)_ +-92 NoClash(D_E_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(C)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(B)_ --3.0 NoClash(B_C_2_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(E)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(D)_ ++8.0 NoClash(D_E_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(C)_ --92 NoClash(B_C_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(E)_ +-92 NoClash(D_E_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(B)_ --92 NoClash(B_C_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(D)_ +-92 NoClash(D_E_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(A)_ -+9.0 NoClash(A_G_5_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(D)_ ++2.0 NoClash(D_E_3_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(G)_ --92 NoClash(A_G_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(E)_ +-92 NoClash(D_E_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(A)_ --92 NoClash(A_G_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(D)_ +-92 NoClash(D_E_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(A)_ --3.0 NoClash(A_G_5_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(E)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(D)_ ++9.0 NoClash(D_E_3_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(G)_ --92 NoClash(A_G_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(E)_ +-92 NoClash(D_E_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(A)_ --92 NoClash(A_G_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(D)_ +-92 NoClash(D_E_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_transformedConstraints(c_0_ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(A)_ -+4.0 NoClash(A_F_3_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(D)_ +-1 NoClash(D_F_3_0)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)__t(F)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(F)_ --92 NoClash(A_F_3_0)_binary_indicator_var +-92 NoClash(D_F_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(A)_ --92 NoClash(A_F_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(D)_ +-92 NoClash(D_F_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_transformedConstraints(c_0_ub)_: -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(F)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(A)_ -+6.0 NoClash(A_F_3_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(D)_ ++11.0 NoClash(D_F_3_1)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)__t(F)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(F)_ --92 NoClash(A_F_3_1)_binary_indicator_var +-92 NoClash(D_F_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(A)_ --92 NoClash(A_F_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(D)_ +-92 NoClash(D_F_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_transformedConstraints(c_0_ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(A)_ -+2.0 NoClash(A_F_1_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(D)_ ++1 NoClash(D_F_4_0)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)__t(F)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(F)_ --92 NoClash(A_F_1_0)_binary_indicator_var +-92 NoClash(D_F_4_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(A)_ --92 NoClash(A_F_1_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(D)_ +-92 NoClash(D_F_4_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_transformedConstraints(c_0_ub)_: -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(F)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(A)_ -+3.0 NoClash(A_F_1_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(D)_ ++7.0 NoClash(D_F_4_1)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)__t(F)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(F)_ --92 NoClash(A_F_1_1)_binary_indicator_var +-92 NoClash(D_F_4_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(A)_ --92 NoClash(A_F_1_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(D)_ +-92 NoClash(D_F_4_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(A)_ -+4.0 NoClash(A_E_5_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(D)_ ++8.0 NoClash(D_G_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(E)_ --92 NoClash(A_E_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(G)_ +-92 NoClash(D_G_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(A)_ --92 NoClash(A_E_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(D)_ +-92 NoClash(D_G_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(E)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(G)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(D)_ ++8.0 NoClash(D_G_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(E)_ --92 NoClash(A_E_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(G)_ +-92 NoClash(D_G_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(A)_ --92 NoClash(A_E_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(D)_ +-92 NoClash(D_G_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(A)_ -+7.0 NoClash(A_E_3_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(D)_ <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(E)_ --92 NoClash(A_E_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(G)_ +-92 NoClash(D_G_4_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(A)_ --92 NoClash(A_E_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(D)_ +-92 NoClash(D_G_4_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(E)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(A)_ -+4.0 NoClash(A_E_3_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(G)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(D)_ ++6.0 NoClash(D_G_4_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(E)_ --92 NoClash(A_E_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(G)_ +-92 NoClash(D_G_4_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(A)_ --92 NoClash(A_E_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(D)_ +-92 NoClash(D_G_4_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(A)_ -+10.0 NoClash(A_D_3_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(E)_ ++3.0 NoClash(E_F_3_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(D)_ --92 NoClash(A_D_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(F)_ +-92 NoClash(E_F_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(A)_ --92 NoClash(A_D_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(E)_ +-92 NoClash(E_F_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(D)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(F)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(E)_ ++8.0 NoClash(E_F_3_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(D)_ --92 NoClash(A_D_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(F)_ +-92 NoClash(E_F_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(A)_ --92 NoClash(A_D_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(E)_ +-92 NoClash(E_F_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(A)_ -+6.0 NoClash(A_C_1_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(E)_ ++8.0 NoClash(E_G_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(C)_ --92 NoClash(A_C_1_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(G)_ +-92 NoClash(E_G_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(A)_ --92 NoClash(A_C_1_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(E)_ +-92 NoClash(E_G_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(C)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(A)_ -+3.0 NoClash(A_C_1_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(G)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(E)_ ++4.0 NoClash(E_G_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(C)_ --92 NoClash(A_C_1_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(G)_ +-92 NoClash(E_G_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(A)_ --92 NoClash(A_C_1_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(E)_ +-92 NoClash(E_G_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(A)_ -+2.0 NoClash(A_B_5_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(E)_ ++7.0 NoClash(E_G_5_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(B)_ --92 NoClash(A_B_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(G)_ +-92 NoClash(E_G_5_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(A)_ --92 NoClash(A_B_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(E)_ +-92 NoClash(E_G_5_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(B)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(A)_ -+3.0 NoClash(A_B_5_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(G)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(E)_ +-1 NoClash(E_G_5_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(B)_ --92 NoClash(A_B_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(G)_ +-92 NoClash(E_G_5_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(A)_ --92 NoClash(A_B_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(E)_ +-92 NoClash(E_G_5_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(A)_ -+4.0 NoClash(A_B_3_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(F)_ ++6.0 NoClash(F_G_4_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(B)_ --92 NoClash(A_B_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(G)_ +-92 NoClash(F_G_4_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(A)_ --92 NoClash(A_B_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(F)_ +-92 NoClash(F_G_4_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(B)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(A)_ -+5.0 NoClash(A_B_3_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(G)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(F)_ ++6.0 NoClash(F_G_4_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(B)_ --92 NoClash(A_B_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(G)_ +-92 NoClash(F_G_4_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(A)_ --92 NoClash(A_B_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(F)_ +-92 NoClash(F_G_4_1)_binary_indicator_var <= 0 bounds @@ -1761,285 +1761,285 @@ bounds 0 <= t(E) <= 92 0 <= t(F) <= 92 0 <= t(G) <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(A)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(A)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(B)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(E)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(B)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(34)_disaggregatedVars__t(G)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(35)_disaggregatedVars__t(G)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(34)_disaggregatedVars__t(B)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(35)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(C)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(E)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(D)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(D)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(A)_ <= 92 - 0 <= NoClash(F_G_4_0)_binary_indicator_var <= 1 - 0 <= NoClash(F_G_4_1)_binary_indicator_var <= 1 - 0 <= NoClash(E_G_5_0)_binary_indicator_var <= 1 - 0 <= NoClash(E_G_5_1)_binary_indicator_var <= 1 - 0 <= NoClash(E_G_2_0)_binary_indicator_var <= 1 - 0 <= NoClash(E_G_2_1)_binary_indicator_var <= 1 - 0 <= NoClash(E_F_3_0)_binary_indicator_var <= 1 - 0 <= NoClash(E_F_3_1)_binary_indicator_var <= 1 - 0 <= NoClash(D_G_4_0)_binary_indicator_var <= 1 - 0 <= NoClash(D_G_4_1)_binary_indicator_var <= 1 - 0 <= NoClash(D_G_2_0)_binary_indicator_var <= 1 - 0 <= NoClash(D_G_2_1)_binary_indicator_var <= 1 - 0 <= NoClash(D_F_4_0)_binary_indicator_var <= 1 - 0 <= NoClash(D_F_4_1)_binary_indicator_var <= 1 - 0 <= NoClash(D_F_3_0)_binary_indicator_var <= 1 - 0 <= NoClash(D_F_3_1)_binary_indicator_var <= 1 - 0 <= NoClash(D_E_3_0)_binary_indicator_var <= 1 - 0 <= NoClash(D_E_3_1)_binary_indicator_var <= 1 - 0 <= NoClash(D_E_2_0)_binary_indicator_var <= 1 - 0 <= NoClash(D_E_2_1)_binary_indicator_var <= 1 - 0 <= NoClash(C_G_4_0)_binary_indicator_var <= 1 - 0 <= NoClash(C_G_4_1)_binary_indicator_var <= 1 - 0 <= NoClash(C_G_2_0)_binary_indicator_var <= 1 - 0 <= NoClash(C_G_2_1)_binary_indicator_var <= 1 - 0 <= NoClash(C_F_4_0)_binary_indicator_var <= 1 - 0 <= NoClash(C_F_4_1)_binary_indicator_var <= 1 - 0 <= NoClash(C_F_1_0)_binary_indicator_var <= 1 - 0 <= NoClash(C_F_1_1)_binary_indicator_var <= 1 - 0 <= NoClash(C_E_2_0)_binary_indicator_var <= 1 - 0 <= NoClash(C_E_2_1)_binary_indicator_var <= 1 - 0 <= NoClash(C_D_4_0)_binary_indicator_var <= 1 - 0 <= NoClash(C_D_4_1)_binary_indicator_var <= 1 - 0 <= NoClash(C_D_2_0)_binary_indicator_var <= 1 - 0 <= NoClash(C_D_2_1)_binary_indicator_var <= 1 - 0 <= NoClash(B_G_5_0)_binary_indicator_var <= 1 - 0 <= NoClash(B_G_5_1)_binary_indicator_var <= 1 - 0 <= NoClash(B_G_2_0)_binary_indicator_var <= 1 - 0 <= NoClash(B_G_2_1)_binary_indicator_var <= 1 - 0 <= NoClash(B_F_3_0)_binary_indicator_var <= 1 - 0 <= NoClash(B_F_3_1)_binary_indicator_var <= 1 - 0 <= NoClash(B_E_5_0)_binary_indicator_var <= 1 - 0 <= NoClash(B_E_5_1)_binary_indicator_var <= 1 - 0 <= NoClash(B_E_3_0)_binary_indicator_var <= 1 - 0 <= NoClash(B_E_3_1)_binary_indicator_var <= 1 - 0 <= NoClash(B_E_2_0)_binary_indicator_var <= 1 - 0 <= NoClash(B_E_2_1)_binary_indicator_var <= 1 - 0 <= NoClash(B_D_3_0)_binary_indicator_var <= 1 - 0 <= NoClash(B_D_3_1)_binary_indicator_var <= 1 - 0 <= NoClash(B_D_2_0)_binary_indicator_var <= 1 - 0 <= NoClash(B_D_2_1)_binary_indicator_var <= 1 - 0 <= NoClash(B_C_2_0)_binary_indicator_var <= 1 - 0 <= NoClash(B_C_2_1)_binary_indicator_var <= 1 - 0 <= NoClash(A_G_5_0)_binary_indicator_var <= 1 - 0 <= NoClash(A_G_5_1)_binary_indicator_var <= 1 - 0 <= NoClash(A_F_3_0)_binary_indicator_var <= 1 - 0 <= NoClash(A_F_3_1)_binary_indicator_var <= 1 - 0 <= NoClash(A_F_1_0)_binary_indicator_var <= 1 - 0 <= NoClash(A_F_1_1)_binary_indicator_var <= 1 - 0 <= NoClash(A_E_5_0)_binary_indicator_var <= 1 - 0 <= NoClash(A_E_5_1)_binary_indicator_var <= 1 - 0 <= NoClash(A_E_3_0)_binary_indicator_var <= 1 - 0 <= NoClash(A_E_3_1)_binary_indicator_var <= 1 - 0 <= NoClash(A_D_3_0)_binary_indicator_var <= 1 - 0 <= NoClash(A_D_3_1)_binary_indicator_var <= 1 - 0 <= NoClash(A_C_1_0)_binary_indicator_var <= 1 - 0 <= NoClash(A_C_1_1)_binary_indicator_var <= 1 - 0 <= NoClash(A_B_5_0)_binary_indicator_var <= 1 - 0 <= NoClash(A_B_5_1)_binary_indicator_var <= 1 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(F)_ <= 92 0 <= NoClash(A_B_3_0)_binary_indicator_var <= 1 0 <= NoClash(A_B_3_1)_binary_indicator_var <= 1 + 0 <= NoClash(A_B_5_0)_binary_indicator_var <= 1 + 0 <= NoClash(A_B_5_1)_binary_indicator_var <= 1 + 0 <= NoClash(A_C_1_0)_binary_indicator_var <= 1 + 0 <= NoClash(A_C_1_1)_binary_indicator_var <= 1 + 0 <= NoClash(A_D_3_0)_binary_indicator_var <= 1 + 0 <= NoClash(A_D_3_1)_binary_indicator_var <= 1 + 0 <= NoClash(A_E_3_0)_binary_indicator_var <= 1 + 0 <= NoClash(A_E_3_1)_binary_indicator_var <= 1 + 0 <= NoClash(A_E_5_0)_binary_indicator_var <= 1 + 0 <= NoClash(A_E_5_1)_binary_indicator_var <= 1 + 0 <= NoClash(A_F_1_0)_binary_indicator_var <= 1 + 0 <= NoClash(A_F_1_1)_binary_indicator_var <= 1 + 0 <= NoClash(A_F_3_0)_binary_indicator_var <= 1 + 0 <= NoClash(A_F_3_1)_binary_indicator_var <= 1 + 0 <= NoClash(A_G_5_0)_binary_indicator_var <= 1 + 0 <= NoClash(A_G_5_1)_binary_indicator_var <= 1 + 0 <= NoClash(B_C_2_0)_binary_indicator_var <= 1 + 0 <= NoClash(B_C_2_1)_binary_indicator_var <= 1 + 0 <= NoClash(B_D_2_0)_binary_indicator_var <= 1 + 0 <= NoClash(B_D_2_1)_binary_indicator_var <= 1 + 0 <= NoClash(B_D_3_0)_binary_indicator_var <= 1 + 0 <= NoClash(B_D_3_1)_binary_indicator_var <= 1 + 0 <= NoClash(B_E_2_0)_binary_indicator_var <= 1 + 0 <= NoClash(B_E_2_1)_binary_indicator_var <= 1 + 0 <= NoClash(B_E_3_0)_binary_indicator_var <= 1 + 0 <= NoClash(B_E_3_1)_binary_indicator_var <= 1 + 0 <= NoClash(B_E_5_0)_binary_indicator_var <= 1 + 0 <= NoClash(B_E_5_1)_binary_indicator_var <= 1 + 0 <= NoClash(B_F_3_0)_binary_indicator_var <= 1 + 0 <= NoClash(B_F_3_1)_binary_indicator_var <= 1 + 0 <= NoClash(B_G_2_0)_binary_indicator_var <= 1 + 0 <= NoClash(B_G_2_1)_binary_indicator_var <= 1 + 0 <= NoClash(B_G_5_0)_binary_indicator_var <= 1 + 0 <= NoClash(B_G_5_1)_binary_indicator_var <= 1 + 0 <= NoClash(C_D_2_0)_binary_indicator_var <= 1 + 0 <= NoClash(C_D_2_1)_binary_indicator_var <= 1 + 0 <= NoClash(C_D_4_0)_binary_indicator_var <= 1 + 0 <= NoClash(C_D_4_1)_binary_indicator_var <= 1 + 0 <= NoClash(C_E_2_0)_binary_indicator_var <= 1 + 0 <= NoClash(C_E_2_1)_binary_indicator_var <= 1 + 0 <= NoClash(C_F_1_0)_binary_indicator_var <= 1 + 0 <= NoClash(C_F_1_1)_binary_indicator_var <= 1 + 0 <= NoClash(C_F_4_0)_binary_indicator_var <= 1 + 0 <= NoClash(C_F_4_1)_binary_indicator_var <= 1 + 0 <= NoClash(C_G_2_0)_binary_indicator_var <= 1 + 0 <= NoClash(C_G_2_1)_binary_indicator_var <= 1 + 0 <= NoClash(C_G_4_0)_binary_indicator_var <= 1 + 0 <= NoClash(C_G_4_1)_binary_indicator_var <= 1 + 0 <= NoClash(D_E_2_0)_binary_indicator_var <= 1 + 0 <= NoClash(D_E_2_1)_binary_indicator_var <= 1 + 0 <= NoClash(D_E_3_0)_binary_indicator_var <= 1 + 0 <= NoClash(D_E_3_1)_binary_indicator_var <= 1 + 0 <= NoClash(D_F_3_0)_binary_indicator_var <= 1 + 0 <= NoClash(D_F_3_1)_binary_indicator_var <= 1 + 0 <= NoClash(D_F_4_0)_binary_indicator_var <= 1 + 0 <= NoClash(D_F_4_1)_binary_indicator_var <= 1 + 0 <= NoClash(D_G_2_0)_binary_indicator_var <= 1 + 0 <= NoClash(D_G_2_1)_binary_indicator_var <= 1 + 0 <= NoClash(D_G_4_0)_binary_indicator_var <= 1 + 0 <= NoClash(D_G_4_1)_binary_indicator_var <= 1 + 0 <= NoClash(E_F_3_0)_binary_indicator_var <= 1 + 0 <= NoClash(E_F_3_1)_binary_indicator_var <= 1 + 0 <= NoClash(E_G_2_0)_binary_indicator_var <= 1 + 0 <= NoClash(E_G_2_1)_binary_indicator_var <= 1 + 0 <= NoClash(E_G_5_0)_binary_indicator_var <= 1 + 0 <= NoClash(E_G_5_1)_binary_indicator_var <= 1 + 0 <= NoClash(F_G_4_0)_binary_indicator_var <= 1 + 0 <= NoClash(F_G_4_1)_binary_indicator_var <= 1 binary - NoClash(F_G_4_0)_binary_indicator_var - NoClash(F_G_4_1)_binary_indicator_var - NoClash(E_G_5_0)_binary_indicator_var - NoClash(E_G_5_1)_binary_indicator_var - NoClash(E_G_2_0)_binary_indicator_var - NoClash(E_G_2_1)_binary_indicator_var - NoClash(E_F_3_0)_binary_indicator_var - NoClash(E_F_3_1)_binary_indicator_var - NoClash(D_G_4_0)_binary_indicator_var - NoClash(D_G_4_1)_binary_indicator_var - NoClash(D_G_2_0)_binary_indicator_var - NoClash(D_G_2_1)_binary_indicator_var - NoClash(D_F_4_0)_binary_indicator_var - NoClash(D_F_4_1)_binary_indicator_var - NoClash(D_F_3_0)_binary_indicator_var - NoClash(D_F_3_1)_binary_indicator_var - NoClash(D_E_3_0)_binary_indicator_var - NoClash(D_E_3_1)_binary_indicator_var - NoClash(D_E_2_0)_binary_indicator_var - NoClash(D_E_2_1)_binary_indicator_var - NoClash(C_G_4_0)_binary_indicator_var - NoClash(C_G_4_1)_binary_indicator_var - NoClash(C_G_2_0)_binary_indicator_var - NoClash(C_G_2_1)_binary_indicator_var - NoClash(C_F_4_0)_binary_indicator_var - NoClash(C_F_4_1)_binary_indicator_var - NoClash(C_F_1_0)_binary_indicator_var - NoClash(C_F_1_1)_binary_indicator_var - NoClash(C_E_2_0)_binary_indicator_var - NoClash(C_E_2_1)_binary_indicator_var - NoClash(C_D_4_0)_binary_indicator_var - NoClash(C_D_4_1)_binary_indicator_var - NoClash(C_D_2_0)_binary_indicator_var - NoClash(C_D_2_1)_binary_indicator_var - NoClash(B_G_5_0)_binary_indicator_var - NoClash(B_G_5_1)_binary_indicator_var - NoClash(B_G_2_0)_binary_indicator_var - NoClash(B_G_2_1)_binary_indicator_var - NoClash(B_F_3_0)_binary_indicator_var - NoClash(B_F_3_1)_binary_indicator_var - NoClash(B_E_5_0)_binary_indicator_var - NoClash(B_E_5_1)_binary_indicator_var - NoClash(B_E_3_0)_binary_indicator_var - NoClash(B_E_3_1)_binary_indicator_var - NoClash(B_E_2_0)_binary_indicator_var - NoClash(B_E_2_1)_binary_indicator_var - NoClash(B_D_3_0)_binary_indicator_var - NoClash(B_D_3_1)_binary_indicator_var - NoClash(B_D_2_0)_binary_indicator_var - NoClash(B_D_2_1)_binary_indicator_var - NoClash(B_C_2_0)_binary_indicator_var - NoClash(B_C_2_1)_binary_indicator_var - NoClash(A_G_5_0)_binary_indicator_var - NoClash(A_G_5_1)_binary_indicator_var - NoClash(A_F_3_0)_binary_indicator_var - NoClash(A_F_3_1)_binary_indicator_var - NoClash(A_F_1_0)_binary_indicator_var - NoClash(A_F_1_1)_binary_indicator_var - NoClash(A_E_5_0)_binary_indicator_var - NoClash(A_E_5_1)_binary_indicator_var - NoClash(A_E_3_0)_binary_indicator_var - NoClash(A_E_3_1)_binary_indicator_var - NoClash(A_D_3_0)_binary_indicator_var - NoClash(A_D_3_1)_binary_indicator_var - NoClash(A_C_1_0)_binary_indicator_var - NoClash(A_C_1_1)_binary_indicator_var - NoClash(A_B_5_0)_binary_indicator_var - NoClash(A_B_5_1)_binary_indicator_var NoClash(A_B_3_0)_binary_indicator_var NoClash(A_B_3_1)_binary_indicator_var + NoClash(A_B_5_0)_binary_indicator_var + NoClash(A_B_5_1)_binary_indicator_var + NoClash(A_C_1_0)_binary_indicator_var + NoClash(A_C_1_1)_binary_indicator_var + NoClash(A_D_3_0)_binary_indicator_var + NoClash(A_D_3_1)_binary_indicator_var + NoClash(A_E_3_0)_binary_indicator_var + NoClash(A_E_3_1)_binary_indicator_var + NoClash(A_E_5_0)_binary_indicator_var + NoClash(A_E_5_1)_binary_indicator_var + NoClash(A_F_1_0)_binary_indicator_var + NoClash(A_F_1_1)_binary_indicator_var + NoClash(A_F_3_0)_binary_indicator_var + NoClash(A_F_3_1)_binary_indicator_var + NoClash(A_G_5_0)_binary_indicator_var + NoClash(A_G_5_1)_binary_indicator_var + NoClash(B_C_2_0)_binary_indicator_var + NoClash(B_C_2_1)_binary_indicator_var + NoClash(B_D_2_0)_binary_indicator_var + NoClash(B_D_2_1)_binary_indicator_var + NoClash(B_D_3_0)_binary_indicator_var + NoClash(B_D_3_1)_binary_indicator_var + NoClash(B_E_2_0)_binary_indicator_var + NoClash(B_E_2_1)_binary_indicator_var + NoClash(B_E_3_0)_binary_indicator_var + NoClash(B_E_3_1)_binary_indicator_var + NoClash(B_E_5_0)_binary_indicator_var + NoClash(B_E_5_1)_binary_indicator_var + NoClash(B_F_3_0)_binary_indicator_var + NoClash(B_F_3_1)_binary_indicator_var + NoClash(B_G_2_0)_binary_indicator_var + NoClash(B_G_2_1)_binary_indicator_var + NoClash(B_G_5_0)_binary_indicator_var + NoClash(B_G_5_1)_binary_indicator_var + NoClash(C_D_2_0)_binary_indicator_var + NoClash(C_D_2_1)_binary_indicator_var + NoClash(C_D_4_0)_binary_indicator_var + NoClash(C_D_4_1)_binary_indicator_var + NoClash(C_E_2_0)_binary_indicator_var + NoClash(C_E_2_1)_binary_indicator_var + NoClash(C_F_1_0)_binary_indicator_var + NoClash(C_F_1_1)_binary_indicator_var + NoClash(C_F_4_0)_binary_indicator_var + NoClash(C_F_4_1)_binary_indicator_var + NoClash(C_G_2_0)_binary_indicator_var + NoClash(C_G_2_1)_binary_indicator_var + NoClash(C_G_4_0)_binary_indicator_var + NoClash(C_G_4_1)_binary_indicator_var + NoClash(D_E_2_0)_binary_indicator_var + NoClash(D_E_2_1)_binary_indicator_var + NoClash(D_E_3_0)_binary_indicator_var + NoClash(D_E_3_1)_binary_indicator_var + NoClash(D_F_3_0)_binary_indicator_var + NoClash(D_F_3_1)_binary_indicator_var + NoClash(D_F_4_0)_binary_indicator_var + NoClash(D_F_4_1)_binary_indicator_var + NoClash(D_G_2_0)_binary_indicator_var + NoClash(D_G_2_1)_binary_indicator_var + NoClash(D_G_4_0)_binary_indicator_var + NoClash(D_G_4_1)_binary_indicator_var + NoClash(E_F_3_0)_binary_indicator_var + NoClash(E_F_3_1)_binary_indicator_var + NoClash(E_G_2_0)_binary_indicator_var + NoClash(E_G_2_1)_binary_indicator_var + NoClash(E_G_5_0)_binary_indicator_var + NoClash(E_G_5_1)_binary_indicator_var + NoClash(F_G_4_0)_binary_indicator_var + NoClash(F_G_4_1)_binary_indicator_var end diff --git a/pyomo/gdp/tests/jobshop_small_hull.lp b/pyomo/gdp/tests/jobshop_small_hull.lp index 95434e3122f..eccaa800600 100644 --- a/pyomo/gdp/tests/jobshop_small_hull.lp +++ b/pyomo/gdp/tests/jobshop_small_hull.lp @@ -22,17 +22,17 @@ c_u_Feas(C)_: <= -6 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(0)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(C)_ -= 0 - -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(1)_: +1 t(B) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ = 0 +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(1)_: ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ += 0 + c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(2)_: +1 t(C) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ @@ -46,20 +46,20 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(3)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(4)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(B)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(5)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(B)_ = 0 -c_e__pyomo_gdp_hull_reformulation_disj_xor(B_C_2)_: -+1 NoClash(B_C_2_0)_binary_indicator_var -+1 NoClash(B_C_2_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(A_B_3)_: ++1 NoClash(A_B_3_0)_binary_indicator_var ++1 NoClash(A_B_3_1)_binary_indicator_var = 1 c_e__pyomo_gdp_hull_reformulation_disj_xor(A_C_1)_: @@ -67,41 +67,40 @@ c_e__pyomo_gdp_hull_reformulation_disj_xor(A_C_1)_: +1 NoClash(A_C_1_1)_binary_indicator_var = 1 -c_e__pyomo_gdp_hull_reformulation_disj_xor(A_B_3)_: -+1 NoClash(A_B_3_0)_binary_indicator_var -+1 NoClash(A_B_3_1)_binary_indicator_var +c_e__pyomo_gdp_hull_reformulation_disj_xor(B_C_2)_: ++1 NoClash(B_C_2_0)_binary_indicator_var ++1 NoClash(B_C_2_1)_binary_indicator_var = 1 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ -+6.0 NoClash(B_C_2_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(C)_ --19 NoClash(B_C_2_0)_binary_indicator_var -<= 0 - c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)__t(B)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ --19 NoClash(B_C_2_0)_binary_indicator_var +-19 NoClash(A_B_3_0)_binary_indicator_var +<= 0 + +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ +-19 NoClash(A_B_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(C)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ -+1 NoClash(B_C_2_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ ++5.0 NoClash(A_B_3_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(C)_ --19 NoClash(B_C_2_1)_binary_indicator_var -<= 0 - c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)__t(B)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ --19 NoClash(B_C_2_1)_binary_indicator_var +-19 NoClash(A_B_3_1)_binary_indicator_var +<= 0 + +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ +-19 NoClash(A_B_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_transformedConstraints(c_0_ub)_: @@ -137,34 +136,35 @@ c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(A)_bounds_(ub)_: <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ ++6.0 NoClash(B_C_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ --19 NoClash(A_B_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ +-19 NoClash(B_C_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ --19 NoClash(A_B_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ +-19 NoClash(B_C_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(B)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ -+5.0 NoClash(A_B_3_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(B)_ ++1 NoClash(B_C_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(B)_ --19 NoClash(A_B_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ +-19 NoClash(B_C_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ --19 NoClash(A_B_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(B)_ +-19 NoClash(B_C_2_1)_binary_indicator_var <= 0 bounds @@ -172,29 +172,29 @@ bounds 0 <= t(A) <= 19 0 <= t(B) <= 19 0 <= t(C) <= 19 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(C)_ <= 19 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(C)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ <= 19 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ <= 19 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ <= 19 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ <= 19 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(B)_ <= 19 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ <= 19 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ <= 19 - 0 <= NoClash(B_C_2_0)_binary_indicator_var <= 1 - 0 <= NoClash(B_C_2_1)_binary_indicator_var <= 1 - 0 <= NoClash(A_C_1_0)_binary_indicator_var <= 1 - 0 <= NoClash(A_C_1_1)_binary_indicator_var <= 1 0 <= NoClash(A_B_3_0)_binary_indicator_var <= 1 0 <= NoClash(A_B_3_1)_binary_indicator_var <= 1 + 0 <= NoClash(A_C_1_0)_binary_indicator_var <= 1 + 0 <= NoClash(A_C_1_1)_binary_indicator_var <= 1 + 0 <= NoClash(B_C_2_0)_binary_indicator_var <= 1 + 0 <= NoClash(B_C_2_1)_binary_indicator_var <= 1 binary - NoClash(B_C_2_0)_binary_indicator_var - NoClash(B_C_2_1)_binary_indicator_var - NoClash(A_C_1_0)_binary_indicator_var - NoClash(A_C_1_1)_binary_indicator_var NoClash(A_B_3_0)_binary_indicator_var NoClash(A_B_3_1)_binary_indicator_var + NoClash(A_C_1_0)_binary_indicator_var + NoClash(A_C_1_1)_binary_indicator_var + NoClash(B_C_2_0)_binary_indicator_var + NoClash(B_C_2_1)_binary_indicator_var end diff --git a/pyomo/gdp/tests/models.py b/pyomo/gdp/tests/models.py index a52f08b790e..2995cacb450 100644 --- a/pyomo/gdp/tests/models.py +++ b/pyomo/gdp/tests/models.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core import ( Block, ConcreteModel, @@ -463,7 +474,7 @@ def makeNestedDisjunctions(): (makeNestedDisjunctions_NestedDisjuncts is a much simpler model. All this adds is that it has a nested disjunction on a DisjunctData as well - as on a SimpleDisjunct. So mostly it exists for historical reasons.) + as on a ScalarDisjunct. So mostly it exists for historical reasons.) """ m = ConcreteModel() m.x = Var(bounds=(-9, 9)) @@ -552,6 +563,44 @@ def makeNestedDisjunctions_NestedDisjuncts(): return m +def why_indicator_vars_are_not_always_local(): + m = ConcreteModel() + m.x = Var(bounds=(1, 10)) + + @m.Disjunct() + def Z1(d): + m = d.model() + d.c = Constraint(expr=m.x >= 1.1) + + @m.Disjunct() + def Z2(d): + m = d.model() + d.c = Constraint(expr=m.x >= 1.2) + + @m.Disjunct() + def Y1(d): + m = d.model() + d.c = Constraint(expr=(1.15, m.x, 8)) + d.disjunction = Disjunction(expr=[m.Z1, m.Z2]) + + @m.Disjunct() + def Y2(d): + m = d.model() + d.c = Constraint(expr=m.x == 9) + + m.disjunction = Disjunction(expr=[m.Y1, m.Y2]) + + m.logical_cons = LogicalConstraint( + expr=m.Y2.indicator_var.implies(m.Z1.indicator_var.land(m.Z2.indicator_var)) + ) + + # optimal value is 9, but it will be 8 if we wrongly assume that the nested + # indicator_vars are local. + m.obj = Objective(expr=m.x, sense=maximize) + + return m + + def makeTwoSimpleDisjunctions(): """Two SimpleDisjunctions on the same model.""" m = ConcreteModel() @@ -791,7 +840,7 @@ def makeAnyIndexedDisjunctionOfDisjunctDatas(): build from DisjunctDatas. Identical mathematically to makeDisjunctionOfDisjunctDatas. - Used to test that the right things happen for a case where soemone + Used to test that the right things happen for a case where someone implements an algorithm which iteratively generates disjuncts and retransforms""" m = ConcreteModel() diff --git a/pyomo/gdp/tests/test_basic_step.py b/pyomo/gdp/tests/test_basic_step.py index 631611a2651..7e21c46da92 100644 --- a/pyomo/gdp/tests/test_basic_step.py +++ b/pyomo/gdp/tests/test_basic_step.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py index 13ffe30f9f0..c27d7cbe0cb 100644 --- a/pyomo/gdp/tests/test_bigm.py +++ b/pyomo/gdp/tests/test_bigm.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -19,20 +19,24 @@ Set, Constraint, ComponentMap, + LogicalConstraint, + Objective, SolverFactory, Suffix, + TerminationCondition, ConcreteModel, Var, Any, value, ) from pyomo.gdp import Disjunct, Disjunction, GDP_Error -from pyomo.core.base import constraint, _ConstraintData +from pyomo.core.base import constraint, ConstraintData from pyomo.core.expr.compare import ( assertExpressionsEqual, assertExpressionsStructurallyEqual, ) from pyomo.repn import generate_standard_repn +from pyomo.repn.linear import LinearRepnVisitor from pyomo.common.log import LoggingIntercept import logging @@ -154,10 +158,7 @@ def test_or_constraints(self): self, orcons.body, EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, m.d[0].binary_indicator_var)), - EXPR.MonomialTermExpression((1, m.d[1].binary_indicator_var)), - ] + [m.d[0].binary_indicator_var, m.d[1].binary_indicator_var] ), ) self.assertEqual(orcons.lower, 1) @@ -655,14 +656,14 @@ def test_disjunct_and_constraint_maps(self): if src[0]: # equality self.assertEqual(len(transformed), 2) - self.assertIsInstance(transformed[0], _ConstraintData) - self.assertIsInstance(transformed[1], _ConstraintData) + self.assertIsInstance(transformed[0], ConstraintData) + self.assertIsInstance(transformed[1], ConstraintData) self.assertIs(bigm.get_src_constraint(transformed[0]), srcDisjunct.c) self.assertIs(bigm.get_src_constraint(transformed[1]), srcDisjunct.c) else: # >= self.assertEqual(len(transformed), 1) - self.assertIsInstance(transformed[0], _ConstraintData) + self.assertIsInstance(transformed[0], ConstraintData) # check reverse map from the container self.assertIs(bigm.get_src_constraint(transformed[0]), srcDisjunct.c) @@ -1315,26 +1316,18 @@ def test_do_not_transform_deactivated_constraintDatas(self): bigm.apply_to(m) # the real test: This wasn't transformed - log = StringIO() - with LoggingIntercept(log, 'pyomo.gdp', logging.ERROR): - self.assertRaisesRegex( - KeyError, - r".*b.simpledisj1.c\[1\]", - bigm.get_transformed_constraints, - m.b.simpledisj1.c[1], - ) - self.assertRegex( - log.getvalue(), - r".*Constraint 'b.simpledisj1.c\[1\]' has not been transformed.", - ) + with self.assertRaisesRegex( + GDP_Error, r"Constraint 'b.simpledisj1.c\[1\]' has not been transformed." + ): + bigm.get_transformed_constraints(m.b.simpledisj1.c[1]) # and the rest of the container was transformed cons_list = bigm.get_transformed_constraints(m.b.simpledisj1.c[2]) self.assertEqual(len(cons_list), 2) lb = cons_list[0] ub = cons_list[1] - self.assertIsInstance(lb, constraint._GeneralConstraintData) - self.assertIsInstance(ub, constraint._GeneralConstraintData) + self.assertIsInstance(lb, constraint.ConstraintData) + self.assertIsInstance(ub, constraint.ConstraintData) def checkMs( self, m, disj1c1lb, disj1c1ub, disj1c2lb, disj1c2ub, disj2c1ub, disj2c2ub @@ -1764,22 +1757,19 @@ def test_transformation_block_structure(self): # we have the XOR constraints for both the outer and inner disjunctions self.assertIsInstance(transBlock.component("disjunction_xor"), Constraint) - def test_transformation_block_on_inner_disjunct_empty(self): - m = models.makeNestedDisjunctions() - TransformationFactory('gdp.bigm').apply_to(m) - self.assertIsNone(m.disjunct[1].component("_pyomo_gdp_bigm_reformulation")) - def test_mappings_between_disjunctions_and_xors(self): m = models.makeNestedDisjunctions() transform = TransformationFactory('gdp.bigm') transform.apply_to(m) transBlock1 = m.component("_pyomo_gdp_bigm_reformulation") + transBlock2 = m.disjunct[1].component("_pyomo_gdp_bigm_reformulation") + transBlock3 = m.simpledisjunct.component("_pyomo_gdp_bigm_reformulation") disjunctionPairs = [ (m.disjunction, transBlock1.disjunction_xor), - (m.disjunct[1].innerdisjunction[0], transBlock1.innerdisjunction_xor_4[0]), - (m.simpledisjunct.innerdisjunction, transBlock1.innerdisjunction_xor), + (m.disjunct[1].innerdisjunction[0], transBlock2.innerdisjunction_xor[0]), + (m.simpledisjunct.innerdisjunction, transBlock3.innerdisjunction_xor), ] # check disjunction mappings @@ -1892,26 +1882,38 @@ def test_m_value_mappings(self): # many of the transformed constraints look like this, so can call this # function to test them. def check_bigM_constraint(self, cons, variable, M, indicator_var): - repn = generate_standard_repn(cons.body) - self.assertTrue(repn.is_linear()) - self.assertEqual(repn.constant, -M) - self.assertEqual(len(repn.linear_vars), 2) - ct.check_linear_coef(self, repn, variable, 1) - ct.check_linear_coef(self, repn, indicator_var, M) + assertExpressionsEqual( + self, + cons.body, + variable - float(M) * (1 - indicator_var.get_associated_binary()), + ) - def check_inner_xor_constraint( - self, inner_disjunction, outer_disjunct, inner_disjuncts - ): - self.assertIsNotNone(inner_disjunction.algebraic_constraint) - cons = inner_disjunction.algebraic_constraint - self.assertEqual(cons.lower, 0) - self.assertEqual(cons.upper, 0) - repn = generate_standard_repn(cons.body) - self.assertTrue(repn.is_linear()) - self.assertEqual(repn.constant, 0) - for disj in inner_disjuncts: - ct.check_linear_coef(self, repn, disj.binary_indicator_var, 1) - ct.check_linear_coef(self, repn, outer_disjunct.binary_indicator_var, -1) + def check_inner_xor_constraint(self, inner_disjunction, outer_disjunct, bigm): + inner_xor = inner_disjunction.algebraic_constraint + sum_indicators = sum( + d.binary_indicator_var for d in inner_disjunction.disjuncts + ) + assertExpressionsEqual(self, inner_xor.expr, sum_indicators == 1) + # this guy has been transformed + self.assertFalse(inner_xor.active) + cons = bigm.get_transformed_constraints(inner_xor) + self.assertEqual(len(cons), 2) + lb = cons[0] + ct.check_obj_in_active_tree(self, lb) + lb_expr = self.simplify_cons(lb, leq=False) + assertExpressionsEqual( + self, + lb_expr, + 1.0 <= sum_indicators - outer_disjunct.binary_indicator_var + 1.0, + ) + ub = cons[1] + ct.check_obj_in_active_tree(self, ub) + ub_expr = self.simplify_cons(ub, leq=True) + assertExpressionsEqual( + self, + ub_expr, + sum_indicators + outer_disjunct.binary_indicator_var - 1 <= 1.0, + ) def test_transformed_constraints(self): # We'll check all the transformed constraints to make sure @@ -1949,6 +1951,10 @@ def test_transformed_constraints(self): .binary_indicator_var, ) ), + 1, + EXPR.MonomialTermExpression( + (-1, m.disjunct[1].binary_indicator_var) + ), ] ), ) @@ -1958,61 +1964,76 @@ def test_transformed_constraints(self): ] ), ) - self.assertIsNone(cons1ub.lower) - self.assertEqual(cons1ub.upper, 0) - self.check_bigM_constraint( - cons1ub, m.z, 10, m.disjunct[1].innerdisjunct[0].indicator_var + assertExpressionsEqual( + self, + cons1ub.expr, + m.z + - 10.0 + * ( + 1 + - m.disjunct[1].innerdisjunct[0].binary_indicator_var + + 1 + - m.disjunct[1].binary_indicator_var + ) + <= 0.0, ) cons2 = bigm.get_transformed_constraints(m.disjunct[1].innerdisjunct[1].c) self.assertEqual(len(cons2), 1) cons2lb = cons2[0] - self.assertEqual(cons2lb.lower, 5) - self.assertIsNone(cons2lb.upper) - self.check_bigM_constraint( - cons2lb, m.z, -5, m.disjunct[1].innerdisjunct[1].indicator_var + assertExpressionsEqual( + self, + cons2lb.expr, + 5.0 + <= m.z + - (-5.0) + * ( + 1 + - m.disjunct[1].innerdisjunct[1].binary_indicator_var + + 1 + - m.disjunct[1].binary_indicator_var + ), ) cons3 = bigm.get_transformed_constraints(m.simpledisjunct.innerdisjunct0.c) self.assertEqual(len(cons3), 1) cons3ub = cons3[0] - self.assertEqual(cons3ub.upper, 2) - self.assertIsNone(cons3ub.lower) - self.check_bigM_constraint( - cons3ub, m.x, 7, m.simpledisjunct.innerdisjunct0.indicator_var + assertExpressionsEqual( + self, + cons3ub.expr, + m.x + - 7.0 + * ( + 1 + - m.simpledisjunct.innerdisjunct0.binary_indicator_var + + 1 + - m.simpledisjunct.binary_indicator_var + ) + <= 2.0, ) cons4 = bigm.get_transformed_constraints(m.simpledisjunct.innerdisjunct1.c) self.assertEqual(len(cons4), 1) cons4lb = cons4[0] - self.assertEqual(cons4lb.lower, 4) - self.assertIsNone(cons4lb.upper) - self.check_bigM_constraint( - cons4lb, m.x, -13, m.simpledisjunct.innerdisjunct1.indicator_var + assertExpressionsEqual( + self, + cons4lb.expr, + m.x + - (-13.0) + * ( + 1 + - m.simpledisjunct.innerdisjunct1.binary_indicator_var + + 1 + - m.simpledisjunct.binary_indicator_var + ) + >= 4.0, ) # Here we check that the xor constraint from # simpledisjunct.innerdisjunction is transformed. - cons5 = m.simpledisjunct.innerdisjunction.algebraic_constraint - self.assertIsNotNone(cons5) self.check_inner_xor_constraint( - m.simpledisjunct.innerdisjunction, - m.simpledisjunct, - [m.simpledisjunct.innerdisjunct0, m.simpledisjunct.innerdisjunct1], - ) - self.assertIsInstance(cons5, Constraint) - self.assertEqual(cons5.lower, 0) - self.assertEqual(cons5.upper, 0) - repn = generate_standard_repn(cons5.body) - self.assertTrue(repn.is_linear()) - self.assertEqual(repn.constant, 0) - ct.check_linear_coef( - self, repn, m.simpledisjunct.innerdisjunct0.binary_indicator_var, 1 + m.simpledisjunct.innerdisjunction, m.simpledisjunct, bigm ) - ct.check_linear_coef( - self, repn, m.simpledisjunct.innerdisjunct1.binary_indicator_var, 1 - ) - ct.check_linear_coef(self, repn, m.simpledisjunct.binary_indicator_var, -1) cons6 = bigm.get_transformed_constraints(m.disjunct[0].c) self.assertEqual(len(cons6), 2) @@ -2028,9 +2049,7 @@ def test_transformed_constraints(self): # now we check that the xor constraint from disjunct[1].innerdisjunction # is correct. self.check_inner_xor_constraint( - m.disjunct[1].innerdisjunction[0], - m.disjunct[1], - [m.disjunct[1].innerdisjunct[0], m.disjunct[1].innerdisjunct[1]], + m.disjunct[1].innerdisjunction[0], m.disjunct[1], bigm ) cons8 = bigm.get_transformed_constraints(m.disjunct[1].c) @@ -2107,34 +2126,18 @@ def innerIndexed(d, i): m._pyomo_gdp_bigm_reformulation.relaxedDisjuncts, ) - def check_first_disjunct_constraint(self, disj1c, x, ind_var): - self.assertEqual(len(disj1c), 1) - cons = disj1c[0] - self.assertIsNone(cons.lower) - self.assertEqual(cons.upper, 1) - repn = generate_standard_repn(cons.body) - self.assertTrue(repn.is_quadratic()) - self.assertEqual(len(repn.linear_vars), 1) - self.assertEqual(len(repn.quadratic_vars), 4) - ct.check_linear_coef(self, repn, ind_var, 143) - self.assertEqual(repn.constant, -143) - for i in range(1, 5): - ct.check_squared_term_coef(self, repn, x[i], 1) - - def check_second_disjunct_constraint(self, disj2c, x, ind_var): - self.assertEqual(len(disj2c), 1) - cons = disj2c[0] - self.assertIsNone(cons.lower) - self.assertEqual(cons.upper, 1) - repn = generate_standard_repn(cons.body) - self.assertTrue(repn.is_quadratic()) - self.assertEqual(len(repn.linear_vars), 5) - self.assertEqual(len(repn.quadratic_vars), 4) - self.assertEqual(repn.constant, -63) # M = 99, so this is 36 - 99 - ct.check_linear_coef(self, repn, ind_var, 99) - for i in range(1, 5): - ct.check_squared_term_coef(self, repn, x[i], 1) - ct.check_linear_coef(self, repn, x[i], -6) + def simplify_cons(self, cons, leq): + visitor = LinearRepnVisitor({}, {}, {}, None) + repn = visitor.walk_expression(cons.body) + self.assertIsNone(repn.nonlinear) + if leq: + self.assertIsNone(cons.lower) + ub = cons.upper + return ub >= repn.to_expression(visitor) + else: + self.assertIsNone(cons.upper) + lb = cons.lower + return lb <= repn.to_expression(visitor) def check_hierarchical_nested_model(self, m, bigm): outer_xor = m.disjunction_block.disjunction.algebraic_constraint @@ -2142,55 +2145,82 @@ def check_hierarchical_nested_model(self, m, bigm): self, outer_xor, m.disj1, m.disjunct_block.disj2 ) - inner_xor = m.disjunct_block.disj2.disjunction.algebraic_constraint - self.assertEqual(inner_xor.lower, 0) - self.assertEqual(inner_xor.upper, 0) - repn = generate_standard_repn(inner_xor.body) - self.assertTrue(repn.is_linear()) - self.assertEqual(len(repn.linear_vars), 3) - self.assertEqual(repn.constant, 0) - ct.check_linear_coef( - self, - repn, - m.disjunct_block.disj2.disjunction_disjuncts[0].binary_indicator_var, - 1, - ) - ct.check_linear_coef( - self, - repn, - m.disjunct_block.disj2.disjunction_disjuncts[1].binary_indicator_var, - 1, - ) - ct.check_linear_coef( - self, repn, m.disjunct_block.disj2.binary_indicator_var, -1 + self.check_inner_xor_constraint( + m.disjunct_block.disj2.disjunction, m.disjunct_block.disj2, bigm ) # outer disjunction constraints disj1c = bigm.get_transformed_constraints(m.disj1.c) - self.check_first_disjunct_constraint(disj1c, m.x, m.disj1.binary_indicator_var) + self.assertEqual(len(disj1c), 1) + cons = disj1c[0] + assertExpressionsEqual( + self, + cons.expr, + m.x[1] ** 2 + + m.x[2] ** 2 + + m.x[3] ** 2 + + m.x[4] ** 2 + - 143.0 * (1 - m.disj1.binary_indicator_var) + <= 1.0, + ) disj2c = bigm.get_transformed_constraints(m.disjunct_block.disj2.c) - self.check_second_disjunct_constraint( - disj2c, m.x, m.disjunct_block.disj2.binary_indicator_var + self.assertEqual(len(disj2c), 1) + cons = disj2c[0] + assertExpressionsEqual( + self, + cons.expr, + (3 - m.x[1]) ** 2 + + (3 - m.x[2]) ** 2 + + (3 - m.x[3]) ** 2 + + (3 - m.x[4]) ** 2 + - 99.0 * (1 - m.disjunct_block.disj2.binary_indicator_var) + <= 1.0, ) # inner disjunction constraints innerd1c = bigm.get_transformed_constraints( m.disjunct_block.disj2.disjunction_disjuncts[0].constraint[1] ) - self.check_first_disjunct_constraint( - innerd1c, - m.x, - m.disjunct_block.disj2.disjunction_disjuncts[0].binary_indicator_var, + self.assertEqual(len(innerd1c), 1) + cons = innerd1c[0] + assertExpressionsEqual( + self, + cons.expr, + m.x[1] ** 2 + + m.x[2] ** 2 + + m.x[3] ** 2 + + m.x[4] ** 2 + - 143.0 + * ( + 1 + - m.disjunct_block.disj2.disjunction_disjuncts[0].binary_indicator_var + + 1 + - m.disjunct_block.disj2.binary_indicator_var + ) + <= 1.0, ) innerd2c = bigm.get_transformed_constraints( m.disjunct_block.disj2.disjunction_disjuncts[1].constraint[1] ) - self.check_second_disjunct_constraint( - innerd2c, - m.x, - m.disjunct_block.disj2.disjunction_disjuncts[1].binary_indicator_var, + self.assertEqual(len(innerd2c), 1) + cons = innerd2c[0] + assertExpressionsEqual( + self, + cons.expr, + (3 - m.x[1]) ** 2 + + (3 - m.x[2]) ** 2 + + (3 - m.x[3]) ** 2 + + (3 - m.x[4]) ** 2 + - 99.0 + * ( + 1 + - m.disjunct_block.disj2.disjunction_disjuncts[1].binary_indicator_var + + 1 + - m.disjunct_block.disj2.binary_indicator_var + ) + <= 1.0, ) def test_hierarchical_badly_ordered_targets(self): @@ -2214,10 +2244,54 @@ def test_decl_order_opposite_instantiation_order(self): # the same check to make sure everything is transformed correctly. self.check_hierarchical_nested_model(m, bigm) + @unittest.skipUnless(gurobi_available, "Gurobi is not available") + def test_do_not_assume_nested_indicators_local(self): + ct.check_do_not_assume_nested_indicators_local(self, 'gdp.bigm') + + @unittest.skipUnless(gurobi_available, "Gurobi is not available") + def test_constraints_not_enforced_when_an_ancestor_indicator_is_False(self): + m = ConcreteModel() + m.x = Var(bounds=(0, 30)) + + m.left = Disjunct() + m.left.left = Disjunct() + m.left.left.c = Constraint(expr=m.x >= 10) + m.left.right = Disjunct() + m.left.right.c = Constraint(expr=m.x >= 9) + m.left.disjunction = Disjunction(expr=[m.left.left, m.left.right]) + m.right = Disjunct() + m.right.left = Disjunct() + m.right.left.c = Constraint(expr=m.x >= 11) + m.right.right = Disjunct() + m.right.right.c = Constraint(expr=m.x >= 8) + m.right.disjunction = Disjunction(expr=[m.right.left, m.right.right]) + m.disjunction = Disjunction(expr=[m.left, m.right]) + + m.equiv_left = LogicalConstraint( + expr=m.left.left.indicator_var.equivalent_to(m.right.left.indicator_var) + ) + m.equiv_right = LogicalConstraint( + expr=m.left.right.indicator_var.equivalent_to(m.right.right.indicator_var) + ) + + m.obj = Objective(expr=m.x) + + TransformationFactory('gdp.bigm').apply_to(m) + results = SolverFactory('gurobi').solve(m) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.optimal + ) + self.assertTrue(value(m.right.indicator_var)) + self.assertFalse(value(m.left.indicator_var)) + self.assertTrue(value(m.right.right.indicator_var)) + self.assertFalse(value(m.right.left.indicator_var)) + self.assertTrue(value(m.left.right.indicator_var)) + self.assertAlmostEqual(value(m.x), 8) + class IndexedDisjunction(unittest.TestCase): # this tests that if the targets are a subset of the - # _DisjunctDatas in an IndexedDisjunction that the xor constraint + # DisjunctDatas in an IndexedDisjunction that the xor constraint # created on the parent block will still be indexed as expected. def test_xor_constraint(self): ct.check_indexed_xor_constraints_with_targets(self, 'bigm') @@ -2282,18 +2356,12 @@ def check_all_but_evil1_b_anotherblock_constraint_transformed(self, m): self.assertEqual(len(evil1), 2) self.assertIs(evil1[0].parent_block(), disjBlock[1]) self.assertIs(evil1[1].parent_block(), disjBlock[1]) - out = StringIO() - with LoggingIntercept(out, 'pyomo.gdp', logging.ERROR): - self.assertRaisesRegex( - KeyError, - r".*.evil\[1\].b.anotherblock.c", - bigm.get_transformed_constraints, - m.evil[1].b.anotherblock.c, - ) - self.assertRegex( - out.getvalue(), - r".*Constraint 'evil\[1\].b.anotherblock.c' has not been transformed.", - ) + with self.assertRaisesRegex( + GDP_Error, + r"Constraint 'evil\[1\].b.anotherblock.c' has not been transformed.", + ): + bigm.get_transformed_constraints(m.evil[1].b.anotherblock.c) + evil1 = bigm.get_transformed_constraints(m.evil[1].bb[1].c) self.assertEqual(len(evil1), 2) self.assertIs(evil1[0].parent_block(), disjBlock[1]) diff --git a/pyomo/gdp/tests/test_binary_multiplication.py b/pyomo/gdp/tests/test_binary_multiplication.py new file mode 100644 index 00000000000..ae2c44b899e --- /dev/null +++ b/pyomo/gdp/tests/test_binary_multiplication.py @@ -0,0 +1,312 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest + +from pyomo.environ import ( + TransformationFactory, + Block, + Constraint, + ConcreteModel, + Var, + Any, + SolverFactory, +) +from pyomo.gdp import Disjunct, Disjunction +from pyomo.core.expr.compare import assertExpressionsEqual +from pyomo.repn import generate_standard_repn +from pyomo.core.expr.compare import assertExpressionsEqual + +import pyomo.core.expr as EXPR +import pyomo.gdp.tests.models as models +import pyomo.gdp.tests.common_tests as ct + +import random + +gurobi_available = ( + SolverFactory('gurobi').available(exception_flag=False) + and SolverFactory('gurobi').license_is_valid() +) + + +class CommonTests: + def diff_apply_to_and_create_using(self, model): + ct.diff_apply_to_and_create_using(self, model, 'gdp.binary_multiplication') + + +class TwoTermDisj(unittest.TestCase, CommonTests): + def setUp(self): + # set seed so we can test name collisions predictably + random.seed(666) + + def test_new_block_created(self): + m = models.makeTwoTermDisj() + TransformationFactory('gdp.binary_multiplication').apply_to(m) + + # we have a transformation block + transBlock = m.component("_pyomo_gdp_binary_multiplication_reformulation") + self.assertIsInstance(transBlock, Block) + + disjBlock = transBlock.component("relaxedDisjuncts") + self.assertIsInstance(disjBlock, Block) + self.assertEqual(len(disjBlock), 2) + # it has the disjuncts on it + self.assertIs(m.d[0].transformation_block, disjBlock[0]) + self.assertIs(m.d[1].transformation_block, disjBlock[1]) + + def test_disjunction_deactivated(self): + ct.check_disjunction_deactivated(self, 'binary_multiplication') + + def test_disjunctDatas_deactivated(self): + ct.check_disjunctDatas_deactivated(self, 'binary_multiplication') + + def test_do_not_transform_twice_if_disjunction_reactivated(self): + ct.check_do_not_transform_twice_if_disjunction_reactivated( + self, 'binary_multiplication' + ) + + def test_xor_constraint_mapping(self): + ct.check_xor_constraint_mapping(self, 'binary_multiplication') + + def test_xor_constraint_mapping_two_disjunctions(self): + ct.check_xor_constraint_mapping_two_disjunctions(self, 'binary_multiplication') + + def test_disjunct_mapping(self): + ct.check_disjunct_mapping(self, 'binary_multiplication') + + def test_disjunct_and_constraint_maps(self): + """Tests the actual data structures used to store the maps.""" + m = models.makeTwoTermDisj() + binary_multiplication = TransformationFactory('gdp.binary_multiplication') + binary_multiplication.apply_to(m) + disjBlock = m._pyomo_gdp_binary_multiplication_reformulation.relaxedDisjuncts + oldblock = m.component("d") + + # we are counting on the fact that the disjuncts get relaxed in the + # same order every time. + for i in [0, 1]: + self.assertIs(oldblock[i].transformation_block, disjBlock[i]) + self.assertIs( + binary_multiplication.get_src_disjunct(disjBlock[i]), oldblock[i] + ) + + # check constraint dict has right mapping + c1_list = binary_multiplication.get_transformed_constraints(oldblock[1].c1) + # this is an equality + self.assertEqual(len(c1_list), 1) + self.assertIs(c1_list[0].parent_block(), disjBlock[1]) + self.assertIs( + binary_multiplication.get_src_constraint(c1_list[0]), oldblock[1].c1 + ) + + c2_list = binary_multiplication.get_transformed_constraints(oldblock[1].c2) + # just ub + self.assertEqual(len(c2_list), 1) + self.assertIs(c2_list[0].parent_block(), disjBlock[1]) + self.assertIs( + binary_multiplication.get_src_constraint(c2_list[0]), oldblock[1].c2 + ) + + c_list = binary_multiplication.get_transformed_constraints(oldblock[0].c) + # just lb + self.assertEqual(len(c_list), 1) + self.assertIs(c_list[0].parent_block(), disjBlock[0]) + self.assertIs( + binary_multiplication.get_src_constraint(c_list[0]), oldblock[0].c + ) + + def test_new_block_nameCollision(self): + ct.check_transformation_block_name_collision(self, 'binary_multiplication') + + def test_indicator_vars(self): + ct.check_indicator_vars(self, 'binary_multiplication') + + def test_xor_constraints(self): + ct.check_xor_constraint(self, 'binary_multiplication') + + def test_or_constraints(self): + m = models.makeTwoTermDisj() + m.disjunction.xor = False + TransformationFactory('gdp.binary_multiplication').apply_to(m) + + # check or constraint is an or (upper bound is None) + orcons = m._pyomo_gdp_binary_multiplication_reformulation.component( + "disjunction_xor" + ) + self.assertIsInstance(orcons, Constraint) + assertExpressionsEqual( + self, + orcons.body, + EXPR.LinearExpression( + [m.d[0].binary_indicator_var, m.d[1].binary_indicator_var] + ), + ) + self.assertEqual(orcons.lower, 1) + self.assertIsNone(orcons.upper) + + def test_deactivated_constraints(self): + ct.check_deactivated_constraints(self, 'binary_multiplication') + + def test_transformed_constraints(self): + m = models.makeTwoTermDisj() + binary_multiplication = TransformationFactory('gdp.binary_multiplication') + binary_multiplication.apply_to(m) + self.check_transformed_constraints(m, binary_multiplication, -3, 2, 7, 2) + + def test_do_not_transform_userDeactivated_disjuncts(self): + ct.check_user_deactivated_disjuncts(self, 'binary_multiplication') + + def test_improperly_deactivated_disjuncts(self): + ct.check_improperly_deactivated_disjuncts(self, 'binary_multiplication') + + def test_do_not_transform_userDeactivated_IndexedDisjunction(self): + ct.check_do_not_transform_userDeactivated_indexedDisjunction( + self, 'binary_multiplication' + ) + + def check_transformed_constraints( + self, model, binary_multiplication, cons1lb, cons2lb, cons2ub, cons3ub + ): + disjBlock = ( + model._pyomo_gdp_binary_multiplication_reformulation.relaxedDisjuncts + ) + + # first constraint + c = binary_multiplication.get_transformed_constraints(model.d[0].c) + self.assertEqual(len(c), 1) + c_lb = c[0] + self.assertTrue(c[0].active) + ind_var = model.d[0].indicator_var + assertExpressionsEqual( + self, c[0].body, (model.a - model.d[0].c.lower) * ind_var + ) + self.assertEqual(c[0].lower, 0) + self.assertIsNone(c[0].upper) + + # second constraint + c = binary_multiplication.get_transformed_constraints(model.d[1].c1) + self.assertEqual(len(c), 1) + c_eq = c[0] + self.assertTrue(c[0].active) + ind_var = model.d[1].indicator_var + assertExpressionsEqual(self, c[0].body, model.a * ind_var) + self.assertEqual(c[0].lower, 0) + self.assertEqual(c[0].upper, 0) + + # third constraint + c = binary_multiplication.get_transformed_constraints(model.d[1].c2) + self.assertEqual(len(c), 1) + c_ub = c[0] + self.assertTrue(c_ub.active) + assertExpressionsEqual( + self, c_ub.body, (model.x - model.d[1].c2.upper) * ind_var + ) + self.assertIsNone(c_ub.lower) + self.assertEqual(c_ub.upper, 0) + + def test_create_using(self): + m = models.makeTwoTermDisj() + self.diff_apply_to_and_create_using(m) + + def test_indexed_constraints_in_disjunct(self): + m = ConcreteModel() + m.I = [1, 2, 3] + m.x = Var(m.I, bounds=(0, 10)) + + def c_rule(b, i): + m = b.model() + return m.x[i] >= i + + def d_rule(d, j): + m = d.model() + d.c = Constraint(m.I[:j], rule=c_rule) + + m.d = Disjunct(m.I, rule=d_rule) + m.disjunction = Disjunction(expr=[m.d[i] for i in m.I]) + + TransformationFactory('gdp.binary_multiplication').apply_to(m) + transBlock = m._pyomo_gdp_binary_multiplication_reformulation + + # 2 blocks: the original Disjunct and the transformation block + self.assertEqual(len(list(m.component_objects(Block, descend_into=False))), 1) + self.assertEqual(len(list(m.component_objects(Disjunct))), 1) + + # Each relaxed disjunct should have 1 var (the reference to the + # indicator var), and i "d[i].c" Constraints + for i in [1, 2, 3]: + relaxed = transBlock.relaxedDisjuncts[i - 1] + self.assertEqual(len(list(relaxed.component_objects(Var))), 1) + self.assertEqual(len(list(relaxed.component_data_objects(Var))), 1) + self.assertEqual(len(list(relaxed.component_objects(Constraint))), 1) + self.assertEqual(len(list(relaxed.component_data_objects(Constraint))), i) + + def test_virtual_indexed_constraints_in_disjunct(self): + m = ConcreteModel() + m.I = [1, 2, 3] + m.x = Var(m.I, bounds=(0, 10)) + + def d_rule(d, j): + m = d.model() + d.c = Constraint(Any) + for k in range(j): + d.c[k + 1] = m.x[k + 1] >= k + 1 + + m.d = Disjunct(m.I, rule=d_rule) + m.disjunction = Disjunction(expr=[m.d[i] for i in m.I]) + + TransformationFactory('gdp.binary_multiplication').apply_to(m) + transBlock = m._pyomo_gdp_binary_multiplication_reformulation + + # 2 blocks: the original Disjunct and the transformation block + self.assertEqual(len(list(m.component_objects(Block, descend_into=False))), 1) + self.assertEqual(len(list(m.component_objects(Disjunct))), 1) + + # Each relaxed disjunct should have 1 var (the reference to the + # indicator var), and i "d[i].c" Constraints + for i in [1, 2, 3]: + relaxed = transBlock.relaxedDisjuncts[i - 1] + self.assertEqual(len(list(relaxed.component_objects(Var))), 1) + self.assertEqual(len(list(relaxed.component_data_objects(Var))), 1) + self.assertEqual(len(list(relaxed.component_objects(Constraint))), 1) + self.assertEqual(len(list(relaxed.component_data_objects(Constraint))), i) + + def test_local_var(self): + m = models.localVar() + binary_multiplication = TransformationFactory('gdp.binary_multiplication') + binary_multiplication.apply_to(m) + + # we just need to make sure that constraint was transformed correctly, + # which just means that the M values were correct. + transformedC = binary_multiplication.get_transformed_constraints(m.disj2.cons) + self.assertEqual(len(transformedC), 1) + eq = transformedC[0] + repn = generate_standard_repn(eq.body) + self.assertIsNone(repn.nonlinear_expr) + self.assertEqual(len(repn.linear_coefs), 1) + self.assertEqual(len(repn.quadratic_coefs), 2) + ct.check_linear_coef(self, repn, m.disj2.indicator_var, -3) + ct.check_quadratic_coef(self, repn, m.x, m.disj2.indicator_var, 1) + ct.check_quadratic_coef(self, repn, m.disj2.y, m.disj2.indicator_var, 1) + self.assertEqual(repn.constant, 0) + self.assertEqual(eq.lb, 0) + self.assertEqual(eq.ub, 0) + + +class TestNestedGDP(unittest.TestCase): + @unittest.skipUnless(gurobi_available, "Gurobi is not available") + def test_do_not_assume_nested_indicators_local(self): + ct.check_do_not_assume_nested_indicators_local( + self, 'gdp.binary_multiplication' + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/pyomo/gdp/tests/test_bound_pretransformation.py b/pyomo/gdp/tests/test_bound_pretransformation.py index 30ce76b7e31..68db64ce93b 100644 --- a/pyomo/gdp/tests/test_bound_pretransformation.py +++ b/pyomo/gdp/tests/test_bound_pretransformation.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/tests/test_cuttingplane.py b/pyomo/gdp/tests/test_cuttingplane.py index 827eac9aa6a..153e236942d 100644 --- a/pyomo/gdp/tests/test_cuttingplane.py +++ b/pyomo/gdp/tests/test_cuttingplane.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/tests/test_disjunct.py b/pyomo/gdp/tests/test_disjunct.py index ccf5b8c2d6c..f93ac31fb0f 100644 --- a/pyomo/gdp/tests/test_disjunct.py +++ b/pyomo/gdp/tests/test_disjunct.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -108,6 +108,31 @@ def _gen(): self.assertEqual(len(disjuncts[0].parent_component().name), 11) self.assertEqual(disjuncts[0].name, "f_disjuncts[0]") + def test_construct_invalid_component(self): + m = ConcreteModel() + m.d = Disjunct([1, 2]) + with self.assertRaisesRegex( + ValueError, + "Unexpected term for Disjunction 'dd'.\n " + "Expected a Disjunct object, relational expression, or iterable of\n" + " relational expressions but got 'IndexedDisjunct'", + ): + m.dd = Disjunction(expr=[m.d]) + with self.assertRaisesRegex( + ValueError, + "Unexpected term for Disjunction 'ee'.\n " + "Expected a Disjunct object, relational expression, or iterable of\n" + " relational expressions but got 'str' in 'list'", + ): + m.ee = Disjunction(expr=[['a']]) + with self.assertRaisesRegex( + ValueError, + "Unexpected term for Disjunction 'ff'.\n " + "Expected a Disjunct object, relational expression, or iterable of\n" + " relational expressions but got 'str'", + ): + m.ff = Disjunction(expr=['a']) + class TestDisjunct(unittest.TestCase): def test_deactivate(self): @@ -607,19 +632,13 @@ def test_cast_to_binary(self): out = StringIO() with LoggingIntercept(out): e = m.iv + 1 - assertExpressionsEqual( - self, e, EXPR.LinearExpression([EXPR.MonomialTermExpression((1, m.biv)), 1]) - ) + assertExpressionsEqual(self, e, EXPR.LinearExpression([m.biv, 1])) self.assertIn(deprecation_msg, out.getvalue()) out = StringIO() with LoggingIntercept(out): e = m.iv - 1 - assertExpressionsEqual( - self, - e, - EXPR.LinearExpression([EXPR.MonomialTermExpression((1, m.biv)), -1]), - ) + assertExpressionsEqual(self, e, EXPR.LinearExpression([m.biv, -1])) self.assertIn(deprecation_msg, out.getvalue()) out = StringIO() @@ -640,9 +659,7 @@ def test_cast_to_binary(self): out = StringIO() with LoggingIntercept(out): e = 1 + m.iv - assertExpressionsEqual( - self, e, EXPR.LinearExpression([1, EXPR.MonomialTermExpression((1, m.biv))]) - ) + assertExpressionsEqual(self, e, EXPR.LinearExpression([1, m.biv])) self.assertIn(deprecation_msg, out.getvalue()) out = StringIO() @@ -674,20 +691,14 @@ def test_cast_to_binary(self): with LoggingIntercept(out): a = m.iv a += 1 - assertExpressionsEqual( - self, a, EXPR.LinearExpression([EXPR.MonomialTermExpression((1, m.biv)), 1]) - ) + assertExpressionsEqual(self, a, EXPR.LinearExpression([m.biv, 1])) self.assertIn(deprecation_msg, out.getvalue()) out = StringIO() with LoggingIntercept(out): a = m.iv a -= 1 - assertExpressionsEqual( - self, - a, - EXPR.LinearExpression([EXPR.MonomialTermExpression((1, m.biv)), -1]), - ) + assertExpressionsEqual(self, a, EXPR.LinearExpression([m.biv, -1])) self.assertIn(deprecation_msg, out.getvalue()) out = StringIO() diff --git a/pyomo/gdp/tests/test_fix_disjuncts.py b/pyomo/gdp/tests/test_fix_disjuncts.py index 1b741f7a840..6f01e096e9d 100644 --- a/pyomo/gdp/tests/test_fix_disjuncts.py +++ b/pyomo/gdp/tests/test_fix_disjuncts.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/tests/test_gdp.py b/pyomo/gdp/tests/test_gdp.py index 5c810dcce18..b22a60bc04a 100644 --- a/pyomo/gdp/tests/test_gdp.py +++ b/pyomo/gdp/tests/test_gdp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/tests/test_gdp_reclassification_error.py b/pyomo/gdp/tests/test_gdp_reclassification_error.py index a65ccac2d8f..556dc44eead 100644 --- a/pyomo/gdp/tests/test_gdp_reclassification_error.py +++ b/pyomo/gdp/tests/test_gdp_reclassification_error.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index 09f65765fe6..07876a9d213 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -41,6 +41,7 @@ import pyomo.core.expr as EXPR from pyomo.core.base import constraint from pyomo.repn import generate_standard_repn +from pyomo.repn.linear import LinearRepnVisitor from pyomo.gdp import Disjunct, Disjunction, GDP_Error import pyomo.gdp.tests.models as models @@ -51,6 +52,7 @@ import os from os.path import abspath, dirname, join + currdir = dirname(abspath(__file__)) from filecmp import cmp @@ -402,19 +404,13 @@ def test_error_for_or(self): self.assertRaisesRegex( GDP_Error, "Cannot do hull reformulation for Disjunction " - "'disjunction' with OR constraint. Must be an XOR!*", + "'disjunction' with OR constraint. Must be an XOR!*", TransformationFactory('gdp.hull').apply_to, m, ) def check_disaggregation_constraint(self, cons, var, disvar1, disvar2): - repn = generate_standard_repn(cons.body) - self.assertEqual(cons.lower, 0) - self.assertEqual(cons.upper, 0) - self.assertEqual(len(repn.linear_vars), 3) - ct.check_linear_coef(self, repn, var, 1) - ct.check_linear_coef(self, repn, disvar1, -1) - ct.check_linear_coef(self, repn, disvar2, -1) + assertExpressionsEqual(self, cons.expr, var == disvar1 + disvar2) def test_disaggregation_constraint(self): m = models.makeTwoTermDisj_Nonlinear() @@ -426,8 +422,8 @@ def test_disaggregation_constraint(self): self.check_disaggregation_constraint( hull.get_disaggregation_constraint(m.w, m.disjunction), m.w, - disjBlock[1].disaggregatedVars.w, transBlock._disaggregatedVars[1], + disjBlock[1].disaggregatedVars.w, ) self.check_disaggregation_constraint( hull.get_disaggregation_constraint(m.x, m.disjunction), @@ -438,8 +434,8 @@ def test_disaggregation_constraint(self): self.check_disaggregation_constraint( hull.get_disaggregation_constraint(m.y, m.disjunction), m.y, - disjBlock[0].disaggregatedVars.y, transBlock._disaggregatedVars[0], + disjBlock[0].disaggregatedVars.y, ) def test_xor_constraint_mapping(self): @@ -510,10 +506,10 @@ def test_disaggregatedVar_mappings(self): for i in [0, 1]: mappings = ComponentMap() mappings[m.x] = disjBlock[i].disaggregatedVars.x - if i == 1: # this disjunct as x, w, and no y + if i == 1: # this disjunct has x, w, and no y mappings[m.w] = disjBlock[i].disaggregatedVars.w mappings[m.y] = transBlock._disaggregatedVars[0] - elif i == 0: # this disjunct as x, y, and no w + elif i == 0: # this disjunct has x, y, and no w mappings[m.y] = disjBlock[i].disaggregatedVars.y mappings[m.w] = transBlock._disaggregatedVars[1] @@ -668,17 +664,38 @@ def test_global_vars_local_to_a_disjunction_disaggregated(self): self.assertIs(hull.get_src_var(x), m.disj1.x) # there is a spare x on disjunction1's block - x2 = m.disjunction1.algebraic_constraint.parent_block()._disaggregatedVars[2] + x2 = m.disjunction1.algebraic_constraint.parent_block()._disaggregatedVars[0] self.assertIs(hull.get_disaggregated_var(m.disj1.x, m.disj2), x2) self.assertIs(hull.get_src_var(x2), m.disj1.x) + # What really matters is that the above matches this: + agg_cons = hull.get_disaggregation_constraint(m.disj1.x, m.disjunction1) + assertExpressionsEqual( + self, + agg_cons.expr, + m.disj1.x == x2 + hull.get_disaggregated_var(m.disj1.x, m.disj1), + ) # and both a spare x and y on disjunction2's block - x2 = m.disjunction2.algebraic_constraint.parent_block()._disaggregatedVars[0] - y1 = m.disjunction2.algebraic_constraint.parent_block()._disaggregatedVars[1] + x2 = m.disjunction2.algebraic_constraint.parent_block()._disaggregatedVars[1] + y1 = m.disjunction2.algebraic_constraint.parent_block()._disaggregatedVars[2] self.assertIs(hull.get_disaggregated_var(m.disj1.x, m.disj4), x2) self.assertIs(hull.get_src_var(x2), m.disj1.x) self.assertIs(hull.get_disaggregated_var(m.disj1.y, m.disj3), y1) self.assertIs(hull.get_src_var(y1), m.disj1.y) + # and again what really matters is that these align with the + # disaggregation constraints: + agg_cons = hull.get_disaggregation_constraint(m.disj1.x, m.disjunction2) + assertExpressionsEqual( + self, + agg_cons.expr, + m.disj1.x == x2 + hull.get_disaggregated_var(m.disj1.x, m.disj3), + ) + agg_cons = hull.get_disaggregation_constraint(m.disj1.y, m.disjunction2) + assertExpressionsEqual( + self, + agg_cons.expr, + m.disj1.y == y1 + hull.get_disaggregated_var(m.disj1.y, m.disj4), + ) def check_name_collision_disaggregated_vars(self, m, disj): hull = TransformationFactory('gdp.hull') @@ -880,18 +897,10 @@ def test_do_not_transform_deactivated_constraintDatas(self): hull = TransformationFactory('gdp.hull') hull.apply_to(m) # can't ask for simpledisj1.c[1]: it wasn't transformed - log = StringIO() - with LoggingIntercept(log, 'pyomo.gdp', logging.ERROR): - self.assertRaisesRegex( - KeyError, - r".*b.simpledisj1.c\[1\]", - hull.get_transformed_constraints, - m.b.simpledisj1.c[1], - ) - self.assertRegex( - log.getvalue(), - r".*Constraint 'b.simpledisj1.c\[1\]' has not been transformed.", - ) + with self.assertRaisesRegex( + GDP_Error, r"Constraint 'b.simpledisj1.c\[1\]' has not been transformed." + ): + hull.get_transformed_constraints(m.b.simpledisj1.c[1]) # this fixes a[2] to 0, so we should get the disggregated var transformed = hull.get_transformed_constraints(m.b.simpledisj1.c[2]) @@ -1101,7 +1110,7 @@ def check_trans_block_disjunctions_of_disjunct_datas(self, m): self.assertEqual(len(transBlock1.relaxedDisjuncts), 4) hull = TransformationFactory('gdp.hull') - firstTerm2 = transBlock1.relaxedDisjuncts[0] + firstTerm2 = transBlock1.relaxedDisjuncts[2] self.assertIs(firstTerm2, m.firstTerm[2].transformation_block) self.assertIsInstance(firstTerm2.disaggregatedVars.component("x"), Var) constraints = hull.get_transformed_constraints(m.firstTerm[2].cons) @@ -1115,7 +1124,7 @@ def check_trans_block_disjunctions_of_disjunct_datas(self, m): self.assertIs(cons.parent_block(), firstTerm2) self.assertEqual(len(cons), 2) - secondTerm2 = transBlock1.relaxedDisjuncts[1] + secondTerm2 = transBlock1.relaxedDisjuncts[3] self.assertIs(secondTerm2, m.secondTerm[2].transformation_block) self.assertIsInstance(secondTerm2.disaggregatedVars.component("x"), Var) constraints = hull.get_transformed_constraints(m.secondTerm[2].cons) @@ -1129,7 +1138,7 @@ def check_trans_block_disjunctions_of_disjunct_datas(self, m): self.assertIs(cons.parent_block(), secondTerm2) self.assertEqual(len(cons), 2) - firstTerm1 = transBlock1.relaxedDisjuncts[2] + firstTerm1 = transBlock1.relaxedDisjuncts[0] self.assertIs(firstTerm1, m.firstTerm[1].transformation_block) self.assertIsInstance(firstTerm1.disaggregatedVars.component("x"), Var) self.assertTrue(firstTerm1.disaggregatedVars.x.is_fixed()) @@ -1147,7 +1156,7 @@ def check_trans_block_disjunctions_of_disjunct_datas(self, m): self.assertIs(cons.parent_block(), firstTerm1) self.assertEqual(len(cons), 2) - secondTerm1 = transBlock1.relaxedDisjuncts[3] + secondTerm1 = transBlock1.relaxedDisjuncts[1] self.assertIs(secondTerm1, m.secondTerm[1].transformation_block) self.assertIsInstance(secondTerm1.disaggregatedVars.component("x"), Var) constraints = hull.get_transformed_constraints(m.secondTerm[1].cons) @@ -1243,12 +1252,10 @@ def check_second_iteration(self, model): orig = model.component("_pyomo_gdp_hull_reformulation") self.assertIsInstance( - model.disjunctionList[1].algebraic_constraint, - constraint._GeneralConstraintData, + model.disjunctionList[1].algebraic_constraint, constraint.ConstraintData ) self.assertIsInstance( - model.disjunctionList[0].algebraic_constraint, - constraint._GeneralConstraintData, + model.disjunctionList[0].algebraic_constraint, constraint.ConstraintData ) self.assertFalse(model.disjunctionList[1].active) self.assertFalse(model.disjunctionList[0].active) @@ -1375,9 +1382,8 @@ def test_deactivated_disjunct_leaves_nested_disjuncts_active(self): ct.check_deactivated_disjunct_leaves_nested_disjunct_active(self, 'hull') def test_mappings_between_disjunctions_and_xors(self): - # This test is nearly identical to the one in bigm, but because of - # different transformation orders, the name conflict gets resolved in - # the opposite way. + # Tests that the XOR constraints are put on the parent block of the + # disjunction, and checks the mappings. m = models.makeNestedDisjunctions() transform = TransformationFactory('gdp.hull') transform.apply_to(m) @@ -1386,8 +1392,17 @@ def test_mappings_between_disjunctions_and_xors(self): disjunctionPairs = [ (m.disjunction, transBlock.disjunction_xor), - (m.disjunct[1].innerdisjunction[0], transBlock.innerdisjunction_xor[0]), - (m.simpledisjunct.innerdisjunction, transBlock.innerdisjunction_xor_4), + ( + m.disjunct[1].innerdisjunction[0], + m.disjunct[1] + .innerdisjunction[0] + .algebraic_constraint.parent_block() + .innerdisjunction_xor[0], + ), + ( + m.simpledisjunct.innerdisjunction, + m.simpledisjunct.innerdisjunction.algebraic_constraint.parent_block().innerdisjunction_xor, + ), ] # check disjunction mappings @@ -1427,16 +1442,16 @@ def test_relaxation_feasibility(self): solver = SolverFactory(linear_solvers[0]) cases = [ - (1, 1, 1, 1, None), - (0, 0, 0, 0, None), - (1, 0, 0, 0, None), - (0, 1, 0, 0, 1.1), - (0, 0, 1, 0, None), - (0, 0, 0, 1, None), - (1, 1, 0, 0, None), - (1, 0, 1, 0, 1.2), - (1, 0, 0, 1, 1.3), - (1, 0, 1, 1, None), + (True, True, True, True, None), + (False, False, False, False, None), + (True, False, False, False, None), + (False, True, False, False, 1.1), + (False, False, True, False, None), + (False, False, False, True, None), + (True, True, False, False, None), + (True, False, True, False, 1.2), + (True, False, False, True, 1.3), + (True, False, True, True, None), ] for case in cases: m.d1.indicator_var.fix(case[0]) @@ -1468,16 +1483,16 @@ def test_relaxation_feasibility_transform_inner_first(self): solver = SolverFactory(linear_solvers[0]) cases = [ - (1, 1, 1, 1, None), - (0, 0, 0, 0, None), - (1, 0, 0, 0, None), - (0, 1, 0, 0, 1.1), - (0, 0, 1, 0, None), - (0, 0, 0, 1, None), - (1, 1, 0, 0, None), - (1, 0, 1, 0, 1.2), - (1, 0, 0, 1, 1.3), - (1, 0, 1, 1, None), + (True, True, True, True, None), + (False, False, False, False, None), + (True, False, False, False, None), + (False, True, False, False, 1.1), + (False, False, True, False, None), + (False, False, False, True, None), + (True, True, False, False, None), + (True, False, True, False, 1.2), + (True, False, False, True, 1.3), + (True, False, True, True, None), ] for case in cases: m.d1.indicator_var.fix(case[0]) @@ -1550,149 +1565,190 @@ def check_transformed_constraint(self, cons, dis, lb, ind_var): def test_transformed_model_nestedDisjuncts(self): # This test tests *everything* for a simple nested disjunction case. m = models.makeNestedDisjunctions_NestedDisjuncts() + m.LocalVars = Suffix(direction=Suffix.LOCAL) + m.LocalVars[m.d1] = [ + m.d1.binary_indicator_var, + m.d1.d3.binary_indicator_var, + m.d1.d4.binary_indicator_var, + ] hull = TransformationFactory('gdp.hull') hull.apply_to(m) + self.check_transformed_model_nestedDisjuncts( + m, m.d1.d3.binary_indicator_var, m.d1.d4.binary_indicator_var + ) + + # Last, check that there aren't things we weren't expecting + all_cons = list( + m.component_data_objects(Constraint, active=True, descend_into=Block) + ) + # 2 disaggregation constraints for x 0,3 + # + 6 bounds constraints for x 6,8,9,13,14,16 + # + 2 bounds constraints for inner indicator vars 11, 12 + # + 2 exactly-one constraints 1,4 + # + 4 transformed constraints 2,5,7,15 + self.assertEqual(len(all_cons), 16) + + def check_transformed_model_nestedDisjuncts(self, m, d3, d4): + # This function checks all of the 16 constraint expressions from + # transforming models.makeNestedDisjunction_NestedDisjuncts when + # declaring the inner indicator vars (d3 and d4) as local. Note that it + # also is a correct test for the case where the inner indicator vars are + # *not* declared as local, but not a complete one, since there are + # additional constraints in that case (see + # check_transformation_blocks_nestedDisjunctions in common_tests.py). + hull = TransformationFactory('gdp.hull') transBlock = m._pyomo_gdp_hull_reformulation self.assertTrue(transBlock.active) - # outer xor should be on this block + # check outer xor xor = transBlock.disj_xor self.assertIsInstance(xor, Constraint) - self.assertTrue(xor.active) - self.assertEqual(xor.lower, 1) - self.assertEqual(xor.upper, 1) - repn = generate_standard_repn(xor.body) - self.assertTrue(repn.is_linear()) - self.assertEqual(repn.constant, 0) - ct.check_linear_coef(self, repn, m.d1.binary_indicator_var, 1) - ct.check_linear_coef(self, repn, m.d2.binary_indicator_var, 1) + ct.check_obj_in_active_tree(self, xor) + assertExpressionsEqual( + self, xor.expr, m.d1.binary_indicator_var + m.d2.binary_indicator_var == 1 + ) self.assertIs(xor, m.disj.algebraic_constraint) self.assertIs(m.disj, hull.get_src_disjunction(xor)) - # inner xor should be on this block + # check inner xor xor = m.d1.disj2.algebraic_constraint - self.assertIs(xor.parent_block(), transBlock) - self.assertIsInstance(xor, Constraint) - self.assertTrue(xor.active) - self.assertEqual(xor.lower, 0) - self.assertEqual(xor.upper, 0) - repn = generate_standard_repn(xor.body) - self.assertTrue(repn.is_linear()) - self.assertEqual(repn.constant, 0) - ct.check_linear_coef(self, repn, m.d1.d3.binary_indicator_var, 1) - ct.check_linear_coef(self, repn, m.d1.d4.binary_indicator_var, 1) - ct.check_linear_coef(self, repn, m.d1.binary_indicator_var, -1) self.assertIs(m.d1.disj2, hull.get_src_disjunction(xor)) - - # so should both disaggregation constraints - dis = transBlock.disaggregationConstraints - self.assertIsInstance(dis, Constraint) - self.assertTrue(dis.active) - self.assertEqual(len(dis), 2) - self.check_outer_disaggregation_constraint(dis[0], m.x, m.d1, m.d2) - self.assertIs(hull.get_disaggregation_constraint(m.x, m.disj), dis[0]) - self.check_outer_disaggregation_constraint( - dis[1], m.x, m.d1.d3, m.d1.d4, rhs=hull.get_disaggregated_var(m.x, m.d1) - ) - self.assertIs(hull.get_disaggregation_constraint(m.x, m.d1.disj2), dis[1]) - - # we should have four disjunct transformation blocks - disjBlocks = transBlock.relaxedDisjuncts - self.assertTrue(disjBlocks.active) - self.assertEqual(len(disjBlocks), 4) - - ## d1's transformation block - - disj1 = disjBlocks[0] - self.assertTrue(disj1.active) - self.assertIs(disj1, m.d1.transformation_block) - self.assertIs(m.d1, hull.get_src_disjunct(disj1)) - # check the disaggregated x is here - self.assertIsInstance(disj1.disaggregatedVars.x, Var) - self.assertEqual(disj1.disaggregatedVars.x.lb, 0) - self.assertEqual(disj1.disaggregatedVars.x.ub, 2) - self.assertIs(disj1.disaggregatedVars.x, hull.get_disaggregated_var(m.x, m.d1)) - self.assertIs(m.x, hull.get_src_var(disj1.disaggregatedVars.x)) - # check the bounds constraints - self.check_bounds_constraint_ub( - disj1.x_bounds, 2, disj1.disaggregatedVars.x, m.d1.indicator_var - ) - # transformed constraint x >= 1 - cons = hull.get_transformed_constraints(m.d1.c) - self.check_transformed_constraint( - cons, disj1.disaggregatedVars.x, 1, m.d1.indicator_var + xor = hull.get_transformed_constraints(xor) + self.assertEqual(len(xor), 1) + xor = xor[0] + ct.check_obj_in_active_tree(self, xor) + xor_expr = self.simplify_cons(xor) + assertExpressionsEqual( + self, xor_expr, d3 + d4 - m.d1.binary_indicator_var == 0.0 ) - ## d2's transformation block + # check disaggregation constraints + x_d3 = hull.get_disaggregated_var(m.x, m.d1.d3) + x_d4 = hull.get_disaggregated_var(m.x, m.d1.d4) + x_d1 = hull.get_disaggregated_var(m.x, m.d1) + x_d2 = hull.get_disaggregated_var(m.x, m.d2) + for x in [x_d1, x_d2, x_d3, x_d4]: + self.assertEqual(x.lb, 0) + self.assertEqual(x.ub, 2) + # Inner disjunction + cons = hull.get_disaggregation_constraint(m.x, m.d1.disj2) + ct.check_obj_in_active_tree(self, cons) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual(self, cons_expr, x_d1 - x_d3 - x_d4 == 0.0) + # Outer disjunction + cons = hull.get_disaggregation_constraint(m.x, m.disj) + ct.check_obj_in_active_tree(self, cons) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual(self, cons_expr, m.x - x_d1 - x_d2 == 0.0) - disj2 = disjBlocks[1] - self.assertTrue(disj2.active) - self.assertIs(disj2, m.d2.transformation_block) - self.assertIs(m.d2, hull.get_src_disjunct(disj2)) - # disaggregated var - x2 = disj2.disaggregatedVars.x - self.assertIsInstance(x2, Var) - self.assertEqual(x2.lb, 0) - self.assertEqual(x2.ub, 2) - self.assertIs(hull.get_disaggregated_var(m.x, m.d2), x2) - self.assertIs(hull.get_src_var(x2), m.x) - # bounds constraint - x_bounds = disj2.x_bounds - self.check_bounds_constraint_ub(x_bounds, 2, x2, m.d2.binary_indicator_var) - # transformed constraint x >= 1.1 - cons = hull.get_transformed_constraints(m.d2.c) - self.check_transformed_constraint(cons, x2, 1.1, m.d2.binary_indicator_var) - - ## d1.d3's transformation block - - disj3 = disjBlocks[2] - self.assertTrue(disj3.active) - self.assertIs(disj3, m.d1.d3.transformation_block) - self.assertIs(m.d1.d3, hull.get_src_disjunct(disj3)) - # disaggregated var - x3 = disj3.disaggregatedVars.x - self.assertIsInstance(x3, Var) - self.assertEqual(x3.lb, 0) - self.assertEqual(x3.ub, 2) - self.assertIs(hull.get_disaggregated_var(m.x, m.d1.d3), x3) - self.assertIs(hull.get_src_var(x3), m.x) - # bounds constraints - self.check_bounds_constraint_ub( - disj3.x_bounds, 2, x3, m.d1.d3.binary_indicator_var - ) - # transformed x >= 1.2 + ## Transformed constraints cons = hull.get_transformed_constraints(m.d1.d3.c) - self.check_transformed_constraint(cons, x3, 1.2, m.d1.d3.binary_indicator_var) - - ## d1.d4's transformation block - - disj4 = disjBlocks[3] - self.assertTrue(disj4.active) - self.assertIs(disj4, m.d1.d4.transformation_block) - self.assertIs(m.d1.d4, hull.get_src_disjunct(disj4)) - # disaggregated var - x4 = disj4.disaggregatedVars.x - self.assertIsInstance(x4, Var) - self.assertEqual(x4.lb, 0) - self.assertEqual(x4.ub, 2) - self.assertIs(hull.get_disaggregated_var(m.x, m.d1.d4), x4) - self.assertIs(hull.get_src_var(x4), m.x) - # bounds constraints - self.check_bounds_constraint_ub( - disj4.x_bounds, 2, x4, m.d1.d4.binary_indicator_var - ) - # transformed x >= 1.3 + self.assertEqual(len(cons), 1) + cons = cons[0] + ct.check_obj_in_active_tree(self, cons) + cons_expr = self.simplify_leq_cons(cons) + assertExpressionsEqual(self, cons_expr, 1.2 * d3 - x_d3 <= 0.0) + cons = hull.get_transformed_constraints(m.d1.d4.c) - self.check_transformed_constraint(cons, x4, 1.3, m.d1.d4.binary_indicator_var) + self.assertEqual(len(cons), 1) + cons = cons[0] + ct.check_obj_in_active_tree(self, cons) + cons_expr = self.simplify_leq_cons(cons) + assertExpressionsEqual(self, cons_expr, 1.3 * d4 - x_d4 <= 0.0) + + cons = hull.get_transformed_constraints(m.d1.c) + self.assertEqual(len(cons), 1) + cons = cons[0] + ct.check_obj_in_active_tree(self, cons) + cons_expr = self.simplify_leq_cons(cons) + assertExpressionsEqual( + self, cons_expr, 1.0 * m.d1.binary_indicator_var - x_d1 <= 0.0 + ) + + cons = hull.get_transformed_constraints(m.d2.c) + self.assertEqual(len(cons), 1) + cons = cons[0] + ct.check_obj_in_active_tree(self, cons) + cons_expr = self.simplify_leq_cons(cons) + assertExpressionsEqual( + self, cons_expr, 1.1 * m.d2.binary_indicator_var - x_d2 <= 0.0 + ) + + ## Bounds constraints + cons = hull.get_var_bounds_constraint(x_d1) + # the lb is trivial in this case, so we just have 1 + self.assertEqual(len(cons), 1) + ct.check_obj_in_active_tree(self, cons['ub']) + cons_expr = self.simplify_leq_cons(cons['ub']) + assertExpressionsEqual( + self, cons_expr, x_d1 - 2 * m.d1.binary_indicator_var <= 0.0 + ) + cons = hull.get_var_bounds_constraint(x_d2) + # the lb is trivial in this case, so we just have 1 + self.assertEqual(len(cons), 1) + ct.check_obj_in_active_tree(self, cons['ub']) + cons_expr = self.simplify_leq_cons(cons['ub']) + assertExpressionsEqual( + self, cons_expr, x_d2 - 2 * m.d2.binary_indicator_var <= 0.0 + ) + cons = hull.get_var_bounds_constraint(x_d3, m.d1.d3) + # the lb is trivial in this case, so we just have 1 + self.assertEqual(len(cons), 1) + # And we know it has actually been transformed again, so get that one + cons = hull.get_transformed_constraints(cons['ub']) + self.assertEqual(len(cons), 1) + ub = cons[0] + ct.check_obj_in_active_tree(self, ub) + cons_expr = self.simplify_leq_cons(ub) + assertExpressionsEqual(self, cons_expr, x_d3 - 2 * d3 <= 0.0) + cons = hull.get_var_bounds_constraint(x_d4, m.d1.d4) + # the lb is trivial in this case, so we just have 1 + self.assertEqual(len(cons), 1) + # And we know it has actually been transformed again, so get that one + cons = hull.get_transformed_constraints(cons['ub']) + self.assertEqual(len(cons), 1) + ub = cons[0] + ct.check_obj_in_active_tree(self, ub) + cons_expr = self.simplify_leq_cons(ub) + assertExpressionsEqual(self, cons_expr, x_d4 - 2 * d4 <= 0.0) + cons = hull.get_var_bounds_constraint(x_d3, m.d1) + self.assertEqual(len(cons), 1) + ub = cons['ub'] + ct.check_obj_in_active_tree(self, ub) + cons_expr = self.simplify_leq_cons(ub) + assertExpressionsEqual( + self, cons_expr, x_d3 - 2 * m.d1.binary_indicator_var <= 0.0 + ) + cons = hull.get_var_bounds_constraint(x_d4, m.d1) + self.assertEqual(len(cons), 1) + ub = cons['ub'] + ct.check_obj_in_active_tree(self, ub) + cons_expr = self.simplify_leq_cons(ub) + assertExpressionsEqual( + self, cons_expr, x_d4 - 2 * m.d1.binary_indicator_var <= 0.0 + ) + + # Bounds constraints for local vars + cons = hull.get_var_bounds_constraint(d3) + ct.check_obj_in_active_tree(self, cons['ub']) + assertExpressionsEqual(self, cons['ub'].expr, d3 <= m.d1.binary_indicator_var) + cons = hull.get_var_bounds_constraint(d4) + ct.check_obj_in_active_tree(self, cons['ub']) + assertExpressionsEqual(self, cons['ub'].expr, d4 <= m.d1.binary_indicator_var) @unittest.skipIf(not linear_solvers, "No linear solver available") def test_solve_nested_model(self): # This is really a test that our variable references have all been moved # up correctly. m = models.makeNestedDisjunctions_NestedDisjuncts() - + m.LocalVars = Suffix(direction=Suffix.LOCAL) + m.LocalVars[m.d1] = [ + m.d1.binary_indicator_var, + m.d1.d3.binary_indicator_var, + m.d1.d4.binary_indicator_var, + ] hull = TransformationFactory('gdp.hull') m_hull = hull.create_using(m) @@ -1722,10 +1778,10 @@ def test_disaggregated_vars_are_set_to_0_correctly(self): hull.apply_to(m) # this should be a feasible integer solution - m.d1.indicator_var.fix(0) - m.d2.indicator_var.fix(1) - m.d3.indicator_var.fix(0) - m.d4.indicator_var.fix(0) + m.d1.indicator_var.fix(False) + m.d2.indicator_var.fix(True) + m.d3.indicator_var.fix(False) + m.d4.indicator_var.fix(False) results = SolverFactory(linear_solvers[0]).solve(m) self.assertEqual( @@ -1739,10 +1795,10 @@ def test_disaggregated_vars_are_set_to_0_correctly(self): self.assertEqual(value(hull.get_disaggregated_var(m.x, m.d4)), 0) # and what if one of the inner disjuncts is true? - m.d1.indicator_var.fix(1) - m.d2.indicator_var.fix(0) - m.d3.indicator_var.fix(1) - m.d4.indicator_var.fix(0) + m.d1.indicator_var.fix(True) + m.d2.indicator_var.fix(False) + m.d3.indicator_var.fix(True) + m.d4.indicator_var.fix(False) results = SolverFactory(linear_solvers[0]).solve(m) self.assertEqual( @@ -1787,6 +1843,11 @@ def d_r(e): e.c1 = Constraint(expr=e.lambdas[1] + e.lambdas[2] == 1) e.c2 = Constraint(expr=m.x == 2 * e.lambdas[1] + 3 * e.lambdas[2]) + d.LocalVars = Suffix(direction=Suffix.LOCAL) + d.LocalVars[d] = [ + d.d_l.indicator_var.get_associated_binary(), + d.d_r.indicator_var.get_associated_binary(), + ] d.inner_disj = Disjunction(expr=[d.d_l, d.d_r]) m.disj = Disjunction(expr=[m.d_l, m.d_r]) @@ -1809,28 +1870,159 @@ def d_r(e): cons = hull.get_transformed_constraints(d.c1) self.assertEqual(len(cons), 1) convex_combo = cons[0] + convex_combo_expr = self.simplify_cons(convex_combo) assertExpressionsEqual( self, - convex_combo.expr, - lambda1 + lambda2 - (1 - d.indicator_var.get_associated_binary()) * 0.0 - == d.indicator_var.get_associated_binary(), + convex_combo_expr, + lambda1 + lambda2 - d.indicator_var.get_associated_binary() == 0.0, ) cons = hull.get_transformed_constraints(d.c2) self.assertEqual(len(cons), 1) get_x = cons[0] + get_x_expr = self.simplify_cons(get_x) assertExpressionsEqual( - self, - get_x.expr, - x - - (2 * lambda1 + 3 * lambda2) - - (1 - d.indicator_var.get_associated_binary()) * 0.0 - == 0.0 * d.indicator_var.get_associated_binary(), + self, get_x_expr, x - 2 * lambda1 - 3 * lambda2 == 0.0 ) cons = hull.get_disaggregation_constraint(m.x, m.disj) assertExpressionsEqual(self, cons.expr, m.x == x1 + x2) cons = hull.get_disaggregation_constraint(m.x, m.d_r.inner_disj) - assertExpressionsEqual(self, cons.expr, x2 == x3 + x4) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual(self, cons_expr, x2 - x3 - x4 == 0.0) + + def test_nested_with_var_that_does_not_appear_in_every_disjunct(self): + m = ConcreteModel() + m.x = Var(bounds=(0, 10)) + m.y = Var(bounds=(-4, 5)) + m.parent1 = Disjunct() + m.parent2 = Disjunct() + m.parent2.c = Constraint(expr=m.x == 0) + m.parent_disjunction = Disjunction(expr=[m.parent1, m.parent2]) + m.child1 = Disjunct() + m.child1.c = Constraint(expr=m.x <= 8) + m.child2 = Disjunct() + m.child2.c = Constraint(expr=m.x + m.y <= 3) + m.child3 = Disjunct() + m.child3.c = Constraint(expr=m.x <= 7) + m.parent1.disjunction = Disjunction(expr=[m.child1, m.child2, m.child3]) + + hull = TransformationFactory('gdp.hull') + hull.apply_to(m) + + y_c2 = hull.get_disaggregated_var(m.y, m.child2) + self.assertEqual(y_c2.bounds, (-4, 5)) + other_y = hull.get_disaggregated_var(m.y, m.child1) + self.assertEqual(other_y.bounds, (-4, 5)) + other_other_y = hull.get_disaggregated_var(m.y, m.child3) + self.assertIs(other_y, other_other_y) + y_p1 = hull.get_disaggregated_var(m.y, m.parent1) + self.assertEqual(y_p1.bounds, (-4, 5)) + y_p2 = hull.get_disaggregated_var(m.y, m.parent2) + self.assertEqual(y_p2.bounds, (-4, 5)) + + y_cons = hull.get_disaggregation_constraint(m.y, m.parent1.disjunction) + # check that the disaggregated ys in the nested just sum to the original + y_cons_expr = self.simplify_cons(y_cons) + assertExpressionsEqual(self, y_cons_expr, y_p1 - other_y - y_c2 == 0.0) + y_cons = hull.get_disaggregation_constraint(m.y, m.parent_disjunction) + y_cons_expr = self.simplify_cons(y_cons) + assertExpressionsEqual(self, y_cons_expr, m.y - y_p2 - y_p1 == 0.0) + + x_c1 = hull.get_disaggregated_var(m.x, m.child1) + x_c2 = hull.get_disaggregated_var(m.x, m.child2) + x_c3 = hull.get_disaggregated_var(m.x, m.child3) + x_p1 = hull.get_disaggregated_var(m.x, m.parent1) + x_p2 = hull.get_disaggregated_var(m.x, m.parent2) + x_cons_parent = hull.get_disaggregation_constraint(m.x, m.parent_disjunction) + assertExpressionsEqual(self, x_cons_parent.expr, m.x == x_p1 + x_p2) + x_cons_child = hull.get_disaggregation_constraint(m.x, m.parent1.disjunction) + x_cons_child_expr = self.simplify_cons(x_cons_child) + assertExpressionsEqual( + self, x_cons_child_expr, x_p1 - x_c1 - x_c2 - x_c3 == 0.0 + ) + + def simplify_cons(self, cons): + visitor = LinearRepnVisitor({}, {}, {}, None) + lb = cons.lower + ub = cons.upper + self.assertEqual(cons.lb, cons.ub) + repn = visitor.walk_expression(cons.body) + self.assertIsNone(repn.nonlinear) + return repn.to_expression(visitor) == lb + + def simplify_leq_cons(self, cons): + visitor = LinearRepnVisitor({}, {}, {}, None) + self.assertIsNone(cons.lower) + ub = cons.upper + repn = visitor.walk_expression(cons.body) + self.assertIsNone(repn.nonlinear) + return repn.to_expression(visitor) <= ub + + def test_nested_with_var_that_skips_a_level(self): + m = ConcreteModel() + + m.x = Var(bounds=(-2, 9)) + m.y = Var(bounds=(-3, 8)) + + m.y1 = Disjunct() + m.y1.c1 = Constraint(expr=m.x >= 4) + m.y1.z1 = Disjunct() + m.y1.z1.c1 = Constraint(expr=m.y == 2) + m.y1.z1.w1 = Disjunct() + m.y1.z1.w1.c1 = Constraint(expr=m.x == 3) + m.y1.z1.w2 = Disjunct() + m.y1.z1.w2.c1 = Constraint(expr=m.x >= 1) + m.y1.z1.disjunction = Disjunction(expr=[m.y1.z1.w1, m.y1.z1.w2]) + m.y1.z2 = Disjunct() + m.y1.z2.c1 = Constraint(expr=m.y == 1) + m.y1.disjunction = Disjunction(expr=[m.y1.z1, m.y1.z2]) + m.y2 = Disjunct() + m.y2.c1 = Constraint(expr=m.x == 4) + m.disjunction = Disjunction(expr=[m.y1, m.y2]) + + hull = TransformationFactory('gdp.hull') + hull.apply_to(m) + + x_y1 = hull.get_disaggregated_var(m.x, m.y1) + x_y2 = hull.get_disaggregated_var(m.x, m.y2) + x_z1 = hull.get_disaggregated_var(m.x, m.y1.z1) + x_z2 = hull.get_disaggregated_var(m.x, m.y1.z2) + x_w1 = hull.get_disaggregated_var(m.x, m.y1.z1.w1) + x_w2 = hull.get_disaggregated_var(m.x, m.y1.z1.w2) + + y_z1 = hull.get_disaggregated_var(m.y, m.y1.z1) + y_z2 = hull.get_disaggregated_var(m.y, m.y1.z2) + y_y1 = hull.get_disaggregated_var(m.y, m.y1) + y_y2 = hull.get_disaggregated_var(m.y, m.y2) + + cons = hull.get_disaggregation_constraint(m.x, m.y1.z1.disjunction) + self.assertTrue(cons.active) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual(self, cons_expr, x_z1 - x_w1 - x_w2 == 0.0) + cons = hull.get_disaggregation_constraint(m.x, m.y1.disjunction) + self.assertTrue(cons.active) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual(self, cons_expr, x_y1 - x_z2 - x_z1 == 0.0) + cons = hull.get_disaggregation_constraint(m.x, m.disjunction) + self.assertTrue(cons.active) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual(self, cons_expr, m.x - x_y1 - x_y2 == 0.0) + cons = hull.get_disaggregation_constraint( + m.y, m.y1.z1.disjunction, raise_exception=False + ) + self.assertIsNone(cons) + cons = hull.get_disaggregation_constraint(m.y, m.y1.disjunction) + self.assertTrue(cons.active) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual(self, cons_expr, y_y1 - y_z1 - y_z2 == 0.0) + cons = hull.get_disaggregation_constraint(m.y, m.disjunction) + self.assertTrue(cons.active) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual(self, cons_expr, m.y - y_y2 - y_y1 == 0.0) + + @unittest.skipUnless(gurobi_available, "Gurobi is not available") + def test_do_not_assume_nested_indicators_local(self): + ct.check_do_not_assume_nested_indicators_local(self, 'gdp.hull') class TestSpecialCases(unittest.TestCase): @@ -2100,27 +2292,19 @@ def test_mapping_method_errors(self): hull = TransformationFactory('gdp.hull') hull.apply_to(m) - log = StringIO() - with LoggingIntercept(log, 'pyomo.gdp.hull', logging.ERROR): - self.assertRaisesRegex( - AttributeError, - "'NoneType' object has no attribute 'parent_block'", - hull.get_var_bounds_constraint, - m.w, - ) - self.assertRegex( - log.getvalue(), + with self.assertRaisesRegex( + GDP_Error, ".*Either 'w' is not a disaggregated variable, " "or the disjunction that disaggregates it has " "not been properly transformed.", - ) + ): + hull.get_var_bounds_constraint(m.w) log = StringIO() with LoggingIntercept(log, 'pyomo.gdp.hull', logging.ERROR): self.assertRaisesRegex( KeyError, - r".*_pyomo_gdp_hull_reformulation.relaxedDisjuncts\[1\]." - r"disaggregatedVars.w", + r".*disjunction", hull.get_disaggregation_constraint, m.d[1].transformation_block.disaggregatedVars.w, m.disjunction, @@ -2134,36 +2318,22 @@ def test_mapping_method_errors(self): r"Disjunction 'disjunction'", ) - log = StringIO() - with LoggingIntercept(log, 'pyomo.gdp.hull', logging.ERROR): - self.assertRaisesRegex( - AttributeError, - "'NoneType' object has no attribute 'parent_block'", - hull.get_src_var, - m.w, - ) - self.assertRegex( - log.getvalue(), ".*'w' does not appear to be a disaggregated variable" - ) + with self.assertRaisesRegex( + GDP_Error, ".*'w' does not appear to be a disaggregated variable" + ): + hull.get_src_var(m.w) - log = StringIO() - with LoggingIntercept(log, 'pyomo.gdp.hull', logging.ERROR): - self.assertRaisesRegex( - KeyError, - r".*_pyomo_gdp_hull_reformulation.relaxedDisjuncts\[1\]." - r"disaggregatedVars.w", - hull.get_disaggregated_var, - m.d[1].transformation_block.disaggregatedVars.w, - m.d[1], - ) - self.assertRegex( - log.getvalue(), + with self.assertRaisesRegex( + GDP_Error, r".*It does not appear " r"'_pyomo_gdp_hull_reformulation." r"relaxedDisjuncts\[1\].disaggregatedVars.w' " r"is a variable that appears in disjunct " r"'d\[1\]'", - ) + ): + hull.get_disaggregated_var( + m.d[1].transformation_block.disaggregatedVars.w, m.d[1] + ) m.random_disjunction = Disjunction(expr=[m.w == 2, m.w >= 7]) self.assertRaisesRegex( @@ -2398,12 +2568,12 @@ def OneCentroidPerPt(m, i): TransformationFactory('gdp.hull').apply_to(m) # fix an optimal solution - m.AssignPoint[1, 1].indicator_var.fix(1) - m.AssignPoint[1, 2].indicator_var.fix(0) - m.AssignPoint[2, 1].indicator_var.fix(0) - m.AssignPoint[2, 2].indicator_var.fix(1) - m.AssignPoint[3, 1].indicator_var.fix(1) - m.AssignPoint[3, 2].indicator_var.fix(0) + m.AssignPoint[1, 1].indicator_var.fix(True) + m.AssignPoint[1, 2].indicator_var.fix(False) + m.AssignPoint[2, 1].indicator_var.fix(False) + m.AssignPoint[2, 2].indicator_var.fix(True) + m.AssignPoint[3, 1].indicator_var.fix(True) + m.AssignPoint[3, 2].indicator_var.fix(False) m.cluster_center[1].fix(0.3059) m.cluster_center[2].fix(0.8043) diff --git a/pyomo/gdp/tests/test_mbigm.py b/pyomo/gdp/tests/test_mbigm.py index f067e1da5af..9e82b1010f9 100644 --- a/pyomo/gdp/tests/test_mbigm.py +++ b/pyomo/gdp/tests/test_mbigm.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -10,6 +10,7 @@ # ___________________________________________________________________________ from io import StringIO +import logging from os.path import join, normpath import pickle @@ -50,7 +51,25 @@ exdir = normpath(join(PYOMO_ROOT_DIR, 'examples', 'gdp')) -class LinearModelDecisionTreeExample(unittest.TestCase): +class CommonTests(unittest.TestCase): + def check_pretty_bound_constraints(self, cons, var, bounds, lb): + self.assertEqual(value(cons.upper), 0) + self.assertIsNone(cons.lower) + repn = generate_standard_repn(cons.body) + self.assertTrue(repn.is_linear()) + self.assertEqual(len(repn.linear_vars), len(bounds) + 1) + self.assertEqual(repn.constant, 0) + if lb: + check_linear_coef(self, repn, var, -1) + for disj, bnd in bounds.items(): + check_linear_coef(self, repn, disj.binary_indicator_var, bnd) + else: + check_linear_coef(self, repn, var, 1) + for disj, bnd in bounds.items(): + check_linear_coef(self, repn, disj.binary_indicator_var, -bnd) + + +class LinearModelDecisionTreeExample(CommonTests): def make_model(self): m = ConcreteModel() m.x1 = Var(bounds=(-10, 10)) @@ -333,6 +352,43 @@ def test_transformed_constraints_correct_Ms_specified(self): self.check_all_untightened_bounds_constraints(m, mbm) self.check_linear_func_constraints(m, mbm) + def test_local_var_suffix_ignored(self): + m = self.make_model() + m.y = Var(bounds=(2, 5)) + m.d1.another_thing = Constraint(expr=m.y == 3) + m.d1.LocalVars = Suffix(direction=Suffix.LOCAL) + m.d1.LocalVars[m.d1] = m.y + + mbigm = TransformationFactory('gdp.mbigm') + mbigm.apply_to( + m, reduce_bound_constraints=True, only_mbigm_bound_constraints=True + ) + + cons = mbigm.get_transformed_constraints(m.d1.x1_bounds) + self.check_pretty_bound_constraints( + cons[0], m.x1, {m.d1: 0.5, m.d2: 0.65, m.d3: 2}, lb=True + ) + self.check_pretty_bound_constraints( + cons[1], m.x1, {m.d1: 2, m.d2: 3, m.d3: 10}, lb=False + ) + + cons = mbigm.get_transformed_constraints(m.d1.x2_bounds) + self.check_pretty_bound_constraints( + cons[0], m.x2, {m.d1: 0.75, m.d2: 3, m.d3: 0.55}, lb=True + ) + self.check_pretty_bound_constraints( + cons[1], m.x2, {m.d1: 3, m.d2: 10, m.d3: 1}, lb=False + ) + + cons = mbigm.get_transformed_constraints(m.d1.another_thing) + self.assertEqual(len(cons), 2) + self.check_pretty_bound_constraints( + cons[0], m.y, {m.d1: 3, m.d2: 2, m.d3: 2}, lb=True + ) + self.check_pretty_bound_constraints( + cons[1], m.y, {m.d1: 3, m.d2: 5, m.d3: 5}, lb=False + ) + def test_pickle_transformed_model(self): m = self.make_model() TransformationFactory('gdp.mbigm').apply_to(m, bigM=self.get_Ms(m)) @@ -381,22 +437,6 @@ def test_algebraic_constraints(self): check_linear_coef(self, repn, m.d3.binary_indicator_var, 1) check_obj_in_active_tree(self, xor) - def check_pretty_bound_constraints(self, cons, var, bounds, lb): - self.assertEqual(value(cons.upper), 0) - self.assertIsNone(cons.lower) - repn = generate_standard_repn(cons.body) - self.assertTrue(repn.is_linear()) - self.assertEqual(len(repn.linear_vars), len(bounds) + 1) - self.assertEqual(repn.constant, 0) - if lb: - check_linear_coef(self, repn, var, -1) - for disj, bnd in bounds.items(): - check_linear_coef(self, repn, disj.binary_indicator_var, bnd) - else: - check_linear_coef(self, repn, var, 1) - for disj, bnd in bounds.items(): - check_linear_coef(self, repn, disj.binary_indicator_var, -bnd) - def test_bounds_constraints_correct(self): m = self.make_model() @@ -877,6 +917,25 @@ def test_declare_disjuncts_in_disjunction_rule(self): check_nested_disjuncts_in_flat_gdp(self, 'bigm') +class IndexedDisjunctiveConstraints(CommonTests): + def test_empty_constraint_container_on_Disjunct(self): + m = ConcreteModel() + m.d = Disjunct() + m.e = Disjunct() + m.d.c = Constraint(['s', 'i', 'l', 'L', 'y']) + m.x = Var(bounds=(2, 3)) + m.e.c = Constraint(expr=m.x == 2.7) + m.disjunction = Disjunction(expr=[m.d, m.e]) + + mbm = TransformationFactory('gdp.mbigm') + mbm.apply_to(m) + + cons = mbm.get_transformed_constraints(m.e.c) + self.assertEqual(len(cons), 2) + self.check_pretty_bound_constraints(cons[0], m.x, {m.d: 2, m.e: 2.7}, lb=True) + self.check_pretty_bound_constraints(cons[1], m.x, {m.d: 3, m.e: 2.7}, lb=False) + + @unittest.skipUnless(gurobi_available, "Gurobi is not available") class IndexedDisjunction(unittest.TestCase): def test_two_term_indexed_disjunction(self): @@ -930,3 +989,118 @@ def test_two_term_indexed_disjunction(self): self.assertEqual(len(cons_again), 2) self.assertIs(cons_again[0], cons[0]) self.assertIs(cons_again[1], cons[1]) + + +class EdgeCases(unittest.TestCase): + def make_infeasible_disjunct_model(self): + m = ConcreteModel() + m.x = Var(bounds=(1, 12)) + m.y = Var(bounds=(19, 22)) + m.disjunction = Disjunction( + expr=[ + [m.x >= 3 + m.y, m.y == 19.75], # infeasible given bounds + [m.y >= 21 + m.x], # unique solution + [m.x == m.y - 9], # x in interval [10, 12] + ] + ) + return m + + @unittest.skipUnless(gurobi_available, "Gurobi is not available") + def test_calculate_Ms_infeasible_Disjunct(self): + m = self.make_infeasible_disjunct_model() + out = StringIO() + mbm = TransformationFactory('gdp.mbigm') + with LoggingIntercept(out, 'pyomo.gdp.mbigm', logging.DEBUG): + mbm.apply_to(m, reduce_bound_constraints=False) + + # We mentioned the infeasibility at the DEBUG level + self.assertIn( + r"Disjunct 'disjunction_disjuncts[0]' is infeasible, deactivating", + out.getvalue().strip(), + ) + + # We just fixed the infeasible by to False + self.assertFalse(m.disjunction.disjuncts[0].active) + self.assertTrue(m.disjunction.disjuncts[0].indicator_var.fixed) + self.assertFalse(value(m.disjunction.disjuncts[0].indicator_var)) + + # the remaining constraints are transformed correctly. + cons = mbm.get_transformed_constraints(m.disjunction.disjuncts[1].constraint[1]) + self.assertEqual(len(cons), 1) + assertExpressionsEqual( + self, + cons[0].expr, + 21 + m.x - m.y + <= 0 * m.disjunction.disjuncts[0].binary_indicator_var + + 12.0 * m.disjunction.disjuncts[2].binary_indicator_var, + ) + + cons = mbm.get_transformed_constraints(m.disjunction.disjuncts[2].constraint[1]) + self.assertEqual(len(cons), 2) + print(cons[0].expr) + print(cons[1].expr) + assertExpressionsEqual( + self, + cons[0].expr, + 0.0 * m.disjunction_disjuncts[0].binary_indicator_var + - 12.0 * m.disjunction_disjuncts[1].binary_indicator_var + <= m.x - (m.y - 9), + ) + assertExpressionsEqual( + self, + cons[1].expr, + m.x - (m.y - 9) + <= 0.0 * m.disjunction_disjuncts[0].binary_indicator_var + - 12.0 * m.disjunction_disjuncts[1].binary_indicator_var, + ) + + @unittest.skipUnless( + SolverFactory('ipopt').available(exception_flag=False), "Ipopt is not available" + ) + def test_calculate_Ms_infeasible_Disjunct_local_solver(self): + m = self.make_infeasible_disjunct_model() + with self.assertRaisesRegex( + GDP_Error, + r"Unsuccessful solve to calculate M value to " + r"relax constraint 'disjunction_disjuncts\[1\].constraint\[1\]' " + r"on Disjunct 'disjunction_disjuncts\[1\]' when " + r"Disjunct 'disjunction_disjuncts\[0\]' is selected.", + ): + TransformationFactory('gdp.mbigm').apply_to( + m, solver=SolverFactory('ipopt'), reduce_bound_constraints=False + ) + + @unittest.skipUnless(gurobi_available, "Gurobi is not available") + def test_politely_ignore_BigM_Suffix(self): + m = self.make_infeasible_disjunct_model() + m.disjunction.disjuncts[0].deactivate() + m.disjunction.disjuncts[1].BigM = Suffix(direction=Suffix.LOCAL) + out = StringIO() + with LoggingIntercept(out, 'pyomo.gdp.mbigm', logging.DEBUG): + TransformationFactory('gdp.mbigm').apply_to( + m, reduce_bound_constraints=False + ) + warnings = out.getvalue() + self.assertIn( + r"Found active 'BigM' Suffix on 'disjunction_disjuncts[1]'. " + r"The multiple bigM transformation does not currently " + r"support specifying M's with Suffixes and is ignoring " + r"this Suffix.", + warnings, + ) + + @unittest.skipUnless(gurobi_available, "Gurobi is not available") + def test_complain_for_unrecognized_Suffix(self): + m = self.make_infeasible_disjunct_model() + m.disjunction.disjuncts[0].deactivate() + m.disjunction.disjuncts[1].HiThere = Suffix(direction=Suffix.LOCAL) + out = StringIO() + with self.assertRaisesRegex( + GDP_Error, + r"Found active Suffix 'disjunction_disjuncts\[1\].HiThere' " + r"on Disjunct 'disjunction_disjuncts\[1\]'. The multiple bigM " + r"transformation does not support this Suffix.", + ): + TransformationFactory('gdp.mbigm').apply_to( + m, reduce_bound_constraints=False + ) diff --git a/pyomo/gdp/tests/test_partition_disjuncts.py b/pyomo/gdp/tests/test_partition_disjuncts.py index 56faaa9b8f5..dc5ae9f70ce 100644 --- a/pyomo/gdp/tests/test_partition_disjuncts.py +++ b/pyomo/gdp/tests/test_partition_disjuncts.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -227,14 +227,18 @@ def check_transformation_block( aux22ub, partitions, ): - ( - b, - disj1, - disj2, - aux_vars1, - aux_vars2, - ) = self.check_transformation_block_structure( - m, aux11lb, aux11ub, aux12lb, aux12ub, aux21lb, aux21ub, aux22lb, aux22ub + (b, disj1, disj2, aux_vars1, aux_vars2) = ( + self.check_transformation_block_structure( + m, + aux11lb, + aux11ub, + aux12lb, + aux12ub, + aux21lb, + aux21ub, + aux22lb, + aux22ub, + ) ) self.check_disjunct_constraints(disj1, disj2, aux_vars1, aux_vars2) @@ -351,14 +355,12 @@ def check_transformation_block_nested_disjunction( else: block_prefix = disjunction_block + "." disjunction_parent = m.component(disjunction_block) - ( - inner_b, - inner_disj1, - inner_disj2, - ) = self.check_transformation_block_disjuncts_and_constraints( - disj2, - disjunction_parent.disj2.disjunction, - "%sdisj2.disjunction" % block_prefix, + (inner_b, inner_disj1, inner_disj2) = ( + self.check_transformation_block_disjuncts_and_constraints( + disj2, + disjunction_parent.disj2.disjunction, + "%sdisj2.disjunction" % block_prefix, + ) ) # Has it's own indicator var, the aux vars, and the Reference to the @@ -753,13 +755,9 @@ def test_assume_fixed_vars_permanent(self): # This actually changes the structure of the model because fixed vars # move to the constants. I think this is fair, and we should allow it # because it will allow for a tighter relaxation. - ( - b, - disj1, - disj2, - aux_vars1, - aux_vars2, - ) = self.check_transformation_block_structure(m, 0, 36, 0, 72, -9, 16, -18, 32) + (b, disj1, disj2, aux_vars1, aux_vars2) = ( + self.check_transformation_block_structure(m, 0, 36, 0, 72, -9, 16, -18, 32) + ) # check disjunct constraints self.check_disjunct_constraints(disj1, disj2, aux_vars1, aux_vars2) @@ -1702,9 +1700,7 @@ def test_transformation_block_fbbt_bounds(self): compute_bounds_method=compute_fbbt_bounds, ) - self.check_transformation_block( - m, 0, (2 * 6**4) ** 0.25, 0, (2 * 5**4) ** 0.25 - ) + self.check_transformation_block(m, 0, (2 * 6**4) ** 0.25, 0, (2 * 5**4) ** 0.25) def test_invalid_partition_error(self): m = models.makeNonQuadraticNonlinearGDP() diff --git a/pyomo/gdp/tests/test_reclassify.py b/pyomo/gdp/tests/test_reclassify.py index fd98f8f0954..223c28c5c7a 100644 --- a/pyomo/gdp/tests/test_reclassify.py +++ b/pyomo/gdp/tests/test_reclassify.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: UTF-8 -*- """Tests disjunct reclassifier transformation.""" import pyomo.common.unittest as unittest diff --git a/pyomo/gdp/tests/test_transform_current_disjunctive_state.py b/pyomo/gdp/tests/test_transform_current_disjunctive_state.py index d257c3db8fb..54d80c910e5 100644 --- a/pyomo/gdp/tests/test_transform_current_disjunctive_state.py +++ b/pyomo/gdp/tests/test_transform_current_disjunctive_state.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/tests/test_util.py b/pyomo/gdp/tests/test_util.py index 90c63717b81..fa8e953f9f7 100644 --- a/pyomo/gdp/tests/test_util.py +++ b/pyomo/gdp/tests/test_util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -13,7 +13,7 @@ from pyomo.core import ConcreteModel, Var, Expression, Block, RangeSet, Any import pyomo.core.expr as EXPR -from pyomo.core.base.expression import _ExpressionData +from pyomo.core.base.expression import NamedExpressionData from pyomo.gdp.util import ( clone_without_expression_components, is_child_of, @@ -40,7 +40,7 @@ def test_clone_without_expression_components(self): test = clone_without_expression_components(base, {}) self.assertIsNot(base, test) self.assertEqual(base(), test()) - self.assertIsInstance(base, _ExpressionData) + self.assertIsInstance(base, NamedExpressionData) self.assertIsInstance(test, EXPR.SumExpression) test = clone_without_expression_components(base, {id(m.x): m.y}) self.assertEqual(3**2 + 3 - 1, test()) @@ -51,7 +51,7 @@ def test_clone_without_expression_components(self): self.assertEqual(base(), test()) self.assertIsInstance(base, EXPR.SumExpression) self.assertIsInstance(test, EXPR.SumExpression) - self.assertIsInstance(base.arg(0), _ExpressionData) + self.assertIsInstance(base.arg(0), NamedExpressionData) self.assertIsInstance(test.arg(0), EXPR.SumExpression) test = clone_without_expression_components(base, {id(m.x): m.y}) self.assertEqual(3**2 + 3 - 1 + 3, test()) diff --git a/pyomo/gdp/transformed_disjunct.py b/pyomo/gdp/transformed_disjunct.py index 400f77a31f6..287d5ed1652 100644 --- a/pyomo/gdp/transformed_disjunct.py +++ b/pyomo/gdp/transformed_disjunct.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -10,11 +10,11 @@ # ___________________________________________________________________________ from pyomo.common.autoslots import AutoSlots -from pyomo.core.base.block import _BlockData, IndexedBlock +from pyomo.core.base.block import BlockData, IndexedBlock from pyomo.core.base.global_set import UnindexedComponent_index, UnindexedComponent_set -class _TransformedDisjunctData(_BlockData): +class _TransformedDisjunctData(BlockData): __slots__ = ('_src_disjunct',) __autoslot_mappers__ = {'_src_disjunct': AutoSlots.weakref_mapper} @@ -23,7 +23,7 @@ def src_disjunct(self): return None if self._src_disjunct is None else self._src_disjunct() def __init__(self, component): - _BlockData.__init__(self, component) + BlockData.__init__(self, component) # pointer to the Disjunct whose transformation block this is. self._src_disjunct = None diff --git a/pyomo/gdp/util.py b/pyomo/gdp/util.py index b460a3d691c..2fe8e9e1dee 100644 --- a/pyomo/gdp/util.py +++ b/pyomo/gdp/util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -10,10 +10,9 @@ # ___________________________________________________________________________ from pyomo.gdp import GDP_Error, Disjunction -from pyomo.gdp.disjunct import _DisjunctData, Disjunct +from pyomo.gdp.disjunct import DisjunctData, Disjunct import pyomo.core.expr as EXPR -from pyomo.core.base.component import _ComponentBase from pyomo.core import ( Block, Suffix, @@ -22,7 +21,7 @@ LogicalConstraint, value, ) -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.common.collections import ComponentMap, ComponentSet, OrderedSet from pyomo.opt import TerminationCondition, SolverStatus @@ -144,13 +143,13 @@ def parent(self, u): Arg: u : A node in the tree """ + if u in self._parent: + return self._parent[u] if u not in self._vertices: raise ValueError( "'%s' is not a vertex in the GDP tree. Cannot " "retrieve its parent." % u ) - if u in self._parent: - return self._parent[u] else: return None @@ -169,7 +168,10 @@ def parent_disjunct(self, u): Arg: u : A node in the forest """ - return self.parent(self.parent(u)) + if u.ctype is Disjunct: + return self.parent(self.parent(u)) + else: + return self.parent(u) def root_disjunct(self, u): """Returns the highest parent Disjunct in the hierarchy, or None if @@ -183,7 +185,7 @@ def root_disjunct(self, u): while True: if parent is None: return rootmost_disjunct - if isinstance(parent, _DisjunctData) or parent.ctype is Disjunct: + if parent.ctype is Disjunct: rootmost_disjunct = parent parent = self.parent(parent) @@ -243,7 +245,7 @@ def leaves(self): @property def disjunct_nodes(self): for v in self._vertices: - if isinstance(v, _DisjunctData) or v.ctype is Disjunct: + if v.ctype is Disjunct: yield v @@ -327,7 +329,7 @@ def get_gdp_tree(targets, instance, knownBlocks=None): "Target '%s' is not a component on instance " "'%s'!" % (t.name, instance.name) ) - if t.ctype is Block or isinstance(t, _BlockData): + if t.ctype is Block or isinstance(t, BlockData): _blocks = t.values() if t.is_indexed() else (t,) for block in _blocks: if not block.active: @@ -384,7 +386,7 @@ def is_child_of(parent, child, knownBlocks=None): if knownBlocks is None: knownBlocks = {} tmp = set() - node = child if isinstance(child, (Block, _BlockData)) else child.parent_block() + node = child if isinstance(child, (Block, BlockData)) else child.parent_block() while True: known = knownBlocks.get(node) if known: @@ -449,7 +451,7 @@ def get_src_disjunct(transBlock): Parameters ---------- - transBlock: _BlockData which is in the relaxedDisjuncts IndexedBlock + transBlock: BlockData which is in the relaxedDisjuncts IndexedBlock on a transformation block. """ if ( @@ -474,22 +476,23 @@ def get_src_constraint(transformedConstraint): a transformation block """ transBlock = transformedConstraint.parent_block() + src_constraints = transBlock.private_data('pyomo.gdp').src_constraint # This should be our block, so if it's not, the user messed up and gave # us the wrong thing. If they happen to also have a _constraintMap then # the world is really against us. - if not hasattr(transBlock, "_constraintMap"): + if transformedConstraint not in src_constraints: raise GDP_Error( "Constraint '%s' is not a transformed constraint" % transformedConstraint.name ) # if something goes wrong here, it's a bug in the mappings. - return transBlock._constraintMap['srcConstraints'][transformedConstraint] + return src_constraints[transformedConstraint] def _find_parent_disjunct(constraint): # traverse up until we find the disjunct this constraint lives on parent_disjunct = constraint.parent_block() - while not isinstance(parent_disjunct, _DisjunctData): + while not isinstance(parent_disjunct, DisjunctData): if parent_disjunct is None: raise GDP_Error( "Constraint '%s' is not on a disjunct and so was not " @@ -521,24 +524,28 @@ def get_transformed_constraints(srcConstraint): Parameters ---------- - srcConstraint: ScalarConstraint or _ConstraintData, which must be in + srcConstraint: ScalarConstraint or ConstraintData, which must be in the subtree of a transformed Disjunct """ if srcConstraint.is_indexed(): raise GDP_Error( "Argument to get_transformed_constraint should be " - "a ScalarConstraint or _ConstraintData. (If you " + "a ScalarConstraint or ConstraintData. (If you " "want the container for all transformed constraints " "from an IndexedDisjunction, this is the parent " "component of a transformed constraint originating " - "from any of its _ComponentDatas.)" + "from any of its ComponentDatas.)" ) transBlock = _get_constraint_transBlock(srcConstraint) - try: - return transBlock._constraintMap['transformedConstraints'][srcConstraint] - except: - logger.error("Constraint '%s' has not been transformed." % srcConstraint.name) - raise + transformed_constraints = transBlock.private_data( + 'pyomo.gdp' + ).transformed_constraints + if srcConstraint in transformed_constraints: + return transformed_constraints[srcConstraint] + else: + raise GDP_Error( + "Constraint '%s' has not been transformed." % srcConstraint.name + ) def _warn_for_active_disjunct(innerdisjunct, outerdisjunct): diff --git a/pyomo/kernel/__init__.py b/pyomo/kernel/__init__.py index 6ecea6343cd..289fe83f0e4 100644 --- a/pyomo/kernel/__init__.py +++ b/pyomo/kernel/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/kernel/util.py b/pyomo/kernel/util.py index 5fba6a2c2d9..bdfd0939537 100644 --- a/pyomo/kernel/util.py +++ b/pyomo/kernel/util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/mpec/__init__.py b/pyomo/mpec/__init__.py index 3989fe07b8e..a98ab94dc87 100644 --- a/pyomo/mpec/__init__.py +++ b/pyomo/mpec/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/mpec/complementarity.py b/pyomo/mpec/complementarity.py index df991ce9686..26968ef9fca 100644 --- a/pyomo/mpec/complementarity.py +++ b/pyomo/mpec/complementarity.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -19,7 +19,7 @@ from pyomo.core import Constraint, Var, Block, Set from pyomo.core.base.component import ModelComponentFactory from pyomo.core.base.global_set import UnindexedComponent_index -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.base.disable_methods import disable_methods from pyomo.core.base.initializer import ( Initializer, @@ -43,7 +43,7 @@ def complements(a, b): return ComplementarityTuple(a, b) -class _ComplementarityData(_BlockData): +class ComplementarityData(BlockData): def _canonical_expression(self, e): # Note: as the complimentarity component maintains references to # the original expression (e), it is NOT safe or valid to bypass @@ -179,9 +179,14 @@ def set_value(self, cc): ) +class _ComplementarityData(metaclass=RenamedClass): + __renamed__new_class__ = ComplementarityData + __renamed__version__ = '6.7.2' + + @ModelComponentFactory.register("Complementarity conditions.") class Complementarity(Block): - _ComponentDataClass = _ComplementarityData + _ComponentDataClass = ComplementarityData def __new__(cls, *args, **kwds): if cls != Complementarity: @@ -298,9 +303,9 @@ def _conditional_block_printer(ostream, idx, data): ) -class ScalarComplementarity(_ComplementarityData, Complementarity): +class ScalarComplementarity(ComplementarityData, Complementarity): def __init__(self, *args, **kwds): - _ComplementarityData.__init__(self, self) + ComplementarityData.__init__(self, self) Complementarity.__init__(self, *args, **kwds) self._data[None] = self self._index = UnindexedComponent_index @@ -357,13 +362,18 @@ def construct(self, data=None): """ Construct the expression(s) for this complementarity condition. """ - if is_debug_set(logger): - logger.debug("Constructing complementarity list %s", self.name) if self._constructed: return - timer = ConstructionTimer(self) self._constructed = True + timer = ConstructionTimer(self) + if is_debug_set(logger): + logger.debug("Constructing complementarity list %s", self.name) + + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() + if self._init_rule is not None: _init = self._init_rule(self.parent_block(), ()) for cc in iter(_init): diff --git a/pyomo/mpec/plugins/__init__.py b/pyomo/mpec/plugins/__init__.py index 3317e1ce829..1ff8c316e9b 100644 --- a/pyomo/mpec/plugins/__init__.py +++ b/pyomo/mpec/plugins/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/mpec/plugins/mpec1.py b/pyomo/mpec/plugins/mpec1.py index ad6905158c7..5935569d370 100644 --- a/pyomo/mpec/plugins/mpec1.py +++ b/pyomo/mpec/plugins/mpec1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/mpec/plugins/mpec2.py b/pyomo/mpec/plugins/mpec2.py index d019424ea4b..89d6c0814b2 100644 --- a/pyomo/mpec/plugins/mpec2.py +++ b/pyomo/mpec/plugins/mpec2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/mpec/plugins/mpec3.py b/pyomo/mpec/plugins/mpec3.py index d681c305a2d..1b7eb58b021 100644 --- a/pyomo/mpec/plugins/mpec3.py +++ b/pyomo/mpec/plugins/mpec3.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/mpec/plugins/mpec4.py b/pyomo/mpec/plugins/mpec4.py index 5b32886711a..fa3e37b16fe 100644 --- a/pyomo/mpec/plugins/mpec4.py +++ b/pyomo/mpec/plugins/mpec4.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/mpec/plugins/pathampl.py b/pyomo/mpec/plugins/pathampl.py index 7875251c04b..23b1b393ef3 100644 --- a/pyomo/mpec/plugins/pathampl.py +++ b/pyomo/mpec/plugins/pathampl.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/mpec/plugins/solver1.py b/pyomo/mpec/plugins/solver1.py index 0ac1af85522..02659844f1c 100644 --- a/pyomo/mpec/plugins/solver1.py +++ b/pyomo/mpec/plugins/solver1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/mpec/plugins/solver2.py b/pyomo/mpec/plugins/solver2.py index 491c8122d2e..5f5b6922e6f 100644 --- a/pyomo/mpec/plugins/solver2.py +++ b/pyomo/mpec/plugins/solver2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/mpec/tests/__init__.py b/pyomo/mpec/tests/__init__.py index c5e495e5aa3..a2a2c61779a 100644 --- a/pyomo/mpec/tests/__init__.py +++ b/pyomo/mpec/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/mpec/tests/cov2_None.txt b/pyomo/mpec/tests/cov2_None.txt index 2f7d59572a8..c3c0baeeb9e 100644 --- a/pyomo/mpec/tests/cov2_None.txt +++ b/pyomo/mpec/tests/cov2_None.txt @@ -1,2 +1,2 @@ -cc : Size=0, Index=cc_index, Active=True +cc : Size=0, Index={0, 1, 2}, Active=True Key : Arg0 : Arg1 : Active diff --git a/pyomo/mpec/tests/cov2_mpec.nl.txt b/pyomo/mpec/tests/cov2_mpec.nl.txt index a526784344b..9b7b9ed53f4 100644 --- a/pyomo/mpec/tests/cov2_mpec.nl.txt +++ b/pyomo/mpec/tests/cov2_mpec.nl.txt @@ -1,8 +1,3 @@ -1 Set Declarations - cc_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {0, 1, 2} - 4 Var Declarations x1 : Size=1, Index=None Key : Lower : Value : Upper : Fixed : Stale : Domain @@ -23,7 +18,7 @@ None : 0.5 : x1 : 0.5 : True 1 Block Declarations - cc : Size=0, Index=cc_index, Active=True + cc : Size=0, Index={0, 1, 2}, Active=True Key : Arg0 : Arg1 : Active -7 Declarations: y x1 x2 x3 cc_index cc keep_var_con +6 Declarations: y x1 x2 x3 cc keep_var_con diff --git a/pyomo/mpec/tests/cov2_mpec.simple_disjunction.txt b/pyomo/mpec/tests/cov2_mpec.simple_disjunction.txt index 2f7d59572a8..c3c0baeeb9e 100644 --- a/pyomo/mpec/tests/cov2_mpec.simple_disjunction.txt +++ b/pyomo/mpec/tests/cov2_mpec.simple_disjunction.txt @@ -1,2 +1,2 @@ -cc : Size=0, Index=cc_index, Active=True +cc : Size=0, Index={0, 1, 2}, Active=True Key : Arg0 : Arg1 : Active diff --git a/pyomo/mpec/tests/cov2_mpec.simple_nonlinear.txt b/pyomo/mpec/tests/cov2_mpec.simple_nonlinear.txt index 2f7d59572a8..c3c0baeeb9e 100644 --- a/pyomo/mpec/tests/cov2_mpec.simple_nonlinear.txt +++ b/pyomo/mpec/tests/cov2_mpec.simple_nonlinear.txt @@ -1,2 +1,2 @@ -cc : Size=0, Index=cc_index, Active=True +cc : Size=0, Index={0, 1, 2}, Active=True Key : Arg0 : Arg1 : Active diff --git a/pyomo/mpec/tests/cov2_mpec.standard_form.txt b/pyomo/mpec/tests/cov2_mpec.standard_form.txt index 2f7d59572a8..c3c0baeeb9e 100644 --- a/pyomo/mpec/tests/cov2_mpec.standard_form.txt +++ b/pyomo/mpec/tests/cov2_mpec.standard_form.txt @@ -1,2 +1,2 @@ -cc : Size=0, Index=cc_index, Active=True +cc : Size=0, Index={0, 1, 2}, Active=True Key : Arg0 : Arg1 : Active diff --git a/pyomo/mpec/tests/list1_None.txt b/pyomo/mpec/tests/list1_None.txt index 8e849242bcd..34c358a1521 100644 --- a/pyomo/mpec/tests/list1_None.txt +++ b/pyomo/mpec/tests/list1_None.txt @@ -1,4 +1,4 @@ -cc : Size=2, Index=cc_index, Active=True +cc : Size=2, Index={1, 2}, Active=True Key : Arg0 : Arg1 : Active 1 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 2 : True diff --git a/pyomo/mpec/tests/list1_mpec.nl.txt b/pyomo/mpec/tests/list1_mpec.nl.txt index 16310c59317..62edc488b47 100644 --- a/pyomo/mpec/tests/list1_mpec.nl.txt +++ b/pyomo/mpec/tests/list1_mpec.nl.txt @@ -1,8 +1,3 @@ -1 Set Declarations - cc_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 2 : {1, 2} - 4 Var Declarations x1 : Size=1, Index=None Key : Lower : Value : Upper : Fixed : Stale : Domain @@ -18,7 +13,7 @@ None : None : None : None : False : True : Reals 1 Block Declarations - cc : Size=2, Index=cc_index, Active=True + cc : Size=2, Index={1, 2}, Active=True Key : Arg0 : Arg1 : Active 1 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 2 : True @@ -37,4 +32,4 @@ 1 Declarations: c -6 Declarations: y x1 x2 x3 cc_index cc +5 Declarations: y x1 x2 x3 cc diff --git a/pyomo/mpec/tests/list1_mpec.simple_disjunction.txt b/pyomo/mpec/tests/list1_mpec.simple_disjunction.txt index 816e56af56c..c2bfe5e0399 100644 --- a/pyomo/mpec/tests/list1_mpec.simple_disjunction.txt +++ b/pyomo/mpec/tests/list1_mpec.simple_disjunction.txt @@ -1,4 +1,4 @@ -cc : Size=2, Index=cc_index, Active=True +cc : Size=2, Index={1, 2}, Active=True Key : Arg0 : Arg1 : Active 1 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 2 : True diff --git a/pyomo/mpec/tests/list1_mpec.simple_nonlinear.txt b/pyomo/mpec/tests/list1_mpec.simple_nonlinear.txt index 816e56af56c..c2bfe5e0399 100644 --- a/pyomo/mpec/tests/list1_mpec.simple_nonlinear.txt +++ b/pyomo/mpec/tests/list1_mpec.simple_nonlinear.txt @@ -1,4 +1,4 @@ -cc : Size=2, Index=cc_index, Active=True +cc : Size=2, Index={1, 2}, Active=True Key : Arg0 : Arg1 : Active 1 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 2 : True diff --git a/pyomo/mpec/tests/list1_mpec.standard_form.txt b/pyomo/mpec/tests/list1_mpec.standard_form.txt index 816e56af56c..c2bfe5e0399 100644 --- a/pyomo/mpec/tests/list1_mpec.standard_form.txt +++ b/pyomo/mpec/tests/list1_mpec.standard_form.txt @@ -1,4 +1,4 @@ -cc : Size=2, Index=cc_index, Active=True +cc : Size=2, Index={1, 2}, Active=True Key : Arg0 : Arg1 : Active 1 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 2 : True diff --git a/pyomo/mpec/tests/list2_None.txt b/pyomo/mpec/tests/list2_None.txt index cc84321fe3e..465bc347766 100644 --- a/pyomo/mpec/tests/list2_None.txt +++ b/pyomo/mpec/tests/list2_None.txt @@ -1,4 +1,4 @@ -cc : Size=3, Index=cc_index, Active=True +cc : Size=3, Index={1, 2, 3}, Active=True Key : Arg0 : Arg1 : Active 1 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 1 : False diff --git a/pyomo/mpec/tests/list2_mpec.nl.txt b/pyomo/mpec/tests/list2_mpec.nl.txt index c8c461e08e8..6dc49cef8dd 100644 --- a/pyomo/mpec/tests/list2_mpec.nl.txt +++ b/pyomo/mpec/tests/list2_mpec.nl.txt @@ -1,8 +1,3 @@ -1 Set Declarations - cc_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {1, 2, 3} - 4 Var Declarations x1 : Size=1, Index=None Key : Lower : Value : Upper : Fixed : Stale : Domain @@ -18,7 +13,7 @@ None : None : None : None : False : True : Reals 1 Block Declarations - cc : Size=3, Index=cc_index, Active=True + cc : Size=3, Index={1, 2, 3}, Active=True Key : Arg0 : Arg1 : Active 1 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 1 : False @@ -40,4 +35,4 @@ 1 Declarations: c -6 Declarations: y x1 x2 x3 cc_index cc +5 Declarations: y x1 x2 x3 cc diff --git a/pyomo/mpec/tests/list2_mpec.simple_disjunction.txt b/pyomo/mpec/tests/list2_mpec.simple_disjunction.txt index 82688e8f017..c71d6461d22 100644 --- a/pyomo/mpec/tests/list2_mpec.simple_disjunction.txt +++ b/pyomo/mpec/tests/list2_mpec.simple_disjunction.txt @@ -1,4 +1,4 @@ -cc : Size=3, Index=cc_index, Active=True +cc : Size=3, Index={1, 2, 3}, Active=True Key : Arg0 : Arg1 : Active 1 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 1 : False diff --git a/pyomo/mpec/tests/list2_mpec.simple_nonlinear.txt b/pyomo/mpec/tests/list2_mpec.simple_nonlinear.txt index 82688e8f017..c71d6461d22 100644 --- a/pyomo/mpec/tests/list2_mpec.simple_nonlinear.txt +++ b/pyomo/mpec/tests/list2_mpec.simple_nonlinear.txt @@ -1,4 +1,4 @@ -cc : Size=3, Index=cc_index, Active=True +cc : Size=3, Index={1, 2, 3}, Active=True Key : Arg0 : Arg1 : Active 1 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 1 : False diff --git a/pyomo/mpec/tests/list2_mpec.standard_form.txt b/pyomo/mpec/tests/list2_mpec.standard_form.txt index 82688e8f017..c71d6461d22 100644 --- a/pyomo/mpec/tests/list2_mpec.standard_form.txt +++ b/pyomo/mpec/tests/list2_mpec.standard_form.txt @@ -1,4 +1,4 @@ -cc : Size=3, Index=cc_index, Active=True +cc : Size=3, Index={1, 2, 3}, Active=True Key : Arg0 : Arg1 : Active 1 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 1 : False diff --git a/pyomo/mpec/tests/list5_None.txt b/pyomo/mpec/tests/list5_None.txt index 8e6ed9a8164..962ee6cbc3a 100644 --- a/pyomo/mpec/tests/list5_None.txt +++ b/pyomo/mpec/tests/list5_None.txt @@ -1,4 +1,4 @@ -cc : Size=3, Index=cc_index, Active=True +cc : Size=3, Index={1, 2, 3}, Active=True Key : Arg0 : Arg1 : Active 1 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 1 : True diff --git a/pyomo/mpec/tests/list5_mpec.nl.txt b/pyomo/mpec/tests/list5_mpec.nl.txt index adb64af0457..93ee89f3389 100644 --- a/pyomo/mpec/tests/list5_mpec.nl.txt +++ b/pyomo/mpec/tests/list5_mpec.nl.txt @@ -1,8 +1,3 @@ -1 Set Declarations - cc_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {1, 2, 3} - 4 Var Declarations x1 : Size=1, Index=None Key : Lower : Value : Upper : Fixed : Stale : Domain @@ -18,7 +13,7 @@ None : None : None : None : False : True : Reals 1 Block Declarations - cc : Size=3, Index=cc_index, Active=True + cc : Size=3, Index={1, 2, 3}, Active=True Key : Arg0 : Arg1 : Active 1 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 1 : True @@ -45,4 +40,4 @@ 1 Declarations: c -6 Declarations: y x1 x2 x3 cc_index cc +5 Declarations: y x1 x2 x3 cc diff --git a/pyomo/mpec/tests/list5_mpec.simple_disjunction.txt b/pyomo/mpec/tests/list5_mpec.simple_disjunction.txt index 69178523d96..15622fa84e1 100644 --- a/pyomo/mpec/tests/list5_mpec.simple_disjunction.txt +++ b/pyomo/mpec/tests/list5_mpec.simple_disjunction.txt @@ -1,4 +1,4 @@ -cc : Size=3, Index=cc_index, Active=True +cc : Size=3, Index={1, 2, 3}, Active=True Key : Arg0 : Arg1 : Active 1 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 1 : True diff --git a/pyomo/mpec/tests/list5_mpec.simple_nonlinear.txt b/pyomo/mpec/tests/list5_mpec.simple_nonlinear.txt index 69178523d96..15622fa84e1 100644 --- a/pyomo/mpec/tests/list5_mpec.simple_nonlinear.txt +++ b/pyomo/mpec/tests/list5_mpec.simple_nonlinear.txt @@ -1,4 +1,4 @@ -cc : Size=3, Index=cc_index, Active=True +cc : Size=3, Index={1, 2, 3}, Active=True Key : Arg0 : Arg1 : Active 1 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 1 : True diff --git a/pyomo/mpec/tests/list5_mpec.standard_form.txt b/pyomo/mpec/tests/list5_mpec.standard_form.txt index 69178523d96..15622fa84e1 100644 --- a/pyomo/mpec/tests/list5_mpec.standard_form.txt +++ b/pyomo/mpec/tests/list5_mpec.standard_form.txt @@ -1,4 +1,4 @@ -cc : Size=3, Index=cc_index, Active=True +cc : Size=3, Index={1, 2, 3}, Active=True Key : Arg0 : Arg1 : Active 1 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 1 : True diff --git a/pyomo/mpec/tests/t10_None.txt b/pyomo/mpec/tests/t10_None.txt index afc38166ab3..7d6b4c429cc 100644 --- a/pyomo/mpec/tests/t10_None.txt +++ b/pyomo/mpec/tests/t10_None.txt @@ -1,4 +1,4 @@ -cc : Size=3, Index=cc_index, Active=True +cc : Size=3, Index={0, 1, 2}, Active=True Key : Arg0 : Arg1 : Active 0 : y + x3 : x1 + 2*x2 == 0 : True 1 : y + x3 : x1 + 2*x2 == 1 : False diff --git a/pyomo/mpec/tests/t10_mpec.nl.txt b/pyomo/mpec/tests/t10_mpec.nl.txt index a4a16713eaa..12db893ddba 100644 --- a/pyomo/mpec/tests/t10_mpec.nl.txt +++ b/pyomo/mpec/tests/t10_mpec.nl.txt @@ -1,8 +1,3 @@ -1 Set Declarations - cc_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {0, 1, 2} - 4 Var Declarations x1 : Size=1, Index=None Key : Lower : Value : Upper : Fixed : Stale : Domain @@ -18,7 +13,7 @@ None : None : None : None : False : True : Reals 1 Block Declarations - cc : Size=3, Index=cc_index, Active=True + cc : Size=3, Index={0, 1, 2}, Active=True Key : Arg0 : Arg1 : Active 0 : y + x3 : x1 + 2*x2 == 0 : True 1 : y + x3 : x1 + 2*x2 == 1 : False @@ -40,4 +35,4 @@ 1 Declarations: c -6 Declarations: y x1 x2 x3 cc_index cc +5 Declarations: y x1 x2 x3 cc diff --git a/pyomo/mpec/tests/t10_mpec.simple_disjunction.txt b/pyomo/mpec/tests/t10_mpec.simple_disjunction.txt index c53c1b8e62b..37aaaafcf68 100644 --- a/pyomo/mpec/tests/t10_mpec.simple_disjunction.txt +++ b/pyomo/mpec/tests/t10_mpec.simple_disjunction.txt @@ -1,4 +1,4 @@ -cc : Size=3, Index=cc_index, Active=True +cc : Size=3, Index={0, 1, 2}, Active=True Key : Arg0 : Arg1 : Active 0 : y + x3 : x1 + 2*x2 == 0 : True 1 : y + x3 : x1 + 2*x2 == 1 : False diff --git a/pyomo/mpec/tests/t10_mpec.simple_nonlinear.txt b/pyomo/mpec/tests/t10_mpec.simple_nonlinear.txt index c53c1b8e62b..37aaaafcf68 100644 --- a/pyomo/mpec/tests/t10_mpec.simple_nonlinear.txt +++ b/pyomo/mpec/tests/t10_mpec.simple_nonlinear.txt @@ -1,4 +1,4 @@ -cc : Size=3, Index=cc_index, Active=True +cc : Size=3, Index={0, 1, 2}, Active=True Key : Arg0 : Arg1 : Active 0 : y + x3 : x1 + 2*x2 == 0 : True 1 : y + x3 : x1 + 2*x2 == 1 : False diff --git a/pyomo/mpec/tests/t10_mpec.standard_form.txt b/pyomo/mpec/tests/t10_mpec.standard_form.txt index c53c1b8e62b..37aaaafcf68 100644 --- a/pyomo/mpec/tests/t10_mpec.standard_form.txt +++ b/pyomo/mpec/tests/t10_mpec.standard_form.txt @@ -1,4 +1,4 @@ -cc : Size=3, Index=cc_index, Active=True +cc : Size=3, Index={0, 1, 2}, Active=True Key : Arg0 : Arg1 : Active 0 : y + x3 : x1 + 2*x2 == 0 : True 1 : y + x3 : x1 + 2*x2 == 1 : False diff --git a/pyomo/mpec/tests/t13_None.txt b/pyomo/mpec/tests/t13_None.txt index b2e24eb1166..fde3cc15a18 100644 --- a/pyomo/mpec/tests/t13_None.txt +++ b/pyomo/mpec/tests/t13_None.txt @@ -1,4 +1,4 @@ -cc : Size=2, Index=cc_index, Active=True +cc : Size=2, Index={0, 1, 2}, Active=True Key : Arg0 : Arg1 : Active 0 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 2 : True diff --git a/pyomo/mpec/tests/t13_mpec.nl.txt b/pyomo/mpec/tests/t13_mpec.nl.txt index dc47767efb7..9e709e35b6f 100644 --- a/pyomo/mpec/tests/t13_mpec.nl.txt +++ b/pyomo/mpec/tests/t13_mpec.nl.txt @@ -1,8 +1,3 @@ -1 Set Declarations - cc_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {0, 1, 2} - 4 Var Declarations x1 : Size=1, Index=None Key : Lower : Value : Upper : Fixed : Stale : Domain @@ -18,7 +13,7 @@ None : None : None : None : False : True : Reals 1 Block Declarations - cc : Size=2, Index=cc_index, Active=True + cc : Size=2, Index={0, 1, 2}, Active=True Key : Arg0 : Arg1 : Active 0 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 2 : True @@ -37,4 +32,4 @@ 1 Declarations: c -6 Declarations: y x1 x2 x3 cc_index cc +5 Declarations: y x1 x2 x3 cc diff --git a/pyomo/mpec/tests/t13_mpec.simple_disjunction.txt b/pyomo/mpec/tests/t13_mpec.simple_disjunction.txt index 1ff09babad8..9b361c7e503 100644 --- a/pyomo/mpec/tests/t13_mpec.simple_disjunction.txt +++ b/pyomo/mpec/tests/t13_mpec.simple_disjunction.txt @@ -1,4 +1,4 @@ -cc : Size=2, Index=cc_index, Active=True +cc : Size=2, Index={0, 1, 2}, Active=True Key : Arg0 : Arg1 : Active 0 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 2 : True diff --git a/pyomo/mpec/tests/t13_mpec.simple_nonlinear.txt b/pyomo/mpec/tests/t13_mpec.simple_nonlinear.txt index 1ff09babad8..9b361c7e503 100644 --- a/pyomo/mpec/tests/t13_mpec.simple_nonlinear.txt +++ b/pyomo/mpec/tests/t13_mpec.simple_nonlinear.txt @@ -1,4 +1,4 @@ -cc : Size=2, Index=cc_index, Active=True +cc : Size=2, Index={0, 1, 2}, Active=True Key : Arg0 : Arg1 : Active 0 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 2 : True diff --git a/pyomo/mpec/tests/t13_mpec.standard_form.txt b/pyomo/mpec/tests/t13_mpec.standard_form.txt index 1ff09babad8..9b361c7e503 100644 --- a/pyomo/mpec/tests/t13_mpec.standard_form.txt +++ b/pyomo/mpec/tests/t13_mpec.standard_form.txt @@ -1,4 +1,4 @@ -cc : Size=2, Index=cc_index, Active=True +cc : Size=2, Index={0, 1, 2}, Active=True Key : Arg0 : Arg1 : Active 0 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 2 : True diff --git a/pyomo/mpec/tests/test_complementarity.py b/pyomo/mpec/tests/test_complementarity.py index 1eb0385c3e5..545104364cf 100644 --- a/pyomo/mpec/tests/test_complementarity.py +++ b/pyomo/mpec/tests/test_complementarity.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/mpec/tests/test_minlp.py b/pyomo/mpec/tests/test_minlp.py index 367a57b817e..965906f4235 100644 --- a/pyomo/mpec/tests/test_minlp.py +++ b/pyomo/mpec/tests/test_minlp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/mpec/tests/test_nlp.py b/pyomo/mpec/tests/test_nlp.py index be5234136a1..a87d4ad2b09 100644 --- a/pyomo/mpec/tests/test_nlp.py +++ b/pyomo/mpec/tests/test_nlp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/mpec/tests/test_path.py b/pyomo/mpec/tests/test_path.py index 5dd7178acf5..0501d19d2ac 100644 --- a/pyomo/mpec/tests/test_path.py +++ b/pyomo/mpec/tests/test_path.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/neos/__init__.py b/pyomo/neos/__init__.py index 73ac0c51216..7d18535e753 100644 --- a/pyomo/neos/__init__.py +++ b/pyomo/neos/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/neos/kestrel.py b/pyomo/neos/kestrel.py index 44734294eb4..8959a81bd0f 100644 --- a/pyomo/neos/kestrel.py +++ b/pyomo/neos/kestrel.py @@ -2,7 +2,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/neos/plugins/NEOS.py b/pyomo/neos/plugins/NEOS.py index 2d5929fa9a1..84bc51645c0 100644 --- a/pyomo/neos/plugins/NEOS.py +++ b/pyomo/neos/plugins/NEOS.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -50,7 +50,7 @@ def create_command_line(self, executable, problem_files): logger.info("Solver log file: '%s'" % (self._log_file,)) if self._soln_file is not None: logger.info("Solver solution file: '%s'" % (self._soln_file,)) - if self._problem_files is not []: + if self._problem_files != []: logger.info("Solver problem files: %s" % (self._problem_files,)) return Bunch(cmd="", log_file=self._log_file, env="") diff --git a/pyomo/neos/plugins/__init__.py b/pyomo/neos/plugins/__init__.py index 323f96e9bdc..75105e87088 100644 --- a/pyomo/neos/plugins/__init__.py +++ b/pyomo/neos/plugins/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/neos/plugins/kestrel_plugin.py b/pyomo/neos/plugins/kestrel_plugin.py index 72d73d15ace..fecb98e0084 100644 --- a/pyomo/neos/plugins/kestrel_plugin.py +++ b/pyomo/neos/plugins/kestrel_plugin.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -193,13 +193,9 @@ def _perform_wait_any(self): del self._ah[jobNumber] ah.status = ActionStatus.done - ( - opt, - smap_id, - load_solutions, - select_index, - default_variable_value, - ) = self._opt_data[jobNumber] + (opt, smap_id, load_solutions, select_index, default_variable_value) = ( + self._opt_data[jobNumber] + ) del self._opt_data[jobNumber] args = self._args[jobNumber] @@ -262,11 +258,10 @@ def _perform_wait_any(self): # minutes. If NEOS doesn't produce intermediate results # by then we will need to catch (and eat) the exception try: - ( - message_fragment, - new_offset, - ) = self.kestrel.neos.getIntermediateResults( - jobNumber, self._ah[jobNumber].password, current_offset + (message_fragment, new_offset) = ( + self.kestrel.neos.getIntermediateResults( + jobNumber, self._ah[jobNumber].password, current_offset + ) ) logger.info(message_fragment) self._neos_log[jobNumber] = ( diff --git a/pyomo/neos/tests/__init__.py b/pyomo/neos/tests/__init__.py index 1cf642c0eac..83603e3d8ba 100644 --- a/pyomo/neos/tests/__init__.py +++ b/pyomo/neos/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/neos/tests/model_min_lp.py b/pyomo/neos/tests/model_min_lp.py index 56e1b124cd4..eacf0451c94 100644 --- a/pyomo/neos/tests/model_min_lp.py +++ b/pyomo/neos/tests/model_min_lp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/neos/tests/test_neos.py b/pyomo/neos/tests/test_neos.py index c43869e65cc..a4c4e9e6367 100644 --- a/pyomo/neos/tests/test_neos.py +++ b/pyomo/neos/tests/test_neos.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/network/__init__.py b/pyomo/network/__init__.py index 097471102be..6ccfb64f79c 100644 --- a/pyomo/network/__init__.py +++ b/pyomo/network/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/network/arc.py b/pyomo/network/arc.py index ff1874b0274..f2597b4c1bd 100644 --- a/pyomo/network/arc.py +++ b/pyomo/network/arc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['Arc'] - from pyomo.network.port import Port from pyomo.core.base.component import ActiveComponentData, ModelComponentFactory from pyomo.core.base.indexed_component import ( @@ -54,7 +52,7 @@ def _iterable_to_dict(vals, directed, name): return vals -class _ArcData(ActiveComponentData): +class ArcData(ActiveComponentData): """ This class defines the data for a single Arc @@ -248,6 +246,11 @@ def _validate_ports(self, source, destination, ports): ) +class _ArcData(metaclass=RenamedClass): + __renamed__new_class__ = ArcData + __renamed__version__ = '6.7.2' + + @ModelComponentFactory.register("Component used for connecting two Ports.") class Arc(ActiveIndexedComponent): """ @@ -269,7 +272,7 @@ class Arc(ActiveIndexedComponent): or a two-member iterable of ports """ - _ComponentDataClass = _ArcData + _ComponentDataClass = ArcData def __new__(cls, *args, **kwds): if cls != Arc: @@ -296,14 +299,18 @@ def __init__(self, *args, **kwds): def construct(self, data=None): """Initialize the Arc""" - if is_debug_set(logger): - logger.debug("Constructing Arc %s" % self.name) - if self._constructed: return + self._constructed = True + + if is_debug_set(logger): + logger.debug("Constructing Arc %s" % self.name) timer = ConstructionTimer(self) - self._constructed = True + + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() if self._rule is None and self._init_vals is None: # No construction rule or values specified @@ -371,9 +378,9 @@ def _pprint(self): ) -class ScalarArc(_ArcData, Arc): +class ScalarArc(ArcData, Arc): def __init__(self, *args, **kwds): - _ArcData.__init__(self, self) + ArcData.__init__(self, self) Arc.__init__(self, *args, **kwds) self.index = UnindexedComponent_index diff --git a/pyomo/network/decomposition.py b/pyomo/network/decomposition.py index ae306766ae0..1ffb6a710ff 100644 --- a/pyomo/network/decomposition.py +++ b/pyomo/network/decomposition.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['SequentialDecomposition'] - from pyomo.network import Port, Arc from pyomo.network.foqus_graph import FOQUSGraph from pyomo.core import ( diff --git a/pyomo/network/foqus_graph.py b/pyomo/network/foqus_graph.py index e6fc34aaf62..7c6c05256d9 100644 --- a/pyomo/network/foqus_graph.py +++ b/pyomo/network/foqus_graph.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -358,9 +358,9 @@ def scc_calculation_order(self, sccNodes, ie, oe): done = False for i in range(len(sccNodes)): for j in range(len(sccNodes)): - for ine in ie[i]: - for oute in oe[j]: - if ine == oute: + for in_e in ie[i]: + for out_e in oe[j]: + if in_e == out_e: adj[j].append(i) adjR[i].append(j) done = True diff --git a/pyomo/network/plugins/__init__.py b/pyomo/network/plugins/__init__.py index 5e9677d2bc4..ab3cde23daa 100644 --- a/pyomo/network/plugins/__init__.py +++ b/pyomo/network/plugins/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/network/plugins/expand_arcs.py b/pyomo/network/plugins/expand_arcs.py index 4f6185d3173..b1f915214eb 100644 --- a/pyomo/network/plugins/expand_arcs.py +++ b/pyomo/network/plugins/expand_arcs.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/network/port.py b/pyomo/network/port.py index 4afb0e23ed0..f6706dce644 100644 --- a/pyomo/network/port.py +++ b/pyomo/network/port.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['Port'] - import logging, sys from weakref import ref as weakref_ref @@ -38,7 +36,7 @@ logger = logging.getLogger('pyomo.network') -class _PortData(ComponentData): +class PortData(ComponentData): """ This class defines the data for a single Port @@ -287,6 +285,11 @@ def get_split_fraction(self, arc): return res +class _PortData(metaclass=RenamedClass): + __renamed__new_class__ = PortData + __renamed__version__ = '6.7.2' + + @ModelComponentFactory.register( "A bundle of variables that can be connected to other ports." ) @@ -341,21 +344,25 @@ def __init__(self, *args, **kwd): # IndexedComponent that support implicit definition def _getitem_when_not_present(self, idx): """Returns the default component data value.""" - tmp = self._data[idx] = _PortData(component=self) + tmp = self._data[idx] = PortData(component=self) tmp._index = idx return tmp def construct(self, data=None): - if is_debug_set(logger): # pragma:nocover - logger.debug("Constructing Port, name=%s, from data=%s" % (self.name, data)) - if self._constructed: return + self._constructed = True timer = ConstructionTimer(self) - self._constructed = True - # Construct _PortData objects for all index values + if is_debug_set(logger): # pragma:nocover + logger.debug("Constructing Port, name=%s, from data=%s" % (self.name, data)) + + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() + + # Construct PortData objects for all index values if self.is_indexed(): self._initialize_members(self._index_set) else: @@ -761,9 +768,9 @@ def _create_evar(member, name, eblock, index_set): return evar -class ScalarPort(Port, _PortData): +class ScalarPort(Port, PortData): def __init__(self, *args, **kwd): - _PortData.__init__(self, component=self) + PortData.__init__(self, component=self) Port.__init__(self, *args, **kwd) self._index = UnindexedComponent_index diff --git a/pyomo/network/tests/__init__.py b/pyomo/network/tests/__init__.py index 1eb6d95e148..173fdc4e727 100644 --- a/pyomo/network/tests/__init__.py +++ b/pyomo/network/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/network/tests/test_arc.py b/pyomo/network/tests/test_arc.py index cd340cace7a..8356bcce9d8 100644 --- a/pyomo/network/tests/test_arc.py +++ b/pyomo/network/tests/test_arc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -504,11 +504,11 @@ def test_expand_indexed(self): os.getvalue(), """c_expanded : Size=1, Index=None, Active=True 3 Constraint Declarations - a_equality : Size=2, Index=x_index, Active=True + a_equality : Size=2, Index={1, 2}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : x[1] - t[1] : 0.0 : True 2 : 0.0 : x[2] - t[2] : 0.0 : True - b_equality : Size=4, Index=y_index, Active=True + b_equality : Size=4, Index={1, 2}*{1, 2}, Active=True Key : Lower : Body : Upper : Active (1, 1) : 0.0 : y[1,1] - u[1,1] : 0.0 : True (1, 2) : 0.0 : y[1,2] - u[1,2] : 0.0 : True @@ -677,7 +677,7 @@ def test_expand_empty_indexed(self): os.getvalue(), """c_expanded : Size=1, Index=None, Active=True 2 Constraint Declarations - x_equality : Size=2, Index=x_index, Active=True + x_equality : Size=2, Index={1, 2}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : x[1] - EPRT_auto_x[1] : 0.0 : True 2 : 0.0 : x[2] - EPRT_auto_x[2] : 0.0 : True @@ -739,7 +739,7 @@ def test_expand_multiple_empty_indexed(self): os.getvalue(), """c_expanded : Size=1, Index=None, Active=True 2 Constraint Declarations - x_equality : Size=2, Index=x_index, Active=True + x_equality : Size=2, Index={1, 2}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : x[1] - EPRT1_auto_x[1] : 0.0 : True 2 : 0.0 : x[2] - EPRT1_auto_x[2] : 0.0 : True @@ -757,7 +757,7 @@ def test_expand_multiple_empty_indexed(self): os.getvalue(), """d_expanded : Size=1, Index=None, Active=True 2 Constraint Declarations - x_equality : Size=2, Index=x_index, Active=True + x_equality : Size=2, Index={1, 2}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : EPRT2_auto_x[1] - EPRT1_auto_x[1] : 0.0 : True 2 : 0.0 : EPRT2_auto_x[2] - EPRT1_auto_x[2] : 0.0 : True @@ -812,7 +812,7 @@ def test_expand_multiple_indexed(self): os.getvalue(), """c_expanded : Size=1, Index=None, Active=True 2 Constraint Declarations - x_equality : Size=2, Index=x_index, Active=True + x_equality : Size=2, Index={1, 2}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : x[1] - a1[1] : 0.0 : True 2 : 0.0 : x[2] - a1[2] : 0.0 : True @@ -830,7 +830,7 @@ def test_expand_multiple_indexed(self): os.getvalue(), """d_expanded : Size=1, Index=None, Active=True 2 Constraint Declarations - x_equality : Size=2, Index=x_index, Active=True + x_equality : Size=2, Index={1, 2}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : a2[1] - a1[1] : 0.0 : True 2 : 0.0 : a2[2] - a1[2] : 0.0 : True @@ -903,7 +903,7 @@ def test_expand_implicit_indexed(self): os.getvalue(), """c_expanded : Size=1, Index=None, Active=True 2 Constraint Declarations - x_equality : Size=2, Index=a2_index, Active=True + x_equality : Size=2, Index={1, 2}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : a2[1] - x[1] : 0.0 : True 2 : 0.0 : a2[2] - x[2] : 0.0 : True @@ -921,7 +921,7 @@ def test_expand_implicit_indexed(self): os.getvalue(), """d_expanded : Size=1, Index=None, Active=True 2 Constraint Declarations - x_equality : Size=2, Index=a2_index, Active=True + x_equality : Size=2, Index={1, 2}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : EPRT2_auto_x[1] - x[1] : 0.0 : True 2 : 0.0 : EPRT2_auto_x[2] - x[2] : 0.0 : True @@ -964,7 +964,7 @@ def rule(m, i): m.component('eq_expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """eq_expanded : Size=2, Index=eq_index, Active=True + """eq_expanded : Size=2, Index={1, 2}, Active=True eq_expanded[1] : Active=True 1 Constraint Declarations v_equality : Size=1, Index=None, Active=True diff --git a/pyomo/network/tests/test_decomposition.py b/pyomo/network/tests/test_decomposition.py index 4e4d0231d00..2db310217d0 100644 --- a/pyomo/network/tests/test_decomposition.py +++ b/pyomo/network/tests/test_decomposition.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/network/tests/test_port.py b/pyomo/network/tests/test_port.py index bc9a6fc527f..a417a832015 100644 --- a/pyomo/network/tests/test_port.py +++ b/pyomo/network/tests/test_port.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/network/util.py b/pyomo/network/util.py index be0fa2c84d1..4865218aca8 100644 --- a/pyomo/network/util.py +++ b/pyomo/network/util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/__init__.py b/pyomo/opt/__init__.py index 8c12d3fa201..c78dd0384d2 100644 --- a/pyomo/opt/__init__.py +++ b/pyomo/opt/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/base/__init__.py b/pyomo/opt/base/__init__.py index 9d29efc859d..8d11114dd09 100644 --- a/pyomo/opt/base/__init__.py +++ b/pyomo/opt/base/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/base/convert.py b/pyomo/opt/base/convert.py index 972239a65cd..28ad6727d3e 100644 --- a/pyomo/opt/base/convert.py +++ b/pyomo/opt/base/convert.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['convert_problem'] - import copy import os @@ -55,7 +53,7 @@ def convert_problem( if os.sep in fname: # pragma:nocover fname = tmp.split(os.sep)[-1] source_ptype = [guess_format(fname)] - if source_ptype is [None]: + if source_ptype == [None]: raise ConverterError("Unknown suffix type: " + tmp) else: source_ptype = args[0].valid_problem_types() diff --git a/pyomo/opt/base/error.py b/pyomo/opt/base/error.py index aa97469f6d0..b03fafd7037 100644 --- a/pyomo/opt/base/error.py +++ b/pyomo/opt/base/error.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/base/formats.py b/pyomo/opt/base/formats.py index 2acd77b80e4..6e9d3958f48 100644 --- a/pyomo/opt/base/formats.py +++ b/pyomo/opt/base/formats.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,11 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -# -# The formats that are supported by Pyomo -# -__all__ = ['ProblemFormat', 'ResultsFormat', 'guess_format'] - import enum diff --git a/pyomo/opt/base/opt_config.py b/pyomo/opt/base/opt_config.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/opt/base/opt_config.py +++ b/pyomo/opt/base/opt_config.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/base/problem.py b/pyomo/opt/base/problem.py index 6be1d4d6db6..804a97e2e4c 100644 --- a/pyomo/opt/base/problem.py +++ b/pyomo/opt/base/problem.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ["AbstractProblemWriter", "WriterFactory", "BranchDirection"] - from pyomo.common import Factory diff --git a/pyomo/opt/base/results.py b/pyomo/opt/base/results.py index 68999fae6e4..ea295a66315 100644 --- a/pyomo/opt/base/results.py +++ b/pyomo/opt/base/results.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['AbstractResultsReader', 'ReaderFactory'] - from pyomo.common import Factory diff --git a/pyomo/opt/base/solvers.py b/pyomo/opt/base/solvers.py index 0de60902af2..c0698165603 100644 --- a/pyomo/opt/base/solvers.py +++ b/pyomo/opt/base/solvers.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ('OptSolver', 'SolverFactory', 'UnknownSolver', 'check_available_solvers') - import re import sys import time @@ -18,12 +16,11 @@ import shlex from pyomo.common import Factory -from pyomo.common.config import ConfigDict from pyomo.common.errors import ApplicationError from pyomo.common.collections import Bunch from pyomo.opt.base.convert import convert_problem -from pyomo.opt.base.formats import ResultsFormat, ProblemFormat +from pyomo.opt.base.formats import ResultsFormat import pyomo.opt.base.results logger = logging.getLogger('pyomo.opt') @@ -181,7 +178,11 @@ def __call__(self, _name=None, **kwds): return opt +LegacySolverFactory = SolverFactoryClass('solver type') + SolverFactory = SolverFactoryClass('solver type') +SolverFactory._cls = LegacySolverFactory._cls +SolverFactory._doc = LegacySolverFactory._doc # @@ -535,15 +536,15 @@ def solve(self, *args, **kwds): # If the inputs are models, then validate that they have been # constructed! Collect suffix names to try and import from solution. # - from pyomo.core.base.block import _BlockData + from pyomo.core.base.block import BlockData import pyomo.core.base.suffix from pyomo.core.kernel.block import IBlock import pyomo.core.kernel.suffix _model = None for arg in args: - if isinstance(arg, (_BlockData, IBlock)): - if isinstance(arg, _BlockData): + if isinstance(arg, (BlockData, IBlock)): + if isinstance(arg, BlockData): if not arg.is_constructed(): raise RuntimeError( "Attempting to solve model=%s with unconstructed " @@ -552,7 +553,7 @@ def solve(self, *args, **kwds): _model = arg # import suffixes must be on the top-level model - if isinstance(arg, _BlockData): + if isinstance(arg, BlockData): model_suffixes = list( name for ( @@ -641,9 +642,9 @@ def solve(self, *args, **kwds): result.solution(0).symbol_map = getattr( _model, "._symbol_maps" )[result._smap_id] - result.solution( - 0 - ).default_variable_value = self._default_variable_value + result.solution(0).default_variable_value = ( + self._default_variable_value + ) if self._load_solutions: _model.load_solution(result.solution(0)) else: @@ -699,12 +700,10 @@ def _presolve(self, *args, **kwds): if self._problem_format: write_start_time = time.time() - ( - self._problem_files, - self._problem_format, - self._smap_id, - ) = self._convert_problem( - args, self._problem_format, self._valid_problem_formats, **kwds + (self._problem_files, self._problem_format, self._smap_id) = ( + self._convert_problem( + args, self._problem_format, self._valid_problem_formats, **kwds + ) ) total_time = time.time() - write_start_time if self._report_timing: diff --git a/pyomo/opt/parallel/__init__.py b/pyomo/opt/parallel/__init__.py index 9820f39afd4..dbfdf2302ca 100644 --- a/pyomo/opt/parallel/__init__.py +++ b/pyomo/opt/parallel/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/parallel/async_solver.py b/pyomo/opt/parallel/async_solver.py index e9806b7125a..74e222e2241 100644 --- a/pyomo/opt/parallel/async_solver.py +++ b/pyomo/opt/parallel/async_solver.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,9 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ - -__all__ = ['AsynchronousSolverManager', 'SolverManagerFactory'] - from pyomo.common import Factory from pyomo.opt.parallel.manager import AsynchronousActionManager diff --git a/pyomo/opt/parallel/local.py b/pyomo/opt/parallel/local.py index a7a80a7d33c..e130ea0407f 100644 --- a/pyomo/opt/parallel/local.py +++ b/pyomo/opt/parallel/local.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,9 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ - -__all__ = () - import time from pyomo.common.collections import OrderedDict diff --git a/pyomo/opt/parallel/manager.py b/pyomo/opt/parallel/manager.py index a97f6ae1d27..203c348e119 100644 --- a/pyomo/opt/parallel/manager.py +++ b/pyomo/opt/parallel/manager.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,16 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ - -__all__ = [ - 'ActionManagerError', - 'ActionHandle', - 'AsynchronousActionManager', - 'ActionStatus', - 'FailedActionHandle', - 'solve_all_instances', -] - import enum diff --git a/pyomo/opt/plugins/__init__.py b/pyomo/opt/plugins/__init__.py index 797147f5f69..5ea2490b534 100644 --- a/pyomo/opt/plugins/__init__.py +++ b/pyomo/opt/plugins/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/plugins/driver.py b/pyomo/opt/plugins/driver.py index 23757053beb..c7c7103835c 100644 --- a/pyomo/opt/plugins/driver.py +++ b/pyomo/opt/plugins/driver.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/plugins/res.py b/pyomo/opt/plugins/res.py index 25d25d5feb0..31971ee7d25 100644 --- a/pyomo/opt/plugins/res.py +++ b/pyomo/opt/plugins/res.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/plugins/sol.py b/pyomo/opt/plugins/sol.py index 6e1ca666633..10da469f186 100644 --- a/pyomo/opt/plugins/sol.py +++ b/pyomo/opt/plugins/sol.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -241,9 +241,9 @@ def _load(self, fin, res, soln, suffixes): translated_suffix_name = ( suffix_name[0].upper() + suffix_name[1:] ) - soln_constraint[key][ - translated_suffix_name - ] = convert_function(suf_line[1]) + soln_constraint[key][translated_suffix_name] = ( + convert_function(suf_line[1]) + ) elif kind == 2: # Obj for cnt in range(nvalues): suf_line = fin.readline().split() diff --git a/pyomo/opt/problem/__init__.py b/pyomo/opt/problem/__init__.py index 1b1a5328beb..8199553247d 100644 --- a/pyomo/opt/problem/__init__.py +++ b/pyomo/opt/problem/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/problem/ampl.py b/pyomo/opt/problem/ampl.py index 625c342f005..ed107cace60 100644 --- a/pyomo/opt/problem/ampl.py +++ b/pyomo/opt/problem/ampl.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -14,8 +14,6 @@ can be optimized with the Acro COLIN optimizers. """ -__all__ = ['AmplModel'] - import os from pyomo.opt.base import ProblemFormat, convert_problem, guess_format diff --git a/pyomo/opt/results/__init__.py b/pyomo/opt/results/__init__.py index 8b2933adfe0..64a1b42ac86 100644 --- a/pyomo/opt/results/__init__.py +++ b/pyomo/opt/results/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/results/container.py b/pyomo/opt/results/container.py index 98a68048b45..4bbaf44edf7 100644 --- a/pyomo/opt/results/container.py +++ b/pyomo/opt/results/container.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,24 +9,12 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = [ - 'UndefinedData', - 'undefined', - 'ignore', - 'ScalarData', - 'ListContainer', - 'MapContainer', - 'default_print_options', - 'ScalarType', -] - import copy - -from math import inf -from pyomo.common.collections import Bunch - import enum from io import StringIO +from math import inf + +from pyomo.common.collections import Bunch class ScalarType(str, enum.Enum): diff --git a/pyomo/opt/results/problem.py b/pyomo/opt/results/problem.py index 71fd748dd81..a8eca1e3b41 100644 --- a/pyomo/opt/results/problem.py +++ b/pyomo/opt/results/problem.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,24 +9,19 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['ProblemInformation', 'ProblemSense'] - import enum from pyomo.opt.results.container import MapContainer +from pyomo.common.enums import ExtendedEnumType, ObjectiveSense + + +class ProblemSense(enum.IntEnum, metaclass=ExtendedEnumType): + __base_enum__ = ObjectiveSense -class ProblemSense(str, enum.Enum): - unknown = 'unknown' - minimize = 'minimize' - maximize = 'maximize' + unknown = 0 - # Overloading __str__ is needed to match the behavior of the old - # pyutilib.enum class (removed June 2020). There are spots in the - # code base that expect the string representation for items in the - # enum to not include the class name. New uses of enum shouldn't - # need to do this. def __str__(self): - return self.value + return self.name class ProblemInformation(MapContainer): diff --git a/pyomo/opt/results/results_.py b/pyomo/opt/results/results_.py index 2852bb72e8a..0a045550517 100644 --- a/pyomo/opt/results/results_.py +++ b/pyomo/opt/results/results_.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['SolverResults'] - import math import sys import copy @@ -18,7 +16,7 @@ import logging import os.path -from pyomo.common.dependencies import yaml, yaml_load_args, yaml_available +from pyomo.common.dependencies import yaml, yaml_load_args import pyomo.opt from pyomo.opt.results.container import undefined, ignore, ListContainer, MapContainer import pyomo.opt.results.solution diff --git a/pyomo/opt/results/solution.py b/pyomo/opt/results/solution.py index 0cb8e92e730..6dcd348ea72 100644 --- a/pyomo/opt/results/solution.py +++ b/pyomo/opt/results/solution.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['SolutionStatus', 'Solution'] - import math import enum from pyomo.opt.results.container import MapContainer, ListContainer, ignore diff --git a/pyomo/opt/results/solver.py b/pyomo/opt/results/solver.py index 5f9ceb3b68e..d4cf46c38a9 100644 --- a/pyomo/opt/results/solver.py +++ b/pyomo/opt/results/solver.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,14 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = [ - 'SolverInformation', - 'SolverStatus', - 'TerminationCondition', - 'check_optimal_termination', - 'assert_optimal_termination', -] - import enum from pyomo.opt.results.container import MapContainer, ScalarType diff --git a/pyomo/opt/solver/__init__.py b/pyomo/opt/solver/__init__.py index 961d7e0edbd..6da73d408fa 100644 --- a/pyomo/opt/solver/__init__.py +++ b/pyomo/opt/solver/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/solver/ilmcmd.py b/pyomo/opt/solver/ilmcmd.py index d08feab7d9a..c956b2ed42f 100644 --- a/pyomo/opt/solver/ilmcmd.py +++ b/pyomo/opt/solver/ilmcmd.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['ILMLicensedSystemCallSolver'] - import re import sys import os diff --git a/pyomo/opt/solver/shellcmd.py b/pyomo/opt/solver/shellcmd.py index aad4298729a..baa0369e1d6 100644 --- a/pyomo/opt/solver/shellcmd.py +++ b/pyomo/opt/solver/shellcmd.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['SystemCallSolver'] - import os import sys import time @@ -62,6 +60,7 @@ def __init__(self, **kwargs): # a solver plugin may not report execution time. self._last_solve_time = None self._define_signal_handlers = None + self._version_timeout = 2 if executable is not None: self.set_executable(name=executable, validate=validate) @@ -260,7 +259,7 @@ def _apply_solver(self): print("Solver log file: '%s'" % self._log_file) if self._soln_file is not None: print("Solver solution file: '%s'" % self._soln_file) - if self._problem_files is not []: + if self._problem_files != []: print("Solver problem files: %s" % str(self._problem_files)) sys.stdout.flush() diff --git a/pyomo/opt/testing/__init__.py b/pyomo/opt/testing/__init__.py index 5d0d8ebd8d7..37ed419fbe3 100644 --- a/pyomo/opt/testing/__init__.py +++ b/pyomo/opt/testing/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/testing/pyunit.py b/pyomo/opt/testing/pyunit.py index 527b72cec7a..bb96806d520 100644 --- a/pyomo/opt/testing/pyunit.py +++ b/pyomo/opt/testing/pyunit.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,9 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ - -__all__ = ['TestCase'] - import sys import os import re diff --git a/pyomo/opt/tests/__init__.py b/pyomo/opt/tests/__init__.py index 65dc8785c9b..b333eb78878 100644 --- a/pyomo/opt/tests/__init__.py +++ b/pyomo/opt/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/tests/base/__init__.py b/pyomo/opt/tests/base/__init__.py index dbebb21e4f1..cde23945b56 100644 --- a/pyomo/opt/tests/base/__init__.py +++ b/pyomo/opt/tests/base/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/tests/base/test_ampl.py b/pyomo/opt/tests/base/test_ampl.py index 1baffcbb0af..d37befcac57 100644 --- a/pyomo/opt/tests/base/test_ampl.py +++ b/pyomo/opt/tests/base/test_ampl.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/tests/base/test_convert.py b/pyomo/opt/tests/base/test_convert.py index f8f0bef0fe4..30a8fb0d1fc 100644 --- a/pyomo/opt/tests/base/test_convert.py +++ b/pyomo/opt/tests/base/test_convert.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/tests/base/test_factory.py b/pyomo/opt/tests/base/test_factory.py index ab2a64a6330..441ba245c5e 100644 --- a/pyomo/opt/tests/base/test_factory.py +++ b/pyomo/opt/tests/base/test_factory.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/tests/base/test_sol.py b/pyomo/opt/tests/base/test_sol.py index ff233b42a43..fada795b925 100644 --- a/pyomo/opt/tests/base/test_sol.py +++ b/pyomo/opt/tests/base/test_sol.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/tests/base/test_soln.py b/pyomo/opt/tests/base/test_soln.py index 0511b3ceb9c..d39baeab15f 100644 --- a/pyomo/opt/tests/base/test_soln.py +++ b/pyomo/opt/tests/base/test_soln.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/tests/base/test_solver.py b/pyomo/opt/tests/base/test_solver.py index 73d6067efe4..8ffc647804d 100644 --- a/pyomo/opt/tests/base/test_solver.py +++ b/pyomo/opt/tests/base/test_solver.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/tests/solver/__init__.py b/pyomo/opt/tests/solver/__init__.py index 4c145a1b507..d27a8ab41d6 100644 --- a/pyomo/opt/tests/solver/__init__.py +++ b/pyomo/opt/tests/solver/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/tests/solver/test_shellcmd.py b/pyomo/opt/tests/solver/test_shellcmd.py index f71fcf07c6d..b6cc264b8f7 100644 --- a/pyomo/opt/tests/solver/test_shellcmd.py +++ b/pyomo/opt/tests/solver/test_shellcmd.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/pysp/__init__.py b/pyomo/pysp/__init__.py index 3fb4abbbd42..bb8a401e45e 100644 --- a/pyomo/pysp/__init__.py +++ b/pyomo/pysp/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/__init__.py b/pyomo/repn/__init__.py index 1b27071c404..842f4750127 100644 --- a/pyomo/repn/__init__.py +++ b/pyomo/repn/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/beta/__init__.py b/pyomo/repn/beta/__init__.py index fd7fac1125a..a75a75ec760 100644 --- a/pyomo/repn/beta/__init__.py +++ b/pyomo/repn/beta/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/beta/matrix.py b/pyomo/repn/beta/matrix.py index ff2d6857bd6..0201c46eb18 100644 --- a/pyomo/repn/beta/matrix.py +++ b/pyomo/repn/beta/matrix.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,12 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ( - "_LinearConstraintData", - "MatrixConstraint", - "compile_block_linear_constraints", -) - import time import logging import array @@ -30,7 +24,7 @@ Constraint, IndexedConstraint, ScalarConstraint, - _ConstraintData, + ConstraintData, ) from pyomo.core.expr.numvalue import native_numeric_types from pyomo.repn import generate_standard_repn @@ -253,7 +247,7 @@ def _get_bound(exp): constraint_containers_removed += 1 for constraint, index in constraint_data_to_remove: # Note that this del is not needed: assigning Constraint.Skip - # above removes the _ConstraintData from the _data dict. + # above removes the ConstraintData from the _data dict. # del constraint[index] constraints_removed += 1 for block, constraint in constraint_containers_to_remove: @@ -354,12 +348,12 @@ def _get_bound(exp): ) -# class _LinearConstraintData(_ConstraintData,LinearCanonicalRepn): +# class _LinearConstraintData(ConstraintData,LinearCanonicalRepn): # # This change breaks this class, but it's unclear whether this # is being used... # -class _LinearConstraintData(_ConstraintData): +class _LinearConstraintData(ConstraintData): """ This class defines the data for a single linear constraint in canonical form. @@ -399,7 +393,7 @@ def __init__(self, index, component=None): # # These lines represent in-lining of the # following constructors: - # - _ConstraintData, + # - ConstraintData, # - ActiveComponentData # - ComponentData self._component = weakref_ref(component) if (component is not None) else None @@ -448,7 +442,7 @@ def __init__(self, index, component=None): # These lines represent in-lining of the # following constructors: # - _LinearConstraintData - # - _ConstraintData, + # - ConstraintData, # - ActiveComponentData # - ComponentData self._component = weakref_ref(component) if (component is not None) else None @@ -590,7 +584,7 @@ def constant(self): return sum(terms) # - # Abstract Interface (_ConstraintData) + # Abstract Interface (ConstraintData) # @property diff --git a/pyomo/repn/linear.py b/pyomo/repn/linear.py index 8bffbf1d49b..6d084067511 100644 --- a/pyomo/repn/linear.py +++ b/pyomo/repn/linear.py @@ -1,21 +1,25 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain # rights in this software. # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import collections + import logging import sys from operator import itemgetter from itertools import filterfalse from pyomo.common.deprecation import deprecation_warning -from pyomo.common.numeric_types import native_types, native_numeric_types +from pyomo.common.numeric_types import ( + native_types, + native_numeric_types, + native_complex_types, +) from pyomo.core.expr.numeric_expr import ( NegationExpression, ProductExpression, @@ -27,8 +31,8 @@ MonomialTermExpression, LinearExpression, SumExpression, - NPV_SumExpression, ExternalFunctionExpression, + mutable_expression, ) from pyomo.core.expr.relational_expr import ( EqualityExpression, @@ -37,10 +41,11 @@ ) from pyomo.core.expr.visitor import StreamBasedExpressionVisitor, _EvaluationVisitor from pyomo.core.expr import is_fixed, value -from pyomo.core.base.expression import ScalarExpression, _GeneralExpressionData -from pyomo.core.base.objective import ScalarObjective, _GeneralObjectiveData +from pyomo.core.base.expression import Expression import pyomo.core.kernel as kernel from pyomo.repn.util import ( + BeforeChildDispatcher, + ExitNodeDispatcher, ExprType, InvalidNumber, apply_node_operation, @@ -115,22 +120,14 @@ def to_expression(self, visitor): ans = 0 if self.linear: var_map = visitor.var_map - if len(self.linear) == 1: - vid, coef = next(iter(self.linear.items())) - if coef == 1: - ans += var_map[vid] - elif coef: - ans += MonomialTermExpression((coef, var_map[vid])) - else: - pass - else: - ans += LinearExpression( - [ - MonomialTermExpression((coef, var_map[vid])) - for vid, coef in self.linear.items() - if coef - ] - ) + with mutable_expression() as e: + for vid, coef in self.linear.items(): + if coef: + e += coef * var_map[vid] + if e.nargs() > 1: + ans += e + elif e.nargs() == 1: + ans += e.arg(0) if self.constant: ans += self.constant if self.multiplier != 1: @@ -203,9 +200,8 @@ def _handle_negation_ANY(visitor, node, arg): _exit_node_handlers[NegationExpression] = { + None: _handle_negation_ANY, (_CONSTANT,): _handle_negation_constant, - (_LINEAR,): _handle_negation_ANY, - (_GENERAL,): _handle_negation_ANY, } # @@ -214,20 +210,18 @@ def _handle_negation_ANY(visitor, node, arg): def _handle_product_constant_constant(visitor, node, arg1, arg2): - _, arg1 = arg1 - _, arg2 = arg2 - ans = arg1 * arg2 + ans = arg1[1] * arg2[1] if ans != ans: - if not arg1 or not arg2: + if not arg1[1] or not arg2[1]: deprecation_warning( - f"Encountered {str(arg1)}*{str(arg2)} in expression tree. " + f"Encountered {str(arg1[1])}*{str(arg2[1])} in expression tree. " "Mapping the NaN result to 0 for compatibility " "with the lp_v1 writer. In the future, this NaN " "will be preserved/emitted to comply with IEEE-754.", version='6.6.0', ) - return _, 0 - return _, arg1 * arg2 + return _CONSTANT, 0 + return _CONSTANT, ans def _handle_product_constant_ANY(visitor, node, arg1, arg2): @@ -279,15 +273,12 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): _exit_node_handlers[ProductExpression] = { + None: _handle_product_nonlinear, (_CONSTANT, _CONSTANT): _handle_product_constant_constant, (_CONSTANT, _LINEAR): _handle_product_constant_ANY, (_CONSTANT, _GENERAL): _handle_product_constant_ANY, (_LINEAR, _CONSTANT): _handle_product_ANY_constant, - (_LINEAR, _LINEAR): _handle_product_nonlinear, - (_LINEAR, _GENERAL): _handle_product_nonlinear, (_GENERAL, _CONSTANT): _handle_product_ANY_constant, - (_GENERAL, _LINEAR): _handle_product_nonlinear, - (_GENERAL, _GENERAL): _handle_product_nonlinear, } _exit_node_handlers[MonomialTermExpression] = _exit_node_handlers[ProductExpression] @@ -301,7 +292,7 @@ def _handle_division_constant_constant(visitor, node, arg1, arg2): def _handle_division_ANY_constant(visitor, node, arg1, arg2): - arg1[1].multiplier /= arg2[1] + arg1[1].multiplier = apply_node_operation(node, (arg1[1].multiplier, arg2[1])) return arg1 @@ -312,15 +303,10 @@ def _handle_division_nonlinear(visitor, node, arg1, arg2): _exit_node_handlers[DivisionExpression] = { + None: _handle_division_nonlinear, (_CONSTANT, _CONSTANT): _handle_division_constant_constant, - (_CONSTANT, _LINEAR): _handle_division_nonlinear, - (_CONSTANT, _GENERAL): _handle_division_nonlinear, (_LINEAR, _CONSTANT): _handle_division_ANY_constant, - (_LINEAR, _LINEAR): _handle_division_nonlinear, - (_LINEAR, _GENERAL): _handle_division_nonlinear, (_GENERAL, _CONSTANT): _handle_division_ANY_constant, - (_GENERAL, _LINEAR): _handle_division_nonlinear, - (_GENERAL, _GENERAL): _handle_division_nonlinear, } # @@ -328,10 +314,9 @@ def _handle_division_nonlinear(visitor, node, arg1, arg2): # -def _handle_pow_constant_constant(visitor, node, *args): - arg1, arg2 = args +def _handle_pow_constant_constant(visitor, node, arg1, arg2): ans = apply_node_operation(node, (arg1[1], arg2[1])) - if ans.__class__ in _complex_types: + if ans.__class__ in native_complex_types: ans = complex_number_error(ans, visitor, node) return _CONSTANT, ans @@ -361,15 +346,10 @@ def _handle_pow_nonlinear(visitor, node, arg1, arg2): _exit_node_handlers[PowExpression] = { + None: _handle_pow_nonlinear, (_CONSTANT, _CONSTANT): _handle_pow_constant_constant, - (_CONSTANT, _LINEAR): _handle_pow_nonlinear, - (_CONSTANT, _GENERAL): _handle_pow_nonlinear, (_LINEAR, _CONSTANT): _handle_pow_ANY_constant, - (_LINEAR, _LINEAR): _handle_pow_nonlinear, - (_LINEAR, _GENERAL): _handle_pow_nonlinear, (_GENERAL, _CONSTANT): _handle_pow_ANY_constant, - (_GENERAL, _LINEAR): _handle_pow_nonlinear, - (_GENERAL, _GENERAL): _handle_pow_nonlinear, } # @@ -380,7 +360,7 @@ def _handle_pow_nonlinear(visitor, node, arg1, arg2): def _handle_unary_constant(visitor, node, arg): ans = apply_node_operation(node, (arg[1],)) # Unary includes sqrt() which can return complex numbers - if ans.__class__ in _complex_types: + if ans.__class__ in native_complex_types: ans = complex_number_error(ans, visitor, node) return _CONSTANT, ans @@ -392,9 +372,8 @@ def _handle_unary_nonlinear(visitor, node, arg): _exit_node_handlers[UnaryFunctionExpression] = { + None: _handle_unary_nonlinear, (_CONSTANT,): _handle_unary_constant, - (_LINEAR,): _handle_unary_nonlinear, - (_GENERAL,): _handle_unary_nonlinear, } _exit_node_handlers[AbsExpression] = _exit_node_handlers[UnaryFunctionExpression] @@ -416,23 +395,11 @@ def _handle_named_ANY(visitor, node, arg1): return _type, arg1.duplicate() -_exit_node_handlers[ScalarExpression] = { +_exit_node_handlers[Expression] = { + None: _handle_named_ANY, (_CONSTANT,): _handle_named_constant, - (_LINEAR,): _handle_named_ANY, - (_GENERAL,): _handle_named_ANY, } -_named_subexpression_types = [ - ScalarExpression, - _GeneralExpressionData, - kernel.expression.expression, - kernel.expression.noclone, - # Note: objectives are special named expressions - _GeneralObjectiveData, - ScalarObjective, - kernel.objective.objective, -] - # # EXPR_IF handlers # @@ -463,12 +430,7 @@ def _handle_expr_if_nonlinear(visitor, node, arg1, arg2, arg3): return _GENERAL, ans -_exit_node_handlers[Expr_ifExpression] = { - (i, j, k): _handle_expr_if_nonlinear - for i in (_LINEAR, _GENERAL) - for j in (_CONSTANT, _LINEAR, _GENERAL) - for k in (_CONSTANT, _LINEAR, _GENERAL) -} +_exit_node_handlers[Expr_ifExpression] = {None: _handle_expr_if_nonlinear} for j in (_CONSTANT, _LINEAR, _GENERAL): for k in (_CONSTANT, _LINEAR, _GENERAL): _exit_node_handlers[Expr_ifExpression][_CONSTANT, j, k] = _handle_expr_if_const @@ -501,11 +463,9 @@ def _handle_equality_general(visitor, node, arg1, arg2): _exit_node_handlers[EqualityExpression] = { - (i, j): _handle_equality_general - for i in (_CONSTANT, _LINEAR, _GENERAL) - for j in (_CONSTANT, _LINEAR, _GENERAL) + None: _handle_equality_general, + (_CONSTANT, _CONSTANT): _handle_equality_const, } -_exit_node_handlers[EqualityExpression][_CONSTANT, _CONSTANT] = _handle_equality_const def _handle_inequality_const(visitor, node, arg1, arg2): @@ -531,13 +491,9 @@ def _handle_inequality_general(visitor, node, arg1, arg2): _exit_node_handlers[InequalityExpression] = { - (i, j): _handle_inequality_general - for i in (_CONSTANT, _LINEAR, _GENERAL) - for j in (_CONSTANT, _LINEAR, _GENERAL) + None: _handle_inequality_general, + (_CONSTANT, _CONSTANT): _handle_inequality_const, } -_exit_node_handlers[InequalityExpression][ - _CONSTANT, _CONSTANT -] = _handle_inequality_const def _handle_ranged_const(visitor, node, arg1, arg2, arg3): @@ -568,287 +524,237 @@ def _handle_ranged_general(visitor, node, arg1, arg2, arg3): _exit_node_handlers[RangedExpression] = { - (i, j, k): _handle_ranged_general - for i in (_CONSTANT, _LINEAR, _GENERAL) - for j in (_CONSTANT, _LINEAR, _GENERAL) - for k in (_CONSTANT, _LINEAR, _GENERAL) + None: _handle_ranged_general, + (_CONSTANT, _CONSTANT, _CONSTANT): _handle_ranged_const, } -_exit_node_handlers[RangedExpression][ - _CONSTANT, _CONSTANT, _CONSTANT -] = _handle_ranged_const - -def _before_native(visitor, child): - return False, (_CONSTANT, child) - - -def _before_invalid(visitor, child): - return False, ( - _CONSTANT, - InvalidNumber(child, "'{child}' is not a valid numeric type"), - ) - -def _before_complex(visitor, child): - return False, (_CONSTANT, complex_number_error(child, visitor, child)) - - -def _before_var(visitor, child): - _id = id(child) - if _id not in visitor.var_map: - if child.fixed: - return False, (_CONSTANT, visitor._eval_fixed(child)) - visitor.var_map[_id] = child - visitor.var_order[_id] = len(visitor.var_order) - ans = visitor.Result() - ans.linear[_id] = 1 - return False, (_LINEAR, ans) - - -def _before_param(visitor, child): - return False, (_CONSTANT, visitor._eval_fixed(child)) - - -def _before_npv(visitor, child): - try: - return False, (_CONSTANT, visitor._eval_expr(child)) - except (ValueError, ArithmeticError): - return True, None - - -def _before_monomial(visitor, child): - # - # The following are performance optimizations for common - # situations (Monomial terms and Linear expressions) - # - arg1, arg2 = child._args_ - if arg1.__class__ not in native_types: +class LinearBeforeChildDispatcher(BeforeChildDispatcher): + def __init__(self): + # Special handling for external functions: will be handled + # as terminal nodes from the point of view of the visitor + self[ExternalFunctionExpression] = self._before_external + # Special linear / summation expressions + self[MonomialTermExpression] = self._before_monomial + self[LinearExpression] = self._before_linear + self[SumExpression] = self._before_general_expression + + @staticmethod + def _record_var(visitor, var): + # We always add all indices to the var_map at once so that + # we can honor deterministic ordering of unordered sets + # (because the user could have iterated over an unordered + # set when constructing an expression, thereby altering the + # order in which we would see the variables) + vm = visitor.var_map + vo = visitor.var_order + l = len(vo) try: - arg1 = visitor._eval_expr(arg1) - except (ValueError, ArithmeticError): - return True, None - - # We want to check / update the var_map before processing "0" - # coefficients so that we are consistent with what gets added to the - # var_map (e.g., 0*x*y: y is processed by _before_var and will - # always be added, but x is processed here) - _id = id(arg2) - if _id not in visitor.var_map: - if arg2.fixed: - return False, (_CONSTANT, arg1 * visitor._eval_fixed(arg2)) - visitor.var_map[_id] = arg2 - visitor.var_order[_id] = len(visitor.var_order) - - # Trap multiplication by 0 and nan. - if not arg1: - if arg2.fixed: - arg2 = visitor._eval_fixed(arg2) - if arg2 != arg2: - deprecation_warning( - f"Encountered {arg1}*{str(arg2.value)} in expression " - "tree. Mapping the NaN result to 0 for compatibility " - "with the lp_v1 writer. In the future, this NaN " - "will be preserved/emitted to comply with IEEE-754.", - version='6.6.0', - ) - return False, (_CONSTANT, arg1) - - ans = visitor.Result() - ans.linear[_id] = arg1 - return False, (_LINEAR, ans) - - -def _before_linear(visitor, child): - var_map = visitor.var_map - var_order = visitor.var_order - next_i = len(var_order) - ans = visitor.Result() - const = 0 - linear = ans.linear - for arg in child.args: - if arg.__class__ is MonomialTermExpression: - arg1, arg2 = arg._args_ - if arg1.__class__ not in native_types: - try: - arg1 = visitor._eval_expr(arg1) - except (ValueError, ArithmeticError): - return True, None - - # Trap multiplication by 0 and nan. - if not arg1: - if arg2.fixed: - arg2 = visitor._eval_fixed(arg2) - if arg2 != arg2: - deprecation_warning( - f"Encountered {arg1}*{str(arg2.value)} in expression " - "tree. Mapping the NaN result to 0 for compatibility " - "with the lp_v1 writer. In the future, this NaN " - "will be preserved/emitted to comply with IEEE-754.", - version='6.6.0', - ) + _iter = var.parent_component().values(visitor.sorter) + except AttributeError: + # Note that this only works for the AML, as kernel does not + # provide a parent_component() + _iter = (var,) + for v in _iter: + if v.fixed: continue + vid = id(v) + vm[vid] = v + vo[vid] = l + l += 1 + + @staticmethod + def _before_var(visitor, child): + _id = id(child) + if _id not in visitor.var_map: + if child.fixed: + return False, (_CONSTANT, visitor.check_constant(child.value, child)) + LinearBeforeChildDispatcher._record_var(visitor, child) + ans = visitor.Result() + ans.linear[_id] = 1 + return False, (_LINEAR, ans) - _id = id(arg2) - if _id not in var_map: - if arg2.fixed: - const += arg1 * visitor._eval_fixed(arg2) - continue - var_map[_id] = arg2 - var_order[_id] = next_i - next_i += 1 - linear[_id] = arg1 - elif _id in linear: - linear[_id] += arg1 - else: - linear[_id] = arg1 - elif arg.__class__ in native_numeric_types: - const += arg - else: + @staticmethod + def _before_monomial(visitor, child): + # + # The following are performance optimizations for common + # situations (Monomial terms and Linear expressions) + # + arg1, arg2 = child._args_ + if arg1.__class__ not in native_types: try: - const += visitor._eval_expr(arg) + arg1 = visitor.check_constant(visitor.evaluate(arg1), arg1) except (ValueError, ArithmeticError): return True, None - if linear: - ans.constant = const - return False, (_LINEAR, ans) - else: - return False, (_CONSTANT, const) - -def _before_named_expression(visitor, child): - _id = id(child) - if _id in visitor.subexpression_cache: - _type, expr = visitor.subexpression_cache[_id] - if _type is _CONSTANT: - return False, (_type, expr) - else: - return False, (_type, expr.duplicate()) - else: - return True, None - - -def _before_external(visitor, child): - ans = visitor.Result() - if all(is_fixed(arg) for arg in child.args): - try: - ans.constant = visitor._eval_expr(child) - return False, (_CONSTANT, ans) - except: - pass - ans.nonlinear = child - return False, (_GENERAL, ans) + # We want to check / update the var_map before processing "0" + # coefficients so that we are consistent with what gets added to the + # var_map (e.g., 0*x*y: y is processed by _before_var and will + # always be added, but x is processed here) + _id = id(arg2) + if _id not in visitor.var_map: + if arg2.fixed: + return False, ( + _CONSTANT, + arg1 * visitor.check_constant(arg2.value, arg2), + ) + LinearBeforeChildDispatcher._record_var(visitor, arg2) + # Trap multiplication by 0 and nan. + if not arg1: + if arg2.fixed: + arg2 = visitor.check_constant(arg2.value, arg2) + if arg2 != arg2: + deprecation_warning( + f"Encountered {arg1}*{str(arg2.value)} in expression " + "tree. Mapping the NaN result to 0 for compatibility " + "with the lp_v1 writer. In the future, this NaN " + "will be preserved/emitted to comply with IEEE-754.", + version='6.6.0', + ) + return False, (_CONSTANT, arg1) -def _before_general_expression(visitor, child): - return True, None + ans = visitor.Result() + ans.linear[_id] = arg1 + return False, (_LINEAR, ans) + @staticmethod + def _before_linear(visitor, child): + var_map = visitor.var_map + var_order = visitor.var_order + ans = visitor.Result() + const = 0 + linear = ans.linear + for arg in child.args: + if arg.__class__ is MonomialTermExpression: + arg1, arg2 = arg._args_ + if arg1.__class__ not in native_types: + try: + arg1 = visitor.check_constant(visitor.evaluate(arg1), arg1) + except (ValueError, ArithmeticError): + return True, None + + # Trap multiplication by 0 and nan. + if not arg1: + if arg2.fixed: + arg2 = visitor.check_constant(arg2.value, arg2) + if arg2 != arg2: + deprecation_warning( + f"Encountered {arg1}*{str(arg2.value)} in expression " + "tree. Mapping the NaN result to 0 for compatibility " + "with the lp_v1 writer. In the future, this NaN " + "will be preserved/emitted to comply with IEEE-754.", + version='6.6.0', + ) + continue -def _register_new_before_child_dispatcher(visitor, child): - dispatcher = _before_child_dispatcher - child_type = child.__class__ - if child_type in native_numeric_types: - if issubclass(child_type, complex): - _complex_types.add(child_type) - dispatcher[child_type] = _before_complex + _id = id(arg2) + if _id not in var_map: + if arg2.fixed: + const += arg1 * visitor.check_constant(arg2.value, arg2) + continue + LinearBeforeChildDispatcher._record_var(visitor, arg2) + linear[_id] = arg1 + elif _id in linear: + linear[_id] += arg1 + else: + linear[_id] = arg1 + elif arg.__class__ in native_numeric_types: + const += arg + elif arg.is_variable_type(): + _id = id(arg) + if _id not in var_map: + if arg.fixed: + const += visitor.check_constant(arg.value, arg) + continue + LinearBeforeChildDispatcher._record_var(visitor, arg) + linear[_id] = 1 + elif _id in linear: + linear[_id] += 1 + else: + linear[_id] = 1 + else: + try: + const += visitor.check_constant(visitor.evaluate(arg), arg) + except (ValueError, ArithmeticError): + return True, None + if linear: + ans.constant = const + return False, (_LINEAR, ans) else: - dispatcher[child_type] = _before_native - elif child_type in native_types: - dispatcher[child_type] = _before_invalid - elif not child.is_expression_type(): - if child.is_potentially_variable(): - dispatcher[child_type] = _before_var + return False, (_CONSTANT, const) + + @staticmethod + def _before_named_expression(visitor, child): + _id = id(child) + if _id in visitor.subexpression_cache: + _type, expr = visitor.subexpression_cache[_id] + if _type is _CONSTANT: + return False, (_type, expr) + else: + return False, (_type, expr.duplicate()) else: - dispatcher[child_type] = _before_param - elif not child.is_potentially_variable(): - dispatcher[child_type] = _before_npv - # If we descend into the named expression (because of an - # evaluation error), then on the way back out, we will use - # the potentially variable handler to process the result. - pv_base_type = child.potentially_variable_base_class() - if pv_base_type not in dispatcher: - try: - child.__class__ = pv_base_type - _register_new_before_child_dispatcher(visitor, child) - finally: - child.__class__ = child_type - if pv_base_type in visitor.exit_node_handlers: - visitor.exit_node_handlers[child_type] = visitor.exit_node_handlers[ - pv_base_type - ] - for args, fcn in visitor.exit_node_handlers[child_type].items(): - visitor.exit_node_dispatcher[(child_type, *args)] = fcn - elif id(child) in visitor.subexpression_cache or issubclass( - child_type, _GeneralExpressionData - ): - dispatcher[child_type] = _before_named_expression - visitor.exit_node_handlers[child_type] = visitor.exit_node_handlers[ - ScalarExpression - ] - for args, fcn in visitor.exit_node_handlers[child_type].items(): - visitor.exit_node_dispatcher[(child_type, *args)] = fcn - else: - dispatcher[child_type] = _before_general_expression - return dispatcher[child_type](visitor, child) - + return True, None -_before_child_dispatcher = collections.defaultdict( - lambda: _register_new_before_child_dispatcher -) + @staticmethod + def _before_external(visitor, child): + ans = visitor.Result() + if all(is_fixed(arg) for arg in child.args): + try: + ans.constant = visitor.check_constant(visitor.evaluate(child), child) + return False, (_CONSTANT, ans) + except: + pass + ans.nonlinear = child + return False, (_GENERAL, ans) -# For efficiency reasons, we will maintain a separate list of all -# complex number types -_complex_types = set((complex,)) -# We do not support writing complex numbers out -_before_child_dispatcher[complex] = _before_complex -# Special handling for external functions: will be handled -# as terminal nodes from the point of view of the visitor -_before_child_dispatcher[ExternalFunctionExpression] = _before_external -# Special linear / summation expressions -_before_child_dispatcher[MonomialTermExpression] = _before_monomial -_before_child_dispatcher[LinearExpression] = _before_linear -_before_child_dispatcher[SumExpression] = _before_general_expression +_before_child_dispatcher = LinearBeforeChildDispatcher() # # Initialize the _exit_node_dispatcher # def _initialize_exit_node_dispatcher(exit_handlers): - # expand the knowns set of named expressiosn - for expr in _named_subexpression_types: - exit_handlers[expr] = exit_handlers[ScalarExpression] - exit_dispatcher = {} for cls, handlers in exit_handlers.items(): for args, fcn in handlers.items(): - exit_dispatcher[(cls, *args)] = fcn + if args is None: + exit_dispatcher[cls] = fcn + else: + exit_dispatcher[(cls, *args)] = fcn return exit_dispatcher class LinearRepnVisitor(StreamBasedExpressionVisitor): Result = LinearRepn exit_node_handlers = _exit_node_handlers - exit_node_dispatcher = _initialize_exit_node_dispatcher(_exit_node_handlers) + exit_node_dispatcher = ExitNodeDispatcher( + _initialize_exit_node_dispatcher(_exit_node_handlers) + ) expand_nonlinear_products = False max_exponential_expansion = 1 - def __init__(self, subexpression_cache, var_map, var_order): + def __init__(self, subexpression_cache, var_map, var_order, sorter): super().__init__() self.subexpression_cache = subexpression_cache self.var_map = var_map self.var_order = var_order + self.sorter = sorter self._eval_expr_visitor = _EvaluationVisitor(True) + self.evaluate = self._eval_expr_visitor.dfs_postorder_stack - def _eval_fixed(self, obj): - ans = obj.value + def check_constant(self, ans, obj): if ans.__class__ not in native_numeric_types: # None can be returned from uninitialized Var/Param objects if ans is None: return InvalidNumber( - None, f"'{obj}' contains a nonnumeric value '{ans}'" + None, f"'{obj}' evaluated to a nonnumeric value '{ans}'" ) if ans.__class__ is InvalidNumber: return ans + elif ans.__class__ in native_complex_types: + return complex_number_error(ans, self, obj) else: # It is possible to get other non-numeric types. Most # common are bool and 1-element numpy.array(). We will @@ -862,43 +768,12 @@ def _eval_fixed(self, obj): ans = float(ans) except: return InvalidNumber( - ans, f"'{obj}' contains a nonnumeric value '{ans}'" + ans, f"'{obj}' evaluated to a nonnumeric value '{ans}'" ) if ans != ans: - return InvalidNumber(nan, f"'{obj}' contains a nonnumeric value '{ans}'") - if ans.__class__ in _complex_types: - return complex_number_error(ans, self, obj) - return ans - - def _eval_expr(self, expr): - ans = self._eval_expr_visitor.dfs_postorder_stack(expr) - if ans.__class__ not in native_numeric_types: - # None can be returned from uninitialized Expression objects - if ans is None: - return InvalidNumber( - ans, f"'{expr}' evaluated to nonnumeric value '{ans}'" - ) - if ans.__class__ is InvalidNumber: - return ans - else: - # It is possible to get other non-numeric types. Most - # common are bool and 1-element numpy.array(). We will - # attempt to convert the value to a float before - # proceeding. - # - # TODO: we should check bool and warn/error (while bool is - # convertible to float in Python, they have very - # different semantic meanings in Pyomo). - try: - ans = float(ans) - except: - return InvalidNumber( - ans, f"'{expr}' evaluated to nonnumeric value '{ans}'" - ) - if ans != ans: - return InvalidNumber(ans, f"'{expr}' evaluated to nonnumeric value '{ans}'") - if ans.__class__ in _complex_types: - return complex_number_error(ans, self, expr) + return InvalidNumber( + nan, f"'{obj}' evaluated to a nonnumeric value '{ans}'" + ) return ans def initializeWalker(self, expr): diff --git a/pyomo/repn/plugins/__init__.py b/pyomo/repn/plugins/__init__.py index f1e8270b8c7..d3804c55106 100644 --- a/pyomo/repn/plugins/__init__.py +++ b/pyomo/repn/plugins/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -18,6 +18,7 @@ def load(): import pyomo.repn.plugins.gams_writer import pyomo.repn.plugins.lp_writer import pyomo.repn.plugins.nl_writer + import pyomo.repn.plugins.standard_form from pyomo.opt import WriterFactory diff --git a/pyomo/repn/plugins/ampl/__init__.py b/pyomo/repn/plugins/ampl/__init__.py index 493bc06d9c4..d935056c90b 100644 --- a/pyomo/repn/plugins/ampl/__init__.py +++ b/pyomo/repn/plugins/ampl/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/plugins/ampl/ampl_.py b/pyomo/repn/plugins/ampl/ampl_.py index a2bd55cb73a..cc99e9cfdae 100644 --- a/pyomo/repn/plugins/ampl/ampl_.py +++ b/pyomo/repn/plugins/ampl/ampl_.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -13,8 +13,6 @@ # AMPL Problem Writer Plugin # -__all__ = ['ProblemWriter_nl'] - import itertools import logging import operator @@ -35,7 +33,7 @@ from pyomo.core.base import ( SymbolMap, NameLabeler, - _ExpressionData, + NamedExpressionData, SortComponents, var, param, @@ -170,11 +168,11 @@ def _build_op_template(): _op_template[EXPR.EqualityExpression] = "o24{C}\n" _op_comment[EXPR.EqualityExpression] = "\t#eq" - _op_template[var._VarData] = "v%d{C}\n" - _op_comment[var._VarData] = "\t#%s" + _op_template[var.VarData] = "v%d{C}\n" + _op_comment[var.VarData] = "\t#%s" - _op_template[param._ParamData] = "n%r{C}\n" - _op_comment[param._ParamData] = "" + _op_template[param.ParamData] = "n%r{C}\n" + _op_comment[param.ParamData] = "" _op_template[NumericConstant] = "n%r{C}\n" _op_comment[NumericConstant] = "" @@ -726,7 +724,7 @@ def _print_nonlinear_terms_NL(self, exp): self._print_nonlinear_terms_NL(exp.arg(0)) self._print_nonlinear_terms_NL(exp.arg(1)) - elif isinstance(exp, (_ExpressionData, IIdentityExpression)): + elif isinstance(exp, (NamedExpressionData, IIdentityExpression)): self._print_nonlinear_terms_NL(exp.expr) else: @@ -735,24 +733,24 @@ def _print_nonlinear_terms_NL(self, exp): % (exp_type) ) - elif isinstance(exp, (var._VarData, IVariable)) and (not exp.is_fixed()): + elif isinstance(exp, (var.VarData, IVariable)) and (not exp.is_fixed()): # (self._output_fixed_variable_bounds or if not self._symbolic_solver_labels: OUTPUT.write( - self._op_string[var._VarData] + self._op_string[var.VarData] % (self.ampl_var_id[self._varID_map[id(exp)]]) ) else: OUTPUT.write( - self._op_string[var._VarData] + self._op_string[var.VarData] % ( self.ampl_var_id[self._varID_map[id(exp)]], self._name_labeler(exp), ) ) - elif isinstance(exp, param._ParamData): - OUTPUT.write(self._op_string[param._ParamData] % (value(exp))) + elif isinstance(exp, param.ParamData): + OUTPUT.write(self._op_string[param.ParamData] % (value(exp))) elif isinstance(exp, NumericConstant) or exp.is_fixed(): OUTPUT.write(self._op_string[NumericConstant] % (value(exp))) @@ -1964,9 +1962,9 @@ def _print_model_NL( for obj_ID, (obj, wrapped_repn) in Objectives_dict.items(): grad_entries = {} for idx, obj_var in enumerate(wrapped_repn.linear_vars): - grad_entries[ - self_ampl_var_id[obj_var] - ] = wrapped_repn.repn.linear_coefs[idx] + grad_entries[self_ampl_var_id[obj_var]] = ( + wrapped_repn.repn.linear_coefs[idx] + ) for obj_var in wrapped_repn.nonlinear_vars: if obj_var not in wrapped_repn.linear_vars: grad_entries[self_ampl_var_id[obj_var]] = 0 diff --git a/pyomo/repn/plugins/baron_writer.py b/pyomo/repn/plugins/baron_writer.py index 4242ae7431c..ab673b0c1c3 100644 --- a/pyomo/repn/plugins/baron_writer.py +++ b/pyomo/repn/plugins/baron_writer.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -174,14 +174,27 @@ def _monomial_to_string(self, node): return self.smap.getSymbol(var) return ftoa(const, True) + '*' + self.smap.getSymbol(var) + def _var_to_string(self, node): + if node.is_fixed(): + return ftoa(node.value, True) + self.variables.add(id(node)) + return self.smap.getSymbol(node) + def _linear_to_string(self, node): values = [ - self._monomial_to_string(arg) - if ( - arg.__class__ is EXPR.MonomialTermExpression - and not arg.arg(1).is_fixed() + ( + self._monomial_to_string(arg) + if arg.__class__ is EXPR.MonomialTermExpression + else ( + ftoa(arg) + if arg.__class__ in native_numeric_types + else ( + self._var_to_string(arg) + if arg.is_variable_type() + else ftoa(value(arg), True) + ) + ) ) - else ftoa(value(arg)) for arg in node.args ] return node._to_string(values, False, self.smap) @@ -644,19 +657,18 @@ def _write_bar_file(self, model, output_file, solver_capability, io_options): # variables. # equation_section_stream = StringIO() - ( - referenced_variable_ids, - branching_priorities_suffixes, - ) = self._write_equations_section( - model, - equation_section_stream, - all_blocks_list, - active_components_data_var, - symbol_map, - c_labeler, - output_fixed_variable_bounds, - skip_trivial_constraints, - sorter, + (referenced_variable_ids, branching_priorities_suffixes) = ( + self._write_equations_section( + model, + equation_section_stream, + all_blocks_list, + active_components_data_var, + symbol_map, + c_labeler, + output_fixed_variable_bounds, + skip_trivial_constraints, + sorter, + ) ) # diff --git a/pyomo/repn/plugins/cpxlp.py b/pyomo/repn/plugins/cpxlp.py index cdcb4b42c3b..45f4279f8fe 100644 --- a/pyomo/repn/plugins/cpxlp.py +++ b/pyomo/repn/plugins/cpxlp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -60,7 +60,7 @@ def __init__(self): # The LP writer tracks which variables are # referenced in constraints, so that a user does not end up with a # zillion "unreferenced variables" warning messages. - # This dictionary maps id(_VarData) -> _VarData. + # This dictionary maps id(VarData) -> VarData. self._referenced_variable_ids = {} # Per ticket #4319, we are using %.17g, which mocks the @@ -374,7 +374,7 @@ def _print_expr_canonical( def printSOS(self, symbol_map, labeler, variable_symbol_map, soscondata, output): """ - Prints the SOS constraint associated with the _SOSConstraintData object + Prints the SOS constraint associated with the SOSConstraintData object """ sos_template_string = self.sos_template_string diff --git a/pyomo/repn/plugins/gams_writer.py b/pyomo/repn/plugins/gams_writer.py index de0e4684fc4..a0f407d7952 100644 --- a/pyomo/repn/plugins/gams_writer.py +++ b/pyomo/repn/plugins/gams_writer.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -180,9 +180,20 @@ def _monomial_to_string(self, node): def _linear_to_string(self, node): values = [ - self._monomial_to_string(arg) - if arg.__class__ is EXPR.MonomialTermExpression - else ftoa(arg, True) + ( + self._monomial_to_string(arg) + if arg.__class__ is EXPR.MonomialTermExpression + else ( + ftoa(arg, True) + if arg.__class__ in native_numeric_types + else ( + self.smap.getSymbol(arg) + if arg.is_variable_type() + and (not arg.fixed or self.output_fixed_variables) + else ftoa(value(arg), True) + ) + ) + ) for arg in node.args ] return node._to_string(values, False, self.smap) diff --git a/pyomo/repn/plugins/lp_writer.py b/pyomo/repn/plugins/lp_writer.py index fab94d313d5..627a54e3f68 100644 --- a/pyomo/repn/plugins/lp_writer.py +++ b/pyomo/repn/plugins/lp_writer.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -310,12 +310,13 @@ def write(self, model): _qp = self.config.allow_quadratic_objective _qc = self.config.allow_quadratic_constraint objective_visitor = (QuadraticRepnVisitor if _qp else LinearRepnVisitor)( - {}, var_map, self.var_order + {}, var_map, self.var_order, sorter ) constraint_visitor = (QuadraticRepnVisitor if _qc else LinearRepnVisitor)( objective_visitor.subexpression_cache if _qp == _qc else {}, var_map, self.var_order, + sorter, ) timer.toc('Initialized column order', level=logging.DEBUG) @@ -427,8 +428,6 @@ def write(self, model): # Pull out the constant: we will move it to the bounds offset = repn.constant - if offset.__class__ not in int_float: - offset = float(offset) repn.constant = 0 if repn.linear or getattr(repn, 'quadratic', None): @@ -584,8 +583,6 @@ def write_expression(self, ostream, expr, is_objective): for vid, coef in sorted( expr.linear.items(), key=lambda x: getVarOrder(x[0]) ): - if coef.__class__ not in int_float: - coef = float(coef) if coef < 0: ostream.write(f'{coef!r} {getSymbol(getVar(vid))}\n') else: @@ -607,8 +604,6 @@ def _normalize_constraint(data): else: col = c1, c2 sym = f' {getSymbol(getVar(vid1))} * {getSymbol(getVar(vid2))}\n' - if coef.__class__ not in int_float: - coef = float(coef) if coef < 0: return col, repr(coef) + sym else: diff --git a/pyomo/repn/plugins/mps.py b/pyomo/repn/plugins/mps.py index 89420929778..e1a0d2187fc 100644 --- a/pyomo/repn/plugins/mps.py +++ b/pyomo/repn/plugins/mps.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -55,14 +55,14 @@ def _get_bound(exp): @WriterFactory.register('mps', 'Generate the corresponding MPS file') class ProblemWriter_mps(AbstractProblemWriter): - def __init__(self): + def __init__(self, int_marker=False): AbstractProblemWriter.__init__(self, ProblemFormat.mps) # the MPS writer is responsible for tracking which variables are # referenced in constraints, so that one doesn't end up with a # zillion "unreferenced variables" warning messages. stored at # the object level to avoid additional method arguments. - # dictionary of id(_VarData)->_VarData. + # dictionary of id(VarData)->VarData. self._referenced_variable_ids = {} # Keven Hunter made a nice point about using %.16g in his attachment @@ -78,6 +78,8 @@ def __init__(self): # the number's sign. self._precision_string = '.17g' + self._int_marker = int_marker + def __call__(self, model, output_filename, solver_capability, io_options): # Make sure not to modify the user's dictionary, # they may be reusing it outside of this call @@ -515,10 +517,27 @@ def yield_all_constraints(): column_template = " %s %s %" + self._precision_string + "\n" output_file.write("COLUMNS\n") cnt = 0 + in_integer_section = False + mark_cnt = 0 for vardata in variable_list: col_entries = column_data[variable_to_column[vardata]] cnt += 1 if len(col_entries) > 0: + if self._int_marker: + if vardata.is_integer(): + if not in_integer_section: + output_file.write( + f" MARK{mark_cnt:04d} 'MARKER' 'INTORG'\n" + ) + in_integer_section = True + mark_cnt += 1 + elif in_integer_section: # and not vardata.is_integer() + output_file.write( + f" MARK{mark_cnt:04d} 'MARKER' 'INTEND'\n" + ) + in_integer_section = False + mark_cnt += 1 + var_label = variable_symbol_dictionary[id(vardata)] for i, (row_label, coef) in enumerate(col_entries): output_file.write( @@ -536,6 +555,9 @@ def yield_all_constraints(): var_label = variable_symbol_dictionary[id(vardata)] output_file.write(column_template % (var_label, objective_label, 0)) + if self._int_marker and in_integer_section: + output_file.write(f" MARK{mark_cnt:04d} 'MARKER' 'INTEND'\n") + assert cnt == len(column_data) - 1 if len(column_data[-1]) > 0: col_entries = column_data[-1] diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 296ea350648..644fd26987b 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,12 +9,16 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import ctypes import logging import os -from collections import deque, defaultdict -from operator import itemgetter, attrgetter, setitem +from collections import deque, defaultdict, namedtuple from contextlib import nullcontext +from itertools import filterfalse, product +from math import log10 as _log10 +from operator import itemgetter, attrgetter, setitem +from pyomo.common.collections import ComponentMap, ComponentSet from pyomo.common.config import ( ConfigBlock, ConfigValue, @@ -22,8 +26,14 @@ document_kwargs_from_configdict, ) from pyomo.common.deprecation import deprecation_warning -from pyomo.common.errors import DeveloperError +from pyomo.common.errors import DeveloperError, InfeasibleConstraintException, MouseTrap from pyomo.common.gc_manager import PauseGC +from pyomo.common.numeric_types import ( + native_complex_types, + native_numeric_types, + native_types, + value, +) from pyomo.common.timing import TicTocTimer from pyomo.core.expr import ( @@ -41,9 +51,6 @@ RangedExpression, Expr_ifExpression, ExternalFunctionExpression, - native_types, - native_numeric_types, - value, ) from pyomo.core.expr.visitor import StreamBasedExpressionVisitor, _EvaluationVisitor from pyomo.core.base import ( @@ -62,13 +69,18 @@ minimize, ) from pyomo.core.base.component import ActiveComponent -from pyomo.core.base.expression import ScalarExpression, _GeneralExpressionData -from pyomo.core.base.objective import ScalarObjective, _GeneralObjectiveData +from pyomo.core.base.constraint import ConstraintData +from pyomo.core.base.expression import ScalarExpression, ExpressionData +from pyomo.core.base.objective import ScalarObjective, ObjectiveData +from pyomo.core.base.suffix import SuffixFinder +from pyomo.core.base.var import VarData import pyomo.core.kernel as kernel from pyomo.core.pyomoobject import PyomoObject from pyomo.opt import WriterFactory from pyomo.repn.util import ( + BeforeChildDispatcher, + ExitNodeDispatcher, ExprType, FileDeterminism, FileDeterminism_to_SortComponents, @@ -97,11 +109,16 @@ TOL = 1e-8 inf = float('inf') minus_inf = -inf +allowable_binary_var_bounds = {(0, 0), (0, 1), (1, 1)} _CONSTANT = ExprType.CONSTANT _MONOMIAL = ExprType.MONOMIAL _GENERAL = ExprType.GENERAL +ScalingFactors = namedtuple( + 'ScalingFactors', ['variables', 'constraints', 'objectives'] +) + # TODO: make a proper base class class NLWriterInfo(object): @@ -109,17 +126,17 @@ class NLWriterInfo(object): Attributes ---------- - variables: List[_VarData] + variables: List[VarData] The list of (unfixed) Pyomo model variables in the order written to the NL file - constraints: List[_ConstraintData] + constraints: List[ConstraintData] The list of (active) Pyomo model constraints in the order written to the NL file - objectives: List[_ObjectiveData] + objectives: List[ObjectiveData] The list of (active) Pyomo model objectives in the order written to the NL file @@ -142,15 +159,42 @@ class NLWriterInfo(object): file in the same order as the :py:attr:`variables` and generated .col file. + eliminated_vars: List[Tuple[VarData, NumericExpression]] + + The list of variables in the model that were eliminated by the + presolve. Each entry is a 2-tuple of (:py:class:`VarData`, + :py:class`NumericExpression`|`float`). The list is in the + necessary order for correct evaluation (i.e., all variables + appearing in the expression must either have been sent to the + solver, or appear *earlier* in this list. + + scaling: ScalingFactors or None + + namedtuple holding 3 lists of (variables, constraints, objectives) + scaling factors in the same order (and size) as the `variables`, + `constraints`, and `objectives` attributes above. + """ - def __init__(self, var, con, obj, extlib, row_lbl, col_lbl): + def __init__( + self, + var, + con, + obj, + external_libs, + row_labels, + col_labels, + eliminated_vars, + scaling, + ): self.variables = var self.constraints = con self.objectives = obj - self.external_function_libraries = extlib - self.row_labels = row_lbl - self.column_labels = col_lbl + self.external_function_libraries = external_libs + self.row_labels = row_labels + self.column_labels = col_labels + self.eliminated_vars = eliminated_vars + self.scaling = scaling @WriterFactory.register('nl_v2', 'Generate the corresponding AMPL NL file (version 2).') @@ -167,7 +211,7 @@ class NLWriter(object): CONFIG.declare( 'skip_trivial_constraints', ConfigValue( - default=False, + default=True, domain=bool, description='Skip writing constraints whose body is constant', ), @@ -196,6 +240,18 @@ class NLWriter(object): description='Write the corresponding .row and .col files', ), ) + CONFIG.declare( + 'scale_model', + ConfigValue( + default=True, + domain=bool, + description="Write variables and constraints in scaled space", + doc=""" + If True, then the writer will output the model constraints and + variables in 'scaled space' using the scaling from the + 'scaling_factor' Suffix, if provided.""", + ), + ) CONFIG.declare( 'export_nonlinear_variables', ConfigValue( @@ -247,6 +303,17 @@ class NLWriter(object): variables'.""", ), ) + CONFIG.declare( + 'linear_presolve', + ConfigValue( + default=True, + domain=bool, + description='Perform linear presolve', + doc=""" + If True, we will perform a basic linear presolve by performing + variable elimination (without fill-in).""", + ), + ) def __init__(self): self.config = self.CONFIG() @@ -259,6 +326,18 @@ def __call__(self, model, filename, solver_capability, io_options): col_fname = filename_base + '.col' config = self.config(io_options) + + # There is no (convenient) way to pass the scaling factors or + # information about presolved variables back to the solver + # through the old "call" interface (since solvers that used that + # interface predated scaling / presolve). We will play it safe + # and disable scaling / presolve when called through this API + config.scale_model = False + config.linear_presolve = False + + # just for backwards compatibility + config.skip_trivial_constraints = False + if config.symbolic_solver_labels: _open = lambda fname: open(fname, 'w') else: @@ -267,6 +346,18 @@ def __call__(self, model, filename, solver_capability, io_options): row_fname ) as ROWFILE, _open(col_fname) as COLFILE: info = self.write(model, FILE, ROWFILE, COLFILE, config=config) + if not info.variables: + # This exception is included for compatibility with the + # original NL writer v1. + os.remove(filename) + if config.symbolic_solver_labels: + os.remove(row_fname) + os.remove(col_fname) + raise ValueError( + "No variables appear in the Pyomo model constraints or" + " objective. This is not supported by the NL file interface" + ) + # Historically, the NL writer communicated the external function # libraries back to the ASL interface through the PYOMO_AMPLFUNC # environment variable. @@ -278,7 +369,9 @@ def __call__(self, model, filename, solver_capability, io_options): return filename, symbol_map @document_kwargs_from_configdict(CONFIG) - def write(self, model, ostream, rowstream=None, colstream=None, **options): + def write( + self, model, ostream, rowstream=None, colstream=None, **options + ) -> NLWriterInfo: """Write a model in NL format. Returns @@ -316,123 +409,125 @@ def _generate_symbol_map(self, info): # Now that the row/column ordering is resolved, create the labels symbol_map = SymbolMap() symbol_map.addSymbols( - (info[0], f"v{idx}") for idx, info in enumerate(info.variables) + (info, f"v{idx}") for idx, info in enumerate(info.variables) ) symbol_map.addSymbols( - (info[0], f"c{idx}") for idx, info in enumerate(info.constraints) + (info, f"c{idx}") for idx, info in enumerate(info.constraints) ) symbol_map.addSymbols( - (info[0], f"o{idx}") for idx, info in enumerate(info.objectives) + (info, f"o{idx}") for idx, info in enumerate(info.objectives) ) return symbol_map -def _RANGE_TYPE(lb, ub): - if lb == ub: - if lb is None: - return 3 # -inf <= c <= inf - else: - return 4 # L == c == U - elif lb is None: - return 1 # c <= U - elif ub is None: - return 2 # L <= c - else: - return 0 # L <= c <= U - - class _SuffixData(object): - def __init__(self, name, column_order, row_order, obj_order, model_id): - self._name = name - self._column_order = column_order - self._row_order = row_order - self._obj_order = obj_order - self._model_id = model_id + def __init__(self, name): + self.name = name self.obj = {} self.con = {} self.var = {} self.prob = {} self.datatype = set() + self.values = ComponentMap() def update(self, suffix): - missing_component = missing_other = 0 self.datatype.add(suffix.datatype) - for obj, val in suffix.items(): - missing = self._store(obj, val) - if missing: - if missing > 0: - missing_component += missing + self.values.update(suffix) + + def store(self, obj, val): + self.values[obj] = val + + def compile(self, column_order, row_order, obj_order, model_id): + var_con_obj = {Var, Constraint, Objective} + missing_component_data = ComponentSet() + unknown_data = ComponentSet() + queue = [self.values.items()] + while queue: + for obj, val in queue.pop(0): + if val.__class__ not in int_float: + # [JDS] I am not entirely sure why, but we have + # historically supported suffix values that hold + # dictionaries that map arbitrary component data + # objects to values. We will preserve that behavior + # here. This behavior is exercised by a + # ExternalGreyBox test. + if isinstance(val, dict): + queue.append(val.items()) + continue + val = float(val) + _id = id(obj) + if _id in column_order: + self.var[column_order[_id]] = val + elif _id in row_order: + self.con[row_order[_id]] = val + elif _id in obj_order: + self.obj[obj_order[_id]] = val + elif _id == model_id: + self.prob[0] = val + elif getattr(obj, 'ctype', None) in var_con_obj: + if obj.is_indexed(): + # Expand this indexed component to store the + # individual ComponentDatas, but ONLY if the + # component data is not in the original dictionary + # of values that we extracted from the Suffixes + queue.append( + product( + filterfalse(self.values.__contains__, obj.values()), + (val,), + ) + ) + else: + missing_component_data.add(obj) else: - missing_other -= missing - if missing_component: + unknown_data.add(obj) + if missing_component_data: logger.warning( - f"model contains export suffix '{suffix.name}' that " - f"contains {missing_component} component keys that are " + f"model contains export suffix '{self.name}' that " + f"contains {len(missing_component_data)} component keys that are " "not exported as part of the NL file. " "Skipping." ) - if missing_other: + if logger.isEnabledFor(logging.DEBUG): + logger.debug( + "Skipped component keys:\n\t" + + "\n\t".join(sorted(map(str, missing_component_data))) + ) + if unknown_data: logger.warning( - f"model contains export suffix '{suffix.name}' that " - f"contains {missing_other} keys that are not " + f"model contains export suffix '{self.name}' that " + f"contains {len(unknown_data)} keys that are not " "Var, Constraint, Objective, or the model. Skipping." ) + if logger.isEnabledFor(logging.DEBUG): + logger.debug( + "Skipped component keys:\n\t" + + "\n\t".join(sorted(map(str, unknown_data))) + ) - def store(self, obj, val): - missing = self._store(obj, val) - if not missing: - return - if missing == 1: - logger.warning( - f"model contains export suffix '{self._name}' with " - f"{obj.ctype.__name__} key '{obj.name}', but that " - "object is not exported as part of the NL file. " - "Skipping." - ) - elif missing > 1: - logger.warning( - f"model contains export suffix '{self._name}' with " - f"{obj.ctype.__name__} key '{obj.name}', but that " - "object contained {missing} data objects that are " - "not exported as part of the NL file. " - "Skipping." - ) - else: - logger.warning( - f"model contains export suffix '{self._name}' with " - f"{obj.__class__.__name__} key '{obj}' that is not " - "a Var, Constraint, Objective, or the model. Skipping." - ) - def _store(self, obj, val): +class CachingNumericSuffixFinder(SuffixFinder): + scale = True + + def __init__(self, name, default=None): + super().__init__(name, default) + self.suffix_cache = {} + + def __call__(self, obj): _id = id(obj) - if _id in self._column_order: - obj = self.var - key = self._column_order[_id] - elif _id in self._row_order: - obj = self.con - key = self._row_order[_id] - elif _id in self._obj_order: - obj = self.obj - key = self._obj_order[_id] - elif _id == self._model_id: - obj = self.prob - key = 0 - else: - missing_ct = 0 - if isinstance(obj, PyomoObject): - if obj.is_indexed(): - for o in obj.values(): - missing_ct += self._store(o, val) - else: - missing_ct = 1 - else: - missing_ct = -1 - return missing_ct - if val.__class__ not in int_float: - val = float(val) - obj[key] = val - return 0 + if _id in self.suffix_cache: + return self.suffix_cache[_id] + ans = self.find(obj) + if ans.__class__ not in int_float: + ans = float(ans) + self.suffix_cache[_id] = ans + return ans + + +class _NoScalingFactor(object): + scale = False + + def __call__(self, obj): + return 1 class _NLWriter_impl(object): @@ -451,6 +546,7 @@ def __init__(self, ostream, rowstream, colstream, config): self.external_functions = {} self.used_named_expressions = set() self.var_map = {} + self.sorter = FileDeterminism_to_SortComponents(config.file_determinism) self.visitor = AMPLRepnVisitor( self.template, self.subexpression_cache, @@ -460,6 +556,7 @@ def __init__(self, ostream, rowstream, colstream, config): self.used_named_expressions, self.symbolic_solver_labels, self.config.export_defined_variables, + self.sorter, ) self.next_V_line_id = 0 self.pause_gc = None @@ -521,11 +618,52 @@ def write(self, model): symbolic_solver_labels = self.symbolic_solver_labels visitor = self.visitor ostream = self.ostream + linear_presolve = self.config.linear_presolve var_map = self.var_map initialize_var_map_from_column_order(model, self.config, var_map) timer.toc('Initialized column order', level=logging.DEBUG) + # Collect all defined EXPORT suffixes on the model + suffix_data = {} + if component_map[Suffix]: + # Note: reverse the block list so that higher-level Suffix + # components override lower level ones. + for block in reversed(component_map[Suffix]): + for suffix in block.component_objects( + Suffix, active=True, descend_into=False, sort=sorter + ): + if not suffix.export_enabled() or not suffix: + continue + name = suffix.local_name + if name not in suffix_data: + suffix_data[name] = _SuffixData(name) + suffix_data[name].update(suffix) + # + # Data structures to support variable/constraint scaling + # + if self.config.scale_model and 'scaling_factor' in suffix_data: + scaling_factor = CachingNumericSuffixFinder('scaling_factor', 1) + scaling_cache = scaling_factor.suffix_cache + del suffix_data['scaling_factor'] + else: + scaling_factor = _NoScalingFactor() + scale_model = scaling_factor.scale + + timer.toc("Collected suffixes", level=logging.DEBUG) + + # + # Data structures to support presolve + # + # lcon_by_linear_nnz stores all linear constraints grouped by the NNZ + # in the linear portion of the expression. The value is another + # dict mapping id(con) to constraint info + lcon_by_linear_nnz = defaultdict(dict) + # comp_by_linear_var maps id(var) to lists of constraint / + # object infos that have that var in the linear portion of the + # expression + comp_by_linear_var = defaultdict(list) + # # Tabulate the model expressions # @@ -534,18 +672,27 @@ def write(self, model): last_parent = None for obj in model.component_data_objects(Objective, active=True, sort=sorter): if with_debug_timing and obj.parent_component() is not last_parent: - timer.toc('Objective %s', last_parent, level=logging.DEBUG) + if last_parent is None: + timer.toc(None) + else: + timer.toc('Objective %s', last_parent, level=logging.DEBUG) last_parent = obj.parent_component() - expr = visitor.walk_expression((obj.expr, obj, 1)) - if expr.named_exprs: - self._record_named_expression_usage(expr.named_exprs, obj, 1) - if expr.nonlinear: - objectives.append((obj, expr)) + expr_info = visitor.walk_expression((obj.expr, obj, 1, scaling_factor(obj))) + if expr_info.named_exprs: + self._record_named_expression_usage(expr_info.named_exprs, obj, 1) + if expr_info.nonlinear: + objectives.append((obj, expr_info)) else: - linear_objs.append((obj, expr)) + linear_objs.append((obj, expr_info)) + if linear_presolve: + obj_id = id(obj) + for _id in expr_info.linear: + comp_by_linear_var[_id].append((obj_id, expr_info)) if with_debug_timing: # report the last objective timer.toc('Objective %s', last_parent, level=logging.DEBUG) + else: + timer.toc('Processed %s objectives', len(objectives)) # Order the objectives, moving all nonlinear objectives to # the beginning @@ -564,86 +711,86 @@ def write(self, model): # required for solvers like PATH. n_complementarity_range = 0 n_complementarity_nz_var_lb = 0 + # + last_parent = None for con in ordered_active_constraints(model, self.config): if with_debug_timing and con.parent_component() is not last_parent: - timer.toc('Constraint %s', last_parent, level=logging.DEBUG) + if last_parent is None: + timer.toc(None) + else: + timer.toc('Constraint %s', last_parent, level=logging.DEBUG) last_parent = con.parent_component() - expr = visitor.walk_expression((con.body, con, 0)) - if expr.named_exprs: - self._record_named_expression_usage(expr.named_exprs, con, 0) + scale = scaling_factor(con) + expr_info = visitor.walk_expression((con.body, con, 0, scale)) + if expr_info.named_exprs: + self._record_named_expression_usage(expr_info.named_exprs, con, 0) + # Note: Constraint.lb/ub guarantee a return value that is # either a (finite) native_numeric_type, or None - const = expr.const - if const.__class__ not in int_float: - const = float(const) lb = con.lb - if lb is not None: - lb = repr(lb - const) ub = con.ub - if ub is not None: - ub = repr(ub - const) - _type = _RANGE_TYPE(lb, ub) - if _type == 4: - n_equality += 1 - elif _type == 0: - n_ranges += 1 - elif _type == 3: # and self.config.skip_trivial_constraints: + if lb is None and ub is None: # and self.config.skip_trivial_constraints: continue - pass - # FIXME: this is a HACK to be compatible with the NLv1 - # writer. In the future, this writer should be expanded to - # look for and process Complementarity components (assuming - # that they are in an acceptable form). - if hasattr(con, '_complementarity'): - _type = 5 - # we are going to pass the complementarity type and the - # corresponding variable id() as the "lb" and "ub" for - # the range. - lb = con._complementarity - ub = con._vid - if expr.nonlinear: - n_complementarity_nonlin += 1 - else: - n_complementarity_lin += 1 - if expr.nonlinear: - constraints.append((con, expr, _type, lb, ub)) - elif expr.linear: - linear_cons.append((con, expr, _type, lb, ub)) + if scale != 1: + if lb is not None: + lb = lb * scale + if ub is not None: + ub = ub * scale + if scale < 0: + lb, ub = ub, lb + if expr_info.nonlinear: + constraints.append((con, expr_info, lb, ub)) + elif expr_info.linear: + linear_cons.append((con, expr_info, lb, ub)) elif not self.config.skip_trivial_constraints: - linear_cons.append((con, expr, _type, lb, ub)) + linear_cons.append((con, expr_info, lb, ub)) else: # constant constraint and skip_trivial_constraints - # - # TODO: skip_trivial_constraints should be an - # enum that also accepts "Exception" so that - # solvers can be (easily) notified of infeasible - # trivial constraints. - if (lb is not None and float(lb) > TOL) or ( - ub is not None and float(ub) < -TOL + c = expr_info.const + if (lb is not None and lb - c > TOL) or ( + ub is not None and ub - c < -TOL ): - logger.warning( + raise InfeasibleConstraintException( "model contains a trivially infeasible " - f"constraint {con.name}, but " - "skip_trivial_constraints==True and the " - "constraint is being omitted from the NL " - "file. Solving the model may incorrectly " - "report a feasible solution." + f"constraint '{con.name}' (fixed body value " + f"{c} outside bounds [{lb}, {ub}])." ) + if linear_presolve: + con_id = id(con) + if not expr_info.nonlinear and lb == ub and lb is not None: + lcon_by_linear_nnz[len(expr_info.linear)][con_id] = expr_info, lb + for _id in expr_info.linear: + comp_by_linear_var[_id].append((con_id, expr_info)) if with_debug_timing: # report the last constraint timer.toc('Constraint %s', last_parent, level=logging.DEBUG) + else: + timer.toc('Processed %s constraints', len(constraints)) + + # This may fetch more bounds than needed, but only in the cases + # where variables were completely eliminated while walking the + # expressions, or when users provide superfluous variables in + # the column ordering. + var_bounds = {_id: v.bounds for _id, v in var_map.items()} + + eliminated_cons, eliminated_vars = self._linear_presolve( + comp_by_linear_var, lcon_by_linear_nnz, var_bounds + ) + del comp_by_linear_var + del lcon_by_linear_nnz # Order the constraints, moving all nonlinear constraints to # the beginning n_nonlinear_cons = len(constraints) - constraints.extend(linear_cons) + if eliminated_cons: + _removed = eliminated_cons.__contains__ + constraints.extend(filterfalse(lambda c: _removed(id(c[0])), linear_cons)) + else: + constraints.extend(linear_cons) n_cons = len(constraints) - # initialize an empty row order, to be populated later if we need it - row_order = {} - # - # Collect constraints and objectives into the groupings - # necessary for AMPL + # Collect variables from constraints and objectives into the + # groupings necessary for AMPL # # For efficiency, we will do everything with ids (and not the # var objects themselves) @@ -654,12 +801,21 @@ def write(self, model): filter(self.used_named_expressions.__contains__, self.subexpression_order) ) - # linear contribution by (constraint, objective) component. + # linear contribution by (constraint, objective, variable) component. # Keys are component id(), Values are dicts mapping variable # id() to linear coefficient. All nonzeros in the component # (variables appearing in the linear and/or nonlinear # subexpressions) will appear in the dict. - linear_by_comp = {} + # + # We initialize the map with any variables eliminated from + # (presolved out of) the model (necessary so that + # _categorize_vars will map eliminated vars to the current + # vars). Note that at the moment, we only consider linear + # equality constraints in the presolve. If that ever changes + # (e.g., to support eliminating variables appearing linearly in + # nonlinear equality constraints), then this logic will need to + # be revisited. + linear_by_comp = {_id: info.linear for _id, info in eliminated_vars.items()} # We need to categorize the named subexpressions first so that # we know their linear / nonlinear vars when we encounter them @@ -678,14 +834,19 @@ def write(self, model): if self.config.export_nonlinear_variables: for v in self.config.export_nonlinear_variables: + # Note that because we have already walked all the + # expressions, we have already "seen" all the variables + # we will see, so we don't need to fill in any VarData + # from IndexedVar containers here. if v.is_indexed(): - _iter = v.values() + _iter = v.values(sorter) else: _iter = (v,) for _v in _iter: _id = id(_v) if _id not in var_map: var_map[_id] = _v + var_bounds[_id] = _v.bounds con_vars_nonlinear.add(_id) con_nnz = sum(con_nnz_by_var.values()) @@ -710,13 +871,6 @@ def write(self, model): con_vars = con_vars_linear | con_vars_nonlinear all_vars = con_vars | obj_vars n_vars = len(all_vars) - if n_vars < 1: - # TODO: Remove this. This exception is included for - # compatibility with the original NL writer v1. - raise ValueError( - "No variables appear in the Pyomo model constraints or" - " objective. This is not supported by the NL file interface" - ) continuous_vars = set() binary_vars = set() @@ -728,7 +882,12 @@ def write(self, model): elif v.is_binary(): binary_vars.add(_id) elif v.is_integer(): - integer_vars.add(_id) + # Note: integer variables whose bounds are in {0, 1} + # should be classified as binary + if var_bounds[_id] in allowable_binary_var_bounds: + binary_vars.add(_id) + else: + integer_vars.add(_id) else: raise ValueError( f"Variable '{v.name}' has a domain that is not Real, " @@ -797,8 +956,8 @@ def write(self, model): linear_binary_vars = linear_integer_vars = set() assert len(variables) == n_vars timer.toc( - 'Set row / column ordering: %s variables [%s, %s, %s R/B/Z], ' - '%s constraints [%s, %s L/NL]', + 'Set row / column ordering: %s var [%s, %s, %s R/B/Z], ' + '%s con [%s, %s L/NL]', n_vars, len(continuous_vars), len(binary_vars), @@ -809,7 +968,7 @@ def write(self, model): level=logging.DEBUG, ) - # Fill in the variable list and update the new column order. + # Update the column order (based on our reordering of the variables above). # # Note that as we allow var_map to contain "known" variables # that are not needed in the NL file (and column_order was @@ -817,48 +976,9 @@ def write(self, model): # column_order to *just* contain the variables that we are # sending to the NL. self.column_order = column_order = {_id: i for i, _id in enumerate(variables)} - for idx, _id in enumerate(variables): - v = var_map[_id] - # Note: Var.bounds guarantees the values are either (finite) - # native_numeric_types or None - lb, ub = v.bounds - if lb is not None: - lb = repr(lb) - if ub is not None: - ub = repr(ub) - variables[idx] = (v, _id, _RANGE_TYPE(lb, ub), lb, ub) - timer.toc("Computed variable bounds", level=logging.DEBUG) - - # Collect all defined EXPORT suffixes on the model - suffix_data = {} - if component_map[Suffix]: - if not row_order: - row_order = {id(con[0]): i for i, con in enumerate(constraints)} - obj_order = {id(obj[0]): i for i, obj in enumerate(objectives)} - model_id = id(model) - # Note: reverse the block list so that higher-level Suffix - # components override lower level ones. - for block in reversed(component_map[Suffix]): - for suffix in block.component_objects( - Suffix, active=True, descend_into=False, sort=sorter - ): - if not (suffix.direction & Suffix.EXPORT): - continue - name = suffix.local_name - if name not in suffix_data: - suffix_data[name] = _SuffixData( - name, column_order, row_order, obj_order, model_id - ) - suffix_data[name].update(suffix) - timer.toc("Collected suffixes", level=logging.DEBUG) # Collect all defined SOSConstraints on the model if component_map[SOSConstraint]: - if not row_order: - row_order = {id(con[0]): i for i, con in enumerate(constraints)} - if not component_map[Suffix]: - obj_order = {id(obj[0]): i for i, obj in enumerate(objectives)} - model_id = id(model) for name in ('sosno', 'ref'): # I am choosing not to allow a user to mix the use of the Pyomo # SOSConstraint component and manual sosno declarations within @@ -879,9 +999,7 @@ def write(self, model): "model. To avoid this error please use only one of " "these methods to define special ordered sets." ) - suffix_data[name] = _SuffixData( - name, column_order, row_order, obj_order, model_id - ) + suffix_data[name] = _SuffixData(name) suffix_data[name].datatype.add(Suffix.INT) sos_id = 0 sosno = suffix_data['sosno'] @@ -910,17 +1028,22 @@ def write(self, model): sosno.store(v, tag) ref.store(v, r) + if suffix_data: + row_order = {id(con[0]): i for i, con in enumerate(constraints)} + obj_order = {id(obj[0]): i for i, obj in enumerate(objectives)} + model_id = id(model) + if symbolic_solver_labels: labeler = NameLabeler() row_labels = [labeler(info[0]) for info in constraints] + [ labeler(info[0]) for info in objectives ] row_comments = [f'\t#{lbl}' for lbl in row_labels] - col_labels = [labeler(info[0]) for info in variables] + col_labels = [labeler(var_map[_id]) for _id in variables] col_comments = [f'\t#{lbl}' for lbl in col_labels] self.var_id_to_nl = { - info[1]: f'{var_idx}{col_comments[var_idx]}' - for var_idx, info in enumerate(variables) + _id: f'v{var_idx}{col_comments[var_idx]}' + for var_idx, _id in enumerate(variables) } # Write out the .row and .col data if self.rowstream is not None: @@ -933,8 +1056,104 @@ def write(self, model): row_labels = row_comments = [''] * (n_cons + n_objs) col_labels = col_comments = [''] * len(variables) self.var_id_to_nl = { - info[1]: var_idx for var_idx, info in enumerate(variables) + _id: f"v{var_idx}" for var_idx, _id in enumerate(variables) } + + _vmap = self.var_id_to_nl + if scale_model: + template = self.template + objective_scaling = [scaling_cache[id(info[0])] for info in objectives] + constraint_scaling = [scaling_cache[id(info[0])] for info in constraints] + variable_scaling = [scaling_factor(var_map[_id]) for _id in variables] + for _id, scale in zip(variables, variable_scaling): + if scale == 1: + continue + # Update var_bounds to be scaled bounds + if scale < 0: + # Note: reverse bounds for negative scaling factors + ub, lb = var_bounds[_id] + else: + lb, ub = var_bounds[_id] + if lb is not None: + lb *= scale + if ub is not None: + ub *= scale + var_bounds[_id] = lb, ub + # Update _vmap to output scaled variables in NL expressions + _vmap[_id] = ( + template.division + _vmap[_id] + '\n' + template.const % scale + ).rstrip() + + # Update any eliminated variables to point to the (potentially + # scaled) substituted variables + for _id, expr_info in list(eliminated_vars.items()): + nl, args, _ = expr_info.compile_repn(visitor) + for _i in args: + # It is possible that the eliminated variable could + # reference another variable that is no longer part of + # the model and therefore does not have a _vmap entry. + # This can happen when there is an underdetermined + # independent linear subsystem and the presolve removed + # all the constraints from the subsystem. Because the + # free variables in the subsystem are not referenced + # anywhere else in the model, they are not part of the + # `variables` list. Implicitly "fix" it to an arbitrary + # valid value from the presolved domain (see #3192). + if _i not in _vmap: + lb, ub = var_bounds[_i] + if lb is None: + lb = -inf + if ub is None: + ub = inf + if lb <= 0 <= ub: + val = 0 + else: + val = lb if abs(lb) < abs(ub) else ub + eliminated_vars[_i] = AMPLRepn(val, {}, None) + _vmap[_i] = expr_info.compile_repn(visitor)[0] + logger.warning( + "presolve identified an underdetermined independent " + "linear subsystem that was removed from the model. " + f"Setting '{var_map[_i]}' == {val}" + ) + _vmap[_id] = nl.rstrip() % tuple(_vmap[_i] for _i in args) + + r_lines = [None] * n_cons + for idx, (con, expr_info, lb, ub) in enumerate(constraints): + if lb == ub: # TBD: should this be within tolerance? + if lb is None: + # type = 3 # -inf <= c <= inf + r_lines[idx] = "3" + else: + # _type = 4 # L == c == U + r_lines[idx] = f"4 {lb - expr_info.const!r}" + n_equality += 1 + elif lb is None: + # _type = 1 # c <= U + r_lines[idx] = f"1 {ub - expr_info.const!r}" + elif ub is None: + # _type = 2 # L <= c + r_lines[idx] = f"2 {lb - expr_info.const!r}" + else: + # _type = 0 # L <= c <= U + r_lines[idx] = f"0 {lb - expr_info.const!r} {ub - expr_info.const!r}" + n_ranges += 1 + expr_info.const = 0 + # FIXME: this is a HACK to be compatible with the NLv1 + # writer. In the future, this writer should be expanded to + # look for and process Complementarity components (assuming + # that they are in an acceptable form). + if hasattr(con, '_complementarity'): + # _type = 5 + r_lines[idx] = f"5 {con._complementarity} {1+column_order[con._vid]}" + if expr_info.nonlinear: + n_complementarity_nonlin += 1 + else: + n_complementarity_lin += 1 + if symbolic_solver_labels: + for idx in range(len(constraints)): + r_lines[idx] += row_comments[idx] + timer.toc("Generated row/col labels & comments", level=logging.DEBUG) # @@ -1063,8 +1282,8 @@ def write(self, model): len(linear_binary_vars), len(linear_integer_vars), len(both_vars_nonlinear.intersection(discrete_vars)), - len(con_vars_nonlinear.intersection(discrete_vars)), - len(obj_vars_nonlinear.intersection(discrete_vars)), + len(con_only_nonlinear_vars.intersection(discrete_vars)), + len(obj_only_nonlinear_vars.intersection(discrete_vars)), ) ) # @@ -1106,6 +1325,7 @@ def write(self, model): for name, data in suffix_data.items(): if name == 'dual': continue + data.compile(column_order, row_order, obj_order, model_id) if len(data.datatype) > 1: raise ValueError( "The NL file writer found multiple active export " @@ -1129,7 +1349,7 @@ def write(self, model): if not _vals: continue ostream.write(f"S{_field|_float} {len(_vals)} {name}\n") - # Note: _SuffixData store/update guarantee the value is int/float + # Note: _SuffixData.compile() guarantees the value is int/float ostream.write( ''.join(f"{_id} {_vals[_id]!r}\n" for _id in sorted(_vals)) ) @@ -1174,12 +1394,18 @@ def write(self, model): # constraints at the end (as their nonlinear expressions # are the constant 0). _expr = self.template.const % 0 - ostream.write( - _expr.join( - f'C{i}{row_comments[i]}\n' - for i in range(row_idx, len(constraints)) + if symbolic_solver_labels: + ostream.write( + _expr.join( + f'C{i}{row_comments[i]}\n' + for i in range(row_idx, len(constraints)) + ) ) - ) + else: + ostream.write( + _expr.join(f'C{i}\n' for i in range(row_idx, len(constraints))) + ) + # We know that there is at least one linear expression # (row_idx), so we can unconditionally emit the last "0 # expression": @@ -1210,18 +1436,32 @@ def write(self, model): # "d" lines (dual initialization) # if 'dual' in suffix_data: - _data = suffix_data['dual'] - if _data.var: + data = suffix_data['dual'] + data.compile(column_order, row_order, obj_order, model_id) + if scale_model: + if objectives: + if len(objectives) > 1: + logger.warning( + "Scaling model with dual suffixes and multiple " + "objectives. Assuming that the duals are computed " + "against the first objective." + ) + _obj_scale = objective_scaling[0] + else: + _obj_scale = 1 + for i in data.con: + data.con[i] *= _obj_scale / constraint_scaling[i] + if data.var: logger.warning("ignoring 'dual' suffix for Var types") - if _data.obj: + if data.obj: logger.warning("ignoring 'dual' suffix for Objective types") - if _data.prob: + if data.prob: logger.warning("ignoring 'dual' suffix for Model") - if _data.con: - ostream.write(f"d{len(_data.con)}\n") - # Note: _SuffixData store/update guarantee the value is int/float + if data.con: + ostream.write(f"d{len(data.con)}\n") + # Note: _SuffixData.compile() guarantees the value is int/float ostream.write( - ''.join(f"{_id} {_data.con[_id]!r}\n" for _id in sorted(_data.con)) + ''.join(f"{_id} {data.con[_id]!r}\n" for _id in sorted(data.con)) ) # @@ -1229,9 +1469,14 @@ def write(self, model): # _init_lines = [ (var_idx, val if val.__class__ in int_float else float(val)) - for var_idx, val in enumerate(v[0].value for v in variables) + for var_idx, val in enumerate(var_map[_id].value for _id in variables) if val is not None ] + if scale_model: + _init_lines = [ + (var_idx, val * variable_scaling[var_idx]) + for var_idx, val in _init_lines + ] ostream.write( 'x%d%s\n' % (len(_init_lines), "\t# initial guess" if symbolic_solver_labels else '') @@ -1249,28 +1494,16 @@ def write(self, model): ostream.write( 'r%s\n' % ( - "\t#%d ranges (rhs's)" % len(constraints) - if symbolic_solver_labels - else '', + ( + "\t#%d ranges (rhs's)" % len(constraints) + if symbolic_solver_labels + else '' + ), ) ) - for row_idx, info in enumerate(constraints): - i = info[2] - if i == 4: # == - ostream.write(f"4 {info[3]}{row_comments[row_idx]}\n") - elif i == 1: # body <= ub - ostream.write(f"1 {info[4]}{row_comments[row_idx]}\n") - elif i == 2: # lb <= body - ostream.write(f"2 {info[3]}{row_comments[row_idx]}\n") - elif i == 0: # lb <= body <= ub - ostream.write(f"0 {info[3]} {info[4]}{row_comments[row_idx]}\n") - elif i == 5: # complementarity - ostream.write( - f"5 {info[3]} {1+column_order[info[4]]}" - f"{row_comments[row_idx]}\n" - ) - else: # i == 3; unbounded - ostream.write(f"3{row_comments[row_idx]}\n") + ostream.write("\n".join(r_lines)) + if r_lines: + ostream.write("\n") # # "b" lines (variable bounds) @@ -1278,25 +1511,26 @@ def write(self, model): ostream.write( 'b%s\n' % ( - "\t#%d bounds (on variables)" % len(variables) - if symbolic_solver_labels - else '', + ( + "\t#%d bounds (on variables)" % len(variables) + if symbolic_solver_labels + else '' + ), ) ) - for var_idx, info in enumerate(variables): - # _bound_writer[info[2]](info, col_comments[var_idx]) - ### - i = info[2] - if i == 0: # lb <= body <= ub - ostream.write(f"0 {info[3]} {info[4]}{col_comments[var_idx]}\n") - elif i == 2: # lb <= body - ostream.write(f"2 {info[3]}{col_comments[var_idx]}\n") - elif i == 1: # body <= ub - ostream.write(f"1 {info[4]}{col_comments[var_idx]}\n") - elif i == 4: # == - ostream.write(f"4 {info[3]}{col_comments[var_idx]}\n") - else: # i == 3; unbounded - ostream.write(f"3{col_comments[var_idx]}\n") + for var_idx, _id in enumerate(variables): + lb, ub = var_bounds[_id] + if lb == ub: + if lb is None: # unbounded + ostream.write(f"3{col_comments[var_idx]}\n") + else: # == + ostream.write(f"4 {lb!r}{col_comments[var_idx]}\n") + elif lb is None: # var <= ub + ostream.write(f"1 {ub!r}{col_comments[var_idx]}\n") + elif ub is None: # lb <= body + ostream.write(f"2 {lb!r}{col_comments[var_idx]}\n") + else: # lb <= body <= ub + ostream.write(f"0 {lb!r} {ub!r}{col_comments[var_idx]}\n") # # "k" lines (column offsets in Jacobian NNZ) @@ -1305,14 +1539,16 @@ def write(self, model): 'k%d%s\n' % ( len(variables) - 1, - "\t#intermediate Jacobian column lengths" - if symbolic_solver_labels - else '', + ( + "\t#intermediate Jacobian column lengths" + if symbolic_solver_labels + else '' + ), ) ) ktot = 0 - for var_idx, info in enumerate(variables[:-1]): - ktot += con_nnz_by_var.get(info[1], 0) + for var_idx, _id in enumerate(variables[:-1]): + ktot += con_nnz_by_var.get(_id, 0) ostream.write(f"{ktot}\n") # @@ -1321,15 +1557,15 @@ def write(self, model): for row_idx, info in enumerate(constraints): linear = info[1].linear # ASL will fail on "J 0", so if there are no coefficients - # (i.e., a constant objective), then skip this entry + # (e.g., a nonlinear-only constraint), then skip this entry if not linear: continue + if scale_model: + for _id, val in linear.items(): + linear[_id] /= scaling_cache[_id] ostream.write(f'J{row_idx} {len(linear)}{row_comments[row_idx]}\n') - for _id in sorted(linear.keys(), key=column_order.__getitem__): - val = linear[_id] - if val.__class__ not in int_float: - val = float(val) - ostream.write(f'{column_order[_id]} {val!r}\n') + for _id in sorted(linear, key=column_order.__getitem__): + ostream.write(f'{column_order[_id]} {linear[_id]!r}\n') # # "G" lines (non-empty terms in the Objective) @@ -1337,24 +1573,39 @@ def write(self, model): for obj_idx, info in enumerate(objectives): linear = info[1].linear # ASL will fail on "G 0", so if there are no coefficients - # (i.e., a constant objective), then skip this entry + # (e.g., a constant objective), then skip this entry if not linear: continue + if scale_model: + for _id, val in linear.items(): + linear[_id] /= scaling_cache[_id] ostream.write(f'G{obj_idx} {len(linear)}{row_comments[obj_idx + n_cons]}\n') - for _id in sorted(linear.keys(), key=column_order.__getitem__): - val = linear[_id] - if val.__class__ not in int_float: - val = float(val) - ostream.write(f'{column_order[_id]} {val!r}\n') + for _id in sorted(linear, key=column_order.__getitem__): + ostream.write(f'{column_order[_id]} {linear[_id]!r}\n') # Generate the return information + eliminated_vars = [ + (var_map[_id], expr_info.to_expr(var_map)) + for _id, expr_info in eliminated_vars.items() + ] + eliminated_vars.reverse() + if scale_model: + scaling = ScalingFactors( + variables=variable_scaling, + constraints=constraint_scaling, + objectives=objective_scaling, + ) + else: + scaling = None info = NLWriterInfo( - variables, - constraints, - objectives, - sorted(amplfunc_libraries), - row_labels, - col_labels, + var=[var_map[_id] for _id in variables], + con=[info[0] for info in constraints], + obj=[info[0] for info in objectives], + external_libs=sorted(amplfunc_libraries), + row_labels=row_labels, + col_labels=col_labels, + eliminated_vars=eliminated_vars, + scaling=scaling, ) timer.toc("Wrote NL stream", level=logging.DEBUG) timer.toc("Generated NL representation", delta=False) @@ -1420,8 +1671,10 @@ def _categorize_vars(self, comp_list, linear_by_comp): if expr_info.nonlinear: nonlinear_vars = set() for _id in expr_info.nonlinear[1]: + if _id in nonlinear_vars: + continue if _id in linear_by_comp: - nonlinear_vars.update(linear_by_comp[_id].keys()) + nonlinear_vars.update(linear_by_comp[_id]) else: nonlinear_vars.add(_id) # Recreate nz if this component has both linear and @@ -1429,7 +1682,7 @@ def _categorize_vars(self, comp_list, linear_by_comp): if expr_info.linear: # Ensure any variables that only appear nonlinearly # in the expression have 0's in the linear dict - for i in nonlinear_vars - linear_vars: + for i in filterfalse(linear_vars.__contains__, nonlinear_vars): expr_info.linear[i] = 0 else: # All variables are nonlinear; generate the linear @@ -1482,6 +1735,164 @@ def _count_subexpression_occurrences(self): n_subexpressions[0] += 1 return n_subexpressions + def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds): + eliminated_vars = {} + eliminated_cons = set() + if not self.config.linear_presolve: + return eliminated_cons, eliminated_vars + + # We need to record all named expressions with linear components + # so that any eliminated variables are removed from them. + for expr, info, _ in self.subexpression_cache.values(): + if not info.linear: + continue + expr_id = id(expr) + for _id in info.linear: + comp_by_linear_var[_id].append((expr_id, info)) + + fixed_vars = [ + _id for _id, (lb, ub) in var_bounds.items() if lb == ub and lb is not None + ] + var_map = self.var_map + substitutions_by_linear_var = defaultdict(set) + template = self.template + one_var = lcon_by_linear_nnz[1] + two_var = lcon_by_linear_nnz[2] + while 1: + if fixed_vars: + _id = fixed_vars.pop() + a = x = None + b, _ = var_bounds[_id] + logger.debug("NL presolve: bounds fixed %s := %s", var_map[_id], b) + eliminated_vars[_id] = AMPLRepn(b, {}, None) + elif one_var: + con_id, info = one_var.popitem() + expr_info, lb = info + _id, coef = expr_info.linear.popitem() + # substituting _id with a*x + b + a = x = None + b = expr_info.const = (lb - expr_info.const) / coef + logger.debug("NL presolve: substituting %s := %s", var_map[_id], b) + eliminated_vars[_id] = expr_info + lb, ub = var_bounds[_id] + if (lb is not None and lb - b > TOL) or ( + ub is not None and ub - b < -TOL + ): + raise InfeasibleConstraintException( + "model contains a trivially infeasible variable " + f"'{var_map[_id].name}' (presolved to a value of " + f"{b} outside bounds [{lb}, {ub}])." + ) + eliminated_cons.add(con_id) + elif two_var: + con_id, info = two_var.popitem() + expr_info, lb = info + _id, coef = expr_info.linear.popitem() + id2, coef2 = expr_info.linear.popitem() + # + id2_isdiscrete = var_map[id2].domain.isdiscrete() + if var_map[_id].domain.isdiscrete() ^ id2_isdiscrete: + # if only one variable is discrete, then we need to + # substitute out the other + if id2_isdiscrete: + _id, id2 = id2, _id + coef, coef2 = coef2, coef + else: + # In an attempt to improve numerical stability, we will + # solve for (and substitute out) the variable with the + # coefficient closer to +/-1) + log_coef = _log10(abs(coef)) + log_coef2 = _log10(abs(coef2)) + if abs(log_coef2) < abs(log_coef) or ( + log_coef2 == -log_coef and log_coef2 < log_coef + ): + _id, id2 = id2, _id + coef, coef2 = coef2, coef + # substituting _id with a*x + b + a = -coef2 / coef + x = id2 + b = expr_info.const = (lb - expr_info.const) / coef + expr_info.linear[x] = a + substitutions_by_linear_var[x].add(_id) + eliminated_vars[_id] = expr_info + logger.debug( + "NL presolve: substituting %s := %s*%s + %s", + var_map[_id], + a, + var_map[x], + b, + ) + # Tighten variable bounds + x_lb, x_ub = var_bounds[x] + lb, ub = var_bounds[_id] + if lb is not None: + lb = (lb - b) / a + if ub is not None: + ub = (ub - b) / a + if a < 0: + lb, ub = ub, lb + if x_lb is None or (lb is not None and lb > x_lb): + x_lb = lb + if x_ub is None or (ub is not None and ub < x_ub): + x_ub = ub + var_bounds[x] = x_lb, x_ub + if x_lb == x_ub and x_lb is not None: + fixed_vars.append(x) + eliminated_cons.add(con_id) + else: + return eliminated_cons, eliminated_vars + for con_id, expr_info in comp_by_linear_var[_id]: + # Note that if we were aggregating (i.e., _id was + # from two_var), then one of these info's will be + # for the constraint we just eliminated. In this + # case, _id will no longer be in expr_info.linear - so c + # will be 0 - thereby preventing us from re-updating + # the expression. We still want it to persist so + # that if later substitutions replace x with + # something else, then the expr_info gets updated + # appropriately (that expr_info is persisting in the + # eliminated_vars dict - and we will use that to + # update other linear expressions later.) + old_nnz = len(expr_info.linear) + c = expr_info.linear.pop(_id, 0) + nnz = old_nnz - 1 + expr_info.const += c * b + if x in expr_info.linear: + expr_info.linear[x] += c * a + if expr_info.linear[x] == 0: + nnz -= 1 + coef = expr_info.linear.pop(x) + elif a: + expr_info.linear[x] = c * a + # replacing _id with x... NNZ is not changing, + # but we need to remember that x is now part of + # this constraint + comp_by_linear_var[x].append((con_id, expr_info)) + continue + _old = lcon_by_linear_nnz[old_nnz] + if con_id in _old: + if not nnz: + if abs(expr_info.const) > TOL: + # constraint is trivially infeasible + raise InfeasibleConstraintException( + "model contains a trivially infeasible constraint " + f"{expr_info.const} == {coef}*{var_map[x]}" + ) + # constraint is trivially feasible + eliminated_cons.add(con_id) + lcon_by_linear_nnz[nnz][con_id] = _old.pop(con_id) + # If variables were replaced by the variable that + # we are currently eliminating, then we need to update + # the representation of those variables + for resubst in substitutions_by_linear_var.pop(_id, ()): + expr_info = eliminated_vars[resubst] + c = expr_info.linear.pop(_id, 0) + expr_info.const += c * b + if x in expr_info.linear: + expr_info.linear[x] += c * a + elif a: + expr_info.linear[x] = c * a + def _record_named_expression_usage(self, named_exprs, src, comp_type): self.used_named_expressions.update(named_exprs) src = id(src) @@ -1497,33 +1908,18 @@ def _write_nl_expression(self, repn, include_const): # compiled before this point). Omitting the assertion for # efficiency. # assert repn.mult == 1 + # + # Note that repn.const should always be a int/float (it has + # already been compiled) if repn.nonlinear: nl, args = repn.nonlinear if include_const and repn.const: # Add the constant to the NL expression. AMPL adds the # constant as the second argument, so we will too. - nl = ( - self.template.binary_sum - + nl - + ( - self.template.const - % ( - repn.const - if repn.const.__class__ in int_float - else float(repn.const) - ) - ) - ) + nl = self.template.binary_sum + nl + self.template.const % repn.const self.ostream.write(nl % tuple(map(self.var_id_to_nl.__getitem__, args))) elif include_const: - self.ostream.write( - self.template.const - % ( - repn.const - if repn.const.__class__ in int_float - else float(repn.const) - ) - ) + self.ostream.write(self.template.const % repn.const) else: self.ostream.write(self.template.const % 0) @@ -1535,7 +1931,7 @@ def _write_v_line(self, expr_id, k): lbl = '\t#%s' % info[0].name else: lbl = '' - self.var_id_to_nl[expr_id] = f"{self.next_V_line_id}{lbl}" + self.var_id_to_nl[expr_id] = f"v{self.next_V_line_id}{lbl}" # Do NOT write out 0 coefficients here: doing so fouls up the # ASL's logic for calculating derivatives, leading to 'nan' in # the Hessian results. @@ -1543,10 +1939,7 @@ def _write_v_line(self, expr_id, k): # ostream.write(f'V{self.next_V_line_id} {len(linear)} {k}{lbl}\n') for _id in sorted(linear, key=column_order.__getitem__): - val = linear[_id] - if val.__class__ not in int_float: - val = float(val) - ostream.write(f'{column_order[_id]} {val!r}\n') + ostream.write(f'{column_order[_id]} {linear[_id]!r}\n') self._write_nl_expression(info[1], True) self.next_V_line_id += 1 @@ -1597,6 +1990,23 @@ def __str__(self): def __repr__(self): return str(self) + def __eq__(self, other): + return other.__class__ is AMPLRepn and ( + self.nl == other.nl + and self.mult == other.mult + and self.const == other.const + and self.linear == other.linear + and self.nonlinear == other.nonlinear + and self.named_exprs == other.named_exprs + ) + + def __hash__(self): + # Approximation of the Python default object hash + # (4 LSB are rolled to the MSB to reduce hash collisions) + return id(self) // 16 + ( + (id(self) & 15) << 8 * ctypes.sizeof(ctypes.c_void_p) - 4 + ) + def duplicate(self): ans = self.__class__.__new__(self.__class__) ans.nl = self.nl @@ -1671,9 +2081,7 @@ def compile_repn(self, visitor, prefix='', args=None, named_exprs=None): args.extend(self.nonlinear[1]) if self.const: nterms += 1 - nl_sum += template.const % ( - self.const if self.const.__class__ in int_float else float(self.const) - ) + nl_sum += template.const % self.const if nterms > 2: return (prefix + (template.nary_sum % nterms) + nl_sum, args, named_exprs) @@ -1786,6 +2194,24 @@ def append(self, other): elif _type is _CONSTANT: self.const += other[1] + def to_expr(self, var_map): + if self.nl is not None or self.nonlinear is not None: + # TODO: support converting general nonlinear expressiosn + # back to Pyomo expressions. This will require an AMPL + # parser. + raise MouseTrap("Cannot convert nonlinear AMPLRepn to Pyomo Expression") + if self.linear: + # Explicitly generate the LinearExpression. At time of + # writing, this is about 40% faster than standard operator + # overloading for O(1000) element sums + ans = LinearExpression( + [coef * var_map[vid] for vid, coef in self.linear.items()] + ) + ans += self.const + else: + ans = self.const + return ans * self.mult + def _create_strict_inequality_map(vars_): vars_['strict_inequality_map'] = { @@ -1833,7 +2259,7 @@ class text_nl_debug_template(object): less_equal = 'o23\t# le\n' equality = 'o24\t# eq\n' external_fcn = 'f%d %d%s\n' - var = 'v%s\n' + var = '%s\n' # NOTE: to support scaling, we do NOT include the 'v' here const = 'n%r\n' string = 'h%d:%s\n' monomial = product + const + var.replace('%', '%%') @@ -1983,7 +2409,7 @@ def handle_pow_node(visitor, node, arg1, arg2): if arg2[0] is _CONSTANT: if arg1[0] is _CONSTANT: ans = apply_node_operation(node, (arg1[1], arg2[1])) - if ans.__class__ in _complex_types: + if ans.__class__ in native_complex_types: ans = complex_number_error(ans, visitor, node) return _CONSTANT, ans elif not arg2[1]: @@ -2230,232 +2656,204 @@ def handle_external_function_node(visitor, node, *args): return (_GENERAL, AMPLRepn(0, None, nonlin)) -_operator_handles = { - NegationExpression: handle_negation_node, - ProductExpression: handle_product_node, - DivisionExpression: handle_division_node, - PowExpression: handle_pow_node, - AbsExpression: handle_abs_node, - UnaryFunctionExpression: handle_unary_node, - Expr_ifExpression: handle_exprif_node, - EqualityExpression: handle_equality_node, - InequalityExpression: handle_inequality_node, - RangedExpression: handle_ranged_inequality_node, - _GeneralExpressionData: handle_named_expression_node, - ScalarExpression: handle_named_expression_node, - kernel.expression.expression: handle_named_expression_node, - kernel.expression.noclone: handle_named_expression_node, - # Note: objectives are special named expressions - _GeneralObjectiveData: handle_named_expression_node, - ScalarObjective: handle_named_expression_node, - kernel.objective.objective: handle_named_expression_node, - ExternalFunctionExpression: handle_external_function_node, - # These are handled explicitly in beforeChild(): - # LinearExpression: handle_linear_expression, - # SumExpression: handle_sum_expression, - # - # Note: MonomialTermExpression is only hit when processing NPV - # subexpressions that raise errors (e.g., log(0) * m.x), so no - # special processing is needed [it is just a product expression] - MonomialTermExpression: handle_product_node, -} - - -def _before_native(visitor, child): - return False, (_CONSTANT, child) - - -def _before_complex(visitor, child): - return False, (_CONSTANT, complex_number_error(child, visitor, child)) - - -def _before_string(visitor, child): - visitor.encountered_string_arguments = True - ans = AMPLRepn(child, None, None) - ans.nl = (visitor.template.string % (len(child), child), ()) - return False, (_GENERAL, ans) - - -def _before_var(visitor, child): - _id = id(child) - if _id not in visitor.var_map: - if child.fixed: - return False, (_CONSTANT, visitor._eval_fixed(child)) - visitor.var_map[_id] = child - return False, (_MONOMIAL, _id, 1) - - -def _before_param(visitor, child): - return False, (_CONSTANT, visitor._eval_fixed(child)) - +_operator_handles = ExitNodeDispatcher( + { + NegationExpression: handle_negation_node, + ProductExpression: handle_product_node, + DivisionExpression: handle_division_node, + PowExpression: handle_pow_node, + AbsExpression: handle_abs_node, + UnaryFunctionExpression: handle_unary_node, + Expr_ifExpression: handle_exprif_node, + EqualityExpression: handle_equality_node, + InequalityExpression: handle_inequality_node, + RangedExpression: handle_ranged_inequality_node, + Expression: handle_named_expression_node, + ExternalFunctionExpression: handle_external_function_node, + # These are handled explicitly in beforeChild(): + # LinearExpression: handle_linear_expression, + # SumExpression: handle_sum_expression, + # + # Note: MonomialTermExpression is only hit when processing NPV + # subexpressions that raise errors (e.g., log(0) * m.x), so no + # special processing is needed [it is just a product expression] + MonomialTermExpression: handle_product_node, + } +) -def _before_npv(visitor, child): - try: - return False, (_CONSTANT, visitor._eval_expr(child)) - except (ValueError, ArithmeticError): - return True, None +class AMPLBeforeChildDispatcher(BeforeChildDispatcher): + __slots__ = () -def _before_monomial(visitor, child): - # - # The following are performance optimizations for common - # situations (Monomial terms and Linear expressions) - # - arg1, arg2 = child._args_ - if arg1.__class__ not in native_types: + def __init__(self): + # Special linear / summation expressions + self[MonomialTermExpression] = self._before_monomial + self[LinearExpression] = self._before_linear + self[SumExpression] = self._before_general_expression + + @staticmethod + def _record_var(visitor, var): + # We always add all indices to the var_map at once so that + # we can honor deterministic ordering of unordered sets + # (because the user could have iterated over an unordered + # set when constructing an expression, thereby altering the + # order in which we would see the variables) + vm = visitor.var_map try: - arg1 = visitor._eval_expr(arg1) - except (ValueError, ArithmeticError): - return True, None - - # Trap multiplication by 0 and nan. - if not arg1: - if arg2.fixed: - arg2 = visitor._eval_fixed(arg2) - if arg2 != arg2: - deprecation_warning( - f"Encountered {arg1}*{arg2} in expression tree. " - "Mapping the NaN result to 0 for compatibility " - "with the nl_v1 writer. In the future, this NaN " - "will be preserved/emitted to comply with IEEE-754.", - version='6.4.3', - ) - return False, (_CONSTANT, arg1) - - _id = id(arg2) - if _id not in visitor.var_map: - if arg2.fixed: - return False, (_CONSTANT, arg1 * visitor._eval_fixed(arg2)) - visitor.var_map[_id] = arg2 - return False, (_MONOMIAL, _id, arg1) - - -def _before_linear(visitor, child): - # Because we are going to modify the LinearExpression in this - # walker, we need to make a copy of the arg list from the original - # expression tree. - var_map = visitor.var_map - const = 0 - linear = {} - for arg in child.args: - if arg.__class__ is MonomialTermExpression: - arg1, arg2 = arg._args_ - if arg1.__class__ not in native_types: - try: - arg1 = visitor._eval_expr(arg1) - except (ValueError, ArithmeticError): - return True, None - - # Trap multiplication by 0 and nan. - if not arg1: - if arg2.fixed: - arg2 = visitor._eval_fixed(arg2) - if arg2 != arg2: - deprecation_warning( - f"Encountered {arg1}*{str(arg2.value)} in expression " - "tree. Mapping the NaN result to 0 for compatibility " - "with the nl_v1 writer. In the future, this NaN " - "will be preserved/emitted to comply with IEEE-754.", - version='6.4.3', - ) + _iter = var.parent_component().values(visitor.sorter) + except AttributeError: + # Note that this only works for the AML, as kernel does not + # provide a parent_component() + _iter = (var,) + for v in _iter: + if v.fixed: continue - - _id = id(arg2) - if _id not in var_map: - if arg2.fixed: - const += arg1 * visitor._eval_fixed(arg2) - continue - var_map[_id] = arg2 - linear[_id] = arg1 - elif _id in linear: - linear[_id] += arg1 - else: - linear[_id] = arg1 - elif arg.__class__ in native_types: - const += arg - else: + vm[id(v)] = v + + @staticmethod + def _before_string(visitor, child): + visitor.encountered_string_arguments = True + ans = AMPLRepn(child, None, None) + ans.nl = (visitor.template.string % (len(child), child), ()) + return False, (_GENERAL, ans) + + @staticmethod + def _before_var(visitor, child): + _id = id(child) + if _id not in visitor.var_map: + if child.fixed: + if _id not in visitor.fixed_vars: + visitor.cache_fixed_var(_id, child) + return False, (_CONSTANT, visitor.fixed_vars[_id]) + _before_child_handlers._record_var(visitor, child) + return False, (_MONOMIAL, _id, 1) + + @staticmethod + def _before_monomial(visitor, child): + # + # The following are performance optimizations for common + # situations (Monomial terms and Linear expressions) + # + arg1, arg2 = child._args_ + if arg1.__class__ not in native_types: try: - const += visitor._eval_expr(arg) + arg1 = visitor.check_constant(visitor.evaluate(arg1), arg1) except (ValueError, ArithmeticError): return True, None - if linear: - return False, (_GENERAL, AMPLRepn(const, linear, None)) - else: - return False, (_CONSTANT, const) - + # Trap multiplication by 0 and nan. + if not arg1: + if arg2.fixed: + _id = id(arg2) + if _id not in visitor.fixed_vars: + visitor.cache_fixed_var(id(arg2), arg2) + arg2 = visitor.fixed_vars[_id] + if arg2 != arg2: + deprecation_warning( + f"Encountered {arg1}*{arg2} in expression tree. " + "Mapping the NaN result to 0 for compatibility " + "with the nl_v1 writer. In the future, this NaN " + "will be preserved/emitted to comply with IEEE-754.", + version='6.4.3', + ) + return False, (_CONSTANT, arg1) + + _id = id(arg2) + if _id not in visitor.var_map: + if arg2.fixed: + if _id not in visitor.fixed_vars: + visitor.cache_fixed_var(_id, arg2) + return False, (_CONSTANT, arg1 * visitor.fixed_vars[_id]) + _before_child_handlers._record_var(visitor, arg2) + return False, (_MONOMIAL, _id, arg1) + + @staticmethod + def _before_linear(visitor, child): + # Because we are going to modify the LinearExpression in this + # walker, we need to make a copy of the arg list from the original + # expression tree. + var_map = visitor.var_map + const = 0 + linear = {} + for arg in child.args: + if arg.__class__ is MonomialTermExpression: + arg1, arg2 = arg._args_ + if arg1.__class__ not in native_types: + try: + arg1 = visitor.check_constant(visitor.evaluate(arg1), arg1) + except (ValueError, ArithmeticError): + return True, None + + # Trap multiplication by 0 and nan. + if not arg1: + if arg2.fixed: + arg2 = visitor.check_constant(arg2.value, arg2) + if arg2 != arg2: + deprecation_warning( + f"Encountered {arg1}*{str(arg2.value)} in expression " + "tree. Mapping the NaN result to 0 for compatibility " + "with the nl_v1 writer. In the future, this NaN " + "will be preserved/emitted to comply with IEEE-754.", + version='6.4.3', + ) + continue -def _before_named_expression(visitor, child): - _id = id(child) - if _id in visitor.subexpression_cache: - obj, repn, info = visitor.subexpression_cache[_id] - if info[2]: - if repn.linear: - return False, (_MONOMIAL, next(iter(repn.linear)), 1) + _id = id(arg2) + if _id not in var_map: + if arg2.fixed: + if _id not in visitor.fixed_vars: + visitor.cache_fixed_var(_id, arg2) + const += arg1 * visitor.fixed_vars[_id] + continue + _before_child_handlers._record_var(visitor, arg2) + linear[_id] = arg1 + elif _id in linear: + linear[_id] += arg1 + else: + linear[_id] = arg1 + elif arg.__class__ in native_types: + const += arg + elif arg.is_variable_type(): + _id = id(arg) + if _id not in var_map: + if arg.fixed: + if _id not in visitor.fixed_vars: + visitor.cache_fixed_var(_id, arg) + const += visitor.fixed_vars[_id] + continue + _before_child_handlers._record_var(visitor, arg) + linear[_id] = 1 + elif _id in linear: + linear[_id] += 1 + else: + linear[_id] = 1 else: - return False, (_CONSTANT, repn.const) - return False, (_GENERAL, repn.duplicate()) - else: - return True, None - - -def _before_general_expression(visitor, child): - return True, None - + try: + const += visitor.check_constant(visitor.evaluate(arg), arg) + except (ValueError, ArithmeticError): + return True, None -def _register_new_before_child_handler(visitor, child): - handlers = _before_child_handlers - child_type = child.__class__ - if child_type in native_numeric_types: - if isinstance(child_type, complex): - _complex_types.add(child_type) - handlers[child_type] = _before_complex + if linear: + return False, (_GENERAL, AMPLRepn(const, linear, None)) else: - handlers[child_type] = _before_native - elif issubclass(child_type, str): - handlers[child_type] = _before_string - elif child_type in native_types: - handlers[child_type] = _before_native - elif not child.is_expression_type(): - if child.is_potentially_variable(): - handlers[child_type] = _before_var - else: - handlers[child_type] = _before_param - elif not child.is_potentially_variable(): - handlers[child_type] = _before_npv - # If we descend into the named expression (because of an - # evaluation error), then on the way back out, we will use - # the potentially variable handler to process the result. - pv_base_type = child.potentially_variable_base_class() - if pv_base_type not in handlers: - try: - child.__class__ = pv_base_type - _register_new_before_child_handler(visitor, child) - finally: - child.__class__ = child_type - if pv_base_type in _operator_handles: - _operator_handles[child_type] = _operator_handles[pv_base_type] - elif id(child) in visitor.subexpression_cache or issubclass( - child_type, _GeneralExpressionData - ): - handlers[child_type] = _before_named_expression - _operator_handles[child_type] = handle_named_expression_node - else: - handlers[child_type] = _before_general_expression - return handlers[child_type](visitor, child) + return False, (_CONSTANT, const) + @staticmethod + def _before_named_expression(visitor, child): + _id = id(child) + if _id in visitor.subexpression_cache: + obj, repn, info = visitor.subexpression_cache[_id] + if info[2]: + if repn.linear: + return False, (_MONOMIAL, next(iter(repn.linear)), 1) + else: + return False, (_CONSTANT, repn.const) + return False, (_GENERAL, repn.duplicate()) + else: + return True, None -_before_child_handlers = defaultdict(lambda: _register_new_before_child_handler) -_complex_types = set((complex,)) -_before_child_handlers[complex] = _before_complex -for _type in native_types: - if issubclass(_type, str): - _before_child_handlers[_type] = _before_string -# Special linear / summation expressions -_before_child_handlers[MonomialTermExpression] = _before_monomial -_before_child_handlers[LinearExpression] = _before_linear -_before_child_handlers[SumExpression] = _before_general_expression +_before_child_handlers = AMPLBeforeChildDispatcher() class AMPLRepnVisitor(StreamBasedExpressionVisitor): @@ -2469,6 +2867,7 @@ def __init__( used_named_expressions, symbolic_solver_labels, use_named_exprs, + sorter, ): super().__init__() self.template = template @@ -2481,18 +2880,22 @@ def __init__( self.symbolic_solver_labels = symbolic_solver_labels self.use_named_exprs = use_named_exprs self.encountered_string_arguments = False + self.fixed_vars = {} self._eval_expr_visitor = _EvaluationVisitor(True) + self.evaluate = self._eval_expr_visitor.dfs_postorder_stack + self.sorter = sorter - def _eval_fixed(self, obj): - ans = obj.value + def check_constant(self, ans, obj): if ans.__class__ not in native_numeric_types: # None can be returned from uninitialized Var/Param objects if ans is None: return InvalidNumber( - None, f"'{obj}' contains a nonnumeric value '{ans}'" + None, f"'{obj}' evaluated to a nonnumeric value '{ans}'" ) if ans.__class__ is InvalidNumber: return ans + elif ans.__class__ in native_complex_types: + return complex_number_error(ans, self, obj) else: # It is possible to get other non-numeric types. Most # common are bool and 1-element numpy.array(). We will @@ -2506,47 +2909,27 @@ def _eval_fixed(self, obj): ans = float(ans) except: return InvalidNumber( - ans, f"'{obj}' contains a nonnumeric value '{ans}'" + ans, f"'{obj}' evaluated to a nonnumeric value '{ans}'" ) if ans != ans: - return InvalidNumber(nan, f"'{obj}' contains a nonnumeric value '{ans}'") - if ans.__class__ in _complex_types: - return complex_number_error(ans, self, obj) + return InvalidNumber( + nan, f"'{obj}' evaluated to a nonnumeric value '{ans}'" + ) return ans - def _eval_expr(self, expr): - ans = self._eval_expr_visitor.dfs_postorder_stack(expr) - if ans.__class__ not in native_numeric_types: - # None can be returned from uninitialized Expression objects - if ans is None: - return InvalidNumber( - ans, f"'{expr}' evaluated to nonnumeric value '{ans}'" - ) - if ans.__class__ is InvalidNumber: - return ans - else: - # It is possible to get other non-numeric types. Most - # common are bool and 1-element numpy.array(). We will - # attempt to convert the value to a float before - # proceeding. - # - # TODO: we should check bool and warn/error (while bool is - # convertible to float in Python, they have very - # different semantic meanings in Pyomo). - try: - ans = float(ans) - except: - return InvalidNumber( - ans, f"'{expr}' evaluated to nonnumeric value '{ans}'" - ) - if ans != ans: - return InvalidNumber(ans, f"'{expr}' evaluated to nonnumeric value '{ans}'") - if ans.__class__ in _complex_types: - return complex_number_error(ans, self, expr) - return ans + def cache_fixed_var(self, _id, child): + val = self.check_constant(child.value, child) + lb, ub = child.bounds + if (lb is not None and lb - val > TOL) or (ub is not None and ub - val < -TOL): + raise InfeasibleConstraintException( + "model contains a trivially infeasible " + f"variable '{child.name}' (fixed value " + f"{val} outside bounds [{lb}, {ub}])." + ) + self.fixed_vars[_id] = self.check_constant(child.value, child) def initializeWalker(self, expr): - expr, src, src_idx = expr + expr, src, src_idx, self.expression_scaling_factor = expr self.active_expression_source = (src_idx, id(src)) walk, result = self.beforeChild(None, expr, 0) if not walk: @@ -2581,6 +2964,9 @@ def exitNode(self, node, data): def finalizeResult(self, result): ans = node_result_to_amplrepn(result) + # Multiply the expression by the scaling factor provided by the caller + ans.mult *= self.expression_scaling_factor + # If this was a nonlinear named expression, and that expression # has no linear portion, then we will directly use this as a # named expression. We need to mark that the expression was @@ -2612,7 +2998,6 @@ def finalizeResult(self, result): # variables are not accidentally re-characterized as # nonlinear. pass - # ans.nonlinear = orig.nonlinear ans.nl = None if ans.nonlinear.__class__ is list: @@ -2620,8 +3005,8 @@ def finalizeResult(self, result): if not ans.linear: ans.linear = {} - linear = ans.linear if ans.mult != 1: + linear = ans.linear mult, ans.mult = ans.mult, 1 ans.const *= mult if linear: diff --git a/pyomo/repn/plugins/standard_form.py b/pyomo/repn/plugins/standard_form.py new file mode 100644 index 00000000000..e684829e2f4 --- /dev/null +++ b/pyomo/repn/plugins/standard_form.py @@ -0,0 +1,595 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import collections +import logging +from operator import attrgetter + +from pyomo.common.config import ( + ConfigBlock, + ConfigValue, + InEnum, + document_kwargs_from_configdict, +) +from pyomo.common.dependencies import scipy, numpy as np +from pyomo.common.enums import ObjectiveSense +from pyomo.common.gc_manager import PauseGC +from pyomo.common.timing import TicTocTimer + +from pyomo.core.base import ( + Block, + Objective, + Constraint, + Var, + Param, + Expression, + SortComponents, + Suffix, + SymbolMap, + maximize, +) +from pyomo.opt import WriterFactory +from pyomo.repn.linear import LinearRepnVisitor +from pyomo.repn.util import ( + FileDeterminism, + FileDeterminism_to_SortComponents, + categorize_valid_components, + initialize_var_map_from_column_order, + ordered_active_constraints, +) + +### FIXME: Remove the following as soon as non-active components no +### longer report active==True +from pyomo.core.base import Set, RangeSet, ExternalFunction +from pyomo.network import Port + +logger = logging.getLogger(__name__) + +RowEntry = collections.namedtuple('RowEntry', ['constraint', 'bound_type']) + + +# TODO: make a proper base class +class LinearStandardFormInfo(object): + """Return type for LinearStandardFormCompiler.write() + + Attributes + ---------- + c : scipy.sparse.csc_array + + The objective coefficients. Note that this is a sparse array + and may contain multiple rows (for multiobjective problems). The + objectives may be calculated by "c @ x" + + c_offset : numpy.ndarray + + The list of objective constant offsets + + A : scipy.sparse.csc_array + + The constraint coefficients. The constraint bodies may be + calculated by "A @ x" + + rhs : numpy.ndarray + + The constraint right-hand sides. + + rows : List[Tuple[ConstraintData, int]] + + The list of Pyomo constraint objects corresponding to the rows + in `A`. Each element in the list is a 2-tuple of + (ConstraintData, row_multiplier). The `row_multiplier` will be + +/- 1 indicating if the row was multiplied by -1 (corresponding + to a constraint lower bound) or +1 (upper bound). + + columns : List[VarData] + + The list of Pyomo variable objects corresponding to columns in + the `A` and `c` matrices. + + objectives : List[ObjectiveData] + + The list of Pyomo objective objects corresponding to the active objectives + + eliminated_vars: List[Tuple[VarData, NumericExpression]] + + The list of variables from the original model that do not appear + in the standard form (usually because they were replaced by + nonnegative variables). Each entry is a 2-tuple of + (:py:class:`VarData`, :py:class`NumericExpression`|`float`). + The list is in the necessary order for correct evaluation (i.e., + all variables appearing in the expression must either have + appeared in the standard form, or appear *earlier* in this list. + + """ + + def __init__(self, c, c_offset, A, rhs, rows, columns, objectives, eliminated_vars): + self.c = c + self.c_offset = c_offset + self.A = A + self.rhs = rhs + self.rows = rows + self.columns = columns + self.objectives = objectives + self.eliminated_vars = eliminated_vars + + @property + def x(self): + return self.columns + + @property + def b(self): + return self.rhs + + +@WriterFactory.register( + 'compile_standard_form', 'Compile an LP to standard form (`min cTx s.t. Ax <= b`)' +) +class LinearStandardFormCompiler(object): + CONFIG = ConfigBlock('compile_standard_form') + CONFIG.declare( + 'nonnegative_vars', + ConfigValue( + default=False, + domain=bool, + description='Convert all variables to be nonnegative variables', + ), + ) + CONFIG.declare( + 'slack_form', + ConfigValue( + default=False, + domain=bool, + description='Add slack variables and return `min cTx s.t. Ax == b`', + ), + ) + CONFIG.declare( + 'mixed_form', + ConfigValue( + default=False, + domain=bool, + description='Return A in mixed form (the comparison operator is a ' + 'mix of <=, ==, and >=)', + ), + ) + CONFIG.declare( + 'set_sense', + ConfigValue( + default=ObjectiveSense.minimize, + domain=InEnum(ObjectiveSense), + description='If not None, map all objectives to the specified sense.', + ), + ) + CONFIG.declare( + 'show_section_timing', + ConfigValue( + default=False, + domain=bool, + description='Print timing after each stage of the compilation process', + ), + ) + CONFIG.declare( + 'file_determinism', + ConfigValue( + default=FileDeterminism.ORDERED, + domain=InEnum(FileDeterminism), + description='How much effort to ensure result is deterministic', + doc=""" + How much effort do we want to put into ensuring the + resulting matrices are produced deterministically: + NONE (0) : None + ORDERED (10): rely on underlying component ordering (default) + SORT_INDICES (20) : sort keys of indexed components + SORT_SYMBOLS (30) : sort keys AND sort names (not declaration order) + """, + ), + ) + CONFIG.declare( + 'row_order', + ConfigValue( + default=None, + description='Preferred constraint ordering', + doc=""" + List of constraints in the order that they should appear in + the resulting `A` matrix. Unspecified constraints will + appear at the end.""", + ), + ) + CONFIG.declare( + 'column_order', + ConfigValue( + default=None, + description='Preferred variable ordering', + doc=""" + List of variables in the order that they should appear in + the compiled representation. Unspecified variables will be + appended to the end of this list.""", + ), + ) + + def __init__(self): + self.config = self.CONFIG() + + @document_kwargs_from_configdict(CONFIG) + def write(self, model, ostream=None, **options): + """Convert a model to standard form (`min cTx s.t. Ax <= b`) + + Returns + ------- + LinearStandardFormInfo + + Parameters + ---------- + model: ConcreteModel + The concrete Pyomo model to write out. + + ostream: None + This is provided for API compatibility with other writers + and is ignored here. + + """ + config = self.config(options) + + # Pause the GC, as the walker that generates the compiled LP + # representation generates (and disposes of) a large number of + # small objects. + with PauseGC(): + return _LinearStandardFormCompiler_impl(config).write(model) + + +class _LinearStandardFormCompiler_impl(object): + def __init__(self, config): + self.config = config + + def write(self, model): + timing_logger = logging.getLogger('pyomo.common.timing.writer') + timer = TicTocTimer(logger=timing_logger) + with_debug_timing = ( + timing_logger.isEnabledFor(logging.DEBUG) and timing_logger.hasHandlers() + ) + + sorter = FileDeterminism_to_SortComponents(self.config.file_determinism) + component_map, unknown = categorize_valid_components( + model, + active=True, + sort=sorter, + valid={ + Block, + Constraint, + Var, + Param, + Expression, + # FIXME: Non-active components should not report as Active + ExternalFunction, + Set, + RangeSet, + Port, + # TODO: Piecewise, Complementarity + }, + targets={Suffix, Objective}, + ) + if unknown: + raise ValueError( + "The model ('%s') contains the following active components " + "that the Linear Standard Form compiler does not know how to " + "process:\n\t%s" + % ( + model.name, + "\n\t".join( + "%s:\n\t\t%s" % (k, "\n\t\t".join(map(attrgetter('name'), v))) + for k, v in unknown.items() + ), + ) + ) + + self.var_map = var_map = {} + initialize_var_map_from_column_order(model, self.config, var_map) + var_order = {_id: i for i, _id in enumerate(var_map)} + + visitor = LinearRepnVisitor({}, var_map, var_order, sorter) + + timer.toc('Initialized column order', level=logging.DEBUG) + + # We don't export any suffix information to the Standard Form + # + if component_map[Suffix]: + suffixesByName = {} + for block in component_map[Suffix]: + for suffix in block.component_objects( + Suffix, active=True, descend_into=False, sort=sorter + ): + if not suffix.export_enabled() or not suffix: + continue + name = suffix.local_name + if name in suffixesByName: + suffixesByName[name].append(suffix) + else: + suffixesByName[name] = [suffix] + for name, suffixes in suffixesByName.items(): + n = len(suffixes) + plural = 's' if n > 1 else '' + logger.warning( + f"EXPORT Suffix '{name}' found on {n} block{plural}:\n " + + "\n ".join(s.name for s in suffixes) + + "\nStandard Form compiler ignores export suffixes. Skipping." + ) + + # + # Process objective + # + set_sense = self.config.set_sense + objectives = [] + for blk in component_map[Objective]: + objectives.extend( + blk.component_data_objects( + Objective, active=True, descend_into=False, sort=sorter + ) + ) + obj_offset = [] + obj_data = [] + obj_index = [] + obj_index_ptr = [0] + for obj in objectives: + repn = visitor.walk_expression(obj.expr) + if repn.nonlinear is not None: + raise ValueError( + f"Model objective ({obj.name}) contains nonlinear terms that " + "cannot be compiled to standard (linear) form." + ) + N = len(repn.linear) + obj_data.append(np.fromiter(repn.linear.values(), float, N)) + obj_offset.append(repn.constant) + if set_sense is not None and set_sense != obj.sense: + obj_data[-1] *= -1 + obj_offset[-1] *= -1 + obj_index.append( + np.fromiter(map(var_order.__getitem__, repn.linear), float, N) + ) + obj_index_ptr.append(obj_index_ptr[-1] + N) + if with_debug_timing: + timer.toc('Objective %s', obj, level=logging.DEBUG) + + # + # Tabulate constraints + # + slack_form = self.config.slack_form + mixed_form = self.config.mixed_form + if slack_form and mixed_form: + raise ValueError("cannot specify both slack_form and mixed_form") + rows = [] + rhs = [] + con_data = [] + con_index = [] + con_index_ptr = [0] + last_parent = None + for con in ordered_active_constraints(model, self.config): + if with_debug_timing and con.parent_component() is not last_parent: + if last_parent is not None: + timer.toc('Constraint %s', last_parent, level=logging.DEBUG) + last_parent = con.parent_component() + # Note: Constraint.lb/ub guarantee a return value that is + # either a (finite) native_numeric_type, or None + lb = con.lb + ub = con.ub + + repn = visitor.walk_expression(con.body) + + if lb is None and ub is None: + # Note: you *cannot* output trivial (unbounded) + # constraints in matrix format. I suppose we could add a + # slack variable, but that seems rather silly. + continue + if repn.nonlinear is not None: + raise ValueError( + f"Model constraint ({con.name}) contains nonlinear terms that " + "cannot be compiled to standard (linear) form." + ) + + # Pull out the constant: we will move it to the bounds + offset = repn.constant + repn.constant = 0 + + if not repn.linear: + if (lb is None or lb <= offset) and (ub is None or ub >= offset): + continue + raise InfeasibleError( + f"model contains a trivially infeasible constraint, '{con.name}'" + ) + + if mixed_form: + N = len(repn.linear) + _data = np.fromiter(repn.linear.values(), float, N) + _index = np.fromiter(map(var_order.__getitem__, repn.linear), float, N) + if ub == lb: + rows.append(RowEntry(con, 0)) + rhs.append(ub - offset) + con_data.append(_data) + con_index.append(_index) + con_index_ptr.append(con_index_ptr[-1] + N) + else: + if ub is not None: + rows.append(RowEntry(con, 1)) + rhs.append(ub - offset) + con_data.append(_data) + con_index.append(_index) + con_index_ptr.append(con_index_ptr[-1] + N) + if lb is not None: + rows.append(RowEntry(con, -1)) + rhs.append(lb - offset) + con_data.append(_data) + con_index.append(_index) + con_index_ptr.append(con_index_ptr[-1] + N) + elif slack_form: + _data = list(repn.linear.values()) + _index = list(map(var_order.__getitem__, repn.linear)) + if lb == ub: # TODO: add tolerance? + rhs.append(ub - offset) + else: + # add slack variable + v = Var(name=f'_slack_{len(rhs)}', bounds=(None, None)) + v.construct() + if lb is None: + rhs.append(ub - offset) + v.lb = 0 + else: + rhs.append(lb - offset) + v.ub = 0 + if ub is not None: + v.lb = lb - ub + var_map[id(v)] = v + var_order[id(v)] = slack_col = len(var_order) + _data.append(1) + _index.append(slack_col) + rows.append(RowEntry(con, 1)) + con_data.append(np.array(_data)) + con_index.append(np.array(_index)) + con_index_ptr.append(con_index_ptr[-1] + len(_index)) + else: + N = len(repn.linear) + _data = np.fromiter(repn.linear.values(), float, N) + _index = np.fromiter(map(var_order.__getitem__, repn.linear), float, N) + if ub is not None: + rows.append(RowEntry(con, 1)) + rhs.append(ub - offset) + con_data.append(_data) + con_index.append(_index) + con_index_ptr.append(con_index_ptr[-1] + N) + if lb is not None: + rows.append(RowEntry(con, -1)) + rhs.append(offset - lb) + con_data.append(-_data) + con_index.append(_index) + con_index_ptr.append(con_index_ptr[-1] + N) + + if with_debug_timing: + # report the last constraint + timer.toc('Constraint %s', last_parent, level=logging.DEBUG) + + # Get the variable list + columns = list(var_map.values()) + # Convert the compiled data to scipy sparse matrices + if obj_data: + obj_data = np.concatenate(obj_data) + obj_index = np.concatenate(obj_index) + c = scipy.sparse.csr_array( + (obj_data, obj_index, obj_index_ptr), [len(obj_index_ptr) - 1, len(columns)] + ).tocsc() + if rows: + con_data = np.concatenate(con_data) + con_index = np.concatenate(con_index) + A = scipy.sparse.csr_array( + (con_data, con_index, con_index_ptr), [len(rows), len(columns)] + ).tocsc() + + # Some variables in the var_map may not actually appear in the + # objective or constraints (e.g., added from col_order, or + # multiplied by 0 in the expressions). The easiest way to check + # for empty columns is to convert from CSR to CSC and then look + # at the index pointer list (an O(num_var) operation). + c_ip = c.indptr + A_ip = A.indptr + active_var_mask = (A_ip[1:] > A_ip[:-1]) | (c_ip[1:] > c_ip[:-1]) + + # Masks on NumPy arrays are very fast. Build the reduced A + # indptr and then check if we actually have to manipulate the + # columns + augmented_mask = np.concatenate((active_var_mask, [True])) + reduced_A_indptr = A.indptr[augmented_mask] + nCol = len(reduced_A_indptr) - 1 + if nCol != len(columns): + columns = [v for k, v in zip(active_var_mask, columns) if k] + c = scipy.sparse.csc_array( + (c.data, c.indices, c.indptr[augmented_mask]), [c.shape[0], nCol] + ) + # active_var_idx[-1] = len(columns) + A = scipy.sparse.csc_array( + (A.data, A.indices, reduced_A_indptr), [A.shape[0], nCol] + ) + + if self.config.nonnegative_vars: + c, A, columns, eliminated_vars = _csc_to_nonnegative_vars(c, A, columns) + else: + eliminated_vars = [] + + info = LinearStandardFormInfo( + c, np.array(obj_offset), A, rhs, rows, columns, objectives, eliminated_vars + ) + timer.toc("Generated linear standard form representation", delta=False) + return info + + +def _csc_to_nonnegative_vars(c, A, columns): + eliminated_vars = [] + new_columns = [] + new_c_data = [] + new_c_indices = [] + new_c_indptr = [0] + new_A_data = [] + new_A_indices = [] + new_A_indptr = [0] + for i, v in enumerate(columns): + lb, ub = v.bounds + if lb is None or lb < 0: + name = v.name + new_columns.append( + Var( + name=f'_neg_{i}', + domain=v.domain, + bounds=(0, None if lb is None else -lb), + ) + ) + new_columns[-1].construct() + s, e = A.indptr[i : i + 2] + new_A_data.append(-A.data[s:e]) + new_A_indices.append(A.indices[s:e]) + new_A_indptr.append(new_A_indptr[-1] + e - s) + s, e = c.indptr[i : i + 2] + new_c_data.append(-c.data[s:e]) + new_c_indices.append(c.indices[s:e]) + new_c_indptr.append(new_c_indptr[-1] + e - s) + if ub is None or ub > 0: + # Crosses 0; split into 2 vars + new_columns.append( + Var(name=f'_pos_{i}', domain=v.domain, bounds=(0, ub)) + ) + new_columns[-1].construct() + s, e = A.indptr[i : i + 2] + new_A_data.append(A.data[s:e]) + new_A_indices.append(A.indices[s:e]) + new_A_indptr.append(new_A_indptr[-1] + e - s) + s, e = c.indptr[i : i + 2] + new_c_data.append(c.data[s:e]) + new_c_indices.append(c.indices[s:e]) + new_c_indptr.append(new_c_indptr[-1] + e - s) + eliminated_vars.append((v, new_columns[-1] - new_columns[-2])) + else: + new_columns[-1].lb = -ub + eliminated_vars.append((v, -new_columns[-1])) + else: # lb >= 0 + new_columns.append(v) + s, e = A.indptr[i : i + 2] + new_A_data.append(A.data[s:e]) + new_A_indices.append(A.indices[s:e]) + new_A_indptr.append(new_A_indptr[-1] + e - s) + s, e = c.indptr[i : i + 2] + new_c_data.append(c.data[s:e]) + new_c_indices.append(c.indices[s:e]) + new_c_indptr.append(new_c_indptr[-1] + e - s) + + nCol = len(new_columns) + c = scipy.sparse.csc_array( + (np.concatenate(new_c_data), np.concatenate(new_c_indices), new_c_indptr), + [c.shape[0], nCol], + ) + A = scipy.sparse.csc_array( + (np.concatenate(new_A_data), np.concatenate(new_A_indices), new_A_indptr), + [A.shape[0], nCol], + ) + return c, A, new_columns, eliminated_vars diff --git a/pyomo/repn/quadratic.py b/pyomo/repn/quadratic.py index fbe3860078a..f6e0a43623d 100644 --- a/pyomo/repn/quadratic.py +++ b/pyomo/repn/quadratic.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -28,7 +28,7 @@ InequalityExpression, RangedExpression, ) -from pyomo.core.base.expression import ScalarExpression +from pyomo.core.base.expression import Expression from . import linear from .linear import _merge_dict, to_expression @@ -98,22 +98,15 @@ def to_expression(self, visitor): e += coef * (var_map[x1] * var_map[x2]) ans += e if self.linear: - if len(self.linear) == 1: - vid, coef = next(iter(self.linear.items())) - if coef == 1: - ans += var_map[vid] - elif coef: - ans += MonomialTermExpression((coef, var_map[vid])) - else: - pass - else: - ans += LinearExpression( - [ - MonomialTermExpression((coef, var_map[vid])) - for vid, coef in self.linear.items() - if coef - ] - ) + var_map = visitor.var_map + with mutable_expression() as e: + for vid, coef in self.linear.items(): + if coef: + e += coef * var_map[vid] + if e.nargs() > 1: + ans += e + elif e.nargs() == 1: + ans += e.arg(0) if self.constant: ans += self.constant if self.multiplier != 1: @@ -284,18 +277,11 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): _exit_node_handlers[ProductExpression].update( { + None: _handle_product_nonlinear, (_CONSTANT, _QUADRATIC): linear._handle_product_constant_ANY, - (_LINEAR, _QUADRATIC): _handle_product_nonlinear, - (_QUADRATIC, _QUADRATIC): _handle_product_nonlinear, - (_GENERAL, _QUADRATIC): _handle_product_nonlinear, (_QUADRATIC, _CONSTANT): linear._handle_product_ANY_constant, - (_QUADRATIC, _LINEAR): _handle_product_nonlinear, - (_QUADRATIC, _GENERAL): _handle_product_nonlinear, # Replace handler from the linear walker (_LINEAR, _LINEAR): _handle_product_linear_linear, - (_GENERAL, _GENERAL): _handle_product_nonlinear, - (_GENERAL, _LINEAR): _handle_product_nonlinear, - (_LINEAR, _GENERAL): _handle_product_nonlinear, } ) @@ -303,15 +289,7 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): # DIVISION # _exit_node_handlers[DivisionExpression].update( - { - (_CONSTANT, _QUADRATIC): linear._handle_division_nonlinear, - (_LINEAR, _QUADRATIC): linear._handle_division_nonlinear, - (_QUADRATIC, _QUADRATIC): linear._handle_division_nonlinear, - (_GENERAL, _QUADRATIC): linear._handle_division_nonlinear, - (_QUADRATIC, _CONSTANT): linear._handle_division_ANY_constant, - (_QUADRATIC, _LINEAR): linear._handle_division_nonlinear, - (_QUADRATIC, _GENERAL): linear._handle_division_nonlinear, - } + {(_QUADRATIC, _CONSTANT): linear._handle_division_ANY_constant} ) @@ -319,87 +297,47 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): # EXPONENTIATION # _exit_node_handlers[PowExpression].update( - { - (_CONSTANT, _QUADRATIC): linear._handle_pow_nonlinear, - (_LINEAR, _QUADRATIC): linear._handle_pow_nonlinear, - (_QUADRATIC, _QUADRATIC): linear._handle_pow_nonlinear, - (_GENERAL, _QUADRATIC): linear._handle_pow_nonlinear, - (_QUADRATIC, _CONSTANT): linear._handle_pow_ANY_constant, - (_QUADRATIC, _LINEAR): linear._handle_pow_nonlinear, - (_QUADRATIC, _GENERAL): linear._handle_pow_nonlinear, - } + {(_QUADRATIC, _CONSTANT): linear._handle_pow_ANY_constant} ) # # ABS and UNARY handlers # -_exit_node_handlers[AbsExpression][(_QUADRATIC,)] = linear._handle_unary_nonlinear -_exit_node_handlers[UnaryFunctionExpression][ - (_QUADRATIC,) -] = linear._handle_unary_nonlinear +# (no changes needed) # # NAMED EXPRESSION handlers # -_exit_node_handlers[ScalarExpression][(_QUADRATIC,)] = linear._handle_named_ANY +# (no changes needed) # # EXPR_IF handlers # # Note: it is easier to just recreate the entire data structure, rather # than update it -_exit_node_handlers[Expr_ifExpression] = { - (i, j, k): linear._handle_expr_if_nonlinear - for i in (_LINEAR, _QUADRATIC, _GENERAL) - for j in (_CONSTANT, _LINEAR, _QUADRATIC, _GENERAL) - for k in (_CONSTANT, _LINEAR, _QUADRATIC, _GENERAL) -} -for j in (_CONSTANT, _LINEAR, _QUADRATIC, _GENERAL): - for k in (_CONSTANT, _LINEAR, _QUADRATIC, _GENERAL): - _exit_node_handlers[Expr_ifExpression][ - _CONSTANT, j, k - ] = linear._handle_expr_if_const - -# -# RELATIONAL handlers -# -_exit_node_handlers[EqualityExpression].update( +_exit_node_handlers[Expr_ifExpression].update( { - (_CONSTANT, _QUADRATIC): linear._handle_equality_general, - (_LINEAR, _QUADRATIC): linear._handle_equality_general, - (_QUADRATIC, _QUADRATIC): linear._handle_equality_general, - (_GENERAL, _QUADRATIC): linear._handle_equality_general, - (_QUADRATIC, _CONSTANT): linear._handle_equality_general, - (_QUADRATIC, _LINEAR): linear._handle_equality_general, - (_QUADRATIC, _GENERAL): linear._handle_equality_general, + (_CONSTANT, i, _QUADRATIC): linear._handle_expr_if_const + for i in (_CONSTANT, _LINEAR, _QUADRATIC, _GENERAL) } ) -_exit_node_handlers[InequalityExpression].update( +_exit_node_handlers[Expr_ifExpression].update( { - (_CONSTANT, _QUADRATIC): linear._handle_inequality_general, - (_LINEAR, _QUADRATIC): linear._handle_inequality_general, - (_QUADRATIC, _QUADRATIC): linear._handle_inequality_general, - (_GENERAL, _QUADRATIC): linear._handle_inequality_general, - (_QUADRATIC, _CONSTANT): linear._handle_inequality_general, - (_QUADRATIC, _LINEAR): linear._handle_inequality_general, - (_QUADRATIC, _GENERAL): linear._handle_inequality_general, - } -) -_exit_node_handlers[RangedExpression].update( - { - (_CONSTANT, _QUADRATIC): linear._handle_ranged_general, - (_LINEAR, _QUADRATIC): linear._handle_ranged_general, - (_QUADRATIC, _QUADRATIC): linear._handle_ranged_general, - (_GENERAL, _QUADRATIC): linear._handle_ranged_general, - (_QUADRATIC, _CONSTANT): linear._handle_ranged_general, - (_QUADRATIC, _LINEAR): linear._handle_ranged_general, - (_QUADRATIC, _GENERAL): linear._handle_ranged_general, + (_CONSTANT, _QUADRATIC, i): linear._handle_expr_if_const + for i in (_CONSTANT, _LINEAR, _GENERAL) } ) +# +# RELATIONAL handlers +# +# (no changes needed) + class QuadraticRepnVisitor(linear.LinearRepnVisitor): Result = QuadraticRepn exit_node_handlers = _exit_node_handlers - exit_node_dispatcher = linear._initialize_exit_node_dispatcher(_exit_node_handlers) + exit_node_dispatcher = linear.ExitNodeDispatcher( + linear._initialize_exit_node_dispatcher(_exit_node_handlers) + ) max_exponential_expansion = 2 diff --git a/pyomo/repn/standard_aux.py b/pyomo/repn/standard_aux.py index 7995949fc05..403320c462c 100644 --- a/pyomo/repn/standard_aux.py +++ b/pyomo/repn/standard_aux.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,10 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from __future__ import division - -__all__ = ['compute_standard_repn'] - from pyomo.repn.standard_repn import ( preprocess_block_constraints, diff --git a/pyomo/repn/standard_repn.py b/pyomo/repn/standard_repn.py index 95fa824b14a..b767ab727af 100644 --- a/pyomo/repn/standard_repn.py +++ b/pyomo/repn/standard_repn.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,10 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from __future__ import division - -__all__ = ['StandardRepn', 'generate_standard_repn'] - import sys import logging @@ -23,11 +19,15 @@ import pyomo.core.expr as EXPR from pyomo.core.expr.numvalue import NumericConstant -from pyomo.core.base.objective import _GeneralObjectiveData, ScalarObjective -from pyomo.core.base import _ExpressionData, Expression -from pyomo.core.base.expression import ScalarExpression, _GeneralExpressionData -from pyomo.core.base.var import ScalarVar, Var, _GeneralVarData, value -from pyomo.core.base.param import ScalarParam, _ParamData +from pyomo.core.base.objective import ObjectiveData, ScalarObjective +from pyomo.core.base import Expression +from pyomo.core.base.expression import ( + ScalarExpression, + NamedExpressionData, + ExpressionData, +) +from pyomo.core.base.var import ScalarVar, Var, VarData, value +from pyomo.core.base.param import ScalarParam, ParamData from pyomo.core.kernel.expression import expression, noclone from pyomo.core.kernel.variable import IVariable, variable from pyomo.core.kernel.objective import objective @@ -325,6 +325,16 @@ def generate_standard_repn( linear_vars[id_] = v elif arg.__class__ in native_numeric_types: C_ += arg + elif arg.is_variable_type(): + if arg.fixed: + C_ += arg.value + continue + id_ = id(arg) + if id_ in linear_coefs: + linear_coefs[id_] += 1 + else: + linear_coefs[id_] = 1 + linear_vars[id_] = arg else: C_ += EXPR.evaluate_expression(arg) else: # compute_values == False @@ -340,6 +350,18 @@ def generate_standard_repn( else: linear_coefs[id_] = c linear_vars[id_] = v + elif arg.__class__ in native_numeric_types: + C_ += arg + elif arg.is_variable_type(): + if arg.fixed: + C_ += arg + continue + id_ = id(arg) + if id_ in linear_coefs: + linear_coefs[id_] += 1 + else: + linear_coefs[id_] = 1 + linear_vars[id_] = arg else: C_ += arg @@ -1118,25 +1140,25 @@ def _collect_external_fn(exp, multiplier, idMap, compute_values, verbose, quadra EXPR.RangedExpression: _collect_comparison, EXPR.EqualityExpression: _collect_comparison, EXPR.ExternalFunctionExpression: _collect_external_fn, - # _ConnectorData : _collect_linear_connector, + # ConnectorData : _collect_linear_connector, # ScalarConnector : _collect_linear_connector, - _ParamData: _collect_const, + ParamData: _collect_const, ScalarParam: _collect_const, # param.Param : _collect_linear_const, # parameter : _collect_linear_const, NumericConstant: _collect_const, - _GeneralVarData: _collect_var, + VarData: _collect_var, ScalarVar: _collect_var, Var: _collect_var, variable: _collect_var, IVariable: _collect_var, - _GeneralExpressionData: _collect_identity, + ExpressionData: _collect_identity, ScalarExpression: _collect_identity, expression: _collect_identity, noclone: _collect_identity, - _ExpressionData: _collect_identity, + NamedExpressionData: _collect_identity, Expression: _collect_identity, - _GeneralObjectiveData: _collect_identity, + ObjectiveData: _collect_identity, ScalarObjective: _collect_identity, objective: _collect_identity, } @@ -1518,24 +1540,24 @@ def _linear_collect_pow(exp, multiplier, idMap, compute_values, verbose, coef): #EXPR.EqualityExpression : _linear_collect_comparison, #EXPR.ExternalFunctionExpression : _linear_collect_external_fn, ##EXPR.LinearSumExpression : _collect_linear_sum, - ##_ConnectorData : _collect_linear_connector, + ##ConnectorData : _collect_linear_connector, ##ScalarConnector : _collect_linear_connector, - ##param._ParamData : _collect_linear_const, + ##param.ParamData : _collect_linear_const, ##param.ScalarParam : _collect_linear_const, ##param.Param : _collect_linear_const, ##parameter : _collect_linear_const, - _GeneralVarData : _linear_collect_var, + VarData : _linear_collect_var, ScalarVar : _linear_collect_var, Var : _linear_collect_var, variable : _linear_collect_var, IVariable : _linear_collect_var, - _GeneralExpressionData : _linear_collect_identity, + ExpressionData : _linear_collect_identity, ScalarExpression : _linear_collect_identity, expression : _linear_collect_identity, noclone : _linear_collect_identity, - _ExpressionData : _linear_collect_identity, + NamedExpressionData : _linear_collect_identity, Expression : _linear_collect_identity, - _GeneralObjectiveData : _linear_collect_identity, + ObjectiveData : _linear_collect_identity, ScalarObjective : _linear_collect_identity, objective : _linear_collect_identity, } diff --git a/pyomo/repn/tests/__init__.py b/pyomo/repn/tests/__init__.py index 5e413c0132c..a9e1a5bea47 100644 --- a/pyomo/repn/tests/__init__.py +++ b/pyomo/repn/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/__init__.py b/pyomo/repn/tests/ampl/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/repn/tests/ampl/__init__.py +++ b/pyomo/repn/tests/ampl/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/repn/tests/ampl/helper.py b/pyomo/repn/tests/ampl/helper.py index eb09afc37cc..2bf2198d20f 100644 --- a/pyomo/repn/tests/ampl/helper.py +++ b/pyomo/repn/tests/ampl/helper.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/nl_diff.py b/pyomo/repn/tests/ampl/nl_diff.py index ecac3967dfe..9fe352ee503 100644 --- a/pyomo/repn/tests/ampl/nl_diff.py +++ b/pyomo/repn/tests/ampl/nl_diff.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/small10_testCase.py b/pyomo/repn/tests/ampl/small10_testCase.py index f51aea76d3e..deb56f92a88 100644 --- a/pyomo/repn/tests/ampl/small10_testCase.py +++ b/pyomo/repn/tests/ampl/small10_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/small11_testCase.py b/pyomo/repn/tests/ampl/small11_testCase.py index 5874007e13c..11b61805d5e 100644 --- a/pyomo/repn/tests/ampl/small11_testCase.py +++ b/pyomo/repn/tests/ampl/small11_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/small12_testCase.py b/pyomo/repn/tests/ampl/small12_testCase.py index 63d4ba29cf6..b73a8f528f2 100644 --- a/pyomo/repn/tests/ampl/small12_testCase.py +++ b/pyomo/repn/tests/ampl/small12_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/small13_testCase.py b/pyomo/repn/tests/ampl/small13_testCase.py index 9814c979cc7..c24185bf8d7 100644 --- a/pyomo/repn/tests/ampl/small13_testCase.py +++ b/pyomo/repn/tests/ampl/small13_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/small14_testCase.py b/pyomo/repn/tests/ampl/small14_testCase.py index 3d896242243..fb2c2bc6c5e 100644 --- a/pyomo/repn/tests/ampl/small14_testCase.py +++ b/pyomo/repn/tests/ampl/small14_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/small15_testCase.py b/pyomo/repn/tests/ampl/small15_testCase.py index 8345621cecd..d4d5796aaa5 100644 --- a/pyomo/repn/tests/ampl/small15_testCase.py +++ b/pyomo/repn/tests/ampl/small15_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/small1_testCase.py b/pyomo/repn/tests/ampl/small1_testCase.py index 00e6dd322ed..06f5ad122d9 100644 --- a/pyomo/repn/tests/ampl/small1_testCase.py +++ b/pyomo/repn/tests/ampl/small1_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/small2_testCase.py b/pyomo/repn/tests/ampl/small2_testCase.py index 2df3aebb139..8a65779f55e 100644 --- a/pyomo/repn/tests/ampl/small2_testCase.py +++ b/pyomo/repn/tests/ampl/small2_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/small3_testCase.py b/pyomo/repn/tests/ampl/small3_testCase.py index f11137979b4..999143d9a0c 100644 --- a/pyomo/repn/tests/ampl/small3_testCase.py +++ b/pyomo/repn/tests/ampl/small3_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/small4_testCase.py b/pyomo/repn/tests/ampl/small4_testCase.py index 08d68c21f50..9736dd9bf3b 100644 --- a/pyomo/repn/tests/ampl/small4_testCase.py +++ b/pyomo/repn/tests/ampl/small4_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/small5_testCase.py b/pyomo/repn/tests/ampl/small5_testCase.py index 1e976820f9b..1f254b7f04d 100644 --- a/pyomo/repn/tests/ampl/small5_testCase.py +++ b/pyomo/repn/tests/ampl/small5_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/small6_testCase.py b/pyomo/repn/tests/ampl/small6_testCase.py index da9f1d58f9b..9d309c09fef 100644 --- a/pyomo/repn/tests/ampl/small6_testCase.py +++ b/pyomo/repn/tests/ampl/small6_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/small7_testCase.py b/pyomo/repn/tests/ampl/small7_testCase.py index 22a75a33394..485962dd211 100644 --- a/pyomo/repn/tests/ampl/small7_testCase.py +++ b/pyomo/repn/tests/ampl/small7_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/small8_testCase.py b/pyomo/repn/tests/ampl/small8_testCase.py index 554e27c0924..61a3e3ccce7 100644 --- a/pyomo/repn/tests/ampl/small8_testCase.py +++ b/pyomo/repn/tests/ampl/small8_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/small9_testCase.py b/pyomo/repn/tests/ampl/small9_testCase.py index 3d7af602a88..7cb0913a762 100644 --- a/pyomo/repn/tests/ampl/small9_testCase.py +++ b/pyomo/repn/tests/ampl/small9_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/test_ampl_comparison.py b/pyomo/repn/tests/ampl/test_ampl_comparison.py index eb5aff329e1..8210bbdd173 100644 --- a/pyomo/repn/tests/ampl/test_ampl_comparison.py +++ b/pyomo/repn/tests/ampl/test_ampl_comparison.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/test_ampl_nl.py b/pyomo/repn/tests/ampl/test_ampl_nl.py index bd58c254bfd..53a2d3cda82 100644 --- a/pyomo/repn/tests/ampl/test_ampl_nl.py +++ b/pyomo/repn/tests/ampl/test_ampl_nl.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/test_ampl_repn.py b/pyomo/repn/tests/ampl/test_ampl_repn.py index cf1a889006e..9c911540eb0 100644 --- a/pyomo/repn/tests/ampl/test_ampl_repn.py +++ b/pyomo/repn/tests/ampl/test_ampl_repn.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index fe04847b6cb..27d129ca886 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -13,8 +13,10 @@ import pyomo.common.unittest as unittest import io +import logging import math import os +import re import pyomo.repn.util as repn_util import pyomo.repn.plugins.nl_writer as nl_writer @@ -22,8 +24,11 @@ from pyomo.repn.tests.nl_diff import nl_diff from pyomo.common.dependencies import numpy, numpy_available +from pyomo.common.errors import MouseTrap from pyomo.common.log import LoggingIntercept +from pyomo.common.tee import capture_output from pyomo.common.tempfiles import TempfileManager +from pyomo.common.timing import report_timing from pyomo.core.expr import Expr_if, inequality, LinearExpression from pyomo.core.base.expression import ScalarExpression from pyomo.environ import ( @@ -37,6 +42,8 @@ Suffix, Constraint, Expression, + Binary, + Integers, ) import pyomo.environ as pyo @@ -65,6 +72,7 @@ def __init__(self, symbolic=False): self.used_named_expressions, self.symbolic_solver_labels, True, + None, ) def __enter__(self): @@ -85,19 +93,19 @@ def test_divide(self): info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((m.x**2 / m.p, None, None)) + repn = info.visitor.walk_expression((m.x**2 / m.p, None, None, 1)) self.assertEqual(LOG.getvalue(), "") self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, 0) self.assertEqual(repn.linear, {}) - self.assertEqual(repn.nonlinear, ('o5\nv%s\nn2\n', [id(m.x)])) + self.assertEqual(repn.nonlinear, ('o5\n%s\nn2\n', [id(m.x)])) m.p = 2 info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((4 / m.p, None, None)) + repn = info.visitor.walk_expression((4 / m.p, None, None, 1)) self.assertEqual(LOG.getvalue(), "") self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) @@ -107,7 +115,7 @@ def test_divide(self): info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((m.x / m.p, None, None)) + repn = info.visitor.walk_expression((m.x / m.p, None, None, 1)) self.assertEqual(LOG.getvalue(), "") self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) @@ -117,7 +125,7 @@ def test_divide(self): info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression(((4 * m.x) / m.p, None, None)) + repn = info.visitor.walk_expression(((4 * m.x) / m.p, None, None, 1)) self.assertEqual(LOG.getvalue(), "") self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) @@ -127,7 +135,7 @@ def test_divide(self): info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((4 * (m.x + 2) / m.p, None, None)) + repn = info.visitor.walk_expression((4 * (m.x + 2) / m.p, None, None, 1)) self.assertEqual(LOG.getvalue(), "") self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) @@ -137,23 +145,23 @@ def test_divide(self): info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((m.x**2 / m.p, None, None)) + repn = info.visitor.walk_expression((m.x**2 / m.p, None, None, 1)) self.assertEqual(LOG.getvalue(), "") self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, 0) self.assertEqual(repn.linear, {}) - self.assertEqual(repn.nonlinear, ('o2\nn0.5\no5\nv%s\nn2\n', [id(m.x)])) + self.assertEqual(repn.nonlinear, ('o2\nn0.5\no5\n%s\nn2\n', [id(m.x)])) info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((log(m.x) / m.x, None, None)) + repn = info.visitor.walk_expression((log(m.x) / m.x, None, None, 1)) self.assertEqual(LOG.getvalue(), "") self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, 0) self.assertEqual(repn.linear, {}) - self.assertEqual(repn.nonlinear, ('o3\no43\nv%s\nv%s\n', [id(m.x), id(m.x)])) + self.assertEqual(repn.nonlinear, ('o3\no43\n%s\n%s\n', [id(m.x), id(m.x)])) def test_errors_divide_by_0(self): m = ConcreteModel() @@ -162,7 +170,7 @@ def test_errors_divide_by_0(self): info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((1 / m.p, None, None)) + repn = info.visitor.walk_expression((1 / m.p, None, None, 1)) self.assertEqual( LOG.getvalue(), "Exception encountered evaluating expression 'div(1, 0)'\n" @@ -177,7 +185,7 @@ def test_errors_divide_by_0(self): info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((m.x / m.p, None, None)) + repn = info.visitor.walk_expression((m.x / m.p, None, None, 1)) self.assertEqual( LOG.getvalue(), "Exception encountered evaluating expression 'div(1, 0)'\n" @@ -192,7 +200,7 @@ def test_errors_divide_by_0(self): info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression(((3 * m.x) / m.p, None, None)) + repn = info.visitor.walk_expression(((3 * m.x) / m.p, None, None, 1)) self.assertEqual( LOG.getvalue(), "Exception encountered evaluating expression 'div(3, 0)'\n" @@ -207,7 +215,7 @@ def test_errors_divide_by_0(self): info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((3 * (m.x + 2) / m.p, None, None)) + repn = info.visitor.walk_expression((3 * (m.x + 2) / m.p, None, None, 1)) self.assertEqual( LOG.getvalue(), "Exception encountered evaluating expression 'div(3, 0)'\n" @@ -222,7 +230,7 @@ def test_errors_divide_by_0(self): info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((m.x**2 / m.p, None, None)) + repn = info.visitor.walk_expression((m.x**2 / m.p, None, None, 1)) self.assertEqual( LOG.getvalue(), "Exception encountered evaluating expression 'div(1, 0)'\n" @@ -242,18 +250,18 @@ def test_pow(self): info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((m.x**m.p, None, None)) + repn = info.visitor.walk_expression((m.x**m.p, None, None, 1)) self.assertEqual(LOG.getvalue(), "") self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, 0) self.assertEqual(repn.linear, {}) - self.assertEqual(repn.nonlinear, ('o5\nv%s\nn2\n', [id(m.x)])) + self.assertEqual(repn.nonlinear, ('o5\n%s\nn2\n', [id(m.x)])) m.p = 1 info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((m.x**m.p, None, None)) + repn = info.visitor.walk_expression((m.x**m.p, None, None, 1)) self.assertEqual(LOG.getvalue(), "") self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) @@ -264,7 +272,7 @@ def test_pow(self): m.p = 0 info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((m.x**m.p, None, None)) + repn = info.visitor.walk_expression((m.x**m.p, None, None, 1)) self.assertEqual(LOG.getvalue(), "") self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) @@ -281,7 +289,7 @@ def test_errors_divide_by_0_mult_by_0(self): info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((m.p * (1 / m.p), None, None)) + repn = info.visitor.walk_expression((m.p * (1 / m.p), None, None, 1)) self.assertIn( "Exception encountered evaluating expression 'div(1, 0)'\n" "\tmessage: division by zero\n" @@ -296,7 +304,7 @@ def test_errors_divide_by_0_mult_by_0(self): info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression(((1 / m.p) * m.p, None, None)) + repn = info.visitor.walk_expression(((1 / m.p) * m.p, None, None, 1)) self.assertIn( "Exception encountered evaluating expression 'div(1, 0)'\n" "\tmessage: division by zero\n" @@ -311,7 +319,7 @@ def test_errors_divide_by_0_mult_by_0(self): info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((m.p * (m.x / m.p), None, None)) + repn = info.visitor.walk_expression((m.p * (m.x / m.p), None, None, 1)) self.assertIn( "Exception encountered evaluating expression 'div(1, 0)'\n" "\tmessage: division by zero\n" @@ -327,7 +335,7 @@ def test_errors_divide_by_0_mult_by_0(self): info = INFO() with LoggingIntercept() as LOG: repn = info.visitor.walk_expression( - (m.p * (3 * (m.x + 2) / m.p), None, None) + (m.p * (3 * (m.x + 2) / m.p), None, None, 1) ) self.assertIn( "Exception encountered evaluating expression 'div(3, 0)'\n" @@ -343,7 +351,7 @@ def test_errors_divide_by_0_mult_by_0(self): info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((m.p * (m.x**2 / m.p), None, None)) + repn = info.visitor.walk_expression((m.p * (m.x**2 / m.p), None, None, 1)) self.assertIn( "Exception encountered evaluating expression 'div(1, 0)'\n" "\tmessage: division by zero\n" @@ -368,7 +376,7 @@ def test_errors_divide_by_0_halt(self): try: info = INFO() with LoggingIntercept() as LOG, self.assertRaises(ZeroDivisionError): - info.visitor.walk_expression((1 / m.p, None, None)) + info.visitor.walk_expression((1 / m.p, None, None, 1)) self.assertEqual( LOG.getvalue(), "Exception encountered evaluating expression 'div(1, 0)'\n" @@ -378,7 +386,7 @@ def test_errors_divide_by_0_halt(self): info = INFO() with LoggingIntercept() as LOG, self.assertRaises(ZeroDivisionError): - info.visitor.walk_expression((m.x / m.p, None, None)) + info.visitor.walk_expression((m.x / m.p, None, None, 1)) self.assertEqual( LOG.getvalue(), "Exception encountered evaluating expression 'div(1, 0)'\n" @@ -388,7 +396,7 @@ def test_errors_divide_by_0_halt(self): info = INFO() with LoggingIntercept() as LOG, self.assertRaises(ZeroDivisionError): - info.visitor.walk_expression((3 * (m.x + 2) / m.p, None, None)) + info.visitor.walk_expression((3 * (m.x + 2) / m.p, None, None, 1)) self.assertEqual( LOG.getvalue(), "Exception encountered evaluating expression 'div(3, 0)'\n" @@ -398,7 +406,7 @@ def test_errors_divide_by_0_halt(self): info = INFO() with LoggingIntercept() as LOG, self.assertRaises(ZeroDivisionError): - info.visitor.walk_expression((m.x**2 / m.p, None, None)) + info.visitor.walk_expression((m.x**2 / m.p, None, None, 1)) self.assertEqual( LOG.getvalue(), "Exception encountered evaluating expression 'div(1, 0)'\n" @@ -415,7 +423,7 @@ def test_errors_negative_frac_pow(self): info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((m.p ** (0.5), None, None)) + repn = info.visitor.walk_expression((m.p ** (0.5), None, None, 1)) self.assertEqual( LOG.getvalue(), "Complex number returned from expression\n" @@ -431,7 +439,7 @@ def test_errors_negative_frac_pow(self): m.x.fix(0.5) info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((m.p**m.x, None, None)) + repn = info.visitor.walk_expression((m.p**m.x, None, None, 1)) self.assertEqual( LOG.getvalue(), "Complex number returned from expression\n" @@ -451,7 +459,7 @@ def test_errors_unary_func(self): info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((log(m.p), None, None)) + repn = info.visitor.walk_expression((log(m.p), None, None, 1)) self.assertEqual( LOG.getvalue(), "Exception encountered evaluating expression 'log(0)'\n" @@ -474,9 +482,8 @@ def test_errors_propagate_nan(self): expr = m.y**2 * m.x**2 * (((3 * m.x) / m.p) * m.x) / m.y - info = INFO() - with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((expr, None, None)) + with LoggingIntercept() as LOG, INFO() as info: + repn = info.visitor.walk_expression((expr, None, None, 1)) self.assertEqual( LOG.getvalue(), "Exception encountered evaluating expression 'div(3, 0)'\n" @@ -491,7 +498,8 @@ def test_errors_propagate_nan(self): m.y.fix(None) expr = log(m.y) + 3 - repn = info.visitor.walk_expression((expr, None, None)) + with INFO() as info: + repn = info.visitor.walk_expression((expr, None, None, 1)) self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) self.assertEqual(str(repn.const), 'InvalidNumber(nan)') @@ -499,7 +507,8 @@ def test_errors_propagate_nan(self): self.assertEqual(repn.nonlinear, None) expr = 3 * m.y - repn = info.visitor.walk_expression((expr, None, None)) + with INFO() as info: + repn = info.visitor.walk_expression((expr, None, None, 1)) self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, InvalidNumber(None)) @@ -508,7 +517,8 @@ def test_errors_propagate_nan(self): m.p.value = None expr = 5 * (m.p * m.x + 2 * m.z) - repn = info.visitor.walk_expression((expr, None, None)) + with INFO() as info: + repn = info.visitor.walk_expression((expr, None, None, 1)) self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, 0) @@ -516,7 +526,8 @@ def test_errors_propagate_nan(self): self.assertEqual(repn.nonlinear, None) expr = m.y * m.x - repn = info.visitor.walk_expression((expr, None, None)) + with INFO() as info: + repn = info.visitor.walk_expression((expr, None, None, 1)) self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, 0) @@ -527,17 +538,17 @@ def test_errors_propagate_nan(self): m.z[1].fix(None) expr = m.z[1] - ((m.z[2] * m.z[3]) * m.z[4]) with INFO() as info: - repn = info.visitor.walk_expression((expr, None, None)) + repn = info.visitor.walk_expression((expr, None, None, 1)) self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, InvalidNumber(None)) self.assertEqual(repn.linear, {}) - self.assertEqual(repn.nonlinear[0], 'o16\no2\no2\nv%s\nv%s\nv%s\n') + self.assertEqual(repn.nonlinear[0], 'o16\no2\no2\n%s\n%s\n%s\n') self.assertEqual(repn.nonlinear[1], [id(m.z[2]), id(m.z[3]), id(m.z[4])]) m.z[3].fix(float('nan')) with INFO() as info: - repn = info.visitor.walk_expression((expr, None, None)) + repn = info.visitor.walk_expression((expr, None, None, 1)) self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, InvalidNumber(None)) @@ -560,6 +571,7 @@ def test_linearexpression_npv(self): ), None, None, + 1, ) ) self.assertEqual(LOG.getvalue(), "") @@ -575,18 +587,18 @@ def test_eval_pow(self): info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((m.x ** (0.5), None, None)) + repn = info.visitor.walk_expression((m.x ** (0.5), None, None, 1)) self.assertEqual(LOG.getvalue(), "") self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, 0) self.assertEqual(repn.linear, {}) - self.assertEqual(repn.nonlinear, ('o5\nv%s\nn0.5\n', [id(m.x)])) + self.assertEqual(repn.nonlinear, ('o5\n%s\nn0.5\n', [id(m.x)])) m.x.fix() info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((m.x ** (0.5), None, None)) + repn = info.visitor.walk_expression((m.x ** (0.5), None, None, 1)) self.assertEqual(LOG.getvalue(), "") self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) @@ -600,18 +612,18 @@ def test_eval_abs(self): info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((abs(m.x), None, None)) + repn = info.visitor.walk_expression((abs(m.x), None, None, 1)) self.assertEqual(LOG.getvalue(), "") self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, 0) self.assertEqual(repn.linear, {}) - self.assertEqual(repn.nonlinear, ('o15\nv%s\n', [id(m.x)])) + self.assertEqual(repn.nonlinear, ('o15\n%s\n', [id(m.x)])) m.x.fix() info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((abs(m.x), None, None)) + repn = info.visitor.walk_expression((abs(m.x), None, None, 1)) self.assertEqual(LOG.getvalue(), "") self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) @@ -625,18 +637,18 @@ def test_eval_unary_func(self): info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((log(m.x), None, None)) + repn = info.visitor.walk_expression((log(m.x), None, None, 1)) self.assertEqual(LOG.getvalue(), "") self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, 0) self.assertEqual(repn.linear, {}) - self.assertEqual(repn.nonlinear, ('o43\nv%s\n', [id(m.x)])) + self.assertEqual(repn.nonlinear, ('o43\n%s\n', [id(m.x)])) m.x.fix() info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((log(m.x), None, None)) + repn = info.visitor.walk_expression((log(m.x), None, None, 1)) self.assertEqual(LOG.getvalue(), "") self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) @@ -652,7 +664,7 @@ def test_eval_expr_if_lessEq(self): info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((expr, None, None)) + repn = info.visitor.walk_expression((expr, None, None, 1)) self.assertEqual(LOG.getvalue(), "") self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) @@ -660,13 +672,13 @@ def test_eval_expr_if_lessEq(self): self.assertEqual(repn.linear, {}) self.assertEqual( repn.nonlinear, - ('o35\no23\nv%s\nn4\no5\nv%s\nn2\nv%s\n', [id(m.x), id(m.x), id(m.y)]), + ('o35\no23\n%s\nn4\no5\n%s\nn2\n%s\n', [id(m.x), id(m.x), id(m.y)]), ) m.x.fix() info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((expr, None, None)) + repn = info.visitor.walk_expression((expr, None, None, 1)) self.assertEqual(LOG.getvalue(), "") self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) @@ -677,7 +689,7 @@ def test_eval_expr_if_lessEq(self): m.x.fix(5) info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((expr, None, None)) + repn = info.visitor.walk_expression((expr, None, None, 1)) self.assertEqual(LOG.getvalue(), "") self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) @@ -693,7 +705,7 @@ def test_eval_expr_if_Eq(self): info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((expr, None, None)) + repn = info.visitor.walk_expression((expr, None, None, 1)) self.assertEqual(LOG.getvalue(), "") self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) @@ -701,13 +713,13 @@ def test_eval_expr_if_Eq(self): self.assertEqual(repn.linear, {}) self.assertEqual( repn.nonlinear, - ('o35\no24\nv%s\nn4\no5\nv%s\nn2\nv%s\n', [id(m.x), id(m.x), id(m.y)]), + ('o35\no24\n%s\nn4\no5\n%s\nn2\n%s\n', [id(m.x), id(m.x), id(m.y)]), ) m.x.fix() info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((expr, None, None)) + repn = info.visitor.walk_expression((expr, None, None, 1)) self.assertEqual(LOG.getvalue(), "") self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) @@ -718,7 +730,7 @@ def test_eval_expr_if_Eq(self): m.x.fix(5) info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((expr, None, None)) + repn = info.visitor.walk_expression((expr, None, None, 1)) self.assertEqual(LOG.getvalue(), "") self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) @@ -734,7 +746,7 @@ def test_eval_expr_if_ranged(self): info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((expr, None, None)) + repn = info.visitor.walk_expression((expr, None, None, 1)) self.assertEqual(LOG.getvalue(), "") self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) @@ -743,7 +755,7 @@ def test_eval_expr_if_ranged(self): self.assertEqual( repn.nonlinear, ( - 'o35\no21\no23\nn1\nv%s\no23\nv%s\nn4\no5\nv%s\nn2\nv%s\n', + 'o35\no21\no23\nn1\n%s\no23\n%s\nn4\no5\n%s\nn2\n%s\n', [id(m.x), id(m.x), id(m.x), id(m.y)], ), ) @@ -751,7 +763,7 @@ def test_eval_expr_if_ranged(self): m.x.fix() info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((expr, None, None)) + repn = info.visitor.walk_expression((expr, None, None, 1)) self.assertEqual(LOG.getvalue(), "") self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) @@ -762,7 +774,7 @@ def test_eval_expr_if_ranged(self): m.x.fix(5) info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((expr, None, None)) + repn = info.visitor.walk_expression((expr, None, None, 1)) self.assertEqual(LOG.getvalue(), "") self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) @@ -773,7 +785,7 @@ def test_eval_expr_if_ranged(self): m.x.fix(0) info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((expr, None, None)) + repn = info.visitor.walk_expression((expr, None, None, 1)) self.assertEqual(LOG.getvalue(), "") self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) @@ -793,7 +805,7 @@ class CustomExpression(ScalarExpression): expr = m.e + m.e info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((expr, None, None)) + repn = info.visitor.walk_expression((expr, None, None, 1)) self.assertEqual(LOG.getvalue(), "") self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) @@ -804,7 +816,7 @@ class CustomExpression(ScalarExpression): self.assertEqual(len(info.subexpression_cache), 1) obj, repn, info = info.subexpression_cache[id(m.e)] self.assertIs(obj, m.e) - self.assertEqual(repn.nl, ('v%s\n', (id(m.e),))) + self.assertEqual(repn.nl, ('%s\n', (id(m.e),))) self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, 3) self.assertEqual(repn.linear, {id(m.x): 1}) @@ -825,13 +837,13 @@ def test_nested_operator_zero_arg(self): info = INFO() with LoggingIntercept() as LOG: - repn = info.visitor.walk_expression((expr, None, None)) + repn = info.visitor.walk_expression((expr, None, None, 1)) self.assertEqual(LOG.getvalue(), "") self.assertEqual(repn.nl, None) self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, 0) self.assertEqual(repn.linear, {}) - self.assertEqual(repn.nonlinear, ('o24\no3\nn1\nv%s\nn0\n', [id(m.x)])) + self.assertEqual(repn.nonlinear, ('o24\no3\nn1\n%s\nn0\n', [id(m.x)])) def test_duplicate_shared_linear_expressions(self): # This tests an issue where AMPLRepn.duplicate() was not copying @@ -848,8 +860,8 @@ def test_duplicate_shared_linear_expressions(self): info = INFO() with LoggingIntercept() as LOG: - repn1 = info.visitor.walk_expression((expr1, None, None)) - repn2 = info.visitor.walk_expression((expr2, None, None)) + repn1 = info.visitor.walk_expression((expr1, None, None, 1)) + repn2 = info.visitor.walk_expression((expr2, None, None, 1)) self.assertEqual(LOG.getvalue(), "") self.assertEqual(repn1.nl, None) self.assertEqual(repn1.mult, 1) @@ -863,6 +875,67 @@ def test_duplicate_shared_linear_expressions(self): self.assertEqual(repn2.linear, {id(m.x): 102, id(m.y): 103}) self.assertEqual(repn2.nonlinear, None) + def test_AMPLRepn_to_expr(self): + m = ConcreteModel() + m.p = Param([2, 3, 4], mutable=True, initialize=lambda m, i: i**2) + m.x = Var([2, 3, 4], initialize=lambda m, i: i) + + e = 10 + info = INFO() + with LoggingIntercept() as LOG: + repn = info.visitor.walk_expression((e, None, None, 1)) + self.assertEqual(LOG.getvalue(), "") + self.assertEqual(repn.nl, None) + self.assertEqual(repn.mult, 1) + self.assertEqual(repn.const, 10) + self.assertEqual(repn.linear, {}) + self.assertEqual(repn.nonlinear, None) + ee = repn.to_expr(info.var_map) + self.assertExpressionsEqual(ee, 10) + + e += sum(m.x[i] * m.p[i] for i in m.x) + info = INFO() + with LoggingIntercept() as LOG: + repn = info.visitor.walk_expression((e, None, None, 1)) + self.assertEqual(LOG.getvalue(), "") + self.assertEqual(repn.nl, None) + self.assertEqual(repn.mult, 1) + self.assertEqual(repn.const, 10) + self.assertEqual(repn.linear, {id(m.x[2]): 4, id(m.x[3]): 9, id(m.x[4]): 16}) + self.assertEqual(repn.nonlinear, None) + ee = repn.to_expr(info.var_map) + self.assertExpressionsEqual(ee, 4 * m.x[2] + 9 * m.x[3] + 16 * m.x[4] + 10) + self.assertEqual(ee(), 10 + 8 + 27 + 64) + + e = sum(m.x[i] * m.p[i] for i in m.x) + info = INFO() + with LoggingIntercept() as LOG: + repn = info.visitor.walk_expression((e, None, None, 1)) + self.assertEqual(LOG.getvalue(), "") + self.assertEqual(repn.nl, None) + self.assertEqual(repn.mult, 1) + self.assertEqual(repn.const, 0) + self.assertEqual(repn.linear, {id(m.x[2]): 4, id(m.x[3]): 9, id(m.x[4]): 16}) + self.assertEqual(repn.nonlinear, None) + ee = repn.to_expr(info.var_map) + self.assertExpressionsEqual(ee, 4 * m.x[2] + 9 * m.x[3] + 16 * m.x[4]) + self.assertEqual(ee(), 8 + 27 + 64) + + e += m.x[2] ** 2 + info = INFO() + with LoggingIntercept() as LOG: + repn = info.visitor.walk_expression((e, None, None, 1)) + self.assertEqual(LOG.getvalue(), "") + self.assertEqual(repn.nl, None) + self.assertEqual(repn.mult, 1) + self.assertEqual(repn.const, 0) + self.assertEqual(repn.linear, {id(m.x[2]): 4, id(m.x[3]): 9, id(m.x[4]): 16}) + self.assertEqual(repn.nonlinear, ('o5\n%s\nn2\n', [id(m.x[2])])) + with self.assertRaisesRegex( + MouseTrap, "Cannot convert nonlinear AMPLRepn to Pyomo Expression" + ): + ee = repn.to_expr(info.var_map) + class Test_NLWriter(unittest.TestCase): def test_external_function_str_args(self): @@ -945,6 +1018,14 @@ def d(m, i): "keys that are not exported as part of the NL file. Skipping.\n", LOG.getvalue(), ) + with LoggingIntercept(level=logging.DEBUG) as LOG: + nl_writer.NLWriter().write(m, OUT) + self.assertEqual( + "model contains export suffix 'junk' that contains 1 component " + "keys that are not exported as part of the NL file. Skipping.\n" + "Skipped component keys:\n\ty\n", + LOG.getvalue(), + ) m.junk[m.z] = 1 with LoggingIntercept() as LOG: @@ -954,6 +1035,14 @@ def d(m, i): "keys that are not exported as part of the NL file. Skipping.\n", LOG.getvalue(), ) + with LoggingIntercept(level=logging.DEBUG) as LOG: + nl_writer.NLWriter().write(m, OUT) + self.assertEqual( + "model contains export suffix 'junk' that contains 3 component " + "keys that are not exported as part of the NL file. Skipping.\n" + "Skipped component keys:\n\ty\n\tz[1]\n\tz[3]\n", + LOG.getvalue(), + ) m.junk[m.c] = 2 with LoggingIntercept() as LOG: @@ -984,6 +1073,50 @@ def d(m, i): "Skipping.\n", LOG.getvalue(), ) + with LoggingIntercept(level=logging.DEBUG) as LOG: + nl_writer.NLWriter().write(m, OUT) + self.assertEqual( + "model contains export suffix 'junk' that contains 6 component " + "keys that are not exported as part of the NL file. Skipping.\n" + "Skipped component keys:\n\tc\n\td[1]\n\td[3]\n\ty\n\tz[1]\n\tz[3]\n" + "model contains export suffix 'junk' that contains 1 keys that " + "are not Var, Constraint, Objective, or the model. Skipping.\n" + "Skipped component keys:\n\t5\n", + LOG.getvalue(), + ) + + def test_log_timing(self): + # This tests an error possibly reported by #2810 + m = ConcreteModel() + m.x = Var(range(6)) + m.x[0].domain = pyo.Binary + m.x[1].domain = pyo.Integers + m.x[2].domain = pyo.Integers + m.p = Param(initialize=5, mutable=True) + m.o1 = Objective([1, 2], rule=lambda m, i: 1) + m.o2 = Objective(expr=m.x[1] * m.x[2]) + m.c1 = Constraint([1, 2], rule=lambda m, i: sum(m.x.values()) == 1) + m.c2 = Constraint(expr=m.p * m.x[1] ** 2 + m.x[2] ** 3 <= 100) + + OUT = io.StringIO() + with capture_output() as LOG: + with report_timing(level=logging.DEBUG): + nl_writer.NLWriter().write(m, OUT) + self.assertEqual( + """ [+ #.##] Initialized column order + [+ #.##] Collected suffixes + [+ #.##] Objective o1 + [+ #.##] Objective o2 + [+ #.##] Constraint c1 + [+ #.##] Constraint c2 + [+ #.##] Categorized model variables: 14 nnz + [+ #.##] Set row / column ordering: 6 var [3, 1, 2 R/B/Z], 3 con [2, 1 L/NL] + [+ #.##] Generated row/col labels & comments + [+ #.##] Wrote NL stream + [ #.##] Generated NL representation +""", + re.sub(r'\d\.\d\d\]', '#.##]', LOG.getvalue()), + ) def test_linear_constraint_npv_const(self): # This tests an error possibly reported by #2810 @@ -991,16 +1124,14 @@ def test_linear_constraint_npv_const(self): m.x = Var([1, 2]) m.p = Param(initialize=5, mutable=True) m.o = Objective(expr=1) - m.c = Constraint( - expr=LinearExpression([m.p**2, 5 * m.x[1], 10 * m.x[2]]) == 0 - ) + m.c = Constraint(expr=LinearExpression([m.p**2, 5 * m.x[1], 10 * m.x[2]]) <= 0) OUT = io.StringIO() nl_writer.NLWriter().write(m, OUT) self.assertEqual( *nl_diff( """g3 1 1 0 # problem unknown - 2 1 1 0 1 # vars, constraints, objectives, ranges, eqns + 2 1 1 0 0 # vars, constraints, objectives, ranges, eqns 0 0 0 0 0 0 # nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb 0 0 # network constraints: nonlinear, linear 0 0 0 # nonlinear vars in constraints, objectives, both @@ -1015,7 +1146,7 @@ def test_linear_constraint_npv_const(self): n1.0 x0 r -4 -25 +1 -25 b 3 3 @@ -1122,9 +1253,13 @@ def test_nonfloat_constants(self): m.weight = pyo.Constraint(expr=pyo.sum_product(m.w, m.x) <= m.limit) OUT = io.StringIO() + ROW = io.StringIO() + COL = io.StringIO() with LoggingIntercept() as LOG: - nl_writer.NLWriter().write(m, OUT, symbolic_solver_labels=True) + nl_writer.NLWriter().write(m, OUT, ROW, COL, symbolic_solver_labels=True) self.assertEqual(LOG.getvalue(), "") + self.assertEqual(ROW.getvalue(), "weight\nvalue\n") + self.assertEqual(COL.getvalue(), "x[0]\nx[1]\nx[2]\nx[3]\n") self.assertEqual( *nl_diff( """g3 1 1 0 #problem unknown @@ -1133,7 +1268,7 @@ def test_nonfloat_constants(self): 0 0 #network constraints: nonlinear, linear 0 0 0 #nonlinear vars in constraints, objectives, both 0 0 0 1 #linear network variables; functions; arith, flags - 0 4 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o) + 4 0 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o) 4 4 #nonzeros in Jacobian, obj. gradient 6 4 #max name lengths: constraints, variables 0 0 0 0 0 #common exprs: b,c,o,c1,o1 @@ -1167,6 +1302,1008 @@ def test_nonfloat_constants(self): 1 3 2 6 3 11 +""", + OUT.getvalue(), + ) + ) + + def test_presolve_lower_triangular(self): + m = pyo.ConcreteModel() + m.x = pyo.Var(range(5), bounds=(-10, 10)) + m.obj = Objective(expr=m.x[3] + m.x[4]) + m.c = pyo.ConstraintList() + m.c.add(m.x[0] == 5) + m.c.add(2 * m.x[0] + 3 * m.x[2] == 19) + m.c.add(m.x[0] + 2 * m.x[2] - 2 * m.x[1] == 3) + m.c.add(-2 * m.x[0] + m.x[2] + m.x[1] - m.x[3] == 1) + + OUT = io.StringIO() + with LoggingIntercept() as LOG: + nlinfo = nl_writer.NLWriter().write(m, OUT, linear_presolve=True) + self.assertEqual(LOG.getvalue(), "") + + self.assertEqual( + nlinfo.eliminated_vars, + [(m.x[3], -4.0), (m.x[1], 4.0), (m.x[2], 3.0), (m.x[0], 5.0)], + ) + self.assertEqual( + *nl_diff( + """g3 1 1 0 # problem unknown + 1 0 1 0 0 # vars, constraints, objectives, ranges, eqns + 0 0 0 0 0 0 # nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 # network constraints: nonlinear, linear + 0 0 0 # nonlinear vars in constraints, objectives, both + 0 0 0 1 # linear network variables; functions; arith, flags + 0 0 0 0 0 # discrete variables: binary, integer, nonlinear (b,c,o) + 0 1 # nonzeros in Jacobian, obj. gradient + 0 0 # max name lengths: constraints, variables + 0 0 0 0 0 # common exprs: b,c,o,c1,o1 +O0 0 +n-4.0 +x0 +r +b +0 -10 10 +k0 +G0 1 +0 1 +""", + OUT.getvalue(), + ) + ) + + def test_presolve_lower_triangular_fixed(self): + m = pyo.ConcreteModel() + m.x = pyo.Var(range(5), bounds=(-10, 10)) + m.obj = Objective(expr=m.x[3] + m.x[4]) + m.c = pyo.ConstraintList() + # m.c.add(m.x[0] == 5) + m.x[0].bounds = (5, 5) + m.c.add(2 * m.x[0] + 3 * m.x[2] == 19) + m.c.add(m.x[0] + 2 * m.x[2] - 2 * m.x[1] == 3) + m.c.add(-2 * m.x[0] + m.x[2] + m.x[1] - m.x[3] == 1) + + OUT = io.StringIO() + with LoggingIntercept() as LOG: + nlinfo = nl_writer.NLWriter().write(m, OUT, linear_presolve=True) + self.assertEqual(LOG.getvalue(), "") + + self.assertEqual( + nlinfo.eliminated_vars, + [(m.x[3], -4.0), (m.x[1], 4.0), (m.x[2], 3.0), (m.x[0], 5.0)], + ) + self.assertEqual( + *nl_diff( + """g3 1 1 0 # problem unknown + 1 0 1 0 0 # vars, constraints, objectives, ranges, eqns + 0 0 0 0 0 0 # nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 # network constraints: nonlinear, linear + 0 0 0 # nonlinear vars in constraints, objectives, both + 0 0 0 1 # linear network variables; functions; arith, flags + 0 0 0 0 0 # discrete variables: binary, integer, nonlinear (b,c,o) + 0 1 # nonzeros in Jacobian, obj. gradient + 0 0 # max name lengths: constraints, variables + 0 0 0 0 0 # common exprs: b,c,o,c1,o1 +O0 0 +n-4.0 +x0 +r +b +0 -10 10 +k0 +G0 1 +0 1 +""", + OUT.getvalue(), + ) + ) + + def test_presolve_lower_triangular_implied(self): + m = pyo.ConcreteModel() + m.x = pyo.Var(range(6), bounds=(-10, 10)) + m.obj = Objective(expr=m.x[3] + m.x[4]) + m.c = pyo.ConstraintList() + m.c.add(m.x[0] == m.x[5]) + m.x[0].bounds = (None, 5) + m.x[5].bounds = (5, None) + m.c.add(2 * m.x[0] + 3 * m.x[2] == 19) + m.c.add(m.x[0] + 2 * m.x[2] - 2 * m.x[1] == 3) + m.c.add(-2 * m.x[0] + m.x[2] + m.x[1] - m.x[3] == 1) + + OUT = io.StringIO() + with LoggingIntercept() as LOG: + nlinfo = nl_writer.NLWriter().write(m, OUT, linear_presolve=True) + self.assertEqual(LOG.getvalue(), "") + + self.assertEqual( + nlinfo.eliminated_vars, + [ + (m.x[1], 4.0), + (m.x[5], 5.0), + (m.x[3], -4.0), + (m.x[2], 3.0), + (m.x[0], 5.0), + ], + ) + self.assertEqual( + *nl_diff( + """g3 1 1 0 # problem unknown + 1 0 1 0 0 # vars, constraints, objectives, ranges, eqns + 0 0 0 0 0 0 # nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 # network constraints: nonlinear, linear + 0 0 0 # nonlinear vars in constraints, objectives, both + 0 0 0 1 # linear network variables; functions; arith, flags + 0 0 0 0 0 # discrete variables: binary, integer, nonlinear (b,c,o) + 0 1 # nonzeros in Jacobian, obj. gradient + 0 0 # max name lengths: constraints, variables + 0 0 0 0 0 # common exprs: b,c,o,c1,o1 +O0 0 +n-4.0 +x0 +r +b +0 -10 10 +k0 +G0 1 +0 1 +""", + OUT.getvalue(), + ) + ) + + def test_presolve_almost_lower_triangular(self): + m = pyo.ConcreteModel() + m.x = pyo.Var(range(5), bounds=(-10, 10)) + m.obj = Objective(expr=m.x[3] + m.x[4]) + m.c = pyo.ConstraintList() + m.c.add(m.x[0] + 2 * m.x[4] == 5) + m.c.add(2 * m.x[0] + 3 * m.x[2] == 19) + m.c.add(m.x[0] + 2 * m.x[2] - 2 * m.x[1] == 3) + m.c.add(-2 * m.x[0] + m.x[2] + m.x[1] - m.x[3] == 1) + + OUT = io.StringIO() + with LoggingIntercept() as LOG: + nlinfo = nl_writer.NLWriter().write(m, OUT, linear_presolve=True) + self.assertEqual(LOG.getvalue(), "") + + self.assertIs(nlinfo.eliminated_vars[0][0], m.x[4]) + self.assertExpressionsEqual(nlinfo.eliminated_vars[0][1], 3.0 * m.x[1] - 12.0) + + self.assertIs(nlinfo.eliminated_vars[1][0], m.x[3]) + self.assertExpressionsEqual(nlinfo.eliminated_vars[1][1], 17.0 * m.x[1] - 72.0) + + self.assertIs(nlinfo.eliminated_vars[2][0], m.x[2]) + self.assertExpressionsEqual(nlinfo.eliminated_vars[2][1], 4.0 * m.x[1] - 13.0) + + self.assertIs(nlinfo.eliminated_vars[3][0], m.x[0]) + self.assertExpressionsEqual(nlinfo.eliminated_vars[3][1], -6.0 * m.x[1] + 29.0) + + # Note: bounds on x[1] are: + # min(22/3, 82/17, 23/4, -39/-6) == 4.823529411764706 + # max(2/3, 62/17, 3/4, -19/-6) == 3.6470588235294117 + self.assertEqual( + *nl_diff( + """g3 1 1 0 # problem unknown + 1 0 1 0 0 # vars, constraints, objectives, ranges, eqns + 0 0 0 0 0 0 # nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 # network constraints: nonlinear, linear + 0 0 0 # nonlinear vars in constraints, objectives, both + 0 0 0 1 # linear network variables; functions; arith, flags + 0 0 0 0 0 # discrete variables: binary, integer, nonlinear (b,c,o) + 0 1 # nonzeros in Jacobian, obj. gradient + 0 0 # max name lengths: constraints, variables + 0 0 0 0 0 # common exprs: b,c,o,c1,o1 +O0 0 +n-84.0 +x0 +r +b +0 3.6470588235294117 4.823529411764706 +k0 +G0 1 +0 20 +""", + OUT.getvalue(), + ) + ) + + def test_presolve_almost_lower_triangular_nonlinear(self): + m = pyo.ConcreteModel() + m.x = pyo.Var(range(5), bounds=(-10, 10)) + m.obj = Objective(expr=m.x[3] + m.x[4] + pyo.log(m.x[0])) + m.c = pyo.ConstraintList() + m.c.add(m.x[0] + 2 * m.x[4] == 5) + m.c.add(2 * m.x[0] + 3 * m.x[2] == 19) + m.c.add(m.x[0] + 2 * m.x[2] - 2 * m.x[1] == 3) + m.c.add(-2 * m.x[0] + m.x[2] + m.x[1] - m.x[3] == 1) + m.c.add(2 * (m.x[0] ** 2) + m.x[0] + m.x[2] + 3 * (m.x[3] ** 3) == 10) + + OUT = io.StringIO() + with LoggingIntercept() as LOG: + nlinfo = nl_writer.NLWriter().write(m, OUT, linear_presolve=True) + self.assertEqual(LOG.getvalue(), "") + + self.assertIs(nlinfo.eliminated_vars[0][0], m.x[4]) + self.assertExpressionsEqual(nlinfo.eliminated_vars[0][1], 3.0 * m.x[1] - 12.0) + + self.assertIs(nlinfo.eliminated_vars[1][0], m.x[3]) + self.assertExpressionsEqual(nlinfo.eliminated_vars[1][1], 17.0 * m.x[1] - 72.0) + + self.assertIs(nlinfo.eliminated_vars[2][0], m.x[2]) + self.assertExpressionsEqual(nlinfo.eliminated_vars[2][1], 4.0 * m.x[1] - 13.0) + + self.assertIs(nlinfo.eliminated_vars[3][0], m.x[0]) + self.assertExpressionsEqual(nlinfo.eliminated_vars[3][1], -6.0 * m.x[1] + 29.0) + + # Note: bounds on x[1] are: + # min(22/3, 82/17, 23/4, -39/-6) == 4.823529411764706 + # max(2/3, 62/17, 3/4, -19/-6) == 3.6470588235294117 + self.assertEqual( + *nl_diff( + """g3 1 1 0 # problem unknown + 1 1 1 0 1 # vars, constraints, objectives, ranges, eqns + 1 1 0 0 0 0 # nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 # network constraints: nonlinear, linear + 1 1 1 # nonlinear vars in constraints, objectives, both + 0 0 0 1 # linear network variables; functions; arith, flags + 0 0 0 0 0 # discrete variables: binary, integer, nonlinear (b,c,o) + 1 1 # nonzeros in Jacobian, obj. gradient + 0 0 # max name lengths: constraints, variables + 0 0 0 0 0 # common exprs: b,c,o,c1,o1 +C0 +o0 +o2 +n2 +o5 +o0 +o2 +n-6.0 +v0 +n29.0 +n2 +o2 +n3 +o5 +o0 +o2 +n17.0 +v0 +n-72.0 +n3 +O0 0 +o0 +o43 +o0 +o2 +n-6.0 +v0 +n29.0 +n-84.0 +x0 +r +4 -6.0 +b +0 3.6470588235294117 4.823529411764706 +k0 +J0 1 +0 -2.0 +G0 1 +0 20.0 +""", + OUT.getvalue(), + ) + ) + + def test_presolve_lower_triangular_out_of_bounds(self): + m = pyo.ConcreteModel() + m.x = pyo.Var(range(5), domain=pyo.NonNegativeReals) + m.obj = Objective(expr=m.x[3] + m.x[4]) + m.c = pyo.ConstraintList() + m.c.add(m.x[0] == 5) + m.c.add(2 * m.x[0] + 3 * m.x[2] == 19) + m.c.add(m.x[0] + 2 * m.x[2] - 2 * m.x[1] == 3) + m.c.add(-2 * m.x[0] + m.x[2] + m.x[1] - m.x[3] == 1) + + OUT = io.StringIO() + with self.assertRaisesRegex( + nl_writer.InfeasibleConstraintException, + r"model contains a trivially infeasible variable 'x\[3\]' " + r"\(presolved to a value of -4.0 outside bounds \[0, None\]\).", + ): + with LoggingIntercept() as LOG: + nlinfo = nl_writer.NLWriter().write(m, OUT, linear_presolve=True) + self.assertEqual(LOG.getvalue(), "") + + def test_presolve_named_expressions(self): + # Test from #3055 + m = pyo.ConcreteModel() + m.x = pyo.Var([1, 2, 3], initialize=1, bounds=(0, 10)) + m.subexpr = pyo.Expression(pyo.Integers) + m.subexpr[1] = m.x[1] + m.x[2] + m.eq = pyo.Constraint(pyo.Integers) + m.eq[1] = m.x[1] == 7 + m.eq[2] = m.x[3] == 0.1 * m.subexpr[1] * m.x[2] + m.obj = pyo.Objective(expr=m.x[1] ** 2 + m.x[2] ** 2 + m.x[3] ** 3) + + OUT = io.StringIO() + with LoggingIntercept() as LOG: + nlinfo = nl_writer.NLWriter().write( + m, OUT, symbolic_solver_labels=True, linear_presolve=True + ) + self.assertEqual(LOG.getvalue(), "") + + self.assertEqual(nlinfo.eliminated_vars, [(m.x[1], 7)]) + + self.assertEqual( + *nl_diff( + """g3 1 1 0 # problem unknown + 2 1 1 0 1 # vars, constraints, objectives, ranges, eqns + 1 1 0 0 0 0 # nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 # network constraints: nonlinear, linear + 1 2 1 # nonlinear vars in constraints, objectives, both + 0 0 0 1 # linear network variables; functions; arith, flags + 0 0 0 0 0 # discrete variables: binary, integer, nonlinear (b,c,o) + 2 2 # nonzeros in Jacobian, obj. gradient + 5 4 # max name lengths: constraints, variables + 0 0 0 1 0 # common exprs: b,c,o,c1,o1 +V2 1 1 #subexpr[1] +0 1 +n7.0 +C0 #eq[2] +o16 #- +o2 #* +o2 #* +n0.1 +v2 #subexpr[1] +v0 #x[2] +O0 0 #obj +o54 # sumlist +3 # (n) +o5 #^ +n7.0 +n2 +o5 #^ +v0 #x[2] +n2 +o5 #^ +v1 #x[3] +n3 +x2 # initial guess +0 1 #x[2] +1 1 #x[3] +r #1 ranges (rhs's) +4 0 #eq[2] +b #2 bounds (on variables) +0 0 10 #x[2] +0 0 10 #x[3] +k1 #intermediate Jacobian column lengths +1 +J0 2 #eq[2] +0 0 +1 1 +G0 2 #obj +0 0 +1 0 +""", + OUT.getvalue(), + ) + ) + + def test_presolve_zero_coef(self): + m = ConcreteModel() + m.x = Var() + m.y = Var() + m.z = Var() + m.obj = Objective(expr=m.x**2 + m.y**2 + m.z**2) + m.c1 = Constraint(expr=m.x == m.y + m.z + 1.5) + m.c2 = Constraint(expr=m.z == -m.y) + + OUT = io.StringIO() + with LoggingIntercept() as LOG: + nlinfo = nl_writer.NLWriter().write( + m, OUT, symbolic_solver_labels=True, linear_presolve=True + ) + self.assertEqual(LOG.getvalue(), "") + + self.assertEqual(nlinfo.eliminated_vars[0], (m.x, 1.5)) + self.assertIs(nlinfo.eliminated_vars[1][0], m.y) + self.assertExpressionsEqual( + nlinfo.eliminated_vars[1][1], LinearExpression([-1.0 * m.z]) + ) + + self.assertEqual( + *nl_diff( + """g3 1 1 0 # problem unknown + 1 0 1 0 0 #vars, constraints, objectives, ranges, eqns + 0 1 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 #network constraints: nonlinear, linear + 0 1 0 #nonlinear vars in constraints, objectives, both + 0 0 0 1 #linear network variables; functions; arith, flags + 0 0 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o) + 0 1 #nonzeros in Jacobian, obj. gradient + 3 1 #max name lengths: constraints, variables + 0 0 0 0 0 #common exprs: b,c,o,c1,o1 +O0 0 #obj +o54 #sumlist +3 #(n) +o5 #^ +n1.5 +n2 +o5 #^ +o16 #- +v0 #z +n2 +o5 #^ +v0 #z +n2 +x0 #initial guess +r #0 ranges (rhs's) +b #1 bounds (on variables) +3 #z +k0 #intermediate Jacobian column lengths +G0 1 #obj +0 0 +""", + OUT.getvalue(), + ) + ) + + m.c3 = Constraint(expr=m.x == 2) + OUT = io.StringIO() + with LoggingIntercept() as LOG: + with self.assertRaisesRegex( + nl_writer.InfeasibleConstraintException, + r"model contains a trivially infeasible constraint 0.5 == 0.0\*y", + ): + nlinfo = nl_writer.NLWriter().write( + m, OUT, symbolic_solver_labels=True, linear_presolve=True + ) + self.assertEqual(LOG.getvalue(), "") + + m.c1.set_value(m.x >= m.y + m.z + 1.5) + OUT = io.StringIO() + with LoggingIntercept() as LOG: + nlinfo = nl_writer.NLWriter().write( + m, OUT, symbolic_solver_labels=True, linear_presolve=True + ) + self.assertEqual(LOG.getvalue(), "") + + self.assertIs(nlinfo.eliminated_vars[0][0], m.y) + self.assertExpressionsEqual( + nlinfo.eliminated_vars[0][1], LinearExpression([-1.0 * m.z]) + ) + self.assertEqual(nlinfo.eliminated_vars[1], (m.x, 2)) + + self.assertEqual( + *nl_diff( + """g3 1 1 0 # problem unknown + 1 1 1 0 0 #vars, constraints, objectives, ranges, eqns + 0 1 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 #network constraints: nonlinear, linear + 0 1 0 #nonlinear vars in constraints, objectives, both + 0 0 0 1 #linear network variables; functions; arith, flags + 0 0 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o) + 0 1 #nonzeros in Jacobian, obj. gradient + 3 1 #max name lengths: constraints, variables + 0 0 0 0 0 #common exprs: b,c,o,c1,o1 +C0 #c1 +n0 +O0 0 #obj +o54 #sumlist +3 #(n) +o5 #^ +n2 +n2 +o5 #^ +o16 #- +v0 #z +n2 +o5 #^ +v0 #z +n2 +x0 #initial guess +r #1 ranges (rhs's) +1 0.5 #c1 +b #1 bounds (on variables) +3 #z +k0 #intermediate Jacobian column lengths +G0 1 #obj +0 0 +""", + OUT.getvalue(), + ) + ) + + def test_presolve_independent_subsystem(self): + # This is derived from the example in #3192 + m = ConcreteModel() + m.x = Var() + m.y = Var() + m.z = Var() + m.d = Constraint(expr=m.z == m.y) + m.c = Constraint(expr=m.y == m.x) + m.o = Objective(expr=0) + + ref = """g3 1 1 0 #problem unknown + 0 0 1 0 0 #vars, constraints, objectives, ranges, eqns + 0 0 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 #network constraints: nonlinear, linear + 0 0 0 #nonlinear vars in constraints, objectives, both + 0 0 0 1 #linear network variables; functions; arith, flags + 0 0 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o) + 0 0 #nonzeros in Jacobian, obj. gradient + 1 0 #max name lengths: constraints, variables + 0 0 0 0 0 #common exprs: b,c,o,c1,o1 +O0 0 #o +n0 +x0 #initial guess +r #0 ranges (rhs's) +b #0 bounds (on variables) +k-1 #intermediate Jacobian column lengths +""" + + OUT = io.StringIO() + with LoggingIntercept() as LOG: + nlinfo = nl_writer.NLWriter().write( + m, OUT, symbolic_solver_labels=True, linear_presolve=True + ) + self.assertEqual( + LOG.getvalue(), + "presolve identified an underdetermined independent linear subsystem " + "that was removed from the model. Setting 'z' == 0\n", + ) + + self.assertEqual(*nl_diff(ref, OUT.getvalue())) + + m.x.lb = 5.0 + + OUT = io.StringIO() + with LoggingIntercept() as LOG: + nlinfo = nl_writer.NLWriter().write( + m, OUT, symbolic_solver_labels=True, linear_presolve=True + ) + self.assertEqual( + LOG.getvalue(), + "presolve identified an underdetermined independent linear subsystem " + "that was removed from the model. Setting 'z' == 5.0\n", + ) + + self.assertEqual(*nl_diff(ref, OUT.getvalue())) + + m.x.lb = -5.0 + m.z.ub = -2.0 + + OUT = io.StringIO() + with LoggingIntercept() as LOG: + nlinfo = nl_writer.NLWriter().write( + m, OUT, symbolic_solver_labels=True, linear_presolve=True + ) + self.assertEqual( + LOG.getvalue(), + "presolve identified an underdetermined independent linear subsystem " + "that was removed from the model. Setting 'z' == -2.0\n", + ) + + self.assertEqual(*nl_diff(ref, OUT.getvalue())) + + def test_scaling(self): + m = pyo.ConcreteModel() + m.x = pyo.Var(initialize=0) + m.y = pyo.Var(initialize=0, bounds=(-2e5, 1e5)) + m.z = pyo.Var(initialize=0, bounds=(1e3, None)) + m.v = pyo.Var(initialize=0, bounds=(1e3, 1e3)) + m.w = pyo.Var(initialize=0, bounds=(None, 1e3)) + m.obj = pyo.Objective(expr=m.x**2 + (m.y - 50000) ** 2 + m.z) + m.c = pyo.ConstraintList() + m.c.add(100 * m.x + m.y / 100 >= 600) + m.c.add(1000 * m.w + m.v * m.x <= 100) + m.dual = pyo.Suffix(direction=pyo.Suffix.IMPORT_EXPORT) + + m.dual[m.c[1]] = 0.02 + + OUT = io.StringIO() + with LoggingIntercept() as LOG: + nlinfo = nl_writer.NLWriter().write( + m, OUT, scale_model=False, linear_presolve=False + ) + self.assertEqual(LOG.getvalue(), "") + + nl1 = OUT.getvalue() + self.assertEqual( + *nl_diff( + """g3 1 1 0 # problem unknown + 5 2 1 0 0 # vars, constraints, objectives, ranges, eqns + 1 1 0 0 0 0 # nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 # network constraints: nonlinear, linear + 2 3 1 # nonlinear vars in constraints, objectives, both + 0 0 0 1 # linear network variables; functions; arith, flags + 0 0 0 0 0 # discrete variables: binary, integer, nonlinear (b,c,o) + 5 3 # nonzeros in Jacobian, obj. gradient + 0 0 # max name lengths: constraints, variables + 0 0 0 0 0 # common exprs: b,c,o,c1,o1 +C0 +o2 +v1 +v0 +C1 +n0 +O0 0 +o0 +o5 +v0 +n2 +o5 +o0 +v2 +n-50000 +n2 +d1 +1 0.02 +x5 +0 0 +1 0 +2 0 +3 0 +4 0 +r +1 100 +2 600 +b +3 +4 1000.0 +0 -200000.0 100000.0 +2 1000.0 +1 1000.0 +k4 +2 +3 +4 +4 +J0 3 +0 0 +1 0 +4 1000 +J1 2 +0 100 +2 0.01 +G0 3 +0 0 +2 0 +3 1 +""", + nl1, + ) + ) + + m.scaling_factor = pyo.Suffix(direction=pyo.Suffix.EXPORT) + m.scaling_factor[m.v] = 1 / 250 + m.scaling_factor[m.w] = 1 / 500 + # m.scaling_factor[m.x] = 1 + m.scaling_factor[m.y] = -1 / 50000 + m.scaling_factor[m.z] = 1 / 1000 + m.scaling_factor[m.c[1]] = 1 / 10 + m.scaling_factor[m.c[2]] = -1 / 100 + m.scaling_factor[m.obj] = 1 / 100 + + OUT = io.StringIO() + with LoggingIntercept() as LOG: + nlinfo = nl_writer.NLWriter().write( + m, OUT, scale_model=True, linear_presolve=False + ) + self.assertEqual(LOG.getvalue(), "") + + nl2 = OUT.getvalue() + + self.assertEqual( + *nl_diff( + """g3 1 1 0 # problem unknown + 5 2 1 0 0 # vars, constraints, objectives, ranges, eqns + 1 1 0 0 0 0 # nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 # network constraints: nonlinear, linear + 2 3 1 # nonlinear vars in constraints, objectives, both + 0 0 0 1 # linear network variables; functions; arith, flags + 0 0 0 0 0 # discrete variables: binary, integer, nonlinear (b,c,o) + 5 3 # nonzeros in Jacobian, obj. gradient + 0 0 # max name lengths: constraints, variables + 0 0 0 0 0 # common exprs: b,c,o,c1,o1 +C0 +o2 +n-0.01 +o2 +o3 +v1 +n0.004 +v0 +C1 +n0 +O0 0 +o2 +n0.01 +o0 +o5 +v0 +n2 +o5 +o0 +o3 +v2 +n-2e-05 +n-50000 +n2 +d1 +1 0.002 +x5 +0 0 +1 0.0 +2 0.0 +3 0.0 +4 0.0 +r +2 -1.0 +2 60.0 +b +3 +4 4.0 +0 -2.0 4.0 +2 1.0 +1 2.0 +k4 +2 +3 +4 +4 +J0 3 +0 0.0 +1 0.0 +4 -5000.0 +J1 2 +0 10.0 +2 -50.0 +G0 3 +0 0.0 +2 0.0 +3 10.0 +""", + nl2, + ) + ) + + # Debugging: this diffs the unscaled & scaled models + # self.assertEqual(*nl_diff(nl1, nl2)) + + def test_named_expressions(self): + # This tests an error possibly reported by #2810 + m = ConcreteModel() + m.x = Var() + m.y = Var() + m.z = Var() + m.E1 = Expression(expr=3 * (m.x * m.y + m.z)) + m.E2 = Expression(expr=m.z * m.y) + m.E3 = Expression(expr=m.x * m.z + m.y) + m.o1 = Objective(expr=m.E1 + m.E2) + m.o2 = Objective(expr=m.E1**2) + m.c1 = Constraint(expr=m.E2 + 2 * m.E3 >= 0) + m.c2 = Constraint(expr=pyo.inequality(0, m.E3**2, 10)) + + OUT = io.StringIO() + nl_writer.NLWriter().write(m, OUT, symbolic_solver_labels=True) + + self.assertEqual( + *nl_diff( + """g3 1 1 0 # problem unknown + 3 2 2 1 0 # vars, constraints, objectives, ranges, eqns + 2 2 0 0 0 0 # nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 # network constraints: nonlinear, linear + 3 3 3 # nonlinear vars in constraints, objectives, both + 0 0 0 1 # linear network variables; functions; arith, flags + 0 0 0 0 0 # discrete variables: binary, integer, nonlinear (b,c,o) + 6 6 # nonzeros in Jacobian, obj. gradient + 2 1 # max name lengths: constraints, variables + 1 1 1 1 1 # common exprs: b,c,o,c1,o1 +V3 0 0 #nl(E1) +o2 #* +v0 #x +v1 #y +V4 0 0 #E2 +o2 #* +v2 #z +v1 #y +V5 0 0 #nl(E3) +o2 #* +v0 #x +v2 #z +C0 #c1 +o0 #+ +v4 #E2 +o2 #* +n2 +v5 #nl(E3) +V6 1 2 #E3 +1 1 +v5 #nl(E3) +C1 #c2 +o5 #^ +v6 #E3 +n2 +O0 0 #o1 +o0 #+ +o2 #* +n3 +v3 #nl(E1) +v4 #E2 +V7 1 4 #E1 +2 3 +o2 #* +n3 +v3 #nl(E1) +O1 0 #o2 +o5 #^ +v7 #E1 +n2 +x0 # initial guess +r #2 ranges (rhs's) +2 0 #c1 +0 0 10 #c2 +b #3 bounds (on variables) +3 #x +3 #y +3 #z +k2 #intermediate Jacobian column lengths +2 +4 +J0 3 #c1 +0 0 +1 2 +2 0 +J1 3 #c2 +0 0 +1 0 +2 0 +G0 3 #o1 +0 0 +1 0 +2 3 +G1 3 #o2 +0 0 +1 0 +2 0 +""", + OUT.getvalue(), + ) + ) + + def test_discrete_var_tabulation(self): + # This tests an error reported in #3235 + # + # Among other issues, this verifies that nonlinear discrete + # variables are tabulated correctly (header line 7), and that + # integer variables with bounds in {0, 1} are mapped to binary + # variables. + m = ConcreteModel() + m.p1 = Var(bounds=(0.85, 1.15)) + m.p2 = Var(bounds=(0.68, 0.92)) + m.c1 = Var(bounds=(-0.0, 0.7)) + m.c2 = Var(bounds=(-0.0, 0.7)) + m.t1 = Var(within=Binary, bounds=(0, 1)) + m.t2 = Var(within=Binary, bounds=(0, 1)) + m.t3 = Var(within=Binary, bounds=(0, 1)) + m.t4 = Var(within=Binary, bounds=(0, 1)) + m.t5 = Var(within=Integers, bounds=(0, None)) + m.t6 = Var(within=Integers, bounds=(0, None)) + m.x1 = Var(within=Binary) + m.x2 = Var(within=Integers, bounds=(0, 1)) + m.x3 = Var(within=Integers, bounds=(0, None)) + m.const = Constraint( + expr=( + (0.7 - (m.c1 * m.t1 + m.c2 * m.t2)) + <= (m.p1 * m.t1 + m.p2 * m.t2 + m.p1 * m.t4 + m.t6 * m.t5) + ) + ) + m.OBJ = Objective( + expr=(m.p1 * m.t1 + m.p2 * m.t2 + m.p2 * m.t3 + m.x1 + m.x2 + m.x3) + ) + + OUT = io.StringIO() + nl_writer.NLWriter().write(m, OUT, symbolic_solver_labels=True) + + self.assertEqual( + *nl_diff( + """g3 1 1 0 # problem unknown + 13 1 1 0 0 #vars, constraints, objectives, ranges, eqns + 1 1 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 #network constraints: nonlinear, linear + 9 10 4 #nonlinear vars in constraints, objectives, both + 0 0 0 1 #linear network variables; functions; arith, flags + 2 1 2 3 1 #discrete variables: binary, integer, nonlinear (b,c,o) + 9 8 #nonzeros in Jacobian, obj. gradient + 5 2 #max name lengths: constraints, variables + 0 0 0 0 0 #common exprs: b,c,o,c1,o1 +C0 #const +o0 #+ +o16 #- +o0 #+ +o2 #* +v4 #c1 +v2 #t1 +o2 #* +v5 #c2 +v3 #t2 +o16 #- +o54 #sumlist +4 #(n) +o2 #* +v0 #p1 +v2 #t1 +o2 #* +v1 #p2 +v3 #t2 +o2 #* +v0 #p1 +v6 #t4 +o2 #* +v7 #t6 +v8 #t5 +O0 0 #OBJ +o54 #sumlist +3 #(n) +o2 #* +v0 #p1 +v2 #t1 +o2 #* +v1 #p2 +v3 #t2 +o2 #* +v1 #p2 +v9 #t3 +x0 #initial guess +r #1 ranges (rhs's) +1 -0.7 #const +b #13 bounds (on variables) +0 0.85 1.15 #p1 +0 0.68 0.92 #p2 +0 0 1 #t1 +0 0 1 #t2 +0 -0.0 0.7 #c1 +0 -0.0 0.7 #c2 +0 0 1 #t4 +2 0 #t6 +2 0 #t5 +0 0 1 #t3 +0 0 1 #x1 +0 0 1 #x2 +2 0 #x3 +k12 #intermediate Jacobian column lengths +1 +2 +3 +4 +5 +6 +7 +8 +9 +9 +9 +9 +J0 9 #const +0 0 +1 0 +2 0 +3 0 +4 0 +5 0 +6 0 +7 0 +8 0 +G0 8 #OBJ +0 0 +1 0 +2 0 +3 0 +9 0 +10 1 +11 1 +12 1 """, OUT.getvalue(), ) diff --git a/pyomo/repn/tests/ampl/test_suffixes.py b/pyomo/repn/tests/ampl/test_suffixes.py index e73060e7e8c..1372da68bdc 100644 --- a/pyomo/repn/tests/ampl/test_suffixes.py +++ b/pyomo/repn/tests/ampl/test_suffixes.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/baron/__init__.py b/pyomo/repn/tests/baron/__init__.py index 030f46eaca8..c693bb8accd 100644 --- a/pyomo/repn/tests/baron/__init__.py +++ b/pyomo/repn/tests/baron/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/baron/small14a_testCase.py b/pyomo/repn/tests/baron/small14a_testCase.py index 72190756dc7..b2cf5afcb72 100644 --- a/pyomo/repn/tests/baron/small14a_testCase.py +++ b/pyomo/repn/tests/baron/small14a_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/baron/test_baron.py b/pyomo/repn/tests/baron/test_baron.py index 348ad6036fb..6f22f26cd38 100644 --- a/pyomo/repn/tests/baron/test_baron.py +++ b/pyomo/repn/tests/baron/test_baron.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/baron/test_baron_comparison.py b/pyomo/repn/tests/baron/test_baron_comparison.py index 7c480321624..1b394f6a5b1 100644 --- a/pyomo/repn/tests/baron/test_baron_comparison.py +++ b/pyomo/repn/tests/baron/test_baron_comparison.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/cpxlp/__init__.py b/pyomo/repn/tests/cpxlp/__init__.py index 8ffbfd52054..f216a76f48b 100644 --- a/pyomo/repn/tests/cpxlp/__init__.py +++ b/pyomo/repn/tests/cpxlp/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/cpxlp/test_cpxlp.py b/pyomo/repn/tests/cpxlp/test_cpxlp.py index 28c9043a8de..567c5184517 100644 --- a/pyomo/repn/tests/cpxlp/test_cpxlp.py +++ b/pyomo/repn/tests/cpxlp/test_cpxlp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/cpxlp/test_lpv2.py b/pyomo/repn/tests/cpxlp/test_lpv2.py index fbe4f15fbe9..fbef24c77c3 100644 --- a/pyomo/repn/tests/cpxlp/test_lpv2.py +++ b/pyomo/repn/tests/cpxlp/test_lpv2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -14,22 +14,23 @@ import pyomo.common.unittest as unittest from pyomo.common.log import LoggingIntercept -from pyomo.environ import ConcreteModel, Block, Constraint, Var, Objective, Suffix + +import pyomo.environ as pyo from pyomo.repn.plugins.lp_writer import LPWriter class TestLPv2(unittest.TestCase): def test_warn_export_suffixes(self): - m = ConcreteModel() - m.x = Var() - m.obj = Objective(expr=m.x) - m.con = Constraint(expr=m.x >= 2) - m.b = Block() - m.ignored = Suffix(direction=Suffix.IMPORT) - m.duals = Suffix(direction=Suffix.IMPORT_EXPORT) - m.b.duals = Suffix(direction=Suffix.IMPORT_EXPORT) - m.b.scaling = Suffix(direction=Suffix.EXPORT) + m = pyo.ConcreteModel() + m.x = pyo.Var() + m.obj = pyo.Objective(expr=m.x) + m.con = pyo.Constraint(expr=m.x >= 2) + m.b = pyo.Block() + m.ignored = pyo.Suffix(direction=pyo.Suffix.IMPORT) + m.duals = pyo.Suffix(direction=pyo.Suffix.IMPORT_EXPORT) + m.b.duals = pyo.Suffix(direction=pyo.Suffix.IMPORT_EXPORT) + m.b.scaling = pyo.Suffix(direction=pyo.Suffix.EXPORT) # Empty suffixes are ignored writer = LPWriter() @@ -73,3 +74,68 @@ def test_warn_export_suffixes(self): LP writer cannot export suffixes to LP files. Skipping. """, ) + + def test_deterministic_unordered_sets(self): + ref = """\\* Source Pyomo model name=unknown *\\ + +min +o: ++1 x(a) ++1 x(aaaaa) ++1 x(ooo) ++1 x(z) + +s.t. + +c_l_c(a)_: ++1 x(a) +>= 1 + +c_l_c(aaaaa)_: ++1 x(aaaaa) +>= 5 + +c_l_c(ooo)_: ++1 x(ooo) +>= 3 + +c_l_c(z)_: ++1 x(z) +>= 1 + +bounds + -inf <= x(a) <= +inf + -inf <= x(aaaaa) <= +inf + -inf <= x(ooo) <= +inf + -inf <= x(z) <= +inf +end +""" + set_init = ['a', 'z', 'ooo', 'aaaaa'] + + m = pyo.ConcreteModel() + m.I = pyo.Set(initialize=set_init, ordered=False) + m.x = pyo.Var(m.I) + m.c = pyo.Constraint(m.I, rule=lambda m, i: m.x[i] >= len(i)) + m.o = pyo.Objective(expr=sum(m.x[i] for i in m.I)) + + OUT = StringIO() + with LoggingIntercept() as LOG: + LPWriter().write(m, OUT, symbolic_solver_labels=True) + self.assertEqual(LOG.getvalue(), "") + print(OUT.getvalue()) + self.assertEqual(ref, OUT.getvalue()) + + m = pyo.ConcreteModel() + m.I = pyo.Set() + m.x = pyo.Var(pyo.Any) + m.c = pyo.Constraint(pyo.Any) + for i in set_init: + m.c[i] = m.x[i] >= len(i) + m.o = pyo.Objective(expr=sum(m.x.values())) + + OUT = StringIO() + with LoggingIntercept() as LOG: + LPWriter().write(m, OUT, symbolic_solver_labels=True) + self.assertEqual(LOG.getvalue(), "") + + self.assertEqual(ref, OUT.getvalue()) diff --git a/pyomo/repn/tests/diffutils.py b/pyomo/repn/tests/diffutils.py index 24188d46c86..c346f8c48b2 100644 --- a/pyomo/repn/tests/diffutils.py +++ b/pyomo/repn/tests/diffutils.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/gams/__init__.py b/pyomo/repn/tests/gams/__init__.py index 8d13c4ffb99..e548666fd72 100644 --- a/pyomo/repn/tests/gams/__init__.py +++ b/pyomo/repn/tests/gams/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/gams/small14a_testCase.py b/pyomo/repn/tests/gams/small14a_testCase.py index c7e3e0805ea..1efdd1baa25 100644 --- a/pyomo/repn/tests/gams/small14a_testCase.py +++ b/pyomo/repn/tests/gams/small14a_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/gams/test_gams.py b/pyomo/repn/tests/gams/test_gams.py index e6b729e5dfc..e3304e18491 100644 --- a/pyomo/repn/tests/gams/test_gams.py +++ b/pyomo/repn/tests/gams/test_gams.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/gams/test_gams_comparison.py b/pyomo/repn/tests/gams/test_gams_comparison.py index 4e530b10d43..42fa9f71dda 100644 --- a/pyomo/repn/tests/gams/test_gams_comparison.py +++ b/pyomo/repn/tests/gams/test_gams_comparison.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/lp_diff.py b/pyomo/repn/tests/lp_diff.py index 23b24f8b51b..2c119d72c6f 100644 --- a/pyomo/repn/tests/lp_diff.py +++ b/pyomo/repn/tests/lp_diff.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/mps/__init__.py b/pyomo/repn/tests/mps/__init__.py index 1a8a69a1409..effc182aa1c 100644 --- a/pyomo/repn/tests/mps/__init__.py +++ b/pyomo/repn/tests/mps/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/mps/integer_variable_declaration_with_marker.mps.baseline b/pyomo/repn/tests/mps/integer_variable_declaration_with_marker.mps.baseline new file mode 100644 index 00000000000..d455b38af0c --- /dev/null +++ b/pyomo/repn/tests/mps/integer_variable_declaration_with_marker.mps.baseline @@ -0,0 +1,27 @@ +* Source: Pyomo MPS Writer +* Format: Free MPS +* +NAME Example-mix-integer-linear-problem +OBJSENSE + MIN +ROWS + N obj + G c_l_const1_ + L c_u_const2_ +COLUMNS + MARK0000 'MARKER' 'INTORG' + x1 obj 3 + x1 c_l_const1_ 4 + x1 c_u_const2_ 1 + MARK0001 'MARKER' 'INTEND' + x2 obj 2 + x2 c_l_const1_ 3 + x2 c_u_const2_ 2 +RHS + RHS c_l_const1_ 10 + RHS c_u_const2_ 7 +BOUNDS + LI BOUND x1 0 + UI BOUND x1 10E20 + LO BOUND x2 0 +ENDATA diff --git a/pyomo/repn/tests/mps/knapsack_problem_binary_variable_declaration_with_marker.mps.baseline b/pyomo/repn/tests/mps/knapsack_problem_binary_variable_declaration_with_marker.mps.baseline new file mode 100644 index 00000000000..d19c9285e5c --- /dev/null +++ b/pyomo/repn/tests/mps/knapsack_problem_binary_variable_declaration_with_marker.mps.baseline @@ -0,0 +1,40 @@ +* Source: Pyomo MPS Writer +* Format: Free MPS +* +NAME knapsack problem +OBJSENSE + MIN +ROWS + N obj + G c_l_const1_ +COLUMNS + MARK0000 'MARKER' 'INTORG' + x(_1_) obj 3 + x(_1_) c_l_const1_ 30 + x(_2_) obj 2 + x(_2_) c_l_const1_ 24 + x(_3_) obj 2 + x(_3_) c_l_const1_ 11 + x(_4_) obj 4 + x(_4_) c_l_const1_ 35 + x(_5_) obj 5 + x(_5_) c_l_const1_ 29 + x(_6_) obj 4 + x(_6_) c_l_const1_ 8 + x(_7_) obj 3 + x(_7_) c_l_const1_ 31 + x(_8_) obj 1 + x(_8_) c_l_const1_ 18 + MARK0001 'MARKER' 'INTEND' +RHS + RHS c_l_const1_ 60 +BOUNDS + BV BOUND x(_1_) + BV BOUND x(_2_) + BV BOUND x(_3_) + BV BOUND x(_4_) + BV BOUND x(_5_) + BV BOUND x(_6_) + BV BOUND x(_7_) + BV BOUND x(_8_) +ENDATA diff --git a/pyomo/repn/tests/mps/test_mps.py b/pyomo/repn/tests/mps/test_mps.py index 44f3d93b75e..ff7981b391c 100644 --- a/pyomo/repn/tests/mps/test_mps.py +++ b/pyomo/repn/tests/mps/test_mps.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -18,7 +18,17 @@ from filecmp import cmp import pyomo.common.unittest as unittest -from pyomo.environ import ConcreteModel, Var, Objective, Constraint, ComponentMap +from pyomo.environ import ( + ConcreteModel, + Var, + Objective, + Constraint, + ComponentMap, + minimize, + Binary, + NonNegativeReals, + NonNegativeIntegers, +) thisdir = os.path.dirname(os.path.abspath(__file__)) @@ -36,11 +46,15 @@ def _get_fnames(self): return prefix + ".mps.baseline", prefix + ".mps.out" def _check_baseline(self, model, **kwds): + int_marker = kwds.pop("int_marker", False) baseline_fname, test_fname = self._get_fnames() self._cleanup(test_fname) io_options = {"symbolic_solver_labels": True} io_options.update(kwds) - model.write(test_fname, format="mps", io_options=io_options) + model.write( + test_fname, format="mps", io_options=io_options, int_marker=int_marker + ) + self.assertTrue( cmp(test_fname, baseline_fname), msg="Files %s and %s differ" % (test_fname, baseline_fname), @@ -185,6 +199,52 @@ def test_row_ordering(self): row_order[model.con4[2]] = -1 self._check_baseline(model, row_order=row_order) + def test_knapsack_problem_binary_variable_declaration_with_marker(self): + elements_size = [30, 24, 11, 35, 29, 8, 31, 18] + elements_weight = [3, 2, 2, 4, 5, 4, 3, 1] + capacity = 60 + + model = ConcreteModel("knapsack problem") + var_names = [f"{i + 1}" for i in range(len(elements_size))] + + model.x = Var(var_names, within=Binary) + + model.obj = Objective( + expr=sum( + model.x[var_names[i]] * elements_weight[i] + for i in range(len(elements_size)) + ), + sense=minimize, + name="obj", + ) + + model.const1 = Constraint( + expr=sum( + model.x[var_names[i]] * elements_size[i] + for i in range(len(elements_size)) + ) + >= capacity, + name="const", + ) + + self._check_baseline(model, int_marker=True) + + def test_integer_variable_declaration_with_marker(self): + model = ConcreteModel("Example-mix-integer-linear-problem") + + # Define the decision variables + model.x1 = Var(within=NonNegativeIntegers) # Integer variable + model.x2 = Var(within=NonNegativeReals) # Continuous variable + + # Define the objective function + model.obj = Objective(expr=3 * model.x1 + 2 * model.x2, sense=minimize) + + # Define the constraints + model.const1 = Constraint(expr=4 * model.x1 + 3 * model.x2 >= 10) + model.const2 = Constraint(expr=model.x1 + 2 * model.x2 <= 7) + + self._check_baseline(model, int_marker=True) + if __name__ == "__main__": unittest.main() diff --git a/pyomo/repn/tests/nl_diff.py b/pyomo/repn/tests/nl_diff.py index e96d6f6357b..aa2b4519db3 100644 --- a/pyomo/repn/tests/nl_diff.py +++ b/pyomo/repn/tests/nl_diff.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/test_linear.py b/pyomo/repn/tests/test_linear.py index 1501ecfcc9d..861fecc7888 100644 --- a/pyomo/repn/tests/test_linear.py +++ b/pyomo/repn/tests/test_linear.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -40,9 +40,10 @@ def __init__(self): self.subexpr = {} self.var_map = {} self.var_order = {} + self.sorter = None def __iter__(self): - return iter((self.subexpr, self.var_map, self.var_order)) + return iter((self.subexpr, self.var_map, self.var_order, self.sorter)) def sum_sq(args, fixed, fgh): @@ -576,8 +577,10 @@ def test_linear(self): cfg = VisitorConfig() repn = LinearRepnVisitor(*cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) - self.assertEqual(cfg.var_map, {id(m.x[0]): m.x[0]}) - self.assertEqual(cfg.var_order, {id(m.x[0]): 0}) + self.assertEqual( + cfg.var_map, {id(m.x[0]): m.x[0], id(m.x[1]): m.x[1], id(m.x[2]): m.x[2]} + ) + self.assertEqual(cfg.var_order, {id(m.x[0]): 0, id(m.x[1]): 1, id(m.x[2]): 2}) self.assertEqual(repn.multiplier, 1) self.assertEqual(repn.constant, 0) self.assertEqual(repn.linear, {id(m.x[0]): 1}) @@ -588,8 +591,10 @@ def test_linear(self): cfg = VisitorConfig() repn = LinearRepnVisitor(*cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) - self.assertEqual(cfg.var_map, {id(m.x[0]): m.x[0]}) - self.assertEqual(cfg.var_order, {id(m.x[0]): 0}) + self.assertEqual( + cfg.var_map, {id(m.x[0]): m.x[0], id(m.x[1]): m.x[1], id(m.x[2]): m.x[2]} + ) + self.assertEqual(cfg.var_order, {id(m.x[0]): 0, id(m.x[1]): 1, id(m.x[2]): 2}) self.assertEqual(repn.multiplier, 1) self.assertEqual(repn.constant, 0) self.assertEqual(repn.linear, {id(m.x[0]): 3}) @@ -600,8 +605,10 @@ def test_linear(self): cfg = VisitorConfig() repn = LinearRepnVisitor(*cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) - self.assertEqual(cfg.var_map, {id(m.x[0]): m.x[0], id(m.x[1]): m.x[1]}) - self.assertEqual(cfg.var_order, {id(m.x[0]): 0, id(m.x[1]): 1}) + self.assertEqual( + cfg.var_map, {id(m.x[0]): m.x[0], id(m.x[1]): m.x[1], id(m.x[2]): m.x[2]} + ) + self.assertEqual(cfg.var_order, {id(m.x[0]): 0, id(m.x[1]): 1, id(m.x[2]): 2}) self.assertEqual(repn.multiplier, 1) self.assertEqual(repn.constant, 0) self.assertEqual(repn.linear, {id(m.x[0]): 3, id(m.x[1]): 4}) @@ -612,8 +619,10 @@ def test_linear(self): cfg = VisitorConfig() repn = LinearRepnVisitor(*cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) - self.assertEqual(cfg.var_map, {id(m.x[0]): m.x[0], id(m.x[1]): m.x[1]}) - self.assertEqual(cfg.var_order, {id(m.x[0]): 0, id(m.x[1]): 1}) + self.assertEqual( + cfg.var_map, {id(m.x[0]): m.x[0], id(m.x[1]): m.x[1], id(m.x[2]): m.x[2]} + ) + self.assertEqual(cfg.var_order, {id(m.x[0]): 0, id(m.x[1]): 1, id(m.x[2]): 2}) self.assertEqual(repn.multiplier, 1) self.assertEqual(repn.constant, 0) self.assertEqual(repn.linear, {id(m.x[0]): 3, id(m.x[1]): 6}) @@ -624,8 +633,10 @@ def test_linear(self): cfg = VisitorConfig() repn = LinearRepnVisitor(*cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) - self.assertEqual(cfg.var_map, {id(m.x[0]): m.x[0], id(m.x[1]): m.x[1]}) - self.assertEqual(cfg.var_order, {id(m.x[0]): 0, id(m.x[1]): 1}) + self.assertEqual( + cfg.var_map, {id(m.x[0]): m.x[0], id(m.x[1]): m.x[1], id(m.x[2]): m.x[2]} + ) + self.assertEqual(cfg.var_order, {id(m.x[0]): 0, id(m.x[1]): 1, id(m.x[2]): 2}) self.assertEqual(repn.multiplier, 1) self.assertEqual(repn.constant, 10) self.assertEqual(repn.linear, {id(m.x[0]): 3, id(m.x[1]): 6}) @@ -636,8 +647,10 @@ def test_linear(self): cfg = VisitorConfig() repn = LinearRepnVisitor(*cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) - self.assertEqual(cfg.var_map, {id(m.x[0]): m.x[0], id(m.x[1]): m.x[1]}) - self.assertEqual(cfg.var_order, {id(m.x[0]): 0, id(m.x[1]): 1}) + self.assertEqual( + cfg.var_map, {id(m.x[0]): m.x[0], id(m.x[1]): m.x[1], id(m.x[2]): m.x[2]} + ) + self.assertEqual(cfg.var_order, {id(m.x[0]): 0, id(m.x[1]): 1, id(m.x[2]): 2}) self.assertEqual(repn.multiplier, 1) self.assertEqual(repn.constant, 50) self.assertEqual(repn.linear, {id(m.x[0]): 3, id(m.x[1]): 6}) @@ -648,8 +661,10 @@ def test_linear(self): cfg = VisitorConfig() repn = LinearRepnVisitor(*cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) - self.assertEqual(cfg.var_map, {id(m.x[0]): m.x[0], id(m.x[1]): m.x[1]}) - self.assertEqual(cfg.var_order, {id(m.x[0]): 0, id(m.x[1]): 1}) + self.assertEqual( + cfg.var_map, {id(m.x[0]): m.x[0], id(m.x[1]): m.x[1], id(m.x[2]): m.x[2]} + ) + self.assertEqual(cfg.var_order, {id(m.x[0]): 0, id(m.x[1]): 1, id(m.x[2]): 2}) self.assertEqual(repn.multiplier, 1) self.assertEqual(repn.constant, 0) self.assertStructuredAlmostEqual( @@ -663,8 +678,10 @@ def test_linear(self): cfg = VisitorConfig() repn = LinearRepnVisitor(*cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) - self.assertEqual(cfg.var_map, {id(m.x[0]): m.x[0], id(m.x[1]): m.x[1]}) - self.assertEqual(cfg.var_order, {id(m.x[0]): 0, id(m.x[1]): 1}) + self.assertEqual( + cfg.var_map, {id(m.x[0]): m.x[0], id(m.x[1]): m.x[1], id(m.x[2]): m.x[2]} + ) + self.assertEqual(cfg.var_order, {id(m.x[0]): 0, id(m.x[1]): 1, id(m.x[2]): 2}) self.assertEqual(repn.multiplier, 1) self.assertEqual(repn.constant, 10) self.assertStructuredAlmostEqual( @@ -677,8 +694,8 @@ def test_linear(self): cfg = VisitorConfig() repn = LinearRepnVisitor(*cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) - self.assertEqual(cfg.var_map, {id(m.x[1]): m.x[1]}) - self.assertEqual(cfg.var_order, {id(m.x[1]): 0}) + self.assertEqual(cfg.var_map, {id(m.x[1]): m.x[1], id(m.x[2]): m.x[2]}) + self.assertEqual(cfg.var_order, {id(m.x[1]): 0, id(m.x[2]): 1}) self.assertEqual(repn.multiplier, 1) self.assertEqual(repn.constant, 40) self.assertStructuredAlmostEqual(repn.linear, {id(m.x[1]): InvalidNumber(nan)}) @@ -1419,6 +1436,22 @@ def test_errors_propagate_nan(self): m.z = Var() m.y.fix(1) + expr = (m.x + 1) / m.p + cfg = VisitorConfig() + with LoggingIntercept() as LOG: + repn = LinearRepnVisitor(*cfg).walk_expression(expr) + self.assertEqual( + LOG.getvalue(), + "Exception encountered evaluating expression 'div(1, 0)'\n" + "\tmessage: division by zero\n" + "\texpression: (x + 1)/p\n", + ) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(str(repn.constant), 'InvalidNumber(nan)') + self.assertEqual(len(repn.linear), 1) + self.assertEqual(str(repn.linear[id(m.x)]), 'InvalidNumber(nan)') + self.assertEqual(repn.nonlinear, None) + expr = m.y + m.x + m.z + ((3 * m.x) / m.p) / m.y cfg = VisitorConfig() with LoggingIntercept() as LOG: @@ -1492,53 +1525,48 @@ def test_type_registrations(self): visitor = LinearRepnVisitor(*cfg) _orig_dispatcher = linear._before_child_dispatcher - linear._before_child_dispatcher = bcd = {} + linear._before_child_dispatcher = bcd = _orig_dispatcher.__class__() + bcd.clear() try: # native type self.assertEqual( - linear._register_new_before_child_dispatcher(visitor, 5), - (False, (linear._CONSTANT, 5)), + bcd.register_dispatcher(visitor, 5), (False, (linear._CONSTANT, 5)) ) self.assertEqual(len(bcd), 1) - self.assertIs(bcd[int], linear._before_native) + self.assertIs(bcd[int], bcd._before_native_numeric) # complex type self.assertEqual( - linear._register_new_before_child_dispatcher(visitor, 5j), - (False, (linear._CONSTANT, 5j)), + bcd.register_dispatcher(visitor, 5j), (False, (linear._CONSTANT, 5j)) ) self.assertEqual(len(bcd), 2) - self.assertIs(bcd[complex], linear._before_complex) + self.assertIs(bcd[complex], bcd._before_complex) # ScalarParam m.p = Param(initialize=5) self.assertEqual( - linear._register_new_before_child_dispatcher(visitor, m.p), - (False, (linear._CONSTANT, 5)), + bcd.register_dispatcher(visitor, m.p), (False, (linear._CONSTANT, 5)) ) self.assertEqual(len(bcd), 3) - self.assertIs(bcd[m.p.__class__], linear._before_param) + self.assertIs(bcd[m.p.__class__], bcd._before_param) # ParamData m.q = Param([0], initialize=6, mutable=True) self.assertEqual( - linear._register_new_before_child_dispatcher(visitor, m.q[0]), - (False, (linear._CONSTANT, 6)), + bcd.register_dispatcher(visitor, m.q[0]), (False, (linear._CONSTANT, 6)) ) self.assertEqual(len(bcd), 4) - self.assertIs(bcd[m.q[0].__class__], linear._before_param) + self.assertIs(bcd[m.q[0].__class__], bcd._before_param) # NPV_SumExpression self.assertEqual( - linear._register_new_before_child_dispatcher(visitor, m.p + m.q[0]), + bcd.register_dispatcher(visitor, m.p + m.q[0]), (False, (linear._CONSTANT, 11)), ) self.assertEqual(len(bcd), 6) - self.assertIs(bcd[NPV_SumExpression], linear._before_npv) - self.assertIs(bcd[LinearExpression], linear._before_general_expression) + self.assertIs(bcd[NPV_SumExpression], bcd._before_npv) + self.assertIs(bcd[LinearExpression], bcd._before_general_expression) # Named expression m.e = Expression(expr=m.p + m.q[0]) - self.assertEqual( - linear._register_new_before_child_dispatcher(visitor, m.e), (True, None) - ) + self.assertEqual(bcd.register_dispatcher(visitor, m.e), (True, None)) self.assertEqual(len(bcd), 7) - self.assertIs(bcd[m.e.__class__], linear._before_named_expression) + self.assertIs(bcd[m.e.__class__], bcd._before_named_expression) finally: linear._before_child_dispatcher = _orig_dispatcher @@ -1577,7 +1605,7 @@ def test_to_expression(self): expr.constant = 0 expr.linear[id(m.x)] = 0 expr.linear[id(m.y)] = 0 - assertExpressionsEqual(self, expr.to_expression(visitor), LinearExpression()) + assertExpressionsEqual(self, expr.to_expression(visitor), 0) @unittest.skipUnless(numpy_available, "Test requires numpy") def test_nonnumeric(self): diff --git a/pyomo/repn/tests/test_quadratic.py b/pyomo/repn/tests/test_quadratic.py index b034099de98..2d2e4022037 100644 --- a/pyomo/repn/tests/test_quadratic.py +++ b/pyomo/repn/tests/test_quadratic.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -29,9 +29,10 @@ def __init__(self): self.subexpr = {} self.var_map = {} self.var_order = {} + self.sorter = None def __iter__(self): - return iter((self.subexpr, self.var_map, self.var_order)) + return iter((self.subexpr, self.var_map, self.var_order, self.sorter)) class TestQuadratic(unittest.TestCase): diff --git a/pyomo/repn/tests/test_standard.py b/pyomo/repn/tests/test_standard.py index b62d18e6eff..6c5a6e3e033 100644 --- a/pyomo/repn/tests/test_standard.py +++ b/pyomo/repn/tests/test_standard.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/test_standard_form.py b/pyomo/repn/tests/test_standard_form.py new file mode 100644 index 00000000000..4c66ae87c41 --- /dev/null +++ b/pyomo/repn/tests/test_standard_form.py @@ -0,0 +1,311 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ +# + +import pyomo.common.unittest as unittest + +import pyomo.environ as pyo + +from pyomo.common.dependencies import numpy as np, scipy_available, numpy_available +from pyomo.common.log import LoggingIntercept +from pyomo.repn.plugins.standard_form import LinearStandardFormCompiler + +for sol in ['glpk', 'cbc', 'gurobi', 'cplex', 'xpress']: + linear_solver = pyo.SolverFactory(sol) + if linear_solver.available(exception_flag=False): + break +else: + linear_solver = None + + +@unittest.skipUnless( + scipy_available & numpy_available, "standard_form requires scipy and numpy" +) +class TestLinearStandardFormCompiler(unittest.TestCase): + def test_linear_model(self): + m = pyo.ConcreteModel() + m.x = pyo.Var() + m.y = pyo.Var([1, 2, 3]) + m.c = pyo.Constraint(expr=m.x + 2 * m.y[1] >= 3) + m.d = pyo.Constraint(expr=m.y[1] + 4 * m.y[3] <= 5) + + repn = LinearStandardFormCompiler().write(m) + + self.assertTrue(np.all(repn.c == np.array([0, 0, 0]))) + self.assertTrue(np.all(repn.A == np.array([[-1, -2, 0], [0, 1, 4]]))) + self.assertTrue(np.all(repn.rhs == np.array([-3, 5]))) + self.assertEqual(repn.rows, [(m.c, -1), (m.d, 1)]) + self.assertEqual(repn.columns, [m.x, m.y[1], m.y[3]]) + + def test_almost_dense_linear_model(self): + m = pyo.ConcreteModel() + m.x = pyo.Var() + m.y = pyo.Var([1, 2, 3]) + m.c = pyo.Constraint(expr=m.x + 2 * m.y[1] + 4 * m.y[3] >= 10) + m.d = pyo.Constraint(expr=5 * m.x + 6 * m.y[1] + 8 * m.y[3] <= 20) + + repn = LinearStandardFormCompiler().write(m) + + self.assertTrue(np.all(repn.c == np.array([0, 0, 0]))) + self.assertTrue(np.all(repn.A == np.array([[-1, -2, -4], [5, 6, 8]]))) + self.assertTrue(np.all(repn.rhs == np.array([-10, 20]))) + self.assertEqual(repn.rows, [(m.c, -1), (m.d, 1)]) + self.assertEqual(repn.columns, [m.x, m.y[1], m.y[3]]) + + def test_linear_model_row_col_order(self): + m = pyo.ConcreteModel() + m.x = pyo.Var() + m.y = pyo.Var([1, 2, 3]) + m.c = pyo.Constraint(expr=m.x + 2 * m.y[1] >= 3) + m.d = pyo.Constraint(expr=m.y[1] + 4 * m.y[3] <= 5) + + repn = LinearStandardFormCompiler().write( + m, column_order=[m.y[3], m.y[2], m.x, m.y[1]], row_order=[m.d, m.c] + ) + + self.assertTrue(np.all(repn.c == np.array([0, 0, 0]))) + self.assertTrue(np.all(repn.A == np.array([[4, 0, 1], [0, -1, -2]]))) + self.assertTrue(np.all(repn.rhs == np.array([5, -3]))) + self.assertEqual(repn.rows, [(m.d, 1), (m.c, -1)]) + self.assertEqual(repn.columns, [m.y[3], m.x, m.y[1]]) + + def test_suffix_warning(self): + m = pyo.ConcreteModel() + m.x = pyo.Var() + m.y = pyo.Var([1, 2, 3]) + m.c = pyo.Constraint(expr=m.x + 2 * m.y[1] >= 3) + m.d = pyo.Constraint(expr=m.y[1] + 4 * m.y[3] <= 5) + + m.dual = pyo.Suffix(direction=pyo.Suffix.EXPORT) + m.b = pyo.Block() + m.b.dual = pyo.Suffix(direction=pyo.Suffix.IMPORT_EXPORT) + + with LoggingIntercept() as LOG: + repn = LinearStandardFormCompiler().write(m) + self.assertEqual(LOG.getvalue(), "") + + m.dual[m.c] = 5 + with LoggingIntercept() as LOG: + repn = LinearStandardFormCompiler().write(m) + self.assertEqual( + LOG.getvalue(), + "EXPORT Suffix 'dual' found on 1 block:\n" + " dual\n" + "Standard Form compiler ignores export suffixes. Skipping.\n", + ) + + m.b.dual[m.d] = 1 + with LoggingIntercept() as LOG: + repn = LinearStandardFormCompiler().write(m) + self.assertEqual( + LOG.getvalue(), + "EXPORT Suffix 'dual' found on 2 blocks:\n" + " dual\n" + " b.dual\n" + "Standard Form compiler ignores export suffixes. Skipping.\n", + ) + + def _verify_solution(self, soln, repn, eq): + # clear out any old solution + for v, val in soln: + v.value = None + for v in repn.x: + v.value = None + + x = np.array(repn.x, dtype=object) + ax = repn.A.todense() @ x + + def c_rule(m, i): + if eq: + return ax[i] == repn.b[i] + else: + return ax[i] <= repn.b[i] + + test_model = pyo.ConcreteModel() + test_model.o = pyo.Objective(expr=repn.c[[1], :].todense()[0] @ x) + test_model.c = pyo.Constraint(range(len(repn.b)), rule=c_rule) + linear_solver.solve(test_model, tee=True) + + # Propagate any solution back to the original variables + for v, expr in repn.eliminated_vars: + v.value = pyo.value(expr) + self.assertEqual(*zip(*((v.value, val) for v, val in soln))) + + @unittest.skipIf( + linear_solver is None, 'verifying results requires a linear solver' + ) + def test_alternative_forms(self): + m = pyo.ConcreteModel() + m.x = pyo.Var() + m.y = pyo.Var( + [0, 1, 3], bounds=lambda m, i: (-1 * (i % 2) * 5, 10 - 12 * (i // 2)) + ) + m.c = pyo.Constraint(expr=m.x + 2 * m.y[1] >= 3) + m.d = pyo.Constraint(expr=m.y[1] + 4 * m.y[3] <= 5) + m.e = pyo.Constraint(expr=pyo.inequality(-2, m.y[0] + 1 + 6 * m.y[1], 7)) + m.f = pyo.Constraint(expr=m.x + m.y[0] + 2 == 10) + m.o = pyo.Objective([1, 3], rule=lambda m, i: m.x + i * 5 * m.y[i]) + m.o[1].sense = pyo.maximize + + col_order = [m.x, m.y[0], m.y[1], m.y[3]] + + m.o[1].deactivate() + linear_solver.solve(m) + m.o[1].activate() + soln = [(v, v.value) for v in col_order] + + repn = LinearStandardFormCompiler().write(m, column_order=col_order) + + self.assertEqual( + repn.rows, [(m.c, -1), (m.d, 1), (m.e, 1), (m.e, -1), (m.f, 1), (m.f, -1)] + ) + self.assertEqual(repn.x, [m.x, m.y[0], m.y[1], m.y[3]]) + ref = np.array( + [ + [-1, 0, -2, 0], + [0, 0, 1, 4], + [0, 1, 6, 0], + [0, -1, -6, 0], + [1, 1, 0, 0], + [-1, -1, 0, 0], + ] + ) + self.assertTrue(np.all(repn.A == ref)) + self.assertTrue(np.all(repn.b == np.array([-3, 5, 6, 3, 8, -8]))) + self.assertTrue(np.all(repn.c == np.array([[-1, 0, -5, 0], [1, 0, 0, 15]]))) + self._verify_solution(soln, repn, False) + + repn = LinearStandardFormCompiler().write( + m, nonnegative_vars=True, column_order=col_order + ) + + self.assertEqual( + repn.rows, [(m.c, -1), (m.d, 1), (m.e, 1), (m.e, -1), (m.f, 1), (m.f, -1)] + ) + self.assertEqual( + list(map(str, repn.x)), + ['_neg_0', '_pos_0', 'y[0]', '_neg_2', '_pos_2', '_neg_3'], + ) + ref = np.array( + [ + [1, -1, 0, 2, -2, 0], + [0, 0, 0, -1, 1, -4], + [0, 0, 1, -6, 6, 0], + [0, 0, -1, 6, -6, 0], + [-1, 1, 1, 0, 0, 0], + [1, -1, -1, 0, 0, 0], + ] + ) + self.assertTrue(np.all(repn.A == ref)) + self.assertTrue(np.all(repn.b == np.array([-3, 5, 6, 3, 8, -8]))) + self.assertTrue( + np.all(repn.c == np.array([[1, -1, 0, 5, -5, 0], [-1, 1, 0, 0, 0, -15]])) + ) + self._verify_solution(soln, repn, False) + + repn = LinearStandardFormCompiler().write( + m, slack_form=True, column_order=col_order + ) + + self.assertEqual(repn.rows, [(m.c, 1), (m.d, 1), (m.e, 1), (m.f, 1)]) + self.assertEqual( + list(map(str, repn.x)), + ['x', 'y[0]', 'y[1]', 'y[3]', '_slack_0', '_slack_1', '_slack_2'], + ) + self.assertEqual( + list(v.bounds for v in repn.x), + [(None, None), (0, 10), (-5, 10), (-5, -2), (None, 0), (0, None), (-9, 0)], + ) + ref = np.array( + [ + [1, 0, 2, 0, 1, 0, 0], + [0, 0, 1, 4, 0, 1, 0], + [0, 1, 6, 0, 0, 0, 1], + [1, 1, 0, 0, 0, 0, 0], + ] + ) + self.assertTrue(np.all(repn.A == ref)) + self.assertTrue(np.all(repn.b == np.array([3, 5, -3, 8]))) + self.assertTrue( + np.all( + repn.c == np.array([[-1, 0, -5, 0, 0, 0, 0], [1, 0, 0, 15, 0, 0, 0]]) + ) + ) + self._verify_solution(soln, repn, True) + + repn = LinearStandardFormCompiler().write( + m, mixed_form=True, column_order=col_order + ) + + self.assertEqual( + repn.rows, [(m.c, -1), (m.d, 1), (m.e, 1), (m.e, -1), (m.f, 0)] + ) + self.assertEqual(list(map(str, repn.x)), ['x', 'y[0]', 'y[1]', 'y[3]']) + self.assertEqual( + list(v.bounds for v in repn.x), [(None, None), (0, 10), (-5, 10), (-5, -2)] + ) + ref = np.array( + [[1, 0, 2, 0], [0, 0, 1, 4], [0, 1, 6, 0], [0, 1, 6, 0], [1, 1, 0, 0]] + ) + self.assertTrue(np.all(repn.A == ref)) + self.assertTrue(np.all(repn.b == np.array([3, 5, 6, -3, 8]))) + self.assertTrue(np.all(repn.c == np.array([[-1, 0, -5, 0], [1, 0, 0, 15]]))) + # Note that the mixed_form solution is a mix of inequality and + # equality constraints, so we cannot (easily) reuse the + # _verify_solutions helper (as in the above cases): + # self._verify_solution(soln, repn, False) + + repn = LinearStandardFormCompiler().write( + m, slack_form=True, nonnegative_vars=True, column_order=col_order + ) + + self.assertEqual(repn.rows, [(m.c, 1), (m.d, 1), (m.e, 1), (m.f, 1)]) + self.assertEqual( + list(map(str, repn.x)), + [ + '_neg_0', + '_pos_0', + 'y[0]', + '_neg_2', + '_pos_2', + '_neg_3', + '_neg_4', + '_slack_1', + '_neg_6', + ], + ) + self.assertEqual( + list(v.bounds for v in repn.x), + [ + (0, None), + (0, None), + (0, 10), + (0, 5), + (0, 10), + (2, 5), + (0, None), + (0, None), + (0, 9), + ], + ) + ref = np.array( + [ + [-1, 1, 0, -2, 2, 0, -1, 0, 0], + [0, 0, 0, -1, 1, -4, 0, 1, 0], + [0, 0, 1, -6, 6, 0, 0, 0, -1], + [-1, 1, 1, 0, 0, 0, 0, 0, 0], + ] + ) + self.assertTrue(np.all(repn.A == ref)) + self.assertTrue(np.all(repn.b == np.array([3, 5, -3, 8]))) + ref = np.array([[1, -1, 0, 5, -5, 0, 0, 0, 0], [-1, 1, 0, 0, 0, -15, 0, 0, 0]]) + self.assertTrue(np.all(repn.c == ref)) + self._verify_solution(soln, repn, True) diff --git a/pyomo/repn/tests/test_util.py b/pyomo/repn/tests/test_util.py index 58cbbe049cf..e0fea0fb45c 100644 --- a/pyomo/repn/tests/test_util.py +++ b/pyomo/repn/tests/test_util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -18,6 +18,14 @@ from pyomo.common.collections import ComponentMap from pyomo.common.errors import DeveloperError, InvalidValueError from pyomo.common.log import LoggingIntercept +from pyomo.core.expr import ( + NumericExpression, + ProductExpression, + NPV_ProductExpression, + SumExpression, + DivisionExpression, + NPV_DivisionExpression, +) from pyomo.environ import ( ConcreteModel, Block, @@ -32,6 +40,9 @@ ) import pyomo.repn.util from pyomo.repn.util import ( + _CONSTANT, + BeforeChildDispatcher, + ExitNodeDispatcher, FileDeterminism, FileDeterminism_to_SortComponents, InvalidNumber, @@ -90,7 +101,7 @@ def test_ftoa_precision(self): # Depending on the platform, np.longdouble may or may not have # higher precision than float: if f == float(f): - test = self.assertNotRegexpMatches + test = self.assertNotRegex else: test = self.assertRegex test( @@ -369,7 +380,7 @@ def test_FileDeterminism_to_SortComponents(self): ) self.assertEqual( FileDeterminism_to_SortComponents(FileDeterminism.ORDERED), - SortComponents.unsorted, + SortComponents.deterministic, ) self.assertEqual( FileDeterminism_to_SortComponents(FileDeterminism.SORT_INDICES), @@ -470,7 +481,7 @@ class MockConfig(object): MockConfig.file_determinism = FileDeterminism.ORDERED self.assertEqual( list(initialize_var_map_from_column_order(m, MockConfig, {}).values()), - [m.b.y[7], m.b.y[6], m.y[3], m.y[2], m.c.y[4], m.x], + [m.b.y[7], m.b.y[6], m.y[3], m.y[2], m.c.y[4], m.x, m.c.y[5]], ) MockConfig.file_determinism = FileDeterminism.SORT_INDICES self.assertEqual( @@ -489,7 +500,7 @@ class MockConfig(object): MockConfig.file_determinism = FileDeterminism.ORDERED self.assertEqual( list(initialize_var_map_from_column_order(m, MockConfig, {}).values()), - [m.b.y[7], m.b.y[6], m.y[3], m.y[2], m.c.y[4], m.x], + [m.b.y[7], m.b.y[6], m.y[3], m.y[2], m.c.y[4], m.x, m.c.y[5]], ) # verify no side effects self.assertEqual(MockConfig.column_order, ref) @@ -637,6 +648,193 @@ class MockConfig(object): # verify no side effects self.assertEqual(MockConfig.row_order, ref) + def test_ExitNodeDispatcher_registration(self): + end = ExitNodeDispatcher( + { + ProductExpression: lambda v, n, d1, d2: d1 * d2, + Expression: lambda v, n, d: d, + } + ) + self.assertEqual(len(end), 2) + + node = ProductExpression((3, 4)) + self.assertEqual(end[node.__class__](None, node, *node.args), 12) + self.assertEqual(len(end), 2) + + node = Expression(initialize=5) + node.construct() + self.assertEqual(end[node.__class__](None, node, *node.args), 5) + self.assertEqual(len(end), 3) + self.assertIn(node.__class__, end) + + node = NPV_ProductExpression((6, 7)) + self.assertEqual(end[node.__class__](None, node, *node.args), 42) + self.assertEqual(len(end), 4) + self.assertIn(NPV_ProductExpression, end) + + end[SumExpression, 2] = lambda v, n, *d: 2 * sum(d) + self.assertEqual(len(end), 5) + + node = SumExpression((1, 2, 3)) + self.assertEqual(end[node.__class__, 2](None, node, *node.args), 12) + self.assertEqual(len(end), 5) + + with self.assertRaisesRegex( + DeveloperError, + r"(?s)Base expression key '\(, 3\)' not found when.*" + r"inserting dispatcher for node 'SumExpression' while walking.*" + r"expression tree.", + ): + end[node.__class__, 3](None, node, *node.args) + self.assertEqual(len(end), 5) + + end[SumExpression] = lambda v, n, *d: sum(d) + self.assertEqual(len(end), 6) + self.assertIn(SumExpression, end) + + self.assertEqual(end[node.__class__, 1](None, node, *node.args), 6) + self.assertEqual(len(end), 7) + self.assertIn((SumExpression, 1), end) + + self.assertEqual(end[node.__class__, 3, 4, 5, 6](None, node, *node.args), 6) + self.assertEqual(len(end), 7) + # We don't cache etypes with more than 3 arguments + self.assertNotIn((SumExpression, 3, 4, 5, 6), end) + + class NewProductExpression(ProductExpression): + pass + + node = NewProductExpression((6, 7)) + self.assertEqual(end[node.__class__](None, node, *node.args), 42) + self.assertEqual(len(end), 8) + self.assertIn(NewProductExpression, end) + + class UnknownExpression(NumericExpression): + pass + + node = UnknownExpression((6, 7)) + with self.assertRaisesRegex( + DeveloperError, r".*Unexpected expression node type 'UnknownExpression'" + ): + end[node.__class__](None, node, *node.args) + self.assertEqual(len(end), 8) + + node = UnknownExpression((6, 7)) + with self.assertRaisesRegex( + DeveloperError, r".*Unexpected expression node type 'UnknownExpression'" + ): + end[node.__class__, 6, 7](None, node, *node.args) + self.assertEqual(len(end), 8) + + def test_BeforeChildDispatcher_registration(self): + class BeforeChildDispatcherTester(BeforeChildDispatcher): + @staticmethod + def _before_var(visitor, child): + return child + + @staticmethod + def _before_named_expression(visitor, child): + return child + + class VisitorTester(object): + def check_constant(self, value, node): + return value + + def evaluate(self, node): + return node() + + visitor = VisitorTester() + + bcd = BeforeChildDispatcherTester() + self.assertEqual(len(bcd), 0) + + node = 5 + self.assertEqual(bcd[node.__class__](None, node), (False, (_CONSTANT, 5))) + self.assertIs(bcd[int], bcd._before_native_numeric) + self.assertEqual(len(bcd), 1) + + node = 'string' + ans = bcd[node.__class__](None, node) + self.assertEqual(ans, (False, (_CONSTANT, InvalidNumber(node)))) + self.assertEqual( + ''.join(ans[1][1].causes), "'string' (str) is not a valid numeric type" + ) + self.assertIs(bcd[str], bcd._before_string) + self.assertEqual(len(bcd), 2) + + node = True + ans = bcd[node.__class__](None, node) + self.assertEqual(ans, (False, (_CONSTANT, InvalidNumber(node)))) + self.assertEqual( + ''.join(ans[1][1].causes), "True (bool) is not a valid numeric type" + ) + self.assertIs(bcd[bool], bcd._before_native_logical) + self.assertEqual(len(bcd), 3) + + node = 1j + ans = bcd[node.__class__](None, node) + self.assertEqual(ans, (False, (_CONSTANT, InvalidNumber(node)))) + self.assertEqual( + ''.join(ans[1][1].causes), "Complex number returned from expression" + ) + self.assertIs(bcd[complex], bcd._before_complex) + self.assertEqual(len(bcd), 4) + + class new_int(int): + pass + + node = new_int(5) + self.assertEqual(bcd[node.__class__](None, node), (False, (_CONSTANT, 5))) + self.assertIs(bcd[new_int], bcd._before_native_numeric) + self.assertEqual(len(bcd), 5) + + node = [] + ans = bcd[node.__class__](None, node) + self.assertEqual(ans, (False, (_CONSTANT, InvalidNumber([])))) + self.assertEqual( + ''.join(ans[1][1].causes), "[] (list) is not a valid numeric type" + ) + self.assertIs(bcd[list], bcd._before_invalid) + self.assertEqual(len(bcd), 6) + + node = Var(initialize=7) + node.construct() + self.assertIs(bcd[node.__class__](None, node), node) + self.assertIs(bcd[node.__class__], bcd._before_var) + self.assertEqual(len(bcd), 7) + + node = Param(initialize=8) + node.construct() + self.assertEqual(bcd[node.__class__](visitor, node), (False, (_CONSTANT, 8))) + self.assertIs(bcd[node.__class__], bcd._before_param) + self.assertEqual(len(bcd), 8) + + node = Expression(initialize=9) + node.construct() + self.assertIs(bcd[node.__class__](None, node), node) + self.assertIs(bcd[node.__class__], bcd._before_named_expression) + self.assertEqual(len(bcd), 9) + + node = SumExpression((3, 5)) + self.assertEqual(bcd[node.__class__](None, node), (True, None)) + self.assertIs(bcd[node.__class__], bcd._before_general_expression) + self.assertEqual(len(bcd), 10) + + node = NPV_ProductExpression((3, 5)) + self.assertEqual(bcd[node.__class__](visitor, node), (False, (_CONSTANT, 15))) + self.assertEqual(len(bcd), 12) + self.assertIs(bcd[NPV_ProductExpression], bcd._before_npv) + self.assertIs(bcd[ProductExpression], bcd._before_general_expression) + self.assertEqual(len(bcd), 12) + + node = NPV_DivisionExpression((3, 0)) + self.assertEqual(bcd[node.__class__](visitor, node), (True, None)) + self.assertEqual(len(bcd), 14) + self.assertIs(bcd[NPV_DivisionExpression], bcd._before_npv) + self.assertIs(bcd[DivisionExpression], bcd._before_general_expression) + self.assertEqual(len(bcd), 14) + if __name__ == "__main__": unittest.main() diff --git a/pyomo/repn/util.py b/pyomo/repn/util.py index e60adbc0b33..8d902d0f99a 100644 --- a/pyomo/repn/util.py +++ b/pyomo/repn/util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,15 +9,24 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import collections import enum +import functools import itertools import logging import operator import sys -from pyomo.common.collections import Sequence, ComponentMap +from pyomo.common.collections import Sequence, ComponentMap, ComponentSet from pyomo.common.deprecation import deprecation_warning from pyomo.common.errors import DeveloperError, InvalidValueError +from pyomo.common.numeric_types import ( + check_if_numeric_type, + native_types, + native_numeric_types, + native_complex_types, + native_logical_types, +) from pyomo.core.pyomoobject import PyomoObject from pyomo.core.base import ( Var, @@ -26,11 +35,13 @@ Objective, Block, Constraint, + Expression, Suffix, SortComponents, ) from pyomo.core.base.component import ActiveComponent -from pyomo.core.expr.numvalue import native_numeric_types, is_fixed, value +from pyomo.core.base.expression import NamedExpressionData +from pyomo.core.expr.numvalue import is_fixed, value import pyomo.core.expr as EXPR import pyomo.core.kernel as kernel @@ -43,6 +54,11 @@ EXPR.LinearExpression, EXPR.NPV_SumExpression, } +_named_subexpression_types = ( + NamedExpressionData, + kernel.expression.expression, + kernel.objective.objective, +) HALT_ON_EVALUATION_ERROR = False nan = float('nan') @@ -221,6 +237,220 @@ def __rpow__(self, other): return self._op(operator.pow, other, self) +_CONSTANT = ExprType.CONSTANT + + +class BeforeChildDispatcher(collections.defaultdict): + """Dispatcher for handling the :py:class:`StreamBasedExpressionVisitor` + `beforeChild` callback + + This dispatcher implements a specialization of :py:`defaultdict` + that supports automatic type registration. Any missing types will + return the :py:meth:`register_dispatcher` method, which (when called + as a callback) will interrogate the type, identify the appropriate + callback, add the callback to the dict, and return the result of + calling the callback. As the callback is added to the dict, no type + will incur the overhead of `register_dispatcher` more than once. + + Note that all dispatchers are implemented as `staticmethod` + functions to avoid the (unnecessary) overhead of binding to the + dispatcher object. + + """ + + __slots__ = () + + def __missing__(self, key): + return self.register_dispatcher + + def register_dispatcher(self, visitor, child): + child_type = type(child) + if child_type in native_numeric_types: + self[child_type] = self._before_native_numeric + elif child_type in native_logical_types: + self[child_type] = self._before_native_logical + elif issubclass(child_type, str): + self[child_type] = self._before_string + elif child_type in native_types: + if issubclass(child_type, tuple(native_complex_types)): + self[child_type] = self._before_complex + else: + self[child_type] = self._before_invalid + elif not hasattr(child, 'is_expression_type'): + if check_if_numeric_type(child): + self[child_type] = self._before_native_numeric + else: + self[child_type] = self._before_invalid + elif not child.is_expression_type(): + if child.is_potentially_variable(): + self[child_type] = self._before_var + else: + self[child_type] = self._before_param + elif not child.is_potentially_variable(): + self[child_type] = self._before_npv + pv_base_type = child.potentially_variable_base_class() + if pv_base_type not in self: + try: + child.__class__ = pv_base_type + self.register_dispatcher(visitor, child) + finally: + child.__class__ = child_type + elif ( + issubclass(child_type, _named_subexpression_types) + or child_type is kernel.expression.noclone + ): + self[child_type] = self._before_named_expression + else: + self[child_type] = self._before_general_expression + return self[child_type](visitor, child) + + @staticmethod + def _before_general_expression(visitor, child): + return True, None + + @staticmethod + def _before_native_numeric(visitor, child): + return False, (_CONSTANT, child) + + @staticmethod + def _before_native_logical(visitor, child): + return False, ( + _CONSTANT, + InvalidNumber( + child, f"{child!r} ({type(child).__name__}) is not a valid numeric type" + ), + ) + + @staticmethod + def _before_complex(visitor, child): + return False, (_CONSTANT, complex_number_error(child, visitor, child)) + + @staticmethod + def _before_invalid(visitor, child): + return False, ( + _CONSTANT, + InvalidNumber( + child, f"{child!r} ({type(child).__name__}) is not a valid numeric type" + ), + ) + + @staticmethod + def _before_string(visitor, child): + return False, ( + _CONSTANT, + InvalidNumber( + child, f"{child!r} ({type(child).__name__}) is not a valid numeric type" + ), + ) + + @staticmethod + def _before_npv(visitor, child): + try: + return False, ( + _CONSTANT, + visitor.check_constant(visitor.evaluate(child), child), + ) + except (ValueError, ArithmeticError): + return True, None + + @staticmethod + def _before_param(visitor, child): + return False, (_CONSTANT, visitor.check_constant(child.value, child)) + + # + # The following methods must be defined by derivative classes (along + # with any other special-case handling they want to implement; + # usually including handling for Monomial, Linear, and + # ExternalFunction + # + + # @staticmethod + # def _before_var(visitor, child): + # pass + + # @staticmethod + # def _before_named_expression(visitor, child): + # pass + + +class ExitNodeDispatcher(collections.defaultdict): + """Dispatcher for handling the :py:class:`StreamBasedExpressionVisitor` + `exitNode` callback + + This dispatcher implements a specialization of :py:`defaultdict` + that supports automatic type registration. Any missing types will + return the :py:meth:`register_dispatcher` method, which (when called + as a callback) will interrogate the type, identify the appropriate + callback, add the callback to the dict, and return the result of + calling the callback. As the callback is added to the dict, no type + will incur the overhead of `register_dispatcher` more than once. + + Note that in this case, the client is expected to register all + non-NPV expression types. The auto-registration is designed to only + handle two cases: + - Auto-detection of user-defined Named Expression types + - Automatic mappimg of NPV expressions to their equivalent non-NPV handlers + + """ + + __slots__ = () + + def __init__(self, *args, **kwargs): + super().__init__(None, *args, **kwargs) + + def __missing__(self, key): + if type(key) is tuple: + # Only lookup/cache argument-specific handlers for unary, + # binary and ternary operators + if len(key) <= 3: + node_class = key[0] + node_args = key[1:] + else: + node_class = key = key[0] + if node_class in self: + return self[node_class] + else: + node_class = key + bases = node_class.__mro__ + # Note: if we add an `etype`, then this special-case can be removed + if ( + issubclass(node_class, _named_subexpression_types) + or node_class is kernel.expression.noclone + ): + bases = [Expression] + fcn = None + for base_type in bases: + if key is not node_class: + if (base_type,) + node_args in self: + fcn = self[(base_type,) + node_args] + break + if base_type in self: + fcn = self[base_type] + break + if fcn is None: + partial_matches = set( + k[0] for k in self if type(k) is tuple and issubclass(node_class, k[0]) + ) + for base_type in node_class.__mro__: + if node_class is not key: + key = (base_type,) + node_args + if base_type in partial_matches: + raise DeveloperError( + f"Base expression key '{key}' not found when inserting " + f"dispatcher for node '{node_class.__name__}' while walking " + "expression tree." + ) + return self.unexpected_expression_type + self[key] = fcn + return fcn + + def unexpected_expression_type(self, visitor, node, *args): + raise DeveloperError( + f"Unexpected expression node type '{type(node).__name__}' " + f"found while walking expression tree in {type(visitor).__name__}." + ) + + def apply_node_operation(node, args): try: ans = node._apply_operation(args) @@ -265,7 +495,7 @@ def categorize_valid_components( Parameters ---------- - model: _BlockData + model: BlockData The model tree to walk active: True or None @@ -286,7 +516,7 @@ def categorize_valid_components( Returns ------- - component_map: Dict[type, List[_BlockData]] + component_map: Dict[type, List[BlockData]] A dict mapping component type to a list of block data objects that contain declared component of that type. @@ -340,12 +570,13 @@ def categorize_valid_components( def FileDeterminism_to_SortComponents(file_determinism): - sorter = SortComponents.unsorted + if file_determinism >= FileDeterminism.SORT_SYMBOLS: + return SortComponents.ALPHABETICAL | SortComponents.SORTED_INDICES if file_determinism >= FileDeterminism.SORT_INDICES: - sorter = sorter | SortComponents.indices - if file_determinism >= FileDeterminism.SORT_SYMBOLS: - sorter = sorter | SortComponents.alphabetical - return sorter + return SortComponents.SORTED_INDICES + if file_determinism >= FileDeterminism.ORDERED: + return SortComponents.ORDERED_INDICES + return SortComponents.UNSORTED def initialize_var_map_from_column_order(model, config, var_map): @@ -377,13 +608,33 @@ def initialize_var_map_from_column_order(model, config, var_map): if column_order is not None: # Note that Vars that appear twice (e.g., through a # Reference) will be sorted with the FIRST occurrence. + fill_in = ComponentSet() for var in column_order: if var.is_indexed(): for _v in var.values(sorter): if not _v.fixed: var_map[id(_v)] = _v elif not var.fixed: + pc = var.parent_component() + if pc is not var and pc not in fill_in: + # For any VarData in an IndexedVar, remember the + # IndexedVar so that after all the VarData that the + # user has specified in the column ordering have + # been processed (and added to the var_map) we can + # go back and fill in any missing VarData from that + # IndexedVar. This is needed because later when + # walking expressions we assume that any VarData + # that is not in the var_map will be the first + # VarData from its Var container (indexed or scalar). + fill_in.add(pc) var_map[id(var)] = var + # Note that ComponentSet iteration is deterministic, and + # re-inserting _v into the var_map will not change the ordering + # for any pre-existing variables + for pc in fill_in: + for _v in pc.values(sorter): + if not _v.fixed: + var_map[id(_v)] = _v return var_map diff --git a/pyomo/scripting/__init__.py b/pyomo/scripting/__init__.py index a3c2c1bb7ce..7cb5ac652fc 100644 --- a/pyomo/scripting/__init__.py +++ b/pyomo/scripting/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/scripting/commands.py b/pyomo/scripting/commands.py index 7782962c2c1..ef59d64b542 100644 --- a/pyomo/scripting/commands.py +++ b/pyomo/scripting/commands.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/scripting/convert.py b/pyomo/scripting/convert.py index 2f0c0e5b400..20f9ef6d382 100644 --- a/pyomo/scripting/convert.py +++ b/pyomo/scripting/convert.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['pyomo2lp', 'pyomo2nl', 'pyomo2dakota'] - import os import sys diff --git a/pyomo/scripting/driver_help.py b/pyomo/scripting/driver_help.py index 81970a6b5cc..38d1a4c16bf 100644 --- a/pyomo/scripting/driver_help.py +++ b/pyomo/scripting/driver_help.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/scripting/interface.py b/pyomo/scripting/interface.py index efb97470e43..fca485b279b 100644 --- a/pyomo/scripting/interface.py +++ b/pyomo/scripting/interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/scripting/plugins/__init__.py b/pyomo/scripting/plugins/__init__.py index 44e3956f314..86a3100e077 100644 --- a/pyomo/scripting/plugins/__init__.py +++ b/pyomo/scripting/plugins/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/scripting/plugins/build_ext.py b/pyomo/scripting/plugins/build_ext.py index 9ae63cbb8a1..5b4ac836a00 100644 --- a/pyomo/scripting/plugins/build_ext.py +++ b/pyomo/scripting/plugins/build_ext.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/scripting/plugins/convert.py b/pyomo/scripting/plugins/convert.py index 55290ed90ce..ea6742cec56 100644 --- a/pyomo/scripting/plugins/convert.py +++ b/pyomo/scripting/plugins/convert.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/scripting/plugins/download.py b/pyomo/scripting/plugins/download.py index 73a164ee708..eea858a737f 100644 --- a/pyomo/scripting/plugins/download.py +++ b/pyomo/scripting/plugins/download.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/scripting/plugins/extras.py b/pyomo/scripting/plugins/extras.py index 4cf9e623212..2bd1c4a0803 100644 --- a/pyomo/scripting/plugins/extras.py +++ b/pyomo/scripting/plugins/extras.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/scripting/plugins/solve.py b/pyomo/scripting/plugins/solve.py index 69451a04e3c..b2a849e995b 100644 --- a/pyomo/scripting/plugins/solve.py +++ b/pyomo/scripting/plugins/solve.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/scripting/pyomo_command.py b/pyomo/scripting/pyomo_command.py index b652e95372a..8beec41a8b1 100644 --- a/pyomo/scripting/pyomo_command.py +++ b/pyomo/scripting/pyomo_command.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/scripting/pyomo_main.py b/pyomo/scripting/pyomo_main.py index 9acafea0471..6497206fdda 100644 --- a/pyomo/scripting/pyomo_main.py +++ b/pyomo/scripting/pyomo_main.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/scripting/pyomo_parser.py b/pyomo/scripting/pyomo_parser.py index 345d400a1aa..9294d46f85e 100644 --- a/pyomo/scripting/pyomo_parser.py +++ b/pyomo/scripting/pyomo_parser.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['add_subparser', 'get_parser', 'subparsers'] - import argparse import sys diff --git a/pyomo/scripting/solve_config.py b/pyomo/scripting/solve_config.py index 3048431d443..7ce3505d045 100644 --- a/pyomo/scripting/solve_config.py +++ b/pyomo/scripting/solve_config.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/scripting/tests/__init__.py b/pyomo/scripting/tests/__init__.py index 88e18b19035..d9146f7eee4 100644 --- a/pyomo/scripting/tests/__init__.py +++ b/pyomo/scripting/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/scripting/tests/test_cmds.py b/pyomo/scripting/tests/test_cmds.py index 960e0d4ada1..9a120c8c175 100644 --- a/pyomo/scripting/tests/test_cmds.py +++ b/pyomo/scripting/tests/test_cmds.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/scripting/util.py b/pyomo/scripting/util.py index 3ec0feccd66..b2a30ebaecd 100644 --- a/pyomo/scripting/util.py +++ b/pyomo/scripting/util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -146,7 +146,7 @@ def pyomo_excepthook(etype, value, tb): if valueStr[0] == valueStr[-1] and valueStr[0] in "\"'": valueStr = valueStr[1:-1] - logger.error(msg + valueStr) + logger.error(msg + valueStr, extra={'cleandoc': False}) tb_list = traceback.extract_tb(tb, None) i = 0 @@ -1129,7 +1129,7 @@ def _run_command_impl(command, parser, args, name, data, options): if type(err) == KeyError and errStr != "None": errStr = str(err).replace(r"\n", "\n")[1:-1] - logger.error(msg + errStr) + logger.error(msg + errStr, extra={'cleandoc': False}) errorcode = 1 return retval, errorcode diff --git a/pyomo/solvers/__init__.py b/pyomo/solvers/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/solvers/__init__.py +++ b/pyomo/solvers/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/amplfunc_merge.py b/pyomo/solvers/amplfunc_merge.py new file mode 100644 index 00000000000..e49fd20e20f --- /dev/null +++ b/pyomo/solvers/amplfunc_merge.py @@ -0,0 +1,32 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + +def amplfunc_string_merge(amplfunc, pyomo_amplfunc): + """Merge two AMPLFUNC variable strings eliminating duplicate lines""" + # Assume that the strings amplfunc and pyomo_amplfunc don't contain duplicates + # Assume that the path separator is correct for the OS so we don't need to + # worry about comparing Unix and Windows paths. + amplfunc_lines = amplfunc.split("\n") + existing = set(amplfunc_lines) + for line in pyomo_amplfunc.split("\n"): + # Skip lines we already have + if line not in existing: + amplfunc_lines.append(line) + # Remove empty lines which could happen if one or both of the strings is + # empty or there are two new lines in a row for whatever reason. + amplfunc_lines = [s for s in amplfunc_lines if s != ""] + return "\n".join(amplfunc_lines) + + +def amplfunc_merge(env): + """Merge AMPLFUNC and PYOMO_AMPLFUNC in an environment var dict""" + return amplfunc_string_merge(env.get("AMPLFUNC", ""), env.get("PYOMO_AMPLFUNC", "")) diff --git a/pyomo/solvers/mockmip.py b/pyomo/solvers/mockmip.py index 9497a6dff9d..2c28b7a9be0 100644 --- a/pyomo/solvers/mockmip.py +++ b/pyomo/solvers/mockmip.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/__init__.py b/pyomo/solvers/plugins/__init__.py index 797ed5036bd..2a7bf2fea04 100644 --- a/pyomo/solvers/plugins/__init__.py +++ b/pyomo/solvers/plugins/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/converter/__init__.py b/pyomo/solvers/plugins/converter/__init__.py index b6baf4f6682..56c32f1c8c1 100644 --- a/pyomo/solvers/plugins/converter/__init__.py +++ b/pyomo/solvers/plugins/converter/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/converter/ampl.py b/pyomo/solvers/plugins/converter/ampl.py index b718faf2d21..0798115a448 100644 --- a/pyomo/solvers/plugins/converter/ampl.py +++ b/pyomo/solvers/plugins/converter/ampl.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/converter/glpsol.py b/pyomo/solvers/plugins/converter/glpsol.py index a38892e3cf5..9b404567c4d 100644 --- a/pyomo/solvers/plugins/converter/glpsol.py +++ b/pyomo/solvers/plugins/converter/glpsol.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/converter/model.py b/pyomo/solvers/plugins/converter/model.py index 89a521d1521..817df157bf5 100644 --- a/pyomo/solvers/plugins/converter/model.py +++ b/pyomo/solvers/plugins/converter/model.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/converter/pico.py b/pyomo/solvers/plugins/converter/pico.py index 7fd0d11222b..e5d008da347 100644 --- a/pyomo/solvers/plugins/converter/pico.py +++ b/pyomo/solvers/plugins/converter/pico.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/solvers/ASL.py b/pyomo/solvers/plugins/solvers/ASL.py index debcd27f75e..bb8174a013e 100644 --- a/pyomo/solvers/plugins/solvers/ASL.py +++ b/pyomo/solvers/plugins/solvers/ASL.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -23,6 +23,7 @@ from pyomo.opt.solver import SystemCallSolver from pyomo.core.kernel.block import IBlock from pyomo.solvers.mockmip import MockMIP +from pyomo.solvers.amplfunc_merge import amplfunc_merge from pyomo.core import TransformationFactory import logging @@ -158,11 +159,9 @@ def create_command_line(self, executable, problem_files): # Pyomo/Pyomo) with any user-specified external function # libraries # - if 'PYOMO_AMPLFUNC' in env: - if 'AMPLFUNC' in env: - env['AMPLFUNC'] += "\n" + env['PYOMO_AMPLFUNC'] - else: - env['AMPLFUNC'] = env['PYOMO_AMPLFUNC'] + amplfunc = amplfunc_merge(env) + if amplfunc: + env['AMPLFUNC'] = amplfunc cmd = [executable, problem_files[0], '-AMPL'] if self._timer: diff --git a/pyomo/solvers/plugins/solvers/BARON.py b/pyomo/solvers/plugins/solvers/BARON.py index eb5ac0830c5..044cab27b86 100644 --- a/pyomo/solvers/plugins/solvers/BARON.py +++ b/pyomo/solvers/plugins/solvers/BARON.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/solvers/CBCplugin.py b/pyomo/solvers/plugins/solvers/CBCplugin.py index 86871dbc1ac..f22fb117c8b 100644 --- a/pyomo/solvers/plugins/solvers/CBCplugin.py +++ b/pyomo/solvers/plugins/solvers/CBCplugin.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['CBC', 'MockCBC'] - import os import re import time @@ -18,6 +16,7 @@ import subprocess from pyomo.common import Executable +from pyomo.common.enums import maximize, minimize from pyomo.common.errors import ApplicationError from pyomo.common.collections import Bunch from pyomo.common.tempfiles import TempfileManager @@ -31,7 +30,6 @@ SolverStatus, TerminationCondition, SolutionStatus, - ProblemSense, Solution, ) from pyomo.opt.solver import SystemCallSolver @@ -445,7 +443,7 @@ def process_logfile(self): # # Parse logfile lines # - results.problem.sense = ProblemSense.minimize + results.problem.sense = minimize results.problem.name = None optim_value = float('inf') lower_bound = None @@ -580,7 +578,7 @@ def process_logfile(self): 'CoinLpIO::readLp(): Maximization problem reformulated as minimization' in ' '.join(tokens) ): - results.problem.sense = ProblemSense.maximize + results.problem.sense = maximize # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L3047 elif n_tokens > 3 and tokens[:2] == ('Result', '-'): if tokens[2:4] in [('Run', 'abandoned'), ('User', 'ctrl-c')]: @@ -754,9 +752,9 @@ def process_logfile(self): "maxIterations parameter." ) soln.gap = gap - if results.problem.sense == ProblemSense.minimize: + if results.problem.sense == minimize: upper_bound = optim_value - elif results.problem.sense == ProblemSense.maximize: + elif results.problem.sense == maximize: _ver = self.version() if _ver and _ver[:3] < (2, 10, 2): optim_value *= -1 @@ -826,7 +824,7 @@ def process_soln_file(self, results): INPUT = [] _ver = self.version() - invert_objective_sense = results.problem.sense == ProblemSense.maximize and ( + invert_objective_sense = results.problem.sense == maximize and ( _ver and _ver[:3] < (2, 10, 2) ) diff --git a/pyomo/solvers/plugins/solvers/CONOPT.py b/pyomo/solvers/plugins/solvers/CONOPT.py index 30e8ada11a1..3455eede67b 100644 --- a/pyomo/solvers/plugins/solvers/CONOPT.py +++ b/pyomo/solvers/plugins/solvers/CONOPT.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -79,7 +79,7 @@ def _get_version(self): return _extract_version('') results = subprocess.run( [solver_exec], - timeout=1, + timeout=self._version_timeout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, diff --git a/pyomo/solvers/plugins/solvers/CPLEX.py b/pyomo/solvers/plugins/solvers/CPLEX.py index 9755bc58614..3a08257c87c 100644 --- a/pyomo/solvers/plugins/solvers/CPLEX.py +++ b/pyomo/solvers/plugins/solvers/CPLEX.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -17,6 +17,7 @@ import subprocess from pyomo.common import Executable +from pyomo.common.enums import maximize, minimize from pyomo.common.errors import ApplicationError from pyomo.common.tempfiles import TempfileManager @@ -28,7 +29,6 @@ SolverStatus, TerminationCondition, SolutionStatus, - ProblemSense, Solution, ) from pyomo.opt.solver import ILMLicensedSystemCallSolver @@ -404,7 +404,7 @@ def _get_version(self): return _extract_version('') results = subprocess.run( [solver_exec, '-c', 'quit'], - timeout=1, + timeout=self._version_timeout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, @@ -547,9 +547,9 @@ def process_logfile(self): ): # CPLEX 11.2 and subsequent has two Nonzeros sections. results.problem.number_of_nonzeros = int(tokens[2]) elif len(tokens) >= 5 and tokens[4] == "MINIMIZE": - results.problem.sense = ProblemSense.minimize + results.problem.sense = minimize elif len(tokens) >= 5 and tokens[4] == "MAXIMIZE": - results.problem.sense = ProblemSense.maximize + results.problem.sense = maximize elif ( len(tokens) >= 4 and tokens[0] == "Solution" @@ -859,9 +859,9 @@ def process_soln_file(self, results): else: sense = tokens[0].lower() if sense in ['max', 'maximize']: - results.problem.sense = ProblemSense.maximize + results.problem.sense = maximize if sense in ['min', 'minimize']: - results.problem.sense = ProblemSense.minimize + results.problem.sense = minimize break tINPUT.close() @@ -952,7 +952,7 @@ def process_soln_file(self, results): ) if primal_feasible == 1: soln.status = SolutionStatus.feasible - if results.problem.sense == ProblemSense.minimize: + if results.problem.sense == minimize: results.problem.upper_bound = soln.objective[ '__default_objective__' ]['Value'] @@ -964,7 +964,7 @@ def process_soln_file(self, results): soln.status = SolutionStatus.infeasible if self._best_bound is not None: - if results.problem.sense == ProblemSense.minimize: + if results.problem.sense == minimize: results.problem.lower_bound = self._best_bound else: results.problem.upper_bound = self._best_bound diff --git a/pyomo/solvers/plugins/solvers/GAMS.py b/pyomo/solvers/plugins/solvers/GAMS.py index 16a126b9af4..035bd0b7603 100644 --- a/pyomo/solvers/plugins/solvers/GAMS.py +++ b/pyomo/solvers/plugins/solvers/GAMS.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -36,12 +36,11 @@ Solution, SolutionStatus, TerminationCondition, - ProblemSense, ) from pyomo.common.dependencies import attempt_import -gdxcc, gdxcc_available = attempt_import('gdxcc', defer_check=True) +gdxcc, gdxcc_available = attempt_import('gdxcc') logger = logging.getLogger('pyomo.solvers') @@ -198,8 +197,8 @@ def _get_version(self): return _extract_version('') from gams import GamsWorkspace - ws = GamsWorkspace() - version = tuple(int(i) for i in ws._version.split('.')[:4]) + workspace = GamsWorkspace() + version = tuple(int(i) for i in workspace._version.split('.')[:4]) while len(version) < 4: version += (0,) return version @@ -209,8 +208,8 @@ def _run_simple_model(self, n): try: from gams import GamsWorkspace, DebugLevel - ws = GamsWorkspace(debug=DebugLevel.Off, working_directory=tmpdir) - t1 = ws.add_job_from_string(self._simple_model(n)) + workspace = GamsWorkspace(debug=DebugLevel.Off, working_directory=tmpdir) + t1 = workspace.add_job_from_string(self._simple_model(n)) t1.run() return True except: @@ -330,12 +329,12 @@ def solve(self, *args, **kwds): if tmpdir is not None and os.path.exists(tmpdir): newdir = False - ws = GamsWorkspace( + workspace = GamsWorkspace( debug=DebugLevel.KeepFiles if keepfiles else DebugLevel.Off, working_directory=tmpdir, ) - t1 = ws.add_job_from_string(output_file.getvalue()) + t1 = workspace.add_job_from_string(output_file.getvalue()) try: with OutputStream(tee=tee, logfile=logfile) as output_stream: @@ -349,7 +348,9 @@ def solve(self, *args, **kwds): # Always name working directory or delete files, # regardless of any errors. if keepfiles: - print("\nGAMS WORKING DIRECTORY: %s\n" % ws.working_directory) + print( + "\nGAMS WORKING DIRECTORY: %s\n" % workspace.working_directory + ) elif tmpdir is not None: # Garbage collect all references to t1.out_db # So that .gdx file can be deleted @@ -359,7 +360,7 @@ def solve(self, *args, **kwds): except: # Catch other errors and remove files first if keepfiles: - print("\nGAMS WORKING DIRECTORY: %s\n" % ws.working_directory) + print("\nGAMS WORKING DIRECTORY: %s\n" % workspace.working_directory) elif tmpdir is not None: # Garbage collect all references to t1.out_db # So that .gdx file can be deleted @@ -398,7 +399,9 @@ def solve(self, *args, **kwds): extract_rc = 'rc' in model_suffixes results = SolverResults() - results.problem.name = os.path.join(ws.working_directory, t1.name + '.gms') + results.problem.name = os.path.join( + workspace.working_directory, t1.name + '.gms' + ) results.problem.lower_bound = t1.out_db["OBJEST"].find_record().value results.problem.upper_bound = t1.out_db["OBJEST"].find_record().value results.problem.number_of_variables = t1.out_db["NUMVAR"].find_record().value @@ -418,11 +421,10 @@ def solve(self, *args, **kwds): assert len(obj) == 1, 'Only one objective is allowed.' obj = obj[0] objctvval = t1.out_db["OBJVAL"].find_record().value + results.problem.sense = obj.sense if obj.is_minimizing(): - results.problem.sense = ProblemSense.minimize results.problem.upper_bound = objctvval else: - results.problem.sense = ProblemSense.maximize results.problem.lower_bound = objctvval results.solver.name = "GAMS " + str(self.version()) @@ -587,7 +589,7 @@ def solve(self, *args, **kwds): results.solution.insert(soln) if keepfiles: - print("\nGAMS WORKING DIRECTORY: %s\n" % ws.working_directory) + print("\nGAMS WORKING DIRECTORY: %s\n" % workspace.working_directory) elif tmpdir is not None: # Garbage collect all references to t1.out_db # So that .gdx file can be deleted @@ -605,9 +607,9 @@ def solve(self, *args, **kwds): results.solution(0).symbol_map = getattr(model, "._symbol_maps")[ results._smap_id ] - results.solution( - 0 - ).default_variable_value = self._default_variable_value + results.solution(0).default_variable_value = ( + self._default_variable_value + ) if load_solutions: model.load_solution(results.solution(0)) else: @@ -980,11 +982,10 @@ def solve(self, *args, **kwds): assert len(obj) == 1, 'Only one objective is allowed.' obj = obj[0] objctvval = stat_vars["OBJVAL"] + results.problem.sense = obj.sense if obj.is_minimizing(): - results.problem.sense = ProblemSense.minimize results.problem.upper_bound = objctvval else: - results.problem.sense = ProblemSense.maximize results.problem.lower_bound = objctvval results.solver.name = "GAMS " + str(self.version()) @@ -1187,9 +1188,9 @@ def solve(self, *args, **kwds): results.solution(0).symbol_map = getattr(model, "._symbol_maps")[ results._smap_id ] - results.solution( - 0 - ).default_variable_value = self._default_variable_value + results.solution(0).default_variable_value = ( + self._default_variable_value + ) if load_solutions: model.load_solution(results.solution(0)) else: diff --git a/pyomo/solvers/plugins/solvers/GLPK.py b/pyomo/solvers/plugins/solvers/GLPK.py index a5b8ad9c019..c8d5bc14237 100644 --- a/pyomo/solvers/plugins/solvers/GLPK.py +++ b/pyomo/solvers/plugins/solvers/GLPK.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -19,6 +19,8 @@ from pyomo.common import Executable from pyomo.common.collections import Bunch +from pyomo.common.enums import maximize, minimize +from pyomo.common.errors import ApplicationError from pyomo.opt import ( SolverFactory, OptSolver, @@ -27,7 +29,6 @@ SolverResults, TerminationCondition, SolutionStatus, - ProblemSense, ) from pyomo.opt.base.solvers import _extract_version from pyomo.opt.solver import SystemCallSolver @@ -137,7 +138,7 @@ def _get_version(self, executable=None): [executable, "--version"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - timeout=1, + timeout=self._version_timeout, universal_newlines=True, ) return _extract_version(result.stdout) @@ -307,10 +308,8 @@ def process_soln_file(self, results): ): raise ValueError - self.is_integer = 'mip' == ptype and True or False - prob.sense = ( - 'min' == psense and ProblemSense.minimize or ProblemSense.maximize - ) + self.is_integer = 'mip' == ptype + prob.sense = minimize if 'min' == psense else maximize prob.number_of_constraints = prows prob.number_of_nonzeros = pnonz prob.number_of_variables = pcols diff --git a/pyomo/solvers/plugins/solvers/GUROBI.py b/pyomo/solvers/plugins/solvers/GUROBI.py index 3c7c227b9e7..3a3a4d52322 100644 --- a/pyomo/solvers/plugins/solvers/GUROBI.py +++ b/pyomo/solvers/plugins/solvers/GUROBI.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -18,6 +18,7 @@ from pyomo.common import Executable from pyomo.common.collections import Bunch +from pyomo.common.enums import maximize, minimize from pyomo.common.fileutils import this_file_dir from pyomo.common.tee import capture_output from pyomo.common.tempfiles import TempfileManager @@ -28,7 +29,6 @@ SolverStatus, TerminationCondition, SolutionStatus, - ProblemSense, Solution, ) from pyomo.opt.solver import ILMLicensedSystemCallSolver @@ -472,7 +472,7 @@ def process_soln_file(self, results): soln.objective['__default_objective__'] = { 'Value': float(tokens[1]) } - if results.problem.sense == ProblemSense.minimize: + if results.problem.sense == minimize: results.problem.upper_bound = float(tokens[1]) else: results.problem.lower_bound = float(tokens[1]) @@ -480,9 +480,9 @@ def process_soln_file(self, results): name = tokens[1] if name != "c_e_ONE_VAR_CONSTANT": if name.startswith('c_'): - soln_constraints.setdefault(tokens[1], {})[ - "Dual" - ] = float(tokens[2]) + soln_constraints.setdefault(tokens[1], {})["Dual"] = ( + float(tokens[2]) + ) elif name.startswith('r_l_'): range_duals.setdefault(name[4:], [0, 0])[0] = float( tokens[2] @@ -495,9 +495,9 @@ def process_soln_file(self, results): name = tokens[1] if name != "c_e_ONE_VAR_CONSTANT": if name.startswith('c_'): - soln_constraints.setdefault(tokens[1], {})[ - "Slack" - ] = float(tokens[2]) + soln_constraints.setdefault(tokens[1], {})["Slack"] = ( + float(tokens[2]) + ) elif name.startswith('r_l_'): range_slacks.setdefault(name[4:], [0, 0])[0] = float( tokens[2] @@ -514,9 +514,9 @@ def process_soln_file(self, results): elif section == 1: if tokens[0] == 'sense': if tokens[1] == 'minimize': - results.problem.sense = ProblemSense.minimize + results.problem.sense = minimize elif tokens[1] == 'maximize': - results.problem.sense = ProblemSense.maximize + results.problem.sense = maximize else: try: val = eval(tokens[1]) diff --git a/pyomo/solvers/plugins/solvers/GUROBI_RUN.py b/pyomo/solvers/plugins/solvers/GUROBI_RUN.py index 2b505adf49c..88f953e18ae 100644 --- a/pyomo/solvers/plugins/solvers/GUROBI_RUN.py +++ b/pyomo/solvers/plugins/solvers/GUROBI_RUN.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/solvers/IPOPT.py b/pyomo/solvers/plugins/solvers/IPOPT.py index 611180113c8..21045cb7b4f 100644 --- a/pyomo/solvers/plugins/solvers/IPOPT.py +++ b/pyomo/solvers/plugins/solvers/IPOPT.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -21,6 +21,8 @@ from pyomo.opt.results import SolverStatus, SolverResults, TerminationCondition from pyomo.opt.solver import SystemCallSolver +from pyomo.solvers.amplfunc_merge import amplfunc_merge + import logging logger = logging.getLogger('pyomo.solvers') @@ -79,7 +81,7 @@ def _get_version(self): return _extract_version('') results = subprocess.run( [solver_exec, "-v"], - timeout=1, + timeout=self._version_timeout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, @@ -119,11 +121,9 @@ def create_command_line(self, executable, problem_files): # Pyomo/Pyomo) with any user-specified external function # libraries # - if 'PYOMO_AMPLFUNC' in env: - if 'AMPLFUNC' in env: - env['AMPLFUNC'] += "\n" + env['PYOMO_AMPLFUNC'] - else: - env['AMPLFUNC'] = env['PYOMO_AMPLFUNC'] + amplfunc = amplfunc_merge(env) + if amplfunc: + env['AMPLFUNC'] = amplfunc cmd = [executable, problem_files[0], '-AMPL'] if self._timer: diff --git a/pyomo/solvers/plugins/solvers/SCIPAMPL.py b/pyomo/solvers/plugins/solvers/SCIPAMPL.py index 69a24455706..98dad4ca5fd 100644 --- a/pyomo/solvers/plugins/solvers/SCIPAMPL.py +++ b/pyomo/solvers/plugins/solvers/SCIPAMPL.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -20,12 +20,7 @@ from pyomo.opt.base import ProblemFormat, ResultsFormat from pyomo.opt.base.solvers import _extract_version, SolverFactory -from pyomo.opt.results import ( - SolverStatus, - TerminationCondition, - SolutionStatus, - ProblemSense, -) +from pyomo.opt.results import SolverStatus, TerminationCondition, SolutionStatus from pyomo.opt.solver import SystemCallSolver import logging @@ -103,7 +98,7 @@ def _get_version(self, solver_exec=None): return _extract_version('') results = subprocess.run( [solver_exec, "--version"], - timeout=1, + timeout=self._version_timeout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, @@ -288,7 +283,7 @@ def _postsolve(self): # UNKNOWN # unknown='unknown' # An uninitialized value - if results.solver.message == "unknown": + if "unknown" in results.solver.message: results.solver.status = SolverStatus.unknown results.solver.termination_condition = TerminationCondition.unknown if len(results.solution) > 0: @@ -296,7 +291,7 @@ def _postsolve(self): # ABORTED # userInterrupt='userInterrupt' # Interrupt signal generated by user - elif results.solver.message == "user interrupt": + elif "user interrupt" in results.solver.message: results.solver.status = SolverStatus.aborted results.solver.termination_condition = TerminationCondition.userInterrupt if len(results.solution) > 0: @@ -304,7 +299,7 @@ def _postsolve(self): # OK # maxEvaluations='maxEvaluations' # Exceeded maximum number of problem evaluations - elif results.solver.message == "node limit reached": + elif "node limit reached" in results.solver.message: results.solver.status = SolverStatus.ok results.solver.termination_condition = TerminationCondition.maxEvaluations if len(results.solution) > 0: @@ -312,7 +307,7 @@ def _postsolve(self): # OK # maxEvaluations='maxEvaluations' # Exceeded maximum number of problem evaluations - elif results.solver.message == "total node limit reached": + elif "total node limit reached" in results.solver.message: results.solver.status = SolverStatus.ok results.solver.termination_condition = TerminationCondition.maxEvaluations if len(results.solution) > 0: @@ -320,7 +315,7 @@ def _postsolve(self): # OK # maxEvaluations='maxEvaluations' # Exceeded maximum number of problem evaluations - elif results.solver.message == "stall node limit reached": + elif "stall node limit reached" in results.solver.message: results.solver.status = SolverStatus.ok results.solver.termination_condition = TerminationCondition.maxEvaluations if len(results.solution) > 0: @@ -328,7 +323,7 @@ def _postsolve(self): # OK # maxTimeLimit='maxTimeLimit' # Exceeded maximum time limited allowed by user but having return a feasible solution - elif results.solver.message == "time limit reached": + elif "time limit reached" in results.solver.message: results.solver.status = SolverStatus.ok results.solver.termination_condition = TerminationCondition.maxTimeLimit if len(results.solution) > 0: @@ -336,7 +331,7 @@ def _postsolve(self): # OK # other='other' # Other, uncategorized normal termination - elif results.solver.message == "memory limit reached": + elif "memory limit reached" in results.solver.message: results.solver.status = SolverStatus.ok results.solver.termination_condition = TerminationCondition.other if len(results.solution) > 0: @@ -344,7 +339,7 @@ def _postsolve(self): # OK # other='other' # Other, uncategorized normal termination - elif results.solver.message == "gap limit reached": + elif "gap limit reached" in results.solver.message: results.solver.status = SolverStatus.ok results.solver.termination_condition = TerminationCondition.other if len(results.solution) > 0: @@ -352,7 +347,7 @@ def _postsolve(self): # OK # other='other' # Other, uncategorized normal termination - elif results.solver.message == "solution limit reached": + elif "solution limit reached" in results.solver.message: results.solver.status = SolverStatus.ok results.solver.termination_condition = TerminationCondition.other if len(results.solution) > 0: @@ -360,7 +355,7 @@ def _postsolve(self): # OK # other='other' # Other, uncategorized normal termination - elif results.solver.message == "solution improvement limit reached": + elif "solution improvement limit reached" in results.solver.message: results.solver.status = SolverStatus.ok results.solver.termination_condition = TerminationCondition.other if len(results.solution) > 0: @@ -368,19 +363,29 @@ def _postsolve(self): # OK # optimal='optimal' # Found an optimal solution - elif results.solver.message == "optimal solution found": + elif "optimal solution" in results.solver.message: results.solver.status = SolverStatus.ok results.solver.termination_condition = TerminationCondition.optimal if len(results.solution) > 0: results.solution(0).status = SolutionStatus.optimal - if results.problem.sense == ProblemSense.minimize: - results.problem.lower_bound = results.solver.primal_bound - else: - results.problem.upper_bound = results.solver.primal_bound + try: + if results.solver.primal_bound < results.solver.dual_bound: + results.problem.lower_bound = results.solver.primal_bound + results.problem.upper_bound = results.solver.dual_bound + else: + results.problem.lower_bound = results.solver.dual_bound + results.problem.upper_bound = results.solver.primal_bound + except AttributeError: + """ + This may occur if SCIP solves the problem during presolve. In that case, + the log file may not get parsed correctly (self.read_scip_log), and + results.solver.primal_bound will not be populated. + """ + pass # WARNING # infeasible='infeasible' # Demonstrated that the problem is infeasible - elif results.solver.message == "infeasible": + elif "infeasible" in results.solver.message: results.solver.status = SolverStatus.warning results.solver.termination_condition = TerminationCondition.infeasible if len(results.solution) > 0: @@ -388,7 +393,7 @@ def _postsolve(self): # WARNING # unbounded='unbounded' # Demonstrated that problem is unbounded - elif results.solver.message == "unbounded": + elif "unbounded" in results.solver.message: results.solver.status = SolverStatus.warning results.solver.termination_condition = TerminationCondition.unbounded if len(results.solution) > 0: @@ -396,7 +401,7 @@ def _postsolve(self): # WARNING # infeasibleOrUnbounded='infeasibleOrUnbounded' # Problem is either infeasible or unbounded - elif results.solver.message == "infeasible or unbounded": + elif "infeasible or unbounded" in results.solver.message: results.solver.status = SolverStatus.warning results.solver.termination_condition = ( TerminationCondition.infeasibleOrUnbounded @@ -447,7 +452,7 @@ def read_scip_log(filename: str): solver_status = scip_lines[0][colon_position + 2 : scip_lines[0].index('\n')] solving_time = float( - scip_lines[1][colon_position + 2 : scip_lines[1].index('\n')] + scip_lines[1][colon_position + 2 : scip_lines[1].index('\n')].split(' ')[0] ) try: diff --git a/pyomo/solvers/plugins/solvers/XPRESS.py b/pyomo/solvers/plugins/solvers/XPRESS.py index 6ab51cfbbf3..2c16d971144 100644 --- a/pyomo/solvers/plugins/solvers/XPRESS.py +++ b/pyomo/solvers/plugins/solvers/XPRESS.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.opt.base import OptSolver from pyomo.opt.base.solvers import SolverFactory import logging diff --git a/pyomo/solvers/plugins/solvers/__init__.py b/pyomo/solvers/plugins/solvers/__init__.py index c5fbfa97e42..9b2507d876c 100644 --- a/pyomo/solvers/plugins/solvers/__init__.py +++ b/pyomo/solvers/plugins/solvers/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/solvers/cplex_direct.py b/pyomo/solvers/plugins/solvers/cplex_direct.py index 3ddb328ebdd..93d8015514e 100644 --- a/pyomo/solvers/plugins/solvers/cplex_direct.py +++ b/pyomo/solvers/plugins/solvers/cplex_direct.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -596,8 +596,13 @@ def _set_objective(self, obj): cplex_expr, referenced_vars = self._get_expr_from_pyomo_expr( obj.expr, self._max_obj_degree ) - for i in range(len(cplex_expr.q_coefficients)): - cplex_expr.q_coefficients[i] *= 2 + # CPLEX actually uses x'Qx/2 in the objective, as the + # off-diagonal entries appear in both the lower triangle and the + # upper triangle (i.e., c*x1*x2 and c*x2*x1). However, since + # the diagonal entries only appear once, we need to double them. + for i, v1 in enumerate(cplex_expr.q_variables1): + if v1 == cplex_expr.q_variables2[i]: + cplex_expr.q_coefficients[i] *= 2 for var in referenced_vars: self._referenced_variables[var] += 1 diff --git a/pyomo/solvers/plugins/solvers/cplex_persistent.py b/pyomo/solvers/plugins/solvers/cplex_persistent.py index a7fdcc45ade..754dadc09e2 100644 --- a/pyomo/solvers/plugins/solvers/cplex_persistent.py +++ b/pyomo/solvers/plugins/solvers/cplex_persistent.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -82,7 +82,7 @@ def update_var(self, var): Parameters ---------- - var: Var (scalar Var or single _VarData) + var: Var (scalar Var or single VarData) """ # see PR #366 for discussion about handling indexed @@ -130,7 +130,7 @@ def _add_column(self, var, obj_coef, constraints, coefficients): Parameters ---------- - var: Var (scalar Var or single _VarData) + var: Var (scalar Var or single VarData) obj_coef: float constraints: list of solver constraints coefficients: list of coefficients to put on var in the associated constraint diff --git a/pyomo/solvers/plugins/solvers/direct_or_persistent_solver.py b/pyomo/solvers/plugins/solvers/direct_or_persistent_solver.py index 09bbfbda70f..de38a0372d0 100644 --- a/pyomo/solvers/plugins/solvers/direct_or_persistent_solver.py +++ b/pyomo/solvers/plugins/solvers/direct_or_persistent_solver.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -10,7 +10,7 @@ # ___________________________________________________________________________ from pyomo.core.base.PyomoModel import Model -from pyomo.core.base.block import Block, _BlockData +from pyomo.core.base.block import Block, BlockData from pyomo.core.kernel.block import IBlock from pyomo.opt.base.solvers import OptSolver from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler @@ -177,7 +177,7 @@ def _postsolve(self): """ This method should be implemented by subclasses.""" def _set_instance(self, model, kwds={}): - if not isinstance(model, (Model, IBlock, Block, _BlockData)): + if not isinstance(model, (Model, IBlock, Block, BlockData)): msg = ( "The problem instance supplied to the {0} plugin " "'_presolve' method must be a Model or a Block".format(type(self)) diff --git a/pyomo/solvers/plugins/solvers/direct_solver.py b/pyomo/solvers/plugins/solvers/direct_solver.py index a99eec79fd9..609a81b2018 100644 --- a/pyomo/solvers/plugins/solvers/direct_solver.py +++ b/pyomo/solvers/plugins/solvers/direct_solver.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -15,7 +15,7 @@ from pyomo.solvers.plugins.solvers.direct_or_persistent_solver import ( DirectOrPersistentSolver, ) -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.kernel.block import IBlock from pyomo.core.base.suffix import active_import_suffix_generator from pyomo.core.kernel.suffix import import_suffix_generator @@ -79,8 +79,8 @@ def solve(self, *args, **kwds): # _model = None for arg in args: - if isinstance(arg, (_BlockData, IBlock)): - if isinstance(arg, _BlockData): + if isinstance(arg, (BlockData, IBlock)): + if isinstance(arg, BlockData): if not arg.is_constructed(): raise RuntimeError( "Attempting to solve model=%s with unconstructed " @@ -89,7 +89,7 @@ def solve(self, *args, **kwds): _model = arg # import suffixes must be on the top-level model - if isinstance(arg, _BlockData): + if isinstance(arg, BlockData): model_suffixes = list( name for (name, comp) in active_import_suffix_generator(arg) ) @@ -178,9 +178,9 @@ def solve(self, *args, **kwds): result.solution(0).symbol_map = getattr( _model, "._symbol_maps" )[result._smap_id] - result.solution( - 0 - ).default_variable_value = self._default_variable_value + result.solution(0).default_variable_value = ( + self._default_variable_value + ) if self._load_solutions: _model.load_solution(result.solution(0)) else: diff --git a/pyomo/solvers/plugins/solvers/gurobi_direct.py b/pyomo/solvers/plugins/solvers/gurobi_direct.py index 54ea9111508..ed66a4e0e7b 100644 --- a/pyomo/solvers/plugins/solvers/gurobi_direct.py +++ b/pyomo/solvers/plugins/solvers/gurobi_direct.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -493,9 +493,8 @@ def _add_constraint(self, con): if not con.active: return None - if is_fixed(con.body): - if self._skip_trivial_constraints: - return None + if self._skip_trivial_constraints and is_fixed(con.body): + return None conname = self._symbol_map.getSymbol(con, self._labeler) diff --git a/pyomo/solvers/plugins/solvers/gurobi_persistent.py b/pyomo/solvers/plugins/solvers/gurobi_persistent.py index 382cb7c4e6d..94a2ac6b734 100644 --- a/pyomo/solvers/plugins/solvers/gurobi_persistent.py +++ b/pyomo/solvers/plugins/solvers/gurobi_persistent.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -111,7 +111,7 @@ def update_var(self, var): Parameters ---------- - var: Var (scalar Var or single _VarData) + var: Var (scalar Var or single VarData) """ # see PR #366 for discussion about handling indexed @@ -157,7 +157,7 @@ def set_linear_constraint_attr(self, con, attr, val): Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be modified. attr: str @@ -192,7 +192,7 @@ def set_var_attr(self, var, attr, val): Parameters ---------- - con: pyomo.core.base.var._GeneralVarData + con: pyomo.core.base.var.VarData The pyomo var for which the corresponding gurobi var attribute should be modified. attr: str @@ -342,7 +342,7 @@ def get_var_attr(self, var, attr): Parameters ---------- - var: pyomo.core.base.var._GeneralVarData + var: pyomo.core.base.var.VarData The pyomo var for which the corresponding gurobi var attribute should be retrieved. attr: str @@ -384,7 +384,7 @@ def get_linear_constraint_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be retrieved. attr: str @@ -413,7 +413,7 @@ def get_sos_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.sos._SOSConstraintData + con: pyomo.core.base.sos.SOSConstraintData The pyomo SOS constraint for which the corresponding gurobi SOS constraint attribute should be retrieved. attr: str @@ -431,7 +431,7 @@ def get_quadratic_constraint_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be retrieved. attr: str @@ -569,7 +569,7 @@ def cbCut(self, con): Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The cut to add """ if not con.active: @@ -647,7 +647,7 @@ def cbLazy(self, con): """ Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The lazy constraint to add """ if not con.active: @@ -710,7 +710,7 @@ def _add_column(self, var, obj_coef, constraints, coefficients): Parameters ---------- - var: Var (scalar Var or single _VarData) + var: Var (scalar Var or single VarData) obj_coef: float constraints: list of solver constraints coefficients: list of coefficients to put on var in the associated constraint diff --git a/pyomo/solvers/plugins/solvers/mosek_direct.py b/pyomo/solvers/plugins/solvers/mosek_direct.py index 6a21e0fcb9b..025c71d36f0 100644 --- a/pyomo/solvers/plugins/solvers/mosek_direct.py +++ b/pyomo/solvers/plugins/solvers/mosek_direct.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -305,10 +305,12 @@ def _get_expr_from_pyomo_repn(self, repn, max_degree=2): referenced_vars.update(q_vars) qsubi, qsubj = zip( *[ - (i, j) - if self._pyomo_var_to_solver_var_map[i] - >= self._pyomo_var_to_solver_var_map[j] - else (j, i) + ( + (i, j) + if self._pyomo_var_to_solver_var_map[i] + >= self._pyomo_var_to_solver_var_map[j] + else (j, i) + ) for i, j in repn.quadratic_vars ] ) @@ -465,15 +467,19 @@ def _add_constraints(self, con_seq): q_is, q_js, q_vals = zip(*qexp) l_ids, l_coefs, constants = zip(*arow) lbs = tuple( - -inf - if value(lq_all[i].lower) is None - else value(lq_all[i].lower) - constants[i] + ( + -inf + if value(lq_all[i].lower) is None + else value(lq_all[i].lower) - constants[i] + ) for i in range(num_lq) ) ubs = tuple( - inf - if value(lq_all[i].upper) is None - else value(lq_all[i].upper) - constants[i] + ( + inf + if value(lq_all[i].upper) is None + else value(lq_all[i].upper) - constants[i] + ) for i in range(num_lq) ) fxs = tuple(c.equality for c in lq_all) @@ -486,13 +492,10 @@ def _add_constraints(self, con_seq): ptrb = (0,) + ptre[:-1] asubs = tuple(itertools.chain.from_iterable(l_ids)) avals = tuple(itertools.chain.from_iterable(l_coefs)) - qcsubi = tuple(itertools.chain.from_iterable(q_is)) - qcsubj = tuple(itertools.chain.from_iterable(q_js)) - qcval = tuple(itertools.chain.from_iterable(q_vals)) - qcsubk = tuple(i for i in sub for j in range(len(q_is[i - con_num]))) self._solver_model.appendcons(num_lq) self._solver_model.putarowlist(sub, ptrb, ptre, asubs, avals) - self._solver_model.putqcon(qcsubk, qcsubi, qcsubj, qcval) + for k, i, j, v in zip(sub, q_is, q_js, q_vals): + self._solver_model.putqconk(k, i, j, v) self._solver_model.putconboundlist(sub, bound_types, lbs, ubs) for i, s_n in enumerate(sub_names): self._solver_model.putconname(sub[i], s_n) @@ -552,7 +555,7 @@ def _add_block(self, block): Parameters ---------- - block: Block (scalar Block or single _BlockData) + block: Block (scalar Block or single BlockData) """ var_seq = tuple( block.component_data_objects( diff --git a/pyomo/solvers/plugins/solvers/mosek_persistent.py b/pyomo/solvers/plugins/solvers/mosek_persistent.py index 4e2aa97b379..efcbb7dd9dd 100644 --- a/pyomo/solvers/plugins/solvers/mosek_persistent.py +++ b/pyomo/solvers/plugins/solvers/mosek_persistent.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -85,7 +85,7 @@ def add_constraints(self, con_seq): Parameters ---------- - con_seq: tuple/list of Constraint (scalar Constraint or single _ConstraintData) + con_seq: tuple/list of Constraint (scalar Constraint or single ConstraintData) """ self._add_constraints(con_seq) @@ -95,7 +95,7 @@ def remove_var(self, solver_var): This will keep any other model components intact. Parameters ---------- - solver_var: Var (scalar Var or single _VarData) + solver_var: Var (scalar Var or single VarData) """ self.remove_vars(solver_var) @@ -106,7 +106,7 @@ def remove_vars(self, *solver_vars): This will keep any other model components intact. Parameters ---------- - *solver_var: Var (scalar Var or single _VarData) + *solver_var: Var (scalar Var or single VarData) """ try: var_ids = [] @@ -137,7 +137,7 @@ def remove_constraint(self, solver_con): To remove a conic-domain, you should use the remove_block method. Parameters ---------- - solver_con: Constraint (scalar Constraint or single _ConstraintData) + solver_con: Constraint (scalar Constraint or single ConstraintData) """ self.remove_constraints(solver_con) @@ -151,7 +151,7 @@ def remove_constraints(self, *solver_cons): Parameters ---------- - *solver_cons: Constraint (scalar Constraint or single _ConstraintData) + *solver_cons: Constraint (scalar Constraint or single ConstraintData) """ lq_cons = tuple( itertools.filterfalse(lambda x: isinstance(x, _ConicBase), solver_cons) @@ -205,7 +205,7 @@ def update_vars(self, *solver_vars): changing variable types and bounds. Parameters ---------- - *solver_var: Constraint (scalar Constraint or single _ConstraintData) + *solver_var: Constraint (scalar Constraint or single ConstraintData) """ try: var_ids = [] @@ -213,19 +213,19 @@ def update_vars(self, *solver_vars): var_ids.append(self._pyomo_var_to_solver_var_map[v]) vtypes = tuple(map(self._mosek_vartype_from_var, solver_vars)) lbs = tuple( - value(v) - if v.fixed - else -float('inf') - if value(v.lb) is None - else value(v.lb) + ( + value(v) + if v.fixed + else -float('inf') if value(v.lb) is None else value(v.lb) + ) for v in solver_vars ) ubs = tuple( - value(v) - if v.fixed - else float('inf') - if value(v.ub) is None - else value(v.ub) + ( + value(v) + if v.fixed + else float('inf') if value(v.ub) is None else value(v.ub) + ) for v in solver_vars ) fxs = tuple(v.is_fixed() for v in solver_vars) diff --git a/pyomo/solvers/plugins/solvers/persistent_solver.py b/pyomo/solvers/plugins/solvers/persistent_solver.py index 34df4e4b454..3c2a9e52eab 100644 --- a/pyomo/solvers/plugins/solvers/persistent_solver.py +++ b/pyomo/solvers/plugins/solvers/persistent_solver.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -12,7 +12,7 @@ from pyomo.solvers.plugins.solvers.direct_or_persistent_solver import ( DirectOrPersistentSolver, ) -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.kernel.block import IBlock from pyomo.core.base.suffix import active_import_suffix_generator from pyomo.core.kernel.suffix import import_suffix_generator @@ -96,7 +96,7 @@ def add_block(self, block): Parameters ---------- - block: Block (scalar Block or single _BlockData) + block: Block (scalar Block or single BlockData) """ if self._pyomo_model is None: @@ -132,7 +132,7 @@ def add_constraint(self, con): Parameters ---------- - con: Constraint (scalar Constraint or single _ConstraintData) + con: Constraint (scalar Constraint or single ConstraintData) """ if self._pyomo_model is None: @@ -206,9 +206,9 @@ def add_column(self, model, var, obj_coef, constraints, coefficients): Parameters ---------- model: pyomo ConcreteModel to which the column will be added - var: Var (scalar Var or single _VarData) + var: Var (scalar Var or single VarData) obj_coef: float, pyo.Param - constraints: list of scalar Constraints of single _ConstraintDatas + constraints: list of scalar Constraints of single ConstraintDatas coefficients: list of the coefficient to put on var in the associated constraint """ @@ -295,7 +295,7 @@ def remove_block(self, block): Parameters ---------- - block: Block (scalar Block or a single _BlockData) + block: Block (scalar Block or a single BlockData) """ # see PR #366 for discussion about handling indexed @@ -328,7 +328,7 @@ def remove_constraint(self, con): Parameters ---------- - con: Constraint (scalar Constraint or single _ConstraintData) + con: Constraint (scalar Constraint or single ConstraintData) """ # see PR #366 for discussion about handling indexed @@ -380,7 +380,7 @@ def remove_var(self, var): Parameters ---------- - var: Var (scalar Var or single _VarData) + var: Var (scalar Var or single VarData) """ # see PR #366 for discussion about handling indexed @@ -455,7 +455,7 @@ def solve(self, *args, **kwds): self.available(exception_flag=True) # Collect suffix names to try and import from solution. - if isinstance(self._pyomo_model, _BlockData): + if isinstance(self._pyomo_model, BlockData): model_suffixes = list( name for (name, comp) in active_import_suffix_generator(self._pyomo_model) @@ -547,9 +547,9 @@ def solve(self, *args, **kwds): result.solution(0).symbol_map = getattr( _model, "._symbol_maps" )[result._smap_id] - result.solution( - 0 - ).default_variable_value = self._default_variable_value + result.solution(0).default_variable_value = ( + self._default_variable_value + ) if self._load_solutions: _model.load_solution(result.solution(0)) else: diff --git a/pyomo/solvers/plugins/solvers/pywrapper.py b/pyomo/solvers/plugins/solvers/pywrapper.py index 8f72e630a3d..c3ec2eaf709 100644 --- a/pyomo/solvers/plugins/solvers/pywrapper.py +++ b/pyomo/solvers/plugins/solvers/pywrapper.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/solvers/xpress_direct.py b/pyomo/solvers/plugins/solvers/xpress_direct.py index aa5a4ba1b4e..c62f76d85ce 100644 --- a/pyomo/solvers/plugins/solvers/xpress_direct.py +++ b/pyomo/solvers/plugins/solvers/xpress_direct.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -667,9 +667,8 @@ def _add_constraint(self, con): if not con.active: return None - if is_fixed(con.body): - if self._skip_trivial_constraints: - return None + if self._skip_trivial_constraints and is_fixed(con.body): + return None conname = self._symbol_map.getSymbol(con, self._labeler) diff --git a/pyomo/solvers/plugins/solvers/xpress_persistent.py b/pyomo/solvers/plugins/solvers/xpress_persistent.py index 56024bc0540..fbdc2866dcf 100644 --- a/pyomo/solvers/plugins/solvers/xpress_persistent.py +++ b/pyomo/solvers/plugins/solvers/xpress_persistent.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -90,7 +90,7 @@ def update_var(self, var): Parameters ---------- - var: Var (scalar Var or single _VarData) + var: Var (scalar Var or single VarData) """ # see PR #366 for discussion about handling indexed @@ -124,7 +124,7 @@ def _add_column(self, var, obj_coef, constraints, coefficients): Parameters ---------- - var: Var (scalar Var or single _VarData) + var: Var (scalar Var or single VarData) obj_coef: float constraints: list of solver constraints coefficients: list of coefficients to put on var in the associated constraint diff --git a/pyomo/solvers/tests/__init__.py b/pyomo/solvers/tests/__init__.py index 42c694b0170..4d8d45da724 100644 --- a/pyomo/solvers/tests/__init__.py +++ b/pyomo/solvers/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/checks/__init__.py b/pyomo/solvers/tests/checks/__init__.py index 03a34303759..ccd3a0f98a4 100644 --- a/pyomo/solvers/tests/checks/__init__.py +++ b/pyomo/solvers/tests/checks/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/checks/test_BARON.py b/pyomo/solvers/tests/checks/test_BARON.py index eb58076b09c..29c7ffb0148 100644 --- a/pyomo/solvers/tests/checks/test_BARON.py +++ b/pyomo/solvers/tests/checks/test_BARON.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -20,9 +20,9 @@ from pyomo.opt import SolverFactory, TerminationCondition # check if BARON is available -from pyomo.solvers.tests.solvers import test_solver_cases +from pyomo.solvers.tests.solvers import test_solver_cases as _test_solver_cases -baron_available = test_solver_cases('baron', 'bar').available +baron_available = _test_solver_cases('baron', 'bar').available @unittest.skipIf(not baron_available, "The 'BARON' solver is not available") diff --git a/pyomo/solvers/tests/checks/test_CBCplugin.py b/pyomo/solvers/tests/checks/test_CBCplugin.py index fe01a89bb53..ad8846509ea 100644 --- a/pyomo/solvers/tests/checks/test_CBCplugin.py +++ b/pyomo/solvers/tests/checks/test_CBCplugin.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -29,7 +29,7 @@ maximize, minimize, ) -from pyomo.opt import SolverFactory, ProblemSense, TerminationCondition, SolverStatus +from pyomo.opt import SolverFactory, TerminationCondition, SolverStatus from pyomo.solvers.plugins.solvers.CBCplugin import CBCSHELL cbc_available = SolverFactory('cbc', solver_io='lp').available(exception_flag=False) @@ -62,7 +62,7 @@ def test_infeasible_lp(self): results = self.opt.solve(self.model) - self.assertEqual(ProblemSense.minimize, results.problem.sense) + self.assertEqual(minimize, results.problem.sense) self.assertEqual( TerminationCondition.infeasible, results.solver.termination_condition ) @@ -81,7 +81,7 @@ def test_unbounded_lp(self): results = self.opt.solve(self.model) - self.assertEqual(ProblemSense.maximize, results.problem.sense) + self.assertEqual(maximize, results.problem.sense) self.assertEqual( TerminationCondition.unbounded, results.solver.termination_condition ) @@ -99,7 +99,7 @@ def test_optimal_lp(self): self.assertEqual(0.0, results.problem.lower_bound) self.assertEqual(0.0, results.problem.upper_bound) - self.assertEqual(ProblemSense.minimize, results.problem.sense) + self.assertEqual(minimize, results.problem.sense) self.assertEqual( TerminationCondition.optimal, results.solver.termination_condition ) @@ -118,7 +118,7 @@ def test_infeasible_mip(self): results = self.opt.solve(self.model) - self.assertEqual(ProblemSense.minimize, results.problem.sense) + self.assertEqual(minimize, results.problem.sense) self.assertEqual( TerminationCondition.infeasible, results.solver.termination_condition ) @@ -134,7 +134,7 @@ def test_unbounded_mip(self): results = self.opt.solve(self.model) - self.assertEqual(ProblemSense.minimize, results.problem.sense) + self.assertEqual(minimize, results.problem.sense) self.assertEqual( TerminationCondition.unbounded, results.solver.termination_condition ) @@ -159,7 +159,7 @@ def test_optimal_mip(self): self.assertEqual(1.0, results.problem.upper_bound) self.assertEqual(results.problem.number_of_binary_variables, 2) self.assertEqual(results.problem.number_of_integer_variables, 4) - self.assertEqual(ProblemSense.maximize, results.problem.sense) + self.assertEqual(maximize, results.problem.sense) self.assertEqual( TerminationCondition.optimal, results.solver.termination_condition ) diff --git a/pyomo/solvers/tests/checks/test_CPLEXDirect.py b/pyomo/solvers/tests/checks/test_CPLEXDirect.py index 86e03d1024f..400d7ee5f75 100644 --- a/pyomo/solvers/tests/checks/test_CPLEXDirect.py +++ b/pyomo/solvers/tests/checks/test_CPLEXDirect.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/checks/test_CPLEXPersistent.py b/pyomo/solvers/tests/checks/test_CPLEXPersistent.py index d7f00d0f486..442212d4fbb 100644 --- a/pyomo/solvers/tests/checks/test_CPLEXPersistent.py +++ b/pyomo/solvers/tests/checks/test_CPLEXPersistent.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -101,7 +101,7 @@ def test_add_column_exceptions(self): # add indexed constraint self.assertRaises(AttributeError, opt.add_column, m, m.y, -2, [m.ci], [1]) - # add something not a _ConstraintData + # add something not a ConstraintData self.assertRaises(AttributeError, opt.add_column, m, m.y, -2, [m.x], [1]) # constraint not on solver model diff --git a/pyomo/solvers/tests/checks/test_GAMS.py b/pyomo/solvers/tests/checks/test_GAMS.py index c4913672694..1eef09819f7 100644 --- a/pyomo/solvers/tests/checks/test_GAMS.py +++ b/pyomo/solvers/tests/checks/test_GAMS.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -212,12 +212,12 @@ def test_fixed_var_sign_gms(self): def test_long_var_py(self): with SolverFactory("gams", solver_io="python") as opt: m = ConcreteModel() - x = ( - m.a23456789012345678901234567890123456789012345678901234567890123 - ) = Var() - y = ( - m.b234567890123456789012345678901234567890123456789012345678901234 - ) = Var() + x = m.a23456789012345678901234567890123456789012345678901234567890123 = ( + Var() + ) + y = m.b234567890123456789012345678901234567890123456789012345678901234 = ( + Var() + ) z = ( m.c23456789012345678901234567890123456789012345678901234567890123456789012345678901234567890 ) = Var() @@ -236,12 +236,12 @@ def test_long_var_py(self): def test_long_var_gms(self): with SolverFactory("gams", solver_io="gms") as opt: m = ConcreteModel() - x = ( - m.a23456789012345678901234567890123456789012345678901234567890123 - ) = Var() - y = ( - m.b234567890123456789012345678901234567890123456789012345678901234 - ) = Var() + x = m.a23456789012345678901234567890123456789012345678901234567890123 = ( + Var() + ) + y = m.b234567890123456789012345678901234567890123456789012345678901234 = ( + Var() + ) z = ( m.c23456789012345678901234567890123456789012345678901234567890123456789012345678901234567890 ) = Var() diff --git a/pyomo/solvers/tests/checks/test_MOSEKDirect.py b/pyomo/solvers/tests/checks/test_MOSEKDirect.py index 369cc08161a..2cf7034b80a 100644 --- a/pyomo/solvers/tests/checks/test_MOSEKDirect.py +++ b/pyomo/solvers/tests/checks/test_MOSEKDirect.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/checks/test_MOSEKPersistent.py b/pyomo/solvers/tests/checks/test_MOSEKPersistent.py index 59ea930c4f0..a4c0aa21666 100644 --- a/pyomo/solvers/tests/checks/test_MOSEKPersistent.py +++ b/pyomo/solvers/tests/checks/test_MOSEKPersistent.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.common.unittest as unittest from pyomo.opt import ( diff --git a/pyomo/solvers/tests/checks/test_amplfunc_merge.py b/pyomo/solvers/tests/checks/test_amplfunc_merge.py new file mode 100644 index 00000000000..2c819404d2f --- /dev/null +++ b/pyomo/solvers/tests/checks/test_amplfunc_merge.py @@ -0,0 +1,162 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest +from pyomo.solvers.amplfunc_merge import amplfunc_string_merge, amplfunc_merge + + +class TestAMPLFUNCStringMerge(unittest.TestCase): + def test_merge_no_dup(self): + s1 = "my/place/l1.so\nanother/place/l1.so" + s2 = "my/place/l2.so" + sm = amplfunc_string_merge(s1, s2) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 3) + # The order of lines should be maintained with the second string + # following the first + self.assertEqual(sm_list[0], "my/place/l1.so") + self.assertEqual(sm_list[1], "another/place/l1.so") + self.assertEqual(sm_list[2], "my/place/l2.so") + + def test_merge_empty1(self): + s1 = "" + s2 = "my/place/l2.so" + sm = amplfunc_string_merge(s1, s2) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 1) + self.assertEqual(sm_list[0], "my/place/l2.so") + + def test_merge_empty2(self): + s1 = "my/place/l2.so" + s2 = "" + sm = amplfunc_string_merge(s1, s2) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 1) + self.assertEqual(sm_list[0], "my/place/l2.so") + + def test_merge_empty_both(self): + s1 = "" + s2 = "" + sm = amplfunc_string_merge(s1, s2) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 1) + self.assertEqual(sm_list[0], "") + + def test_merge_bad_type(self): + self.assertRaises(AttributeError, amplfunc_string_merge, "", 3) + self.assertRaises(AttributeError, amplfunc_string_merge, 3, "") + self.assertRaises(AttributeError, amplfunc_string_merge, 3, 3) + self.assertRaises(AttributeError, amplfunc_string_merge, None, "") + self.assertRaises(AttributeError, amplfunc_string_merge, "", None) + self.assertRaises(AttributeError, amplfunc_string_merge, 2.3, "") + self.assertRaises(AttributeError, amplfunc_string_merge, "", 2.3) + + def test_merge_duplicate1(self): + s1 = "my/place/l1.so\nanother/place/l1.so" + s2 = "my/place/l1.so\nanother/place/l1.so" + sm = amplfunc_string_merge(s1, s2) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 2) + # The order of lines should be maintained with the second string + # following the first + self.assertEqual(sm_list[0], "my/place/l1.so") + self.assertEqual(sm_list[1], "another/place/l1.so") + + def test_merge_duplicate2(self): + s1 = "my/place/l1.so\nanother/place/l1.so" + s2 = "my/place/l1.so" + sm = amplfunc_string_merge(s1, s2) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 2) + # The order of lines should be maintained with the second string + # following the first + self.assertEqual(sm_list[0], "my/place/l1.so") + self.assertEqual(sm_list[1], "another/place/l1.so") + + def test_merge_extra_linebreaks(self): + s1 = "\nmy/place/l1.so\nanother/place/l1.so\n" + s2 = "\nmy/place/l1.so\n\n" + sm = amplfunc_string_merge(s1, s2) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 2) + # The order of lines should be maintained with the second string + # following the first + self.assertEqual(sm_list[0], "my/place/l1.so") + self.assertEqual(sm_list[1], "another/place/l1.so") + + +class TestAMPLFUNCMerge(unittest.TestCase): + def test_merge_no_dup(self): + env = { + "AMPLFUNC": "my/place/l1.so\nanother/place/l1.so", + "PYOMO_AMPLFUNC": "my/place/l2.so", + } + sm = amplfunc_merge(env) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 3) + self.assertEqual(sm_list[0], "my/place/l1.so") + self.assertEqual(sm_list[1], "another/place/l1.so") + self.assertEqual(sm_list[2], "my/place/l2.so") + + def test_merge_empty1(self): + env = {"AMPLFUNC": "", "PYOMO_AMPLFUNC": "my/place/l2.so"} + sm = amplfunc_merge(env) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 1) + self.assertEqual(sm_list[0], "my/place/l2.so") + + def test_merge_empty2(self): + env = {"AMPLFUNC": "my/place/l2.so", "PYOMO_AMPLFUNC": ""} + sm = amplfunc_merge(env) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 1) + self.assertEqual(sm_list[0], "my/place/l2.so") + + def test_merge_empty_both(self): + env = {"AMPLFUNC": "", "PYOMO_AMPLFUNC": ""} + sm = amplfunc_merge(env) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 1) + self.assertEqual(sm_list[0], "") + + def test_merge_duplicate1(self): + env = { + "AMPLFUNC": "my/place/l1.so\nanother/place/l1.so", + "PYOMO_AMPLFUNC": "my/place/l1.so\nanother/place/l1.so", + } + sm = amplfunc_merge(env) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 2) + self.assertEqual(sm_list[0], "my/place/l1.so") + self.assertEqual(sm_list[1], "another/place/l1.so") + + def test_merge_no_pyomo(self): + env = {"AMPLFUNC": "my/place/l1.so\nanother/place/l1.so"} + sm = amplfunc_merge(env) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 2) + self.assertEqual(sm_list[0], "my/place/l1.so") + self.assertEqual(sm_list[1], "another/place/l1.so") + + def test_merge_no_user(self): + env = {"PYOMO_AMPLFUNC": "my/place/l1.so\nanother/place/l1.so"} + sm = amplfunc_merge(env) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 2) + self.assertEqual(sm_list[0], "my/place/l1.so") + self.assertEqual(sm_list[1], "another/place/l1.so") + + def test_merge_nothing(self): + env = {} + sm = amplfunc_merge(env) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 1) + self.assertEqual(sm_list[0], "") diff --git a/pyomo/solvers/tests/checks/test_cbc.py b/pyomo/solvers/tests/checks/test_cbc.py index 0fd6e9f49a1..420de7cc61d 100644 --- a/pyomo/solvers/tests/checks/test_cbc.py +++ b/pyomo/solvers/tests/checks/test_cbc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/checks/test_cplex.py b/pyomo/solvers/tests/checks/test_cplex.py index 4f1d7aca99b..ff5ac5f17e1 100644 --- a/pyomo/solvers/tests/checks/test_cplex.py +++ b/pyomo/solvers/tests/checks/test_cplex.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -129,16 +129,14 @@ def get_mock_model(self): def get_mock_cplex_shell(self, mock_model): solver = MockCPLEX() - ( - solver._problem_files, - solver._problem_format, - solver._smap_id, - ) = convert_problem( - (mock_model,), - ProblemFormat.cpxlp, - [ProblemFormat.cpxlp], - has_capability=lambda x: True, - symbolic_solver_labels=True, + (solver._problem_files, solver._problem_format, solver._smap_id) = ( + convert_problem( + (mock_model,), + ProblemFormat.cpxlp, + [ProblemFormat.cpxlp], + has_capability=lambda x: True, + symbolic_solver_labels=True, + ) ) return solver diff --git a/pyomo/solvers/tests/checks/test_gurobi.py b/pyomo/solvers/tests/checks/test_gurobi.py index f33a00ce8a2..e87685a046c 100644 --- a/pyomo/solvers/tests/checks/test_gurobi.py +++ b/pyomo/solvers/tests/checks/test_gurobi.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.common.unittest as unittest from unittest.mock import patch, MagicMock diff --git a/pyomo/solvers/tests/checks/test_gurobi_direct.py b/pyomo/solvers/tests/checks/test_gurobi_direct.py index 7c60b207a9f..1e3a366a37a 100644 --- a/pyomo/solvers/tests/checks/test_gurobi_direct.py +++ b/pyomo/solvers/tests/checks/test_gurobi_direct.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ Tests for working with Gurobi environments. Some require a single-use license and are skipped if this isn't the case. diff --git a/pyomo/solvers/tests/checks/test_gurobi_persistent.py b/pyomo/solvers/tests/checks/test_gurobi_persistent.py index 9d69c1dd920..812390c23a4 100644 --- a/pyomo/solvers/tests/checks/test_gurobi_persistent.py +++ b/pyomo/solvers/tests/checks/test_gurobi_persistent.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -382,7 +382,7 @@ def test_add_column_exceptions(self): # add indexed constraint self.assertRaises(AttributeError, opt.add_column, m, m.y, -2, [m.ci], [1]) - # add something not a _ConstraintData + # add something not a ConstraintData self.assertRaises(AttributeError, opt.add_column, m, m.y, -2, [m.x], [1]) # constraint not on solver model diff --git a/pyomo/solvers/tests/checks/test_no_solution_behavior.py b/pyomo/solvers/tests/checks/test_no_solution_behavior.py index 9ba8e86a013..81a2d2bf297 100644 --- a/pyomo/solvers/tests/checks/test_no_solution_behavior.py +++ b/pyomo/solvers/tests/checks/test_no_solution_behavior.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/checks/test_pickle.py b/pyomo/solvers/tests/checks/test_pickle.py index d8551b34740..745320cb4eb 100644 --- a/pyomo/solvers/tests/checks/test_pickle.py +++ b/pyomo/solvers/tests/checks/test_pickle.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/checks/test_writers.py b/pyomo/solvers/tests/checks/test_writers.py index e406e07a4d6..55002c71357 100644 --- a/pyomo/solvers/tests/checks/test_writers.py +++ b/pyomo/solvers/tests/checks/test_writers.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/checks/test_xpress_persistent.py b/pyomo/solvers/tests/checks/test_xpress_persistent.py index cd9c30fc73b..dcd36780f62 100644 --- a/pyomo/solvers/tests/checks/test_xpress_persistent.py +++ b/pyomo/solvers/tests/checks/test_xpress_persistent.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.common.unittest as unittest import pyomo.environ as pe from pyomo.core.expr.taylor_series import taylor_series_expansion @@ -251,7 +262,7 @@ def test_add_column_exceptions(self): # add indexed constraint self.assertRaises(AttributeError, opt.add_column, m, m.y, -2, [m.ci], [1]) - # add something not a _ConstraintData + # add something not a ConstraintData self.assertRaises(AttributeError, opt.add_column, m, m.y, -2, [m.x], [1]) # constraint not on solver model diff --git a/pyomo/solvers/tests/mip/__init__.py b/pyomo/solvers/tests/mip/__init__.py index c95d27d9497..707a8c4b7e5 100644 --- a/pyomo/solvers/tests/mip/__init__.py +++ b/pyomo/solvers/tests/mip/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/mip/model.py b/pyomo/solvers/tests/mip/model.py index 389151160b8..83c1411fe6c 100644 --- a/pyomo/solvers/tests/mip/model.py +++ b/pyomo/solvers/tests/mip/model.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/mip/test_asl.py b/pyomo/solvers/tests/mip/test_asl.py index 1e6a9e53030..6f23a06eff2 100644 --- a/pyomo/solvers/tests/mip/test_asl.py +++ b/pyomo/solvers/tests/mip/test_asl.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -47,9 +47,9 @@ class mock_all(unittest.TestCase): def setUpClass(cls): global cplexamp_available import pyomo.environ - from pyomo.solvers.tests.solvers import test_solver_cases + from pyomo.solvers.tests.solvers import test_solver_cases as _test_solver_cases - cplexamp_available = test_solver_cases('cplex', 'nl').available + cplexamp_available = _test_solver_cases('cplex', 'nl').available def setUp(self): self.do_setup(False) diff --git a/pyomo/solvers/tests/mip/test_convert.py b/pyomo/solvers/tests/mip/test_convert.py index cd916da29f2..962b021c4ae 100644 --- a/pyomo/solvers/tests/mip/test_convert.py +++ b/pyomo/solvers/tests/mip/test_convert.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/mip/test_factory.py b/pyomo/solvers/tests/mip/test_factory.py index 6960a0f8ced..31d47486aa4 100644 --- a/pyomo/solvers/tests/mip/test_factory.py +++ b/pyomo/solvers/tests/mip/test_factory.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/mip/test_ipopt.py b/pyomo/solvers/tests/mip/test_ipopt.py index ca553c12447..38c3b35d8a1 100644 --- a/pyomo/solvers/tests/mip/test_ipopt.py +++ b/pyomo/solvers/tests/mip/test_ipopt.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -42,9 +42,9 @@ class Test(unittest.TestCase): def setUpClass(cls): global ipopt_available import pyomo.environ - from pyomo.solvers.tests.solvers import test_solver_cases + from pyomo.solvers.tests.solvers import test_solver_cases as _test_solver_cases - ipopt_available = test_solver_cases('ipopt', 'nl').available + ipopt_available = _test_solver_cases('ipopt', 'nl').available def setUp(self): if not ipopt_available: diff --git a/pyomo/solvers/tests/mip/test_mip.py b/pyomo/solvers/tests/mip/test_mip.py index 0257e65de20..58cdfe9f7de 100644 --- a/pyomo/solvers/tests/mip/test_mip.py +++ b/pyomo/solvers/tests/mip/test_mip.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/mip/test_qp.py b/pyomo/solvers/tests/mip/test_qp.py new file mode 100644 index 00000000000..9c5cb5ffbc4 --- /dev/null +++ b/pyomo/solvers/tests/mip/test_qp.py @@ -0,0 +1,194 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ +# + +import pyomo.common.unittest as unittest + +from pyomo.environ import ConcreteModel, Var, Objective, ConstraintList, SolverFactory + +gurobi_lp = SolverFactory('gurobi', solver_io='lp') +gurobi_nl = SolverFactory('gurobi', solver_io='nl') +gurobi_direct = SolverFactory('gurobi_direct') +gurobi_persistent = SolverFactory('gurobi_persistent') +gurobi_appsi = SolverFactory('appsi_gurobi') + +cplex_lp = SolverFactory('cplex', solver_io='lp') +cplex_nl = SolverFactory('cplex', solver_io='nl') +cplex_direct = SolverFactory('cplex_direct') +cplex_persistent = SolverFactory('cplex_persistent') +cplex_appsi = SolverFactory('appsi_cplex') + +xpress_lp = SolverFactory('xpress', solver_io='lp') +xpress_nl = SolverFactory('xpress', solver_io='nl') +xpress_direct = SolverFactory('xpress_direct') +xpress_persistent = SolverFactory('xpress_persistent') +xpress_appsi = SolverFactory('appsi_xpress') + + +class TestQuadraticModels(unittest.TestCase): + def _qp_model(self): + m = ConcreteModel(name="test") + m.x = Var([0, 1, 2]) + m.obj = Objective( + expr=m.x[0] + + 10 * m.x[1] + + 100 * m.x[2] + + 1000 * m.x[1] * m.x[2] + + 10000 * m.x[0] ** 2 + + 10000 * m.x[1] ** 2 + + 100000 * m.x[2] ** 2 + ) + m.c = ConstraintList() + m.c.add(m.x[0] == 1) + m.c.add(m.x[1] == 2) + m.c.add(m.x[2] == 4) + return m + + @unittest.skipUnless( + gurobi_lp.available(exception_flag=False), "needs Gurobi LP interface" + ) + def test_qp_objective_gurobi_lp(self): + m = self._qp_model() + results = gurobi_lp.solve(m) + self.assertEqual(m.obj(), results['Problem'][0]['Upper bound']) + + @unittest.skipUnless( + gurobi_nl.available(exception_flag=False), "needs Gurobi NL interface" + ) + def test_qp_objective_gurobi_nl(self): + m = self._qp_model() + results = gurobi_nl.solve(m) + # TODO: the NL interface should set either the Upper or Lower + # bound for feasible solutions! + # + # self.assertEqual(m.obj(), results['Problem'][0]['Upper bound']) + self.assertIn(str(int(m.obj())), results['Solver'][0]['Message']) + + @unittest.skipUnless( + gurobi_appsi.available(exception_flag=False), "needs Gurobi APPSI interface" + ) + def test_qp_objective_gurobi_appsi(self): + m = self._qp_model() + gurobi_appsi.set_instance(m) + results = gurobi_appsi.solve(m) + self.assertEqual(m.obj(), results['Problem'][0]['Upper bound']) + + @unittest.skipUnless( + gurobi_direct.available(exception_flag=False), "needs Gurobi Direct interface" + ) + def test_qp_objective_gurobi_direct(self): + m = self._qp_model() + results = gurobi_direct.solve(m) + self.assertEqual(m.obj(), results['Problem'][0]['Upper bound']) + + @unittest.skipUnless( + gurobi_persistent.available(exception_flag=False), + "needs Gurobi Persistent interface", + ) + def test_qp_objective_gurobi_persistent(self): + m = self._qp_model() + gurobi_persistent.set_instance(m) + results = gurobi_persistent.solve(m) + self.assertEqual(m.obj(), results['Problem'][0]['Upper bound']) + + @unittest.skipUnless( + cplex_lp.available(exception_flag=False), "needs Cplex LP interface" + ) + def test_qp_objective_cplex_lp(self): + m = self._qp_model() + results = cplex_lp.solve(m) + self.assertEqual(m.obj(), results['Problem'][0]['Upper bound']) + + @unittest.skipUnless( + cplex_nl.available(exception_flag=False), "needs Cplex NL interface" + ) + def test_qp_objective_cplex_nl(self): + m = self._qp_model() + results = cplex_nl.solve(m) + # TODO: the NL interface should set either the Upper or Lower + # bound for feasible solutions! + # + # self.assertEqual(m.obj(), results['Problem'][0]['Upper bound']) + self.assertIn(str(int(m.obj())), results['Solver'][0]['Message']) + + @unittest.skipUnless( + cplex_direct.available(exception_flag=False), "needs Cplex Direct interface" + ) + def test_qp_objective_cplex_direct(self): + m = self._qp_model() + results = cplex_direct.solve(m) + self.assertEqual(m.obj(), results['Problem'][0]['Upper bound']) + + @unittest.skipUnless( + cplex_persistent.available(exception_flag=False), + "needs Cplex Persistent interface", + ) + def test_qp_objective_cplex_persistent(self): + m = self._qp_model() + cplex_persistent.set_instance(m) + results = cplex_persistent.solve(m) + self.assertEqual(m.obj(), results['Problem'][0]['Upper bound']) + + @unittest.skipUnless( + cplex_appsi.available(exception_flag=False), "needs Cplex APPSI interface" + ) + def test_qp_objective_cplex_appsi(self): + m = self._qp_model() + cplex_appsi.set_instance(m) + results = cplex_appsi.solve(m) + self.assertEqual(m.obj(), results['Problem'][0]['Upper bound']) + + @unittest.skipUnless( + xpress_lp.available(exception_flag=False), "needs Xpress LP interface" + ) + def test_qp_objective_xpress_lp(self): + m = self._qp_model() + results = xpress_lp.solve(m) + self.assertEqual(m.obj(), results['Problem'][0]['Upper bound']) + + @unittest.skipUnless( + xpress_nl.available(exception_flag=False), "needs Xpress NL interface" + ) + def test_qp_objective_xpress_nl(self): + m = self._qp_model() + results = xpress_nl.solve(m) + # TODO: the NL interface should set either the Upper or Lower + # bound for feasible solutions! + # + # self.assertEqual(m.obj(), results['Problem'][0]['Upper bound']) + self.assertIn(str(int(m.obj())), results['Solver'][0]['Message']) + + @unittest.skipUnless( + xpress_direct.available(exception_flag=False), "needs Xpress Direct interface" + ) + def test_qp_objective_xpress_direct(self): + m = self._qp_model() + results = xpress_direct.solve(m) + self.assertEqual(m.obj(), results['Problem'][0]['Upper bound']) + + @unittest.skipUnless( + xpress_persistent.available(exception_flag=False), + "needs Xpress Persistent interface", + ) + def test_qp_objective_xpress_persistent(self): + m = self._qp_model() + xpress_persistent.set_instance(m) + results = xpress_persistent.solve(m) + self.assertEqual(m.obj(), results['Problem'][0]['Upper bound']) + + @unittest.skipUnless( + xpress_appsi.available(exception_flag=False), "needs Xpress APPSI interface" + ) + def test_qp_objective_xpress_appsi(self): + m = self._qp_model() + xpress_appsi.set_instance(m) + results = xpress_appsi.solve(m) + self.assertEqual(m.obj(), results['Problem'][0]['Upper bound']) diff --git a/pyomo/solvers/tests/mip/test_scip.py b/pyomo/solvers/tests/mip/test_scip.py index 8a43b120a34..ad54daeddc0 100644 --- a/pyomo/solvers/tests/mip/test_scip.py +++ b/pyomo/solvers/tests/mip/test_scip.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -33,9 +33,9 @@ class Test(unittest.TestCase): def setUpClass(cls): global scip_available import pyomo.environ - from pyomo.solvers.tests.solvers import test_solver_cases + from pyomo.solvers.tests.solvers import test_solver_cases as _test_solver_cases - scip_available = test_solver_cases('scip', 'nl').available + scip_available = _test_solver_cases('scip', 'nl').available def setUp(self): if not scip_available: @@ -106,6 +106,12 @@ def test_scip_solve_from_instance_options(self): results.write(filename=_out, times=False, format='json') self.compare_json(_out, join(currdir, "test_scip_solve_from_instance.baseline")) + def test_scip_solve_from_instance_with_reoptimization(self): + # Test scip with re-optimization option enabled + # This case changes the Scip output results which may break the results parser + self.scip.options['reoptimization/enable'] = True + self.test_scip_solve_from_instance() + if __name__ == "__main__": deleteFiles = False diff --git a/pyomo/solvers/tests/mip/test_scip_log_data.py b/pyomo/solvers/tests/mip/test_scip_log_data.py index 8f756de220a..a0006d69eb7 100644 --- a/pyomo/solvers/tests/mip/test_scip_log_data.py +++ b/pyomo/solvers/tests/mip/test_scip_log_data.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ diff --git a/pyomo/solvers/tests/mip/test_scip_solve_from_instance.baseline b/pyomo/solvers/tests/mip/test_scip_solve_from_instance.baseline index a3eb9ffacec..976e4a1b82e 100644 --- a/pyomo/solvers/tests/mip/test_scip_solve_from_instance.baseline +++ b/pyomo/solvers/tests/mip/test_scip_solve_from_instance.baseline @@ -1,7 +1,7 @@ { "Problem": [ { - "Lower bound": -Infinity, + "Lower bound": 1.0, "Number of constraints": 0, "Number of objectives": 1, "Number of variables": 1, diff --git a/pyomo/solvers/tests/mip/test_scip_version.py b/pyomo/solvers/tests/mip/test_scip_version.py index c0cc80c0316..f83bed2da32 100644 --- a/pyomo/solvers/tests/mip/test_scip_version.py +++ b/pyomo/solvers/tests/mip/test_scip_version.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/mip/test_solver.py b/pyomo/solvers/tests/mip/test_solver.py index 90a7076cbca..bf3550a001d 100644 --- a/pyomo/solvers/tests/mip/test_solver.py +++ b/pyomo/solvers/tests/mip/test_solver.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/LP_block.py b/pyomo/solvers/tests/models/LP_block.py index 64c866faa9e..37b01dc1c2d 100644 --- a/pyomo/solvers/tests/models/LP_block.py +++ b/pyomo/solvers/tests/models/LP_block.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/LP_compiled.py b/pyomo/solvers/tests/models/LP_compiled.py index 686406e7ec6..960b8730e0c 100644 --- a/pyomo/solvers/tests/models/LP_compiled.py +++ b/pyomo/solvers/tests/models/LP_compiled.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/LP_constant_objective1.py b/pyomo/solvers/tests/models/LP_constant_objective1.py index 306a7a867a2..0c01cd7085f 100644 --- a/pyomo/solvers/tests/models/LP_constant_objective1.py +++ b/pyomo/solvers/tests/models/LP_constant_objective1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/LP_constant_objective2.py b/pyomo/solvers/tests/models/LP_constant_objective2.py index 17da01bf209..07739c1f708 100644 --- a/pyomo/solvers/tests/models/LP_constant_objective2.py +++ b/pyomo/solvers/tests/models/LP_constant_objective2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/LP_duals_maximize.py b/pyomo/solvers/tests/models/LP_duals_maximize.py index 61d827daa62..ed45e4eee29 100644 --- a/pyomo/solvers/tests/models/LP_duals_maximize.py +++ b/pyomo/solvers/tests/models/LP_duals_maximize.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/LP_duals_minimize.py b/pyomo/solvers/tests/models/LP_duals_minimize.py index 77471d0182c..3f97276a61e 100644 --- a/pyomo/solvers/tests/models/LP_duals_minimize.py +++ b/pyomo/solvers/tests/models/LP_duals_minimize.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/LP_inactive_index.py b/pyomo/solvers/tests/models/LP_inactive_index.py index d3fdd5b32ca..5e2b570a1e8 100644 --- a/pyomo/solvers/tests/models/LP_inactive_index.py +++ b/pyomo/solvers/tests/models/LP_inactive_index.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/LP_infeasible1.py b/pyomo/solvers/tests/models/LP_infeasible1.py index 28243574a37..8cba441a6c3 100644 --- a/pyomo/solvers/tests/models/LP_infeasible1.py +++ b/pyomo/solvers/tests/models/LP_infeasible1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/LP_infeasible2.py b/pyomo/solvers/tests/models/LP_infeasible2.py index 383267c0e3c..7f417d9145c 100644 --- a/pyomo/solvers/tests/models/LP_infeasible2.py +++ b/pyomo/solvers/tests/models/LP_infeasible2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/LP_piecewise.py b/pyomo/solvers/tests/models/LP_piecewise.py index f6350b38591..22ee9d08694 100644 --- a/pyomo/solvers/tests/models/LP_piecewise.py +++ b/pyomo/solvers/tests/models/LP_piecewise.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/LP_simple.py b/pyomo/solvers/tests/models/LP_simple.py index 3449a657f79..4f1e6dcbc7e 100644 --- a/pyomo/solvers/tests/models/LP_simple.py +++ b/pyomo/solvers/tests/models/LP_simple.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/LP_trivial_constraints.py b/pyomo/solvers/tests/models/LP_trivial_constraints.py index 096c9e71712..3958f2b4493 100644 --- a/pyomo/solvers/tests/models/LP_trivial_constraints.py +++ b/pyomo/solvers/tests/models/LP_trivial_constraints.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/LP_unbounded.py b/pyomo/solvers/tests/models/LP_unbounded.py index e3173e2ff07..e75977c40ba 100644 --- a/pyomo/solvers/tests/models/LP_unbounded.py +++ b/pyomo/solvers/tests/models/LP_unbounded.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/LP_unique_duals.py b/pyomo/solvers/tests/models/LP_unique_duals.py index 624181eb27d..f5a4df6338d 100644 --- a/pyomo/solvers/tests/models/LP_unique_duals.py +++ b/pyomo/solvers/tests/models/LP_unique_duals.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/LP_unused_vars.py b/pyomo/solvers/tests/models/LP_unused_vars.py index 5e6b40fa4bf..0062fc58463 100644 --- a/pyomo/solvers/tests/models/LP_unused_vars.py +++ b/pyomo/solvers/tests/models/LP_unused_vars.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/MILP_discrete_var_bounds.py b/pyomo/solvers/tests/models/MILP_discrete_var_bounds.py index 8fef69ef76a..22876a7a291 100644 --- a/pyomo/solvers/tests/models/MILP_discrete_var_bounds.py +++ b/pyomo/solvers/tests/models/MILP_discrete_var_bounds.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/MILP_infeasible1.py b/pyomo/solvers/tests/models/MILP_infeasible1.py index 2a0bf1bd188..e95fef92744 100644 --- a/pyomo/solvers/tests/models/MILP_infeasible1.py +++ b/pyomo/solvers/tests/models/MILP_infeasible1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/MILP_simple.py b/pyomo/solvers/tests/models/MILP_simple.py index fb157ea6555..488c7841024 100644 --- a/pyomo/solvers/tests/models/MILP_simple.py +++ b/pyomo/solvers/tests/models/MILP_simple.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/MILP_unbounded.py b/pyomo/solvers/tests/models/MILP_unbounded.py index 364f3ffeb86..c5a166a6141 100644 --- a/pyomo/solvers/tests/models/MILP_unbounded.py +++ b/pyomo/solvers/tests/models/MILP_unbounded.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/MILP_unused_vars.py b/pyomo/solvers/tests/models/MILP_unused_vars.py index 742d0f951a8..b6e06c8db0c 100644 --- a/pyomo/solvers/tests/models/MILP_unused_vars.py +++ b/pyomo/solvers/tests/models/MILP_unused_vars.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/MIQCP_simple.py b/pyomo/solvers/tests/models/MIQCP_simple.py index 46c1293b23c..5946e83fadb 100644 --- a/pyomo/solvers/tests/models/MIQCP_simple.py +++ b/pyomo/solvers/tests/models/MIQCP_simple.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/MIQP_simple.py b/pyomo/solvers/tests/models/MIQP_simple.py index 1d43d96ab8b..6922d6be97d 100644 --- a/pyomo/solvers/tests/models/MIQP_simple.py +++ b/pyomo/solvers/tests/models/MIQP_simple.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/QCP_simple.py b/pyomo/solvers/tests/models/QCP_simple.py index 5f8405f1f00..5f4311a3ab9 100644 --- a/pyomo/solvers/tests/models/QCP_simple.py +++ b/pyomo/solvers/tests/models/QCP_simple.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/QP_constant_objective.py b/pyomo/solvers/tests/models/QP_constant_objective.py index 2769fe07556..6ea34b69f51 100644 --- a/pyomo/solvers/tests/models/QP_constant_objective.py +++ b/pyomo/solvers/tests/models/QP_constant_objective.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/QP_simple.py b/pyomo/solvers/tests/models/QP_simple.py index 5959cf1d8b1..c5f4f40c576 100644 --- a/pyomo/solvers/tests/models/QP_simple.py +++ b/pyomo/solvers/tests/models/QP_simple.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/SOS1_simple.py b/pyomo/solvers/tests/models/SOS1_simple.py index e6156ad5c32..ba3c89e680b 100644 --- a/pyomo/solvers/tests/models/SOS1_simple.py +++ b/pyomo/solvers/tests/models/SOS1_simple.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/SOS2_simple.py b/pyomo/solvers/tests/models/SOS2_simple.py index 4f192773ca4..2062611f8cf 100644 --- a/pyomo/solvers/tests/models/SOS2_simple.py +++ b/pyomo/solvers/tests/models/SOS2_simple.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/__init__.py b/pyomo/solvers/tests/models/__init__.py index c6a550397d5..46a1c96936d 100644 --- a/pyomo/solvers/tests/models/__init__.py +++ b/pyomo/solvers/tests/models/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/base.py b/pyomo/solvers/tests/models/base.py index 106e8860145..25442611806 100644 --- a/pyomo/solvers/tests/models/base.py +++ b/pyomo/solvers/tests/models/base.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/__init__.py b/pyomo/solvers/tests/piecewise_linear/__init__.py index bcaa157f6f4..79b33f0d427 100644 --- a/pyomo/solvers/tests/piecewise_linear/__init__.py +++ b/pyomo/solvers/tests/piecewise_linear/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/indexed.lp b/pyomo/solvers/tests/piecewise_linear/indexed.lp index 32e9a0161fc..e73fb8331bd 100644 --- a/pyomo/solvers/tests/piecewise_linear/indexed.lp +++ b/pyomo/solvers/tests/piecewise_linear/indexed.lp @@ -25,11 +25,11 @@ c_e_linearized_constraint(0_1)_LOG_constraint2_: +0.49663531783502585 linearized_constraint(0_1)_LOG_lambda(2) +0.3836621854632263 linearized_constraint(0_1)_LOG_lambda(3) -0.7511436155469337 linearized_constraint(0_1)_LOG_lambda(4) ++1.0 linearized_constraint(0_1)_LOG_lambda(5) -0.8511436155469337 linearized_constraint(0_1)_LOG_lambda(6) +0.18366218546322624 linearized_constraint(0_1)_LOG_lambda(7) +0.1966353178350258 linearized_constraint(0_1)_LOG_lambda(8) -1.0390715290764525 linearized_constraint(0_1)_LOG_lambda(9) -+1.0 linearized_constraint(0_1)_LOG_lambda(5) = 0 c_e_linearized_constraint(0_1)_LOG_constraint3_: @@ -37,11 +37,11 @@ c_e_linearized_constraint(0_1)_LOG_constraint3_: +1 linearized_constraint(0_1)_LOG_lambda(2) +1 linearized_constraint(0_1)_LOG_lambda(3) +1 linearized_constraint(0_1)_LOG_lambda(4) ++1 linearized_constraint(0_1)_LOG_lambda(5) +1 linearized_constraint(0_1)_LOG_lambda(6) +1 linearized_constraint(0_1)_LOG_lambda(7) +1 linearized_constraint(0_1)_LOG_lambda(8) +1 linearized_constraint(0_1)_LOG_lambda(9) -+1 linearized_constraint(0_1)_LOG_lambda(5) = 1 c_u_linearized_constraint(0_1)_LOG_constraint4(1)_: @@ -54,8 +54,8 @@ c_u_linearized_constraint(0_1)_LOG_constraint4(1)_: c_u_linearized_constraint(0_1)_LOG_constraint4(2)_: +1 linearized_constraint(0_1)_LOG_lambda(4) -+1 linearized_constraint(0_1)_LOG_lambda(6) +1 linearized_constraint(0_1)_LOG_lambda(5) ++1 linearized_constraint(0_1)_LOG_lambda(6) -1 linearized_constraint(0_1)_LOG_bin_y(2) <= 0 @@ -83,8 +83,8 @@ c_u_linearized_constraint(0_1)_LOG_constraint5(2)_: c_u_linearized_constraint(0_1)_LOG_constraint5(3)_: +1 linearized_constraint(0_1)_LOG_lambda(1) -+1 linearized_constraint(0_1)_LOG_lambda(9) +1 linearized_constraint(0_1)_LOG_lambda(5) ++1 linearized_constraint(0_1)_LOG_lambda(9) +1 linearized_constraint(0_1)_LOG_bin_y(3) <= 1 @@ -106,11 +106,11 @@ c_e_linearized_constraint(8_3)_LOG_constraint2_: +0.49663531783502585 linearized_constraint(8_3)_LOG_lambda(2) +0.3836621854632263 linearized_constraint(8_3)_LOG_lambda(3) -0.7511436155469337 linearized_constraint(8_3)_LOG_lambda(4) ++1.0 linearized_constraint(8_3)_LOG_lambda(5) -0.8511436155469337 linearized_constraint(8_3)_LOG_lambda(6) +0.18366218546322624 linearized_constraint(8_3)_LOG_lambda(7) +0.1966353178350258 linearized_constraint(8_3)_LOG_lambda(8) -1.0390715290764525 linearized_constraint(8_3)_LOG_lambda(9) -+1.0 linearized_constraint(8_3)_LOG_lambda(5) = 0 c_e_linearized_constraint(8_3)_LOG_constraint3_: @@ -118,11 +118,11 @@ c_e_linearized_constraint(8_3)_LOG_constraint3_: +1 linearized_constraint(8_3)_LOG_lambda(2) +1 linearized_constraint(8_3)_LOG_lambda(3) +1 linearized_constraint(8_3)_LOG_lambda(4) ++1 linearized_constraint(8_3)_LOG_lambda(5) +1 linearized_constraint(8_3)_LOG_lambda(6) +1 linearized_constraint(8_3)_LOG_lambda(7) +1 linearized_constraint(8_3)_LOG_lambda(8) +1 linearized_constraint(8_3)_LOG_lambda(9) -+1 linearized_constraint(8_3)_LOG_lambda(5) = 1 c_u_linearized_constraint(8_3)_LOG_constraint4(1)_: @@ -135,8 +135,8 @@ c_u_linearized_constraint(8_3)_LOG_constraint4(1)_: c_u_linearized_constraint(8_3)_LOG_constraint4(2)_: +1 linearized_constraint(8_3)_LOG_lambda(4) -+1 linearized_constraint(8_3)_LOG_lambda(6) +1 linearized_constraint(8_3)_LOG_lambda(5) ++1 linearized_constraint(8_3)_LOG_lambda(6) -1 linearized_constraint(8_3)_LOG_bin_y(2) <= 0 @@ -164,8 +164,8 @@ c_u_linearized_constraint(8_3)_LOG_constraint5(2)_: c_u_linearized_constraint(8_3)_LOG_constraint5(3)_: +1 linearized_constraint(8_3)_LOG_lambda(1) -+1 linearized_constraint(8_3)_LOG_lambda(9) +1 linearized_constraint(8_3)_LOG_lambda(5) ++1 linearized_constraint(8_3)_LOG_lambda(9) +1 linearized_constraint(8_3)_LOG_bin_y(3) <= 1 @@ -173,28 +173,28 @@ bounds -inf <= Z(0_1) <= +inf -inf <= Z(8_3) <= +inf -2 <= X(0_1) <= 2 + -2 <= X(8_3) <= 2 0 <= linearized_constraint(0_1)_LOG_lambda(1) <= +inf 0 <= linearized_constraint(0_1)_LOG_lambda(2) <= +inf 0 <= linearized_constraint(0_1)_LOG_lambda(3) <= +inf 0 <= linearized_constraint(0_1)_LOG_lambda(4) <= +inf + 0 <= linearized_constraint(0_1)_LOG_lambda(5) <= +inf 0 <= linearized_constraint(0_1)_LOG_lambda(6) <= +inf 0 <= linearized_constraint(0_1)_LOG_lambda(7) <= +inf 0 <= linearized_constraint(0_1)_LOG_lambda(8) <= +inf 0 <= linearized_constraint(0_1)_LOG_lambda(9) <= +inf - 0 <= linearized_constraint(0_1)_LOG_lambda(5) <= +inf 0 <= linearized_constraint(0_1)_LOG_bin_y(1) <= 1 0 <= linearized_constraint(0_1)_LOG_bin_y(2) <= 1 0 <= linearized_constraint(0_1)_LOG_bin_y(3) <= 1 - -2 <= X(8_3) <= 2 0 <= linearized_constraint(8_3)_LOG_lambda(1) <= +inf 0 <= linearized_constraint(8_3)_LOG_lambda(2) <= +inf 0 <= linearized_constraint(8_3)_LOG_lambda(3) <= +inf 0 <= linearized_constraint(8_3)_LOG_lambda(4) <= +inf + 0 <= linearized_constraint(8_3)_LOG_lambda(5) <= +inf 0 <= linearized_constraint(8_3)_LOG_lambda(6) <= +inf 0 <= linearized_constraint(8_3)_LOG_lambda(7) <= +inf 0 <= linearized_constraint(8_3)_LOG_lambda(8) <= +inf 0 <= linearized_constraint(8_3)_LOG_lambda(9) <= +inf - 0 <= linearized_constraint(8_3)_LOG_lambda(5) <= +inf 0 <= linearized_constraint(8_3)_LOG_bin_y(1) <= 1 0 <= linearized_constraint(8_3)_LOG_bin_y(2) <= 1 0 <= linearized_constraint(8_3)_LOG_bin_y(3) <= 1 diff --git a/pyomo/solvers/tests/piecewise_linear/kernel_problems/concave_var.py b/pyomo/solvers/tests/piecewise_linear/kernel_problems/concave_var.py index 38c840f9ed9..45270d7dc34 100644 --- a/pyomo/solvers/tests/piecewise_linear/kernel_problems/concave_var.py +++ b/pyomo/solvers/tests/piecewise_linear/kernel_problems/concave_var.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/kernel_problems/convex_var.py b/pyomo/solvers/tests/piecewise_linear/kernel_problems/convex_var.py index 3aef735965e..cf28dc044eb 100644 --- a/pyomo/solvers/tests/piecewise_linear/kernel_problems/convex_var.py +++ b/pyomo/solvers/tests/piecewise_linear/kernel_problems/convex_var.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/kernel_problems/piecewise_var.py b/pyomo/solvers/tests/piecewise_linear/kernel_problems/piecewise_var.py index b77566e9d2d..cadbff305e8 100644 --- a/pyomo/solvers/tests/piecewise_linear/kernel_problems/piecewise_var.py +++ b/pyomo/solvers/tests/piecewise_linear/kernel_problems/piecewise_var.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/kernel_problems/step_var.py b/pyomo/solvers/tests/piecewise_linear/kernel_problems/step_var.py index 642181deb7d..1e6e418acf0 100644 --- a/pyomo/solvers/tests/piecewise_linear/kernel_problems/step_var.py +++ b/pyomo/solvers/tests/piecewise_linear/kernel_problems/step_var.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/problems/concave_multi_vararray1.py b/pyomo/solvers/tests/piecewise_linear/problems/concave_multi_vararray1.py index b24f7e1bd72..473b3328660 100644 --- a/pyomo/solvers/tests/piecewise_linear/problems/concave_multi_vararray1.py +++ b/pyomo/solvers/tests/piecewise_linear/problems/concave_multi_vararray1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/problems/concave_multi_vararray2.py b/pyomo/solvers/tests/piecewise_linear/problems/concave_multi_vararray2.py index 24c8beeba34..e6b57a4b652 100644 --- a/pyomo/solvers/tests/piecewise_linear/problems/concave_multi_vararray2.py +++ b/pyomo/solvers/tests/piecewise_linear/problems/concave_multi_vararray2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/problems/concave_var.py b/pyomo/solvers/tests/piecewise_linear/problems/concave_var.py index 4eedf7bdeb9..b225cee4f87 100644 --- a/pyomo/solvers/tests/piecewise_linear/problems/concave_var.py +++ b/pyomo/solvers/tests/piecewise_linear/problems/concave_var.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/problems/concave_vararray.py b/pyomo/solvers/tests/piecewise_linear/problems/concave_vararray.py index be013b62309..727fc33ed80 100644 --- a/pyomo/solvers/tests/piecewise_linear/problems/concave_vararray.py +++ b/pyomo/solvers/tests/piecewise_linear/problems/concave_vararray.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/problems/convex_multi_vararray1.py b/pyomo/solvers/tests/piecewise_linear/problems/convex_multi_vararray1.py index 8d00a99d49d..98f369b8c45 100644 --- a/pyomo/solvers/tests/piecewise_linear/problems/convex_multi_vararray1.py +++ b/pyomo/solvers/tests/piecewise_linear/problems/convex_multi_vararray1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/problems/convex_multi_vararray2.py b/pyomo/solvers/tests/piecewise_linear/problems/convex_multi_vararray2.py index 2892b759a65..c877bb6b72b 100644 --- a/pyomo/solvers/tests/piecewise_linear/problems/convex_multi_vararray2.py +++ b/pyomo/solvers/tests/piecewise_linear/problems/convex_multi_vararray2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/problems/convex_var.py b/pyomo/solvers/tests/piecewise_linear/problems/convex_var.py index bb4609be7c9..842ef50515b 100644 --- a/pyomo/solvers/tests/piecewise_linear/problems/convex_var.py +++ b/pyomo/solvers/tests/piecewise_linear/problems/convex_var.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/problems/convex_vararray.py b/pyomo/solvers/tests/piecewise_linear/problems/convex_vararray.py index 140d69dcb1a..087d0977ee0 100644 --- a/pyomo/solvers/tests/piecewise_linear/problems/convex_vararray.py +++ b/pyomo/solvers/tests/piecewise_linear/problems/convex_vararray.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/problems/piecewise_multi_vararray.py b/pyomo/solvers/tests/piecewise_linear/problems/piecewise_multi_vararray.py index 3c587d694e1..56452e0cd19 100644 --- a/pyomo/solvers/tests/piecewise_linear/problems/piecewise_multi_vararray.py +++ b/pyomo/solvers/tests/piecewise_linear/problems/piecewise_multi_vararray.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/problems/piecewise_var.py b/pyomo/solvers/tests/piecewise_linear/problems/piecewise_var.py index 5b18842f81d..60c45a69e80 100644 --- a/pyomo/solvers/tests/piecewise_linear/problems/piecewise_var.py +++ b/pyomo/solvers/tests/piecewise_linear/problems/piecewise_var.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/problems/piecewise_vararray.py b/pyomo/solvers/tests/piecewise_linear/problems/piecewise_vararray.py index d35c308e172..9e53edb0c93 100644 --- a/pyomo/solvers/tests/piecewise_linear/problems/piecewise_vararray.py +++ b/pyomo/solvers/tests/piecewise_linear/problems/piecewise_vararray.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/problems/step_var.py b/pyomo/solvers/tests/piecewise_linear/problems/step_var.py index a0c1062c9d6..59cefdd39c9 100644 --- a/pyomo/solvers/tests/piecewise_linear/problems/step_var.py +++ b/pyomo/solvers/tests/piecewise_linear/problems/step_var.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/problems/step_vararray.py b/pyomo/solvers/tests/piecewise_linear/problems/step_vararray.py index 749df3b6d7f..e4853e666d6 100644 --- a/pyomo/solvers/tests/piecewise_linear/problems/step_vararray.py +++ b/pyomo/solvers/tests/piecewise_linear/problems/step_vararray.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/problems/tester.py b/pyomo/solvers/tests/piecewise_linear/problems/tester.py index 02e04f5052e..56261f7cc38 100644 --- a/pyomo/solvers/tests/piecewise_linear/problems/tester.py +++ b/pyomo/solvers/tests/piecewise_linear/problems/tester.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/step.lp b/pyomo/solvers/tests/piecewise_linear/step.lp index 7ecd9e7e34e..68574f9658e 100644 --- a/pyomo/solvers/tests/piecewise_linear/step.lp +++ b/pyomo/solvers/tests/piecewise_linear/step.lp @@ -64,10 +64,10 @@ bounds -inf <= Z <= +inf 0 <= X <= 3 -inf <= con_INC_delta(1) <= 1 - -inf <= con_INC_delta(3) <= +inf - 0 <= con_INC_delta(5) <= +inf -inf <= con_INC_delta(2) <= +inf + -inf <= con_INC_delta(3) <= +inf -inf <= con_INC_delta(4) <= +inf + 0 <= con_INC_delta(5) <= +inf 0 <= con_INC_bin_y(1) <= 1 0 <= con_INC_bin_y(2) <= 1 0 <= con_INC_bin_y(3) <= 1 diff --git a/pyomo/solvers/tests/piecewise_linear/test_examples.py b/pyomo/solvers/tests/piecewise_linear/test_examples.py index b151ffd2c0e..3454f62d56b 100644 --- a/pyomo/solvers/tests/piecewise_linear/test_examples.py +++ b/pyomo/solvers/tests/piecewise_linear/test_examples.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/test_piecewise_linear.py b/pyomo/solvers/tests/piecewise_linear/test_piecewise_linear.py index adf1a000fb4..48472c2dabf 100644 --- a/pyomo/solvers/tests/piecewise_linear/test_piecewise_linear.py +++ b/pyomo/solvers/tests/piecewise_linear/test_piecewise_linear.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -22,7 +22,7 @@ from pyomo.core.base import Var from pyomo.core.base.objective import minimize, maximize from pyomo.core.base.piecewise import Bound, PWRepn -from pyomo.solvers.tests.solvers import test_solver_cases +from pyomo.solvers.tests.solvers import test_solver_cases as _test_solver_cases smoke_problems = ['convex_var', 'step_var', 'step_vararray'] @@ -50,8 +50,8 @@ # testing_solvers['ipopt','nl'] = False # testing_solvers['cplex','python'] = False # testing_solvers['_cplex_persistent','python'] = False -for _solver, _io in test_solver_cases(): - if (_solver, _io) in testing_solvers and test_solver_cases(_solver, _io).available: +for _solver, _io in _test_solver_cases(): + if (_solver, _io) in testing_solvers and _test_solver_cases(_solver, _io).available: testing_solvers[_solver, _io] = True diff --git a/pyomo/solvers/tests/piecewise_linear/test_piecewise_linear_kernel.py b/pyomo/solvers/tests/piecewise_linear/test_piecewise_linear_kernel.py index 516ee25ffa3..20addb2b1eb 100644 --- a/pyomo/solvers/tests/piecewise_linear/test_piecewise_linear_kernel.py +++ b/pyomo/solvers/tests/piecewise_linear/test_piecewise_linear_kernel.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -19,7 +19,7 @@ from pyomo.common.fileutils import import_file from pyomo.kernel import SolverFactory, variable, maximize, minimize -from pyomo.solvers.tests.solvers import test_solver_cases +from pyomo.solvers.tests.solvers import test_solver_cases as _test_solver_cases problems = ['convex_var', 'concave_var', 'piecewise_var', 'step_var'] @@ -30,8 +30,8 @@ # testing_solvers['ipopt','nl'] = False # testing_solvers['cplex','python'] = False # testing_solvers['_cplex_persistent','python'] = False -for _solver, _io in test_solver_cases(): - if (_solver, _io) in testing_solvers and test_solver_cases(_solver, _io).available: +for _solver, _io in _test_solver_cases(): + if (_solver, _io) in testing_solvers and _test_solver_cases(_solver, _io).available: testing_solvers[_solver, _io] = True diff --git a/pyomo/solvers/tests/solvers.py b/pyomo/solvers/tests/solvers.py index 6bbfe08c7c7..918a801ae37 100644 --- a/pyomo/solvers/tests/solvers.py +++ b/pyomo/solvers/tests/solvers.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['test_solver_cases'] - import logging from pyomo.common.collections import Bunch diff --git a/pyomo/solvers/tests/testcases.py b/pyomo/solvers/tests/testcases.py index eaebbcd9003..6bef40818d9 100644 --- a/pyomo/solvers/tests/testcases.py +++ b/pyomo/solvers/tests/testcases.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -15,7 +15,7 @@ from pyomo.common.collections import Bunch from pyomo.opt import TerminationCondition from pyomo.solvers.tests.models.base import all_models -from pyomo.solvers.tests.solvers import test_solver_cases +from pyomo.solvers.tests.solvers import test_solver_cases as _test_solver_cases from pyomo.core.kernel.block import IBlock # For expected failures that appear in all known version @@ -297,8 +297,8 @@ def generate_scenarios(arg=None): _model = all_models(model) if not arg is None and not arg(_model): continue - for solver, io in sorted(test_solver_cases()): - _solver_case = test_solver_cases(solver, io) + for solver, io in sorted(_test_solver_cases()): + _solver_case = _test_solver_cases(solver, io) _ver = _solver_case.version # Skip this test case if the solver doesn't support the diff --git a/pyomo/solvers/wrappers.py b/pyomo/solvers/wrappers.py index 3b083f7a14f..ee167ce1cb0 100644 --- a/pyomo/solvers/wrappers.py +++ b/pyomo/solvers/wrappers.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/__init__.py b/pyomo/util/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/util/__init__.py +++ b/pyomo/util/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/util/blockutil.py b/pyomo/util/blockutil.py index 52befea6ed5..9f043e64ab7 100644 --- a/pyomo/util/blockutil.py +++ b/pyomo/util/blockutil.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -12,8 +12,6 @@ # the purpose of this file is to collect all utility methods that compute # attributes of blocks, based on their contents. -__all__ = ['has_discrete_variables'] - import logging from pyomo.core import Var, Constraint, TraversalStrategy diff --git a/pyomo/util/calc_var_value.py b/pyomo/util/calc_var_value.py index 81bbd285dd2..42ee3119361 100644 --- a/pyomo/util/calc_var_value.py +++ b/pyomo/util/calc_var_value.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -10,9 +10,9 @@ # ___________________________________________________________________________ from pyomo.common.errors import IterationLimitError -from pyomo.core.expr.numvalue import native_numeric_types, value, is_fixed +from pyomo.common.numeric_types import native_numeric_types, native_complex_types, value from pyomo.core.expr.calculus.derivatives import differentiate -from pyomo.core.base.constraint import Constraint, _ConstraintData +from pyomo.core.base.constraint import Constraint import logging @@ -53,9 +53,9 @@ def calculate_variable_from_constraint( Parameters: ----------- - variable: :py:class:`_VarData` + variable: :py:class:`VarData` The variable to solve for - constraint: :py:class:`_ConstraintData` or relational expression or `tuple` + constraint: :py:class:`ConstraintData` or relational expression or `tuple` The equality constraint to use to solve for the variable value. May be a `ConstraintData` object or any valid argument for ``Constraint(expr=<>)`` (i.e., a relational expression or 2- or @@ -81,10 +81,17 @@ def calculate_variable_from_constraint( """ # Leverage all the Constraint logic to process the incoming tuple/expression - if not isinstance(constraint, _ConstraintData): + if not getattr(constraint, 'ctype', None) is Constraint: constraint = Constraint(expr=constraint, name=type(constraint).__name__) constraint.construct() + if constraint.is_indexed(): + raise ValueError( + 'calculate_variable_from_constraint(): constraint must be a ' + 'scalar constraint or a single ConstraintData. Received ' + f'{constraint.__class__.__name__} ("{constraint.name}")' + ) + body = constraint.body lower = constraint.lb upper = constraint.ub @@ -92,6 +99,9 @@ def calculate_variable_from_constraint( if lower != upper: raise ValueError(f"Constraint '{constraint}' must be an equality constraint") + _invalid_types = set(native_complex_types) + _invalid_types.add(type(None)) + if variable.value is None: # Note that we use "skip_validation=True" here as well, as the # variable domain may not admit the calculated initial guesses, @@ -151,7 +161,7 @@ def calculate_variable_from_constraint( # to using Newton's method. residual_2 = None - if residual_2 is not None and type(residual_2) is not complex: + if residual_2.__class__ not in _invalid_types: # if the variable appears linearly with a coefficient of 1, then we # are done if abs(residual_2 - upper) < eps: @@ -167,11 +177,7 @@ def calculate_variable_from_constraint( if slope: variable.set_value(-intercept / slope, skip_validation=True) body_val = value(body, exception=False) - if ( - body_val is not None - and body_val.__class__ is not complex - and abs(body_val - upper) < eps - ): + if body_val.__class__ not in _invalid_types and abs(body_val - upper) < eps: # Re-set the variable value to trigger any warnings WRT # the final variable state variable.set_value(variable.value) @@ -234,7 +240,7 @@ def calculate_variable_from_constraint( xk = value(variable) try: fk = value(expr) - if type(fk) is complex: + if fk.__class__ in _invalid_types and fk is not None: raise ValueError("Complex numbers are not allowed in Newton's method.") except: # We hit numerical problems with the last step (possible if @@ -275,7 +281,7 @@ def calculate_variable_from_constraint( # HACK for Python3 support, pending resolution of #879 # Issue #879 also pertains to other checks for "complex" # in this method. - if type(fkp1) is complex: + if fkp1.__class__ in _invalid_types: # We cannot perform computations on complex numbers fkp1 = None if fkp1 is not None and fkp1**2 < c1 * fk**2: @@ -289,7 +295,7 @@ def calculate_variable_from_constraint( if alpha <= alpha_min: residual = value(expr, exception=False) - if residual is None or type(residual) is complex: + if residual.__class__ in _invalid_types: residual = "{function evaluation error}" raise IterationLimitError( f"Linesearch iteration limit reached solving for " diff --git a/pyomo/util/check_units.py b/pyomo/util/check_units.py index be72493af3f..6f95486c8cd 100644 --- a/pyomo/util/check_units.py +++ b/pyomo/util/check_units.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/components.py b/pyomo/util/components.py index 02ef8a30f64..2f1d85a4934 100644 --- a/pyomo/util/components.py +++ b/pyomo/util/components.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/diagnostics.py b/pyomo/util/diagnostics.py index d4b7974b9da..709a483f2ff 100644 --- a/pyomo/util/diagnostics.py +++ b/pyomo/util/diagnostics.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: UTF-8 -*- """Module with miscellaneous diagnostic tools""" from pyomo.core.base.block import TraversalStrategy, Block diff --git a/pyomo/util/infeasible.py b/pyomo/util/infeasible.py index 9c8196d1ff4..961d5b35036 100644 --- a/pyomo/util/infeasible.py +++ b/pyomo/util/infeasible.py @@ -2,7 +2,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/model_size.py b/pyomo/util/model_size.py index 9575e327a74..1fdac357368 100644 --- a/pyomo/util/model_size.py +++ b/pyomo/util/model_size.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/report_scaling.py b/pyomo/util/report_scaling.py index 5b4a4df7c84..02b3710c334 100644 --- a/pyomo/util/report_scaling.py +++ b/pyomo/util/report_scaling.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -11,9 +11,9 @@ import pyomo.environ as pyo import math -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.common.collections import ComponentSet -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import Var from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd import logging @@ -42,7 +42,7 @@ def _print_var_set(var_set): return s -def _check_var_bounds(m: _BlockData, too_large: float): +def _check_var_bounds(m: BlockData, too_large: float): vars_without_bounds = ComponentSet() vars_with_large_bounds = ComponentSet() for v in m.component_data_objects(pyo.Var, descend_into=True): @@ -73,7 +73,7 @@ def _check_coefficients( ): ders = reverse_sd(expr) for _v, _der in ders.items(): - if isinstance(_v, _GeneralVarData): + if getattr(_v, 'ctype', None) is Var: if _v.is_fixed(): continue der_lb, der_ub = compute_bounds_on_expr(_der) @@ -90,7 +90,7 @@ def _check_coefficients( def report_scaling( - m: _BlockData, too_large: float = 5e4, too_small: float = 1e-6 + m: BlockData, too_large: float = 5e4, too_small: float = 1e-6 ) -> bool: """ This function logs potentially poorly scaled parts of the model. @@ -107,7 +107,7 @@ def report_scaling( Parameters ---------- - m: _BlockData + m: BlockData The pyomo model or block too_large: float Values above too_large will generate a log entry diff --git a/pyomo/util/slices.py b/pyomo/util/slices.py index 0449acb3f2f..d85aa3fa926 100644 --- a/pyomo/util/slices.py +++ b/pyomo/util/slices.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -98,7 +98,7 @@ def slice_component_along_sets(comp, sets, context=None): sets: `pyomo.common.collections.ComponentSet` Contains the sets to replace with slices context: `pyomo.core.base.block.Block` or - `pyomo.core.base.block._BlockData` + `pyomo.core.base.block.BlockData` Block below which to search for sets Returns: diff --git a/pyomo/util/subsystems.py b/pyomo/util/subsystems.py index 673781def17..00c3b85ce47 100644 --- a/pyomo/util/subsystems.py +++ b/pyomo/util/subsystems.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -14,21 +14,38 @@ from pyomo.core.expr.visitor import identify_variables from pyomo.common.collections import ComponentSet, ComponentMap from pyomo.common.modeling import unique_component_name - +from pyomo.util.vars_from_expressions import get_vars_from_components from pyomo.core.base.constraint import Constraint from pyomo.core.base.expression import Expression +from pyomo.core.base.objective import Objective from pyomo.core.base.external import ExternalFunction from pyomo.core.expr.visitor import StreamBasedExpressionVisitor from pyomo.core.expr.numeric_expr import ExternalFunctionExpression -from pyomo.core.expr.numvalue import native_types +from pyomo.core.expr.numvalue import native_types, NumericValue class _ExternalFunctionVisitor(StreamBasedExpressionVisitor): + def __init__(self, descend_into_named_expressions=True): + super().__init__() + self._descend_into_named_expressions = descend_into_named_expressions + self.named_expressions = [] + def initializeWalker(self, expr): self._functions = [] self._seen = set() return True, None + def beforeChild(self, parent, child, index): + if child.__class__ in native_types: + return False, None + elif ( + not self._descend_into_named_expressions + and child.is_named_expression_type() + ): + self.named_expressions.append(child) + return False, None + return True, None + def exitNode(self, node, data): if type(node) is ExternalFunctionExpression: if id(node) not in self._seen: @@ -38,17 +55,6 @@ def exitNode(self, node, data): def finalizeResult(self, result): return self._functions - def enterNode(self, node): - pass - - def acceptChildResult(self, node, data, child_result, child_idx): - pass - - def acceptChildResult(self, node, data, child_result, child_idx): - if child_result.__class__ in native_types: - return False, None - return child_result.is_expression_type(), None - def identify_external_functions(expr): yield from _ExternalFunctionVisitor().walk_expression(expr) @@ -56,8 +62,28 @@ def identify_external_functions(expr): def add_local_external_functions(block): ef_exprs = [] - for comp in block.component_data_objects((Constraint, Expression), active=True): - ef_exprs.extend(identify_external_functions(comp.expr)) + named_expressions = [] + visitor = _ExternalFunctionVisitor(descend_into_named_expressions=False) + for comp in block.component_data_objects( + (Constraint, Expression, Objective), active=True + ): + ef_exprs.extend(visitor.walk_expression(comp.expr)) + named_expr_set = ComponentSet(visitor.named_expressions) + # List of unique named expressions + named_expressions = list(named_expr_set) + while named_expressions: + expr = named_expressions.pop() + # Clear named expression cache so we don't re-check named expressions + # we've seen before. + visitor.named_expressions.clear() + ef_exprs.extend(visitor.walk_expression(expr)) + # Only add to the stack named expressions that we have + # not encountered yet. + for local_expr in visitor.named_expressions: + if local_expr not in named_expr_set: + named_expressions.append(local_expr) + named_expr_set.add(local_expr) + unique_functions = [] fcn_set = set() for expr in ef_exprs: @@ -106,11 +132,9 @@ def create_subsystem_block(constraints, variables=None, include_fixed=False): block.cons = Reference(constraints) var_set = ComponentSet(variables) input_vars = [] - for con in constraints: - for var in identify_variables(con.expr, include_fixed=include_fixed): - if var not in var_set: - input_vars.append(var) - var_set.add(var) + for var in get_vars_from_components(block, Constraint, include_fixed=include_fixed): + if var not in var_set: + input_vars.append(var) block.input_vars = Reference(input_vars) add_local_external_functions(block) return block @@ -148,7 +172,14 @@ class TemporarySubsystemManager(object): """ - def __init__(self, to_fix=None, to_deactivate=None, to_reset=None, to_unfix=None): + def __init__( + self, + to_fix=None, + to_deactivate=None, + to_reset=None, + to_unfix=None, + remove_bounds_on_fix=False, + ): """ Arguments --------- @@ -168,6 +199,8 @@ def __init__(self, to_fix=None, to_deactivate=None, to_reset=None, to_unfix=None List of var data objects to be temporarily unfixed. These are restored to their original status on exit from this object's context manager. + remove_bounds_on_fix: Bool + Whether bounds should be removed temporarily for fixed variables """ if to_fix is None: @@ -194,6 +227,8 @@ def __init__(self, to_fix=None, to_deactivate=None, to_reset=None, to_unfix=None self._con_was_active = None self._comp_original_value = None self._var_was_unfixed = None + self._remove_bounds_on_fix = remove_bounds_on_fix + self._fixed_var_bounds = None def __enter__(self): to_fix = self._vars_to_fix @@ -203,8 +238,13 @@ def __enter__(self): self._var_was_fixed = [(var, var.fixed) for var in to_fix + to_unfix] self._con_was_active = [(con, con.active) for con in to_deactivate] self._comp_original_value = [(comp, comp.value) for comp in to_set] + self._fixed_var_bounds = [(var.lb, var.ub) for var in to_fix] for var in self._vars_to_fix: + if self._remove_bounds_on_fix: + # TODO: Potentially override var.domain as well? + var.setlb(None) + var.setub(None) var.fix() for con in self._cons_to_deactivate: @@ -223,6 +263,11 @@ def __exit__(self, ex_type, ex_val, ex_bt): var.fix() else: var.unfix() + if self._remove_bounds_on_fix: + for var, (lb, ub) in zip(self._vars_to_fix, self._fixed_var_bounds): + var.setlb(lb) + var.setub(ub) + for con, was_active in self._con_was_active: if was_active: con.activate() diff --git a/pyomo/util/tests/__init__.py b/pyomo/util/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/util/tests/__init__.py +++ b/pyomo/util/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/util/tests/test_blockutil.py b/pyomo/util/tests/test_blockutil.py index 06b75bd6b68..dfe4f482fb2 100644 --- a/pyomo/util/tests/test_blockutil.py +++ b/pyomo/util/tests/test_blockutil.py @@ -2,7 +2,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/tests/test_calc_var_value.py b/pyomo/util/tests/test_calc_var_value.py index 91f23dd5a5d..4bed4d5c843 100644 --- a/pyomo/util/tests/test_calc_var_value.py +++ b/pyomo/util/tests/test_calc_var_value.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -101,6 +101,15 @@ def test_initialize_value(self): ): calculate_variable_from_constraint(m.x, m.lt) + m.indexed = Constraint([1, 2], rule=lambda m, i: m.x <= i) + with self.assertRaisesRegex( + ValueError, + r"calculate_variable_from_constraint\(\): constraint must be a scalar " + r"constraint or a single ConstraintData. Received IndexedConstraint " + r'\("indexed"\)', + ): + calculate_variable_from_constraint(m.x, m.indexed) + def test_linear(self): m = ConcreteModel() m.x = Var() diff --git a/pyomo/util/tests/test_check_units.py b/pyomo/util/tests/test_check_units.py index d2fb35c4f3b..9cde8d8dbae 100644 --- a/pyomo/util/tests/test_check_units.py +++ b/pyomo/util/tests/test_check_units.py @@ -2,7 +2,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/tests/test_components.py b/pyomo/util/tests/test_components.py index 92eb7dd5ef1..1027815ca6b 100644 --- a/pyomo/util/tests/test_components.py +++ b/pyomo/util/tests/test_components.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/tests/test_infeasible.py b/pyomo/util/tests/test_infeasible.py index cefc129b41e..687a578e5c8 100644 --- a/pyomo/util/tests/test_infeasible.py +++ b/pyomo/util/tests/test_infeasible.py @@ -2,7 +2,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/tests/test_model_size.py b/pyomo/util/tests/test_model_size.py index 417ff7526e8..2380d272a24 100644 --- a/pyomo/util/tests/test_model_size.py +++ b/pyomo/util/tests/test_model_size.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/tests/test_report_scaling.py b/pyomo/util/tests/test_report_scaling.py index b010065d697..2eaed2d0ade 100644 --- a/pyomo/util/tests/test_report_scaling.py +++ b/pyomo/util/tests/test_report_scaling.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/tests/test_slices.py b/pyomo/util/tests/test_slices.py index db66a74b468..992bdc0a332 100644 --- a/pyomo/util/tests/test_slices.py +++ b/pyomo/util/tests/test_slices.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/tests/test_subsystems.py b/pyomo/util/tests/test_subsystems.py index a081b51cee9..089888bd6a9 100644 --- a/pyomo/util/tests/test_subsystems.py +++ b/pyomo/util/tests/test_subsystems.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -292,7 +292,7 @@ def test_generate_dont_fix_inputs_with_fixed_var(self): self.assertFalse(m.v3.fixed) self.assertTrue(m.v4.fixed) - def _make_model_with_external_functions(self): + def _make_model_with_external_functions(self, named_expressions=False): m = pyo.ConcreteModel() gsl = find_GSL() m.bessel = pyo.ExternalFunction(library=gsl, function="gsl_sf_bessel_J0") @@ -300,9 +300,21 @@ def _make_model_with_external_functions(self): m.v1 = pyo.Var(initialize=1.0) m.v2 = pyo.Var(initialize=2.0) m.v3 = pyo.Var(initialize=3.0) + if named_expressions: + m.subexpr = pyo.Expression(pyo.PositiveIntegers) + m.subexpr[1] = 2 * m.fermi(m.v1) + m.subexpr[2] = m.bessel(m.v1) - m.bessel(m.v2) + m.subexpr[3] = m.subexpr[2] + m.v3**2 + subexpr1 = m.subexpr[1] + subexpr2 = m.subexpr[2] + subexpr3 = m.subexpr[3] + else: + subexpr1 = 2 * m.fermi(m.v1) + subexpr2 = m.bessel(m.v1) - m.bessel(m.v2) + subexpr3 = subexpr2 + m.v3**2 m.con1 = pyo.Constraint(expr=m.v1 == 0.5) - m.con2 = pyo.Constraint(expr=2 * m.fermi(m.v1) + m.v2**2 - m.v3 == 1.0) - m.con3 = pyo.Constraint(expr=m.bessel(m.v1) - m.bessel(m.v2) + m.v3**2 == 2.0) + m.con2 = pyo.Constraint(expr=subexpr1 + m.v2**2 - m.v3 == 1.0) + m.con3 = pyo.Constraint(expr=subexpr3 == 2.0) return m @unittest.skipUnless(find_GSL(), "Could not find the AMPL GSL library") @@ -329,6 +341,15 @@ def test_identify_external_functions(self): pred_fcn_data = {(gsl, "gsl_sf_bessel_J0"), (gsl, "gsl_sf_fermi_dirac_m1")} self.assertEqual(fcn_data, pred_fcn_data) + @unittest.skipUnless(find_GSL(), "Could not find the AMPL GSL library") + def test_local_external_functions_with_named_expressions(self): + m = self._make_model_with_external_functions(named_expressions=True) + variables = list(m.component_data_objects(pyo.Var)) + constraints = list(m.component_data_objects(pyo.Constraint, active=True)) + b = create_subsystem_block(constraints, variables) + self.assertTrue(isinstance(b._gsl_sf_bessel_J0, pyo.ExternalFunction)) + self.assertTrue(isinstance(b._gsl_sf_fermi_dirac_m1, pyo.ExternalFunction)) + def _solve_ef_model_with_ipopt(self): m = self._make_model_with_external_functions() ipopt = pyo.SolverFactory("ipopt") @@ -362,6 +383,33 @@ def test_with_external_function(self): self.assertAlmostEqual(m.v2.value, m_full.v2.value) self.assertAlmostEqual(m.v3.value, m_full.v3.value) + @unittest.skipUnless(find_GSL(), "Could not find the AMPL GSL library") + @unittest.skipUnless( + pyo.SolverFactory("ipopt").available(), "ipopt is not available" + ) + def test_with_external_function_in_named_expression(self): + m = self._make_model_with_external_functions(named_expressions=True) + subsystem = ([m.con2, m.con3], [m.v2, m.v3]) + + m.v1.set_value(0.5) + block = create_subsystem_block(*subsystem) + ipopt = pyo.SolverFactory("ipopt") + with TemporarySubsystemManager(to_fix=list(block.input_vars.values())): + ipopt.solve(block) + + # Correct values obtained by solving with Ipopt directly + # in another script. + self.assertEqual(m.v1.value, 0.5) + self.assertFalse(m.v1.fixed) + self.assertAlmostEqual(m.v2.value, 1.04816, delta=1e-5) + self.assertAlmostEqual(m.v3.value, 1.34356, delta=1e-5) + + # Result obtained by solving the full system + m_full = self._solve_ef_model_with_ipopt() + self.assertAlmostEqual(m.v1.value, m_full.v1.value) + self.assertAlmostEqual(m.v2.value, m_full.v2.value) + self.assertAlmostEqual(m.v3.value, m_full.v3.value) + @unittest.skipUnless(find_GSL(), "Could not find the AMPL GSL library") def test_external_function_with_potential_name_collision(self): m = self._make_model_with_external_functions() diff --git a/pyomo/util/vars_from_expressions.py b/pyomo/util/vars_from_expressions.py index 8866ba980bd..878a1a13b58 100644 --- a/pyomo/util/vars_from_expressions.py +++ b/pyomo/util/vars_from_expressions.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -43,6 +43,7 @@ def get_vars_from_components( descent_order: Traversal strategy for finding the objects of type ctype """ seen = set() + named_expression_cache = {} for constraint in block.component_data_objects( ctype, active=active, @@ -51,7 +52,9 @@ def get_vars_from_components( descent_order=descent_order, ): for var in EXPR.identify_variables( - constraint.expr, include_fixed=include_fixed + constraint.expr, + include_fixed=include_fixed, + named_expression_cache=named_expression_cache, ): if id(var) not in seen: seen.add(id(var)) diff --git a/pyomo/version/__init__.py b/pyomo/version/__init__.py index 08bcde304a6..acc92ff6b37 100644 --- a/pyomo/version/__init__.py +++ b/pyomo/version/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/version/info.py b/pyomo/version/info.py index d274d0dead1..36945e8e011 100644 --- a/pyomo/version/info.py +++ b/pyomo/version/info.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -26,7 +26,7 @@ # main and needs a hard reference to "suitably new" development. major = 6 minor = 7 -micro = 0 +micro = 3 releaselevel = 'invalid' # releaselevel = 'final' serial = 0 diff --git a/pyomo/version/tests/__init__.py b/pyomo/version/tests/__init__.py index 9fb4f531a5b..f013ccd3fa3 100644 --- a/pyomo/version/tests/__init__.py +++ b/pyomo/version/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/version/tests/check.py b/pyomo/version/tests/check.py index ab3b45ffc6c..0fca9badb2f 100644 --- a/pyomo/version/tests/check.py +++ b/pyomo/version/tests/check.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/version/tests/test_version.py b/pyomo/version/tests/test_version.py index 253ee53137c..3b39bd71cb1 100644 --- a/pyomo/version/tests/test_version.py +++ b/pyomo/version/tests/test_version.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/scripts/admin/README.md b/scripts/admin/README.md new file mode 100644 index 00000000000..50ad2020b94 --- /dev/null +++ b/scripts/admin/README.md @@ -0,0 +1,28 @@ +# Contributors Script + +The `contributors.py` script is intended to be used to determine contributors +to a public GitHub repository within a given time frame. + +## Requirements + +1. Python 3.7+ +1. [PyGithub](https://pypi.org/project/PyGithub/) +1. A [GitHub Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) with `repo` access, exported to the environment variable `GH_TOKEN` + +## Usage + +``` +Usage: contributors.py + : the GitHub organization/repository combo (e.g., Pyomo/pyomo) + : date from which to start exploring contributors in YYYY-MM-DD + : date at which to stop exploring contributors in YYYY-MM-DD + +ALSO REQUIRED: Please generate a GitHub token (with repo permissions) and export to the environment variable GH_TOKEN. + Visit GitHub's official documentation for more details. +``` + +## Results + +A list of contributors will print to the terminal upon completion. More detailed +information, including authors, committers, reviewers, and pull requests, can +be found in the `contributors-start_date-end_date.json` generated file. diff --git a/scripts/admin/contributors.py b/scripts/admin/contributors.py new file mode 100644 index 00000000000..ffc02059d6f --- /dev/null +++ b/scripts/admin/contributors.py @@ -0,0 +1,246 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +""" +This script is intended to query the GitHub REST API and get contributor +information for a given time period. +""" + +import sys +import re +import json +import os + +from datetime import datetime +from os import environ +from time import perf_counter +from github import Github, Auth + + +def collect_contributors(repository, start_date, end_date): + """ + Return contributor information for a repository in a given timeframe + + Parameters + ---------- + repository : String + The org/repo combination for target repository (GitHub). E.g., + IDAES/idaes-pse + start_date : String + Start date in YYYY-MM-DD. + end_date : String + End date in YYYY-MM-DD. + + Returns + ------- + contributor_information : Dict + A dictionary with contributor information including Authors, Reviewers, + Committers, and Pull Requests. + tag_name_map : Dict + A dictionary that maps GitHub handles to GitHub display names (if they + exist). + only_tag_available : List + A list of the handles for contributors who do not have GitHub display names + available. + + """ + # Create data structure + contributor_information = {} + contributor_information['Pull Requests'] = {} + contributor_information['Authors'] = {} + contributor_information['Reviewers'] = {} + contributor_information['Commits'] = {} + # Collect the authorization token from the user's environment + token = environ.get('GH_TOKEN') + auth_token = Auth.Token(token) + # Create a connection to GitHub + gh = Github(auth=auth_token) + # Create a repository object for the requested repository + repo = gh.get_repo(repository) + commits = repo.get_commits(since=start_date, until=end_date) + # Search the commits between the two dates for those that match the string; + # this is the default pull request merge message. This works assuming that + # a repo does not squash commits + merged_prs = [ + int( + commit.commit.message.replace('Merge pull request #', '').split(' from ')[0] + ) + for commit in commits + if commit.commit.message.startswith("Merge pull request") + ] + # If the search above returned nothing, it's likely that the repo squashes + # commits when merging PRs. This is a different regex for that case. + if not merged_prs: + regex_pattern = '\(#.*\)' + for commit in commits: + results = re.search(regex_pattern, commit.commit.message) + try: + merged_prs.append(int(results.group().replace('(#', '').split(')')[0])) + except AttributeError: + continue + # Count the number of commits from each person within the two dates + for commit in commits: + try: + if commit.author.login in contributor_information['Commits'].keys(): + contributor_information['Commits'][commit.author.login] += 1 + else: + contributor_information['Commits'][commit.author.login] = 1 + except AttributeError: + # Sometimes GitHub returns an author who doesn't have a handle, + # which seems impossible but happens. In that case, we just record + # their "human-readable" name + if commit.commit.author.name in contributor_information['Commits'].keys(): + contributor_information['Commits'][commit.commit.author.name] += 1 + else: + contributor_information['Commits'][commit.commit.author.name] = 1 + + author_tags = set() + reviewer_tags = set() + for num in merged_prs: + try: + # sometimes the commit messages can lie and give a PR number + # for a different repository fork/branch. + # We try to query it, and if it doesn't work, whatever, move on. + pr = repo.get_pull(num) + except: + continue + # Sometimes the user does not have a handle recorded by GitHub. + # In this case, we replace it with "NOTFOUND" so the person running + # the code knows to go inspect it manually. + author_tag = pr.user.login + if author_tag is None: + author_tag = "NOTFOUND" + # Count the number of PRs authored by each person + if author_tag in author_tags: + contributor_information['Authors'][author_tag] += 1 + else: + contributor_information['Authors'][author_tag] = 1 + author_tags.add(author_tag) + + # Now we inspect all of the reviews to see who engaged in reviewing + # this specific PR + reviews = pr.get_reviews() + review_tags = set(review.user.login for review in reviews) + # Count how many PRs this person has reviewed + for tag in review_tags: + if tag in reviewer_tags: + contributor_information['Reviewers'][tag] += 1 + else: + contributor_information['Reviewers'][tag] = 1 + reviewer_tags.update(review_tags) + contributor_information['Pull Requests'][num] = { + 'author': author_tag, + 'reviewers': review_tags, + } + # This portion replaces tags with human-readable names, if they are present, + # so as to remove the step of "Who does that handle belong to?" + all_tags = author_tags.union(reviewer_tags) + tag_name_map = {} + only_tag_available = [] + for tag in all_tags: + if tag in tag_name_map.keys(): + continue + name = gh.search_users(tag + ' in:login')[0].name + # If they don't have a name listed, just keep the tag + if name is not None: + tag_name_map[tag] = name + else: + only_tag_available.append(tag) + for key in tag_name_map.keys(): + if key in contributor_information['Authors'].keys(): + contributor_information['Authors'][tag_name_map[key]] = ( + contributor_information['Authors'].pop(key) + ) + if key in contributor_information['Reviewers'].keys(): + contributor_information['Reviewers'][tag_name_map[key]] = ( + contributor_information['Reviewers'].pop(key) + ) + return contributor_information, tag_name_map, only_tag_available + + +def set_default(obj): + """ + Converts sets to list for JSON dump + """ + if isinstance(obj, set): + return list(obj) + raise TypeError + + +if __name__ == '__main__': + if len(sys.argv) != 4: + print(f"Usage: {sys.argv[0]} ") + print( + " : the GitHub organization/repository combo (e.g., Pyomo/pyomo)" + ) + print( + " : date from which to start exploring contributors in YYYY-MM-DD" + ) + print( + " : date at which to stop exploring contributors in YYYY-MM-DD" + ) + print("") + print( + "ALSO REQUIRED: Please generate a GitHub token (with repo permissions) and export to the environment variable GH_TOKEN." + ) + print(" Visit GitHub's official documentation for more details.") + sys.exit(1) + repository = sys.argv[1] + repository_name = sys.argv[1].split('/')[1] + try: + start = sys.argv[2].split('-') + year = int(start[0]) + try: + month = int(start[1]) + except SyntaxError: + month = int(start[1][1]) + try: + day = int(start[2]) + except SyntaxError: + day = int(start[2][1]) + start_date = datetime(year, month, day) + except: + print("Ensure that the start date is in YYYY-MM-DD format.") + sys.exit(1) + try: + end = sys.argv[3].split('-') + year = int(end[0]) + try: + month = int(end[1]) + except SyntaxError: + month = int(end[1][1]) + try: + day = int(end[2]) + except SyntaxError: + day = int(end[2][1]) + end_date = datetime(year, month, day) + except: + print("Ensure that the end date is in YYYY-MM-DD format.") + sys.exit(1) + json_filename = f"contributors-{repository_name}-{sys.argv[2]}-{sys.argv[3]}.json" + if os.path.isfile(json_filename): + raise FileExistsError(f'ERROR: The file {json_filename} already exists!') + print('BEGIN DATA COLLECTION... (this can take some time)') + tic = perf_counter() + contrib_info, author_name_map, tags_only = collect_contributors( + repository, start_date, end_date + ) + toc = perf_counter() + print(f"\nCOLLECTION COMPLETE. Time to completion: {toc - tic:0.4f} seconds") + print(f"\nContributors between {sys.argv[2]} and {sys.argv[3]}:") + for item in author_name_map.values(): + print(item) + print("\nOnly GitHub handles are available for the following contributors:") + for tag in tags_only: + print(tag) + with open(json_filename, 'w') as file: + json.dump(contrib_info, file, default=set_default) + print(f"\nDetailed information can be found in {json_filename}.") diff --git a/scripts/get_pyomo.py b/scripts/get_pyomo.py index a97c0ba3a00..d90773f2315 100644 --- a/scripts/get_pyomo.py +++ b/scripts/get_pyomo.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/scripts/get_pyomo_extras.py b/scripts/get_pyomo_extras.py index d2aa097154a..6688f3c6dc4 100644 --- a/scripts/get_pyomo_extras.py +++ b/scripts/get_pyomo_extras.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/scripts/performance/compare.py b/scripts/performance/compare.py index 5edef9bfadd..e62440fd6d9 100755 --- a/scripts/performance/compare.py +++ b/scripts/performance/compare.py @@ -2,7 +2,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/scripts/performance/compare_components.py b/scripts/performance/compare_components.py index f390fad8454..764b50217ef 100644 --- a/scripts/performance/compare_components.py +++ b/scripts/performance/compare_components.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # # This script compares build time and memory usage for # various modeling objects. The output is organized into diff --git a/scripts/performance/expr_perf.py b/scripts/performance/expr_perf.py index 6566431b9f3..9abdd560887 100644 --- a/scripts/performance/expr_perf.py +++ b/scripts/performance/expr_perf.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # # This script runs performance tests on expressions # diff --git a/scripts/performance/main.py b/scripts/performance/main.py index 10349c0eb73..07dc38a11a7 100755 --- a/scripts/performance/main.py +++ b/scripts/performance/main.py @@ -2,7 +2,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/scripts/performance/simple.py b/scripts/performance/simple.py index 2990f13f413..c5fb836b64b 100644 --- a/scripts/performance/simple.py +++ b/scripts/performance/simple.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * import pyomo.core.expr.current as EXPR import timeit diff --git a/setup.cfg b/setup.cfg index b606138f38c..f670cef8f68 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,3 +22,4 @@ markers = lp: marks lp tests gams: marks gams tests bar: marks bar tests + builders: tests that should be run when testing custom (extension) builders \ No newline at end of file diff --git a/setup.py b/setup.py index 252ef2d063e..a125b02b2fe 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -19,8 +19,10 @@ from setuptools import setup, find_packages, Command try: + # This works beginning in setuptools 40.7.0 (27 Jan 2019) from setuptools import DistutilsOptionError except ImportError: + # Needed for setuptools prior to 40.7.0 from distutils.errors import DistutilsOptionError @@ -232,6 +234,7 @@ def __ne__(self, other): 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Scientific/Engineering :: Mathematics', @@ -240,20 +243,19 @@ def __ne__(self, other): python_requires='>=3.8', install_requires=['ply'], extras_require={ - 'tests': [ - #'codecov', # useful for testing infrastructures, but not required - 'coverage', - 'pytest', - 'pytest-parallel', - 'parameterized', - 'pybind11', - ], + # There are certain tests that also require pytest-qt, but because those + # tests are so environment/machine specific, we are leaving these out of + # the dependencies. + 'tests': ['coverage', 'parameterized', 'pybind11', 'pytest', 'pytest-parallel'], 'docs': [ 'Sphinx>4', 'sphinx-copybutton', 'sphinx_rtd_theme>0.5', 'sphinxcontrib-jsmath', 'sphinxcontrib-napoleon', + 'sphinx-toolbox>=2.16.0', + 'sphinx-jinja2-compat>=0.1.1', + 'enum_tools', 'numpy', # Needed by autodoc for pynumero 'scipy', # Needed by autodoc for pynumero ], @@ -265,7 +267,7 @@ def __ne__(self, other): 'matplotlib!=3.6.1', # network, incidence_analysis, community_detection # Note: networkx 3.2 is Python>-3.9, but there is a broken - # 3.2 package on conda-forgethat will get implicitly + # 3.2 package on conda-forge that will get implicitly # installed on python 3.8 'networkx<3.2; python_version<"3.9"', 'networkx; python_version>="3.9"', @@ -276,6 +278,9 @@ def __ne__(self, other): 'plotly', # incidence_analysis 'python-louvain', # community_detection 'pyyaml', # core + # qtconsole also requires a supported Qt version (PyQt5 or PySide6). + # Because those are environment specific, we have left that out here. + 'qtconsole', # contrib.viewer 'scipy', 'sympy', # differentiation 'xlrd', # dataportals @@ -288,9 +293,7 @@ def __ne__(self, other): # The following optional dependencies are difficult to # install on PyPy (binary wheels are not available), so we # will only "require" them on other (CPython) platforms: - # - # DAE can use casadi - 'casadi; implementation_name!="pypy"', + 'casadi; implementation_name!="pypy"', # dae 'numdifftools; implementation_name!="pypy"', # pynumero 'pandas; implementation_name!="pypy"', 'seaborn; implementation_name!="pypy"', # parmest.graphics @@ -303,6 +306,7 @@ def __ne__(self, other): "pyomo.contrib.mcpp": ["*.cpp"], "pyomo.contrib.pynumero": ['src/*', 'src/tests/*'], "pyomo.contrib.viewer": ["*.ui"], + "pyomo.contrib.simplification.ginac": ["src/*.cpp", "src/*.hpp"], }, ext_modules=ext_modules, entry_points="""