├── requirements.txt ├── src └── amply │ ├── __init__.py │ └── amply.py ├── INSTALL ├── .gitignore ├── pyproject.toml ├── AUTHORS ├── .isort.cfg ├── .pre-commit-config.yaml ├── .github └── workflows │ ├── release.yml │ └── python-package.yml ├── setup.py ├── .travis.yml ├── tests ├── travis_install.sh └── test_amply.py ├── LICENSE └── README.rst /requirements.txt: -------------------------------------------------------------------------------- 1 | pyparsing 2 | -------------------------------------------------------------------------------- /src/amply/__init__.py: -------------------------------------------------------------------------------- 1 | from .amply import Amply, AmplyError 2 | 3 | __all__ = ['Amply', 'AmplyError'] 4 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | Amply is a Python package which can be installed by PyPI using Pip. 2 | 3 | For example: 4 | 5 | pip install amply -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__ 3 | *.pyc 4 | .pytest* 5 | Amply.egg-info 6 | .pypirc 7 | build/ 8 | dist/ 9 | 10 | # Editors 11 | .vscode 12 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.setuptools_scm] 6 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Will Usher 2 | Christophe-Marie Duquesne 3 | Stuart Mitchell 4 | Franco Peschiera 5 | Stu 6 | smit023 7 | Q. Lim -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | line_length=88 3 | indent=' ' 4 | skip=.tox,.venv,build,dist 5 | known_standard_library=setuptools,pkg_resources 6 | known_test=pytest 7 | known_first_party=amply 8 | sections=FUTURE,STDLIB,COMPAT,TEST,THIRDPARTY,FIRSTPARTY,LOCALFOLDER 9 | default_section=THIRDPARTY 10 | multi_line_output=3 11 | include_trailing_comma=True 12 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: '^docs/conf.py' 2 | 3 | repos: 4 | - repo: https://github.com/psf/black 5 | rev: 22.3.0 6 | hooks: 7 | - id: black 8 | language_version: python3.8 9 | 10 | - repo: https://github.com/pre-commit/pre-commit-hooks 11 | rev: v2.2.3 12 | hooks: 13 | - id: trailing-whitespace 14 | - id: check-added-large-files 15 | - id: check-ast 16 | - id: check-json 17 | - id: check-merge-conflict 18 | - id: check-xml 19 | - id: check-yaml 20 | - id: debug-statements 21 | - id: end-of-file-fixer 22 | - id: requirements-txt-fixer 23 | - id: mixed-line-ending 24 | args: ['--fix=no'] 25 | - id: flake8 26 | args: ['--max-line-length=88','--extend-ignore=E203'] # default of Black 27 | 28 | - repo: https://github.com/pre-commit/mirrors-isort 29 | rev: v5.10.1 30 | hooks: 31 | - id: isort 32 | 33 | - repo: https://github.com/pre-commit/mirrors-mypy 34 | rev: v0.942 # Use the sha / tag you want to point at 35 | hooks: 36 | - id: mypy 37 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Set up Python 26 | uses: actions/setup-python@v3 27 | with: 28 | python-version: '3.x' 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install build 33 | - name: Build package 34 | run: python -m build 35 | - name: Publish package 36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ["3.8", "3.9", "3.10"] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v3 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | python -m pip install flake8 pytest 31 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 32 | pip install . 33 | - name: Lint with flake8 34 | run: | 35 | # stop the build if there are Python syntax errors or undefined names 36 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 37 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 38 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 39 | - name: Test with pytest 40 | run: | 41 | pytest 42 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open("README.rst", "r") as fh: 4 | long_description = fh.read() 5 | 6 | with open("AUTHORS", "r") as fh: 7 | authors = [] 8 | text_authors = fh.readlines() 9 | for author in text_authors: 10 | authors.append(author.split("<")[0].strip()) 11 | 12 | setup( 13 | name="amply", 14 | packages=find_packages("src"), 15 | license='Eclipse Public License 1.0 (EPL-1.0)', 16 | use_scm_version=True, 17 | setup_requires=["setuptools_scm"], 18 | # Project uses reStructuredText, so ensure that the docutils get 19 | # installed or upgraded on the target machine 20 | install_requires=["docutils>=0.3", "pyparsing"], 21 | package_dir={"": "src"}, 22 | package_data={ 23 | # If any package contains *.txt or *.rst files, include them: 24 | "": ["*.txt", "*.rst"], 25 | }, 26 | 27 | # metadata to display on PyPI 28 | author=",".join(authors), 29 | author_email="wusher@kth.se", 30 | description="Amply allows you to load and manipulate AMPL/GLPK data as Python data structures", 31 | long_description_content_type="text/x-rst", 32 | long_description=long_description, 33 | keywords="ampl gmpl", 34 | url="http://github.com/willu47/amply", # project home page, if any 35 | project_urls={ 36 | "Bug Tracker": "http://github.com/willu47/amply/issues", 37 | "Documentation": "http://github.com/willu47/amply/README.rst", 38 | "Source Code": "http://github.com/willu47/amply", 39 | }, 40 | classifiers=[ 41 | "License :: OSI Approved :: Eclipse Public License 1.0 (EPL-1.0)" 42 | ], 43 | python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', 44 | ) 45 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | dist: xenial 3 | os: linux 4 | virtualenv: 5 | system_site_packages: false 6 | jobs: 7 | fast_finish: true 8 | include: 9 | - name: conda_python3.8 10 | env: DISTRIB="conda" PYTHON_VERSION="3.8" COVERAGE="true" 11 | install: 12 | - source tests/travis_install.sh 13 | before_script: 14 | - git config --global user.email "wusher@kth.com" 15 | - git config --global user.name "Will Usher" 16 | script: 17 | - pip install . 18 | - pytest --cov=amply 19 | - | 20 | if [[ "$COVERAGE" == "true" ]]; then 21 | pre-commit install 22 | pre-commit run --files src 23 | fi 24 | after_success: 25 | - if [[ "$COVERAGE" == "true" ]]; then coveralls || echo "failed"; fi 26 | after_script: 27 | - travis-cleanup 28 | cache: 29 | pip: true 30 | directories: 31 | - "$HOME/miniconda" 32 | notifications: 33 | email: false 34 | deploy: 35 | edge: true 36 | distributions: "sdist bdist_wheel" 37 | provider: pypi 38 | username: __token__ 39 | password: 40 | secure: Gbi2YhW/BzJE5Tnj/TVkyMG/yqz7kiucxiGT57Q8xcCZovlIXROgtN7BqFxo6P9OZEoP9iGemFEAyvh2qorIolWNBGp5vQynMoJbXTACvA1CTaz4ZPmYZXWB7IPKyHBkUDWLiU1votmvUkI7kwOcgcV6HlxI/IP1HiJUVEL1FSPv52BWR6AD4UBtSQX3sTWqKHr4hUSaxUDauuAH1EUXAWYUNBsJOnjwhTNXYMTyvFNWCqiqJjiNbjbQ2evNCKk7e9s8Vkxy2WBa7wjxyLw93KrXEVVADDr59FLESqDekftKfTcctPBKE4wLw3/vt1O2cFN/h81ASSW1iAmQsBQnR+E066SUHu2t6tpkdFQzMkQdRdE/w74qyC08QOrJrQV9TIBWu9VdVB/mSLsmszSZNGPWhKcEpiO/U47lkjfhl0IEwjjxVmaXlE8tepZZq2raAD5OVRXyjfEKocbXS7HD5t0FZUH6B3GzdoDFhpMt2aceXjUVchOHYs6kdQjz19erK7WGJPA1enta4DcAiufh+1+SckqMytY0ksA6vDoAaHJ6++/pugpPvJr8Gt8fF7BlgTG1ojLEYx+Mr5nsg/51yPAWNmgaUttWAbsloYzS9PnjlyM4WY7CBvVlQtPczVyEtmqGGIwyhVtoVba1Cbap5tGtlvEi2jJkif8TFA9Vow4= 41 | on: 42 | tags: true 43 | all_branches: true 44 | cleanup: false 45 | -------------------------------------------------------------------------------- /tests/travis_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script is meant to be called by the "install" step defined in 3 | # .travis.yml. See http://docs.travis-ci.com/ for more details. 4 | # The behavior of the script is controlled by environment variabled defined 5 | # in the .travis.yml in the top level folder of the project. 6 | # 7 | # This script is inspired by Scikit-Learn (http://scikit-learn.org/) 8 | # 9 | # THIS SCRIPT IS SUPPOSED TO BE AN EXAMPLE. MODIFY IT ACCORDING TO YOUR NEEDS! 10 | 11 | set -e 12 | 13 | if [[ "$DISTRIB" == "conda" ]]; then 14 | # Deactivate the travis-provided virtual environment and setup a 15 | # conda-based environment instead 16 | deactivate 17 | 18 | if [[ -f "$HOME/miniconda/bin/conda" ]]; then 19 | echo "Skip install conda [cached]" 20 | else 21 | # By default, travis caching mechanism creates an empty dir in the 22 | # beginning of the build, but conda installer aborts if it finds an 23 | # existing folder, so let's just remove it: 24 | rm -rf "$HOME/miniconda" 25 | 26 | # Use the miniconda installer for faster download / install of conda 27 | # itself 28 | wget http://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh \ 29 | -O miniconda.sh 30 | chmod +x miniconda.sh && ./miniconda.sh -b -p $HOME/miniconda 31 | fi 32 | export PATH=$HOME/miniconda/bin:$PATH 33 | # Make sure to use the most updated version 34 | conda update --yes conda 35 | 36 | # Configure the conda environment and put it in the path using the 37 | # provided versions 38 | # (prefer local venv, since the miniconda folder is cached) 39 | conda config --add channels conda-forge 40 | conda create -p ./.venv --yes python=${PYTHON_VERSION} pip virtualenv pyparsing docutils 41 | source activate ./.venv 42 | else 43 | echo "Don't install conda for this Python version" 44 | fi 45 | 46 | # for all 47 | pip install -U pip setuptools 48 | 49 | if [[ "$COVERAGE" == "true" ]]; then 50 | pip install -U pytest-cov pytest-virtualenv coverage coveralls flake8 pre-commit 51 | fi 52 | 53 | travis-cleanup() { 54 | printf "Cleaning up environments ... " # printf avoids new lines 55 | if [[ "$DISTRIB" == "conda" ]]; then 56 | # Force the env to be recreated next time, for build consistency 57 | conda deactivate 58 | conda remove -p ./.venv --all --yes 59 | rm -rf ./.venv 60 | fi 61 | echo "DONE" 62 | } 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 1.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 4 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 5 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial code and documentation 12 | distributed under this Agreement, and 13 | 14 | b) in the case of each subsequent Contributor: 15 | 16 | i) changes to the Program, and 17 | 18 | ii) additions to the Program; 19 | 20 | where such changes and/or additions to the Program originate from and are 21 | distributed by that particular Contributor. A Contribution 'originates' from 22 | a Contributor if it was added to the Program by such Contributor itself or 23 | anyone acting on such Contributor's behalf. Contributions do not include additions 24 | to the Program which: (i) are separate modules of software distributed in 25 | conjunction with the Program under their own license agreement, and (ii) are 26 | not derivative works of the Program. 27 | 28 | "Contributor" means any person or entity that distributes the Program. 29 | 30 | "Licensed Patents" mean patent claims licensable by a Contributor which are 31 | necessarily infringed by the use or sale of its Contribution alone or when 32 | combined with the Program. 33 | 34 | "Program" means the Contributions distributed in accordance with this Agreement. 35 | 36 | "Recipient" means anyone who receives the Program under this Agreement, including 37 | all Contributors. 38 | 39 | 2. GRANT OF RIGHTS 40 | 41 | a) Subject to the terms of this Agreement, each Contributor hereby grants 42 | Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, 43 | prepare derivative works of, publicly display, publicly perform, distribute 44 | and sublicense the Contribution of such Contributor, if any, and such derivative 45 | works, in source code and object code form. 46 | 47 | b) Subject to the terms of this Agreement, each Contributor hereby grants 48 | Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed 49 | Patents to make, use, sell, offer to sell, import and otherwise transfer the 50 | Contribution of such Contributor, if any, in source code and object code form. 51 | This patent license shall apply to the combination of the Contribution and 52 | the Program if, at the time the Contribution is added by the Contributor, 53 | such addition of the Contribution causes such combination to be covered by 54 | the Licensed Patents. The patent license shall not apply to any other combinations 55 | which include the Contribution. No hardware per se is licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any Contributor 59 | that the Program does not infringe the patent or other intellectual property 60 | rights of any other entity. Each Contributor disclaims any liability to Recipient 61 | for claims brought by any other entity based on infringement of intellectual 62 | property rights or otherwise. As a condition to exercising the rights and 63 | licenses granted hereunder, each Recipient hereby assumes sole responsibility 64 | to secure any other intellectual property rights needed, if any. For example, 65 | if a third party patent license is required to allow Recipient to distribute 66 | the Program, it is Recipient's responsibility to acquire that license before 67 | distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient copyright 70 | rights in its Contribution, if any, to grant the copyright license set forth 71 | in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for damages, 88 | including direct, indirect, special, incidental and consequential damages, 89 | such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such Contributor, 95 | and informs licensees how to obtain it in a reasonable manner on or through 96 | a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor 116 | who includes the Program in a commercial product offering should do so in 117 | a manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product offering, 119 | such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify 120 | every other Contributor ("Indemnified Contributor") against any losses, damages 121 | and costs (collectively "Losses") arising from claims, lawsuits and other 122 | legal actions brought by a third party against the Indemnified Contributor 123 | to the extent caused by the acts or omissions of such Commercial Contributor 124 | in connection with its distribution of the Program in a commercial product 125 | offering. The obligations in this section do not apply to any claims or Losses 126 | relating to any actual or alleged intellectual property infringement. In order 127 | to qualify, an Indemnified Contributor must: a) promptly notify the Commercial 128 | Contributor in writing of such claim, and b) allow the Commercial Contributor 129 | to control, and cooperate with the Commercial Contributor in, the defense 130 | and any related settlement negotiations. The Indemnified Contributor may participate 131 | in any such claim at its own expense. 132 | 133 | For example, a Contributor might include the Program in a commercial product 134 | offering, Product X. That Contributor is then a Commercial Contributor. If 135 | that Commercial Contributor then makes performance claims, or offers warranties 136 | related to Product X, those performance claims and warranties are such Commercial 137 | Contributor's responsibility alone. Under this section, the Commercial Contributor 138 | would have to defend claims against the other Contributors related to those 139 | performance claims and warranties, and if a court requires any other Contributor 140 | to pay any damages as a result, the Commercial Contributor must pay those 141 | damages. 142 | 143 | 5. NO WARRANTY 144 | 145 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 146 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS 147 | OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF 148 | TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. 149 | Each Recipient is solely responsible for determining the appropriateness of 150 | using and distributing the Program and assumes all risks associated with its 151 | exercise of rights under this Agreement, including but not limited to the 152 | risks and costs of program errors, compliance with applicable laws, damage 153 | to or loss of data, programs or equipment, and unavailability or interruption 154 | of operations. 155 | 156 | 6. DISCLAIMER OF LIABILITY 157 | 158 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 159 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 160 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 161 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 162 | STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY 163 | WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS 164 | GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 165 | 166 | 7. GENERAL 167 | 168 | If any provision of this Agreement is invalid or unenforceable under applicable 169 | law, it shall not affect the validity or enforceability of the remainder of 170 | the terms of this Agreement, and without further action by the parties hereto, 171 | such provision shall be reformed to the minimum extent necessary to make such 172 | provision valid and enforceable. 173 | 174 | If Recipient institutes patent litigation against any entity (including a 175 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 176 | (excluding combinations of the Program with other software or hardware) infringes 177 | such Recipient's patent(s), then such Recipient's rights granted under Section 178 | 2(b) shall terminate as of the date such litigation is filed. 179 | 180 | All Recipient's rights under this Agreement shall terminate if it fails to 181 | comply with any of the material terms or conditions of this Agreement and 182 | does not cure such failure in a reasonable period of time after becoming aware 183 | of such noncompliance. If all Recipient's rights under this Agreement terminate, 184 | Recipient agrees to cease use and distribution of the Program as soon as reasonably 185 | practicable. However, Recipient's obligations under this Agreement and any 186 | licenses granted by Recipient relating to the Program shall continue and survive. 187 | 188 | Everyone is permitted to copy and distribute copies of this Agreement, but 189 | in order to avoid inconsistency the Agreement is copyrighted and may only 190 | be modified in the following manner. The Agreement Steward reserves the right 191 | to publish new versions (including revisions) of this Agreement from time 192 | to time. No one other than the Agreement Steward has the right to modify this 193 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse 194 | Foundation may assign the responsibility to serve as the Agreement Steward 195 | to a suitable separate entity. Each new version of the Agreement will be given 196 | a distinguishing version number. The Program (including Contributions) may 197 | always be distributed subject to the version of the Agreement under which 198 | it was received. In addition, after a new version of the Agreement is published, 199 | Contributor may elect to distribute the Program (including its Contributions) 200 | under the new version. Except as expressly stated in Sections 2(a) and 2(b) 201 | above, Recipient receives no rights or licenses to the intellectual property 202 | of any Contributor under this Agreement, whether expressly, by implication, 203 | estoppel or otherwise. All rights in the Program not expressly granted under 204 | this Agreement are reserved. 205 | 206 | This Agreement is governed by the laws of the State of New York and the intellectual 207 | property laws of the United States of America. No party to this Agreement 208 | will bring a legal action under this Agreement more than one year after the 209 | cause of action arose. Each party waives its rights to a jury trial in any 210 | resulting litigation. 211 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Amply 2 | ====== 3 | 4 | .. image:: https://travis-ci.com/willu47/amply.svg?branch=master 5 | :target: https://travis-ci.com/willu47/amply 6 | .. image:: https://img.shields.io/pypi/v/amply?style=plastic 7 | :alt: PyPI 8 | :target: https://pypi.org/project/amply/ 9 | .. image:: https://coveralls.io/repos/github/willu47/amply/badge.svg?branch=master 10 | :target: https://coveralls.io/github/willu47/amply?branch=master 11 | 12 | 13 | Introduction 14 | ------------ 15 | 16 | Amply allows you to load and manipulate AMPL data as Python data structures. 17 | 18 | Amply only supports a specific subset of the AMPL syntax: 19 | 20 | * set declarations 21 | * set data statements 22 | * parameter declarations 23 | * parameter data statements 24 | 25 | Declarations and data statements 26 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 27 | 28 | Typically, problems expressed in AMPL consist of two parts, a *model* section and a *data* section. 29 | Amply is only designed to parse the parameter and set statements contained within AMPL data sections. 30 | However, in order to parse these statements correctly, information that would usually be contained 31 | within the model section may be required. For instance, it may not be possible to infer the dimension 32 | of a set purely from its data statement. Therefore, Amply also supports set and parameter declarations. 33 | These do not have to be put in a separate section, they only need to occur before the corresponding 34 | data statement. 35 | 36 | 37 | The declaration syntax supported is extremely limited, and does not include most 38 | elements of the AMPL programming language. The intention is that this library 39 | is used as a way of loading data specified in an AMPL-like syntax. 40 | 41 | Furthermore, Amply does not perform any validation on data statements. 42 | 43 | About this document 44 | ^^^^^^^^^^^^^^^^^^^^ 45 | 46 | This document is intended as a guide to the syntax supported by Amply, and not as a general 47 | AMPL reference manual. For more in depth coverage see the `GNU MathProg manual, Chapter 5: Model data 48 | `_ or the following links: 49 | 50 | * `Sets in AMPL `_ 51 | * `Parameters in AMPL `_ 52 | 53 | Quickstart Guide 54 | ---------------- 55 | 56 | >>> from amply import Amply 57 | 58 | Import the class: :: 59 | 60 | >>> from amply import Amply 61 | 62 | A simple set. Sets behave a lot like lists. 63 | 64 | >>> data = Amply("set CITIES := Auckland Wellington Christchurch;") 65 | >>> print data.CITIES 66 | 67 | >>> print data['CITIES'] 68 | 69 | >>> for c in data.CITIES: print c 70 | ... 71 | Auckland 72 | Wellington 73 | Christchurch 74 | >>> print data.CITIES[0] 75 | Auckland 76 | >>> print len(data.CITIES) 77 | 3 78 | 79 | 80 | Data can be integers, reals, symbolic, or quoted strings: 81 | 82 | >>> data = Amply(""" 83 | ... set BitsNPieces := 0 3.2 -6e4 Hello "Hello, World!"; 84 | ... """) 85 | >>> print data.BitsNPieces 86 | 87 | 88 | Sets can contain multidimensional data, but we have to declare them to be so first. 89 | 90 | >>> data = Amply(""" 91 | ... set pairs dimen 2; 92 | ... set pairs := (1, 2) (2, 3) (3, 4); 93 | ... """) 94 | >>> print data.pairs 95 | 96 | 97 | Sets themselves can be multidimensional (i.e. be subscriptable): 98 | 99 | >>> data = Amply(""" 100 | ... set CITIES{COUNTRIES}; 101 | ... set CITIES[Australia] := Adelaide Melbourne Sydney; 102 | ... set CITIES[Italy] := Florence Milan Rome; 103 | ... """) 104 | >>> print data.CITIES['Australia'] 105 | ['Adelaide', 'Melbourne', 'Sydney'] 106 | >>> print data.CITIES['Italy'] 107 | ['Florence', 'Milan', 'Rome'] 108 | 109 | Note that in the above example, the set COUNTRIES didn't actually have to exist itself. 110 | Amply does not perform any validation on subscripts, it only uses them to figure out 111 | how many subscripts a set has. To specify more than one, separate them by commas: 112 | 113 | >>> data = Amply(""" 114 | ... set SUBURBS{COUNTRIES, CITIES}; 115 | ... set SUBURBS[Australia, Melbourne] := Docklands 'South Wharf' Kensington; 116 | ... """) 117 | >>> print data.SUBURBS['Australia', 'Melbourne'] 118 | ['Docklands', 'South Wharf', 'Kensington'] 119 | 120 | *Slices* can be used to simplify the entry of multi-dimensional data. 121 | 122 | >>> data=Amply(""" 123 | ... set TRIPLES dimen 3; 124 | ... set TRIPLES := (1, 1, *) 2 3 4 (*, 2, *) 6 7 8 9 (*, *, *) (1, 1, 1); 125 | ... """) 126 | >>> print data.TRIPLES 127 | 128 | > 129 | 130 | Set data can also be specified using a matrix notation. 131 | A '+' indicates that the pair is included in the set whereas a '-' indicates a 132 | pair not in the set. 133 | 134 | >>> data=Amply(""" 135 | ... set ROUTES dimen 2; 136 | ... set ROUTES : A B C D := 137 | ... E + - - + 138 | ... F + + - - 139 | ... ; 140 | ... """) 141 | >>> print data.ROUTES 142 | 143 | 144 | Matrices can also be transposed: 145 | 146 | >>> data=Amply(""" 147 | ... set ROUTES dimen 2; 148 | ... set ROUTES (tr) : E F := 149 | ... A + + 150 | ... B - + 151 | ... C - - 152 | ... D + - 153 | ... ; 154 | ... """) 155 | >>> print data.ROUTES 156 | 157 | 158 | Matrices only specify 2d data, however they can be combined with slices 159 | to define higher-dimensional data: 160 | 161 | >>> data = Amply(""" 162 | ... set QUADS dimen 2; 163 | ... set QUADS := 164 | ... (1, 1, *, *) : 2 3 4 := 165 | ... 2 + - + 166 | ... 3 - + + 167 | ... (1, 2, *, *) : 2 3 4 := 168 | ... 2 - + - 169 | ... 3 + - - 170 | ... ; 171 | ... """) 172 | >>> print data.QUADS 173 | 174 | 175 | Parameters are also supported: 176 | 177 | >>> data = Amply(""" 178 | ... param T := 30; 179 | ... param n := 5; 180 | ... """) 181 | >>> print data.T 182 | 30 183 | >>> print data.n 184 | 5 185 | 186 | Parameters are commonly indexed over sets. No validation is done by Amply, 187 | and the sets do not have to exist. Parameter objects are represented 188 | as a mapping. 189 | 190 | >>> data = Amply(""" 191 | ... param COSTS{PRODUCTS}; 192 | ... param COSTS := 193 | ... FISH 8.5 194 | ... CARROTS 2.4 195 | ... POTATOES 1.6 196 | ... ; 197 | ... """) 198 | >>> print data.COSTS 199 | 200 | >>> print data.COSTS['FISH'] 201 | 8.5 202 | 203 | Parameter data statements can include a *default* clause. If a '.' is included 204 | in the data, it is replaced with the default value: 205 | 206 | >>> data = Amply(""" 207 | ... param COSTS{P}; 208 | ... param COSTS default 2 := 209 | ... F 2 210 | ... E 1 211 | ... D . 212 | ... ; 213 | ... """) 214 | >>> print data.COSTS['D'] 215 | 2.0 216 | 217 | Parameter declarations can also have a default clause. For these parameters, 218 | any attempt to access the parameter for a key that has not been defined 219 | will return the default value: 220 | 221 | >>> data = Amply(""" 222 | ... param COSTS{P} default 42; 223 | ... param COSTS := 224 | ... F 2 225 | ... E 1 226 | ... ; 227 | ... """) 228 | >>> print data.COSTS['DOES NOT EXIST'] 229 | 42.0 230 | 231 | Parameters can be indexed over multiple sets. The resulting values can be 232 | accessed by treating the parameter object as a nested dictionary, or by 233 | using a tuple as an index: 234 | 235 | >>> data = Amply(""" 236 | ... param COSTS{CITIES, PRODUCTS}; 237 | ... param COSTS := 238 | ... Auckland FISH 5 239 | ... Auckland CHIPS 3 240 | ... Wellington FISH 4 241 | ... Wellington CHIPS 1 242 | ... ; 243 | ... """) 244 | >>> print data.COSTS 245 | 246 | >>> print data.COSTS['Wellington']['CHIPS'] # nested dict 247 | 1.0 248 | >>> print data.COSTS['Wellington', 'CHIPS'] # tuple as key 249 | 1.0 250 | 251 | Parameters support a slice syntax similar to that of sets: 252 | 253 | >>> data = Amply(""" 254 | ... param COSTS{CITIES, PRODUCTS}; 255 | ... param COSTS := 256 | ... [Auckland, * ] 257 | ... FISH 5 258 | ... CHIPS 3 259 | ... [Wellington, * ] 260 | ... FISH 4 261 | ... CHIPS 1 262 | ... ; 263 | ... """) 264 | >>> print data.COSTS 265 | 266 | 267 | 268 | 269 | Parameters indexed over two sets can also be specified in tabular format: 270 | 271 | 272 | >>> data = Amply(""" 273 | ... param COSTS{CITIES, PRODUCTS}; 274 | ... param COSTS: FISH CHIPS := 275 | ... Auckland 5 3 276 | ... Wellington 4 1 277 | ... ; 278 | ... """) 279 | >>> print data.COSTS 280 | 281 | 282 | Tabular data can also be transposed: 283 | 284 | >>> data = Amply(""" 285 | ... param COSTS{CITIES, PRODUCTS}; 286 | ... param COSTS (tr): Auckland Wellington := 287 | ... FISH 5 4 288 | ... CHIPS 3 1 289 | ... ; 290 | ... """) 291 | >>> print data.COSTS 292 | 293 | 294 | 295 | Slices can be combined with tabular data for parameters indexed over more than 296 | 2 sets: 297 | 298 | >>> data = Amply(""" 299 | ... param COSTS{CITIES, PRODUCTS, SIZE}; 300 | ... param COSTS := 301 | ... [Auckland, *, *] : SMALL LARGE := 302 | ... FISH 5 9 303 | ... CHIPS 3 5 304 | ... [Wellington, *, *] : SMALL LARGE := 305 | ... FISH 4 7 306 | ... CHIPS 1 2 307 | ... ; 308 | ... """) 309 | >>> print data.COSTS 310 | >> a = Amply("param T := 3;") 15 | 16 | Symbols that are defined can be accessed as attributes or items. 17 | 18 | >>> print(a.T) 19 | 3.0 20 | >>> print(a['T']) 21 | 3.0 22 | 23 | The load_string and load_file methods can be used to parse additional data 24 | 25 | >>> a.load_string("set N := 1 2 3;") 26 | >>> a.load_file(open('some_file.dat')) 27 | 28 | An Amply object can be constructed from a file using Amply.from_file 29 | 30 | >>> a = Amply.from_file(open('some_file.dat')) 31 | 32 | 33 | How it works: 34 | The Amply class parses the input using Pyparsing. This results in a list 35 | of Stmt objects, each representing a MathProg statement. The statements 36 | are then evaluated by calling their eval() method. 37 | """ 38 | from pyparsing import ( 39 | Combine, 40 | Group, 41 | Keyword, 42 | Literal, 43 | NotAny, 44 | OneOrMore, 45 | Optional, 46 | ParseException, 47 | ParserElement, 48 | ParseResults, 49 | QuotedString, 50 | SkipTo, 51 | StringEnd, 52 | Suppress, 53 | Word, 54 | ZeroOrMore, 55 | alphanums, 56 | delimitedList, 57 | lineEnd, 58 | nums, 59 | oneOf, 60 | ) 61 | 62 | ParserElement.enablePackrat() 63 | 64 | 65 | __all__ = ["Amply", "AmplyError"] 66 | 67 | 68 | class AmplyObject(object): 69 | """ 70 | Represents the value of some object (e.g. a Set object 71 | or Parameter object 72 | """ 73 | 74 | 75 | class AmplyStmt(object): 76 | """ 77 | Represents a statement that has been parsed 78 | 79 | Statements implement an eval method. When the eval method is called, the 80 | Stmt object is responsible for modifying the Amply object that 81 | gets passed in appropriately (i.e. by adding or modifying a symbol) 82 | """ 83 | 84 | def eval(self, amply): # pragma: no coverage 85 | raise NotImplementedError() 86 | 87 | 88 | class NoDefault(object): 89 | """ 90 | Sentinel 91 | """ 92 | 93 | 94 | class AmplyError(Exception): 95 | """ 96 | Amply Exception Class 97 | """ 98 | 99 | 100 | def chunk(it, n): 101 | """ 102 | Yields n-tuples from iterator 103 | """ 104 | c = [] 105 | for i, x in enumerate(it): 106 | c.append(x) 107 | if (i + 1) % n == 0: 108 | yield tuple(c) 109 | c = [] 110 | if c: 111 | yield tuple(c) 112 | 113 | 114 | def access_data(curr_dict, keys, default=NoDefault): 115 | """ 116 | Convenience method for walking down a series of nested dictionaries 117 | 118 | keys is a tuple of strings 119 | 120 | access_data(dict, ('key1', 'key2', 'key3') is equivalent to 121 | dict['key1']['key2']['key3'] 122 | 123 | All dictionaries must exist, but the last dictionary in the hierarchy 124 | does not have to contain the final key, if default is set. 125 | """ 126 | 127 | if keys in curr_dict: 128 | return curr_dict[keys] 129 | 130 | if isinstance(keys, tuple): 131 | for sym in keys[:-1]: 132 | curr_dict = curr_dict[sym] 133 | r = curr_dict.get(keys[-1], default) 134 | if r is not NoDefault: 135 | return r 136 | 137 | if default is not NoDefault: 138 | return default 139 | 140 | raise KeyError() 141 | 142 | 143 | def transpose(data): 144 | """ 145 | Transpose a matrix represented as a dict of dicts 146 | """ 147 | 148 | rows = list(data.keys()) 149 | cols = set() 150 | for d in list(data.values()): 151 | cols.update(list(d.keys())) 152 | 153 | d = {} 154 | 155 | for col in cols: 156 | d[col] = {} 157 | for row in rows: 158 | d[col][row] = data[row][col] 159 | return d 160 | 161 | 162 | class SetDefStmt(AmplyStmt): 163 | """ 164 | Represents a set definition statement 165 | """ 166 | 167 | def __init__(self, tokens): 168 | assert tokens[0] == "set" 169 | self.name = tokens[1] 170 | self.dimen = tokens.get("dimen", None) 171 | self.subscripts = len(tokens.get("subscripts", ())) 172 | 173 | def __repr__(self): # pragma: no cover 174 | return "<%s: %s[%s]>" % (self.__class__.__name__, self.name, self.dimen) 175 | 176 | def eval(self, amply): 177 | set_obj = SetObject(subscripts=self.subscripts, dimen=self.dimen) 178 | amply._addSymbol(self.name, set_obj) 179 | 180 | 181 | class SetStmt(AmplyStmt): 182 | """ 183 | Represents a set statement 184 | """ 185 | 186 | def __init__(self, tokens): 187 | assert tokens[0] == "set" 188 | self.name = tokens[1] 189 | self.records = tokens.get("records") 190 | self.member = tokens.get("member", None) 191 | 192 | def __repr__(self): 193 | return "<%s: %s[%s] = %s>" % ( 194 | self.__class__.__name__, 195 | self.name, 196 | self.member, 197 | self.records, 198 | ) 199 | 200 | def eval(self, amply): 201 | if self.name in amply.symbols: 202 | obj = amply.symbols[self.name] 203 | assert isinstance(obj, SetObject) 204 | else: 205 | obj = SetObject() 206 | 207 | obj.addData(self.member, self.records) 208 | amply._addSymbol(self.name, obj) 209 | 210 | 211 | class SliceRecord(object): 212 | """ 213 | Represents a parameter or set slice record 214 | """ 215 | 216 | def __init__(self, tokens): 217 | self.components = tuple(tokens) 218 | 219 | def __repr__(self): 220 | return "<%s: %s>" % (self.__class__.__name__, self.components) 221 | 222 | 223 | class TabularRecord(object): 224 | """ 225 | Represents a parameter tabular record 226 | """ 227 | 228 | def __init__(self, tokens): 229 | self._columns = tokens.columns 230 | self._data = tokens.data 231 | self.transposed = False 232 | 233 | def setTransposed(self, t): 234 | self.transposed = t 235 | 236 | def _rows(self): 237 | c = Chunker(self._data) 238 | while c.notEmpty(): 239 | row_label = c.chunk() 240 | data = c.chunk(len(self._columns)) 241 | yield row_label, data 242 | 243 | def data(self): 244 | d = {} 245 | for row, data in self._rows(): 246 | d[row] = {} 247 | for col, value in zip(self._columns, data): 248 | d[row][col] = value 249 | if self.transposed: 250 | return transpose(d) 251 | else: 252 | return d 253 | 254 | def __repr__(self): 255 | return "<%s: %s>" % (self.__class__.__name__, self.data()) 256 | 257 | 258 | class MatrixData(TabularRecord): 259 | """ 260 | Represents a set matrix data record 261 | """ 262 | 263 | def _rows(self): 264 | for row in self._data: 265 | yield row[0], row[1:] 266 | 267 | def data(self): 268 | d = [] 269 | for row_label, data in self._rows(): 270 | for col, value in zip(self._columns, data): 271 | if value == "+": 272 | if self.transposed: 273 | d.append((col, row_label)) 274 | else: 275 | d.append((row_label, col)) 276 | return d 277 | 278 | 279 | class ParamStmt(AmplyStmt): 280 | """ 281 | Represents a parameter statement 282 | """ 283 | 284 | def __init__(self, tokens): 285 | assert tokens[0] == "param" 286 | self.name = tokens.name 287 | self.records = tokens.records 288 | self.default = tokens.get("default", 0) 289 | self.tokens = tokens 290 | 291 | def __repr__(self): 292 | return "<%s: %s = %s>" % (self.__class__.__name__, self.name, self.records) 293 | 294 | def eval(self, amply): 295 | if self.name in amply.symbols: 296 | obj = amply.symbols[self.name] 297 | assert isinstance(obj, ParamObject) 298 | else: 299 | obj = ParamObject() 300 | 301 | if obj.subscripts == 0: 302 | if len(self.records) != 1: 303 | raise AmplyError( 304 | "Error in number of records of {} when reading {}".format( 305 | self.name, self.tokens 306 | ) 307 | ) 308 | assert len(self.records[0]) == 1 309 | amply._addSymbol(self.name, self.records[0][0]) 310 | else: 311 | obj.addData(self.records.asList(), default=self.default) 312 | 313 | amply._addSymbol(self.name, obj) 314 | 315 | 316 | class Chunker(object): 317 | """ 318 | Chunker class - used to consume tuples from 319 | an iterator 320 | """ 321 | 322 | def __init__(self, it): 323 | """ 324 | it is a sequence or iterator 325 | """ 326 | 327 | self.it = iter(it) 328 | self.empty = False 329 | self.next = None 330 | self._getNext() 331 | 332 | def _getNext(self): 333 | """ 334 | basically acts as a 1 element buffer so that 335 | we can detect if we've reached the end of the 336 | iterator 337 | """ 338 | 339 | old = self.next 340 | try: 341 | self.next = next(self.it) 342 | except StopIteration: 343 | self.empty = True 344 | return old 345 | 346 | def notEmpty(self): 347 | """ 348 | Test if the iterator has reached the end 349 | """ 350 | 351 | return not self.empty 352 | 353 | def chunk(self, n=None): 354 | """ 355 | Return a list with the next n elements from the iterator, 356 | or the next element if n is None 357 | """ 358 | if n is None: 359 | return self._getNext() 360 | return [self._getNext() for i in range(n)] 361 | 362 | 363 | class ParamTabbingStmt(AmplyStmt): 364 | """ 365 | Represents a parameter tabbing data statement 366 | """ 367 | 368 | def __init__(self, tokens): 369 | assert tokens[0] == "param" 370 | 371 | self.default = tokens.get("default", 0) 372 | self.params = tokens.params 373 | self.data = tokens.data 374 | 375 | def eval(self, amply): 376 | for i, param_name in enumerate(self.params): 377 | if param_name in amply.symbols: 378 | obj = amply.symbols[param_name] 379 | else: 380 | raise AmplyError("Param %s not previously defined" % param_name) 381 | 382 | for subs, data in self._rows(obj.subscripts): 383 | obj.setValue(subs, data[i]) 384 | 385 | def _rows(self, n_subscripts): 386 | c = Chunker(self.data) 387 | while c.notEmpty(): 388 | subscripts = c.chunk(n_subscripts) 389 | data = c.chunk(len(self.params)) 390 | yield (subscripts, data) 391 | 392 | 393 | class ParamDefStmt(AmplyStmt): 394 | """ 395 | Represents a parameter definition 396 | """ 397 | 398 | def __init__(self, tokens): 399 | assert tokens[0] == "param" 400 | self.name = tokens.get("name") 401 | self.subscripts = tokens.get("subscripts") 402 | self.default = tokens.get("default", NoDefault) 403 | 404 | def eval(self, amply): 405 | def _getDimen(symbol): 406 | s = amply[symbol] 407 | if s is None or s.dimen is None: 408 | return 1 409 | return s.dimen 410 | 411 | try: 412 | num_subscripts = sum(_getDimen(s) for s in self.subscripts) 413 | except TypeError: 414 | num_subscripts = 1 415 | amply._addSymbol(self.name, ParamObject(num_subscripts, self.default)) 416 | 417 | 418 | class ParamObject(AmplyObject): 419 | def __init__(self, subscripts=0, default=NoDefault): 420 | self.subscripts = subscripts 421 | self.default = default 422 | 423 | self.data = {} 424 | 425 | # initial slice is all *'s 426 | self._setSlice(SliceRecord(["*"] * self.subscripts)) 427 | 428 | def addData(self, data, default=0): 429 | def _v(v): 430 | if v == ".": 431 | return default 432 | return v 433 | 434 | for record in data: 435 | if isinstance(record, SliceRecord): 436 | self._setSlice(record) 437 | elif isinstance(record, list): 438 | # a plain data record 439 | rec_len = len(self.free_indices) + 1 440 | if len(record) % rec_len != 0: 441 | raise AmplyError( 442 | "Incomplete data record, expecting %d" 443 | " subscripts per value" % len(self.free_indices) 444 | ) 445 | for c in chunk(record, len(self.free_indices) + 1): 446 | self.setValue(c[:-1], _v(c[-1])) 447 | elif isinstance(record, TabularRecord): 448 | record_data = record.data() 449 | for row_symbol in record_data: 450 | for col_symbol, value in list(record_data[row_symbol].items()): 451 | self.setValue((row_symbol, col_symbol), _v(value)) 452 | 453 | def _setSlice(self, slice): 454 | self.current_slice = list(slice.components) # copy 455 | self.free_indices = [i for i, v in enumerate(self.current_slice) if v == "*"] 456 | 457 | def setValue(self, symbols, value): 458 | if value == ".": 459 | value = self.default 460 | 461 | assert len(symbols) == len(self.free_indices) 462 | symbol_path = self.current_slice 463 | for index, symbol in zip(self.free_indices, symbols): 464 | symbol_path[index] = symbol 465 | 466 | curr_dict = self.data 467 | for symbol in symbol_path[:-1]: 468 | if symbol not in curr_dict: 469 | curr_dict[symbol] = {} 470 | curr_dict = curr_dict[symbol] 471 | curr_dict[symbol_path[-1]] = value 472 | 473 | def __getitem__(self, key): 474 | return access_data(self.data, key, self.default) 475 | 476 | def __repr__(self): 477 | return "<%s: %s>" % (self.__class__.__name__, self.data) 478 | 479 | def __eq__(self, other): 480 | return self.data == other 481 | 482 | def __ne__(self, other): 483 | return self.data != other 484 | 485 | 486 | class SetObject(AmplyObject): 487 | def __init__(self, subscripts=0, dimen=None): 488 | self.dimen = dimen 489 | self.subscripts = subscripts 490 | 491 | if self.subscripts == 0: 492 | self.data = [] 493 | else: 494 | self.data = {} 495 | 496 | self.current_slice = None 497 | 498 | def addData(self, member, data): 499 | dest_list = self._memberList(member) 500 | 501 | if self.dimen is not None and self.current_slice is None: 502 | self._setSlice(["*"] * self.dimen) 503 | 504 | for record in data: 505 | if isinstance(record, SliceRecord): 506 | self._setSlice(record.components) 507 | elif isinstance(record, MatrixData): 508 | if self.dimen is None: 509 | self.dimen = 2 510 | self._setSlice(["*"] * 2) 511 | d = record.data() 512 | for v in d: 513 | self._addValue(dest_list, v) 514 | 515 | else: # simple-data 516 | self._addSimpleData(dest_list, record) 517 | 518 | def _setSlice(self, slice): 519 | self.current_slice = slice 520 | self.free_indices = [i for i, v in enumerate(self.current_slice) if v == "*"] 521 | 522 | def _memberList(self, member): 523 | if member is None: 524 | return self.data 525 | assert len(member) == self.subscripts 526 | 527 | curr_dict = self.data 528 | for symbol in member[:-1]: 529 | if symbol not in curr_dict: 530 | curr_dict[symbol] = {} 531 | curr_dict = curr_dict[symbol] 532 | if member[-1] not in curr_dict: 533 | curr_dict[member[-1]] = [] 534 | return curr_dict[member[-1]] 535 | 536 | def _dataLen(self, d): 537 | if isinstance(d, (tuple, list)): 538 | return len(d) 539 | return 1 540 | 541 | def _addSimpleData(self, data_list, data): 542 | if isinstance(data[0], ParseResults): 543 | inferred_dimen = len(data[0]) 544 | else: 545 | inferred_dimen = 1 546 | 547 | if self.dimen is None: 548 | # infer dimension from records 549 | self.dimen = inferred_dimen 550 | 551 | if self.current_slice is None: 552 | self._setSlice(tuple(["*"] * self.dimen)) 553 | 554 | if len(self.free_indices) == inferred_dimen: 555 | for d in data.asList(): 556 | self._addValue(data_list, d) 557 | elif len(self.free_indices) > 1 and inferred_dimen: 558 | for c in chunk(data, len(self.free_indices)): 559 | self._addValue(data_list, c) 560 | else: 561 | raise AmplyError( 562 | "Dimension of elements (%d) does not match " 563 | "declared dimension, (%d)" % (inferred_dimen, self.dimen) 564 | ) 565 | 566 | def _addValue(self, data_list, item): 567 | if self.dimen == 1: 568 | data_list.append(item) 569 | else: 570 | assert len(self.free_indices) == self._dataLen(item) 571 | 572 | to_add = list(self.current_slice) 573 | if isinstance(item, (tuple, list)): 574 | for index, value in zip(self.free_indices, item): 575 | to_add[index] = value 576 | else: 577 | assert len(self.free_indices) == 1 578 | to_add[self.free_indices[0]] = item 579 | data_list.append(tuple(to_add)) 580 | 581 | def __getitem__(self, key): 582 | if not self.subscripts: 583 | return self.data[key] 584 | return access_data(self.data, key) 585 | 586 | def __len__(self): 587 | return len(self.data) 588 | 589 | def __iter__(self): 590 | return iter(self.data) 591 | 592 | def __contains__(self, item): 593 | return item in self.data 594 | 595 | def __eq__(self, other): 596 | return self.data == other 597 | 598 | def __ne__(self, other): 599 | return self.data != other 600 | 601 | def __repr__(self): 602 | return "<%s: %s>" % (self.__class__.__name__, self.data) 603 | 604 | 605 | def mark_transposed(tokens): 606 | tokens[0].setTransposed(True) 607 | return tokens 608 | 609 | 610 | # What follows is a Pyparsing description of the grammar 611 | 612 | index = Word(alphanums, exact=1) 613 | symbol = Word(alphanums + "_", bodyChars=alphanums + "_", min=1) 614 | sign = Optional(oneOf("+ -")) 615 | integer = Combine(sign + Word(nums)).setParseAction(lambda t: int(t[0])) 616 | number = Combine( 617 | Word("+-" + nums, nums) 618 | + Optional("." + Optional(Word(nums))) 619 | + Optional(oneOf("e E") + Word("+-" + nums, nums)) 620 | ).setParseAction(lambda t: float(t[0])) 621 | 622 | LPAREN = Suppress("(") 623 | RPAREN = Suppress(")") 624 | LBRACE = Suppress("{") 625 | RBRACE = Suppress("}") 626 | LBRACKET = Suppress("[") 627 | RBRACKET = Suppress("]") 628 | END = Suppress(";") 629 | 630 | PLUS = Literal("+") 631 | MINUS = Literal("-") 632 | 633 | # Keywords 634 | KW_PARAM = Keyword("param") 635 | KW_SET = Keyword("set") 636 | KW_DEFAULT = Keyword("default") 637 | 638 | single = number ^ symbol | QuotedString('"') | QuotedString("'") 639 | tuple_ = Group(LPAREN + delimitedList(single) + RPAREN) 640 | 641 | domain_index = Suppress(index + Keyword("in")) 642 | subscript_domain = ( 643 | LBRACE 644 | + delimitedList(Optional(domain_index) + symbol).setResultsName("subscripts") 645 | + RBRACE 646 | ) 647 | 648 | data = single | tuple_ 649 | 650 | # should not match a single (tr) 651 | simple_data = Group(NotAny("(tr)") + data + ZeroOrMore(Optional(Suppress(",")) + data)) 652 | # the first element of a set data record cannot be 'dimen', or else 653 | # these would match set_def_stmts 654 | non_dimen_simple_data = ~Keyword("dimen") + simple_data 655 | 656 | matrix_row = Group(single + OneOrMore(PLUS | MINUS)) 657 | matrix_data = ( 658 | ":" 659 | + OneOrMore(single).setResultsName("columns") 660 | + ":=" 661 | + OneOrMore(matrix_row).setResultsName("data") 662 | ) 663 | matrix_data.setParseAction(MatrixData) 664 | 665 | tr_matrix_data = Suppress("(tr)") + matrix_data 666 | tr_matrix_data.setParseAction(mark_transposed) 667 | 668 | set_slice_component = number | symbol | "*" 669 | set_slice_record = LPAREN + NotAny("tr") + delimitedList(set_slice_component) + RPAREN 670 | set_slice_record.setParseAction(SliceRecord) 671 | 672 | _set_record = set_slice_record | matrix_data | tr_matrix_data | Suppress(":=") 673 | set_record = simple_data | _set_record 674 | non_dimen_set_record = non_dimen_simple_data | _set_record 675 | 676 | set_def_stmt = ( 677 | KW_SET 678 | + symbol 679 | + Optional(subscript_domain) 680 | + Optional(Keyword("dimen") + integer.setResultsName("dimen")) 681 | + END 682 | ) 683 | set_def_stmt.setParseAction(SetDefStmt) 684 | 685 | set_member = LBRACKET + delimitedList(data) + RBRACKET 686 | 687 | set_stmt = ( 688 | KW_SET 689 | + symbol 690 | + Optional(set_member).setResultsName("member") 691 | + Group( 692 | non_dimen_set_record + ZeroOrMore(Optional(Suppress(",")) + set_record) 693 | ).setResultsName("records") 694 | + END 695 | ) 696 | set_stmt.setParseAction(SetStmt) 697 | 698 | subscript = single 699 | 700 | param_data = data | "." 701 | plain_data = ( 702 | param_data 703 | | subscript + ZeroOrMore(Optional(Suppress(",")) + subscript) + param_data 704 | ) 705 | # should not match a single (tr) 706 | plain_data_record = Group( 707 | NotAny("(tr)") + plain_data + NotAny(plain_data) 708 | | plain_data + OneOrMore(plain_data) + NotAny(plain_data) 709 | ) 710 | 711 | tabular_record = ( 712 | ":" 713 | + OneOrMore(single).setResultsName("columns") 714 | + ":=" 715 | + OneOrMore(single | ".").setResultsName("data") 716 | ) 717 | tabular_record.setParseAction(TabularRecord) 718 | 719 | tr_tabular_record = Suppress("(tr)") + tabular_record 720 | tr_tabular_record.setParseAction(mark_transposed) 721 | 722 | param_slice_component = number | symbol | "*" 723 | param_slice_record = LBRACKET + delimitedList(param_slice_component) + RBRACKET 724 | param_slice_record.setParseAction(SliceRecord) 725 | 726 | param_record = ( 727 | param_slice_record 728 | | plain_data_record 729 | | tabular_record 730 | | tr_tabular_record 731 | | Suppress(":=") 732 | ) 733 | 734 | param_default = Optional(KW_DEFAULT + single.setResultsName("default")) 735 | 736 | param_stmt = ( 737 | KW_PARAM 738 | + ~KW_DEFAULT 739 | + symbol.setResultsName("name") 740 | + param_default 741 | + Group(OneOrMore(param_record)).setResultsName("records") 742 | + END 743 | ) 744 | param_stmt.setParseAction(ParamStmt) 745 | 746 | param_tabbing_stmt = ( 747 | KW_PARAM 748 | + param_default 749 | + ":" 750 | + Optional(symbol + ": ") 751 | + OneOrMore(data).setResultsName("params") 752 | + ":=" 753 | + ZeroOrMore(single).setResultsName("data") 754 | + END 755 | ) 756 | param_tabbing_stmt.setParseAction(ParamTabbingStmt) 757 | 758 | param_def_stmt = ( 759 | KW_PARAM 760 | + symbol.setResultsName("name") 761 | + Optional(subscript_domain) 762 | + param_default 763 | + END 764 | ) 765 | param_def_stmt.setParseAction(ParamDefStmt) 766 | 767 | stmts = set_stmt | set_def_stmt | param_def_stmt | param_stmt | param_tabbing_stmt 768 | grammar = ZeroOrMore(stmts) + StringEnd() 769 | grammar.ignore("#" + SkipTo(lineEnd)) 770 | grammar.ignore("end;" + SkipTo(lineEnd)) 771 | 772 | 773 | class Amply(object): 774 | """ 775 | Data parsing interface 776 | """ 777 | 778 | def __init__(self, s=""): 779 | """ 780 | Create an Amply parser instance 781 | 782 | @param s (default ""): initial string to parse 783 | """ 784 | 785 | self.symbols = {} 786 | 787 | self.load_string(s) 788 | 789 | def __getitem__(self, key): 790 | """ 791 | Override so that symbols can be accessed using 792 | [] subscripts 793 | """ 794 | if key in self.symbols: 795 | return self.symbols[key] 796 | 797 | def __getattr__(self, name): 798 | """ 799 | Override so that symbols can be accessed as attributes 800 | """ 801 | if name in self.symbols: 802 | return self.symbols[name] 803 | return super(Amply, self).__getattr__(name) 804 | 805 | def _addSymbol(self, name, value): 806 | """ 807 | Adds a symbol to this instance. 808 | 809 | Typically, this class is called by objects created by 810 | the parser, and should not need to be called by users 811 | directly 812 | """ 813 | 814 | self.symbols[name] = value 815 | 816 | def load_string(self, string): 817 | """ 818 | Load and parse string 819 | 820 | @param string string to parse 821 | """ 822 | try: 823 | for obj in grammar.parseString(string): 824 | obj.eval(self) 825 | except ParseException as ex: 826 | print(string) 827 | raise ParseException(ex) 828 | 829 | def load_file(self, f): 830 | """ 831 | Load and parse file 832 | 833 | @param f file-like object 834 | """ 835 | self.load_string(f.read()) 836 | 837 | @staticmethod 838 | def from_file(f): 839 | """ 840 | Create a new Amply instance from file (factory method) 841 | 842 | @param f file-like object 843 | """ 844 | return Amply(f.read()) 845 | 846 | 847 | if __name__ == "__main__": 848 | 849 | grammar.create_diagram("parser_rr_diag.html") 850 | --------------------------------------------------------------------------------