pax_global_header00006660000000000000000000000064144443436700014523gustar00rootroot0000000000000052 comment=47db97130c2d9a37b1b97daa9d9699d94e1ed165 amply-0.1.6/000077500000000000000000000000001444434367000126515ustar00rootroot00000000000000amply-0.1.6/.github/000077500000000000000000000000001444434367000142115ustar00rootroot00000000000000amply-0.1.6/.github/workflows/000077500000000000000000000000001444434367000162465ustar00rootroot00000000000000amply-0.1.6/.github/workflows/python-package.yml000066400000000000000000000024651444434367000217120ustar00rootroot00000000000000# This workflow will install Python dependencies, run tests and lint with a variety of Python versions # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python name: Python package on: push: branches: [ "master" ] pull_request: branches: [ "master" ] jobs: build: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ["3.8", "3.9", "3.10"] 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 python -m pip install flake8 pytest if [ -f requirements.txt ]; then pip install -r requirements.txt; fi pip install . - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with pytest run: | pytest amply-0.1.6/.github/workflows/release.yml000066400000000000000000000020741444434367000204140ustar00rootroot00000000000000# This workflow will upload a Python Package using Twine when a release is created # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries # This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by # separate terms of service, privacy policy, and support # documentation. name: Upload Python Package on: release: types: [published] permissions: contents: read jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v3 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install build - name: Build package run: python -m build - name: Publish package uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} amply-0.1.6/.gitignore000066400000000000000000000001331444434367000146360ustar00rootroot00000000000000# Python __pycache__ *.pyc .pytest* Amply.egg-info .pypirc build/ dist/ # Editors .vscode amply-0.1.6/.isort.cfg000066400000000000000000000004551444434367000145540ustar00rootroot00000000000000[settings] line_length=88 indent=' ' skip=.tox,.venv,build,dist known_standard_library=setuptools,pkg_resources known_test=pytest known_first_party=amply sections=FUTURE,STDLIB,COMPAT,TEST,THIRDPARTY,FIRSTPARTY,LOCALFOLDER default_section=THIRDPARTY multi_line_output=3 include_trailing_comma=True amply-0.1.6/.pre-commit-config.yaml000066400000000000000000000015101444434367000171270ustar00rootroot00000000000000exclude: '^docs/conf.py' repos: - repo: https://github.com/psf/black rev: 22.3.0 hooks: - id: black language_version: python3.8 - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.2.3 hooks: - id: trailing-whitespace - id: check-added-large-files - id: check-ast - id: check-json - id: check-merge-conflict - id: check-xml - id: check-yaml - id: debug-statements - id: end-of-file-fixer - id: requirements-txt-fixer - id: mixed-line-ending args: ['--fix=no'] - id: flake8 args: ['--max-line-length=88','--extend-ignore=E203'] # default of Black - repo: https://github.com/pre-commit/mirrors-isort rev: v5.10.1 hooks: - id: isort - repo: https://github.com/pre-commit/mirrors-mypy rev: v0.942 # Use the sha / tag you want to point at hooks: - id: mypy amply-0.1.6/.travis.yml000066400000000000000000000030471444434367000147660ustar00rootroot00000000000000language: python dist: xenial os: linux virtualenv: system_site_packages: false jobs: fast_finish: true include: - name: conda_python3.8 env: DISTRIB="conda" PYTHON_VERSION="3.8" COVERAGE="true" install: - source tests/travis_install.sh before_script: - git config --global user.email "wusher@kth.com" - git config --global user.name "Will Usher" script: - pip install . - pytest --cov=amply - | if [[ "$COVERAGE" == "true" ]]; then pre-commit install pre-commit run --files src fi after_success: - if [[ "$COVERAGE" == "true" ]]; then coveralls || echo "failed"; fi after_script: - travis-cleanup cache: pip: true directories: - "$HOME/miniconda" notifications: email: false deploy: edge: true distributions: "sdist bdist_wheel" provider: pypi username: __token__ password: secure: Gbi2YhW/BzJE5Tnj/TVkyMG/yqz7kiucxiGT57Q8xcCZovlIXROgtN7BqFxo6P9OZEoP9iGemFEAyvh2qorIolWNBGp5vQynMoJbXTACvA1CTaz4ZPmYZXWB7IPKyHBkUDWLiU1votmvUkI7kwOcgcV6HlxI/IP1HiJUVEL1FSPv52BWR6AD4UBtSQX3sTWqKHr4hUSaxUDauuAH1EUXAWYUNBsJOnjwhTNXYMTyvFNWCqiqJjiNbjbQ2evNCKk7e9s8Vkxy2WBa7wjxyLw93KrXEVVADDr59FLESqDekftKfTcctPBKE4wLw3/vt1O2cFN/h81ASSW1iAmQsBQnR+E066SUHu2t6tpkdFQzMkQdRdE/w74qyC08QOrJrQV9TIBWu9VdVB/mSLsmszSZNGPWhKcEpiO/U47lkjfhl0IEwjjxVmaXlE8tepZZq2raAD5OVRXyjfEKocbXS7HD5t0FZUH6B3GzdoDFhpMt2aceXjUVchOHYs6kdQjz19erK7WGJPA1enta4DcAiufh+1+SckqMytY0ksA6vDoAaHJ6++/pugpPvJr8Gt8fF7BlgTG1ojLEYx+Mr5nsg/51yPAWNmgaUttWAbsloYzS9PnjlyM4WY7CBvVlQtPczVyEtmqGGIwyhVtoVba1Cbap5tGtlvEi2jJkif8TFA9Vow4= on: tags: true all_branches: true cleanup: false amply-0.1.6/AUTHORS000066400000000000000000000003511444434367000137200ustar00rootroot00000000000000Will Usher Christophe-Marie Duquesne Stuart Mitchell Franco Peschiera Stu smit023 Q. Lim amply-0.1.6/INSTALL000066400000000000000000000001501444434367000136760ustar00rootroot00000000000000Amply is a Python package which can be installed by PyPI using Pip. For example: pip install amplyamply-0.1.6/LICENSE000066400000000000000000000260701444434367000136630ustar00rootroot00000000000000Eclipse Public License - v 1.0 THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 1. DEFINITIONS "Contribution" means: a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and b) in the case of each subsequent Contributor: i) changes to the Program, and ii) additions to the Program; where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program. "Contributor" means any person or entity that distributes the Program. "Licensed Patents" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. "Program" means the Contributions distributed in accordance with this Agreement. "Recipient" means anyone who receives the Program under this Agreement, including all Contributors. 2. GRANT OF RIGHTS a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form. b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. 3. REQUIREMENTS A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that: a) it complies with the terms and conditions of this Agreement; and b) its license agreement: i) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange. When the Program is made available in source code form: a) it must be made available under this Agreement; and b) a copy of this Agreement must be included with each copy of the Program. Contributors may not remove or alter any copyright notices contained within the Program. Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution. 4. COMMERCIAL DISTRIBUTION Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. 5. NO WARRANTY EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement, including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. 6. DISCLAIMER OF LIABILITY EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), 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 OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 7. GENERAL If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party waives its rights to a jury trial in any resulting litigation. amply-0.1.6/README.rst000066400000000000000000000347601444434367000143520ustar00rootroot00000000000000Amply ====== .. image:: https://travis-ci.com/willu47/amply.svg?branch=master :target: https://travis-ci.com/willu47/amply .. image:: https://img.shields.io/pypi/v/amply?style=plastic :alt: PyPI :target: https://pypi.org/project/amply/ .. image:: https://coveralls.io/repos/github/willu47/amply/badge.svg?branch=master :target: https://coveralls.io/github/willu47/amply?branch=master Introduction ------------ Amply allows you to load and manipulate AMPL data as Python data structures. Amply only supports a specific subset of the AMPL syntax: * set declarations * set data statements * parameter declarations * parameter data statements Declarations and data statements ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Typically, problems expressed in AMPL consist of two parts, a *model* section and a *data* section. Amply is only designed to parse the parameter and set statements contained within AMPL data sections. However, in order to parse these statements correctly, information that would usually be contained within the model section may be required. For instance, it may not be possible to infer the dimension of a set purely from its data statement. Therefore, Amply also supports set and parameter declarations. These do not have to be put in a separate section, they only need to occur before the corresponding data statement. The declaration syntax supported is extremely limited, and does not include most elements of the AMPL programming language. The intention is that this library is used as a way of loading data specified in an AMPL-like syntax. Furthermore, Amply does not perform any validation on data statements. About this document ^^^^^^^^^^^^^^^^^^^^ This document is intended as a guide to the syntax supported by Amply, and not as a general AMPL reference manual. For more in depth coverage see the `GNU MathProg manual, Chapter 5: Model data `_ or the following links: * `Sets in AMPL `_ * `Parameters in AMPL `_ Quickstart Guide ---------------- >>> from amply import Amply Import the class: :: >>> from amply import Amply A simple set. Sets behave a lot like lists. >>> data = Amply("set CITIES := Auckland Wellington Christchurch;") >>> print data.CITIES >>> print data['CITIES'] >>> for c in data.CITIES: print c ... Auckland Wellington Christchurch >>> print data.CITIES[0] Auckland >>> print len(data.CITIES) 3 Data can be integers, reals, symbolic, or quoted strings: >>> data = Amply(""" ... set BitsNPieces := 0 3.2 -6e4 Hello "Hello, World!"; ... """) >>> print data.BitsNPieces Sets can contain multidimensional data, but we have to declare them to be so first. >>> data = Amply(""" ... set pairs dimen 2; ... set pairs := (1, 2) (2, 3) (3, 4); ... """) >>> print data.pairs Sets themselves can be multidimensional (i.e. be subscriptable): >>> data = Amply(""" ... set CITIES{COUNTRIES}; ... set CITIES[Australia] := Adelaide Melbourne Sydney; ... set CITIES[Italy] := Florence Milan Rome; ... """) >>> print data.CITIES['Australia'] ['Adelaide', 'Melbourne', 'Sydney'] >>> print data.CITIES['Italy'] ['Florence', 'Milan', 'Rome'] Note that in the above example, the set COUNTRIES didn't actually have to exist itself. Amply does not perform any validation on subscripts, it only uses them to figure out how many subscripts a set has. To specify more than one, separate them by commas: >>> data = Amply(""" ... set SUBURBS{COUNTRIES, CITIES}; ... set SUBURBS[Australia, Melbourne] := Docklands 'South Wharf' Kensington; ... """) >>> print data.SUBURBS['Australia', 'Melbourne'] ['Docklands', 'South Wharf', 'Kensington'] *Slices* can be used to simplify the entry of multi-dimensional data. >>> data=Amply(""" ... set TRIPLES dimen 3; ... set TRIPLES := (1, 1, *) 2 3 4 (*, 2, *) 6 7 8 9 (*, *, *) (1, 1, 1); ... """) >>> print data.TRIPLES > Set data can also be specified using a matrix notation. A '+' indicates that the pair is included in the set whereas a '-' indicates a pair not in the set. >>> data=Amply(""" ... set ROUTES dimen 2; ... set ROUTES : A B C D := ... E + - - + ... F + + - - ... ; ... """) >>> print data.ROUTES Matrices can also be transposed: >>> data=Amply(""" ... set ROUTES dimen 2; ... set ROUTES (tr) : E F := ... A + + ... B - + ... C - - ... D + - ... ; ... """) >>> print data.ROUTES Matrices only specify 2d data, however they can be combined with slices to define higher-dimensional data: >>> data = Amply(""" ... set QUADS dimen 2; ... set QUADS := ... (1, 1, *, *) : 2 3 4 := ... 2 + - + ... 3 - + + ... (1, 2, *, *) : 2 3 4 := ... 2 - + - ... 3 + - - ... ; ... """) >>> print data.QUADS Parameters are also supported: >>> data = Amply(""" ... param T := 30; ... param n := 5; ... """) >>> print data.T 30 >>> print data.n 5 Parameters are commonly indexed over sets. No validation is done by Amply, and the sets do not have to exist. Parameter objects are represented as a mapping. >>> data = Amply(""" ... param COSTS{PRODUCTS}; ... param COSTS := ... FISH 8.5 ... CARROTS 2.4 ... POTATOES 1.6 ... ; ... """) >>> print data.COSTS >>> print data.COSTS['FISH'] 8.5 Parameter data statements can include a *default* clause. If a '.' is included in the data, it is replaced with the default value: >>> data = Amply(""" ... param COSTS{P}; ... param COSTS default 2 := ... F 2 ... E 1 ... D . ... ; ... """) >>> print data.COSTS['D'] 2.0 Parameter declarations can also have a default clause. For these parameters, any attempt to access the parameter for a key that has not been defined will return the default value: >>> data = Amply(""" ... param COSTS{P} default 42; ... param COSTS := ... F 2 ... E 1 ... ; ... """) >>> print data.COSTS['DOES NOT EXIST'] 42.0 Parameters can be indexed over multiple sets. The resulting values can be accessed by treating the parameter object as a nested dictionary, or by using a tuple as an index: >>> data = Amply(""" ... param COSTS{CITIES, PRODUCTS}; ... param COSTS := ... Auckland FISH 5 ... Auckland CHIPS 3 ... Wellington FISH 4 ... Wellington CHIPS 1 ... ; ... """) >>> print data.COSTS >>> print data.COSTS['Wellington']['CHIPS'] # nested dict 1.0 >>> print data.COSTS['Wellington', 'CHIPS'] # tuple as key 1.0 Parameters support a slice syntax similar to that of sets: >>> data = Amply(""" ... param COSTS{CITIES, PRODUCTS}; ... param COSTS := ... [Auckland, * ] ... FISH 5 ... CHIPS 3 ... [Wellington, * ] ... FISH 4 ... CHIPS 1 ... ; ... """) >>> print data.COSTS Parameters indexed over two sets can also be specified in tabular format: >>> data = Amply(""" ... param COSTS{CITIES, PRODUCTS}; ... param COSTS: FISH CHIPS := ... Auckland 5 3 ... Wellington 4 1 ... ; ... """) >>> print data.COSTS Tabular data can also be transposed: >>> data = Amply(""" ... param COSTS{CITIES, PRODUCTS}; ... param COSTS (tr): Auckland Wellington := ... FISH 5 4 ... CHIPS 3 1 ... ; ... """) >>> print data.COSTS Slices can be combined with tabular data for parameters indexed over more than 2 sets: >>> data = Amply(""" ... param COSTS{CITIES, PRODUCTS, SIZE}; ... param COSTS := ... [Auckland, *, *] : SMALL LARGE := ... FISH 5 9 ... CHIPS 3 5 ... [Wellington, *, *] : SMALL LARGE := ... FISH 4 7 ... CHIPS 1 2 ... ; ... """) >>> print data.COSTS =42", "wheel", "setuptools_scm[toml]>=3.4"] build-backend = "setuptools.build_meta" [tool.setuptools_scm] amply-0.1.6/requirements.txt000066400000000000000000000000121444434367000161260ustar00rootroot00000000000000pyparsing amply-0.1.6/setup.py000066400000000000000000000030321444434367000143610ustar00rootroot00000000000000from setuptools import setup, find_packages with open("README.rst", "r") as fh: long_description = fh.read() with open("AUTHORS", "r") as fh: authors = [] text_authors = fh.readlines() for author in text_authors: authors.append(author.split("<")[0].strip()) setup( name="amply", packages=find_packages("src"), license='Eclipse Public License 1.0 (EPL-1.0)', use_scm_version=True, setup_requires=["setuptools_scm"], # Project uses reStructuredText, so ensure that the docutils get # installed or upgraded on the target machine install_requires=["docutils>=0.3", "pyparsing"], package_dir={"": "src"}, package_data={ # If any package contains *.txt or *.rst files, include them: "": ["*.txt", "*.rst"], }, # metadata to display on PyPI author=",".join(authors), author_email="wusher@kth.se", description="Amply allows you to load and manipulate AMPL/GLPK data as Python data structures", long_description_content_type="text/x-rst", long_description=long_description, keywords="ampl gmpl", url="http://github.com/willu47/amply", # project home page, if any project_urls={ "Bug Tracker": "http://github.com/willu47/amply/issues", "Documentation": "http://github.com/willu47/amply/README.rst", "Source Code": "http://github.com/willu47/amply", }, classifiers=[ "License :: OSI Approved :: Eclipse Public License 1.0 (EPL-1.0)" ], python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', ) amply-0.1.6/src/000077500000000000000000000000001444434367000134405ustar00rootroot00000000000000amply-0.1.6/src/amply/000077500000000000000000000000001444434367000145625ustar00rootroot00000000000000amply-0.1.6/src/amply/__init__.py000066400000000000000000000001101444434367000166630ustar00rootroot00000000000000from .amply import Amply, AmplyError __all__ = ['Amply', 'AmplyError'] amply-0.1.6/src/amply/amply.py000066400000000000000000000535251444434367000162700ustar00rootroot00000000000000#! /usr/bin/env python """ Amply: a GNU MathProg data-parser This module implements a parser for a subset of the GNU MathProg language, namely parameter and set data records. Amply uses the Pyparsing library to parse input: http://pyparsing.wikispaces.com Usage: Create an Amply object, optionally passing in a string to parse. >>> a = Amply("param T := 3;") Symbols that are defined can be accessed as attributes or items. >>> print(a.T) 3.0 >>> print(a['T']) 3.0 The load_string and load_file methods can be used to parse additional data >>> a.load_string("set N := 1 2 3;") >>> a.load_file(open('some_file.dat')) An Amply object can be constructed from a file using Amply.from_file >>> a = Amply.from_file(open('some_file.dat')) How it works: The Amply class parses the input using Pyparsing. This results in a list of Stmt objects, each representing a MathProg statement. The statements are then evaluated by calling their eval() method. """ from pyparsing import ( Combine, Group, Keyword, Literal, NotAny, OneOrMore, Optional, ParseException, ParserElement, ParseResults, QuotedString, SkipTo, StringEnd, Suppress, Word, ZeroOrMore, alphanums, delimitedList, lineEnd, nums, oneOf, ) ParserElement.enablePackrat() __all__ = ["Amply", "AmplyError"] class AmplyObject(object): """ Represents the value of some object (e.g. a Set object or Parameter object """ class AmplyStmt(object): """ Represents a statement that has been parsed Statements implement an eval method. When the eval method is called, the Stmt object is responsible for modifying the Amply object that gets passed in appropriately (i.e. by adding or modifying a symbol) """ def eval(self, amply): # pragma: no coverage raise NotImplementedError() class NoDefault(object): """ Sentinel """ class AmplyError(Exception): """ Amply Exception Class """ def chunk(it, n): """ Yields n-tuples from iterator """ c = [] for i, x in enumerate(it): c.append(x) if (i + 1) % n == 0: yield tuple(c) c = [] if c: yield tuple(c) def access_data(curr_dict, keys, default=NoDefault): """ Convenience method for walking down a series of nested dictionaries keys is a tuple of strings access_data(dict, ('key1', 'key2', 'key3') is equivalent to dict['key1']['key2']['key3'] All dictionaries must exist, but the last dictionary in the hierarchy does not have to contain the final key, if default is set. """ if keys in curr_dict: return curr_dict[keys] if isinstance(keys, tuple): for sym in keys[:-1]: curr_dict = curr_dict[sym] r = curr_dict.get(keys[-1], default) if r is not NoDefault: return r if default is not NoDefault: return default raise KeyError() def transpose(data): """ Transpose a matrix represented as a dict of dicts """ rows = list(data.keys()) cols = set() for d in list(data.values()): cols.update(list(d.keys())) d = {} for col in cols: d[col] = {} for row in rows: d[col][row] = data[row][col] return d class SetDefStmt(AmplyStmt): """ Represents a set definition statement """ def __init__(self, tokens): assert tokens[0] == "set" self.name = tokens[1] self.dimen = tokens.get("dimen", None) self.subscripts = len(tokens.get("subscripts", ())) def __repr__(self): # pragma: no cover return "<%s: %s[%s]>" % (self.__class__.__name__, self.name, self.dimen) def eval(self, amply): set_obj = SetObject(subscripts=self.subscripts, dimen=self.dimen) amply._addSymbol(self.name, set_obj) class SetStmt(AmplyStmt): """ Represents a set statement """ def __init__(self, tokens): assert tokens[0] == "set" self.name = tokens[1] self.records = tokens.get("records") self.member = tokens.get("member", None) def __repr__(self): return "<%s: %s[%s] = %s>" % ( self.__class__.__name__, self.name, self.member, self.records, ) def eval(self, amply): if self.name in amply.symbols: obj = amply.symbols[self.name] assert isinstance(obj, SetObject) else: obj = SetObject() obj.addData(self.member, self.records) amply._addSymbol(self.name, obj) class SliceRecord(object): """ Represents a parameter or set slice record """ def __init__(self, tokens): self.components = tuple(tokens) def __repr__(self): return "<%s: %s>" % (self.__class__.__name__, self.components) class TabularRecord(object): """ Represents a parameter tabular record """ def __init__(self, tokens): self._columns = tokens.columns self._data = tokens.data self.transposed = False def setTransposed(self, t): self.transposed = t def _rows(self): c = Chunker(self._data) while c.notEmpty(): row_label = c.chunk() data = c.chunk(len(self._columns)) yield row_label, data def data(self): d = {} for row, data in self._rows(): d[row] = {} for col, value in zip(self._columns, data): d[row][col] = value if self.transposed: return transpose(d) else: return d def __repr__(self): return "<%s: %s>" % (self.__class__.__name__, self.data()) class MatrixData(TabularRecord): """ Represents a set matrix data record """ def _rows(self): for row in self._data: yield row[0], row[1:] def data(self): d = [] for row_label, data in self._rows(): for col, value in zip(self._columns, data): if value == "+": if self.transposed: d.append((col, row_label)) else: d.append((row_label, col)) return d class ParamStmt(AmplyStmt): """ Represents a parameter statement """ def __init__(self, tokens): assert tokens[0] == "param" self.name = tokens.name self.records = tokens.records self.default = tokens.get("default", 0) self.tokens = tokens def __repr__(self): return "<%s: %s = %s>" % (self.__class__.__name__, self.name, self.records) def eval(self, amply): if self.name in amply.symbols: obj = amply.symbols[self.name] assert isinstance(obj, ParamObject) else: obj = ParamObject() if obj.subscripts == 0: if len(self.records) != 1: raise AmplyError( "Error in number of records of {} when reading {}".format( self.name, self.tokens ) ) assert len(self.records[0]) == 1 amply._addSymbol(self.name, self.records[0][0]) else: obj.addData(self.records.asList(), default=self.default) amply._addSymbol(self.name, obj) class Chunker(object): """ Chunker class - used to consume tuples from an iterator """ def __init__(self, it): """ it is a sequence or iterator """ self.it = iter(it) self.empty = False self.next = None self._getNext() def _getNext(self): """ basically acts as a 1 element buffer so that we can detect if we've reached the end of the iterator """ old = self.next try: self.next = next(self.it) except StopIteration: self.empty = True return old def notEmpty(self): """ Test if the iterator has reached the end """ return not self.empty def chunk(self, n=None): """ Return a list with the next n elements from the iterator, or the next element if n is None """ if n is None: return self._getNext() return [self._getNext() for i in range(n)] class ParamTabbingStmt(AmplyStmt): """ Represents a parameter tabbing data statement """ def __init__(self, tokens): assert tokens[0] == "param" self.default = tokens.get("default", 0) self.params = tokens.params self.data = tokens.data def eval(self, amply): for i, param_name in enumerate(self.params): if param_name in amply.symbols: obj = amply.symbols[param_name] else: raise AmplyError("Param %s not previously defined" % param_name) for subs, data in self._rows(obj.subscripts): obj.setValue(subs, data[i]) def _rows(self, n_subscripts): c = Chunker(self.data) while c.notEmpty(): subscripts = c.chunk(n_subscripts) data = c.chunk(len(self.params)) yield (subscripts, data) class ParamDefStmt(AmplyStmt): """ Represents a parameter definition """ def __init__(self, tokens): assert tokens[0] == "param" self.name = tokens.get("name") self.subscripts = tokens.get("subscripts") self.default = tokens.get("default", NoDefault) def eval(self, amply): def _getDimen(symbol): s = amply[symbol] if s is None or s.dimen is None: return 1 return s.dimen try: num_subscripts = sum(_getDimen(s) for s in self.subscripts) except TypeError: num_subscripts = 1 amply._addSymbol(self.name, ParamObject(num_subscripts, self.default)) class ParamObject(AmplyObject): def __init__(self, subscripts=0, default=NoDefault): self.subscripts = subscripts self.default = default self.data = {} # initial slice is all *'s self._setSlice(SliceRecord(["*"] * self.subscripts)) def addData(self, data, default=0): def _v(v): if v == ".": return default return v for record in data: if isinstance(record, SliceRecord): self._setSlice(record) elif isinstance(record, list): # a plain data record rec_len = len(self.free_indices) + 1 if len(record) % rec_len != 0: raise AmplyError( "Incomplete data record, expecting %d" " subscripts per value" % len(self.free_indices) ) for c in chunk(record, len(self.free_indices) + 1): self.setValue(c[:-1], _v(c[-1])) elif isinstance(record, TabularRecord): record_data = record.data() for row_symbol in record_data: for col_symbol, value in list(record_data[row_symbol].items()): self.setValue((row_symbol, col_symbol), _v(value)) def _setSlice(self, slice): self.current_slice = list(slice.components) # copy self.free_indices = [i for i, v in enumerate(self.current_slice) if v == "*"] def setValue(self, symbols, value): if value == ".": value = self.default assert len(symbols) == len(self.free_indices) symbol_path = self.current_slice for index, symbol in zip(self.free_indices, symbols): symbol_path[index] = symbol curr_dict = self.data for symbol in symbol_path[:-1]: if symbol not in curr_dict: curr_dict[symbol] = {} curr_dict = curr_dict[symbol] curr_dict[symbol_path[-1]] = value def __getitem__(self, key): return access_data(self.data, key, self.default) def __repr__(self): return "<%s: %s>" % (self.__class__.__name__, self.data) def __eq__(self, other): return self.data == other def __ne__(self, other): return self.data != other class SetObject(AmplyObject): def __init__(self, subscripts=0, dimen=None): self.dimen = dimen self.subscripts = subscripts if self.subscripts == 0: self.data = [] else: self.data = {} self.current_slice = None def addData(self, member, data): dest_list = self._memberList(member) if self.dimen is not None and self.current_slice is None: self._setSlice(["*"] * self.dimen) for record in data: if isinstance(record, SliceRecord): self._setSlice(record.components) elif isinstance(record, MatrixData): if self.dimen is None: self.dimen = 2 self._setSlice(["*"] * 2) d = record.data() for v in d: self._addValue(dest_list, v) else: # simple-data self._addSimpleData(dest_list, record) def _setSlice(self, slice): self.current_slice = slice self.free_indices = [i for i, v in enumerate(self.current_slice) if v == "*"] def _memberList(self, member): if member is None: return self.data assert len(member) == self.subscripts curr_dict = self.data for symbol in member[:-1]: if symbol not in curr_dict: curr_dict[symbol] = {} curr_dict = curr_dict[symbol] if member[-1] not in curr_dict: curr_dict[member[-1]] = [] return curr_dict[member[-1]] def _dataLen(self, d): if isinstance(d, (tuple, list)): return len(d) return 1 def _addSimpleData(self, data_list, data): if isinstance(data[0], ParseResults): inferred_dimen = len(data[0]) else: inferred_dimen = 1 if self.dimen is None: # infer dimension from records self.dimen = inferred_dimen if self.current_slice is None: self._setSlice(tuple(["*"] * self.dimen)) if len(self.free_indices) == inferred_dimen: for d in data.asList(): self._addValue(data_list, d) elif len(self.free_indices) > 1 and inferred_dimen: for c in chunk(data, len(self.free_indices)): self._addValue(data_list, c) else: raise AmplyError( "Dimension of elements (%d) does not match " "declared dimension, (%d)" % (inferred_dimen, self.dimen) ) def _addValue(self, data_list, item): if self.dimen == 1: data_list.append(item) else: assert len(self.free_indices) == self._dataLen(item) to_add = list(self.current_slice) if isinstance(item, (tuple, list)): for index, value in zip(self.free_indices, item): to_add[index] = value else: assert len(self.free_indices) == 1 to_add[self.free_indices[0]] = item data_list.append(tuple(to_add)) def __getitem__(self, key): if not self.subscripts: return self.data[key] return access_data(self.data, key) def __len__(self): return len(self.data) def __iter__(self): return iter(self.data) def __contains__(self, item): return item in self.data def __eq__(self, other): return self.data == other def __ne__(self, other): return self.data != other def __repr__(self): return "<%s: %s>" % (self.__class__.__name__, self.data) def mark_transposed(tokens): tokens[0].setTransposed(True) return tokens # What follows is a Pyparsing description of the grammar index = Word(alphanums, exact=1) symbol = Word(alphanums + "_", bodyChars=alphanums + "_", min=1) sign = Optional(oneOf("+ -")) integer = Combine(sign + Word(nums)).setParseAction(lambda t: int(t[0])) number = Combine( Word("+-" + nums, nums) + Optional("." + Optional(Word(nums))) + Optional(oneOf("e E") + Word("+-" + nums, nums)) ).setParseAction(lambda t: float(t[0])) LPAREN = Suppress("(") RPAREN = Suppress(")") LBRACE = Suppress("{") RBRACE = Suppress("}") LBRACKET = Suppress("[") RBRACKET = Suppress("]") END = Suppress(";") PLUS = Literal("+") MINUS = Literal("-") # Keywords KW_PARAM = Keyword("param") KW_SET = Keyword("set") KW_DEFAULT = Keyword("default") single = number ^ symbol | QuotedString('"') | QuotedString("'") tuple_ = Group(LPAREN + delimitedList(single) + RPAREN) domain_index = Suppress(index + Keyword("in")) subscript_domain = ( LBRACE + delimitedList(Optional(domain_index) + symbol).setResultsName("subscripts") + RBRACE ) data = single | tuple_ # should not match a single (tr) simple_data = Group(NotAny("(tr)") + data + ZeroOrMore(Optional(Suppress(",")) + data)) # the first element of a set data record cannot be 'dimen', or else # these would match set_def_stmts non_dimen_simple_data = ~Keyword("dimen") + simple_data matrix_row = Group(single + OneOrMore(PLUS | MINUS)) matrix_data = ( ":" + OneOrMore(single).setResultsName("columns") + ":=" + OneOrMore(matrix_row).setResultsName("data") ) matrix_data.setParseAction(MatrixData) tr_matrix_data = Suppress("(tr)") + matrix_data tr_matrix_data.setParseAction(mark_transposed) set_slice_component = number | symbol | "*" set_slice_record = LPAREN + NotAny("tr") + delimitedList(set_slice_component) + RPAREN set_slice_record.setParseAction(SliceRecord) _set_record = set_slice_record | matrix_data | tr_matrix_data | Suppress(":=") set_record = simple_data | _set_record non_dimen_set_record = non_dimen_simple_data | _set_record set_def_stmt = ( KW_SET + symbol + Optional(subscript_domain) + Optional(Keyword("dimen") + integer.setResultsName("dimen")) + END ) set_def_stmt.setParseAction(SetDefStmt) set_member = LBRACKET + delimitedList(data) + RBRACKET set_stmt = ( KW_SET + symbol + Optional(set_member).setResultsName("member") + Group( non_dimen_set_record + ZeroOrMore(Optional(Suppress(",")) + set_record) ).setResultsName("records") + END ) set_stmt.setParseAction(SetStmt) subscript = single param_data = data | "." plain_data = ( param_data | subscript + ZeroOrMore(Optional(Suppress(",")) + subscript) + param_data ) # should not match a single (tr) plain_data_record = Group( NotAny("(tr)") + plain_data + NotAny(plain_data) | plain_data + OneOrMore(plain_data) + NotAny(plain_data) ) tabular_record = ( ":" + OneOrMore(single).setResultsName("columns") + ":=" + OneOrMore(single | ".").setResultsName("data") ) tabular_record.setParseAction(TabularRecord) tr_tabular_record = Suppress("(tr)") + tabular_record tr_tabular_record.setParseAction(mark_transposed) param_slice_component = number | symbol | "*" param_slice_record = LBRACKET + delimitedList(param_slice_component) + RBRACKET param_slice_record.setParseAction(SliceRecord) param_record = ( param_slice_record | plain_data_record | tabular_record | tr_tabular_record | Suppress(":=") ) param_default = Optional(KW_DEFAULT + single.setResultsName("default")) param_stmt = ( KW_PARAM + ~KW_DEFAULT + symbol.setResultsName("name") + param_default + Group(OneOrMore(param_record)).setResultsName("records") + END ) param_stmt.setParseAction(ParamStmt) param_tabbing_stmt = ( KW_PARAM + param_default + ":" + Optional(symbol + ": ") + OneOrMore(data).setResultsName("params") + ":=" + ZeroOrMore(single).setResultsName("data") + END ) param_tabbing_stmt.setParseAction(ParamTabbingStmt) param_def_stmt = ( KW_PARAM + symbol.setResultsName("name") + Optional(subscript_domain) + param_default + END ) param_def_stmt.setParseAction(ParamDefStmt) stmts = set_stmt | set_def_stmt | param_def_stmt | param_stmt | param_tabbing_stmt grammar = ZeroOrMore(stmts) + StringEnd() grammar.ignore("#" + SkipTo(lineEnd)) grammar.ignore("end;" + SkipTo(lineEnd)) class Amply(object): """ Data parsing interface """ def __init__(self, s=""): """ Create an Amply parser instance @param s (default ""): initial string to parse """ self.symbols = {} self.load_string(s) def __getitem__(self, key): """ Override so that symbols can be accessed using [] subscripts """ if key in self.symbols: return self.symbols[key] def __getattr__(self, name): """ Override so that symbols can be accessed as attributes """ if name in self.symbols: return self.symbols[name] return super(Amply, self).__getattr__(name) def _addSymbol(self, name, value): """ Adds a symbol to this instance. Typically, this class is called by objects created by the parser, and should not need to be called by users directly """ self.symbols[name] = value def load_string(self, string): """ Load and parse string @param string string to parse """ try: for obj in grammar.parseString(string): obj.eval(self) except ParseException as ex: print(string) raise ParseException(ex) def load_file(self, f): """ Load and parse file @param f file-like object """ self.load_string(f.read()) @staticmethod def from_file(f): """ Create a new Amply instance from file (factory method) @param f file-like object """ return Amply(f.read()) if __name__ == "__main__": grammar.create_diagram("parser_rr_diag.html") amply-0.1.6/tests/000077500000000000000000000000001444434367000140135ustar00rootroot00000000000000amply-0.1.6/tests/test_amply.py000066400000000000000000000510021444434367000165440ustar00rootroot00000000000000import pytest import unittest from io import StringIO from amply import amply from amply.amply import ( number, param_def_stmt, param_stmt, set_record, set_stmt, simple_data, single, subscript_domain, symbol, ) class TestSubscript: def test_subscript(self): fixture = """ {a, b, c} {enum} {REGION, YEAR} {REGION, TECHNOLOGY, YEAR} {r in REGION} {r in REGION, y in YEAR, g in GOLF} """ success, result = subscript_domain.runTests(fixture) assert success assert result[0] def test_subscript_result(self): result = subscript_domain.parseString("{a, b, c}") assert result.asDict() == {"subscripts": ["a", "b", "c"]} def test_subscript_result_domain(self): result = subscript_domain.parseString("{a in A, b in B, c in C}") assert result.asDict() == {"subscripts": ["A", "B", "C"]} class TestNumber: def test_not_number(self): fixture = """ one _ 1e e2 + Jan 01Jan Jan_01 01_Jan """ result = number.runTests(fixture, failureTests=True) assert result[0] def test_number(self): fixture = """ 1 1.1 0.234 +1e-049 2 00 0.0 """ result = number.runTests(fixture) assert result[0] class TestSymbol: def test_not_symbol(self): fixture = """ 1.1 0.234 +1e-049 skj!adfk """ result = symbol.runTests(fixture, failureTests=True) assert result[0] def test_symbol(self): fixture = """ Jan 01Jan Jan_01 01_Jan """ result = symbol.runTests(fixture) assert result[0] class TestParameter: def test_param_def(self): fixture = """ param Test{r in REGION, y in YEAR, g in GOLF}; param square {x, y}; param Test; param Test default 1; """ success, results = param_def_stmt.runTests(fixture) assert success success, _ = param_stmt.runTests(fixture, failureTests=True) assert success def test_param_stmt(self): fixture = """ param T := 4; param T := -4; param T := 0.04; param T := -0.04; param 01Jan := -0.04; param 01_Feb := -0.04; """ result = param_stmt.runTests(fixture) assert result[0] result = param_def_stmt.runTests(fixture, failureTests=True) assert result[0] class TestSet: def test_set_stmt(self): fixture = """ set month := Jan Feb Mar Apr; set month := 01Jan 01_Feb Mar A_pr; set 1_2_month := 1 2 3 4; """ result = set_stmt.runTests(fixture) assert result[0] @pytest.fixture() def setup(self): fixture = """ Jan Feb Mar Apr 01Jan 01_Feb Mar A_pr 1 2 3 4 """ return fixture def test_set_record(self, setup): result = set_record.runTests(setup) assert result[0] def test_simple_data(self, setup): result = simple_data.runTests(setup) assert result[0] def test_single(self): fixture = """ 01Jan 01 Jan Jan_01 01_Jan """ result = single.runTests(fixture) assert result[0] class AmplyTest(unittest.TestCase): def test_data(self): result = amply.Amply("param T := 4;")["T"] assert result == 4 result = amply.Amply("param T := -4;")["T"] assert result == -4 result = amply.Amply("param T := 0.04;")["T"] assert result == 0.04 result = amply.Amply("param T := -0.04;")["T"] assert result == -0.04 def test_set(self): result = amply.Amply("set month := Jan Feb Mar Apr;")["month"] assert result == ["Jan", "Feb", "Mar", "Apr"] result = amply.Amply("set month Jan Feb Mar Apr;")["month"] assert result == ["Jan", "Feb", "Mar", "Apr"] assert [i for i in result] == ["Jan", "Feb", "Mar", "Apr"] assert result != [] assert "Jan" in result assert "Foo" not in result assert len(result) == 4 def test_set_alphanumerical(self): result = amply.Amply("set month := 01Jan 01_Feb Mar A_pr;")["month"] assert result == ["01Jan", "01_Feb", "Mar", "A_pr"] def test_param_definition(self): result = amply.Amply("param T;") assert result != [4] def test_param(self): result = amply.Amply("param T := 4;")["T"] assert result != [4] def test_param_subscript(self): result = amply.Amply("param T{foo};param T := 1 2;")["T"] assert not (result == 2) assert result != 2 def test_attr_access(self): result = amply.Amply("param T:= 4;").T assert result == 4 def test_from_file(self): try: s = StringIO("param T:= 4;") except TypeError: s = StringIO("param T:= 4;") assert amply.Amply.from_file(s).T == 4 def test_load_string(self): a = amply.Amply("param T:= 4; param X{foo};") a.load_string("param S := 6; param X := 1 2;") assert a.T == 4 assert a.S == 6 assert a.X[1] == 2 def test_load_file(self): a = amply.Amply("param T:= 4; param X{foo};") try: s = StringIO("param S := 6; param X := 1 2;") except TypeError: s = StringIO("param S := 6; param X := 1 2;") a.load_file(s) assert a.T == 4 assert a.S == 6 assert a.X[1] == 2 def test_empty_init(self): a = amply.Amply() a.load_string("param T := 4;") assert a.T == 4 def test_set_dimen2(self): result = amply.Amply( """ set twotups dimen 2; set twotups := (1, 2) (2, 3) (4, 2) (3, 1); """ )["twotups"] assert result == [(1, 2), (2, 3), (4, 2), (3, 1)] def test_set_dimen_error(self): a = """ set dim1 dimen 1; set dim1 := (1, 2) (2, 3) (3, 2); """ self.assertRaises(amply.AmplyError, lambda: amply.Amply(a)) def test_set_dimen2_noparen(self): result = amply.Amply( """ set twotups dimen 2; set twotups := 1 2 2 3 4 2 3 1; """ )["twotups"] assert result == [(1, 2), (2, 3), (4, 2), (3, 1)] def test_set_subscript(self): result = amply.Amply( """ set days{months}; set days[Jan] := 1 2 3 4; set days[Feb] := 5 6 7 8; """ )["days"] j = result["Jan"] assert j == [1, 2, 3, 4] f = result["Feb"] assert f == [5, 6, 7, 8] def test_set_subscript2(self): result = amply.Amply( """ set days{months, days}; set days[Jan, 3] := 1 2 3 4; set days[Feb, 'Ham '] := 5 6 7 8; """ )["days"] j = result["Jan"][3] assert j == [1, 2, 3, 4] f = result["Feb"]["Ham "] assert f == [5, 6, 7, 8] def test_set_subscript2_tuples(self): result = amply.Amply( """ set days{months, days}; set days[Jan, 3] := 1 2 3 4; set days[Feb, 'Ham '] := 5 6 7 8; """ )["days"] j = result["Jan", 3] assert j == [1, 2, 3, 4] f = result["Feb", "Ham "] assert f == [5, 6, 7, 8] def test_set_matrix(self): result = amply.Amply( """ set A : 1 2 3 := 1 + - - 2 + + - 3 - + - ; """ ) a = result.A assert a == [(1, 1), (2, 1), (2, 2), (3, 2)] def test_set_matrix_tr(self): result = amply.Amply( """ set A (tr) : 1 2 3 := 1 + - - 2 + + - 3 - + - ; """ ) a = result.A assert a == [(1, 1), (1, 2), (2, 2), (2, 3)] def test_set_splice(self): result = amply.Amply( """ set A dimen 3; set A := (1, 2, 3), (1, 1, *) 2 4 (3, *, *) 1 1; """ ) a = result.A assert a == [(1, 2, 3), (1, 1, 2), (1, 1, 4), (3, 1, 1)] def test_set_splice_matrix(self): result = amply.Amply( """ set A dimen 3; set A (1, *, *) : 1 2 3 := 1 + - - 2 + - + 3 - - - (2, *, *) : 1 2 3 := 1 + - + 2 - + - 3 - - + ; """ ) a = result.A assert a == [ (1, 1, 1), (1, 2, 1), (1, 2, 3), (2, 1, 1), (2, 1, 3), (2, 2, 2), (2, 3, 3), ] def test_simple_params(self): result = amply.Amply("param T := 4;")["T"] assert result == 4 def test_sub1_params(self): result = amply.Amply( """ param foo {s}; param foo := 1 Jan 2 Feb 3 Mar; """ ) j = result["foo"][1] assert j == "Jan" f = result["foo"][2] assert f == "Feb" def test_sub1_param_error(self): a = """ param foo{s}; param foo := 1 Jan 2 Feb 3; """ self.assertRaises(amply.AmplyError, lambda: amply.Amply(a)) def test_param_default(self): result = amply.Amply( """ param foo {s} default 3; param foo := Jan 1 Feb 2 Mar 3; """ ) options = [("Jan", 1), ("Mar", 3), ("FOO", 3)] for name, value in options: self.assertEqual(result["foo"][name], value) def test_param_undefined(self): result = amply.Amply( """ param foo {s} ; param foo := Jan 1 Feb 2 Mar 3; """ ) j = result["foo"]["Jan"] assert j == 1 with self.assertRaises(KeyError): result["foo"]["Apr"] def test_sub2_params(self): result = amply.Amply( """ param foo {s, t}; param foo := 1 2 Hi 99 3 4; """ ) h = result["foo"][1][2] assert h == "Hi" f = result["foo"][99][3] assert f == 4 def test_2d_param(self): result = amply.Amply( """ param demand {item, location}; param demand : FRA DET LAN := spoons 200 100 30 plates 30 120 90 cups 666 13 29 ; """ )["demand"] options = [ ("spoons", {"FRA": 200, "DET": 100, "LAN": 30}), ("plates", {"FRA": 30, "DET": 120, "LAN": 90}), ("cups", {"FRA": 666, "DET": 13, "LAN": 29}), ] for name, _dict in options: self.assertDictEqual(result[name], _dict) def test_2d_numeric_param(self): result = amply.Amply( """ param square {x, y}; param square : 1 2 := 4 4 8 3 3 6 ; """ )["square"] f = result[4, 1] assert f == 4 assert result[4, 2] == 8 assert result[3, 1] == 3 assert result[3, 2] == 6 def test_2d_param_defaults(self): result = amply.Amply( """ param demand {item, location}; param demand default 42 : FRA DET LAN := spoons 200 . 30 plates 30 120 . cups . . 29 ; """ )["demand"] options = [ ("spoons", {"FRA": 200, "DET": 42, "LAN": 30}), ("plates", {"FRA": 30, "DET": 120, "LAN": 42}), ("cups", {"FRA": 42, "DET": 42, "LAN": 29}), ] for name, _dict in options: self.assertDictEqual(result[name], _dict) def test_2tables(self): result = amply.Amply( """ param demand {item, location}; param demand default 42 : FRA DET LAN := spoons 200 . 30 plates 30 120 . cups . . 29 ; param square {foo, foo}; param square : A B := A 1 6 B 6 36 ; """ ) demand = result["demand"] options = [ ("spoons", {"FRA": 200, "DET": 42, "LAN": 30}), ("plates", {"FRA": 30, "DET": 120, "LAN": 42}), ("cups", {"FRA": 42, "DET": 42, "LAN": 29}), ] for name, _dict in options: self.assertDictEqual(demand[name], _dict) square = result["square"] options = [ ("A", {"A": 1, "B": 6}), ("B", {"A": 6, "B": 36}), ] for name, _dict in options: self.assertDictEqual(square[name], _dict) def test_2d_param_transpose(self): result = amply.Amply( """ param demand {location, item}; param demand default 42 (tr) : FRA DET LAN := spoons 200 . 30 plates 30 120 . cups . . 29 ; """ )["demand"] self.assertEqual(result["FRA"], {"spoons": 200, "plates": 30, "cups": 42}) self.assertEqual(result["DET"], {"spoons": 42, "plates": 120, "cups": 42}) self.assertEqual(result["LAN"], {"spoons": 30, "plates": 42, "cups": 29}) def test_2d_slice1(self): result = amply.Amply( """ param demand {location, item}; param demand := [Jan, *] Foo 1 Bar 2; """ )["demand"] f = result["Jan"]["Foo"] assert f == 1 assert result["Jan"]["Bar"] == 2 def test_3d_slice2(self): result = amply.Amply( """ param trans_cost{src, dest, product}; param trans_cost := [*,*,bands]: FRA DET LAN := GARY 30 10 8 CLEV 22 7 10 [*,*,coils]: FRA DET LAN := GARY 39 14 11 CLEV 27 9 12 [*,*,plate]: FRA DET LAN := GARY 41 15 12 CLEV 29 9 13 ; """ )["trans_cost"] f = result["GARY"]["FRA"]["bands"] assert f == 30 assert result["GARY"]["DET"]["plate"] == 15 assert result["CLEV"]["LAN"]["coils"] == 12 def test_3d_slice2b(self): result = amply.Amply( """ param trans_cost{src, product, dest}; param trans_cost := [*,bands,*]: FRA DET LAN := GARY 30 10 8 CLEV 22 7 10 [*,coils,*]: FRA DET LAN := GARY 39 14 11 CLEV 27 9 12 [*,plate,*]: FRA DET LAN := GARY 41 15 12 CLEV 29 9 13 ; """ )["trans_cost"] f = result["GARY"]["bands"]["FRA"] assert f == 30 assert result["GARY"]["plate"]["DET"] == 15 assert result["CLEV"]["coils"]["LAN"] == 12 def test_3d_slide2c(self): amply.Amply( """ set REGION := Kenya; set TECHNOLOGY := TRLV_1_0; set YEAR := 2016 2017 2018 2019 2020; param Peakdemand {REGION,TECHNOLOGY,YEAR}; param Peakdemand default 1 := [Kenya,*,*]: 2016 2017 2018 2019 2020 := TRLV_1_0 0 0 0 0.035503748 0.073847796 ; """ ) def test_single_tabbing_data(self): result = amply.Amply( """ set elem; param init_stock{elem}; param cost{elem}; param value{elem}; param : init_stock cost value := iron 7 25 1 nickel 35 3 2 ; """ ) s = result["init_stock"] assert s == {"iron": 7, "nickel": 35} assert result["cost"] == {"iron": 25, "nickel": 3} assert result["value"] == {"iron": 1, "nickel": 2} def test_single_tabbing_data_with_set(self): result = amply.Amply( """ set elem; param init_stock{elem}; param cost{elem}; param value{elem}; param : elem : init_stock cost value := iron 7 25 1 nickel 35 3 2 ; """ ) s = result["init_stock"] assert s == {"iron": 7, "nickel": 35} assert result["cost"] == {"iron": 25, "nickel": 3} assert result["value"] == {"iron": 1, "nickel": 2} def test_set2_tabbing(self): result = amply.Amply( """ set elem dimen 2; set elem := 0 0 1 1 2 2; param cost{elem}; param value{elem}; param : cost value := 0 0 7 25 1 1 35 3 ; """ ) assert result["elem"] == [(0, 0), (1, 1), (2, 2)] def test_undefined_tabbing_param(self): a = """ param cost{elem}; param : cost value := 0 1 2 3 4 5 ; """ self.assertRaises(amply.AmplyError, lambda: amply.Amply(a)) def test_2dset_simpleparam(self): result = amply.Amply( """ set elem dimen 2; param foo{elem}; param foo := 1 2 3 2 3 4 3 4 5 ; """ )["foo"] f = result[1][2] assert f == 3 assert result[2][3] == 4 assert result[3][4] == 5 def test_tuple_param(self): result = amply.Amply( """ set elem dimen 2; param foo{elem}; param foo := 1 2 3 2 3 4 3 4 5 ; """ )["foo"] f = result[1, 2] assert f == 3 assert result[2, 3] == 4 assert result[3, 4] == 5 def test_comment(self): result = amply.Amply( """ # a comment set elem dimen 2; param foo{elem}; param foo := 1 2 3 2 3 4 3 4 5 ; """ )["foo"] f = result[1, 2] assert f == 3 assert result[2, 3] == 4 assert result[3, 4] == 5 def test_empty_tabbing_parameter_statement(self): result = amply.Amply( """ set x; param square {x}; param default 99 : square := ; """ ) assert "square" in result.symbols.keys() assert result.square == {} def test_empty_tabbing_parameters(self): result = amply.Amply( """ set x; param square {x}; param triangle {x}; param default 99 : square triangle := ; """ ) assert "square" in result.symbols.keys() assert result.square == {} def test_empty_parameter_statement(self): result = amply.Amply( """ param square {x}; param square default 99 := ; """ ) assert "square" in result.symbols.keys() assert result.square == {} def test_high_dim_tabbing(self): result = amply.Amply( """ set x; set y; param square {x,y}; param default 99 : square := a a 34 a b 35 a c 36 b a 53 b b 45.3 b c 459.2 ; """ ) assert "square" in result.symbols.keys() print(result.square["b"]) assert result.square["a"] == {"a": 34.0, "b": 35.0, "c": 36.0} assert result.square["b"] == {"a": 53.0, "b": 45.3, "c": 459.2} if __name__ == "__main__": unittest.main() amply-0.1.6/tests/travis_install.sh000066400000000000000000000042541444434367000174120ustar00rootroot00000000000000#!/bin/bash # This script is meant to be called by the "install" step defined in # .travis.yml. See http://docs.travis-ci.com/ for more details. # The behavior of the script is controlled by environment variabled defined # in the .travis.yml in the top level folder of the project. # # This script is inspired by Scikit-Learn (http://scikit-learn.org/) # # THIS SCRIPT IS SUPPOSED TO BE AN EXAMPLE. MODIFY IT ACCORDING TO YOUR NEEDS! set -e if [[ "$DISTRIB" == "conda" ]]; then # Deactivate the travis-provided virtual environment and setup a # conda-based environment instead deactivate if [[ -f "$HOME/miniconda/bin/conda" ]]; then echo "Skip install conda [cached]" else # By default, travis caching mechanism creates an empty dir in the # beginning of the build, but conda installer aborts if it finds an # existing folder, so let's just remove it: rm -rf "$HOME/miniconda" # Use the miniconda installer for faster download / install of conda # itself wget http://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh \ -O miniconda.sh chmod +x miniconda.sh && ./miniconda.sh -b -p $HOME/miniconda fi export PATH=$HOME/miniconda/bin:$PATH # Make sure to use the most updated version conda update --yes conda # Configure the conda environment and put it in the path using the # provided versions # (prefer local venv, since the miniconda folder is cached) conda config --add channels conda-forge conda create -p ./.venv --yes python=${PYTHON_VERSION} pip virtualenv pyparsing docutils source activate ./.venv else echo "Don't install conda for this Python version" fi # for all pip install -U pip setuptools if [[ "$COVERAGE" == "true" ]]; then pip install -U pytest-cov pytest-virtualenv coverage coveralls flake8 pre-commit fi travis-cleanup() { printf "Cleaning up environments ... " # printf avoids new lines if [[ "$DISTRIB" == "conda" ]]; then # Force the env to be recreated next time, for build consistency conda deactivate conda remove -p ./.venv --all --yes rm -rf ./.venv fi echo "DONE" }