commit 803c3d37e8ad503ca9dbd06fc416f8519e39a18e Author: Stuart Mitchell Date: Mon Jan 7 21:34:07 2013 +1300 implemented diff from http://code.google.com/p/pulp-or/issues/detail?id=44 adding features to CPLEX_PY diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..ff3d2d8 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,2 @@ +Roy, J.S +Mitchell, Stuart A diff --git a/HISTORY b/HISTORY new file mode 100644 index 0000000..8a8a632 --- /dev/null +++ b/HISTORY @@ -0,0 +1,77 @@ +# PuLP, Copyright J.S. Roy (js@jeannot.org), 2002-2005 +# Copyright S.A.Mitchell (s.mitchell@auckland.ac.nz), 2007- +# See the LICENSE file for copyright information. +# @(#) $Jeannot: HISTORY,v 1.8 2005/05/05 09:23:51 js Exp $ +1.4.9, 2011-03-30 + Added support for cplex runtime licenses + Made PULP_CBC_CMD the default LP solver for linux + Included 32 and 64 bit versions of cbc +1.4.8, 2011-03-30 + Overdue fix for zero coeff issue + bugfix for default cat for LpVariable.dicts + moved tests to a different file +1.4.6, 2010-01-25 + Bugfix +1.4.4, 2010-01-24 + CBC 2.4 cmd line solver added + CoinMP dll 1.4.0 uses trunk version + CoinMP library object now accessed through COINMP_DLL.lib + Config files now include a %(here)s syntax to identify paths + Included solvers now moved to solvers directory +1.4.2, 2009-12-31 + Fixes before coin Announcement +1.4.0, 2009-10-16 + Added Elastic Constraints + Added Fractional Constraints (tests yet to be added) + Added limited resolve for gurobi + Changed version numbers for coin import + Fixed COIN_CMD + Changed code to be compatible with python 2.4 +1.3.08, 2009-08-08 + Bugfix COINMP_DLL +1.3.07, 2009/06/26 + Changes for pypi +1.3.01, 2009/06/25 + Made constraints ordered dictionaries + Small changes to parameters of COIN_DLL solver + Removed string exceptions + added the constraints from other LpProblems to LpProblem.extend() +1.3.00, 2009/06/21 + Added GUROBI Solver +1.23, 2009/05/25 + Removed old style MEM solvers + Cleaned up CPLEX_DLL interface + Added Sequential solve function +1.22, 2009/04/03 + Added Cplex IntegerOptimalTolerence setMemoryEmphsis, and clarified linux + installation instructions +1.21.02, 2008/07/29 + Added epagap, and logfile in CPLEX_DLL +1.21, 2008/07/28 + Added Combination and Permutation functions + Bugfix for configsolvers + Updated setup.py to start to compile everything +1.20, 2008/06/08 + Certified for inclusion in Coin-Or + Spilt the solver and constant definitions into separate files + Unit Tests make more explicit + Included external definitions for CoinMP.dll + No Makefile +1.11, 2008/03/01 + Contributed by Stuart Mitchell s.mitchell@auckland.ac.nz. + Contains dll solvers that are accessed with the ctypes library. + Can use the CoinMP.dll solver from the coin-or project. + Added column-wise modelling and resolve capabilities. + Wiki added with plenty of examples aphi038@ec.auckland.ac.nz +1.9, 2007/08/06: + Stuart Mitchell + Added support for cplex 10.1.0 using ctypes library + Added support for the CoinMP.dll using ctypes library + Added distutils setup + Added a configuaration file pulp.cfg +1.1, 2005/05/03: + Fix an strange interpretation of unbounded integer variables by COIN and CPLEX + LP return codes are simplified. + C interface modules for GLPK, COIN and CPLEX + Windows compatibility +1.0, 2004/02/29: First release diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000..01a296a --- /dev/null +++ b/INSTALL @@ -0,0 +1,127 @@ +Installation +------------ + +Note that to install PuLP you must first have a working python installation as +described in `installing python`_. + +PuLP requires Python >= 2.5. Though it can be made to work with Python 2.4 + +The latest version of PuLP can be freely obtained from coin-or_. +Please note that this version of PuLP has not been tested with operating systems +other than Microsoft Windows and Ubuntu Linux. + +Easy install and pypi installation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By far the easiest way to install pulp is through the use of EasyInstall_ and +CheeseShop_. + +* Install EasyInstall + * In windows (please make sure easy_install is on your path):: + + c:\Python26\Scripts\> easy_install -U pulp + + * In Linux:: + + $ sudo easy_install -U pulp + $ sudo pulptest #needed to get the default solver to work + +* Then follow the instructions below to test your installation + +To access the examples and pulp source code use the instructions below +to install from source + + +Windows installation from source +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* Install python (`installing python`_) +* Download the `PuLP zipfile`_ +* Extract the zipfile to a suitable location (such as the desktop - the folder will be no longer required after installation) + * Open a command prompt by clicking "Run" in the Start Menu, and type 'cmd' in the window and push enter. + * Navigate to the extracted folder with the setup file in it. [Do this by typing 'cd foldername' at the prompt, where 'cd' stands for current directory and the 'foldername' is the name of the folder to open in the path already listed to the left of the prompt. To return back to a root drive, type 'cd C:\'] + * Type 'setup.py install' at the command prompt. This will install all the PuLP functions into Python's site-packages directory. + +The PuLP function library is now able to be imported from any python command line. Go to IDLE or PyDev and type + +>>> from pulp import * + +to load in the functions. (You need to re-import the functions each time after +you close the GUI) PuLP is written in a programming language called Python, and +to use PuLP you must write Python code to describe your optimization problem. + +Linux Installation +~~~~~~~~~~~~~~~~~~ + +* Extract the `PuLP zipfile`_ folder to a suitable location (such as your home directory - the folder will be no longer required after installation) +* Open a command line navigate to the extracted zipfile with the setup file in it. [Do this by typing 'cd foldername' at the prompt] +* Type the following at the command prompt. This will install all the PuLP functions into Python's callable modules. + +.. code-block:: sh + + $ sudo python setup.py install + +* install a solver for pulp to use either + * use the included 64 or 32-bit binaries cbc-32 and cbc-64 + * install glpk_ debain based distributions may use the following + + .. code-block:: sh + + $ sudo apt-get install glpk + + * install gurobi_ (free academic licenses) + * install cplex_ (and pay $$) + * or compile coinMP_ and pulp from source using buildout and copy the files + + .. code-block:: sh + + $ python bootstrap.py + $ bin/buildout -c solvers.cfg + $ cp parts/lib/* src/pulp/ + $ sudo setup.py install + +.. _glpk: http://www.gnu.org/software/glpk/ +.. _CoinMP: http://projects.coin-or.org/CoinMP +.. _cplex: http://cplex.com +.. _gurobi: http://gurobi.com + +Testing your PuLP installation +------------------------------ +To test that that you pulp installation is working correctly please type the +following into a python interpreter and note that the output should be similar. +The output below is what you would expect if you have not installed any other +solvers and the CoinMP_ solver bundled with pulp works. + +>>> import pulp +>>> pulp.pulpTestAll() +Solver pulp.pulp.COIN_MEM unavailable. +Solver pulp.pulp.COIN_CMD unavailable. + Testing continuous LP solution + Testing maximize continuous LP solution + Testing unbounded continuous LP solution + Testing MIP solution + Testing MIP relaxation + Testing feasibility problem (no objective) + Testing an infeasible problem + Testing an integer infeasible problem (Error to be fixed) + Testing column based modelling + Testing column based modelling with empty constraints + Testing dual variables and slacks reporting + Testing resolve of problem + Testing Sequential Solves + Testing fractional constraints + Testing elastic constraints (no change) + Testing elastic constraints (freebound) + Testing elastic constraints (penalty unchanged) + Testing elastic constraints (penalty unbounded) +* Solver pulp.pulp.COINMP_DLL passed. +Solver pulp.pulp.GLPK_MEM unavailable. +Solver pulp.pulp.GLPK_CMD unavailable. +Solver pulp.pulp.XPRESS unavailable. + +.. _`installing python`: http://www.diveintopython.org/installing_python/index.html +.. _coin-or: https://projects.coin-or.org/PuLP +.. _EasyInstall: http://pypi.python.org/pypi/setuptools +.. _CheeseShop: http://pypi.python.org +.. _`PuLP zipfile`: http://www.coin-or.org/download/source/PuLP/ + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d129b8b --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2002-2005, Jean-Sebastien Roy (js@jeannot.org) +Modifications Copyright (c) 2007- Stuart Anthony Mitchell (s.mitchell@auckland.ac.nz) + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..0cedaf3 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,20 @@ +include AUTHORS +include INSTALL +include LICENSE +include MANIFEST.in +include README +include VERSION +include HISTORY +include setup.py +include ez_setup.py +include bootstrap.py +include buildout.cfg +include docs.cfg +include solvers.cfg +include examples/*.py +include src/pulp/*.cfg.linux +include src/pulp/*.cfg.win +include src/pulp/*.cfg.buildout +include src/pulp/*.py +include src/pulp/solverdir/* +#include pulp-or/doc/*.pdf diff --git a/README b/README new file mode 100644 index 0000000..73613ef --- /dev/null +++ b/README @@ -0,0 +1,70 @@ + +# Copyright J.S. Roy (js@jeannot.org), 2003-2005 +# Copyright Stuart A. Mitchell (stu@stuartmitchell.com) +# See the LICENSE file for copyright information. + +PuLP is an LP modeler written in python. PuLP can generate MPS or LP files +and call GLPK[1], COIN CLP/CBC[2], CPLEX[3], and GUROBI[4] to solve linear +problems. + +See the examples directory for examples. + +PuLP requires Python >= 2.5. + +The examples require at least a solver in your PATH or a shared library file. + +Documentation is found on https://www.coin-or.org/PuLP/. +A comprehensive wiki can be found at https://www.coin-or.org/PuLP/ + +Use LpVariable() to create new variables. To create a variable 0 <= x <= 3 +>>> x = LpVariable("x", 0, 3) + +To create a variable 0 <= y <= 1 +>>> y = LpVariable("y", 0, 1) + +Use LpProblem() to create new problems. Create "myProblem" +>>> prob = LpProblem("myProblem", LpMinimize) + +Combine variables to create expressions and constraints and add them to the +problem. +>>> prob += x + y <= 2 + +If you add an expression (not a constraint), it will +become the objective. +>>> prob += -4*x + y + +Choose a solver and solve the problem. ex: +>>> status = prob.solve(GLPK(msg = 0)) + +Display the status of the solution +>>> LpStatus[status] +'Optimal' + +You can get the value of the variables using value(). ex: +>>> value(x) +2.0 + +Exported Classes: + - LpProblem -- Container class for a Linear programming problem + - LpVariable -- Variables that are added to constraints in the LP + - LpConstraint -- A constraint of the general form + a1x1+a2x2 ...anxn (<=, =, >=) b + - LpConstraintVar -- Used to construct a column of the model in column-wise + modelling + +Exported Functions: + - value() -- Finds the value of a variable or expression + - lpSum() -- given a list of the form [a1*x1, a2x2, ..., anxn] will construct + a linear expression to be used as a constraint or variable + - lpDot() --given two lists of the form [a1, a2, ..., an] and + [ x1, x2, ..., xn] will construct a linear epression to be used + as a constraint or variable + +Comments, bug reports, patches and suggestions are welcome. +pulp-or-discuss@googlegroups.com + +References: +[1] http://www.gnu.org/software/glpk/glpk.html +[2] http://www.coin-or.org/ +[3] http://www.cplex.com/ +[4] http://www.gurobi.com/ diff --git a/ROADMAP b/ROADMAP new file mode 100644 index 0000000..912e5e4 --- /dev/null +++ b/ROADMAP @@ -0,0 +1,34 @@ +Roadmap for Pulp-or +=================== + +Version 1.0 +----------- +Original Pulp version written and conceived by Jean-Sebastien Roy js@jeannot.org + +Version 1.1 +----------- +Contributed by Stuart Mitchell s.mitchell@auckland.ac.nz. +Contains dll solvers that are accessed with the ctypes library. +Can use the CoinMP.dll solver from the coin-or project. +Added column-wise modelling and resolve capabilities. +Wiki added with plenty of examples aphi038@ec.auckland.ac.nz + +Version 1.20 +------------ +Certified for inclusion in Coin-Or +Spilt the solver and constant definitions into separate files +Unit Tests make more explicit +Included external definitions for CoinMP.dll +No Makefile + +Version 1.3 +------------ +Added GUROBI solver +Removed the older MEM solvers + +Future +------ +Discuss language syntax looking towards removing C alike constructs and make + better use of Python namespace conventions +Integrate branch and cut within Pulp +Integrate with other python OR projects eg Pyomo wehart@sandia.gov and POAMS leo@sie.arizona.edu diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..94fe62c --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.5.4 diff --git a/bootstrap.py b/bootstrap.py new file mode 100644 index 0000000..a545477 --- /dev/null +++ b/bootstrap.py @@ -0,0 +1,113 @@ +############################################################################## +# +# Copyright (c) 2006 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Bootstrap a buildout-based project + +Simply run this script in a directory containing a buildout.cfg. +The script accepts buildout command-line options, so you can +use the -c option to specify an alternate configuration file. + +$Id$ +""" + +import os, shutil, sys, tempfile, urllib2 +from optparse import OptionParser + +tmpeggs = tempfile.mkdtemp() + +is_jython = sys.platform.startswith('java') + +# parsing arguments +parser = OptionParser() +parser.add_option("-v", "--version", dest="version", + help="use a specific zc.buildout version") +parser.add_option("-d", "--distribute", + action="store_true", dest="distribute", default=False, + help="Use Disribute rather than Setuptools.") + +options, args = parser.parse_args() + +if options.version is not None: + VERSION = '==%s' % options.version +else: + VERSION = '' + +USE_DISTRIBUTE = options.distribute +args = args + ['bootstrap'] + +to_reload = False +try: + import pkg_resources + if not hasattr(pkg_resources, '_distribute'): + to_reload = True + raise ImportError +except ImportError: + ez = {} + if USE_DISTRIBUTE: + exec urllib2.urlopen('http://python-distribute.org/distribute_setup.py' + ).read() in ez + ez['use_setuptools'](to_dir=tmpeggs, download_delay=0, no_fake=True) + else: + exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py' + ).read() in ez + ez['use_setuptools'](to_dir=tmpeggs, download_delay=0) + + if to_reload: + reload(pkg_resources) + else: + import pkg_resources + +if sys.platform == 'win32': + def quote(c): + if ' ' in c: + return '"%s"' % c # work around spawn lamosity on windows + else: + return c +else: + def quote (c): + return c + +cmd = 'from setuptools.command.easy_install import main; main()' +ws = pkg_resources.working_set + +if USE_DISTRIBUTE: + requirement = 'distribute' +else: + requirement = 'setuptools' + +if is_jython: + import subprocess + + assert subprocess.Popen([sys.executable] + ['-c', quote(cmd), '-mqNxd', + quote(tmpeggs), 'zc.buildout' + VERSION], + env=dict(os.environ, + PYTHONPATH= + ws.find(pkg_resources.Requirement.parse(requirement)).location + ), + ).wait() == 0 + +else: + assert os.spawnle( + os.P_WAIT, sys.executable, quote (sys.executable), + '-c', quote (cmd), '-mqNxd', quote (tmpeggs), 'zc.buildout' + VERSION, + dict(os.environ, + PYTHONPATH= + ws.find(pkg_resources.Requirement.parse(requirement)).location + ), + ) == 0 + +ws.add_entry(tmpeggs) +ws.require('zc.buildout' + VERSION) +import zc.buildout.buildout +zc.buildout.buildout.main(args) +shutil.rmtree(tmpeggs) diff --git a/buildout.cfg b/buildout.cfg new file mode 100644 index 0000000..b63a354 --- /dev/null +++ b/buildout.cfg @@ -0,0 +1,14 @@ +[buildout] +develop = . +parts = pythonpulp scripts + +[pythonpulp] +recipe = zc.recipe.egg +interpreter = pythonpulp +eggs = pulp + +[scripts] +recipe = zc.recipe.egg:scripts +eggs = pulp + + diff --git a/doc/KPyCon2009/IEEEtran.bst b/doc/KPyCon2009/IEEEtran.bst new file mode 100644 index 0000000..2cd5265 --- /dev/null +++ b/doc/KPyCon2009/IEEEtran.bst @@ -0,0 +1,2369 @@ +%% +%% IEEEtran.bst +%% BibTeX Bibliography Style file for IEEE Journals and Conferences (unsorted) +%% Version 1.11 (2003/04/02) +%% +%% Copyright (c) 2003 Michael Shell +%% +%% Original starting code base and algorithms obtained from the output of +%% Patrick W. Daly's makebst package as well as from prior versions of +%% IEEE BibTeX styles: +%% +%% 1. Howard Trickey and Oren Patashnik's ieeetr.bst (1985/1988) +%% 2. Silvano Balemi and Richard H. Roy's IEEEbib.bst (1993) +%% +%% +%% See: +%% http://www.ctan.org/tex-archive/macros/latex/contrib/supported/IEEEtran/ +%% for latest version and current contact information. +%% +%% For use with BibTeX version 0.99a or later +%% +%% This is a numerical citation style. +%% +%%********************************************************************** +%% Legal Notice: +%% This code is offered as-is without any warranty either expressed or +%% implied; without even the implied warranty of MERCHANTABILITY or +%% FITNESS FOR A PARTICULAR PURPOSE! +%% User assumes all risk. +%% In no event shall IEEE or any contributor to this code be liable for +%% any damages or losses, including, but not limited to, incidental, +%% consequential, or any other damages, resulting from the use or misuse +%% of any information contained here. +%% +%% This code is distributed under the Perl Artistic License +%% ( http://language.perl.com/misc/Artistic.html ) +%% and may be freely used, distributed and modified - subject to the +%% constraints therein. +%% Retain all contribution notices, credits and disclaimers. +%% +%% All comments are the opinions of their respective authors and are not +%% necessarily endorsed by the IEEE. +%%********************************************************************** +% +% +% Changelog: +% +% 1.00 (2002/08/13) Initial release +% +% 1.10 (2002/09/27) +% 1. Corrected minor bug for improperly formed warning message when a +% book was not given a title. Thanks to Ming Kin Lai for reporting this. +% 2. Added support for CTLname_format_string and CTLname_latex_cmd fields +% in the BST control entry type. +% +% 1.11 (2003/04/02) +% 1. Fixed bug with URLs containing underscores when using url.sty. Thanks +% to Ming Kin Lai for reporting this. + + + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% DEFAULTS FOR THE CONTROLS OF THE BST STYLE %% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% These are the defaults for the user adjustable controls. The values used +% here can be overridden by the user via IEEEtranBSTCTL entry type. + +% NOTE: The recommended LaTeX command to invoke a control entry type is: +% +%\makeatletter +%\def\bstctlcite#1{\@bsphack +% \@for\@citeb:=#1\do{% +% \edef\@citeb{\expandafter\@firstofone\@citeb}% +% \if@filesw\immediate\write\@auxout{\string\citation{\@citeb}}\fi}% +% \@esphack} +%\makeatother +% +% It is called at the start of the document, before the first \cite, like: +% \bstctlcite{IEEEexample:BSTcontrol} +% +% IEEEtran.cls V1.6 and later does provide this command. + + + +% #0 turns off the display of the number for articles. +% #1 enables +FUNCTION {default.is.use.number.for.article} { #1 } + + +% #0 turns off the display of the paper and type fields in @inproceedings. +% #1 enables +FUNCTION {default.is.use.paper} { #1 } + + +% #0 turns off the forced use of "et al." +% #1 enables +FUNCTION {default.is.forced.et.al} { #0 } + +% The maximum number of names that can be present beyond which an "et al." +% usage is forced. Be sure that num.names.shown.with.forced.et.al (below) +% is not greater than this value! +% Note: There are many instances of references in IEEE journals which have +% a very large number of authors as well as instances in which "et al." is +% used profusely. +FUNCTION {default.max.num.names.before.forced.et.al} { #10 } + +% The number of names that will be shown with a forced "et al.". +% Must be less than or equal to max.num.names.before.forced.et.al +FUNCTION {default.num.names.shown.with.forced.et.al} { #1 } + + +% #0 turns off the alternate interword spacing for entries with URLs. +% #1 enables +FUNCTION {default.is.use.alt.interword.spacing} { #1 } + +% If alternate interword spacing for entries with URLs is enabled, this is +% the interword spacing stretch factor that will be used. For example, the +% default "4" here means that the interword spacing in entries with URLs can +% stretch to four times normal. Does not have to be an integer. Note that +% the value specified here can be overridden by the user in their LaTeX +% code via a command such as: +% "\providecommand\BIBentryALTinterwordstretchfactor{1.5}" in addition to +% that via the IEEEtranBSTCTL entry type. +FUNCTION {default.ALTinterwordstretchfactor} { "4" } + + +% #0 turns off the "dashification" of repeated (i.e., identical to those +% of the previous entry) names. IEEE normally does this. +% #1 enables +FUNCTION {default.is.dash.repeated.names} { #1 } + + +% The default name format control string. +FUNCTION {default.name.format.string}{ "{f.~}{vv~}{ll}{, jj}" } + + +% The default LaTeX font command for the names. +FUNCTION {default.name.latex.cmd}{ "" } + + +% Other controls that cannot be accessed via IEEEtranBSTCTL entry type. + +% #0 turns off the terminal startup banner/completed message so as to +% operate more quietly. +% #1 enables +FUNCTION {is.print.banners.to.terminal} { #1 } + + + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% FILE VERSION AND BANNER %% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +FUNCTION{bst.file.version} { "1.11" } +FUNCTION{bst.file.date} { "2003/04/02" } +FUNCTION{bst.file.website} { "http://www.ctan.org/tex-archive/macros/latex/contrib/supported/IEEEtran/" } + +FUNCTION {banner.message} +{ is.print.banners.to.terminal + { "-- IEEEtran.bst version" " " * bst.file.version * + " (" * bst.file.date * ") " * "by Michael Shell." * + top$ + "-- " bst.file.website * + top$ + "-- See the " quote$ * "IEEEtran_bst_HOWTO.pdf" * quote$ * " manual for usage information." * + top$ + } + { skip$ } + if$ +} + +FUNCTION {completed.message} +{ is.print.banners.to.terminal + { "" + top$ + "Done." + top$ + } + { skip$ } + if$ +} + + + + +%%%%%%%%%%%%%%%%%%%%%% +%% STRING CONSTANTS %% +%%%%%%%%%%%%%%%%%%%%%% + +FUNCTION {bbl.and}{ "and" } +FUNCTION {bbl.etal}{ "et~al." } +FUNCTION {bbl.editors}{ "eds." } +FUNCTION {bbl.editor}{ "ed." } +FUNCTION {bbl.edition}{ "ed." } +FUNCTION {bbl.volume}{ "vol." } +FUNCTION {bbl.of}{ "of" } +FUNCTION {bbl.number}{ "no." } +FUNCTION {bbl.in}{ "in" } +FUNCTION {bbl.pages}{ "pp." } +FUNCTION {bbl.page}{ "p." } +FUNCTION {bbl.chapter}{ "ch." } +FUNCTION {bbl.paper}{ "paper" } +FUNCTION {bbl.part}{ "pt." } +FUNCTION {bbl.patent}{ "Patent" } +FUNCTION {bbl.patentUS}{ "U.S." } +FUNCTION {bbl.revision}{ "Rev." } +FUNCTION {bbl.series}{ "ser." } +FUNCTION {bbl.standard}{ "Std." } +FUNCTION {bbl.techrep}{ "Tech. Rep." } +FUNCTION {bbl.mthesis}{ "Master's thesis" } +FUNCTION {bbl.phdthesis}{ "Ph.D. dissertation" } +FUNCTION {bbl.urlprefix}{ "[Online]" } +FUNCTION {bbl.st}{ "st" } +FUNCTION {bbl.nd}{ "nd" } +FUNCTION {bbl.rd}{ "rd" } +FUNCTION {bbl.th}{ "th" } + + +% This is the LaTeX spacer that is used when a larger than normal space +% is called for (such as just before the address:publisher). +FUNCTION {large.space} { "\hskip 1em plus 0.5em minus 0.4em\relax " } + +% The LaTeX code for dashes that are used to represent repeated names. +% Note: Some older IEEE journals used something like +% "\rule{0.275in}{0.5pt}\," which is fairly thick and runs right along +% the baseline. However, IEEE now uses a thinner, above baseline, +% six dash long sequence. +FUNCTION {repeated.name.dashes} { "------" } + + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% PREDEFINED STRING MACROS %% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +MACRO {jan} {"Jan."} +MACRO {feb} {"Feb."} +MACRO {mar} {"Mar."} +MACRO {apr} {"Apr."} +MACRO {may} {"May"} +MACRO {jun} {"June"} +MACRO {jul} {"July"} +MACRO {aug} {"Aug."} +MACRO {sep} {"Sept."} +MACRO {oct} {"Oct."} +MACRO {nov} {"Nov."} +MACRO {dec} {"Dec."} + + + +%%%%%%%%%%%%%%%%%% +%% ENTRY FIELDS %% +%%%%%%%%%%%%%%%%%% + +ENTRY + { address + assignee + author + booktitle + chapter + day + dayfiled + edition + editor + howpublished + institution + intype + journal + key + language + month + monthfiled + nationality + note + number + organization + pages + paper + publisher + school + series + revision + title + type + url + volume + year + yearfiled + CTLuse_article_number + CTLuse_paper + CTLuse_forced_etal + CTLmax_names_forced_etal + CTLnames_show_etal + CTLuse_alt_spacing + CTLalt_stretch_factor + CTLdash_repeated_names + CTLname_format_string + CTLname_latex_cmd + } + {} + { label } + + + + +%%%%%%%%%%%%%%%%%%%%%%% +%% INTEGER VARIABLES %% +%%%%%%%%%%%%%%%%%%%%%%% + +INTEGERS { prev.status.punct this.status.punct punct.std + punct.no punct.comma punct.period + prev.status.space this.status.space space.std + space.no space.normal space.large + prev.status.quote this.status.quote quote.std + quote.no quote.close + prev.status.nline this.status.nline nline.std + nline.no nline.newblock + status.cap cap.std + cap.no cap.yes} + +INTEGERS { longest.label.width multiresult nameptr namesleft number.label numnames } + +INTEGERS { is.use.number.for.article + is.use.paper + is.forced.et.al + max.num.names.before.forced.et.al + num.names.shown.with.forced.et.al + is.use.alt.interword.spacing + is.dash.repeated.names} + + +%%%%%%%%%%%%%%%%%%%%%% +%% STRING VARIABLES %% +%%%%%%%%%%%%%%%%%%%%%% + +STRINGS { bibinfo + longest.label + oldname + s + t + ALTinterwordstretchfactor + name.format.string + name.latex.cmd} + + + + +%%%%%%%%%%%%%%%%%%%%%%%%% +%% LOW LEVEL FUNCTIONS %% +%%%%%%%%%%%%%%%%%%%%%%%%% + +FUNCTION {initialize.controls} +{ default.is.use.number.for.article 'is.use.number.for.article := + default.is.use.paper 'is.use.paper := + default.is.forced.et.al 'is.forced.et.al := + default.max.num.names.before.forced.et.al 'max.num.names.before.forced.et.al := + default.num.names.shown.with.forced.et.al 'num.names.shown.with.forced.et.al := + default.is.use.alt.interword.spacing 'is.use.alt.interword.spacing := + default.is.dash.repeated.names 'is.dash.repeated.names := + default.ALTinterwordstretchfactor 'ALTinterwordstretchfactor := + default.name.format.string 'name.format.string := + default.name.latex.cmd 'name.latex.cmd := +} + + +% This IEEEtran.bst features a very powerful and flexible mechanism for +% controlling the capitalization, punctuation, spacing, quotation, and +% newlines of the formatted entry fields. (Note: IEEEtran.bst does not need +% or use the newline/newblock feature, but it has been implemented for +% possible future use.) The output states of IEEEtran.bst consist of +% multiple independent attributes and, as such, can be thought of as being +% vectors, rather than the simple scalar values ("before.all", +% "mid.sentence", etc.) used in most other .bst files. +% +% The more flexible and complex design used here was motivated in part by +% IEEE's rather unusual bibliography style. For example, IEEE ends the +% previous field item with a period and large space prior to the publisher +% address; the @electronic entry types use periods as inter-item punctuation +% rather than the commas used by the other entry types; and URLs are never +% followed by periods even though they are the last item in the entry. +% Although it is possible to accommodate these features with the conventional +% output state system, the seemingly endless exceptions make for convoluted, +% unreliable and difficult to maintain code. +% +% IEEEtran.bst's output state system can be easily understood via a simple +% illustration of two most recently formatted entry fields (on the stack): +% +% CURRENT_ITEM +% "PREVIOUS_ITEM +% +% which, in this example, is to eventually appear in the bibliography as: +% +% "PREVIOUS_ITEM," CURRENT_ITEM +% +% It is the job of the output routine to take the previous item off of the +% stack (while leaving the current item at the top of the stack), apply its +% trailing punctuation (including closing quote marks) and spacing, and then +% to write the result to BibTeX's output buffer: +% +% "PREVIOUS_ITEM," +% +% Punctuation (and spacing) between items is often determined by both of the +% items rather than just the first one. The presence of quotation marks +% further complicates the situation because, in standard English, trailing +% punctuation marks are supposed to be contained within the quotes. +% +% IEEEtran.bst maintains two output state (aka "status") vectors which +% correspond to the previous and current (aka "this") items. Each vector +% consists of several independent attributes which track punctuation, +% spacing, quotation, and newlines. Capitalization status is handled by a +% separate scalar because the format routines, not the output routine, +% handle capitalization and, therefore, there is no need to maintain the +% capitalization attribute for both the "previous" and "this" items. +% +% When a format routine adds a new item, it copies the current output status +% vector to the previous output status vector and (usually) resets the +% current (this) output status vector to a "standard status" vector. Using a +% "standard status" vector in this way allows us to redefine what we mean by +% "standard status" at the start of each entry handler and reuse the same +% format routines under the various inter-item separation schemes. For +% example, the standard status vector for the @book entry type may use +% commas for item separators, while the @electronic type may use periods, +% yet both entry handlers exploit many of the exact same format routines. +% +% Because format routines have write access to the output status vector of +% the previous item, they can override the punctuation choices of the +% previous format routine! Therefore, it becomes trivial to implement rules +% such as "Always use a period and a large space before the publisher." By +% pushing the generation of the closing quote mark to the output routine, we +% avoid all the problems caused by having to close a quote before having all +% the information required to determine what the punctuation should be. +% +% The IEEEtran.bst output state system can easily be expanded if needed. +% For instance, it is easy to add a "space.tie" attribute value if the +% bibliography rules mandate that two items have to be joined with an +% unbreakable space. + +FUNCTION {initialize.status.constants} +{ #0 'punct.no := + #1 'punct.comma := + #2 'punct.period := + #0 'space.no := + #1 'space.normal := + #2 'space.large := + #0 'quote.no := + #1 'quote.close := + #0 'cap.no := + #1 'cap.yes := + #0 'nline.no := + #1 'nline.newblock := +} + +FUNCTION {std.status.using.comma} +{ punct.comma 'punct.std := + space.normal 'space.std := + quote.no 'quote.std := + nline.no 'nline.std := + cap.no 'cap.std := +} + +FUNCTION {std.status.using.period} +{ punct.period 'punct.std := + space.normal 'space.std := + quote.no 'quote.std := + nline.no 'nline.std := + cap.yes 'cap.std := +} + +FUNCTION {initialize.prev.this.status} +{ punct.no 'prev.status.punct := + space.no 'prev.status.space := + quote.no 'prev.status.quote := + nline.no 'prev.status.nline := + punct.no 'this.status.punct := + space.no 'this.status.space := + quote.no 'this.status.quote := + nline.no 'this.status.nline := + cap.yes 'status.cap := +} + +FUNCTION {this.status.std} +{ punct.std 'this.status.punct := + space.std 'this.status.space := + quote.std 'this.status.quote := + nline.std 'this.status.nline := +} + +FUNCTION {cap.status.std}{ cap.std 'status.cap := } + +FUNCTION {this.to.prev.status} +{ this.status.punct 'prev.status.punct := + this.status.space 'prev.status.space := + this.status.quote 'prev.status.quote := + this.status.nline 'prev.status.nline := +} + + +FUNCTION {not} +{ { #0 } + { #1 } + if$ +} + +FUNCTION {and} +{ { skip$ } + { pop$ #0 } + if$ +} + +FUNCTION {or} +{ { pop$ #1 } + { skip$ } + if$ +} + + +% convert the strings "yes" or "no" to #1 or #0 respectively +FUNCTION {yes.no.to.int} +{ "l" change.case$ duplicate$ + "yes" = + { pop$ #1 } + { duplicate$ "no" = + { pop$ #0 } + { "unknown boolean " quote$ * swap$ * quote$ * + " in " * cite$ * warning$ + #0 + } + if$ + } + if$ +} + + +% pushes true if the single char string on the stack is in the +% range of "0" to "9" +FUNCTION {is.num} +{ chr.to.int$ + duplicate$ "0" chr.to.int$ < not + swap$ "9" chr.to.int$ > not and +} + +% multiplies the integer on the stack by a factor of 10 +FUNCTION {bump.int.mag} +{ #0 'multiresult := + { duplicate$ #0 > } + { #1 - + multiresult #10 + + 'multiresult := + } + while$ +pop$ +multiresult +} + +% converts a single character string on the stack to an integer +FUNCTION {char.to.integer} +{ duplicate$ + is.num + { chr.to.int$ "0" chr.to.int$ - } + {"noninteger character " quote$ * swap$ * quote$ * + " in integer field of " * cite$ * warning$ + #0 + } + if$ +} + +% converts a string on the stack to an integer +FUNCTION {string.to.integer} +{ duplicate$ text.length$ 'namesleft := + #1 'nameptr := + #0 'numnames := + { nameptr namesleft > not } + { duplicate$ nameptr #1 substring$ + char.to.integer numnames bump.int.mag + + 'numnames := + nameptr #1 + + 'nameptr := + } + while$ +pop$ +numnames +} + + + + +% The output routines write out the *next* to the top (previous) item on the +% stack, adding punctuation and such as needed. Since IEEEtran.bst maintains +% the output status for the top two items on the stack, these output +% routines have to consider the previous output status (which corresponds to +% the item that is being output). Full independent control of punctuation, +% closing quote marks, spacing, and newblock is provided. +% +% "output.nonnull" does not check for the presence of a previous empty +% item. +% +% "output" does check for the presence of a previous empty item and will +% remove an empty item rather than outputing it. +% +% "output.warn" is like "output", but will issue a warning if it detects +% an empty item. + +FUNCTION {output.nonnull} +{ swap$ + prev.status.punct punct.comma = + { "," * } + { skip$ } + if$ + prev.status.punct punct.period = + { add.period$ } + { skip$ } + if$ + prev.status.quote quote.close = + { "''" * } + { skip$ } + if$ + prev.status.space space.normal = + { " " * } + { skip$ } + if$ + prev.status.space space.large = + { large.space * } + { skip$ } + if$ + write$ + prev.status.nline nline.newblock = + { newline$ "\newblock " write$ } + { skip$ } + if$ +} + +FUNCTION {output} +{ duplicate$ empty$ + 'pop$ + 'output.nonnull + if$ +} + +FUNCTION {output.warn} +{ 't := + duplicate$ empty$ + { pop$ "empty " t * " in " * cite$ * warning$ } + 'output.nonnull + if$ +} + +% "fin.entry" is the output routine that handles the last item of the entry +% (which will be on the top of the stack when "fin.entry" is called). + +FUNCTION {fin.entry} +{ this.status.punct punct.no = + { skip$ } + { add.period$ } + if$ + this.status.quote quote.close = + { "''" * } + { skip$ } + if$ +write$ +newline$ +} + + +FUNCTION {is.last.char.not.punct} +{ duplicate$ + "}" * add.period$ + #-1 #1 substring$ "." = +} + +FUNCTION {is.multiple.pages} +{ 't := + #0 'multiresult := + { multiresult not + t empty$ not + and + } + { t #1 #1 substring$ + duplicate$ "-" = + swap$ duplicate$ "," = + swap$ "+" = + or or + { #1 'multiresult := } + { t #2 global.max$ substring$ 't := } + if$ + } + while$ + multiresult +} + +FUNCTION {capitalize}{ "u" change.case$ "t" change.case$ } + +FUNCTION {emphasize} +{ duplicate$ empty$ + { pop$ "" } + { "\emph{" swap$ * "}" * } + if$ +} + +FUNCTION {do.name.latex.cmd} +{ name.latex.cmd + empty$ + { skip$ } + { name.latex.cmd "{" * swap$ * "}" * } + if$ +} + +% IEEEtran.bst uses its own \BIBforeignlanguage command which directly +% invokes the TeX hyphenation patterns without the need of the Babel +% package. Babel does a lot more than switch hyphenation patterns and +% its loading can cause unintended effects in many class files (such as +% IEEEtran.cls). +FUNCTION {select.language} +{ duplicate$ empty$ 'pop$ + { language empty$ 'skip$ + { "\BIBforeignlanguage{" language * "}{" * swap$ * "}" * } + if$ + } + if$ +} + +FUNCTION {tie.or.space.prefix} +{ duplicate$ text.length$ #3 < + { "~" } + { " " } + if$ + swap$ +} + +FUNCTION {get.bbl.editor} +{ editor num.names$ #1 > 'bbl.editors 'bbl.editor if$ } + +FUNCTION {space.word}{ " " swap$ * " " * } + + +% Field Conditioners, Converters, Checkers and External Interfaces + +FUNCTION {empty.field.to.null.string} +{ duplicate$ empty$ + { pop$ "" } + { skip$ } + if$ +} + +FUNCTION {either.or.check} +{ empty$ + { pop$ } + { "can't use both " swap$ * " fields in " * cite$ * warning$ } + if$ +} + +FUNCTION {empty.entry.warn} +{ author empty$ title empty$ howpublished empty$ + month empty$ year empty$ note empty$ url empty$ + and and and and and and + { "all relevant fields are empty in " cite$ * warning$ } + 'skip$ + if$ +} + + +% The bibinfo system provides a way for the electronic parsing/acquisition +% of a bibliography's contents as is done by ReVTeX. For example, a field +% could be entered into the bibliography as: +% \bibinfo{volume}{2} +% Only the "2" would show up in the document, but the LaTeX \bibinfo command +% could do additional things with the information. IEEEtran.bst does provide +% a \bibinfo command via "\providecommand{\bibinfo}[2]{#2}". However, it is +% currently not used as the bogus bibinfo functions defined here output the +% entry values directly without the \bibinfo wrapper. The bibinfo functions +% themselves (and the calls to them) are retained for possible future use. +% +% bibinfo.check avoids acting on missing fields while bibinfo.warn will +% issue a warning message if a missing field is detected. Prior to calling +% the bibinfo functions, the user should push the field value and then its +% name string, in that order. + +FUNCTION {bibinfo.check} +{ swap$ duplicate$ missing$ + { pop$ pop$ "" } + { duplicate$ empty$ + { swap$ pop$ } + { swap$ pop$ } + if$ + } + if$ +} + +FUNCTION {bibinfo.warn} +{ swap$ duplicate$ missing$ + { swap$ "missing " swap$ * " in " * cite$ * warning$ pop$ "" } + { duplicate$ empty$ + { swap$ "empty " swap$ * " in " * cite$ * warning$ } + { swap$ pop$ } + if$ + } + if$ +} + + +% IEEE separates large numbers with more than 4 digits into groups of +% three. IEEE uses a small space to separate these number groups. +% Typical applications include patent and page numbers. + +% number of consecutive digits required to trigger the group separation. +FUNCTION {large.number.trigger}{ #5 } + +% For numbers longer than the trigger, this is the blocksize of the groups. +% The blocksize must be less than the trigger threshold, and 2 * blocksize +% must be greater than the trigger threshold (can't do more than one +% separation on the initial trigger). +FUNCTION {large.number.blocksize}{ #3 } + +% What is actually inserted between the number groups. +FUNCTION {large.number.separator}{ "\," } + +% So as to save on integer variables by reusing existing ones, numnames +% holds the current number of consecutive digits read and nameptr holds +% the number that will trigger an inserted space. +FUNCTION {large.number.separate} +{ 't := + "" + #0 'numnames := + large.number.trigger 'nameptr := + { t empty$ not } + { t #-1 #1 substring$ is.num + { numnames #1 + 'numnames := } + { #0 'numnames := + large.number.trigger 'nameptr := + } + if$ + t #-1 #1 substring$ swap$ * + t #-2 global.max$ substring$ 't := + numnames nameptr = + { duplicate$ #1 nameptr large.number.blocksize - substring$ swap$ + nameptr large.number.blocksize - #1 + global.max$ substring$ + large.number.separator swap$ * * + nameptr large.number.blocksize - 'numnames := + large.number.blocksize #1 + 'nameptr := + } + { skip$ } + if$ + } + while$ +} + +% Converts all single dashes "-" to double dashes "--". +FUNCTION {n.dashify} +{ large.number.separate + 't := + "" + { t empty$ not } + { t #1 #1 substring$ "-" = + { t #1 #2 substring$ "--" = not + { "--" * + t #2 global.max$ substring$ 't := + } + { { t #1 #1 substring$ "-" = } + { "-" * + t #2 global.max$ substring$ 't := + } + while$ + } + if$ + } + { t #1 #1 substring$ * + t #2 global.max$ substring$ 't := + } + if$ + } + while$ +} + + +% This function detects entries with names that are identical to that of +% the previous entry and replaces the repeated names with dashes (if the +% "is.dash.repeated.names" user control is nonzero). +FUNCTION {name.or.dash} +{ 's := + oldname empty$ + { s 'oldname := s } + { s oldname = + { is.dash.repeated.names + { repeated.name.dashes } + { s 'oldname := s } + if$ + } + { s 'oldname := s } + if$ + } + if$ +} + +% Converts the number string on the top of the stack to +% "numerical ordinal form" (e.g., "7" to "7th"). There is +% no artificial limit to the upper bound of the numbers as the +% least significant digit always determines the ordinal form. +FUNCTION {num.to.ordinal} +{ duplicate$ #-1 #1 substring$ "1" = + { bbl.st * } + { duplicate$ #-1 #1 substring$ "2" = + { bbl.nd * } + { duplicate$ #-1 #1 substring$ "3" = + { bbl.rd * } + { bbl.th * } + if$ + } + if$ + } + if$ +} + +% If the string on the top of the stack begins with a number, +% (e.g., 11th) then replace the string with the leading number +% it contains. Otherwise retain the string as-is. s holds the +% extracted number, t holds the part of the string that remains +% to be scanned. +FUNCTION {extract.num} +{ duplicate$ 't := + "" 's := + { t empty$ not } + { t #1 #1 substring$ + t #2 global.max$ substring$ 't := + duplicate$ is.num + { s swap$ * 's := } + { pop$ "" 't := } + if$ + } + while$ + s empty$ + 'skip$ + { pop$ s } + if$ +} + +% Converts the word number string on the top of the stack to +% Arabic string form. Will be successful up to "tenth". +FUNCTION {word.to.num} +{ duplicate$ "l" change.case$ 's := + s "first" = + { pop$ "1" } + { skip$ } + if$ + s "second" = + { pop$ "2" } + { skip$ } + if$ + s "third" = + { pop$ "3" } + { skip$ } + if$ + s "fourth" = + { pop$ "4" } + { skip$ } + if$ + s "fifth" = + { pop$ "5" } + { skip$ } + if$ + s "sixth" = + { pop$ "6" } + { skip$ } + if$ + s "seventh" = + { pop$ "7" } + { skip$ } + if$ + s "eighth" = + { pop$ "8" } + { skip$ } + if$ + s "ninth" = + { pop$ "9" } + { skip$ } + if$ + s "tenth" = + { pop$ "10" } + { skip$ } + if$ +} + + +% Converts the string on the top of the stack to numerical +% ordinal (e.g., "11th") form. +FUNCTION {convert.edition} +{ duplicate$ empty$ 'skip$ + { duplicate$ #1 #1 substring$ is.num + { extract.num + num.to.ordinal + } + { word.to.num + duplicate$ #1 #1 substring$ is.num + { num.to.ordinal } + { "edition ordinal word " quote$ * edition * quote$ * + " may be too high (or improper) for conversion" * " in " * cite$ * warning$ + } + if$ + } + if$ + } + if$ +} + + + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% LATEX BIBLIOGRAPHY CODE %% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +FUNCTION {start.entry} +{ newline$ + "\bibitem{" write$ + cite$ write$ + "}" write$ + newline$ + "" + initialize.prev.this.status +} + +% Here we write out all the LaTeX code that we will need. The most involved +% code sequences are those that control the alternate interword spacing and +% foreign language hyphenation patterns. The heavy use of \providecommand +% gives users a way to override the defaults. Special thanks to Javier Bezos, +% Johannes Braams, Robin Fairburns, Heiko Oberdiek, Donald Arseneau and all +% the other gurus on comp.text.tex for their help and advice on the topic of +% \selectlanguage, Babel and BibTeX. +FUNCTION {begin.bib} +{ preamble$ empty$ 'skip$ + { preamble$ write$ newline$ } + if$ + "\begin{thebibliography}{" longest.label * "}" * + write$ newline$ + "\providecommand{\url}[1]{#1}" + write$ newline$ + "\csname url@rmstyle\endcsname" + write$ newline$ + "\providecommand{\newblock}{\relax}" + write$ newline$ + "\providecommand{\bibinfo}[2]{#2}" + write$ newline$ + "\providecommand\BIBentrySTDinterwordspacing{\spaceskip=0pt\relax}" + write$ newline$ + "\providecommand\BIBentryALTinterwordstretchfactor{" + ALTinterwordstretchfactor * "}" * + write$ newline$ + "\providecommand\BIBentryALTinterwordspacing{\spaceskip=\fontdimen2\font plus " + write$ newline$ + "\BIBentryALTinterwordstretchfactor\fontdimen3\font minus \fontdimen4\font\relax}" + write$ newline$ + "\providecommand\BIBforeignlanguage[2]{{%" + write$ newline$ + "\expandafter\ifx\csname l@#1\endcsname\relax" + write$ newline$ + "\typeout{** WARNING: IEEEtran.bst: No hyphenation pattern has been}%" + write$ newline$ + "\typeout{** loaded for the language `#1'. Using the pattern for}%" + write$ newline$ + "\typeout{** the default language instead.}%" + write$ newline$ + "\else" + write$ newline$ + "\language=\csname l@#1\endcsname" + write$ newline$ + "\fi" + write$ newline$ + "#2}}" + write$ newline$ +} + +FUNCTION {end.bib} +{ newline$ "\end{thebibliography}" write$ newline$ } + +FUNCTION {if.url.alt.interword.spacing} +{ is.use.alt.interword.spacing + {url empty$ 'skip$ {"\BIBentryALTinterwordspacing" write$ newline$} if$} + { skip$ } + if$ +} + +FUNCTION {if.url.std.interword.spacing} +{ is.use.alt.interword.spacing + {url empty$ 'skip$ {"\BIBentrySTDinterwordspacing" write$ newline$} if$} + { skip$ } + if$ +} + + + + +%%%%%%%%%%%%%%%%%%%%%%%% +%% LONGEST LABEL PASS %% +%%%%%%%%%%%%%%%%%%%%%%%% + +FUNCTION {initialize.longest.label} +{ "" 'longest.label := + #1 'number.label := + #0 'longest.label.width := +} + +FUNCTION {longest.label.pass} +{ number.label int.to.str$ 'label := + number.label #1 + 'number.label := + label width$ longest.label.width > + { label 'longest.label := + label width$ 'longest.label.width := + } + 'skip$ + if$ +} + + + + +%%%%%%%%%%%%%%%%%%%%% +%% FORMAT HANDLERS %% +%%%%%%%%%%%%%%%%%%%%% + +%% Lower Level Formats (used by higher level formats) + +FUNCTION {format.address.org.or.pub.date} +{ 't := + "" + year empty$ + { "empty year in " cite$ * warning$ } + { skip$ } + if$ + address empty$ t empty$ and + year empty$ and month empty$ and + { skip$ } + { this.to.prev.status + this.status.std + cap.status.std + address "address" bibinfo.check * + t empty$ + { skip$ } + { punct.period 'prev.status.punct := + space.large 'prev.status.space := + address empty$ + { skip$ } + { ": " * } + if$ + t * + } + if$ + year empty$ month empty$ and + { skip$ } + { t empty$ address empty$ and + { skip$ } + { ", " * } + if$ + month empty$ + { year empty$ + { skip$ } + { year "year" bibinfo.check * } + if$ + } + { month "month" bibinfo.check * + year empty$ + { skip$ } + { " " * year "year" bibinfo.check * } + if$ + } + if$ + } + if$ + } + if$ +} + + +FUNCTION {format.names} +{ 'bibinfo := + duplicate$ empty$ 'skip$ { + this.to.prev.status + this.status.std + 's := + "" 't := + #1 'nameptr := + s num.names$ 'numnames := + numnames 'namesleft := + { namesleft #0 > } + { s nameptr + name.format.string + format.name$ + bibinfo bibinfo.check + 't := + nameptr #1 > + { nameptr num.names.shown.with.forced.et.al #1 + = + numnames max.num.names.before.forced.et.al > + is.forced.et.al and and + { "others" 't := + #1 'namesleft := + } + { skip$ } + if$ + namesleft #1 > + { ", " * t do.name.latex.cmd * } + { + numnames #2 > + { "," * } + 'skip$ + if$ + s nameptr "{ll}" format.name$ duplicate$ "others" = + { 't := } + { pop$ } + if$ + t "others" = + { + " " * bbl.etal emphasize * + } + { + bbl.and + space.word * t do.name.latex.cmd * + } + if$ + } + if$ + } + { t do.name.latex.cmd } + if$ + nameptr #1 + 'nameptr := + namesleft #1 - 'namesleft := + } + while$ + cap.status.std + } if$ +} + + + + +%% Higher Level Formats + +%% addresses/locations + +FUNCTION {format.address} +{ address duplicate$ empty$ 'skip$ + { this.to.prev.status + this.status.std + cap.status.std + } + if$ +} + + + +%% author/editor names + +FUNCTION {format.authors}{ author "author" format.names } + +FUNCTION {format.editors} +{ editor "editor" format.names duplicate$ empty$ 'skip$ + { ", " * + get.bbl.editor + capitalize + * + } + if$ +} + + + +%% date + +FUNCTION {format.date} +{ + month "month" bibinfo.check duplicate$ empty$ + year "year" bibinfo.check duplicate$ empty$ + { swap$ 'skip$ + { this.to.prev.status + this.status.std + cap.status.std + "there's a month but no year in " cite$ * warning$ } + if$ + * + } + { this.to.prev.status + this.status.std + cap.status.std + swap$ 'skip$ + { + swap$ + " " * swap$ + } + if$ + * + } + if$ +} + +FUNCTION {format.date.electronic} +{ month "month" bibinfo.check duplicate$ empty$ + year "year" bibinfo.check duplicate$ empty$ + { swap$ + { pop$ } + { "there's a month but no year in " cite$ * warning$ + pop$ ")" * "(" swap$ * + this.to.prev.status + punct.no 'this.status.punct := + space.normal 'this.status.space := + quote.no 'this.status.quote := + cap.yes 'status.cap := + } + if$ + } + { swap$ + { swap$ pop$ ")" * "(" swap$ * } + { "(" swap$ * ", " * swap$ * ")" * } + if$ + this.to.prev.status + punct.no 'this.status.punct := + space.normal 'this.status.space := + quote.no 'this.status.quote := + cap.yes 'status.cap := + } + if$ +} + + + +%% edition/title + +% Note: IEEE considers the edition to be closely associated with +% the title of a book. So, in IEEEtran.bst the edition is normally handled +% within the formatting of the title. The format.edition function is +% retained here for possible future use. +FUNCTION {format.edition} +{ edition duplicate$ empty$ 'skip$ + { this.to.prev.status + this.status.std + convert.edition + status.cap + { "t" } + { "l" } + if$ change.case$ + "edition" bibinfo.check + "~" * bbl.edition * + cap.status.std + } + if$ +} + +% This is used to format the booktitle of a conference proceedings. +% Here we use the "intype" field to provide the user a way to +% override the word "in" (e.g., with things like "presented at") +% Use of intype stops the emphasis of the booktitle to indicate that +% we no longer mean the written conference proceedings, but the +% conference itself. +FUNCTION {format.in.booktitle} +{ booktitle "booktitle" bibinfo.check duplicate$ empty$ 'skip$ + { this.to.prev.status + this.status.std + select.language + intype missing$ + { emphasize + bbl.in " " * + } + { intype " " * } + if$ + swap$ * + cap.status.std + } + if$ +} + +% This is used to format the booktitle of collection. +% Here the "intype" field is not supported, but "edition" is. +FUNCTION {format.in.booktitle.edition} +{ booktitle "booktitle" bibinfo.check duplicate$ empty$ 'skip$ + { this.to.prev.status + this.status.std + select.language + emphasize + edition empty$ 'skip$ + { ", " * + edition + convert.edition + "l" change.case$ + * "~" * bbl.edition * + } + if$ + bbl.in " " * swap$ * + cap.status.std + } + if$ +} + +FUNCTION {format.article.title} +{ title duplicate$ empty$ 'skip$ + { this.to.prev.status + this.status.std + "t" change.case$ + } + if$ + "title" bibinfo.check + duplicate$ empty$ 'skip$ + { quote.close 'this.status.quote := + is.last.char.not.punct + { punct.std 'this.status.punct := } + { punct.no 'this.status.punct := } + if$ + select.language + "``" swap$ * + cap.status.std + } + if$ +} + +FUNCTION {format.article.title.electronic} +{ title duplicate$ empty$ 'skip$ + { this.to.prev.status + this.status.std + cap.status.std + "t" change.case$ + } + if$ + "title" bibinfo.check + duplicate$ empty$ + { skip$ } + { select.language } + if$ +} + +FUNCTION {format.book.title.edition} +{ title "title" bibinfo.check + duplicate$ empty$ + { "empty title in " cite$ * warning$ } + { this.to.prev.status + this.status.std + select.language + emphasize + edition empty$ 'skip$ + { ", " * + edition + convert.edition + status.cap + { "t" } + { "l" } + if$ + change.case$ + * "~" * bbl.edition * + } + if$ + cap.status.std + } + if$ +} + +FUNCTION {format.book.title} +{ title "title" bibinfo.check + duplicate$ empty$ 'skip$ + { this.to.prev.status + this.status.std + cap.status.std + select.language + emphasize + } + if$ +} + + + +%% journal + +FUNCTION {format.journal} +{ journal duplicate$ empty$ 'skip$ + { this.to.prev.status + this.status.std + cap.status.std + select.language + emphasize + } + if$ +} + + + +%% how published + +FUNCTION {format.howpublished} +{ howpublished duplicate$ empty$ 'skip$ + { this.to.prev.status + this.status.std + cap.status.std + } + if$ +} + + + +%% institutions/organization/publishers/school + +FUNCTION {format.institution} +{ institution duplicate$ empty$ 'skip$ + { this.to.prev.status + this.status.std + cap.status.std + } + if$ +} + +FUNCTION {format.organization} +{ organization duplicate$ empty$ 'skip$ + { this.to.prev.status + this.status.std + cap.status.std + } + if$ +} + +FUNCTION {format.address.publisher.date} +{ publisher "publisher" bibinfo.warn format.address.org.or.pub.date } + +FUNCTION {format.address.publisher.date.nowarn} +{ publisher "publisher" bibinfo.check format.address.org.or.pub.date } + +FUNCTION {format.address.organization.date} +{ organization "organization" bibinfo.check format.address.org.or.pub.date } + +FUNCTION {format.school} +{ school duplicate$ empty$ 'skip$ + { this.to.prev.status + this.status.std + cap.status.std + } + if$ +} + + + +%% volume/number/series/chapter/pages + +FUNCTION {format.volume} +{ volume empty.field.to.null.string + duplicate$ empty$ 'skip$ + { this.to.prev.status + this.status.std + bbl.volume + status.cap + { capitalize } + { skip$ } + if$ + swap$ tie.or.space.prefix + "volume" bibinfo.check + * * + cap.status.std + } + if$ +} + +FUNCTION {format.number} +{ number empty.field.to.null.string + duplicate$ empty$ 'skip$ + { this.to.prev.status + this.status.std + status.cap + { bbl.number capitalize } + { bbl.number } + if$ + swap$ tie.or.space.prefix + "number" bibinfo.check + * * + cap.status.std + } + if$ +} + +FUNCTION {format.number.if.use.for.article} +{ is.use.number.for.article + { format.number } + { "" } + if$ +} + +% IEEE does not seem to tie the series so closely with the volume +% and number as is done in other bibliography styles. Instead the +% series is treated somewhat like an extension of the title. +FUNCTION {format.series} +{ series empty$ + { "" } + { this.to.prev.status + this.status.std + bbl.series " " * + series "series" bibinfo.check * + cap.status.std + } + if$ +} + + +FUNCTION {format.chapter} +{ chapter empty$ + { "" } + { this.to.prev.status + this.status.std + type empty$ + { bbl.chapter } + { type "l" change.case$ + "type" bibinfo.check + } + if$ + chapter tie.or.space.prefix + "chapter" bibinfo.check + * * + cap.status.std + } + if$ +} + + +% The intended use of format.paper is for paper numbers of inproceedings. +% The paper type can be overridden via the type field. +% We allow the type to be displayed even if the paper number is absent +% for things like "postdeadline paper" +FUNCTION {format.paper} +{ is.use.paper + { paper empty$ + { type empty$ + { "" } + { this.to.prev.status + this.status.std + type "type" bibinfo.check + cap.status.std + } + if$ + } + { this.to.prev.status + this.status.std + type empty$ + { bbl.paper } + { type "type" bibinfo.check } + if$ + " " * paper + "paper" bibinfo.check + * + cap.status.std + } + if$ + } + { "" } + if$ +} + + +FUNCTION {format.pages} +{ pages duplicate$ empty$ 'skip$ + { this.to.prev.status + this.status.std + duplicate$ is.multiple.pages + { + bbl.pages swap$ + n.dashify + } + { + bbl.page swap$ + } + if$ + tie.or.space.prefix + "pages" bibinfo.check + * * + cap.status.std + } + if$ +} + + + +%% technical report number + +FUNCTION {format.tech.report.number} +{ number "number" bibinfo.check + type duplicate$ empty$ + { pop$ + this.to.prev.status + this.status.std + cap.status.std + bbl.techrep + } + { skip$ } + if$ + "type" bibinfo.check + swap$ duplicate$ empty$ + { pop$ } + { this.to.prev.status + this.status.std + cap.status.std + tie.or.space.prefix * * } + if$ +} + + + +%% note + +FUNCTION {format.note} +{ note empty$ + { "" } + { this.to.prev.status + this.status.std + punct.period 'this.status.punct := + note #1 #1 substring$ + duplicate$ "{" = + { skip$ } + { status.cap + { "u" } + { "l" } + if$ + change.case$ + } + if$ + note #2 global.max$ substring$ * "note" bibinfo.check + cap.yes 'status.cap := + } + if$ +} + + + +%% patent + +FUNCTION {format.patent.date} +{ this.to.prev.status + this.status.std + year empty$ + { monthfiled duplicate$ empty$ + { "monthfiled" bibinfo.check pop$ "" } + { "monthfiled" bibinfo.check } + if$ + dayfiled duplicate$ empty$ + { "dayfiled" bibinfo.check pop$ "" * } + { "dayfiled" bibinfo.check + monthfiled empty$ + { "dayfiled without a monthfiled in " cite$ * warning$ + * + } + { " " swap$ * * } + if$ + } + if$ + yearfiled empty$ + { "no year or yearfiled in " cite$ * warning$ } + { yearfiled "yearfiled" bibinfo.check + swap$ + duplicate$ empty$ + { pop$ } + { ", " * swap$ * } + if$ + } + if$ + } + { month duplicate$ empty$ + { "month" bibinfo.check pop$ "" } + { "month" bibinfo.check } + if$ + day duplicate$ empty$ + { "day" bibinfo.check pop$ "" * } + { "day" bibinfo.check + month empty$ + { "day without a month in " cite$ * warning$ + * + } + { " " swap$ * * } + if$ + } + if$ + year "year" bibinfo.check + swap$ + duplicate$ empty$ + { pop$ } + { ", " * swap$ * } + if$ + } + if$ + cap.status.std +} + +FUNCTION {format.patent.nationality.type.number} +{ this.to.prev.status + this.status.std + nationality duplicate$ empty$ + { "nationality" bibinfo.warn pop$ "" } + { "nationality" bibinfo.check + duplicate$ "l" change.case$ "united states" = + { pop$ bbl.patentUS } + { skip$ } + if$ + " " * + } + if$ + type empty$ + { bbl.patent "type" bibinfo.check } + { type "type" bibinfo.check } + if$ + * + number duplicate$ empty$ + { "number" bibinfo.warn pop$ } + { "number" bibinfo.check + large.number.separate + swap$ " " * swap$ * + } + if$ + cap.status.std +} + + + +%% standard + +FUNCTION {format.organization.institution.standard.type.number} +{ this.to.prev.status + this.status.std + organization duplicate$ empty$ + { pop$ + institution duplicate$ empty$ + { "institution" bibinfo.warn } + { "institution" bibinfo.warn " " * } + if$ + } + { "organization" bibinfo.warn " " * } + if$ + type empty$ + { bbl.standard "type" bibinfo.check } + { type "type" bibinfo.check } + if$ + * + number duplicate$ empty$ + { "number" bibinfo.check pop$ } + { "number" bibinfo.check + large.number.separate + swap$ " " * swap$ * + } + if$ + cap.status.std +} + +FUNCTION {format.revision} +{ revision empty$ + { "" } + { this.to.prev.status + this.status.std + bbl.revision + revision tie.or.space.prefix + "revision" bibinfo.check + * * + cap.status.std + } + if$ +} + + +%% thesis + +FUNCTION {format.master.thesis.type} +{ this.to.prev.status + this.status.std + type empty$ + { + bbl.mthesis + } + { + type "type" bibinfo.check + } + if$ +cap.status.std +} + +FUNCTION {format.phd.thesis.type} +{ this.to.prev.status + this.status.std + type empty$ + { + bbl.phdthesis + } + { + type "type" bibinfo.check + } + if$ +cap.status.std +} + + + +%% URL + +FUNCTION {format.url} +{ url empty$ + { "" } + { this.to.prev.status + this.status.std + cap.yes 'status.cap := + bbl.urlprefix " " * + "\url{" * url * "}" * + punct.no 'this.status.punct := + punct.period 'prev.status.punct := + space.normal 'this.status.space := + space.normal 'prev.status.space := + quote.no 'this.status.quote := + } + if$ +} + + + + +%%%%%%%%%%%%%%%%%%%% +%% ENTRY HANDLERS %% +%%%%%%%%%%%%%%%%%%%% + + +% Note: In many journals, IEEE (or the authors) tend not to show the number +% for articles, so the display of the number is controlled here by the +% switch "is.use.number.for.article" +FUNCTION {article} +{ std.status.using.comma + start.entry + if.url.alt.interword.spacing + format.authors "author" output.warn + name.or.dash + format.article.title "title" output.warn + format.journal "journal" bibinfo.check "journal" output.warn + format.volume output + format.number.if.use.for.article output + format.pages output + format.date "year" output.warn + format.note output + format.url output + fin.entry + if.url.std.interword.spacing +} + +FUNCTION {book} +{ std.status.using.comma + start.entry + if.url.alt.interword.spacing + author empty$ + { format.editors "author and editor" output.warn } + { format.authors output.nonnull } + if$ + name.or.dash + format.book.title.edition output + format.series output + author empty$ + { skip$ } + { format.editors output } + if$ + format.address.publisher.date output + format.volume output + format.number output + format.note output + format.url output + fin.entry + if.url.std.interword.spacing +} + +FUNCTION {booklet} +{ std.status.using.comma + start.entry + if.url.alt.interword.spacing + format.authors output + name.or.dash + format.article.title "title" output.warn + format.howpublished "howpublished" bibinfo.check output + format.organization "organization" bibinfo.check output + format.address "address" bibinfo.check output + format.date output + format.note output + format.url output + fin.entry + if.url.std.interword.spacing +} + +FUNCTION {electronic} +{ std.status.using.period + start.entry + if.url.alt.interword.spacing + format.authors output + name.or.dash + format.date.electronic output + format.article.title.electronic output + format.howpublished "howpublished" bibinfo.check output + format.organization "organization" bibinfo.check output + format.address "address" bibinfo.check output + format.note output + format.url output + fin.entry + empty.entry.warn + if.url.std.interword.spacing +} + +FUNCTION {inbook} +{ std.status.using.comma + start.entry + if.url.alt.interword.spacing + author empty$ + { format.editors "author and editor" output.warn } + { format.authors output.nonnull } + if$ + name.or.dash + format.book.title.edition output + format.series output + format.address.publisher.date output + format.volume output + format.number output + format.chapter output + format.pages output + format.note output + format.url output + fin.entry + if.url.std.interword.spacing +} + +FUNCTION {incollection} +{ std.status.using.comma + start.entry + if.url.alt.interword.spacing + format.authors "author" output.warn + name.or.dash + format.article.title "title" output.warn + format.in.booktitle.edition "booktitle" output.warn + format.series output + format.editors output + format.address.publisher.date.nowarn output + format.volume output + format.number output + format.chapter output + format.pages output + format.note output + format.url output + fin.entry + if.url.std.interword.spacing +} + +FUNCTION {inproceedings} +{ std.status.using.comma + start.entry + if.url.alt.interword.spacing + format.authors "author" output.warn + name.or.dash + format.article.title "title" output.warn + format.in.booktitle "booktitle" output.warn + format.series output + format.editors output + format.volume output + format.number output + publisher empty$ + { format.address.organization.date output } + { format.organization "organization" bibinfo.check output + format.address.publisher.date output + } + if$ + format.paper output + format.pages output + format.note output + format.url output + fin.entry + if.url.std.interword.spacing +} + +FUNCTION {manual} +{ std.status.using.comma + start.entry + if.url.alt.interword.spacing + format.authors output + name.or.dash + format.book.title.edition "title" output.warn + format.howpublished "howpublished" bibinfo.check output + format.organization "organization" bibinfo.check output + format.address "address" bibinfo.check output + format.date output + format.note output + format.url output + fin.entry + if.url.std.interword.spacing +} + +FUNCTION {mastersthesis} +{ std.status.using.comma + start.entry + if.url.alt.interword.spacing + format.authors "author" output.warn + name.or.dash + format.article.title "title" output.warn + format.master.thesis.type output.nonnull + format.school "school" bibinfo.warn output + format.address "address" bibinfo.check output + format.date "year" output.warn + format.note output + format.url output + fin.entry + if.url.std.interword.spacing +} + +FUNCTION {misc} +{ std.status.using.comma + start.entry + if.url.alt.interword.spacing + format.authors output + name.or.dash + format.article.title output + format.howpublished "howpublished" bibinfo.check output + format.organization "organization" bibinfo.check output + format.address "address" bibinfo.check output + format.pages output + format.date output + format.note output + format.url output + fin.entry + empty.entry.warn + if.url.std.interword.spacing +} + +FUNCTION {patent} +{ std.status.using.comma + start.entry + if.url.alt.interword.spacing + format.authors output + name.or.dash + format.article.title output + format.patent.nationality.type.number output + format.patent.date output + format.note output + format.url output + fin.entry + empty.entry.warn + if.url.std.interword.spacing +} + +FUNCTION {periodical} +{ std.status.using.comma + start.entry + if.url.alt.interword.spacing + format.editors output + name.or.dash + format.book.title "title" output.warn + format.series output + format.volume output + format.number output + format.organization "organization" bibinfo.check output + format.date "year" output.warn + format.note output + format.url output + fin.entry + if.url.std.interword.spacing +} + +FUNCTION {phdthesis} +{ std.status.using.comma + start.entry + if.url.alt.interword.spacing + format.authors "author" output.warn + name.or.dash + format.article.title "title" output.warn + format.phd.thesis.type output.nonnull + format.school "school" bibinfo.warn output + format.address "address" bibinfo.check output + format.date "year" output.warn + format.note output + format.url output + fin.entry + if.url.std.interword.spacing +} + +FUNCTION {proceedings} +{ std.status.using.comma + start.entry + if.url.alt.interword.spacing + format.editors output + name.or.dash + format.book.title "title" output.warn + format.series output + format.volume output + format.number output + publisher empty$ + { format.address.organization.date output } + { format.organization "organization" bibinfo.check output + format.address.publisher.date output + } + if$ + format.note output + format.url output + fin.entry + if.url.std.interword.spacing +} + +FUNCTION {standard} +{ std.status.using.comma + start.entry + if.url.alt.interword.spacing + format.authors output + name.or.dash + format.book.title "title" output.warn + format.howpublished "howpublished" bibinfo.check output + format.organization.institution.standard.type.number output + format.revision output + format.date output + format.note output + format.url output + fin.entry + if.url.std.interword.spacing +} + +FUNCTION {techreport} +{ std.status.using.comma + start.entry + if.url.alt.interword.spacing + format.authors "author" output.warn + name.or.dash + format.article.title "title" output.warn + format.howpublished "howpublished" bibinfo.check output + format.institution "institution" bibinfo.warn output + format.address "address" bibinfo.check output + format.tech.report.number output.nonnull + format.date "year" output.warn + format.note output + format.url output + fin.entry + if.url.std.interword.spacing +} + +FUNCTION {unpublished} +{ std.status.using.comma + start.entry + if.url.alt.interword.spacing + format.authors "author" output.warn + name.or.dash + format.article.title "title" output.warn + format.date output + format.note "note" output.warn + format.url output + fin.entry + if.url.std.interword.spacing +} + + +% The special entry type which provides the user interface to the +% BST controls +FUNCTION {IEEEtranBSTCTL} +{ is.print.banners.to.terminal + { "** IEEEtran BST control entry " quote$ * cite$ * quote$ * " detected." * + top$ + } + { skip$ } + if$ + CTLuse_article_number + empty$ + { skip$ } + { CTLuse_article_number + yes.no.to.int + 'is.use.number.for.article := + } + if$ + CTLuse_paper + empty$ + { skip$ } + { CTLuse_paper + yes.no.to.int + 'is.use.paper := + } + if$ + CTLuse_forced_etal + empty$ + { skip$ } + { CTLuse_forced_etal + yes.no.to.int + 'is.forced.et.al := + } + if$ + CTLmax_names_forced_etal + empty$ + { skip$ } + { CTLmax_names_forced_etal + string.to.integer + 'max.num.names.before.forced.et.al := + } + if$ + CTLnames_show_etal + empty$ + { skip$ } + { CTLnames_show_etal + string.to.integer + 'num.names.shown.with.forced.et.al := + } + if$ + CTLuse_alt_spacing + empty$ + { skip$ } + { CTLuse_alt_spacing + yes.no.to.int + 'is.use.alt.interword.spacing := + } + if$ + CTLalt_stretch_factor + empty$ + { skip$ } + { CTLalt_stretch_factor + 'ALTinterwordstretchfactor := + "\renewcommand\BIBentryALTinterwordstretchfactor{" + ALTinterwordstretchfactor * "}" * + write$ newline$ + } + if$ + CTLdash_repeated_names + empty$ + { skip$ } + { CTLdash_repeated_names + yes.no.to.int + 'is.dash.repeated.names := + } + if$ + CTLname_format_string + empty$ + { skip$ } + { CTLname_format_string + 'name.format.string := + } + if$ + CTLname_latex_cmd + empty$ + { skip$ } + { CTLname_latex_cmd + 'name.latex.cmd := + } + if$ + + + num.names.shown.with.forced.et.al max.num.names.before.forced.et.al > + { "CTLnames_show_etal cannot be greater than CTLmax_names_forced_etal in " cite$ * warning$ + max.num.names.before.forced.et.al 'num.names.shown.with.forced.et.al := + } + { skip$ } + if$ +} + + +%%%%%%%%%%%%%%%%%%% +%% ENTRY ALIASES %% +%%%%%%%%%%%%%%%%%%% +FUNCTION {conference}{inproceedings} +FUNCTION {online}{electronic} +FUNCTION {internet}{electronic} +FUNCTION {webpage}{electronic} +FUNCTION {default.type}{misc} + + + +%%%%%%%%%%%%%%%%%% +%% MAIN PROGRAM %% +%%%%%%%%%%%%%%%%%% + +READ + +EXECUTE {initialize.controls} +EXECUTE {initialize.status.constants} +EXECUTE {banner.message} + +EXECUTE {initialize.longest.label} +ITERATE {longest.label.pass} + +EXECUTE {begin.bib} +ITERATE {call.type$} +EXECUTE {end.bib} + +EXECUTE{completed.message} + + +%% That's all folks, mds. diff --git a/doc/KPyCon2009/PulpForPythonProgrammers.pdf b/doc/KPyCon2009/PulpForPythonProgrammers.pdf new file mode 100644 index 0000000..9d7d2cc Binary files /dev/null and b/doc/KPyCon2009/PulpForPythonProgrammers.pdf differ diff --git a/doc/KPyCon2009/PulpForPythonProgrammers.tex b/doc/KPyCon2009/PulpForPythonProgrammers.tex new file mode 100644 index 0000000..653654e --- /dev/null +++ b/doc/KPyCon2009/PulpForPythonProgrammers.tex @@ -0,0 +1,343 @@ +% Document History: +% +%----------------------------------------------------------------------- +\documentclass[a4paper,oneside]{arlimsTPPM} +%------------------------------------------------------------------------- + +%--- Settings for the ARLIMS document class --- +%\setcounter{page}{8} % The starting page + +%--- This part of the preamble can be fiddled with according to our needs --- + +% Some packages that we like and want to use: +\usepackage{cite} +\usepackage{url} +\usepackage{graphicx} +\usepackage{color} +\usepackage[T1]{fontenc} +\usepackage[scaled]{beramono} + +% For typesetting listings. +\usepackage{listings} +\lstset{% Set some listing parameters. + numbers=left, + numberstyle=\sffamily\scriptsize, + numberblanklines=false, + basicstyle=\ttfamily\footnotesize, + showstringspaces=false, + backgroundcolor=\color[rgb]{0.9,0.9,0.9}, + %frame=tb, + %xleftmargin=4.3ex, + language=Python +} +\usepackage{hyperref} + +% The path(s) to the graphics files: +\graphicspath{{eps/}{pdf/}} + +% correct bad hyphenation here +%\hyphenation{cor-res-pon-ding net-works} + +% This makes line breaks look prettier +\sloppy + +% Conventions for typesetting vectors and matrices. +\renewcommand{\vec}[1]{\boldsymbol{#1}} +\newcommand{\mat}[1]{\boldsymbol{#1}} +% If \boldsymbol doesn't work, try \pmb (poor man's bold, which +% "multistrikes" every letter with small offsets. + +%--- Preamble definitions for actual content --- + +\title{An Introduction to pulp for Python Programmers} +\author{Stuart Mitchell} + +\institute{ +\begin{minipage}[t]{.45\textwidth}\centering +Light Metals Research Centre\\ +University of Auckland\\ +Auckland, New Zealand\\ +\textnormal{\texttt{\small s.mitchell@auckland.ac.nz}} +\end{minipage}} + + + +%------------------------------------------------------------------------- +\begin{document} + +% Put a proper title on the first page: +\maketitle + +%------------------------------------------------------------------------- +\begin{abstract} + Pulp-or (referred to as pulp for the rest of this paper) is a linear programming framework in Python. Pulp is licensed under a modified BSD license. The aim of pulp is to allow an Operations Research (OR) practitioner or programmer to express Linear Programming (LP), and Integer Programming (IP) models in python in a way similar to the conventional mathematical notation. Pulp + will also solve these problems using a variety of free and non-free LP solvers. Pulp models an LP in a natural and pythonic manner. +This paper is aimed at the python programmer who may wish to use pulp in their code. As such this paper contains a short introduction to LP models and their uses. + + % ARLIMS style + \paragraph{Keywords:} Linear programming; Operations Research; pulp-or. +\end{abstract} + +%------------------------------------------------------------------------- +\section{Introduction} + +Operations Research is known by the catch phrase ``The science of better''. From the website \cite{scienceofbetter} we find this brief description + +\begin{quote} +In a nutshell, operations research (O.R.) is the discipline of applying advanced analytical methods to help make better decisions. +\end{quote} + +The particular area of operations research where pulp is useful is the development and modelling of Linear Programming (LP) and Integer Programming (IP) problems. Mathematically, an LP problem is to find a point in a n-dimensional linearly constrained region that maximises a given linear objective function. IP is an LP where the solution must contain discrete variables which take an integer value at the solution, a common special case of an integer variable is a binary variable which must be either 0 or 1 at the solution. + +In general terms, an LP can describe a problem were decisions must be made (for example, the quantity of each ingredient in a can of cat food, detailed in section \ref{sec:whiskas}). These decisions are constrained by the properties of the problem that they model (the total weight of ingredients must be 100 grams and dietary requirements must be met). The quality of a solution is determined by some sort of cost (the total dollar cost to produce the can) and you seek to minimise or maximise the cost subject to the constraints. + + + +\section{A Brief introduction to Linear Programming} +\label{sect:LP} + +In this section we will introduce the reader to the structure of an LP problem and show the syntax used to formulate these models in pulp. The following is a example LP motivated by a real-world case-study. In general, problems of this sort are called `diet' or `blending' problems. An extensive set of case studies (including those below) can be found in \cite{pulpwiki}. + +\subsection{The Whiskas Cat Food Problem (whiskas.py)} +\label{sec:whiskas} + +\begin{figure}[h] + \centering +\includegraphics{images/whiskas_label.jpg} +\caption{A Whiskas cat food label.} +\end{figure} +Whiskas cat food, shown above, is manufactured by Uncle Ben's. Uncle Ben's want to produce their cat food products as cheaply as possible while ensuring they meet the stated nutritional analysis requirements shown on the cans. Thus they want to vary the quantities of each ingredient used (the main ingredients being chicken, beef, mutton, rice, wheat and gel) while still meeting their nutritional standards. + +\begin{figure}[h] + \centering +\includegraphics[scale=0.5]{images/whiskas_ingredients.jpg}\includegraphics[scale=0.5]{images/whiskas_nutrition.jpg} +\caption{Detail of ingredients and nutritional requirements.} +\end{figure} + +\begin{figure}[h] + \centering +\includegraphics[scale=0.6]{images/whiskas_blend.jpg} +\caption{The quantity of each ingredient must be determined.} +\end{figure} +The costs of the chicken, beef, and mutton are \$0.013, \$0.008 and \$0.010 respectively, while the costs of the rice, wheat and gel are \$0.002, \$0.005 and \$0.001 respectively. (All costs are per gram.) For this exercise we will ignore the vitamin and mineral ingredients. (Any costs for these are likely to be very small anyway.) +Each ingredient contributes to the total weight of protein, fat, fibre and salt in the final product which must be 100g (very convenient). The contributions (in grams) per gram of ingredient are given in the table below. + +\begin{table} +\centering +\begin{tabular}{|l|l|l|l|l|}\hline + & Protein & Fat & Fibre & Salt\\\hline +Chicken & 0.100 & 0.080 & 0.001 & 0.002\\ +Beef & 0.200 & 0.100 & 0.005 & 0.005\\ +Rice & 0.000 & 0.010 & 0.100 & 0.008\\ +Wheat bran & 0.040 & 0.010 & 0.150 & 0.000\\\hline +\end{tabular} +\caption{Table of ingredient compositions.} +\end{table} + +A Linear Program consists of three main parts. These are the variable definitions, the objective function and the constraints. + +\subsubsection{Variable definition} +In this problem the variables will be defined as the quantity (in grams) of each ingredient to include in the can. These variables will be continuous and will be able to take any non-negative value. Mathematically, we define set $I$ as the set of ingredients then create an $x$ variable indexed by $I$ + +\begin{eqnarray*} +I &=& \{chicken, beef, mutton, rice, wheat, gel\}\\ +x_i &\ge& 0 \quad \quad i \in I. +\end{eqnarray*} + +\lstinputlisting[linerange={4-12}]{code/whiskas.py} + +\subsubsection{Objective Function} +The objective function is to minimise the cost of production for each can. +\[ +\min \sum_i c_i x_i. +\] +Where: +\begin{itemize} + \item $c_i$ is the cost per gram of ingredient $i$. +\end{itemize} + +\lstinputlisting[linerange={14-17}]{code/whiskas.py} + +\subsubsection{Constraints} +Constraints serve to control the total mass of the ingredients and to model the protein, fat, salt and fibre requirements. + +\begin{align*} +\sum_i x_i &= 100 \; \text{\hspace{19 mm}can mass} \\ +\sum_i p_i x_i &\ge 8.0 \; \text{\hspace{20 mm}protein} \\ +\sum_i f_i x_i &\ge 6.0 \; \text{\hspace{20 mm}fat} \\ +\sum_i b_i x_i &\le 2.0 \; \text{\hspace{20 mm}fibre} \\ +\sum_i s_i x_i &\le 0.4 \; \text{\hspace{20 mm}salt}. +\end{align*} +Where: +\begin{itemize} + \item $p_i$ is the protein per gram of ingredient $i$; + \item $f_i$ is the fat per gram of ingredient $i$; + \item $b_i$ is the fibre per gram of ingredient $i$; + \item $s_i$ is the salt per gram of ingredient $i$. +\end{itemize} + +\lstinputlisting[linerange={18-29}]{code/whiskas.py} + +The pulp model must now be solved with some third party optimisation software. +Pulp currently supports solving with coin-or \cite{coin-or}, glpk \cite{glpk}, +CPLEX \cite{cplex} and Gurobi \cite{gurobi}. +The pulp download includes compiled versions of the coin-or solver for ubuntu and windows computers. + +\lstinputlisting[linerange={31-37}]{code/whiskas.py} + +\section{Other Example LP models} + +Modelling the diet problem is not the only application of linear programming. Other examples include: +\begin{itemize} + \item the transportation problem, + \item the set partitioning problem, + \item the assignment problem, + \item the knapsack problem. +\end{itemize} +In this section we will give short examples of the first two problems modelled in pulp together with a brief description. + +\subsection{The Transportation Problem (beerdistribution.py)} +A transportation problem involves that shipment of items from a set of sources +to a set of sinks (the sources and sinks must be disjoint sets), the problem seeks +to minimise the cost of shipping. In this case-study +crates of beer must be shipped from two breweries to five bars. + +\begin{figure}[h] + \centering + \includegraphics[scale = 0.3]{images/beerdistribution.png} + \caption{The brewery and bar capacities and possible routes.} +\end{figure} + + +The problem is created. +\lstinputlisting[linerange={37-38}]{code/beerdistribution.py} + +A list of possible routes for transportation is created: +\lstinputlisting[linerange={40-41}]{code/beerdistribution.py} + +The variables created determine the amount shipped on each route. The variables +are defined as \lstinline{pulp.LpInteger} therefore solutions must not +ship fractional numbers of crates. +\lstinputlisting[linerange={43-47}]{code/beerdistribution.py} + +The objective is to minimise the total shipping cost of the solution. +\lstinputlisting[linerange={48-50}]{code/beerdistribution.py} + +These constraints ensure that amount shipped from each brewery is less +than the supply available. The names given to the constraints will be preserved +when an `.lp' file is created. +\lstinputlisting[linerange={52-55}]{code/beerdistribution.py} + +These constraints ensure that amount shipped to each bar is greater +than the demand of the bar. These could also be equality constraints, depending +on how the problem is modelled. +\lstinputlisting[linerange={57-60}]{code/beerdistribution.py} + +\subsection{The Set Partitioning Problem (wedding.py)} +A set partitioning problem determines how the items in one set (S) can be partitioned into smaller +subsets. All items in S must be contained in one and only one partition. Related problems are: +\begin{itemize} + \item set packing - all items must be contained in zero or one partitions; + \item set covering - all items must be contained in at least one partition. +\end{itemize} +In this case study a wedding planner must determine guest seating +allocations for +a wedding. To model this problem the tables are modelled as the partitions and the guests invited to the wedding +are modelled as the elements of S. The wedding planner wishes to maximise the total happiness of all of the tables. + +A set partitioning problem may be modelled by explicitly enumerating each +possible subset. Though this approach does become intractable for large numbers of items (without using +column generation \cite{columngeneration}) it does have the advantage that the objective function co-efficients for +the partitions can be non-linear expressions (like happiness) and still allow this problem to be solved +using Linear Programming. + +First we use \lstinline{pulp.allcombinations} to generate a list of all possible table seatings. +\lstinputlisting[linerange={20-22}]{code/wedding.py} + +Then we create a binary variable that will be 1 if the table will be in the solution, or zero otherwise. +\lstinputlisting[linerange={24-28}]{code/wedding.py} + +We create the \lstinline{LpProblem} and then make the objective function. Note that +happiness function used in this script would be difficult to model in any other way. +\lstinputlisting[linerange={30-32}]{code/wedding.py} + +We specify the total number of tables allowed in the solution. +\lstinputlisting[linerange={34-35}]{code/wedding.py} + +This set of constraints defines the set partitioning problem by guaranteeing that a guest is allocated to +exactly one table. +\lstinputlisting[linerange={38-41}]{code/wedding.py} + + + +\section{Why pulp} +Modelling problems using LP and solving them with pulp can be a very useful approach for the python programmer. +This approach allows the programmer to focus on the modelling rather than the algorithms that find the solution. +The third party LP solvers that pulp interfaces with are all mature pieces of software that can be trusted to +produce the correct solution to the model created in pulp. The difference between these solvers, for a user of pulp, +lies mainly in the trade off between cost of the solver and its speed to find a solution. + +LP and IP are well researched areas of mathematics, therefore once a problem is stated as an LP model it is possible +to guarantee some properties of the solutions given. For instance, if the solver delivers an optimal solution it +will be impossible for someone to create a solution that is better (as measured by the objective function). + +Compared to other LP modelling tools including AMPL \cite{ampl}, GAMS \cite{gams}, mathprog \cite{glpk}, flopc++ \cite{flopc}, and pyomo \cite{pyomo} and poams; pulp offers a number of advantages. These include its: +\begin{itemize} +\item non-restrictive licensing; +\item ease of installation; +\item clear syntax; +\item interoperability with an number of solvers; +\item extensive documentation. +\end{itemize} + +\subsection{Licensing} +The licensing of pulp under a permissive open-source license allows pulp to be used as a medium for teaching +operations research as students can download and use pulp for free. The license allows OR practitioners to +integrate pulp in commercial applications for their clients without disclosing how the problem was solved. +The license also allows advanced users to modify and improve pulp for their own purposes. + +\subsection{Installation} +Pulp is very easy to install. The provision of a setup.py file and registration on pipy, allows the user +to use +\begin{verbatim} + $easy_install pulp-or +\end{verbatim} +to download and install pulp on their system. For windows and ubuntu users this binary package also includes +the coin-or \cite{coin-or} solver so pulp will be immediately functional. For users on other platforms a compatible +solver must be installed for a pulp model to be solved. + +\subsection{Syntax} +Ideally an LP modelling framework will allow a one-to-one translation of symbols from mathematical notation. If the syntax follows the formulation the programmer can ensure that the model written is the model required. The OR practitioner can also quickly read and understand the a model as written pulp, without having much python background. The standard mathematical notation is very concise and therefore code that mimics it will also be clear and concise. + +The use of a `Pythonic' construction of the pulp framework allows the programmer to use it easily in a variety of ways. Pulp can be directly imported into the local name-space (against python style) to allow simple LP models to be written by non-programmers, in fact most of the case studies on the wiki \cite{pulpwiki} use this style. Pulp also does not force the programmer to use any particular way to store parameter data. Parameter data can be stored as lists, dictionaries or custom classes. + +\subsection{Interoperability} +The ability for pulp to call a number of free and non-free solvers is important as the OR practitioner often wishes to compare the solution of a problem with a variety of solvers. The user may wish to develop simple LP models in a free solver and then provide a solution to a client using a commercial solver. The use of non-free solvers may dramatically reduce the solution time of the model. Pulp allows the free interchange of solvers without much change in the program, only a parameter for the \lstinline{LpProblem.solve} function is changed. + +\subsection{Documentation} +Pulp is supplied with a user guide with extensive examples (in fact it was converted from a course teaching LP modelling). This user guide and the permissive license were intended to make pulp useful to the largest possible audience. + +\section{Conclusion} +In conclusion pulp nicely bridges the gap between the OR practitioner and the python programmer. This allows the OR practitioner to use python to quickly develop and solve LP and IP models while having access to all of the tools available in the python standard library. The python programmer can now embed LP models in complex programs written in python. So next time you find yourself hacking up an algorithm to solve some problem in your code please consider if it would be appropriate to model this problem as an LP or IP instead. If so download and use pulp. + +\section*{Appendix - Example Code in full} +\subsection*{whiskas.py} +\lstinputlisting{code/whiskas.py} + +\subsection*{beerdistribution.py} +\lstinputlisting{code/beerdistribution.py} + +\subsection*{wedding.py} +\lstinputlisting{code/wedding.py} + + +%------------------------------------------------------------------------- +%--- back matter: bibliography --- +% These are in alphabetical by leading author order. +\newpage +\bibliographystyle{IEEEtran} +\bibliography{references} +%------------------------------------------------------------------------- +% We need this for the style to work properly, so please leave it in here: +\label{lastpagenum} +\end{document} diff --git a/doc/KPyCon2009/arlims.cls b/doc/KPyCon2009/arlims.cls new file mode 100644 index 0000000..8aa860a --- /dev/null +++ b/doc/KPyCon2009/arlims.cls @@ -0,0 +1,898 @@ +%% +%% This is file `arlims.cls', +%% It is based on the original `article.cls' from a tetex 3.0 +%% distribution (Ubuntu tetex-base 3.0-19 package). +%% +%% Additions to it were made by (probably) various authors at the +%% Massey University Institute of Information and Mathematical +%% Sciences. +%% +%% For further information contact Heath James +%% , Guy Kloss , Paul +%% Cowpertwait or anybody you expect +%% suitable for this purpose ... +%% +%% Copyright 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 +%% The LaTeX3 Project and any individual authors listed elsewhere +%% in this file. +%% +%% This file was generated from file(s) of the LaTeX base system. +%% Numerous changes were made to be fit for RLIMS. +%% -------------------------------------------------------------- +%% +%% Changes: +%% +%% * numerous changes previously to fit article.sty for the purpose +%% +%% version 1.5: +%% +%% * fixed the instructions +%% * added better default options to the template +%% * changed the year to 2008 +%% +%% version 1.4: +%% +%% * fitted to a newer version of article.sty +%% * cleanups in layout and formatting +%% * better compliance to LaTeX2e +%% * re-fitted \institute, \instituteB, ...C, ...D +%% it doesn't work 100% nicely, but it works) +%% * removed dependency on natbib.sty (so cite.sty, or else can be +%% used as liked) +%% * changed paper size to a4paper by default +%% * removed dependencies to graphicx.sty and subfigure.sty +%% (users like to choose and do that themselves) +%% +%% -------------------------------------------------------------- +%% +%% It may be distributed and/or modified under the +%% conditions of the LaTeX Project Public License, either version 1.3 +%% of this license or (at your option) any later version. +%% The latest version of this license is in +%% http://www.latex-project.org/lppl.txt +%% and version 1.3 or later is part of all distributions of LaTeX +%% version 2003/12/01 or later. +%% +%% This file has the LPPL maintenance status "maintained". +%% +%% This file may only be distributed together with a copy of the LaTeX +%% base system. You may however distribute the LaTeX base system without +%% such generated files. +%% +%% The list of all files belonging to the LaTeX base distribution is +%% given in the file `manifest.txt'. See also `legal.txt' for additional +%% information. +%% +%% The list of derived (unpacked) files belonging to the distribution +%% and covered by LPPL is defined by the unpacking scripts (with +%% extension .ins) which are part of the distribution. +%% \CharacterTable +%% {Upper-case \A\B\C\D\E\F\G\H\I\J\K\L\M\N\O\P\Q\R\S\T\U\V\W\X\Y\Z +%% Lower-case \a\b\c\d\e\f\g\h\i\j\k\l\m\n\o\p\q\r\s\t\u\v\w\x\y\z +%% Digits \0\1\2"s\4\5\6\7\8\9 +%% Exclamation \! Double quote \" Hash (number) \# +%% Dollar \$ Percent \% Ampersand \& +%% Acute accent \' Left paren \( Right paren \) +%% Asterisk \* Plus \+ Comma \, +%% Minus \- Point \. Solidus \/ +%% Colon \: Semicolon \; Less than \< +%% Equals \= Greater than \> Question mark \? +%% Commercial at \@ Left bracket \[ Backslash \\ +%% Right bracket \] Circumflex \^ Underscore \_ +%% Grave accent \` Left brace \{ Vertical bar \| +%% Right brace \} Tilde \~} +\NeedsTeXFormat{LaTeX2e}[1995/12/01] +\ProvidesClass{arlims} + [2008/04/23 v1.5 + Massey Research Letters Article Class] +\newcommand\@ptsize{} +\newif\if@restonecol +\newif\if@titlepage +\@titlepagefalse +\if@compatibility\else +\DeclareOption{a4paper} + {\setlength\paperheight {297mm}% + \setlength\paperwidth {210mm}} +\DeclareOption{a5paper} + {\setlength\paperheight {210mm}% + \setlength\paperwidth {148mm}} +\DeclareOption{b5paper} + {\setlength\paperheight {250mm}% + \setlength\paperwidth {176mm}} +\DeclareOption{letterpaper} + {\setlength\paperheight {11in}% + \setlength\paperwidth {8.5in}} +\DeclareOption{legalpaper} + {\setlength\paperheight {14in}% + \setlength\paperwidth {8.5in}} +\DeclareOption{executivepaper} + {\setlength\paperheight {10.5in}% + \setlength\paperwidth {7.25in}} +\DeclareOption{landscape} + {\setlength\@tempdima {\paperheight}% + \setlength\paperheight {\paperwidth}% + \setlength\paperwidth {\@tempdima}} +\fi +\if@compatibility + \renewcommand\@ptsize{0} +\else +\DeclareOption{10pt}{\renewcommand\@ptsize{0}} +\fi +\DeclareOption{11pt}{\renewcommand\@ptsize{1}} +\DeclareOption{12pt}{\renewcommand\@ptsize{2}} +\if@compatibility\else +\DeclareOption{oneside}{\@twosidefalse \@mparswitchfalse} +\fi +\DeclareOption{twoside}{\@twosidetrue \@mparswitchtrue} +\DeclareOption{draft}{\setlength\overfullrule{5pt}} +\if@compatibility\else +\DeclareOption{final}{\setlength\overfullrule{0pt}} +\fi +\DeclareOption{titlepage}{\@titlepagetrue} +\if@compatibility\else +\DeclareOption{notitlepage}{\@titlepagefalse} +\fi +\if@compatibility\else +\DeclareOption{onecolumn}{\@twocolumnfalse} +\fi +\DeclareOption{twocolumn}{\@twocolumntrue} +\DeclareOption{leqno}{\input{leqno.clo}} +\DeclareOption{fleqn}{\input{fleqn.clo}} +\DeclareOption{openbib}{% + \AtEndOfPackage{% + \renewcommand\@openbib@code{% + \advance\leftmargin\bibindent + \itemindent -\bibindent + \listparindent \itemindent + \parsep \z@ + }% + \renewcommand\newblock{\par}}% +} +\ExecuteOptions{a4paper,10pt,oneside,onecolumn,final} +\ProcessOptions +\input{size1\@ptsize.clo} +\setlength\lineskip{1\p@} +\setlength\normallineskip{1\p@} +\renewcommand\baselinestretch{} +\setlength\parskip{0\p@ \@plus \p@} +\@lowpenalty 51 +\@medpenalty 151 +\@highpenalty 301 +\setcounter{topnumber}{2} +\renewcommand\topfraction{.7} +\setcounter{bottomnumber}{1} +\renewcommand\bottomfraction{.3} +\setcounter{totalnumber}{3} +\renewcommand\textfraction{.2} +\renewcommand\floatpagefraction{.5} +\setcounter{dbltopnumber}{2} +\renewcommand\dbltopfraction{.7} +\renewcommand\dblfloatpagefraction{.5} +\if@twoside + \def\ps@headings{% + \let\@oddfoot\@empty\let\@evenfoot\@empty + \def\@evenhead{\thepage\hfil\slshape\leftmark}% + \def\@oddhead{{\slshape\rightmark}\hfil\thepage}% + \let\@mkboth\markboth + \def\sectionmark##1{% + \markboth {\MakeUppercase{% + \ifnum \c@secnumdepth >\z@ + \thesection\quad + \fi + ##1}}{}}% + \def\subsectionmark##1{% + \markright {% + \ifnum \c@secnumdepth >\@ne + \thesubsection\quad + \fi + ##1}}} +\else + \def\ps@headings{% + \let\@oddfoot\@empty + \def\@oddhead{{\slshape\rightmark}\hfil\thepage}% + \let\@mkboth\markboth + \def\sectionmark##1{% + \markright {\MakeUppercase{% + \ifnum \c@secnumdepth >\m@ne + \thesection\quad + \fi + ##1}}}} +\fi +\def\ps@myheadings{% + \let\@oddfoot\@empty\let\@evenfoot\@empty + \def\@evenhead{\thepage\hfil\slshape\leftmark}% + \def\@oddhead{{\slshape\rightmark}\hfil\thepage}% + \let\@mkboth\@gobbletwo + \let\sectionmark\@gobble + \let\subsectionmark\@gobble + } + + +%---------- old definition of title page ------------------------------- +\newif\ifGrxlb\ifGrxlb + + \if@titlepage + \newcommand\maketitle{\begin{titlepage}% + \let\footnotesize\small + \let\footnoterule\relax + \let \footnote \thanks + \null\vfil + \vskip 60\p@ + \begin{center}% + {\LARGE \@title \par}% + \vskip 3em% + {\large + \lineskip .75em% + \begin{tabular}[t]{c}% + \@author + \end{tabular}\par}% + \vskip 1.5em% + {\large \@date \par}% % Set date in \large size. + \end{center}\par + \@thanks + \vfil\null + \end{titlepage}% + \setcounter{footnote}{0}% + \global\let\thanks\relax + \global\let\maketitle\relax + \global\let\@thanks\@empty + \global\let\@author\@empty + \global\let\@date\@empty + \global\let\@title\@empty + \global\let\title\relax + \global\let\author\relax + \global\let\date\relax + \global\let\and\relax +} +\else +\newcommand\maketitle{\par + \begingroup + \renewcommand\thefootnote{\@fnsymbol\c@footnote}% + \def\@makefnmark{\rlap{\@textsuperscript{\normalfont\@thefnmark}}}% + \long\def\@makefntext##1{\parindent 1em\noindent + \hb@xt@1.8em{% + \hss\@textsuperscript{\normalfont\@thefnmark}}##1}% + \if@twocolumn + \ifnum \col@number=\@ne + \@maketitle + \else + \twocolumn[\@maketitle]% + \fi + \else + \newpage + \global\@topnum\z@ % Prevents figures from going at top of page. + \@maketitle + \fi + \thispagestyle{plain}\@thanks + \endgroup + \setcounter{footnote}{0}% + \global\let\thanks\relax + \global\let\maketitle\relax + \global\let\@maketitle\relax + \global\let\@thanks\@empty + \global\let\@author\@empty + \global\let\@date\@empty + \global\let\@title\@empty + \global\let\title\relax + \global\let\author\relax + \global\let\date\relax + \global\let\and\relax +} +\def\@maketitle{% + \newpage + \null + \vskip 2em% + \begin{center}% + \let \footnote \thanks + {\LARGE \@title \par}% + \vskip 1.5em% + {\large + \lineskip .5em% + \begin{tabular}[t]{c}% + \@author + \end{tabular}\par}% + \vskip 1em% + {\large \@date}% + \end{center}% + \par + \vskip 1.5em} +\fi + +\fi +%---------------- end of old definition of title page --------------------- + +\setcounter{secnumdepth}{3} +\newcounter {part} +\newcounter {section} +\newcounter {subsection}[section] +\newcounter {subsubsection}[subsection] +\newcounter {paragraph}[subsubsection] +\newcounter {subparagraph}[paragraph] +\renewcommand \thepart {\@Roman\c@part} +\renewcommand \thesection {\@arabic\c@section} +\renewcommand\thesubsection {\thesection.\@arabic\c@subsection} +\renewcommand\thesubsubsection{\thesubsection .\@arabic\c@subsubsection} +\renewcommand\theparagraph {\thesubsubsection.\@arabic\c@paragraph} +\renewcommand\thesubparagraph {\theparagraph.\@arabic\c@subparagraph} +\newcommand\part{% + \if@noskipsec \leavevmode \fi + \par + \addvspace{4ex}% + \@afterindentfalse + \secdef\@part\@spart} + +\def\@part[#1]#2{% + \ifnum \c@secnumdepth >\m@ne + \refstepcounter{part}% + \addcontentsline{toc}{part}{\thepart\hspace{1em}#1}% + \else + \addcontentsline{toc}{part}{#1}% + \fi + {\parindent \z@ \raggedright + \interlinepenalty \@M + \normalfont + \ifnum \c@secnumdepth >\m@ne + \Large\bfseries \partname\nobreakspace\thepart + \par\nobreak + \fi + \huge \bfseries #2% + \markboth{}{}\par}% + \nobreak + \vskip 3ex + \@afterheading} +\def\@spart#1{% + {\parindent \z@ \raggedright + \interlinepenalty \@M + \normalfont + \huge \bfseries #1\par}% + \nobreak + \vskip 3ex + \@afterheading} +\newcommand\section{\@startsection {section}{1}{\z@}% + {-3.5ex \@plus -1ex \@minus -.2ex}% + {2.3ex \@plus.2ex}% + {\normalfont\Large\bfseries}} +\newcommand\subsection{\@startsection{subsection}{2}{\z@}% + {-3.25ex\@plus -1ex \@minus -.2ex}% + {1.5ex \@plus .2ex}% + {\normalfont\large\bfseries}} +\newcommand\subsubsection{\@startsection{subsubsection}{3}{\z@}% + {-3.25ex\@plus -1ex \@minus -.2ex}% + {1.5ex \@plus .2ex}% + {\normalfont\normalsize\bfseries}} +\newcommand\paragraph{\@startsection{paragraph}{4}{\z@}% + {3.25ex \@plus1ex \@minus.2ex}% + {-1em}% + {\normalfont\normalsize\bfseries}} +\newcommand\subparagraph{\@startsection{subparagraph}{5}{\parindent}% + {3.25ex \@plus1ex \@minus .2ex}% + {-1em}% + {\normalfont\normalsize\bfseries}} +\if@twocolumn + \setlength\leftmargini {2em} +\else + \setlength\leftmargini {2.5em} +\fi +\leftmargin \leftmargini +\setlength\leftmarginii {2.2em} +\setlength\leftmarginiii {1.87em} +\setlength\leftmarginiv {1.7em} +\if@twocolumn + \setlength\leftmarginv {.5em} + \setlength\leftmarginvi {.5em} +\else + \setlength\leftmarginv {1em} + \setlength\leftmarginvi {1em} +\fi +\setlength \labelsep {.5em} +\setlength \labelwidth{\leftmargini} +\addtolength\labelwidth{-\labelsep} +\@beginparpenalty -\@lowpenalty +\@endparpenalty -\@lowpenalty +\@itempenalty -\@lowpenalty +\renewcommand\theenumi{\@arabic\c@enumi} +\renewcommand\theenumii{\@alph\c@enumii} +\renewcommand\theenumiii{\@roman\c@enumiii} +\renewcommand\theenumiv{\@Alph\c@enumiv} +\newcommand\labelenumi{\theenumi.} +\newcommand\labelenumii{(\theenumii)} +\newcommand\labelenumiii{\theenumiii.} +\newcommand\labelenumiv{\theenumiv.} +\renewcommand\p@enumii{\theenumi} +\renewcommand\p@enumiii{\theenumi(\theenumii)} +\renewcommand\p@enumiv{\p@enumiii\theenumiii} +\newcommand\labelitemi{\textbullet} +\newcommand\labelitemii{\normalfont\bfseries \textendash} +\newcommand\labelitemiii{\textasteriskcentered} +\newcommand\labelitemiv{\textperiodcentered} +\newenvironment{description} + {\list{}{\labelwidth\z@ \itemindent-\leftmargin + \let\makelabel\descriptionlabel}} + {\endlist} +\newcommand*\descriptionlabel[1]{\hspace\labelsep + \normalfont\bfseries #1} +\if@titlepage + \newenvironment{abstract}{% + \titlepage + \null\vfil + \@beginparpenalty\@lowpenalty + \begin{center}% + \bfseries \abstractname + \@endparpenalty\@M + \end{center}}% + {\par\vfil\null\endtitlepage} +\else + \newenvironment{abstract}{% + \if@twocolumn + \section*{\abstractname}% + \else + \small + \begin{center}% + {\bfseries \abstractname\vspace{-.5em}\vspace{\z@}}% + \end{center}% + \quotation + \fi} + {\if@twocolumn\else\endquotation\fi} +\fi +\newenvironment{verse} + {\let\\\@centercr + \list{}{\itemsep \z@ + \itemindent -1.5em% + \listparindent\itemindent + \rightmargin \leftmargin + \advance\leftmargin 1.5em}% + \item\relax} + {\endlist} +\newenvironment{quotation} + {\list{}{\listparindent 1.5em% + \itemindent \listparindent + \rightmargin \leftmargin + \parsep \z@ \@plus\p@}% + \item\relax} + {\endlist} +\newenvironment{quote} + {\list{}{\rightmargin\leftmargin}% + \item\relax} + {\endlist} +\if@compatibility +\newenvironment{titlepage} + {% + \if@twocolumn + \@restonecoltrue\onecolumn + \else + \@restonecolfalse\newpage + \fi + \thispagestyle{empty}% + \setcounter{page}\z@ + }% + {\if@restonecol\twocolumn \else \newpage \fi + } +\else +\newenvironment{titlepage} + {% + \if@twocolumn + \@restonecoltrue\onecolumn + \else + \@restonecolfalse\newpage + \fi + \thispagestyle{empty}% + \setcounter{page}\@ne + }% + {\if@restonecol\twocolumn \else \newpage \fi + \if@twoside\else + \setcounter{page}\@ne + \fi + } +\fi +\newcommand\appendix{\par + \setcounter{section}{0}% + \setcounter{subsection}{0}% + \gdef\thesection{\@Alph\c@section}} +\setlength\arraycolsep{5\p@} +\setlength\tabcolsep{6\p@} +\setlength\arrayrulewidth{.4\p@} +\setlength\doublerulesep{2\p@} +\setlength\tabbingsep{\labelsep} +\skip\@mpfootins = \skip\footins +\setlength\fboxsep{3\p@} +\setlength\fboxrule{.4\p@} +\renewcommand \theequation {\@arabic\c@equation} +\newcounter{figure} +\renewcommand \thefigure {\@arabic\c@figure} +\def\fps@figure{tbp} +\def\ftype@figure{1} +\def\ext@figure{lof} +\def\fnum@figure{\figurename\nobreakspace\thefigure} +\newenvironment{figure} + {\@float{figure}} + {\end@float} +\newenvironment{figure*} + {\@dblfloat{figure}} + {\end@dblfloat} +\newcounter{table} +\renewcommand\thetable{\@arabic\c@table} +\def\fps@table{tbp} +\def\ftype@table{2} +\def\ext@table{lot} +\def\fnum@table{\tablename\nobreakspace\thetable} +\newenvironment{table} + {\@float{table}} + {\end@float} +\newenvironment{table*} + {\@dblfloat{table}} + {\end@dblfloat} +\newlength\abovecaptionskip +\newlength\belowcaptionskip +\setlength\abovecaptionskip{10\p@} +\setlength\belowcaptionskip{0\p@} +\long\def\@makecaption#1#2{% + \vskip\abovecaptionskip + \sbox\@tempboxa{#1: #2}% + \ifdim \wd\@tempboxa >\hsize + #1: #2\par + \else + \global \@minipagefalse + \hb@xt@\hsize{\hfil\box\@tempboxa\hfil}% + \fi + \vskip\belowcaptionskip} +\DeclareOldFontCommand{\rm}{\normalfont\rmfamily}{\mathrm} +\DeclareOldFontCommand{\sf}{\normalfont\sffamily}{\mathsf} +\DeclareOldFontCommand{\tt}{\normalfont\ttfamily}{\mathtt} +\DeclareOldFontCommand{\bf}{\normalfont\bfseries}{\mathbf} +\DeclareOldFontCommand{\it}{\normalfont\itshape}{\mathit} +\DeclareOldFontCommand{\sl}{\normalfont\slshape}{\@nomath\sl} +\DeclareOldFontCommand{\sc}{\normalfont\scshape}{\@nomath\sc} +\DeclareRobustCommand*\cal{\@fontswitch\relax\mathcal} +\DeclareRobustCommand*\mit{\@fontswitch\relax\mathnormal} +\newcommand\@pnumwidth{1.55em} +\newcommand\@tocrmarg{2.55em} +\newcommand\@dotsep{4.5} +\setcounter{tocdepth}{3} +\newcommand\tableofcontents{% + \section*{\contentsname + \@mkboth{% + \MakeUppercase\contentsname}{\MakeUppercase\contentsname}}% + \@starttoc{toc}% + } +\newcommand*\l@part[2]{% + \ifnum \c@tocdepth >-2\relax + \addpenalty\@secpenalty + \addvspace{2.25em \@plus\p@}% + \setlength\@tempdima{3em}% + \begingroup + \parindent \z@ \rightskip \@pnumwidth + \parfillskip -\@pnumwidth + {\leavevmode + \large \bfseries #1\hfil \hb@xt@\@pnumwidth{\hss #2}}\par + \nobreak + \if@compatibility + \global\@nobreaktrue + \everypar{\global\@nobreakfalse\everypar{}}% + \fi + \endgroup + \fi} +\newcommand*\l@section[2]{% + \ifnum \c@tocdepth >\z@ + \addpenalty\@secpenalty + \addvspace{1.0em \@plus\p@}% + \setlength\@tempdima{1.5em}% + \begingroup + \parindent \z@ \rightskip \@pnumwidth + \parfillskip -\@pnumwidth + \leavevmode \bfseries + \advance\leftskip\@tempdima + \hskip -\leftskip + #1\nobreak\hfil \nobreak\hb@xt@\@pnumwidth{\hss #2}\par + \endgroup + \fi} +\newcommand*\l@subsection{\@dottedtocline{2}{1.5em}{2.3em}} +\newcommand*\l@subsubsection{\@dottedtocline{3}{3.8em}{3.2em}} +\newcommand*\l@paragraph{\@dottedtocline{4}{7.0em}{4.1em}} +\newcommand*\l@subparagraph{\@dottedtocline{5}{10em}{5em}} +\newcommand\listoffigures{% + \section*{\listfigurename}% + \@mkboth{\MakeUppercase\listfigurename}% + {\MakeUppercase\listfigurename}% + \@starttoc{lof}% + } +\newcommand*\l@figure{\@dottedtocline{1}{1.5em}{2.3em}} +\newcommand\listoftables{% + \section*{\listtablename}% + \@mkboth{% + \MakeUppercase\listtablename}% + {\MakeUppercase\listtablename}% + \@starttoc{lot}% + } +\let\l@table\l@figure +\newdimen\bibindent +\setlength\bibindent{1.5em} +\newenvironment{thebibliography}[1] + {\section*{\refname}% + \@mkboth{\MakeUppercase\refname}{\MakeUppercase\refname}% + \list{\@biblabel{\@arabic\c@enumiv}}% + {\settowidth\labelwidth{\@biblabel{#1}}% + \leftmargin\labelwidth + \advance\leftmargin\labelsep + \@openbib@code + \usecounter{enumiv}% + \let\p@enumiv\@empty + \renewcommand\theenumiv{\@arabic\c@enumiv}}% + \sloppy + \clubpenalty4000 + \@clubpenalty \clubpenalty + \widowpenalty4000% + \sfcode`\.\@m} + {\def\@noitemerr + {\@latex@warning{Empty `thebibliography' environment}}% + \endlist} +\newcommand\newblock{\hskip .11em\@plus.33em\@minus.07em} +\let\@openbib@code\@empty +\newenvironment{theindex} + {\if@twocolumn + \@restonecolfalse + \else + \@restonecoltrue + \fi + \twocolumn[\section*{\indexname}]% + \@mkboth{\MakeUppercase\indexname}% + {\MakeUppercase\indexname}% + \thispagestyle{plain}\parindent\z@ + \parskip\z@ \@plus .3\p@\relax + \columnseprule \z@ + \columnsep 35\p@ + \let\item\@idxitem} + {\if@restonecol\onecolumn\else\clearpage\fi} +\newcommand\@idxitem{\par\hangindent 40\p@} +\newcommand\subitem{\@idxitem \hspace*{20\p@}} +\newcommand\subsubitem{\@idxitem \hspace*{30\p@}} +\newcommand\indexspace{\par \vskip 10\p@ \@plus5\p@ \@minus3\p@\relax} +\renewcommand\footnoterule{% + \kern-3\p@ + \hrule\@width.4\columnwidth + \kern2.6\p@} +\newcommand\@makefntext[1]{% + \parindent 1em% + \noindent + \hb@xt@1.8em{\hss\@makefnmark}#1} +\newcommand\contentsname{Contents} +\newcommand\listfigurename{List of Figures} +\newcommand\listtablename{List of Tables} +\newcommand\refname{References} +\newcommand\indexname{Index} +\newcommand\figurename{Figure} +\newcommand\tablename{Table} +\newcommand\partname{Part} +\newcommand\appendixname{Appendix} +\newcommand\abstractname{} +\def\today{\ifcase\month\or + January\or February\or March\or April\or May\or June\or + July\or August\or September\or October\or November\or December\fi + \space\number\day, \number\year} +\setlength\columnsep{10\p@} +\setlength\columnseprule{0\p@} +\pagestyle{plain} +\pagenumbering{arabic} + +% === ARLIMS adaptions following ============================================ +\pagestyle{empty} + +\setlength{\oddsidemargin}{1.5 cm} +\setlength{\evensidemargin}{-0.5 cm} +\setlength{\textheight}{22 cm} +\setlength{\textwidth}{15 cm} +\setlength{\topmargin}{0 cm} + +% --- special indexing to cope with the list of articles +\newif\ifIndexMade +\def\makeartIndex{% + \protected@write1{}{\string\global\string\IndexMadetrue} + \newwrite\@artIndexfile + \immediate\openout\@artIndexfile=\jobname.adx + \def\artIndex{\@bsphack\begingroup\@sanitize\@wrartIndex} + \typeout{Writing artIndex file \jobname.adx}% + \let\makeartIndex\@empty +} + +\def\@wrartIndex#1{% + \protected@write\@artIndexfile{}{% + \string\artIndexentry{\factTitle}% + {\thepage}% + {\string\last#1}% + {\factAuthor\factAuthorB\factAuthorC\factAuthorD}}% + \endgroup% + \@esphack% +} + +\def\artIndex{\@bsphack\begingroup \@sanitize\@artIndex} +\def\@artIndex#1{\endgroup\@esphack} + +\def\artIndexLast#1{% + \protected@write1{}{\string\gdef\string\last#1{\thepage}}% +} + +% --- to set up the headers permanently +\newcommand{\markperm}[2]{% + \global\def\@evenhead{\arabic{page}\hfil{#1}}% + \global\def\@oddhead{{#2}\hfil\arabic{page}} +} +% (yes, I know I've hardwired arabic page numbering) + +% --- to set up the headers with a once off first +\newcommand{\markonce}[3]{% + \global\def\@evenhead{\arabic{page}\hfil{#1}\markperm{#2}{#3}}% + \global\def\@oddhead{{#1}\hfil\arabic{page}\markperm{#2}{#3}}% +} + +%% Put a line after the abstract %% +\newcommand{\drawline}{% + \vspace*{1mm} + \begin{center} + \rule{\textwidth}{0.2mm} + \end{center} + \vspace*{1mm} +} + +\newcommand{\reff}[1]{(\ref{#1})} + + +% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +% Define the author and title commands. Bascially each just stuffs +% the argument into a storage command. I need to define the blank +% storage commands because I am using the "renewcomand", since I +% need to do this for each article. + +\def\author{} +\def\institute{} + +\def\factTitle{} + +\def\factAuthor{} +\def\factAuthorB{} +\def\factAuthorC{} +\def\factAuthorD{} + +\def\factInstitute{} +\def\factInstituteB{} +\def\factInstituteC{} +\def\factInstituteD{} + +\def\footInst{} +\def\footInstB{} +\def\footInstC{} +\def\footInstD{} + +\def\footAuthorInst{} +\def\footAuthorInstB{} +\def\footAuthorInstC{} +\def\footAuthorInstD{} + +\renewcommand{\author}[2][]{ + \renewcommand{\footAuthorInst}{#1} + \renewcommand{\factAuthor}{#2} +} + +\newcommand{\authorB}[2][]{ + \renewcommand{\footAuthorInstB}{#1} + \renewcommand{\factAuthorB}{#2} +} + +\newcommand{\authorC}[2][]{ + \renewcommand{\footAuthorInstC}{#1} + \renewcommand{\factAuthorC}{#2} +} + +\newcommand{\authorD}[2][]{ + \renewcommand{\footAuthorInstD}{#1} + \renewcommand{\factAuthorD}{#2} +} + +\renewcommand{\institute}[2][]{ + \renewcommand{\footInst}{#1} + \renewcommand{\factInstitute}{#2} +} + +\newcommand{\instituteB}[2][*]{ + \renewcommand{\footInstB}{#1} + \renewcommand{\factInstituteB}{#2} +} + +\newcommand{\instituteC}[2][**]{ + \renewcommand{\footInstC}{#1} + \renewcommand{\factInstituteC}{#2} +} + +\newcommand{\instituteD}[2][***]{ + \renewcommand{\footInstD}{#1} + \renewcommand{\factInstituteD}{#2} +} + +\def\nameTag{} + +\renewcommand{\title}[1]{ + \renewcommand{\factTitle}{#1} +} + +\newcommand{\iimsaddress}{ + \begin{center} + \textit{Institute of Information \& Mathematical Sciences\\ + Massey University at Albany, Auckland, New Zealand.} + \end{center} +} + +\newcommand{\maketitle}[1][\factTitle]{ + \begin{center} + \markonce{ + \parbox{10cm}{\small + \textit{Res.\ Lett.\ Inf.\ Math.\ Sci.}, 2009, Vol.~\textbf{13}, pp.~\arabic{page}--\pageref{lastpagenum} \\[1mm] + Available online at http://iims.massey.ac.nz/research/letters/ + } + } + {\factAuthor\factAuthorB\factAuthorC\factAuthorD} + {#1} + + \vspace*{1cm} + + \begin{minipage}{12cm} + \begin{center} + {\bfseries\Large\factTitle\par} + \vspace*{2mm} + \end{center} + \end{minipage} + + \vspace*{3mm} + + \textsc{% + \factAuthor $^{\footAuthorInst}$% + \factAuthorB $^{\footAuthorInstB}$% + \factAuthorC $^{\footAuthorInstC}$% + \factAuthorD $^{\footAuthorInstD}$% + } + + \vspace*{1mm} + + $^{\footInst}$\textit{\factInstitute} + $^{\footInstB}$\textit{\factInstituteB} + $^{\footInstC}$\textit{\factInstituteC} + $^{\footInstD}$\textit{\factInstituteD} + + \artIndex{\nameTag} + \end{center} +} + +% calculate the margins so that the text on opposite sides +% of the same sheet is lined up at the edges +% ok, its a fudge, but you can change the oddsidemargin +% to shift both sides in sync. + +\oddsidemargin1cm +\evensidemargin\paperwidth +\addtolength{\evensidemargin}{-\oddsidemargin} +\addtolength{\evensidemargin}{-205mm} + +% --- turn off a few things ------------------------------------------------- +\renewcommand{\markboth}[2]{} +\renewcommand{\markright}[1]{} +\newcommand{\markheading}[1]{} + +\usepackage{amsmath,amsfonts,dsfont} +\usepackage{amsthm} + +\newif\ifCombinedRlims + +% === end of ARLIMS adaptions =============================================== + +\if@twoside +\else + \raggedbottom +\fi +\if@twocolumn + \twocolumn + \sloppy + \flushbottom +\else + \onecolumn +\fi +\endinput +%% +%% End of file `arlims.cls'. diff --git a/doc/KPyCon2009/arlimsTPP.cls b/doc/KPyCon2009/arlimsTPP.cls new file mode 100644 index 0000000..7caef72 --- /dev/null +++ b/doc/KPyCon2009/arlimsTPP.cls @@ -0,0 +1,898 @@ +%% +%% This is file `arlims.cls', +%% It is based on the original `article.cls' from a tetex 3.0 +%% distribution (Ubuntu tetex-base 3.0-19 package). +%% +%% Additions to it were made by (probably) various authors at the +%% Massey University Institute of Information and Mathematical +%% Sciences. +%% +%% For further information contact Heath James +%% , Guy Kloss , Paul +%% Cowpertwait or anybody you expect +%% suitable for this purpose ... +%% +%% Copyright 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 +%% The LaTeX3 Project and any individual authors listed elsewhere +%% in this file. +%% +%% This file was generated from file(s) of the LaTeX base system. +%% Numerous changes were made to be fit for RLIMS. +%% -------------------------------------------------------------- +%% +%% Changes: +%% +%% * numerous changes previously to fit article.sty for the purpose +%% +%% version 1.5: +%% +%% * fixed the instructions +%% * added better default options to the template +%% * changed the year to 2008 +%% +%% version 1.4: +%% +%% * fitted to a newer version of article.sty +%% * cleanups in layout and formatting +%% * better compliance to LaTeX2e +%% * re-fitted \institute, \instituteB, ...C, ...D +%% it doesn't work 100% nicely, but it works) +%% * removed dependency on natbib.sty (so cite.sty, or else can be +%% used as liked) +%% * changed paper size to a4paper by default +%% * removed dependencies to graphicx.sty and subfigure.sty +%% (users like to choose and do that themselves) +%% +%% -------------------------------------------------------------- +%% +%% It may be distributed and/or modified under the +%% conditions of the LaTeX Project Public License, either version 1.3 +%% of this license or (at your option) any later version. +%% The latest version of this license is in +%% http://www.latex-project.org/lppl.txt +%% and version 1.3 or later is part of all distributions of LaTeX +%% version 2003/12/01 or later. +%% +%% This file has the LPPL maintenance status "maintained". +%% +%% This file may only be distributed together with a copy of the LaTeX +%% base system. You may however distribute the LaTeX base system without +%% such generated files. +%% +%% The list of all files belonging to the LaTeX base distribution is +%% given in the file `manifest.txt'. See also `legal.txt' for additional +%% information. +%% +%% The list of derived (unpacked) files belonging to the distribution +%% and covered by LPPL is defined by the unpacking scripts (with +%% extension .ins) which are part of the distribution. +%% \CharacterTable +%% {Upper-case \A\B\C\D\E\F\G\H\I\J\K\L\M\N\O\P\Q\R\S\T\U\V\W\X\Y\Z +%% Lower-case \a\b\c\d\e\f\g\h\i\j\k\l\m\n\o\p\q\r\s\t\u\v\w\x\y\z +%% Digits \0\1\2"s\4\5\6\7\8\9 +%% Exclamation \! Double quote \" Hash (number) \# +%% Dollar \$ Percent \% Ampersand \& +%% Acute accent \' Left paren \( Right paren \) +%% Asterisk \* Plus \+ Comma \, +%% Minus \- Point \. Solidus \/ +%% Colon \: Semicolon \; Less than \< +%% Equals \= Greater than \> Question mark \? +%% Commercial at \@ Left bracket \[ Backslash \\ +%% Right bracket \] Circumflex \^ Underscore \_ +%% Grave accent \` Left brace \{ Vertical bar \| +%% Right brace \} Tilde \~} +\NeedsTeXFormat{LaTeX2e}[1995/12/01] +\ProvidesClass{arlims} + [2008/04/23 v1.5 + Massey Research Letters Article Class] +\newcommand\@ptsize{} +\newif\if@restonecol +\newif\if@titlepage +\@titlepagefalse +\if@compatibility\else +\DeclareOption{a4paper} + {\setlength\paperheight {297mm}% + \setlength\paperwidth {210mm}} +\DeclareOption{a5paper} + {\setlength\paperheight {210mm}% + \setlength\paperwidth {148mm}} +\DeclareOption{b5paper} + {\setlength\paperheight {250mm}% + \setlength\paperwidth {176mm}} +\DeclareOption{letterpaper} + {\setlength\paperheight {11in}% + \setlength\paperwidth {8.5in}} +\DeclareOption{legalpaper} + {\setlength\paperheight {14in}% + \setlength\paperwidth {8.5in}} +\DeclareOption{executivepaper} + {\setlength\paperheight {10.5in}% + \setlength\paperwidth {7.25in}} +\DeclareOption{landscape} + {\setlength\@tempdima {\paperheight}% + \setlength\paperheight {\paperwidth}% + \setlength\paperwidth {\@tempdima}} +\fi +\if@compatibility + \renewcommand\@ptsize{0} +\else +\DeclareOption{10pt}{\renewcommand\@ptsize{0}} +\fi +\DeclareOption{11pt}{\renewcommand\@ptsize{1}} +\DeclareOption{12pt}{\renewcommand\@ptsize{2}} +\if@compatibility\else +\DeclareOption{oneside}{\@twosidefalse \@mparswitchfalse} +\fi +\DeclareOption{twoside}{\@twosidetrue \@mparswitchtrue} +\DeclareOption{draft}{\setlength\overfullrule{5pt}} +\if@compatibility\else +\DeclareOption{final}{\setlength\overfullrule{0pt}} +\fi +\DeclareOption{titlepage}{\@titlepagetrue} +\if@compatibility\else +\DeclareOption{notitlepage}{\@titlepagefalse} +\fi +\if@compatibility\else +\DeclareOption{onecolumn}{\@twocolumnfalse} +\fi +\DeclareOption{twocolumn}{\@twocolumntrue} +\DeclareOption{leqno}{\input{leqno.clo}} +\DeclareOption{fleqn}{\input{fleqn.clo}} +\DeclareOption{openbib}{% + \AtEndOfPackage{% + \renewcommand\@openbib@code{% + \advance\leftmargin\bibindent + \itemindent -\bibindent + \listparindent \itemindent + \parsep \z@ + }% + \renewcommand\newblock{\par}}% +} +\ExecuteOptions{a4paper,12pt,oneside,onecolumn,final} +\ProcessOptions +\input{size1\@ptsize.clo} +\setlength\lineskip{1\p@} +\setlength\normallineskip{1\p@} +\renewcommand\baselinestretch{} +\setlength\parskip{0\p@ \@plus \p@} +\@lowpenalty 51 +\@medpenalty 151 +\@highpenalty 301 +\setcounter{topnumber}{2} +\renewcommand\topfraction{.7} +\setcounter{bottomnumber}{1} +\renewcommand\bottomfraction{.3} +\setcounter{totalnumber}{3} +\renewcommand\textfraction{.2} +\renewcommand\floatpagefraction{.5} +\setcounter{dbltopnumber}{2} +\renewcommand\dbltopfraction{.7} +\renewcommand\dblfloatpagefraction{.5} +\if@twoside + \def\ps@headings{% + \let\@oddfoot\@empty\let\@evenfoot\@empty + \def\@evenhead{\thepage\hfil\slshape\leftmark}% + \def\@oddhead{{\slshape\rightmark}\hfil\thepage}% + \let\@mkboth\markboth + \def\sectionmark##1{% + \markboth {\MakeUppercase{% + \ifnum \c@secnumdepth >\z@ + \thesection\quad + \fi + ##1}}{}}% + \def\subsectionmark##1{% + \markright {% + \ifnum \c@secnumdepth >\@ne + \thesubsection\quad + \fi + ##1}}} +\else + \def\ps@headings{% + \let\@oddfoot\@empty + \def\@oddhead{{\slshape\rightmark}\hfil\thepage}% + \let\@mkboth\markboth + \def\sectionmark##1{% + \markright {\MakeUppercase{% + \ifnum \c@secnumdepth >\m@ne + \thesection\quad + \fi + ##1}}}} +\fi +\def\ps@myheadings{% + \let\@oddfoot\@empty\let\@evenfoot\@empty + \def\@evenhead{\thepage\hfil\slshape\leftmark}% + \def\@oddhead{{\slshape\rightmark}\hfil\thepage}% + \let\@mkboth\@gobbletwo + \let\sectionmark\@gobble + \let\subsectionmark\@gobble + } + + +%---------- old definition of title page ------------------------------- +\newif\ifGrxlb\ifGrxlb + + \if@titlepage + \newcommand\maketitle{\begin{titlepage}% + \let\footnotesize\small + \let\footnoterule\relax + \let \footnote \thanks + \null\vfil + \vskip 60\p@ + \begin{center}% + {\LARGE \@title \par}% + \vskip 3em% + {\large + \lineskip .75em% + \begin{tabular}[t]{c}% + \@author + \end{tabular}\par}% + \vskip 1.5em% + {\large \@date \par}% % Set date in \large size. + \end{center}\par + \@thanks + \vfil\null + \end{titlepage}% + \setcounter{footnote}{0}% + \global\let\thanks\relax + \global\let\maketitle\relax + \global\let\@thanks\@empty + \global\let\@author\@empty + \global\let\@date\@empty + \global\let\@title\@empty + \global\let\title\relax + \global\let\author\relax + \global\let\date\relax + \global\let\and\relax +} +\else +\newcommand\maketitle{\par + \begingroup + \renewcommand\thefootnote{\@fnsymbol\c@footnote}% + \def\@makefnmark{\rlap{\@textsuperscript{\normalfont\@thefnmark}}}% + \long\def\@makefntext##1{\parindent 1em\noindent + \hb@xt@1.8em{% + \hss\@textsuperscript{\normalfont\@thefnmark}}##1}% + \if@twocolumn + \ifnum \col@number=\@ne + \@maketitle + \else + \twocolumn[\@maketitle]% + \fi + \else + \newpage + \global\@topnum\z@ % Prevents figures from going at top of page. + \@maketitle + \fi + \thispagestyle{plain}\@thanks + \endgroup + \setcounter{footnote}{0}% + \global\let\thanks\relax + \global\let\maketitle\relax + \global\let\@maketitle\relax + \global\let\@thanks\@empty + \global\let\@author\@empty + \global\let\@date\@empty + \global\let\@title\@empty + \global\let\title\relax + \global\let\author\relax + \global\let\date\relax + \global\let\and\relax +} +\def\@maketitle{% + \newpage + \null + \vskip 2em% + \begin{center}% + \let \footnote \thanks + {\LARGE \@title \par}% + \vskip 1.5em% + {\large + \lineskip .5em% + \begin{tabular}[t]{c}% + \@author + \end{tabular}\par}% + \vskip 1em% + {\large \@date}% + \end{center}% + \par + \vskip 1.5em} +\fi + +\fi +%---------------- end of old definition of title page --------------------- + +\setcounter{secnumdepth}{3} +\newcounter {part} +\newcounter {section} +\newcounter {subsection}[section] +\newcounter {subsubsection}[subsection] +\newcounter {paragraph}[subsubsection] +\newcounter {subparagraph}[paragraph] +\renewcommand \thepart {\@Roman\c@part} +\renewcommand \thesection {\@arabic\c@section} +\renewcommand\thesubsection {\thesection.\@arabic\c@subsection} +\renewcommand\thesubsubsection{\thesubsection .\@arabic\c@subsubsection} +\renewcommand\theparagraph {\thesubsubsection.\@arabic\c@paragraph} +\renewcommand\thesubparagraph {\theparagraph.\@arabic\c@subparagraph} +\newcommand\part{% + \if@noskipsec \leavevmode \fi + \par + \addvspace{4ex}% + \@afterindentfalse + \secdef\@part\@spart} + +\def\@part[#1]#2{% + \ifnum \c@secnumdepth >\m@ne + \refstepcounter{part}% + \addcontentsline{toc}{part}{\thepart\hspace{1em}#1}% + \else + \addcontentsline{toc}{part}{#1}% + \fi + {\parindent \z@ \raggedright + \interlinepenalty \@M + \normalfont + \ifnum \c@secnumdepth >\m@ne + \Large\bfseries \partname\nobreakspace\thepart + \par\nobreak + \fi + \huge \bfseries #2% + \markboth{}{}\par}% + \nobreak + \vskip 3ex + \@afterheading} +\def\@spart#1{% + {\parindent \z@ \raggedright + \interlinepenalty \@M + \normalfont + \huge \bfseries #1\par}% + \nobreak + \vskip 3ex + \@afterheading} +\newcommand\section{\@startsection {section}{1}{\z@}% + {-3.5ex \@plus -1ex \@minus -.2ex}% + {2.3ex \@plus.2ex}% + {\normalfont\Large\bfseries}} +\newcommand\subsection{\@startsection{subsection}{2}{\z@}% + {-3.25ex\@plus -1ex \@minus -.2ex}% + {1.5ex \@plus .2ex}% + {\normalfont\large\bfseries}} +\newcommand\subsubsection{\@startsection{subsubsection}{3}{\z@}% + {-3.25ex\@plus -1ex \@minus -.2ex}% + {1.5ex \@plus .2ex}% + {\normalfont\normalsize\bfseries}} +\newcommand\paragraph{\@startsection{paragraph}{4}{\z@}% + {3.25ex \@plus1ex \@minus.2ex}% + {-1em}% + {\normalfont\normalsize\bfseries}} +\newcommand\subparagraph{\@startsection{subparagraph}{5}{\parindent}% + {3.25ex \@plus1ex \@minus .2ex}% + {-1em}% + {\normalfont\normalsize\bfseries}} +\if@twocolumn + \setlength\leftmargini {2em} +\else + \setlength\leftmargini {2.5em} +\fi +\leftmargin \leftmargini +\setlength\leftmarginii {2.2em} +\setlength\leftmarginiii {1.87em} +\setlength\leftmarginiv {1.7em} +\if@twocolumn + \setlength\leftmarginv {.5em} + \setlength\leftmarginvi {.5em} +\else + \setlength\leftmarginv {1em} + \setlength\leftmarginvi {1em} +\fi +\setlength \labelsep {.5em} +\setlength \labelwidth{\leftmargini} +\addtolength\labelwidth{-\labelsep} +\@beginparpenalty -\@lowpenalty +\@endparpenalty -\@lowpenalty +\@itempenalty -\@lowpenalty +\renewcommand\theenumi{\@arabic\c@enumi} +\renewcommand\theenumii{\@alph\c@enumii} +\renewcommand\theenumiii{\@roman\c@enumiii} +\renewcommand\theenumiv{\@Alph\c@enumiv} +\newcommand\labelenumi{\theenumi.} +\newcommand\labelenumii{(\theenumii)} +\newcommand\labelenumiii{\theenumiii.} +\newcommand\labelenumiv{\theenumiv.} +\renewcommand\p@enumii{\theenumi} +\renewcommand\p@enumiii{\theenumi(\theenumii)} +\renewcommand\p@enumiv{\p@enumiii\theenumiii} +\newcommand\labelitemi{\textbullet} +\newcommand\labelitemii{\normalfont\bfseries \textendash} +\newcommand\labelitemiii{\textasteriskcentered} +\newcommand\labelitemiv{\textperiodcentered} +\newenvironment{description} + {\list{}{\labelwidth\z@ \itemindent-\leftmargin + \let\makelabel\descriptionlabel}} + {\endlist} +\newcommand*\descriptionlabel[1]{\hspace\labelsep + \normalfont\bfseries #1} +\if@titlepage + \newenvironment{abstract}{% + \titlepage + \null\vfil + \@beginparpenalty\@lowpenalty + \begin{center}% + \bfseries \abstractname + \@endparpenalty\@M + \end{center}}% + {\par\vfil\null\endtitlepage} +\else + \newenvironment{abstract}{% + \if@twocolumn + \section*{\abstractname}% + \else + \small + \begin{center}% + {\bfseries \abstractname\vspace{-.5em}\vspace{\z@}}% + \end{center}% + \quotation + \fi} + {\if@twocolumn\else\endquotation\fi} +\fi +\newenvironment{verse} + {\let\\\@centercr + \list{}{\itemsep \z@ + \itemindent -1.5em% + \listparindent\itemindent + \rightmargin \leftmargin + \advance\leftmargin 1.5em}% + \item\relax} + {\endlist} +\newenvironment{quotation} + {\list{}{\listparindent 1.5em% + \itemindent \listparindent + \rightmargin \leftmargin + \parsep \z@ \@plus\p@}% + \item\relax} + {\endlist} +\newenvironment{quote} + {\list{}{\rightmargin\leftmargin}% + \item\relax} + {\endlist} +\if@compatibility +\newenvironment{titlepage} + {% + \if@twocolumn + \@restonecoltrue\onecolumn + \else + \@restonecolfalse\newpage + \fi + \thispagestyle{empty}% + \setcounter{page}\z@ + }% + {\if@restonecol\twocolumn \else \newpage \fi + } +\else +\newenvironment{titlepage} + {% + \if@twocolumn + \@restonecoltrue\onecolumn + \else + \@restonecolfalse\newpage + \fi + \thispagestyle{empty}% + \setcounter{page}\@ne + }% + {\if@restonecol\twocolumn \else \newpage \fi + \if@twoside\else + \setcounter{page}\@ne + \fi + } +\fi +\newcommand\appendix{\par + \setcounter{section}{0}% + \setcounter{subsection}{0}% + \gdef\thesection{\@Alph\c@section}} +\setlength\arraycolsep{5\p@} +\setlength\tabcolsep{6\p@} +\setlength\arrayrulewidth{.4\p@} +\setlength\doublerulesep{2\p@} +\setlength\tabbingsep{\labelsep} +\skip\@mpfootins = \skip\footins +\setlength\fboxsep{3\p@} +\setlength\fboxrule{.4\p@} +\renewcommand \theequation {\@arabic\c@equation} +\newcounter{figure} +\renewcommand \thefigure {\@arabic\c@figure} +\def\fps@figure{tbp} +\def\ftype@figure{1} +\def\ext@figure{lof} +\def\fnum@figure{\figurename\nobreakspace\thefigure} +\newenvironment{figure} + {\@float{figure}} + {\end@float} +\newenvironment{figure*} + {\@dblfloat{figure}} + {\end@dblfloat} +\newcounter{table} +\renewcommand\thetable{\@arabic\c@table} +\def\fps@table{tbp} +\def\ftype@table{2} +\def\ext@table{lot} +\def\fnum@table{\tablename\nobreakspace\thetable} +\newenvironment{table} + {\@float{table}} + {\end@float} +\newenvironment{table*} + {\@dblfloat{table}} + {\end@dblfloat} +\newlength\abovecaptionskip +\newlength\belowcaptionskip +\setlength\abovecaptionskip{10\p@} +\setlength\belowcaptionskip{0\p@} +\long\def\@makecaption#1#2{% + \vskip\abovecaptionskip + \sbox\@tempboxa{#1: #2}% + \ifdim \wd\@tempboxa >\hsize + #1: #2\par + \else + \global \@minipagefalse + \hb@xt@\hsize{\hfil\box\@tempboxa\hfil}% + \fi + \vskip\belowcaptionskip} +\DeclareOldFontCommand{\rm}{\normalfont\rmfamily}{\mathrm} +\DeclareOldFontCommand{\sf}{\normalfont\sffamily}{\mathsf} +\DeclareOldFontCommand{\tt}{\normalfont\ttfamily}{\mathtt} +\DeclareOldFontCommand{\bf}{\normalfont\bfseries}{\mathbf} +\DeclareOldFontCommand{\it}{\normalfont\itshape}{\mathit} +\DeclareOldFontCommand{\sl}{\normalfont\slshape}{\@nomath\sl} +\DeclareOldFontCommand{\sc}{\normalfont\scshape}{\@nomath\sc} +\DeclareRobustCommand*\cal{\@fontswitch\relax\mathcal} +\DeclareRobustCommand*\mit{\@fontswitch\relax\mathnormal} +\newcommand\@pnumwidth{1.55em} +\newcommand\@tocrmarg{2.55em} +\newcommand\@dotsep{4.5} +\setcounter{tocdepth}{3} +\newcommand\tableofcontents{% + \section*{\contentsname + \@mkboth{% + \MakeUppercase\contentsname}{\MakeUppercase\contentsname}}% + \@starttoc{toc}% + } +\newcommand*\l@part[2]{% + \ifnum \c@tocdepth >-2\relax + \addpenalty\@secpenalty + \addvspace{2.25em \@plus\p@}% + \setlength\@tempdima{3em}% + \begingroup + \parindent \z@ \rightskip \@pnumwidth + \parfillskip -\@pnumwidth + {\leavevmode + \large \bfseries #1\hfil \hb@xt@\@pnumwidth{\hss #2}}\par + \nobreak + \if@compatibility + \global\@nobreaktrue + \everypar{\global\@nobreakfalse\everypar{}}% + \fi + \endgroup + \fi} +\newcommand*\l@section[2]{% + \ifnum \c@tocdepth >\z@ + \addpenalty\@secpenalty + \addvspace{1.0em \@plus\p@}% + \setlength\@tempdima{1.5em}% + \begingroup + \parindent \z@ \rightskip \@pnumwidth + \parfillskip -\@pnumwidth + \leavevmode \bfseries + \advance\leftskip\@tempdima + \hskip -\leftskip + #1\nobreak\hfil \nobreak\hb@xt@\@pnumwidth{\hss #2}\par + \endgroup + \fi} +\newcommand*\l@subsection{\@dottedtocline{2}{1.5em}{2.3em}} +\newcommand*\l@subsubsection{\@dottedtocline{3}{3.8em}{3.2em}} +\newcommand*\l@paragraph{\@dottedtocline{4}{7.0em}{4.1em}} +\newcommand*\l@subparagraph{\@dottedtocline{5}{10em}{5em}} +\newcommand\listoffigures{% + \section*{\listfigurename}% + \@mkboth{\MakeUppercase\listfigurename}% + {\MakeUppercase\listfigurename}% + \@starttoc{lof}% + } +\newcommand*\l@figure{\@dottedtocline{1}{1.5em}{2.3em}} +\newcommand\listoftables{% + \section*{\listtablename}% + \@mkboth{% + \MakeUppercase\listtablename}% + {\MakeUppercase\listtablename}% + \@starttoc{lot}% + } +\let\l@table\l@figure +\newdimen\bibindent +\setlength\bibindent{1.5em} +\newenvironment{thebibliography}[1] + {\section*{\refname}% + \@mkboth{\MakeUppercase\refname}{\MakeUppercase\refname}% + \list{\@biblabel{\@arabic\c@enumiv}}% + {\settowidth\labelwidth{\@biblabel{#1}}% + \leftmargin\labelwidth + \advance\leftmargin\labelsep + \@openbib@code + \usecounter{enumiv}% + \let\p@enumiv\@empty + \renewcommand\theenumiv{\@arabic\c@enumiv}}% + \sloppy + \clubpenalty4000 + \@clubpenalty \clubpenalty + \widowpenalty4000% + \sfcode`\.\@m} + {\def\@noitemerr + {\@latex@warning{Empty `thebibliography' environment}}% + \endlist} +\newcommand\newblock{\hskip .11em\@plus.33em\@minus.07em} +\let\@openbib@code\@empty +\newenvironment{theindex} + {\if@twocolumn + \@restonecolfalse + \else + \@restonecoltrue + \fi + \twocolumn[\section*{\indexname}]% + \@mkboth{\MakeUppercase\indexname}% + {\MakeUppercase\indexname}% + \thispagestyle{plain}\parindent\z@ + \parskip\z@ \@plus .3\p@\relax + \columnseprule \z@ + \columnsep 35\p@ + \let\item\@idxitem} + {\if@restonecol\onecolumn\else\clearpage\fi} +\newcommand\@idxitem{\par\hangindent 40\p@} +\newcommand\subitem{\@idxitem \hspace*{20\p@}} +\newcommand\subsubitem{\@idxitem \hspace*{30\p@}} +\newcommand\indexspace{\par \vskip 10\p@ \@plus5\p@ \@minus3\p@\relax} +\renewcommand\footnoterule{% + \kern-3\p@ + \hrule\@width.4\columnwidth + \kern2.6\p@} +\newcommand\@makefntext[1]{% + \parindent 1em% + \noindent + \hb@xt@1.8em{\hss\@makefnmark}#1} +\newcommand\contentsname{Contents} +\newcommand\listfigurename{List of Figures} +\newcommand\listtablename{List of Tables} +\newcommand\refname{References} +\newcommand\indexname{Index} +\newcommand\figurename{Figure} +\newcommand\tablename{Table} +\newcommand\partname{Part} +\newcommand\appendixname{Appendix} +\newcommand\abstractname{} +\def\today{\ifcase\month\or + January\or February\or March\or April\or May\or June\or + July\or August\or September\or October\or November\or December\fi + \space\number\day, \number\year} +\setlength\columnsep{10\p@} +\setlength\columnseprule{0\p@} +\pagestyle{plain} +\pagenumbering{arabic} + +% === ARLIMS adaptions following ============================================ +\pagestyle{empty} + +\setlength{\oddsidemargin}{1.5 cm} +\setlength{\evensidemargin}{-0.5 cm} +\setlength{\textheight}{22 cm} +\setlength{\textwidth}{15 cm} +\setlength{\topmargin}{0 cm} + +% --- special indexing to cope with the list of articles +\newif\ifIndexMade +\def\makeartIndex{% + \protected@write1{}{\string\global\string\IndexMadetrue} + \newwrite\@artIndexfile + \immediate\openout\@artIndexfile=\jobname.adx + \def\artIndex{\@bsphack\begingroup\@sanitize\@wrartIndex} + \typeout{Writing artIndex file \jobname.adx}% + \let\makeartIndex\@empty +} + +\def\@wrartIndex#1{% + \protected@write\@artIndexfile{}{% + \string\artIndexentry{\factTitle}% + {\thepage}% + {\string\last#1}% + {\factAuthor\factAuthorB\factAuthorC\factAuthorD}}% + \endgroup% + \@esphack% +} + +\def\artIndex{\@bsphack\begingroup \@sanitize\@artIndex} +\def\@artIndex#1{\endgroup\@esphack} + +\def\artIndexLast#1{% + \protected@write1{}{\string\gdef\string\last#1{\thepage}}% +} + +% --- to set up the headers permanently +\newcommand{\markperm}[2]{% + \global\def\@evenhead{\arabic{page}\hfil{#1}}% + \global\def\@oddhead{{#2}\hfil\arabic{page}} +} +% (yes, I know I've hardwired arabic page numbering) + +% --- to set up the headers with a once off first +\newcommand{\markonce}[3]{% + \global\def\@evenhead{\arabic{page}\hfil{#1}\markperm{#2}{#3}}% + \global\def\@oddhead{{#1}\hfil\arabic{page}\markperm{#2}{#3}}% +} + +%% Put a line after the abstract %% +\newcommand{\drawline}{% + \vspace*{1mm} + \begin{center} + \rule{\textwidth}{0.2mm} + \end{center} + \vspace*{1mm} +} + +\newcommand{\reff}[1]{(\ref{#1})} + + +% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +% Define the author and title commands. Bascially each just stuffs +% the argument into a storage command. I need to define the blank +% storage commands because I am using the "renewcomand", since I +% need to do this for each article. + +\def\author{} +\def\institute{} + +\def\factTitle{} + +\def\factAuthor{} +\def\factAuthorB{} +\def\factAuthorC{} +\def\factAuthorD{} + +\def\factInstitute{} +\def\factInstituteB{} +\def\factInstituteC{} +\def\factInstituteD{} + +\def\footInst{} +\def\footInstB{} +\def\footInstC{} +\def\footInstD{} + +\def\footAuthorInst{} +\def\footAuthorInstB{} +\def\footAuthorInstC{} +\def\footAuthorInstD{} + +\renewcommand{\author}[2][]{ + \renewcommand{\footAuthorInst}{#1} + \renewcommand{\factAuthor}{#2} +} + +\newcommand{\authorB}[2][]{ + \renewcommand{\footAuthorInstB}{#1} + \renewcommand{\factAuthorB}{#2} +} + +\newcommand{\authorC}[2][]{ + \renewcommand{\footAuthorInstC}{#1} + \renewcommand{\factAuthorC}{#2} +} + +\newcommand{\authorD}[2][]{ + \renewcommand{\footAuthorInstD}{#1} + \renewcommand{\factAuthorD}{#2} +} + +\renewcommand{\institute}[2][]{ + \renewcommand{\footInst}{#1} + \renewcommand{\factInstitute}{#2} +} + +\newcommand{\instituteB}[2][*]{ + \renewcommand{\footInstB}{#1} + \renewcommand{\factInstituteB}{#2} +} + +\newcommand{\instituteC}[2][**]{ + \renewcommand{\footInstC}{#1} + \renewcommand{\factInstituteC}{#2} +} + +\newcommand{\instituteD}[2][***]{ + \renewcommand{\footInstD}{#1} + \renewcommand{\factInstituteD}{#2} +} + +\def\nameTag{} + +\renewcommand{\title}[1]{ + \renewcommand{\factTitle}{#1} +} + +\newcommand{\iimsaddress}{ + \begin{center} + \textit{Institute of Information \& Mathematical Sciences\\ + Massey University at Albany, Auckland, New Zealand.} + \end{center} +} + +\newcommand{\maketitle}[1][\factTitle]{ + \begin{center} + \markonce{ + \parbox{10cm}{\small + \textit{The Python Papers,} Vol.~3, No.~3 (2008)\\[1mm] + Available online at http://ojs.pythonpapers.org/index.php/tpp/issue/view/10 + } + } + {\factAuthor\factAuthorB\factAuthorC\factAuthorD} + {#1} + + \vspace*{1cm} + + \begin{minipage}{12cm} + \begin{center} + {\bfseries\Large\factTitle\par} + \vspace*{2mm} + \end{center} + \end{minipage} + + \vspace*{3mm} + + \textsc{% + \factAuthor $^{\footAuthorInst}$% + \factAuthorB $^{\footAuthorInstB}$% + \factAuthorC $^{\footAuthorInstC}$% + \factAuthorD $^{\footAuthorInstD}$% + } + + \vspace*{1mm} + + $^{\footInst}$\textit{\factInstitute} + $^{\footInstB}$\textit{\factInstituteB} + $^{\footInstC}$\textit{\factInstituteC} + $^{\footInstD}$\textit{\factInstituteD} + + \artIndex{\nameTag} + \end{center} +} + +% calculate the margins so that the text on opposite sides +% of the same sheet is lined up at the edges +% ok, its a fudge, but you can change the oddsidemargin +% to shift both sides in sync. + +\oddsidemargin1cm +\evensidemargin\paperwidth +\addtolength{\evensidemargin}{-\oddsidemargin} +\addtolength{\evensidemargin}{-205mm} + +% --- turn off a few things ------------------------------------------------- +\renewcommand{\markboth}[2]{} +\renewcommand{\markright}[1]{} +\newcommand{\markheading}[1]{} + +\usepackage{amsmath,amsfonts,dsfont} +\usepackage{amsthm} + +\newif\ifCombinedRlims + +% === end of ARLIMS adaptions =============================================== + +\if@twoside +\else + \raggedbottom +\fi +\if@twocolumn + \twocolumn + \sloppy + \flushbottom +\else + \onecolumn +\fi +\endinput +%% +%% End of file `arlims.cls'. diff --git a/doc/KPyCon2009/arlimsTPPM.cls b/doc/KPyCon2009/arlimsTPPM.cls new file mode 100644 index 0000000..82d32db --- /dev/null +++ b/doc/KPyCon2009/arlimsTPPM.cls @@ -0,0 +1,898 @@ +%% +%% This is file `arlims.cls', +%% It is based on the original `article.cls' from a tetex 3.0 +%% distribution (Ubuntu tetex-base 3.0-19 package). +%% +%% Additions to it were made by (probably) various authors at the +%% Massey University Institute of Information and Mathematical +%% Sciences. +%% +%% For further information contact Heath James +%% , Guy Kloss , Paul +%% Cowpertwait or anybody you expect +%% suitable for this purpose ... +%% +%% Copyright 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 +%% The LaTeX3 Project and any individual authors listed elsewhere +%% in this file. +%% +%% This file was generated from file(s) of the LaTeX base system. +%% Numerous changes were made to be fit for RLIMS. +%% -------------------------------------------------------------- +%% +%% Changes: +%% +%% * numerous changes previously to fit article.sty for the purpose +%% +%% version 1.5: +%% +%% * fixed the instructions +%% * added better default options to the template +%% * changed the year to 2008 +%% +%% version 1.4: +%% +%% * fitted to a newer version of article.sty +%% * cleanups in layout and formatting +%% * better compliance to LaTeX2e +%% * re-fitted \institute, \instituteB, ...C, ...D +%% it doesn't work 100% nicely, but it works) +%% * removed dependency on natbib.sty (so cite.sty, or else can be +%% used as liked) +%% * changed paper size to a4paper by default +%% * removed dependencies to graphicx.sty and subfigure.sty +%% (users like to choose and do that themselves) +%% +%% -------------------------------------------------------------- +%% +%% It may be distributed and/or modified under the +%% conditions of the LaTeX Project Public License, either version 1.3 +%% of this license or (at your option) any later version. +%% The latest version of this license is in +%% http://www.latex-project.org/lppl.txt +%% and version 1.3 or later is part of all distributions of LaTeX +%% version 2003/12/01 or later. +%% +%% This file has the LPPL maintenance status "maintained". +%% +%% This file may only be distributed together with a copy of the LaTeX +%% base system. You may however distribute the LaTeX base system without +%% such generated files. +%% +%% The list of all files belonging to the LaTeX base distribution is +%% given in the file `manifest.txt'. See also `legal.txt' for additional +%% information. +%% +%% The list of derived (unpacked) files belonging to the distribution +%% and covered by LPPL is defined by the unpacking scripts (with +%% extension .ins) which are part of the distribution. +%% \CharacterTable +%% {Upper-case \A\B\C\D\E\F\G\H\I\J\K\L\M\N\O\P\Q\R\S\T\U\V\W\X\Y\Z +%% Lower-case \a\b\c\d\e\f\g\h\i\j\k\l\m\n\o\p\q\r\s\t\u\v\w\x\y\z +%% Digits \0\1\2"s\4\5\6\7\8\9 +%% Exclamation \! Double quote \" Hash (number) \# +%% Dollar \$ Percent \% Ampersand \& +%% Acute accent \' Left paren \( Right paren \) +%% Asterisk \* Plus \+ Comma \, +%% Minus \- Point \. Solidus \/ +%% Colon \: Semicolon \; Less than \< +%% Equals \= Greater than \> Question mark \? +%% Commercial at \@ Left bracket \[ Backslash \\ +%% Right bracket \] Circumflex \^ Underscore \_ +%% Grave accent \` Left brace \{ Vertical bar \| +%% Right brace \} Tilde \~} +\NeedsTeXFormat{LaTeX2e}[1995/12/01] +\ProvidesClass{arlims} + [2008/04/23 v1.5 + Massey Research Letters Article Class] +\newcommand\@ptsize{} +\newif\if@restonecol +\newif\if@titlepage +\@titlepagefalse +\if@compatibility\else +\DeclareOption{a4paper} + {\setlength\paperheight {297mm}% + \setlength\paperwidth {210mm}} +\DeclareOption{a5paper} + {\setlength\paperheight {210mm}% + \setlength\paperwidth {148mm}} +\DeclareOption{b5paper} + {\setlength\paperheight {250mm}% + \setlength\paperwidth {176mm}} +\DeclareOption{letterpaper} + {\setlength\paperheight {11in}% + \setlength\paperwidth {8.5in}} +\DeclareOption{legalpaper} + {\setlength\paperheight {14in}% + \setlength\paperwidth {8.5in}} +\DeclareOption{executivepaper} + {\setlength\paperheight {10.5in}% + \setlength\paperwidth {7.25in}} +\DeclareOption{landscape} + {\setlength\@tempdima {\paperheight}% + \setlength\paperheight {\paperwidth}% + \setlength\paperwidth {\@tempdima}} +\fi +\if@compatibility + \renewcommand\@ptsize{0} +\else +\DeclareOption{10pt}{\renewcommand\@ptsize{0}} +\fi +\DeclareOption{11pt}{\renewcommand\@ptsize{1}} +\DeclareOption{12pt}{\renewcommand\@ptsize{2}} +\if@compatibility\else +\DeclareOption{oneside}{\@twosidefalse \@mparswitchfalse} +\fi +\DeclareOption{twoside}{\@twosidetrue \@mparswitchtrue} +\DeclareOption{draft}{\setlength\overfullrule{5pt}} +\if@compatibility\else +\DeclareOption{final}{\setlength\overfullrule{0pt}} +\fi +\DeclareOption{titlepage}{\@titlepagetrue} +\if@compatibility\else +\DeclareOption{notitlepage}{\@titlepagefalse} +\fi +\if@compatibility\else +\DeclareOption{onecolumn}{\@twocolumnfalse} +\fi +\DeclareOption{twocolumn}{\@twocolumntrue} +\DeclareOption{leqno}{\input{leqno.clo}} +\DeclareOption{fleqn}{\input{fleqn.clo}} +\DeclareOption{openbib}{% + \AtEndOfPackage{% + \renewcommand\@openbib@code{% + \advance\leftmargin\bibindent + \itemindent -\bibindent + \listparindent \itemindent + \parsep \z@ + }% + \renewcommand\newblock{\par}}% +} +\ExecuteOptions{a4paper,10pt,oneside,onecolumn,final} +\ProcessOptions +\input{size1\@ptsize.clo} +\setlength\lineskip{1\p@} +\setlength\normallineskip{1\p@} +\renewcommand\baselinestretch{} +\setlength\parskip{0\p@ \@plus \p@} +\@lowpenalty 51 +\@medpenalty 151 +\@highpenalty 301 +\setcounter{topnumber}{2} +\renewcommand\topfraction{.7} +\setcounter{bottomnumber}{1} +\renewcommand\bottomfraction{.3} +\setcounter{totalnumber}{3} +\renewcommand\textfraction{.2} +\renewcommand\floatpagefraction{.5} +\setcounter{dbltopnumber}{2} +\renewcommand\dbltopfraction{.7} +\renewcommand\dblfloatpagefraction{.5} +\if@twoside + \def\ps@headings{% + \let\@oddfoot\@empty\let\@evenfoot\@empty + \def\@evenhead{\thepage\hfil\slshape\leftmark}% + \def\@oddhead{{\slshape\rightmark}\hfil\thepage}% + \let\@mkboth\markboth + \def\sectionmark##1{% + \markboth {\MakeUppercase{% + \ifnum \c@secnumdepth >\z@ + \thesection\quad + \fi + ##1}}{}}% + \def\subsectionmark##1{% + \markright {% + \ifnum \c@secnumdepth >\@ne + \thesubsection\quad + \fi + ##1}}} +\else + \def\ps@headings{% + \let\@oddfoot\@empty + \def\@oddhead{{\slshape\rightmark}\hfil\thepage}% + \let\@mkboth\markboth + \def\sectionmark##1{% + \markright {\MakeUppercase{% + \ifnum \c@secnumdepth >\m@ne + \thesection\quad + \fi + ##1}}}} +\fi +\def\ps@myheadings{% + \let\@oddfoot\@empty\let\@evenfoot\@empty + \def\@evenhead{\thepage\hfil\slshape\leftmark}% + \def\@oddhead{{\slshape\rightmark}\hfil\thepage}% + \let\@mkboth\@gobbletwo + \let\sectionmark\@gobble + \let\subsectionmark\@gobble + } + + +%---------- old definition of title page ------------------------------- +\newif\ifGrxlb\ifGrxlb + + \if@titlepage + \newcommand\maketitle{\begin{titlepage}% + \let\footnotesize\small + \let\footnoterule\relax + \let \footnote \thanks + \null\vfil + \vskip 60\p@ + \begin{center}% + {\LARGE \@title \par}% + \vskip 3em% + {\large + \lineskip .75em% + \begin{tabular}[t]{c}% + \@author + \end{tabular}\par}% + \vskip 1.5em% + {\large \@date \par}% % Set date in \large size. + \end{center}\par + \@thanks + \vfil\null + \end{titlepage}% + \setcounter{footnote}{0}% + \global\let\thanks\relax + \global\let\maketitle\relax + \global\let\@thanks\@empty + \global\let\@author\@empty + \global\let\@date\@empty + \global\let\@title\@empty + \global\let\title\relax + \global\let\author\relax + \global\let\date\relax + \global\let\and\relax +} +\else +\newcommand\maketitle{\par + \begingroup + \renewcommand\thefootnote{\@fnsymbol\c@footnote}% + \def\@makefnmark{\rlap{\@textsuperscript{\normalfont\@thefnmark}}}% + \long\def\@makefntext##1{\parindent 1em\noindent + \hb@xt@1.8em{% + \hss\@textsuperscript{\normalfont\@thefnmark}}##1}% + \if@twocolumn + \ifnum \col@number=\@ne + \@maketitle + \else + \twocolumn[\@maketitle]% + \fi + \else + \newpage + \global\@topnum\z@ % Prevents figures from going at top of page. + \@maketitle + \fi + \thispagestyle{plain}\@thanks + \endgroup + \setcounter{footnote}{0}% + \global\let\thanks\relax + \global\let\maketitle\relax + \global\let\@maketitle\relax + \global\let\@thanks\@empty + \global\let\@author\@empty + \global\let\@date\@empty + \global\let\@title\@empty + \global\let\title\relax + \global\let\author\relax + \global\let\date\relax + \global\let\and\relax +} +\def\@maketitle{% + \newpage + \null + \vskip 2em% + \begin{center}% + \let \footnote \thanks + {\LARGE \@title \par}% + \vskip 1.5em% + {\large + \lineskip .5em% + \begin{tabular}[t]{c}% + \@author + \end{tabular}\par}% + \vskip 1em% + {\large \@date}% + \end{center}% + \par + \vskip 1.5em} +\fi + +\fi +%---------------- end of old definition of title page --------------------- + +\setcounter{secnumdepth}{3} +\newcounter {part} +\newcounter {section} +\newcounter {subsection}[section] +\newcounter {subsubsection}[subsection] +\newcounter {paragraph}[subsubsection] +\newcounter {subparagraph}[paragraph] +\renewcommand \thepart {\@Roman\c@part} +\renewcommand \thesection {\@arabic\c@section} +\renewcommand\thesubsection {\thesection.\@arabic\c@subsection} +\renewcommand\thesubsubsection{\thesubsection .\@arabic\c@subsubsection} +\renewcommand\theparagraph {\thesubsubsection.\@arabic\c@paragraph} +\renewcommand\thesubparagraph {\theparagraph.\@arabic\c@subparagraph} +\newcommand\part{% + \if@noskipsec \leavevmode \fi + \par + \addvspace{4ex}% + \@afterindentfalse + \secdef\@part\@spart} + +\def\@part[#1]#2{% + \ifnum \c@secnumdepth >\m@ne + \refstepcounter{part}% + \addcontentsline{toc}{part}{\thepart\hspace{1em}#1}% + \else + \addcontentsline{toc}{part}{#1}% + \fi + {\parindent \z@ \raggedright + \interlinepenalty \@M + \normalfont + \ifnum \c@secnumdepth >\m@ne + \Large\bfseries \partname\nobreakspace\thepart + \par\nobreak + \fi + \huge \bfseries #2% + \markboth{}{}\par}% + \nobreak + \vskip 3ex + \@afterheading} +\def\@spart#1{% + {\parindent \z@ \raggedright + \interlinepenalty \@M + \normalfont + \huge \bfseries #1\par}% + \nobreak + \vskip 3ex + \@afterheading} +\newcommand\section{\@startsection {section}{1}{\z@}% + {-3.5ex \@plus -1ex \@minus -.2ex}% + {2.3ex \@plus.2ex}% + {\normalfont\Large\bfseries}} +\newcommand\subsection{\@startsection{subsection}{2}{\z@}% + {-3.25ex\@plus -1ex \@minus -.2ex}% + {1.5ex \@plus .2ex}% + {\normalfont\large\bfseries}} +\newcommand\subsubsection{\@startsection{subsubsection}{3}{\z@}% + {-3.25ex\@plus -1ex \@minus -.2ex}% + {1.5ex \@plus .2ex}% + {\normalfont\normalsize\bfseries}} +\newcommand\paragraph{\@startsection{paragraph}{4}{\z@}% + {3.25ex \@plus1ex \@minus.2ex}% + {-1em}% + {\normalfont\normalsize\bfseries}} +\newcommand\subparagraph{\@startsection{subparagraph}{5}{\parindent}% + {3.25ex \@plus1ex \@minus .2ex}% + {-1em}% + {\normalfont\normalsize\bfseries}} +\if@twocolumn + \setlength\leftmargini {2em} +\else + \setlength\leftmargini {2.5em} +\fi +\leftmargin \leftmargini +\setlength\leftmarginii {2.2em} +\setlength\leftmarginiii {1.87em} +\setlength\leftmarginiv {1.7em} +\if@twocolumn + \setlength\leftmarginv {.5em} + \setlength\leftmarginvi {.5em} +\else + \setlength\leftmarginv {1em} + \setlength\leftmarginvi {1em} +\fi +\setlength \labelsep {.5em} +\setlength \labelwidth{\leftmargini} +\addtolength\labelwidth{-\labelsep} +\@beginparpenalty -\@lowpenalty +\@endparpenalty -\@lowpenalty +\@itempenalty -\@lowpenalty +\renewcommand\theenumi{\@arabic\c@enumi} +\renewcommand\theenumii{\@alph\c@enumii} +\renewcommand\theenumiii{\@roman\c@enumiii} +\renewcommand\theenumiv{\@Alph\c@enumiv} +\newcommand\labelenumi{\theenumi.} +\newcommand\labelenumii{(\theenumii)} +\newcommand\labelenumiii{\theenumiii.} +\newcommand\labelenumiv{\theenumiv.} +\renewcommand\p@enumii{\theenumi} +\renewcommand\p@enumiii{\theenumi(\theenumii)} +\renewcommand\p@enumiv{\p@enumiii\theenumiii} +\newcommand\labelitemi{\textbullet} +\newcommand\labelitemii{\normalfont\bfseries \textendash} +\newcommand\labelitemiii{\textasteriskcentered} +\newcommand\labelitemiv{\textperiodcentered} +\newenvironment{description} + {\list{}{\labelwidth\z@ \itemindent-\leftmargin + \let\makelabel\descriptionlabel}} + {\endlist} +\newcommand*\descriptionlabel[1]{\hspace\labelsep + \normalfont\bfseries #1} +\if@titlepage + \newenvironment{abstract}{% + \titlepage + \null\vfil + \@beginparpenalty\@lowpenalty + \begin{center}% + \bfseries \abstractname + \@endparpenalty\@M + \end{center}}% + {\par\vfil\null\endtitlepage} +\else + \newenvironment{abstract}{% + \if@twocolumn + \section*{\abstractname}% + \else + \small + \begin{center}% + {\bfseries \abstractname\vspace{-.5em}\vspace{\z@}}% + \end{center}% + \quotation + \fi} + {\if@twocolumn\else\endquotation\fi} +\fi +\newenvironment{verse} + {\let\\\@centercr + \list{}{\itemsep \z@ + \itemindent -1.5em% + \listparindent\itemindent + \rightmargin \leftmargin + \advance\leftmargin 1.5em}% + \item\relax} + {\endlist} +\newenvironment{quotation} + {\list{}{\listparindent 1.5em% + \itemindent \listparindent + \rightmargin \leftmargin + \parsep \z@ \@plus\p@}% + \item\relax} + {\endlist} +\newenvironment{quote} + {\list{}{\rightmargin\leftmargin}% + \item\relax} + {\endlist} +\if@compatibility +\newenvironment{titlepage} + {% + \if@twocolumn + \@restonecoltrue\onecolumn + \else + \@restonecolfalse\newpage + \fi + \thispagestyle{empty}% + \setcounter{page}\z@ + }% + {\if@restonecol\twocolumn \else \newpage \fi + } +\else +\newenvironment{titlepage} + {% + \if@twocolumn + \@restonecoltrue\onecolumn + \else + \@restonecolfalse\newpage + \fi + \thispagestyle{empty}% + \setcounter{page}\@ne + }% + {\if@restonecol\twocolumn \else \newpage \fi + \if@twoside\else + \setcounter{page}\@ne + \fi + } +\fi +\newcommand\appendix{\par + \setcounter{section}{0}% + \setcounter{subsection}{0}% + \gdef\thesection{\@Alph\c@section}} +\setlength\arraycolsep{5\p@} +\setlength\tabcolsep{6\p@} +\setlength\arrayrulewidth{.4\p@} +\setlength\doublerulesep{2\p@} +\setlength\tabbingsep{\labelsep} +\skip\@mpfootins = \skip\footins +\setlength\fboxsep{3\p@} +\setlength\fboxrule{.4\p@} +\renewcommand \theequation {\@arabic\c@equation} +\newcounter{figure} +\renewcommand \thefigure {\@arabic\c@figure} +\def\fps@figure{tbp} +\def\ftype@figure{1} +\def\ext@figure{lof} +\def\fnum@figure{\figurename\nobreakspace\thefigure} +\newenvironment{figure} + {\@float{figure}} + {\end@float} +\newenvironment{figure*} + {\@dblfloat{figure}} + {\end@dblfloat} +\newcounter{table} +\renewcommand\thetable{\@arabic\c@table} +\def\fps@table{tbp} +\def\ftype@table{2} +\def\ext@table{lot} +\def\fnum@table{\tablename\nobreakspace\thetable} +\newenvironment{table} + {\@float{table}} + {\end@float} +\newenvironment{table*} + {\@dblfloat{table}} + {\end@dblfloat} +\newlength\abovecaptionskip +\newlength\belowcaptionskip +\setlength\abovecaptionskip{10\p@} +\setlength\belowcaptionskip{0\p@} +\long\def\@makecaption#1#2{% + \vskip\abovecaptionskip + \sbox\@tempboxa{#1: #2}% + \ifdim \wd\@tempboxa >\hsize + #1: #2\par + \else + \global \@minipagefalse + \hb@xt@\hsize{\hfil\box\@tempboxa\hfil}% + \fi + \vskip\belowcaptionskip} +\DeclareOldFontCommand{\rm}{\normalfont\rmfamily}{\mathrm} +\DeclareOldFontCommand{\sf}{\normalfont\sffamily}{\mathsf} +\DeclareOldFontCommand{\tt}{\normalfont\ttfamily}{\mathtt} +\DeclareOldFontCommand{\bf}{\normalfont\bfseries}{\mathbf} +\DeclareOldFontCommand{\it}{\normalfont\itshape}{\mathit} +\DeclareOldFontCommand{\sl}{\normalfont\slshape}{\@nomath\sl} +\DeclareOldFontCommand{\sc}{\normalfont\scshape}{\@nomath\sc} +\DeclareRobustCommand*\cal{\@fontswitch\relax\mathcal} +\DeclareRobustCommand*\mit{\@fontswitch\relax\mathnormal} +\newcommand\@pnumwidth{1.55em} +\newcommand\@tocrmarg{2.55em} +\newcommand\@dotsep{4.5} +\setcounter{tocdepth}{3} +\newcommand\tableofcontents{% + \section*{\contentsname + \@mkboth{% + \MakeUppercase\contentsname}{\MakeUppercase\contentsname}}% + \@starttoc{toc}% + } +\newcommand*\l@part[2]{% + \ifnum \c@tocdepth >-2\relax + \addpenalty\@secpenalty + \addvspace{2.25em \@plus\p@}% + \setlength\@tempdima{3em}% + \begingroup + \parindent \z@ \rightskip \@pnumwidth + \parfillskip -\@pnumwidth + {\leavevmode + \large \bfseries #1\hfil \hb@xt@\@pnumwidth{\hss #2}}\par + \nobreak + \if@compatibility + \global\@nobreaktrue + \everypar{\global\@nobreakfalse\everypar{}}% + \fi + \endgroup + \fi} +\newcommand*\l@section[2]{% + \ifnum \c@tocdepth >\z@ + \addpenalty\@secpenalty + \addvspace{1.0em \@plus\p@}% + \setlength\@tempdima{1.5em}% + \begingroup + \parindent \z@ \rightskip \@pnumwidth + \parfillskip -\@pnumwidth + \leavevmode \bfseries + \advance\leftskip\@tempdima + \hskip -\leftskip + #1\nobreak\hfil \nobreak\hb@xt@\@pnumwidth{\hss #2}\par + \endgroup + \fi} +\newcommand*\l@subsection{\@dottedtocline{2}{1.5em}{2.3em}} +\newcommand*\l@subsubsection{\@dottedtocline{3}{3.8em}{3.2em}} +\newcommand*\l@paragraph{\@dottedtocline{4}{7.0em}{4.1em}} +\newcommand*\l@subparagraph{\@dottedtocline{5}{10em}{5em}} +\newcommand\listoffigures{% + \section*{\listfigurename}% + \@mkboth{\MakeUppercase\listfigurename}% + {\MakeUppercase\listfigurename}% + \@starttoc{lof}% + } +\newcommand*\l@figure{\@dottedtocline{1}{1.5em}{2.3em}} +\newcommand\listoftables{% + \section*{\listtablename}% + \@mkboth{% + \MakeUppercase\listtablename}% + {\MakeUppercase\listtablename}% + \@starttoc{lot}% + } +\let\l@table\l@figure +\newdimen\bibindent +\setlength\bibindent{1.5em} +\newenvironment{thebibliography}[1] + {\section*{\refname}% + \@mkboth{\MakeUppercase\refname}{\MakeUppercase\refname}% + \list{\@biblabel{\@arabic\c@enumiv}}% + {\settowidth\labelwidth{\@biblabel{#1}}% + \leftmargin\labelwidth + \advance\leftmargin\labelsep + \@openbib@code + \usecounter{enumiv}% + \let\p@enumiv\@empty + \renewcommand\theenumiv{\@arabic\c@enumiv}}% + \sloppy + \clubpenalty4000 + \@clubpenalty \clubpenalty + \widowpenalty4000% + \sfcode`\.\@m} + {\def\@noitemerr + {\@latex@warning{Empty `thebibliography' environment}}% + \endlist} +\newcommand\newblock{\hskip .11em\@plus.33em\@minus.07em} +\let\@openbib@code\@empty +\newenvironment{theindex} + {\if@twocolumn + \@restonecolfalse + \else + \@restonecoltrue + \fi + \twocolumn[\section*{\indexname}]% + \@mkboth{\MakeUppercase\indexname}% + {\MakeUppercase\indexname}% + \thispagestyle{plain}\parindent\z@ + \parskip\z@ \@plus .3\p@\relax + \columnseprule \z@ + \columnsep 35\p@ + \let\item\@idxitem} + {\if@restonecol\onecolumn\else\clearpage\fi} +\newcommand\@idxitem{\par\hangindent 40\p@} +\newcommand\subitem{\@idxitem \hspace*{20\p@}} +\newcommand\subsubitem{\@idxitem \hspace*{30\p@}} +\newcommand\indexspace{\par \vskip 10\p@ \@plus5\p@ \@minus3\p@\relax} +\renewcommand\footnoterule{% + \kern-3\p@ + \hrule\@width.4\columnwidth + \kern2.6\p@} +\newcommand\@makefntext[1]{% + \parindent 1em% + \noindent + \hb@xt@1.8em{\hss\@makefnmark}#1} +\newcommand\contentsname{Contents} +\newcommand\listfigurename{List of Figures} +\newcommand\listtablename{List of Tables} +\newcommand\refname{References} +\newcommand\indexname{Index} +\newcommand\figurename{Figure} +\newcommand\tablename{Table} +\newcommand\partname{Part} +\newcommand\appendixname{Appendix} +\newcommand\abstractname{} +\def\today{\ifcase\month\or + January\or February\or March\or April\or May\or June\or + July\or August\or September\or October\or November\or December\fi + \space\number\day, \number\year} +\setlength\columnsep{10\p@} +\setlength\columnseprule{0\p@} +\pagestyle{plain} +\pagenumbering{arabic} + +% === ARLIMS adaptions following ============================================ +\pagestyle{empty} + +\setlength{\oddsidemargin}{1.5 cm} +\setlength{\evensidemargin}{-0.5 cm} +\setlength{\textheight}{22 cm} +\setlength{\textwidth}{15 cm} +\setlength{\topmargin}{0 cm} + +% --- special indexing to cope with the list of articles +\newif\ifIndexMade +\def\makeartIndex{% + \protected@write1{}{\string\global\string\IndexMadetrue} + \newwrite\@artIndexfile + \immediate\openout\@artIndexfile=\jobname.adx + \def\artIndex{\@bsphack\begingroup\@sanitize\@wrartIndex} + \typeout{Writing artIndex file \jobname.adx}% + \let\makeartIndex\@empty +} + +\def\@wrartIndex#1{% + \protected@write\@artIndexfile{}{% + \string\artIndexentry{\factTitle}% + {\thepage}% + {\string\last#1}% + {\factAuthor\factAuthorB\factAuthorC\factAuthorD}}% + \endgroup% + \@esphack% +} + +\def\artIndex{\@bsphack\begingroup \@sanitize\@artIndex} +\def\@artIndex#1{\endgroup\@esphack} + +\def\artIndexLast#1{% + \protected@write1{}{\string\gdef\string\last#1{\thepage}}% +} + +% --- to set up the headers permanently +\newcommand{\markperm}[2]{% + \global\def\@evenhead{\arabic{page}\hfil{#1}}% + \global\def\@oddhead{{#2}\hfil\arabic{page}} +} +% (yes, I know I've hardwired arabic page numbering) + +% --- to set up the headers with a once off first +\newcommand{\markonce}[3]{% + \global\def\@evenhead{\arabic{page}\hfil{#1}\markperm{#2}{#3}}% + \global\def\@oddhead{{#1}\hfil\arabic{page}\markperm{#2}{#3}}% +} + +%% Put a line after the abstract %% +\newcommand{\drawline}{% + \vspace*{1mm} + \begin{center} + \rule{\textwidth}{0.2mm} + \end{center} + \vspace*{1mm} +} + +\newcommand{\reff}[1]{(\ref{#1})} + + +% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +% Define the author and title commands. Bascially each just stuffs +% the argument into a storage command. I need to define the blank +% storage commands because I am using the "renewcomand", since I +% need to do this for each article. + +\def\author{} +\def\institute{} + +\def\factTitle{} + +\def\factAuthor{} +\def\factAuthorB{} +\def\factAuthorC{} +\def\factAuthorD{} + +\def\factInstitute{} +\def\factInstituteB{} +\def\factInstituteC{} +\def\factInstituteD{} + +\def\footInst{} +\def\footInstB{} +\def\footInstC{} +\def\footInstD{} + +\def\footAuthorInst{} +\def\footAuthorInstB{} +\def\footAuthorInstC{} +\def\footAuthorInstD{} + +\renewcommand{\author}[2][]{ + \renewcommand{\footAuthorInst}{#1} + \renewcommand{\factAuthor}{#2} +} + +\newcommand{\authorB}[2][]{ + \renewcommand{\footAuthorInstB}{#1} + \renewcommand{\factAuthorB}{#2} +} + +\newcommand{\authorC}[2][]{ + \renewcommand{\footAuthorInstC}{#1} + \renewcommand{\factAuthorC}{#2} +} + +\newcommand{\authorD}[2][]{ + \renewcommand{\footAuthorInstD}{#1} + \renewcommand{\factAuthorD}{#2} +} + +\renewcommand{\institute}[2][]{ + \renewcommand{\footInst}{#1} + \renewcommand{\factInstitute}{#2} +} + +\newcommand{\instituteB}[2][*]{ + \renewcommand{\footInstB}{#1} + \renewcommand{\factInstituteB}{#2} +} + +\newcommand{\instituteC}[2][**]{ + \renewcommand{\footInstC}{#1} + \renewcommand{\factInstituteC}{#2} +} + +\newcommand{\instituteD}[2][***]{ + \renewcommand{\footInstD}{#1} + \renewcommand{\factInstituteD}{#2} +} + +\def\nameTag{} + +\renewcommand{\title}[1]{ + \renewcommand{\factTitle}{#1} +} + +\newcommand{\iimsaddress}{ + \begin{center} + \textit{Institute of Information \& Mathematical Sciences\\ + Massey University at Albany, Auckland, New Zealand.} + \end{center} +} + +\newcommand{\maketitle}[1][\factTitle]{ + \begin{center} + \markonce{ + \parbox{10cm}{\small + \textit{The Python Papers Monograph,} Vol.~1 (2009)\\[1mm] + Available online at http://ojs.pythonpapers.org/index.php/tppm + } + } + {\factAuthor\factAuthorB\factAuthorC\factAuthorD} + {#1} + + \vspace*{1cm} + + \begin{minipage}{12cm} + \begin{center} + {\bfseries\Large\factTitle\par} + \vspace*{2mm} + \end{center} + \end{minipage} + + \vspace*{3mm} + + \textsc{% + \factAuthor $^{\footAuthorInst}$% + \factAuthorB $^{\footAuthorInstB}$% + \factAuthorC $^{\footAuthorInstC}$% + \factAuthorD $^{\footAuthorInstD}$% + } + + \vspace*{1mm} + + $^{\footInst}$\textit{\factInstitute} + $^{\footInstB}$\textit{\factInstituteB} + $^{\footInstC}$\textit{\factInstituteC} + $^{\footInstD}$\textit{\factInstituteD} + + \artIndex{\nameTag} + \end{center} +} + +% calculate the margins so that the text on opposite sides +% of the same sheet is lined up at the edges +% ok, its a fudge, but you can change the oddsidemargin +% to shift both sides in sync. + +\oddsidemargin1cm +\evensidemargin\paperwidth +\addtolength{\evensidemargin}{-\oddsidemargin} +\addtolength{\evensidemargin}{-205mm} + +% --- turn off a few things ------------------------------------------------- +\renewcommand{\markboth}[2]{} +\renewcommand{\markright}[1]{} +\newcommand{\markheading}[1]{} + +\usepackage{amsmath,amsfonts,dsfont} +\usepackage{amsthm} + +\newif\ifCombinedRlims + +% === end of ARLIMS adaptions =============================================== + +\if@twoside +\else + \raggedbottom +\fi +\if@twocolumn + \twocolumn + \sloppy + \flushbottom +\else + \onecolumn +\fi +\endinput +%% +%% End of file `arlims.cls'. diff --git a/doc/KPyCon2009/arlimsTPPSC.cls b/doc/KPyCon2009/arlimsTPPSC.cls new file mode 100644 index 0000000..5fcefe7 --- /dev/null +++ b/doc/KPyCon2009/arlimsTPPSC.cls @@ -0,0 +1,898 @@ +%% +%% This is file `arlims.cls', +%% It is based on the original `article.cls' from a tetex 3.0 +%% distribution (Ubuntu tetex-base 3.0-19 package). +%% +%% Additions to it were made by (probably) various authors at the +%% Massey University Institute of Information and Mathematical +%% Sciences. +%% +%% For further information contact Heath James +%% , Guy Kloss , Paul +%% Cowpertwait or anybody you expect +%% suitable for this purpose ... +%% +%% Copyright 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 +%% The LaTeX3 Project and any individual authors listed elsewhere +%% in this file. +%% +%% This file was generated from file(s) of the LaTeX base system. +%% Numerous changes were made to be fit for RLIMS. +%% -------------------------------------------------------------- +%% +%% Changes: +%% +%% * numerous changes previously to fit article.sty for the purpose +%% +%% version 1.5: +%% +%% * fixed the instructions +%% * added better default options to the template +%% * changed the year to 2008 +%% +%% version 1.4: +%% +%% * fitted to a newer version of article.sty +%% * cleanups in layout and formatting +%% * better compliance to LaTeX2e +%% * re-fitted \institute, \instituteB, ...C, ...D +%% it doesn't work 100% nicely, but it works) +%% * removed dependency on natbib.sty (so cite.sty, or else can be +%% used as liked) +%% * changed paper size to a4paper by default +%% * removed dependencies to graphicx.sty and subfigure.sty +%% (users like to choose and do that themselves) +%% +%% -------------------------------------------------------------- +%% +%% It may be distributed and/or modified under the +%% conditions of the LaTeX Project Public License, either version 1.3 +%% of this license or (at your option) any later version. +%% The latest version of this license is in +%% http://www.latex-project.org/lppl.txt +%% and version 1.3 or later is part of all distributions of LaTeX +%% version 2003/12/01 or later. +%% +%% This file has the LPPL maintenance status "maintained". +%% +%% This file may only be distributed together with a copy of the LaTeX +%% base system. You may however distribute the LaTeX base system without +%% such generated files. +%% +%% The list of all files belonging to the LaTeX base distribution is +%% given in the file `manifest.txt'. See also `legal.txt' for additional +%% information. +%% +%% The list of derived (unpacked) files belonging to the distribution +%% and covered by LPPL is defined by the unpacking scripts (with +%% extension .ins) which are part of the distribution. +%% \CharacterTable +%% {Upper-case \A\B\C\D\E\F\G\H\I\J\K\L\M\N\O\P\Q\R\S\T\U\V\W\X\Y\Z +%% Lower-case \a\b\c\d\e\f\g\h\i\j\k\l\m\n\o\p\q\r\s\t\u\v\w\x\y\z +%% Digits \0\1\2"s\4\5\6\7\8\9 +%% Exclamation \! Double quote \" Hash (number) \# +%% Dollar \$ Percent \% Ampersand \& +%% Acute accent \' Left paren \( Right paren \) +%% Asterisk \* Plus \+ Comma \, +%% Minus \- Point \. Solidus \/ +%% Colon \: Semicolon \; Less than \< +%% Equals \= Greater than \> Question mark \? +%% Commercial at \@ Left bracket \[ Backslash \\ +%% Right bracket \] Circumflex \^ Underscore \_ +%% Grave accent \` Left brace \{ Vertical bar \| +%% Right brace \} Tilde \~} +\NeedsTeXFormat{LaTeX2e}[1995/12/01] +\ProvidesClass{arlims} + [2008/04/23 v1.5 + Massey Research Letters Article Class] +\newcommand\@ptsize{} +\newif\if@restonecol +\newif\if@titlepage +\@titlepagefalse +\if@compatibility\else +\DeclareOption{a4paper} + {\setlength\paperheight {297mm}% + \setlength\paperwidth {210mm}} +\DeclareOption{a5paper} + {\setlength\paperheight {210mm}% + \setlength\paperwidth {148mm}} +\DeclareOption{b5paper} + {\setlength\paperheight {250mm}% + \setlength\paperwidth {176mm}} +\DeclareOption{letterpaper} + {\setlength\paperheight {11in}% + \setlength\paperwidth {8.5in}} +\DeclareOption{legalpaper} + {\setlength\paperheight {14in}% + \setlength\paperwidth {8.5in}} +\DeclareOption{executivepaper} + {\setlength\paperheight {10.5in}% + \setlength\paperwidth {7.25in}} +\DeclareOption{landscape} + {\setlength\@tempdima {\paperheight}% + \setlength\paperheight {\paperwidth}% + \setlength\paperwidth {\@tempdima}} +\fi +\if@compatibility + \renewcommand\@ptsize{0} +\else +\DeclareOption{10pt}{\renewcommand\@ptsize{0}} +\fi +\DeclareOption{11pt}{\renewcommand\@ptsize{1}} +\DeclareOption{12pt}{\renewcommand\@ptsize{2}} +\if@compatibility\else +\DeclareOption{oneside}{\@twosidefalse \@mparswitchfalse} +\fi +\DeclareOption{twoside}{\@twosidetrue \@mparswitchtrue} +\DeclareOption{draft}{\setlength\overfullrule{5pt}} +\if@compatibility\else +\DeclareOption{final}{\setlength\overfullrule{0pt}} +\fi +\DeclareOption{titlepage}{\@titlepagetrue} +\if@compatibility\else +\DeclareOption{notitlepage}{\@titlepagefalse} +\fi +\if@compatibility\else +\DeclareOption{onecolumn}{\@twocolumnfalse} +\fi +\DeclareOption{twocolumn}{\@twocolumntrue} +\DeclareOption{leqno}{\input{leqno.clo}} +\DeclareOption{fleqn}{\input{fleqn.clo}} +\DeclareOption{openbib}{% + \AtEndOfPackage{% + \renewcommand\@openbib@code{% + \advance\leftmargin\bibindent + \itemindent -\bibindent + \listparindent \itemindent + \parsep \z@ + }% + \renewcommand\newblock{\par}}% +} +\ExecuteOptions{a4paper,12pt,oneside,onecolumn,final} +\ProcessOptions +\input{size1\@ptsize.clo} +\setlength\lineskip{1\p@} +\setlength\normallineskip{1\p@} +\renewcommand\baselinestretch{} +\setlength\parskip{0\p@ \@plus \p@} +\@lowpenalty 51 +\@medpenalty 151 +\@highpenalty 301 +\setcounter{topnumber}{2} +\renewcommand\topfraction{.7} +\setcounter{bottomnumber}{1} +\renewcommand\bottomfraction{.3} +\setcounter{totalnumber}{3} +\renewcommand\textfraction{.2} +\renewcommand\floatpagefraction{.5} +\setcounter{dbltopnumber}{2} +\renewcommand\dbltopfraction{.7} +\renewcommand\dblfloatpagefraction{.5} +\if@twoside + \def\ps@headings{% + \let\@oddfoot\@empty\let\@evenfoot\@empty + \def\@evenhead{\thepage\hfil\slshape\leftmark}% + \def\@oddhead{{\slshape\rightmark}\hfil\thepage}% + \let\@mkboth\markboth + \def\sectionmark##1{% + \markboth {\MakeUppercase{% + \ifnum \c@secnumdepth >\z@ + \thesection\quad + \fi + ##1}}{}}% + \def\subsectionmark##1{% + \markright {% + \ifnum \c@secnumdepth >\@ne + \thesubsection\quad + \fi + ##1}}} +\else + \def\ps@headings{% + \let\@oddfoot\@empty + \def\@oddhead{{\slshape\rightmark}\hfil\thepage}% + \let\@mkboth\markboth + \def\sectionmark##1{% + \markright {\MakeUppercase{% + \ifnum \c@secnumdepth >\m@ne + \thesection\quad + \fi + ##1}}}} +\fi +\def\ps@myheadings{% + \let\@oddfoot\@empty\let\@evenfoot\@empty + \def\@evenhead{\thepage\hfil\slshape\leftmark}% + \def\@oddhead{{\slshape\rightmark}\hfil\thepage}% + \let\@mkboth\@gobbletwo + \let\sectionmark\@gobble + \let\subsectionmark\@gobble + } + + +%---------- old definition of title page ------------------------------- +\newif\ifGrxlb\ifGrxlb + + \if@titlepage + \newcommand\maketitle{\begin{titlepage}% + \let\footnotesize\small + \let\footnoterule\relax + \let \footnote \thanks + \null\vfil + \vskip 60\p@ + \begin{center}% + {\LARGE \@title \par}% + \vskip 3em% + {\large + \lineskip .75em% + \begin{tabular}[t]{c}% + \@author + \end{tabular}\par}% + \vskip 1.5em% + {\large \@date \par}% % Set date in \large size. + \end{center}\par + \@thanks + \vfil\null + \end{titlepage}% + \setcounter{footnote}{0}% + \global\let\thanks\relax + \global\let\maketitle\relax + \global\let\@thanks\@empty + \global\let\@author\@empty + \global\let\@date\@empty + \global\let\@title\@empty + \global\let\title\relax + \global\let\author\relax + \global\let\date\relax + \global\let\and\relax +} +\else +\newcommand\maketitle{\par + \begingroup + \renewcommand\thefootnote{\@fnsymbol\c@footnote}% + \def\@makefnmark{\rlap{\@textsuperscript{\normalfont\@thefnmark}}}% + \long\def\@makefntext##1{\parindent 1em\noindent + \hb@xt@1.8em{% + \hss\@textsuperscript{\normalfont\@thefnmark}}##1}% + \if@twocolumn + \ifnum \col@number=\@ne + \@maketitle + \else + \twocolumn[\@maketitle]% + \fi + \else + \newpage + \global\@topnum\z@ % Prevents figures from going at top of page. + \@maketitle + \fi + \thispagestyle{plain}\@thanks + \endgroup + \setcounter{footnote}{0}% + \global\let\thanks\relax + \global\let\maketitle\relax + \global\let\@maketitle\relax + \global\let\@thanks\@empty + \global\let\@author\@empty + \global\let\@date\@empty + \global\let\@title\@empty + \global\let\title\relax + \global\let\author\relax + \global\let\date\relax + \global\let\and\relax +} +\def\@maketitle{% + \newpage + \null + \vskip 2em% + \begin{center}% + \let \footnote \thanks + {\LARGE \@title \par}% + \vskip 1.5em% + {\large + \lineskip .5em% + \begin{tabular}[t]{c}% + \@author + \end{tabular}\par}% + \vskip 1em% + {\large \@date}% + \end{center}% + \par + \vskip 1.5em} +\fi + +\fi +%---------------- end of old definition of title page --------------------- + +\setcounter{secnumdepth}{3} +\newcounter {part} +\newcounter {section} +\newcounter {subsection}[section] +\newcounter {subsubsection}[subsection] +\newcounter {paragraph}[subsubsection] +\newcounter {subparagraph}[paragraph] +\renewcommand \thepart {\@Roman\c@part} +\renewcommand \thesection {\@arabic\c@section} +\renewcommand\thesubsection {\thesection.\@arabic\c@subsection} +\renewcommand\thesubsubsection{\thesubsection .\@arabic\c@subsubsection} +\renewcommand\theparagraph {\thesubsubsection.\@arabic\c@paragraph} +\renewcommand\thesubparagraph {\theparagraph.\@arabic\c@subparagraph} +\newcommand\part{% + \if@noskipsec \leavevmode \fi + \par + \addvspace{4ex}% + \@afterindentfalse + \secdef\@part\@spart} + +\def\@part[#1]#2{% + \ifnum \c@secnumdepth >\m@ne + \refstepcounter{part}% + \addcontentsline{toc}{part}{\thepart\hspace{1em}#1}% + \else + \addcontentsline{toc}{part}{#1}% + \fi + {\parindent \z@ \raggedright + \interlinepenalty \@M + \normalfont + \ifnum \c@secnumdepth >\m@ne + \Large\bfseries \partname\nobreakspace\thepart + \par\nobreak + \fi + \huge \bfseries #2% + \markboth{}{}\par}% + \nobreak + \vskip 3ex + \@afterheading} +\def\@spart#1{% + {\parindent \z@ \raggedright + \interlinepenalty \@M + \normalfont + \huge \bfseries #1\par}% + \nobreak + \vskip 3ex + \@afterheading} +\newcommand\section{\@startsection {section}{1}{\z@}% + {-3.5ex \@plus -1ex \@minus -.2ex}% + {2.3ex \@plus.2ex}% + {\normalfont\Large\bfseries}} +\newcommand\subsection{\@startsection{subsection}{2}{\z@}% + {-3.25ex\@plus -1ex \@minus -.2ex}% + {1.5ex \@plus .2ex}% + {\normalfont\large\bfseries}} +\newcommand\subsubsection{\@startsection{subsubsection}{3}{\z@}% + {-3.25ex\@plus -1ex \@minus -.2ex}% + {1.5ex \@plus .2ex}% + {\normalfont\normalsize\bfseries}} +\newcommand\paragraph{\@startsection{paragraph}{4}{\z@}% + {3.25ex \@plus1ex \@minus.2ex}% + {-1em}% + {\normalfont\normalsize\bfseries}} +\newcommand\subparagraph{\@startsection{subparagraph}{5}{\parindent}% + {3.25ex \@plus1ex \@minus .2ex}% + {-1em}% + {\normalfont\normalsize\bfseries}} +\if@twocolumn + \setlength\leftmargini {2em} +\else + \setlength\leftmargini {2.5em} +\fi +\leftmargin \leftmargini +\setlength\leftmarginii {2.2em} +\setlength\leftmarginiii {1.87em} +\setlength\leftmarginiv {1.7em} +\if@twocolumn + \setlength\leftmarginv {.5em} + \setlength\leftmarginvi {.5em} +\else + \setlength\leftmarginv {1em} + \setlength\leftmarginvi {1em} +\fi +\setlength \labelsep {.5em} +\setlength \labelwidth{\leftmargini} +\addtolength\labelwidth{-\labelsep} +\@beginparpenalty -\@lowpenalty +\@endparpenalty -\@lowpenalty +\@itempenalty -\@lowpenalty +\renewcommand\theenumi{\@arabic\c@enumi} +\renewcommand\theenumii{\@alph\c@enumii} +\renewcommand\theenumiii{\@roman\c@enumiii} +\renewcommand\theenumiv{\@Alph\c@enumiv} +\newcommand\labelenumi{\theenumi.} +\newcommand\labelenumii{(\theenumii)} +\newcommand\labelenumiii{\theenumiii.} +\newcommand\labelenumiv{\theenumiv.} +\renewcommand\p@enumii{\theenumi} +\renewcommand\p@enumiii{\theenumi(\theenumii)} +\renewcommand\p@enumiv{\p@enumiii\theenumiii} +\newcommand\labelitemi{\textbullet} +\newcommand\labelitemii{\normalfont\bfseries \textendash} +\newcommand\labelitemiii{\textasteriskcentered} +\newcommand\labelitemiv{\textperiodcentered} +\newenvironment{description} + {\list{}{\labelwidth\z@ \itemindent-\leftmargin + \let\makelabel\descriptionlabel}} + {\endlist} +\newcommand*\descriptionlabel[1]{\hspace\labelsep + \normalfont\bfseries #1} +\if@titlepage + \newenvironment{abstract}{% + \titlepage + \null\vfil + \@beginparpenalty\@lowpenalty + \begin{center}% + \bfseries \abstractname + \@endparpenalty\@M + \end{center}}% + {\par\vfil\null\endtitlepage} +\else + \newenvironment{abstract}{% + \if@twocolumn + \section*{\abstractname}% + \else + \small + \begin{center}% + {\bfseries \abstractname\vspace{-.5em}\vspace{\z@}}% + \end{center}% + \quotation + \fi} + {\if@twocolumn\else\endquotation\fi} +\fi +\newenvironment{verse} + {\let\\\@centercr + \list{}{\itemsep \z@ + \itemindent -1.5em% + \listparindent\itemindent + \rightmargin \leftmargin + \advance\leftmargin 1.5em}% + \item\relax} + {\endlist} +\newenvironment{quotation} + {\list{}{\listparindent 1.5em% + \itemindent \listparindent + \rightmargin \leftmargin + \parsep \z@ \@plus\p@}% + \item\relax} + {\endlist} +\newenvironment{quote} + {\list{}{\rightmargin\leftmargin}% + \item\relax} + {\endlist} +\if@compatibility +\newenvironment{titlepage} + {% + \if@twocolumn + \@restonecoltrue\onecolumn + \else + \@restonecolfalse\newpage + \fi + \thispagestyle{empty}% + \setcounter{page}\z@ + }% + {\if@restonecol\twocolumn \else \newpage \fi + } +\else +\newenvironment{titlepage} + {% + \if@twocolumn + \@restonecoltrue\onecolumn + \else + \@restonecolfalse\newpage + \fi + \thispagestyle{empty}% + \setcounter{page}\@ne + }% + {\if@restonecol\twocolumn \else \newpage \fi + \if@twoside\else + \setcounter{page}\@ne + \fi + } +\fi +\newcommand\appendix{\par + \setcounter{section}{0}% + \setcounter{subsection}{0}% + \gdef\thesection{\@Alph\c@section}} +\setlength\arraycolsep{5\p@} +\setlength\tabcolsep{6\p@} +\setlength\arrayrulewidth{.4\p@} +\setlength\doublerulesep{2\p@} +\setlength\tabbingsep{\labelsep} +\skip\@mpfootins = \skip\footins +\setlength\fboxsep{3\p@} +\setlength\fboxrule{.4\p@} +\renewcommand \theequation {\@arabic\c@equation} +\newcounter{figure} +\renewcommand \thefigure {\@arabic\c@figure} +\def\fps@figure{tbp} +\def\ftype@figure{1} +\def\ext@figure{lof} +\def\fnum@figure{\figurename\nobreakspace\thefigure} +\newenvironment{figure} + {\@float{figure}} + {\end@float} +\newenvironment{figure*} + {\@dblfloat{figure}} + {\end@dblfloat} +\newcounter{table} +\renewcommand\thetable{\@arabic\c@table} +\def\fps@table{tbp} +\def\ftype@table{2} +\def\ext@table{lot} +\def\fnum@table{\tablename\nobreakspace\thetable} +\newenvironment{table} + {\@float{table}} + {\end@float} +\newenvironment{table*} + {\@dblfloat{table}} + {\end@dblfloat} +\newlength\abovecaptionskip +\newlength\belowcaptionskip +\setlength\abovecaptionskip{10\p@} +\setlength\belowcaptionskip{0\p@} +\long\def\@makecaption#1#2{% + \vskip\abovecaptionskip + \sbox\@tempboxa{#1: #2}% + \ifdim \wd\@tempboxa >\hsize + #1: #2\par + \else + \global \@minipagefalse + \hb@xt@\hsize{\hfil\box\@tempboxa\hfil}% + \fi + \vskip\belowcaptionskip} +\DeclareOldFontCommand{\rm}{\normalfont\rmfamily}{\mathrm} +\DeclareOldFontCommand{\sf}{\normalfont\sffamily}{\mathsf} +\DeclareOldFontCommand{\tt}{\normalfont\ttfamily}{\mathtt} +\DeclareOldFontCommand{\bf}{\normalfont\bfseries}{\mathbf} +\DeclareOldFontCommand{\it}{\normalfont\itshape}{\mathit} +\DeclareOldFontCommand{\sl}{\normalfont\slshape}{\@nomath\sl} +\DeclareOldFontCommand{\sc}{\normalfont\scshape}{\@nomath\sc} +\DeclareRobustCommand*\cal{\@fontswitch\relax\mathcal} +\DeclareRobustCommand*\mit{\@fontswitch\relax\mathnormal} +\newcommand\@pnumwidth{1.55em} +\newcommand\@tocrmarg{2.55em} +\newcommand\@dotsep{4.5} +\setcounter{tocdepth}{3} +\newcommand\tableofcontents{% + \section*{\contentsname + \@mkboth{% + \MakeUppercase\contentsname}{\MakeUppercase\contentsname}}% + \@starttoc{toc}% + } +\newcommand*\l@part[2]{% + \ifnum \c@tocdepth >-2\relax + \addpenalty\@secpenalty + \addvspace{2.25em \@plus\p@}% + \setlength\@tempdima{3em}% + \begingroup + \parindent \z@ \rightskip \@pnumwidth + \parfillskip -\@pnumwidth + {\leavevmode + \large \bfseries #1\hfil \hb@xt@\@pnumwidth{\hss #2}}\par + \nobreak + \if@compatibility + \global\@nobreaktrue + \everypar{\global\@nobreakfalse\everypar{}}% + \fi + \endgroup + \fi} +\newcommand*\l@section[2]{% + \ifnum \c@tocdepth >\z@ + \addpenalty\@secpenalty + \addvspace{1.0em \@plus\p@}% + \setlength\@tempdima{1.5em}% + \begingroup + \parindent \z@ \rightskip \@pnumwidth + \parfillskip -\@pnumwidth + \leavevmode \bfseries + \advance\leftskip\@tempdima + \hskip -\leftskip + #1\nobreak\hfil \nobreak\hb@xt@\@pnumwidth{\hss #2}\par + \endgroup + \fi} +\newcommand*\l@subsection{\@dottedtocline{2}{1.5em}{2.3em}} +\newcommand*\l@subsubsection{\@dottedtocline{3}{3.8em}{3.2em}} +\newcommand*\l@paragraph{\@dottedtocline{4}{7.0em}{4.1em}} +\newcommand*\l@subparagraph{\@dottedtocline{5}{10em}{5em}} +\newcommand\listoffigures{% + \section*{\listfigurename}% + \@mkboth{\MakeUppercase\listfigurename}% + {\MakeUppercase\listfigurename}% + \@starttoc{lof}% + } +\newcommand*\l@figure{\@dottedtocline{1}{1.5em}{2.3em}} +\newcommand\listoftables{% + \section*{\listtablename}% + \@mkboth{% + \MakeUppercase\listtablename}% + {\MakeUppercase\listtablename}% + \@starttoc{lot}% + } +\let\l@table\l@figure +\newdimen\bibindent +\setlength\bibindent{1.5em} +\newenvironment{thebibliography}[1] + {\section*{\refname}% + \@mkboth{\MakeUppercase\refname}{\MakeUppercase\refname}% + \list{\@biblabel{\@arabic\c@enumiv}}% + {\settowidth\labelwidth{\@biblabel{#1}}% + \leftmargin\labelwidth + \advance\leftmargin\labelsep + \@openbib@code + \usecounter{enumiv}% + \let\p@enumiv\@empty + \renewcommand\theenumiv{\@arabic\c@enumiv}}% + \sloppy + \clubpenalty4000 + \@clubpenalty \clubpenalty + \widowpenalty4000% + \sfcode`\.\@m} + {\def\@noitemerr + {\@latex@warning{Empty `thebibliography' environment}}% + \endlist} +\newcommand\newblock{\hskip .11em\@plus.33em\@minus.07em} +\let\@openbib@code\@empty +\newenvironment{theindex} + {\if@twocolumn + \@restonecolfalse + \else + \@restonecoltrue + \fi + \twocolumn[\section*{\indexname}]% + \@mkboth{\MakeUppercase\indexname}% + {\MakeUppercase\indexname}% + \thispagestyle{plain}\parindent\z@ + \parskip\z@ \@plus .3\p@\relax + \columnseprule \z@ + \columnsep 35\p@ + \let\item\@idxitem} + {\if@restonecol\onecolumn\else\clearpage\fi} +\newcommand\@idxitem{\par\hangindent 40\p@} +\newcommand\subitem{\@idxitem \hspace*{20\p@}} +\newcommand\subsubitem{\@idxitem \hspace*{30\p@}} +\newcommand\indexspace{\par \vskip 10\p@ \@plus5\p@ \@minus3\p@\relax} +\renewcommand\footnoterule{% + \kern-3\p@ + \hrule\@width.4\columnwidth + \kern2.6\p@} +\newcommand\@makefntext[1]{% + \parindent 1em% + \noindent + \hb@xt@1.8em{\hss\@makefnmark}#1} +\newcommand\contentsname{Contents} +\newcommand\listfigurename{List of Figures} +\newcommand\listtablename{List of Tables} +\newcommand\refname{References} +\newcommand\indexname{Index} +\newcommand\figurename{Figure} +\newcommand\tablename{Table} +\newcommand\partname{Part} +\newcommand\appendixname{Appendix} +\newcommand\abstractname{} +\def\today{\ifcase\month\or + January\or February\or March\or April\or May\or June\or + July\or August\or September\or October\or November\or December\fi + \space\number\day, \number\year} +\setlength\columnsep{10\p@} +\setlength\columnseprule{0\p@} +\pagestyle{plain} +\pagenumbering{arabic} + +% === ARLIMS adaptions following ============================================ +\pagestyle{empty} + +\setlength{\oddsidemargin}{1.5 cm} +\setlength{\evensidemargin}{-0.5 cm} +\setlength{\textheight}{22 cm} +\setlength{\textwidth}{15 cm} +\setlength{\topmargin}{0 cm} + +% --- special indexing to cope with the list of articles +\newif\ifIndexMade +\def\makeartIndex{% + \protected@write1{}{\string\global\string\IndexMadetrue} + \newwrite\@artIndexfile + \immediate\openout\@artIndexfile=\jobname.adx + \def\artIndex{\@bsphack\begingroup\@sanitize\@wrartIndex} + \typeout{Writing artIndex file \jobname.adx}% + \let\makeartIndex\@empty +} + +\def\@wrartIndex#1{% + \protected@write\@artIndexfile{}{% + \string\artIndexentry{\factTitle}% + {\thepage}% + {\string\last#1}% + {\factAuthor\factAuthorB\factAuthorC\factAuthorD}}% + \endgroup% + \@esphack% +} + +\def\artIndex{\@bsphack\begingroup \@sanitize\@artIndex} +\def\@artIndex#1{\endgroup\@esphack} + +\def\artIndexLast#1{% + \protected@write1{}{\string\gdef\string\last#1{\thepage}}% +} + +% --- to set up the headers permanently +\newcommand{\markperm}[2]{% + \global\def\@evenhead{\arabic{page}\hfil{#1}}% + \global\def\@oddhead{{#2}\hfil\arabic{page}} +} +% (yes, I know I've hardwired arabic page numbering) + +% --- to set up the headers with a once off first +\newcommand{\markonce}[3]{% + \global\def\@evenhead{\arabic{page}\hfil{#1}\markperm{#2}{#3}}% + \global\def\@oddhead{{#1}\hfil\arabic{page}\markperm{#2}{#3}}% +} + +%% Put a line after the abstract %% +\newcommand{\drawline}{% + \vspace*{1mm} + \begin{center} + \rule{\textwidth}{0.2mm} + \end{center} + \vspace*{1mm} +} + +\newcommand{\reff}[1]{(\ref{#1})} + + +% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +% Define the author and title commands. Bascially each just stuffs +% the argument into a storage command. I need to define the blank +% storage commands because I am using the "renewcomand", since I +% need to do this for each article. + +\def\author{} +\def\institute{} + +\def\factTitle{} + +\def\factAuthor{} +\def\factAuthorB{} +\def\factAuthorC{} +\def\factAuthorD{} + +\def\factInstitute{} +\def\factInstituteB{} +\def\factInstituteC{} +\def\factInstituteD{} + +\def\footInst{} +\def\footInstB{} +\def\footInstC{} +\def\footInstD{} + +\def\footAuthorInst{} +\def\footAuthorInstB{} +\def\footAuthorInstC{} +\def\footAuthorInstD{} + +\renewcommand{\author}[2][]{ + \renewcommand{\footAuthorInst}{#1} + \renewcommand{\factAuthor}{#2} +} + +\newcommand{\authorB}[2][]{ + \renewcommand{\footAuthorInstB}{#1} + \renewcommand{\factAuthorB}{#2} +} + +\newcommand{\authorC}[2][]{ + \renewcommand{\footAuthorInstC}{#1} + \renewcommand{\factAuthorC}{#2} +} + +\newcommand{\authorD}[2][]{ + \renewcommand{\footAuthorInstD}{#1} + \renewcommand{\factAuthorD}{#2} +} + +\renewcommand{\institute}[2][]{ + \renewcommand{\footInst}{#1} + \renewcommand{\factInstitute}{#2} +} + +\newcommand{\instituteB}[2][*]{ + \renewcommand{\footInstB}{#1} + \renewcommand{\factInstituteB}{#2} +} + +\newcommand{\instituteC}[2][**]{ + \renewcommand{\footInstC}{#1} + \renewcommand{\factInstituteC}{#2} +} + +\newcommand{\instituteD}[2][***]{ + \renewcommand{\footInstD}{#1} + \renewcommand{\factInstituteD}{#2} +} + +\def\nameTag{} + +\renewcommand{\title}[1]{ + \renewcommand{\factTitle}{#1} +} + +\newcommand{\iimsaddress}{ + \begin{center} + \textit{Institute of Information \& Mathematical Sciences\\ + Massey University at Albany, Auckland, New Zealand.} + \end{center} +} + +\newcommand{\maketitle}[1][\factTitle]{ + \begin{center} + \markonce{ + \parbox{10cm}{\small + \textit{The Python Papers Source Codes,} Vol.~1 (2009)\\[1mm] + Available online at http://ojs.pythonpapers.org/index.php/tppsc/issue/view/13 + } + } + {\factAuthor\factAuthorB\factAuthorC\factAuthorD} + {#1} + + \vspace*{1cm} + + \begin{minipage}{12cm} + \begin{center} + {\bfseries\Large\factTitle\par} + \vspace*{2mm} + \end{center} + \end{minipage} + + \vspace*{3mm} + + \textsc{% + \factAuthor $^{\footAuthorInst}$% + \factAuthorB $^{\footAuthorInstB}$% + \factAuthorC $^{\footAuthorInstC}$% + \factAuthorD $^{\footAuthorInstD}$% + } + + \vspace*{1mm} + + $^{\footInst}$\textit{\factInstitute} + $^{\footInstB}$\textit{\factInstituteB} + $^{\footInstC}$\textit{\factInstituteC} + $^{\footInstD}$\textit{\factInstituteD} + + \artIndex{\nameTag} + \end{center} +} + +% calculate the margins so that the text on opposite sides +% of the same sheet is lined up at the edges +% ok, its a fudge, but you can change the oddsidemargin +% to shift both sides in sync. + +\oddsidemargin1cm +\evensidemargin\paperwidth +\addtolength{\evensidemargin}{-\oddsidemargin} +\addtolength{\evensidemargin}{-205mm} + +% --- turn off a few things ------------------------------------------------- +\renewcommand{\markboth}[2]{} +\renewcommand{\markright}[1]{} +\newcommand{\markheading}[1]{} + +\usepackage{amsmath,amsfonts,dsfont} +\usepackage{amsthm} + +\newif\ifCombinedRlims + +% === end of ARLIMS adaptions =============================================== + +\if@twoside +\else + \raggedbottom +\fi +\if@twocolumn + \twocolumn + \sloppy + \flushbottom +\else + \onecolumn +\fi +\endinput +%% +%% End of file `arlims.cls'. diff --git a/doc/KPyCon2009/code/beerdistribution.py b/doc/KPyCon2009/code/beerdistribution.py new file mode 100644 index 0000000..38d9095 --- /dev/null +++ b/doc/KPyCon2009/code/beerdistribution.py @@ -0,0 +1,76 @@ +""" +The Beer Distribution Problem for the PuLP Modeller + +Authors: Antony Phillips, Dr Stuart Mitchell 2007 +""" + +# Import PuLP modeler functions +import pulp + +# Creates a list of all the supply nodes +warehouses = ["A", "B"] + +# Creates a dictionary for the number of units of supply for each supply node +supply = {"A": 1000, + "B": 4000} + +# Creates a list of all demand nodes +bars = ["1", "2", "3", "4", "5"] + +# Creates a dictionary for the number of units of demand for each demand node +demand = {"1":500, + "2":900, + "3":1800, + "4":200, + "5":700,} + +# Creates a list of costs of each transportation path +costs = [ #Bars + #1 2 3 4 5 + [2,4,5,2,1],#A Warehouses + [3,1,3,2,3] #B + ] + +# The cost data is made into a dictionary +costs = pulp.makeDict([warehouses, bars], costs,0) + +# Creates the 'prob' variable to contain the problem data +prob = pulp.LpProblem("Beer Distribution Problem", pulp.LpMinimize) + +# Creates a list of tuples containing all the possible routes for transport +routes = [(w,b) for w in warehouses for b in bars] + +# A dictionary called x is created to contain quantity shipped on the routes +x = pulp.LpVariable.dicts("route", (warehouses, bars), + lowBound = 0 + cat = pulp.LpInteger) + +# The objective function is added to 'prob' first +prob += sum([x[w][b]*costs[w][b] for (w,b) in routes]), \ + "Sum_of_Transporting_Costs" + +# Supply maximum constraints are added to prob for each supply node (warehouse) +for w in warehouses: + prob += sum([x[w][b] for b in bars]) <= supply[w], \ + "Sum_of_Products_out_of_Warehouse_%s"%w + +# Demand minimum constraints are added to prob for each demand node (bar) +for b in bars: + prob += sum([x[w][b] for w in warehouses]) >= demand[b], \ + "Sum_of_Products_into_Bar%s"%b + +# The problem data is written to an .lp file +prob.writeLP("BeerDistributionProblem.lp") + +# The problem is solved using PuLP's choice of Solver +prob.solve() + +# The status of the solution is printed to the screen +print "Status:", pulp.LpStatus[prob.status] + +# Each of the variables is printed with it's resolved optimum value +for v in prob.variables(): + print v.name, "=", v.varValue + +# The optimised objective function value is printed to the screen +print "Total Cost of Transportation = ", prob.objective.value() diff --git a/doc/KPyCon2009/code/wedding.py b/doc/KPyCon2009/code/wedding.py new file mode 100644 index 0000000..7417b60 --- /dev/null +++ b/doc/KPyCon2009/code/wedding.py @@ -0,0 +1,50 @@ +""" +A set partitioning model of a wedding seating problem + +Authors: Stuart Mitchell 2009 +""" + +import pulp + +max_tables = 5 +max_table_size = 4 +guests = 'A B C D E F G I J K L M N O P Q R'.split() + +def happiness(table): + """ + Find the happiness of the table + - by calculating the maximum distance between the letters + """ + return abs(ord(table[0]) - ord(table[-1])) + +#create list of all possible tables +possible_tables = [tuple(c) for c in pulp.allcombinations(guests, + max_table_size)] + +#create a binary variable to state that a table setting is used +x = pulp.LpVariable.dicts('table', possible_tables, + lowBound = 0, + upBound = 1, + cat = pulp.LpInteger) + +seating_model = pulp.LpProblem("Wedding Seating Model", pulp.LpMinimize) + +seating_model += sum([happiness(table) * x[table] for table in possible_tables]) + +#specify the maximum number of tables +seating_model += sum([x[table] for table in possible_tables]) <= max_tables, \ + "Maximum_number_of_tables" + +#A guest must seated at one and only one table +for guest in guests: + seating_model += sum([x[table] for table in possible_tables + if guest in table]) == 1, "Must_seat_%s"%guest + +seating_model.solve() + +print "The choosen tables are out of a total of %s:"%len(possible_tables) +for table in possible_tables: + if x[table].value() == 1.0: + print table + + diff --git a/doc/KPyCon2009/code/whiskas.py b/doc/KPyCon2009/code/whiskas.py new file mode 100644 index 0000000..70349e9 --- /dev/null +++ b/doc/KPyCon2009/code/whiskas.py @@ -0,0 +1,38 @@ +""" +Example problem file that solves the whiskas blending problem +""" +import pulp + +#initialise the model +whiskas_model = pulp.LpProblem('The Whiskas Problem', pulp.LpMinimize) +# make a list of ingredients +ingredients = ['chicken', 'beef', 'mutton', 'rice', 'wheat', 'gel'] +# create a dictionary of pulp variables with keys from ingredients +# the default lower bound is -inf +x = pulp.LpVariable.dict('x_%s', ingredients, lowBound =0) + +# cost data +cost = dict(zip(ingredients, [0.013, 0.008, 0.010, 0.002, 0.005, 0.001])) +# create the objective +whiskas_model += sum( [cost[i] * x[i] for i in ingredients]) + +# ingredient parameters +protein = dict(zip(ingredients, [0.100, 0.200, 0.150, 0.000, 0.040, 0.000])) +fat = dict(zip(ingredients, [0.080, 0.100, 0.110, 0.010, 0.010, 0.000])) +fibre = dict(zip(ingredients, [0.001, 0.005, 0.003, 0.100, 0.150, 0.000])) +salt = dict(zip(ingredients, [0.002, 0.005, 0.007, 0.002, 0.008, 0.000])) + +#note these are constraints and not an objective as there is a equality/inequality +whiskas_model += sum([protein[i]*x[i] for i in ingredients]) >= 8.0 +whiskas_model += sum([fat[i]*x[i] for i in ingredients]) >= 6.0 +whiskas_model += sum([fibre[i]*x[i] for i in ingredients]) <= 2.0 +whiskas_model += sum([salt[i]*x[i] for i in ingredients]) <= 0.4 + +#problem is then solved with the default solver +whiskas_model.solve() + +#print the result +for ingredient in ingredients: + print 'The mass of %s is %s grams per can'%(ingredient, + x[ingredient].value()) + diff --git a/doc/KPyCon2009/images/beerdistribution.png b/doc/KPyCon2009/images/beerdistribution.png new file mode 100644 index 0000000..b9165cc Binary files /dev/null and b/doc/KPyCon2009/images/beerdistribution.png differ diff --git a/doc/KPyCon2009/images/whiskas_blend.jpg b/doc/KPyCon2009/images/whiskas_blend.jpg new file mode 100644 index 0000000..11cb5e4 Binary files /dev/null and b/doc/KPyCon2009/images/whiskas_blend.jpg differ diff --git a/doc/KPyCon2009/images/whiskas_ingredients.jpg b/doc/KPyCon2009/images/whiskas_ingredients.jpg new file mode 100644 index 0000000..fdda9c9 Binary files /dev/null and b/doc/KPyCon2009/images/whiskas_ingredients.jpg differ diff --git a/doc/KPyCon2009/images/whiskas_label.jpg b/doc/KPyCon2009/images/whiskas_label.jpg new file mode 100644 index 0000000..8584271 Binary files /dev/null and b/doc/KPyCon2009/images/whiskas_label.jpg differ diff --git a/doc/KPyCon2009/images/whiskas_nutrition.jpg b/doc/KPyCon2009/images/whiskas_nutrition.jpg new file mode 100644 index 0000000..9622c47 Binary files /dev/null and b/doc/KPyCon2009/images/whiskas_nutrition.jpg differ diff --git a/doc/KPyCon2009/references.bib b/doc/KPyCon2009/references.bib new file mode 100644 index 0000000..c22af5c --- /dev/null +++ b/doc/KPyCon2009/references.bib @@ -0,0 +1,97 @@ +% This file was created with JabRef 2.5. +% Encoding: ISO8859_1 + +@InProceedings{ flopc, + title = "FLOPC++ An Algebraic Modeling Language Embedded in C++", + booktitle = "Operations Research Proceedings 2006", + series = "Operations Research Proceedings", + author = "Tim Helge Hultberg", + editor = "Karl-Heinz Waldmann and Ulrike M. Stocker", + publisher = "Springer Berlin Heidelberg", + location = "Karlsruhe", + pages = "187--190", + number = "6", + year = "2006", + abstract = "FLOPC++ is an open source algebraic modeling language implemented as a C++ class library. It allows linear optimization problems to be modeled in a declarative style, similar to algebraic modeling languages, such as GAMS and AMPL, within a C++ program. The project is part of COmputational INfrastructure for Operations Research (COIN-OR) and uses its Open Solver Interface (OSI) to achieve solver independence." +} + +@Misc{ gams, + author = "A. Brooke and D. Kendrick and A. Meeraus", + title = "GAMS: A User's Guide", + text = "A. Brooke, D. Kendrick, and A. Meeraus. GAMS: A User's Guide, Release 2.25. The Scientific Press, 1992.", + year = "1992", + url = "citeseer.ist.psu.edu/brooke92gams.html" +} + +@Article{ ampl, + title = "AMPL: A Mathematical Programming Language", + author = "Robert Fourer and David M. Gay and Brian W. Kernighan", + journal = "Management Science", + pages = "519--554", + volume = "36", + year = "1990" +} + +@Article{ columngeneration, + title = "Selected Topics in Column Generation ", + author = "Marco E. L{\"u}bbecke and Jacques Desrosiers", + journal = "OPERATIONS RESEARCH", + pages = "1007--1023", + volume = "53", + number = "6", + month = "November- December", + year = "2005", + doi = "10.1287/opre.1050.0234" +} + +@Electronic{ pulpwiki, + title = "Pulp-or wiki", + author = "Stuart A Mitchell and Antony Phillips", + url = "http://pulp-or.googlecode.com" +} + +@Electronic{ scienceofbetter, + title = "Operations Research: The Science of Better", + url = "http://www.scienceofbetter.org/" +} + +@Article{ coin-or, + title = "The Common Optimization INterface for Operations Research", + author = "Robin Lougee-Heimer", + journal = "IBM Journal of Research and Development", + pages = "57--66", + volume = "47", + number = "1", + month = "January", + year = "2003" +} + +@Electronic{ glpk, + title = "GNU Linear Programming Kit", + url = "http://www.gnu.org/software/glpk/glpk.html" +} + +@Electronic{ cplex, + title = "Cplex Website", + url = "http://www.ilog.com/products/cplex/" +} + +@Electronic{ gurobi, + title = "Gurobi Website", + url = "http://www.gurobi.com/" +} + +@Electronic{ coopr, + title = "Coopr: A COmmon Optimization Python Repository", + author = "William E. Hart", + url = "https://software.sandia.gov/trac/coopr" +} + +@InProceedings{ pyomo, + title = "Python Optimization Modeling Objects (Pyomo)", + booktitle = "Proc INFORMS Computing Society Conference", + author = "William Hart", + year = "2009", + url = "http://www.optimization-online.org/DB_HTML/2008/09/2095.html" +} + diff --git a/doc/KiwiPycon.odp b/doc/KiwiPycon.odp new file mode 100644 index 0000000..d67ae35 Binary files /dev/null and b/doc/KiwiPycon.odp differ diff --git a/doc/KiwiPycon.pdf b/doc/KiwiPycon.pdf new file mode 100644 index 0000000..aa034c4 Binary files /dev/null and b/doc/KiwiPycon.pdf differ diff --git a/doc/OpenSourceOptimisation.odp b/doc/OpenSourceOptimisation.odp new file mode 100644 index 0000000..4679344 Binary files /dev/null and b/doc/OpenSourceOptimisation.odp differ diff --git a/doc/OpenSourceOptimisation.pdf b/doc/OpenSourceOptimisation.pdf new file mode 100644 index 0000000..ced3071 Binary files /dev/null and b/doc/OpenSourceOptimisation.pdf differ diff --git a/doc/PulpOptimisation.odp b/doc/PulpOptimisation.odp new file mode 100644 index 0000000..b6d8035 Binary files /dev/null and b/doc/PulpOptimisation.odp differ diff --git a/doc/PulpOptimisation.pdf b/doc/PulpOptimisation.pdf new file mode 100644 index 0000000..f9548e3 Binary files /dev/null and b/doc/PulpOptimisation.pdf differ diff --git a/doc/_OptimisationWithPuLP.pdf b/doc/_OptimisationWithPuLP.pdf new file mode 100644 index 0000000..c582a89 Binary files /dev/null and b/doc/_OptimisationWithPuLP.pdf differ diff --git a/doc/pulp.pdf b/doc/pulp.pdf new file mode 100644 index 0000000..afda7eb Binary files /dev/null and b/doc/pulp.pdf differ diff --git a/doc/source/AUTHORS.txt b/doc/source/AUTHORS.txt new file mode 100644 index 0000000..7f2e1af --- /dev/null +++ b/doc/source/AUTHORS.txt @@ -0,0 +1,5 @@ ++ Stuart Mitchell (s.mitchell@auckland.ac.nz) ++ Anita Kean ++ Andrew Mason ++ Michael O\'Sullivan ++ Antony Phillips diff --git a/doc/source/CaseStudies/a_blending_problem.rst b/doc/source/CaseStudies/a_blending_problem.rst new file mode 100644 index 0000000..ec49b62 --- /dev/null +++ b/doc/source/CaseStudies/a_blending_problem.rst @@ -0,0 +1,336 @@ +A Blending Problem +=================== + +Problem Description +------------------- + +.. image:: images/whiskas_label.jpg + +Whiskas cat food, shown above, is manufactured by Uncle Ben’s. +Uncle Ben’s want to produce their cat food products as cheaply as possible +while ensuring they meet the stated nutritional analysis requirements +shown on the cans. Thus they want to vary the quantities of each +ingredient used (the main ingredients being chicken, beef, mutton, +rice, wheat and gel) while still meeting their nutritional standards. + +.. image:: images/whiskas_blend.jpg + +The costs of the chicken, beef, and mutton are $0.013, $0.008 and +$0.010 respectively, while the costs of the rice, wheat and gel are +$0.002, $0.005 and $0.001 respectively. (All costs are per gram.) For +this exercise we will ignore the vitamin and mineral ingredients. (Any +costs for these are likely to be very small anyway.) + +Each ingredient contributes to the total weight of protein, fat, +fibre and salt in the final product. The contributions (in grams) per +gram of ingredient are given in the table below. + + + ============ ========= ========= ======== ======= + Stuff Protein Fat Fibre Salt + ============ ========= ========= ======== ======= + Chicken 0.100 0.080 0.001 0.002 + Beef 0.200 0.100 0.005 0.005 + Rice 0.000 0.010 0.100 0.002 + Wheat bran 0.040 0.010 0.150 0.008 + ============ ========= ========= ======== ======= + +Simplified Formulation +~~~~~~~~~~~~~~~~~~~~~~ + +First we will consider a simplified problem to build a simple Python model. + +Identify the Decision Variables ++++++++++++++++++++++++++++++++++++ + +Assume Whiskas want to make their cat food out of just two ingredients: +Chicken and Beef. We will first define our decision variables: + +.. math:: + + x_1 &= \text{ percentage of chicken meat in a can of cat food }\\ + x_2 &= \text{ percentage of beef used in a can of cat food } + +The limitations on these variables (greater than zero) must be noted but +for the Python implementation, they are not entered or listed separately or with the other constraints. + +Formulate the Objective Function +++++++++++++++++++++++++++++++++ + +The objective function becomes: + +.. math:: \textbf{ min } 0.013 x_1 + 0.008 x_2 + +The Constraints ++++++++++++++++ + +The constraints on the variables are that they must sum to 100 and that the nutritional requirements are met: + +.. math:: + + 1.000 x_1 + 1.000 x_2 &= 100.0\\ + 0.100 x_1 + 0.200 x_2 &\ge 8.0\\ + 0.080 x_1 + 0.100 x_2 &\ge 6.0\\ + 0.001 x_1 + 0.005 x_2 &\le 2.0\\ + 0.002 x_1 + 0.005 x_2 &\le 0.4\\ + +Solution to Simplified Problem +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To obtain the solution to this Linear Program, we can write a short +program in Python to call PuLP's modelling functions, which will then +call a solver. This will explain step-by-step how to write this Python +program. It is suggested that you repeat the exercise yourself. The code +for this example is found in `WhiskasModel1.py `_ + +The start of the your file should then be headed with a short commenting section outlining the purpose of the program. For example: + +.. literalinclude:: ../../../examples/WhiskasModel1.py + :lines: 1-5 + +Then you will import PuLP's functions for use in your code: + +.. literalinclude:: ../../../examples/WhiskasModel1.py + :lines: 7-8 + +A variable called ``prob`` (although its name is not important) is +created using the :class:`~pulp.LpProblem` function. It has two parameters, the first +being the arbitrary name of this problem (as a string), and the second +parameter being either ``LpMinimize`` or ``LpMaximize`` depending on the +type of LP you are trying to solve: + +.. literalinclude:: ../../../examples/WhiskasModel1.py + :lines: 10-11 + +The problem variables ``x1`` and ``x2`` are created using the +:class:`~pulp.LpVariable` class. It has four parameters, the first is the +arbitrary name of what this variable represents, the second is the lower +bound on this variable, the third is the upper bound, and the fourth +is essentially the type of data (discrete or continuous). The options +for the fourth parameter are ``LpContinuous`` or ``LpInteger``, with the +default as ``LpContinuous``. If we were modelling the number of cans +to produce, we would need to input ``LpInteger`` since it is discrete +data. The bounds can be entered directly as a number, or ``None`` to +represent no bound (i.e. positive or negative infinity), with ``None`` +as the default. If the first few parameters are entered and the rest +are ignored (as shown), they take their default values. However, if you +wish to specify the third parameter, but you want the second to be the +default value, you will need to specifically set the second parameter as +it's default value. i.e you cannot leave a parameter entry blank. +e.g:: + + LpVariable("example", None, 100) + +or:: + + LpVariable("example", upBound = 100) + +To explicitly create the two variables needed for this problem: + +.. literalinclude:: ../../../examples/WhiskasModel1.py + :lines: 13-15 + +The variable ``prob`` now begins collecting problem data with the +``+=`` operator. The objective function is logically entered first, with +an important comma ``,`` at the end of the statement and a short string +explaining what this objective function is: + +.. literalinclude:: ../../../examples/WhiskasModel1.py + :lines: 17-18 + +The constraints are now entered (Note: any "non-negative" +constraints were already included when defining the variables). This is +done with the '+=' operator again, since we are adding more data to the +``prob`` variable. The constraint is logically entered after this, with a +comma at the end of the constraint equation and a brief description of +the cause of that constraint: + +.. literalinclude:: ../../../examples/WhiskasModel1.py + :lines: 20-25 + +Now that all the problem data is entered, the :meth:`~pulp.LpProblem.writeLP` function +can be used to copy this information into a .lp file into the directory +that your code-block is running from. Once your code runs successfully, you +can open this .lp file with a text editor to see what the above steps were +doing. You will notice that there is no assignment operator (such as an +equals sign) on this line. This is because the function/method called +:meth:`~pulp.LpProblem.writeLP` is being performed to the +variable/object ``prob`` (and the +string ``"WhiskasModel.lp"`` is an additional parameter). The dot ``.`` +between the variable/object and the function/method is important and is +seen frequently in Object Oriented software (such as this): + + +.. literalinclude:: ../../../examples/WhiskasModel1.py + :lines: 27-28 + +The LP is solved using the solver that PuLP chooses. The input +brackets after :meth:`~pulp.LpProblem.solve` are left empty in this case, however they can be +used to specify which solver to use (e.g ``prob.solve(CPLEX())`` ): + +.. literalinclude:: ../../../examples/WhiskasModel1.py + :lines: 30-31 + +Now the results of the solver call can be displayed as output to +us. Firstly, we request the status of the solution, which can be one of +"Not Solved", "Infeasible", "Unbounded", "Undefined" or "Optimal". The +value of ``prob`` (:attr:`pulp.pulp.LpProblem.status`) is returned as an integer, which must be converted +to its significant text meaning using the +:attr:`~pulp.constants.LpStatus` dictionary. Since +:attr:`~pulp.constants.LpStatus` is a dictionary(:obj:`dict`), its input must be in square brackets: + +.. literalinclude:: ../../../examples/WhiskasModel1.py + :lines: 33-34 + +The variables and their resolved optimum values can now be printed +to the screen. + +.. literalinclude:: ../../../examples/WhiskasModel1.py + :lines: 36-38 + +The ``for`` loop makes ``variable`` cycle through all +the problem variable names (in this case just ``ChickenPercent`` and +``BeefPercent``). Then it prints each variable name, followed by an +equals sign, followed by its optimum value. +:attr:`~pulp.LpVariable.name` and +:attr:`~pulp.LpVariable.varValue` are +properties of the object ``variable``. + + +The optimised objective function value is printed to the screen, +using the value function. This ensures that the number is in the right +format to be displayed. :attr:`~pulp.LpProblem.objective` is an attribute of the object +``prob``: + +.. literalinclude:: ../../../examples/WhiskasModel1.py + :lines: 40-41 + +Running this file should then produce the output to show that +Chicken will make up 33.33%, Beef will make up 66.67% and the +Total cost of ingredients per can is 96 cents. + +Full Formulation +---------------- + +Now we will formulate the problem fully with +all the variables. Whilst it could be implemented into Python with +little addition to our method above, we will look at a better way which +does not mix the problem data, and the formulation as much. This will +make it easier to change any problem data for other tests. We will start +the same way by algebraically defining the problem: + +#. Identify the Decision Variables + For the Whiskas Cat Food Problem the decision variables are the percentages of + the different ingredients we include in the can. + Since the can is 100g, these percentages also represent the amount in g of each + ingredient included. + We must formally define our decision variables, being sure to state the units + we are using. + + .. math:: + + x_1 &= \text{percentage of chicken meat in a can of cat food}\\ + x_2 &= \text{percentage of beef used in a can of cat food}\\ + x_3 &= \text{percentage of mutton used in a can of cat food}\\ + x_4 &= \text{percentage of rice used in a can of cat food}\\ + x_5 &= \text{percentage of wheat bran used in a can of cat food}\\ + x_6 &= \text{percentage of gel used in a can of cat food} + + Note that these percentages must be between 0 and 100. +#. Formulate the Objective Function + For the Whiskas Cat Food Problem the objective is to minimise the total cost + of ingredients per can of cat food. + We know the cost per g of each ingredient. We decide the percentage of each + ingredient in the can, so we must divide by 100 and multiply by the weight of + the can in g. This will give us the weight in g of each + ingredient: + + .. math:: + + \min \$0.013 x_1 + \$0.008 x_2 + \$0.010 x_3 + \$0.002 x_4 + \$0.005 x_5 + \$0.001 x_6 + +#. Formulate the Constraints + The constraints for the Whiskas Cat Food Problem are that: + + * The sum of the percentages must make up the whole can (= 100%). + * The stated nutritional analysis requirements are met. + + The constraint for the "whole can" is: + + .. math:: x_1 + x_2 + x_3 + x_4 + x_5 +x _6 = 100 + + To meet the nutritional analysis requirements, we need to have at + least 8g of Protein per 100g, 6g of fat, but no more than 2g of fibre + and 0.4g of salt. To formulate these constraints we make use of the + previous table of contributions from each ingredient. This allows us + to formulate the following constraints on the total contributions of + protein, fat, fibre and salt from the ingredients: + + .. math:: + + 0.100 x_1 +0.200 x_2 +0.150 x_3 +0.000 x_4 +0.040 x_5 +0.0 x_6 0&\ge 8.0 \\ + 0.080 x_1 +0.100 x_2 +0.110 x_3 +0.010 x_4 +0.010 x_5 0+0.0 x_6 &\ge 6.0 \\ + 0.001 x_1 +0.005 x_2 +0.003 x_3 +0.100 x_4 0+0.150 x_5 +0.0 x_6 &\le 2.0 \\ + 0.002 x_1 +0.005 x_2 +0.007 x_3 0+0.002 x_4 +0.008 x_5 +0.0 x_6 &\le 0.4 + +Solution to Full Problem +~~~~~~~~~~~~~~~~~~~~~~~~ + +To obtain the solution to this Linear Program, we again write a +short program in Python to call PuLP's modelling functions, which will +then call a solver. This will explain step-by-step how to write this +Python program with it's improvement to the above model. It is suggested +that you repeat the exercise yourself. The code for this example is +found in the `WhiskasModel2.py `_ + +As with last time, it is advisable to head your file with commenting on its +purpose, and the author name and date. Importing of the PuLP functions is also done in the same way: + +.. literalinclude:: ../../../examples/WhiskasModel2.py + :lines: 1-8 + +Next, before the ``prob`` variable or type of problem are defined, +the key problem data is entered into dictionaries. This includes the +list of Ingredients, followed by the cost of each Ingredient, and it's +percentage of each of the four nutrients. These values are clearly laid +out and could easily be changed by someone with little knowledge of +programming. The ingredients are the reference keys, with the numbers as +the data. + +.. literalinclude:: ../../../examples/WhiskasModel2.py + :lines: 10-51 + +The ``prob`` variable is created to contain the formulation, and the +usual parameters are passed into :obj:`~pulp.LpProblem`. + +.. literalinclude:: ../../../examples/WhiskasModel2.py + :lines: 53-54 + +A dictionary called ``ingredient_vars`` is created which contains +the LP variables, with their defined lower bound of zero. The reference +keys to the dictionary are the Ingredient names, and the data is +``Ingr_IngredientName``. (e.g. MUTTON: Ingr_MUTTON) + +.. literalinclude:: ../../../examples/WhiskasModel2.py + :lines: 56-57 + +Since ``costs`` and ``ingredient_vars`` are now dictionaries with the +reference keys as the Ingredient names, the data can be simply extracted +with a list comprehension as shown. The :func:`~pulp.lpSum` function will add the +elements of the resulting list. Thus the objective function is simply +entered and assigned a name: + +.. literalinclude:: ../../../examples/WhiskasModel2.py + :lines: 59-60 + +Further list comprehensions are used to define the other 5 constraints, which are also each given names describing them. + +.. literalinclude:: ../../../examples/WhiskasModel2.py + :lines: 62-67 + +Following this, the :ref:`writeLP` line etc follow exactly the same as +in the simplified example. + +The optimal solution is 60% Beef and 40% Gel leading to a objective +Function value of 52 cents per can. + diff --git a/doc/source/CaseStudies/a_set_partitioning_problem.rst b/doc/source/CaseStudies/a_set_partitioning_problem.rst new file mode 100644 index 0000000..30343e6 --- /dev/null +++ b/doc/source/CaseStudies/a_set_partitioning_problem.rst @@ -0,0 +1,62 @@ +A Set Partitioning Problem +============================ + +A set partitioning problem determines how the items in one set (S) can be partitioned into smaller +subsets. All items in S must be contained in one and only one partition. Related problems are: + ++ set packing - all items must be contained in zero or one partitions; ++ set covering - all items must be contained in at least one partition. + +In this case study a wedding planner must determine guest seating allocations +for a wedding. To model this problem the tables are modelled as the partitions +and the guests invited to the wedding are modelled as the elements of S. The +wedding planner wishes to maximise the total happiness of all of the tables. + +.. image:: images/wedding_seating.jpg + +.. raw:: html + + + +A set partitioning problem may be modelled by explicitly enumerating each +possible subset. Though this approach does become intractable for large numbers +of items (without using column generation) it does have the advantage that the +objective function co-efficients for the partitions can be non-linear +expressions (like happiness) and still allow this problem to be solved +using Linear Programming. + +First we use :func:`~pulp.allcombinations` to generate a list of all +possible table seatings. + +.. literalinclude:: ../../../examples/wedding.py + :lines: 20-22 + +Then we create a binary variable that will be 1 if the table will be in the solution, or zero otherwise. + +.. literalinclude:: ../../../examples/wedding.py + :lines: 24-28 + +We create the :class:`~pulp.LpProblem` and then make the objective function. Note that +happiness function used in this script would be difficult to model in any other way. + +.. literalinclude:: ../../../examples/wedding.py + :lines: 30-32 + +We specify the total number of tables allowed in the solution. + +.. literalinclude:: ../../../examples/wedding.py + :lines: 34-35 + +This set of constraints defines the set partitioning problem by guaranteeing that a guest is allocated to +exactly one table. + +.. literalinclude:: ../../../examples/wedding.py + :lines: 38-41 + +The full file can be found here `wedding.py `_ + +.. literalinclude:: ../../../examples/wedding.py + + + + diff --git a/doc/source/CaseStudies/a_sudoku_problem.rst b/doc/source/CaseStudies/a_sudoku_problem.rst new file mode 100644 index 0000000..15dad31 --- /dev/null +++ b/doc/source/CaseStudies/a_sudoku_problem.rst @@ -0,0 +1,161 @@ +A Sudoku Problem formulated as an LP +==================================== + + +Problem Description +------------------- + +A `sudoku problem `_ is a problem where there are is an incomplete 9x9 table of numbers which must be filled according +to several rules: + ++ Within any of the 9 individual 3x3 boxes, each of the numbers 1 to 9 must be + found ++ Within any column of the 9x9 grid, each of the numbers 1 to 9 must be found ++ Within any row of the 9x9 grid, each of the numbers 1 to 9 must be found + +On this page we will formulate the below problem from wikipedia to model using PuLP. Once created, our code will need little modification to solve any sudoku problem at all. + +.. image:: images/wikisudokuproblem.jpg + +Formulation +----------- + +Identify the Decision Variables +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In order to formulate this problem as a linear program, we cannot simply create +a variable for each of the 81 squares between 1 and 9 representing the value in +that square. This is because in linear programming there is no "not equal to" +operator and so we cannot use the necessary constraints of no squares within a +box/row/column being equal in value to each other. Whilst we can ensure the sum +of all the values in a box/row/column equal 45, this will still result in many +solutions satisfying the 45 constraint but still with 2 of the same number in +the same box/row/column. + +Instead, we must create 729 individual binary (0-1) problem variables. These +represent 9 problem variables per square for each of 81 squares, where the 9 +variables each correspond to the number that might be in that square. The +binary nature of the variable says whether the existence of that number in that +square is true or false. Therefore, there can clearly be only 1 of the 9 +variables for each square as true (1) and the other 8 must be false (0) since +only one number can be placed into any square. This will become more clear. + + +Formulate the Objective Function +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Interestingly, with sudoku there is no solution that is better than another +solution, since a solution by definition, must satisfy all the constraints. +Therefore, we are not really trying to minimise or maximise anything, we are +just trying to find the values on our variables that satisfy the constraints. +Therefore, whilst either :attr:`~pulp.LpMinimize` or :attr:`~pulp.LpMaximize` must be entered, it is not +important which. Similarly, the objective function can be anything, so in this +example it is simply zero. +That is we are trying to minimize zero, subject to our constraints (meeting the constraints being the important part) + +Formulate the Constraints +~~~~~~~~~~~~~~~~~~~~~~~~~ + +These are simply the known constraints of a sudoku problem plus the constraints +on our own created variables we have used to express the features of the +problem: + ++ The values in the squares in any row must be each of 1 to 9 ++ The values in the squares in any column must be each of 1 to 9 ++ The values in the squares in any box must be each of 1 to 9 + (a box is one of the 9 non-overlapping 3x3 grids within the overall 9x9 grid) ++ There must be only one number within any square (seems logically obvious, but + it is important to our formulation to ensure because of our variable choices) ++ The starting sudoku numbers must be in those same places in the final + solution (this is a constraint since these numbers are not changeable in the + actual problem, whereas we can control any other numbers. If none or very few + starting numbers were present, the sudoku problem would have a very large + number of feasible solutions, instead of just one) + + +Solution +-------- + +The introductory commenting and import statement are entered + +.. literalinclude:: ../../../examples/Sudoku1.py + :lines: 1-8 + +In the unique case of the sudoku problem, the row names, column names and variable option values are all the exact same list of numbers (as strings) from "1" to "9". + +.. literalinclude:: ../../../examples/Sudoku1.py + :lines: 10-16 + + +A list called `Boxes` is created with 9 elements, each being another list. These 9 lists correspond to each of the 9 boxes, and each of the lists contains tuples as the elements with the row and column indices for each square in that box. Explicitly entering the values in a similar way to the following would have had the same effect (but would have been a waste of time): + +.. literalinclude:: ../../../examples/Sudoku1.py + :lines: 18-22 + +Therefore, Boxes[0] will return a list of tuples containing the locations of each of the 9 squares in the first box. + +The prob variable is created to contain the problem data. LpMinimize has the same effect as LpMaximise in this case. + +.. literalinclude:: ../../../examples/Sudoku1.py + :lines: 24-25 + + +The 729 problem variables are created since the `(Vals,Rows,Cols)` creates a variable for each combination of value, row and column. An example variable would be: Choice_4_2_9, and it is defined to be a binary variable (able to take only the integers 1 or 0. If Choice_4_2_9 was 1, it would mean the number 4 was present in the square situated in row 2, column 9. (If it was 0, it would mean there was not a 4 there) + +.. literalinclude:: ../../../examples/Sudoku1.py + :lines: 27-28 + +As explained above, the objective function (what we try to change using the problem variables) is simply zero (constant) since we are only concerned with any variable combination that can satisfy the constraints. + +.. literalinclude:: ../../../examples/Sudoku1.py + :lines: 30-31 + +Since there are 9 variables for each square, it is important to specify that only exactly one of them can take the value of "1" (and the rest are "0"). Therefore, the below code reads: for each of the 81 squares, the sum of all the 9 variables (each representing a value that could be there) relating to that particular square must equal 1. + +.. literalinclude:: ../../../examples/Sudoku1.py + :lines: 33-36 + +These constraints ensure that each number (value) can only occur once in each row, column and box. + +.. literalinclude:: ../../../examples/Sudoku1.py + :lines: 38-47 + +The starting numbers are entered as constraints i.e a "5" in row "1" column "1" is true. + +.. literalinclude:: ../../../examples/Sudoku1.py + :lines: 49-78 + +The problem is written to an LP file, solved using CPLEX (due to CPLEX's simple output) and the solution status is printed to the screen + +.. literalinclude:: ../../../examples/Sudoku1.py + :lines: 80-87 + + +Instead of printing out all 729 of the binary problem variables and their respective values, it is most meaningful to draw the solution in a text file. The code also puts lines inbetween every third row and column to make the solution easier to read. The sudokuout.txt file is created in the same folder as the .py file. + +.. literalinclude:: ../../../examples/Sudoku1.py + :lines: 92-108 + +A note of the location of the solution is printed to the solution + +.. literalinclude:: ../../../examples/Sudoku1.py + :lines: 110-111 + + +The full file above is given provided `Sudoku1.py `_ + +The final solution should be the following: + +.. image:: images/wikisudokusolution.jpg + +Extra for Experts +----------------- + +In the above formulation we did not consider the fact that there may be multiple solutions if the sudoku problem is not well defined. + +We can make our code return all the solutions by editing our code as shown after the `prob.writeLP` line. Essentially we are just looping over the solve statement, and each time after a successful solve, adding a constraint that the same solution cannot be used again. When there are no more solutions, our program ends. + +.. literalinclude:: ../../../examples/Sudoku2.py + :lines: 82-115 + +The full file using this is available `Sudoku2.py `_. When using this code for sudoku problems with a large number of solutions, it could take a very long time to solve them all. To create sudoku problems with multiple solutions from unique solution sudoku problem, you can simply delete a starting number constraint. You may find that deleting several constraints will still lead to a single optimal solution but the removal of one particular constraint leads to a sudden dramatic increase in the number of solutions. diff --git a/doc/source/CaseStudies/a_transportation_problem.rst b/doc/source/CaseStudies/a_transportation_problem.rst new file mode 100644 index 0000000..b6c9a84 --- /dev/null +++ b/doc/source/CaseStudies/a_transportation_problem.rst @@ -0,0 +1,422 @@ +A Transportation Problem +======================== + +Problem Description +------------------- +A boutique brewery has two warehouses from which it distributes beer to five +carefully chosen bars. At the start of every week, each bar sends an order to +the brewery’s head office for so many crates of beer, which is then dispatched +from the appropriate warehouse to the bar. The brewery would like to have an +interactive computer program which they can run week by week to tell them which +warehouse should supply which bar so as to minimize the costs of the whole +operation. For example, suppose that at the start of a given week the brewery +has 1000 cases at warehouse A, and 4000 cases at warehouse B, and that the bars +require 500, 900, 1800, 200, and 700 cases respectively. Which warehouse should +supply which bar? + +Formulation +----------- +For transportation problems, using a graphical representation of the problem +is often helpful during formulation. Here is a graphical representation of The +Beer Distribution Problem. + +.. image:: images/brewery_nodes.jpg + +Identify the Decision Variables +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +In transportation problems we are deciding how to transport goods from their +supply nodes to their demand nodes. The decision variables are the Arcs +connecting these nodes, as shown in the diagram below. We are deciding how many +crates of beer to transport from each warehouse to each pub. + +.. image:: images/brewery_arcs.jpg + +* A1 = number of crates of beer to ship from Warehouse A to Bar 1 +* A5 = number of crates of beer to ship from Warehouse A to Bar 5 +* B1 = number of crates of beer to ship from Warehouse B to Bar 1 +* B5 = number of crates of beer to ship from Warehouse B to Bar 5 + +Let: + +.. math:: + + W &= \{A,B\} \\ + B &= \{1, 2, 3, 4, 5 \} \\ + x_{(w,b)} &\ge 0 \ldots \forall w \in W, b \in B \\ + x_{(w,b)} & \in \mathbb{Z}^+ \ldots \forall w \in W, b \in B \\ + +The lower bound on the variables is Zero, and the values must all be Integers +(since the number of crates cannot be negative or fractional). There is no +upper bound. + +Formulate the Objective Function +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The objective function has been loosely defined as cost. The problem can only be +formulated as a linear program if the cost of transportation from warehouse to +pub is a linear function of the amounts of crates transported. Note that this is +sometimes not the case. This may be due to factors such as economies of scale or +fixed costs. For example, transporting 10 crates may not cost 10 times as much +as transporting one crate, since it may be the case that one truck can +accommodate 10 crates as easily as one. Usually in this situation there are +fixed costs in operating a truck which implies that the costs go up in jumps +(when an extra truck is required). In these situations, it is possible to model +such a cost by using zero-one integer variables: we will look at this later in +the course. + +We shall assume then that there is a fixed transportation cost per crate. (If +the capacity of a truck is small compared with the number of crates that must be +delivered then this is a valid assumption). Lets assume we talk with the +financial manager, and she gives us the following transportation costs (dollars +per crate): + + ======================= === === + From Warehouse to Bar A B + ======================= === === + 1 2 3 + 2 4 1 + 3 5 3 + 4 2 2 + 5 1 3 + ======================= === === + +Minimise the Transporting Costs = Cost per crate for RouteA1 * A1 (number of crates on RouteA1) + \+ ... + \+ Cost per crate for RouteB5 * B5 (number of crates on RouteB5) + +.. math:: + + \min \sum_{w \in W, b \in B} c_{(w,b)} x_{(w,b)} + +Formulate the Constraints +~~~~~~~~~~~~~~~~~~~~~~~~~ +The constraints come from considerations of supply and demand. The supply of +beer at warehouse A is 1000 cases. The total amount of beer shipped from +warehouse A cannot exceed this amount. Similarly, the amount of beer shipped +from warehouse B cannot exceed the supply of beer at warehouse B. The sum of +the values on all the arcs leading out of a warehouse, must be less than or +equal to the supply value at that warehouse: + +Such that: + +* A1 + A2 + A3 + A4 + A5 <= 1000 +* B1 + B2 + B3 + B4 + B5 <= 4000 + +.. math:: + + \sum_{ b \in B} x_{(w,b)} <= s_w \ldots \forall w \in W + + + +The demand for beer at bar 1 is 500 cases, so the amount of beer delivered there +must be at least 500 to avoid lost sales. Similarly, considering the amounts +delivered to the other bars must be at least equal to the demand at those bars. +Note, we are assuming there are no penalties for oversupplying bars (other than +the extra transportation cost we incur). We can _balance_ the transportation +problem to make sure that demand is met exactly - there will be more on this +later. For now, the sum of the values on all the arcs leading into a bar, must +be greater than or equal to the demand value at that bar: + +* A1 + B1 >= 500 +* A2 + B2 >= 900 +* A3 + B3 >= 1800 +* A4 + B4 >= 200 +* A5 + B5 >= 700 + +.. math:: + + \sum_{ w \in W} x_{(w,b)} >= d_b \ldots \forall b \in B + +Finally, we have already specified the amount of beer shipped must be +non-negative. + +PuLP Model +---------- + +Whilst the LP as defined above could be formulated into Python code in the same +way as the `A Blending Problem` (Whiskas), for Transportation Problems, there is +a more efficient way which we will use in this course. The example file for this +problem is found in the examples directory BeerDistributionProblem.py + +First, start your Python file with a heading and the import PuLP statement: + +.. code-block:: python + + """ + The Beer Distribution Problem for the PuLP Modeller + + Authors: Antony Phillips, Dr Stuart Mitchell 2007 + """ + + # Import PuLP modeller functions + from pulp import * + +The start of the formulation is a simple definition of the nodes and their limits/capacities. The node names are put into lists, and their associated capacities are put into dictionaries with the node names as the reference keys: + +.. code-block:: python + + # Creates a list of all the supply nodes + Warehouses = ["A","B"] + + # Creates a dictionary for the number of units of supply for each supply node + supply = {"A": 1000, + "B": 4000} + + # Creates a list of all demand nodes + Bars = ["1", "2", "3", "4", "5"] + + # Creates a dictionary for the number of units of demand for each demand node + demand = {"1": 500, + "2": 900, + "3": 1800, + "4": 200, + "5": 700} + +The cost data is then inputted into a list, with two sub lists: the first +containing the costs of shipping from Warehouse A, and the second containing the +costs of shipping from Warehouse B. Note here that the commenting and structure +of the code makes the data appear as a table for easy editing. The `Warehouses` +and `Bars` lists (Supply and Demand nodes) are added to make a large list (of +all nodes) and inputted into PuLPs `makeDict` function. The second parameter is +the costs list as was previously created, and the last parameter sets the +default value for an arc cost. Once the cost dictionary is created, if +`costs["A"]["1"]` is called, it will return the cost of transporting from +warehouse A to bar 1, 2. If `costs["C"]["2"]` is called, it will return 0, since +this is the defined default. + +.. code-block:: python + + # Creates a list of costs of each transportation path + costs = [ #Bars + #1 2 3 4 5 + [2,4,5,2,1],#A Warehouses + [3,1,3,2,3] #B + ] + }}} + +The `prob` variable is created using the `LpProblem` function, with the usual +input parameters. + +.. code-block:: python + + # Creates the prob variable to contain the problem data + prob = LpProblem("Beer Distribution Problem",LpMinimize) + +A list of tuples is created containing all the arcs. + +.. code-block:: python + + # Creates a list of tuples containing all the possible routes for transport + Routes = [(w,b) for w in Warehouses for b in Bars] + +A dictionary called `route_var` is created which contains the LP variables. The +reference keys to the dictionary are the warehouse name, then the bar +name(`["A"]["2"]`) , and the data is `Route_Tuple`. (e.g. `["A"]["2"]`: +Route_A_2). The lower limit of zero is set, the upper limit of `None` is set, +and the variables are defined to be Integers. + +.. code-block:: python + + # A dictionary called route_vars is created to contain the referenced variables (the routes) + route_vars = LpVariable.dicts("Route",(Warehouses,Bars),0,None,LpInteger) + +The objective function is added to the variable `prob` using a list +comprehension. Since `route_vars` and `costs` are now dictionaries (with further +internal dictionaries), they can be used as if they were tables, as `for (w,b) +in Routes` will cycle through all the combinations/arcs. Note that `i` and `j` +could have been used, but `w` and `b` are more meaningful. + +.. code-block:: python + + # The objective function is added to prob first + prob += lpSum([route_vars[w][b]*costs[w][b] for (w,b) in Routes]), "Sum of Transporting Costs" + +The supply and demand constraints are added using a normal `for` loop and a list +comprehension. Supply Constraints: For each warehouse in turn, the values of the +decision variables (number of beer cases on arc) to each of the bars is summed, +and then constrained to being less than or equal to the supply max for that +warehouse. Demand Constraints: For each bar in turn, the values of the decision +variables (number on arc) from each of the warehouses is summed, and then +constrained to being greater than or equal to the demand minimum. + +.. code-block:: python + + # The supply maximum constraints are added to prob for each supply node (warehouse) + for w in Warehouses: + prob += lpSum([route_vars[w][b] for b in Bars]) <= supply[w], "Sum of Products out of Warehouse %s"%w + + # The demand minimum constraints are added to prob for each demand node (bar) + for b in Bars: + prob += lpSum([route_vars[w][b] for w in Warehouses]) >= demand[b], "Sum of Products into Bars %s"%b + +Following this is the `prob.writeLP` line, and the rest as explained in previous +examples. + +You will notice that the linear programme solution was also an integer solution. +As long as Supply and Demand are integers, the linear programming solution will +always be an integer. Read about naturally integer solutions for more details. + +Extensions +---------- + +Validation +~~~~~~~~~~ + +Since we have guaranteed the Supply and Demand are integer, we know that the +solution to the linear programme will be integer, so we don't need to check the +integrality of the solution. + +Storage and "Buying In" +~~~~~~~~~~~~~~~~~~~~~~~ + +Transportation models are usually _balanced_, i.e., the total supply = the total +demand. This is because extra supply usually must be stored somewhere (with an +associated storage cost) and extra demand is usually satisfied by purchasing +extra goods from alternative sources (this is know as "buying in" extra goods) +or by substituting another product (incurring a penalty cost). + +In The Beer Distribution Problem, the total supply is 5000 cases of beer, but +the total demand is only for 4100 cases. The extra supply can be sent to an +_dummy_ demand node. The cost of flow going to the dummy demand node is then the +storage cost at each of the supply nodes. + +.. image:: images/extra_supply.jpg + +This is added into the above model very simply. Since the objective function and +constraints all operated on the original supply, demand and cost +lists/dictionaries, the only changes that must be made to include another demand node are: + +.. code-block:: python + + # Creates a list of all demand nodes + Bars = ["1","2","3","4","5","D"] + + # Creates a dictionary for the number of units of demand for each demand node + demand = {"1": 500, + "2": 900, + "3": 1800, + "4": 200, + "5": 700, + "D": 900} + + # Creates a list of costs of each transportation path + costs = [ #Bars + #1 2 3 4 5 D + [2,4,5,2,1,0],#A Warehouses + [3,1,3,2,3,0] #B + ] + +The `Bars` list is expanded and the `Demand` dictionary is expanded to make the +Dummy Demand require 900 crates, to balance the problem. The cost list is also +expanded, to show the cost of "sending to the Dummy Node" which is realistically +just leaving the stock at the warehouses. This may have an associated cost which +could be entered here instead of the zeros. Note that the solution could still +be solved when there was an unbalanced excess supply. + +If a transportation problem has more demand than supply, we can balance the +problem using a dummy supply node. Note that with excess demand, the problem is +"Infeasible" when unbalanced. + +Assume there has been a production problem and only 4000 cases of beer could be +produced. Since the total demand is 4100, we need to get extra cases of beer from the dummy supply node. + + +.. image:: images/extra_demand.jpg + + +This dummy supply node is added in simply and logically into the `Warehouse` +list, `Supply` dictionary, and `costs` list. The Supply value is chosen to +balance the problem, and cost of transport is zero to all demand nodes. + +.. code-block:: python + + # Creates a list of all the supply nodes + Warehouses = ["A","B","D"] + + # Creates a dictionary for the number of units of supply for each supply node + supply = {"A": 1000, + "B": 3000, + "D": 100} + + # Creates a list of all demand nodes + Bars = ["1","2","3","4","5"] + + # Creates a dictionary for the number of units of demand for each demand node + demand = {"1": 500, + "2": 900, + "3": 1800, + "4": 200, + "5": 700} + + # Creates a list of costs of each transportation path + costs = [ #Bars + #1 2 3 4 5 D + [2,4,5,2,1], #A + [3,1,3,2,3], #B Warehouses + [0,0,0,0,0] #D + ] + + +Presentation of Solution and Analysis +------------------------------------- + +There are many ways to present the solution to The Beer Distribution Problem: +as a list of shipments, in a table, etc. + + +:: + + TRANSPORTATION SOLUTION -- Non-zero shipments + TotalCost = ____ + + Ship ___ crates of beer from warehouse A to pub 1 + Ship ___ crates of beer from warehouse A to pub 5 + Ship ___ crates of beer from warehouse B to pub 1 + Ship ___ crates of beer from warehouse B to pub 2 + Ship ___ crates of beer from warehouse B to pub 3 + Ship ___ crates of beer from warehouse B to pub 4 + +This information gives rise to the following management summary: + +:: + + The Beer Distribution Problem + Mike O'Sullivan, 1234567 + We are minimising the transportation cost for a brewery operation. The brewery + transports cases of beer from its warehouses to several bars. + + The brewery has two warehouses (A and B respectively) and 5 bars (1, 2, 3, 4 and + 5). + + The supply of crates of beer at the warehouses is: + + __________ + + The forecasted demand (in crates of beer) at the bars is: + + __________ + + The cost of transporting 1 crate of beer from a warehouse to a bar is given in + the following table: + + __________ + + To minimise the transportation cost the brewery should make the following + shipments: + + Ship ___ crates of beer from warehouse A to pub 1 + Ship ___ crates of beer from warehouse A to pub 5 + Ship ___ crates of beer from warehouse B to pub 1 + Ship ___ crates of beer from warehouse B to pub 2 + Ship ___ crates of beer from warehouse B to pub 3 + Ship ___ crates of beer from warehouse B to pub 4 + + The total transportation cost of this shipping schedule is $_____. + +Ongoing Monitoring +------------------ + +Ongoing Monitoring may take the form of: + +* Updating your data files and resolving as the data changes (changing costs, supplies, demands); +* Resolving our model for new nodes (e.g., new warehouses or bars); +* Looking to see if cheaper transportation options are available along routes where the transportation cost affects the optimal solution (e.g., how much total savings can we get by reducing the transportation cost from Warehouse B to Bar 1). diff --git a/doc/source/CaseStudies/images/brewery_arcs.jpg b/doc/source/CaseStudies/images/brewery_arcs.jpg new file mode 100644 index 0000000..dd923ad Binary files /dev/null and b/doc/source/CaseStudies/images/brewery_arcs.jpg differ diff --git a/doc/source/CaseStudies/images/brewery_nodes.jpg b/doc/source/CaseStudies/images/brewery_nodes.jpg new file mode 100644 index 0000000..86e6039 Binary files /dev/null and b/doc/source/CaseStudies/images/brewery_nodes.jpg differ diff --git a/doc/source/CaseStudies/images/extra_demand.jpg b/doc/source/CaseStudies/images/extra_demand.jpg new file mode 100644 index 0000000..a52879e Binary files /dev/null and b/doc/source/CaseStudies/images/extra_demand.jpg differ diff --git a/doc/source/CaseStudies/images/extra_supply.jpg b/doc/source/CaseStudies/images/extra_supply.jpg new file mode 100644 index 0000000..d508dc7 Binary files /dev/null and b/doc/source/CaseStudies/images/extra_supply.jpg differ diff --git a/doc/source/CaseStudies/images/wedding_seating.jpg b/doc/source/CaseStudies/images/wedding_seating.jpg new file mode 100644 index 0000000..772592a Binary files /dev/null and b/doc/source/CaseStudies/images/wedding_seating.jpg differ diff --git a/doc/source/CaseStudies/images/whiskas_blend.jpg b/doc/source/CaseStudies/images/whiskas_blend.jpg new file mode 100644 index 0000000..11cb5e4 Binary files /dev/null and b/doc/source/CaseStudies/images/whiskas_blend.jpg differ diff --git a/doc/source/CaseStudies/images/whiskas_label.jpg b/doc/source/CaseStudies/images/whiskas_label.jpg new file mode 100644 index 0000000..8584271 Binary files /dev/null and b/doc/source/CaseStudies/images/whiskas_label.jpg differ diff --git a/doc/source/CaseStudies/images/wikisudokuproblem.jpg b/doc/source/CaseStudies/images/wikisudokuproblem.jpg new file mode 100644 index 0000000..547aeb3 Binary files /dev/null and b/doc/source/CaseStudies/images/wikisudokuproblem.jpg differ diff --git a/doc/source/CaseStudies/images/wikisudokusolution.jpg b/doc/source/CaseStudies/images/wikisudokusolution.jpg new file mode 100644 index 0000000..ada3569 Binary files /dev/null and b/doc/source/CaseStudies/images/wikisudokusolution.jpg differ diff --git a/doc/source/CaseStudies/index.rst b/doc/source/CaseStudies/index.rst new file mode 100644 index 0000000..1a05eaa --- /dev/null +++ b/doc/source/CaseStudies/index.rst @@ -0,0 +1,18 @@ +Case Studies +===================== + + + +.. toctree:: + :maxdepth: 2 + + a_blending_problem + a_set_partitioning_problem + a_sudoku_problem + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/doc/source/_static/freebound.jpg b/doc/source/_static/freebound.jpg new file mode 100644 index 0000000..397ff15 Binary files /dev/null and b/doc/source/_static/freebound.jpg differ diff --git a/doc/source/_static/freebound.pdf b/doc/source/_static/freebound.pdf new file mode 100644 index 0000000..cf979ba Binary files /dev/null and b/doc/source/_static/freebound.pdf differ diff --git a/doc/source/_static/plotter.py b/doc/source/_static/plotter.py new file mode 100644 index 0000000..782d7d7 --- /dev/null +++ b/doc/source/_static/plotter.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- + +from matplotlib import rc +rc('text', usetex=True) +rc('font', family='serif') + + + +def plot_interval(a,c,x_left, x_right,i, fbound): + lh = c*(1-a[0]) + rh = c*(1+a[1]) + x=arange(x_left, x_right+1) + y=0*x + arrow_r = Arrow(c,0, c*a[1],0,0.2) + arrow_l = Arrow(c,0,-c*a[0],0,0.2) + plot(x,y) + text((x_left+lh)/2.0,0.1,'freebound interval [%s, %s] is penalty-free' % (lh,rh)) + text((x_left+lh)/2.0, 0.2, 'rhs=%s, %s' % (c, fbound)) + cur_ax = gca() + cur_ax.add_patch(arrow_l) + cur_ax.add_patch(arrow_r) + axis([x_left,x_right,-0.1,0.3]) + yticks([]) + title('Elasticized constraint\_%s $C(x)= %s $' % (i, c)) + +figure() +subplots_adjust(hspace=0.5) + +fbound = 'proportionFreeBound' +i=1 +subplot(2,1,i) +a=[0.01,0.01] +c = 200 +x_left = 0.97*c +x_right = 1.03*c +fb_string = '%s%s = %s' %(fbound,'', a[0]) +plot_interval(a,c,x_left, x_right,i, fb_string) + +i += 1 +subplot(2,1,i) +a=[0.02, 0.05] +c = 500 +x_left = 0.9*c #scale of window +x_right = 1.2*c #scale of window +fb_string = '%s%s = [%s,%s]' % (fbound,'List', a[0],a[1]) +plot_interval(a,c,x_left, x_right,i, fb_string) +savefig('freebound.jpg') +savefig('freebound.pdf') + +# vim: fenc=utf-8: ft=python:sw=4:et:nu:fdm=indent:fdn=1:syn=python + diff --git a/doc/source/_templates/layout.html b/doc/source/_templates/layout.html new file mode 100644 index 0000000..d3085f2 --- /dev/null +++ b/doc/source/_templates/layout.html @@ -0,0 +1,9 @@ +{% extends '!layout.html' %} + +{% block footer %} +{{ super() }} +
+Creative Commons License
PuLP documentation by Pulp documentation team is licensed under a Creative Commons Attribution-Share Alike 3.0 New Zealand License. +
+{% endblock %} + diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 0000000..dd356ad --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,202 @@ +# -*- coding: utf-8 -*- +# +# pulp sphinx documentation build configuration file, created by +# sphinx-quickstart on Mon Nov 2 11:54:01 2009. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.insert(0,os.path.abspath('../src/pulp/')) + +# -- General configuration ----------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.pngmath', 'sphinx.ext.ifconfig', 'sphinx.ext.autosummary'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +pngmath_use_preview = True +todo_include_todos = True + +autoclass_content = 'both' +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'PuLP' +copyright = u'2009-, pulp documentation team.' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The full version, including alpha/beta/rc tags. +release = open(os.path.join('..','..','VERSION')).read().strip() +# The short X.Y version. +version = release[:3] + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of documents that shouldn't be included in the build. +#unused_docs = [] + +# List of directories, relative to source directory, that shouldn't be searched +# for source files. +exclude_trees = [] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. Major themes that come with +# Sphinx are currently 'default' and 'sphinxdoc'. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_use_modindex = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = '' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'pulpdoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'letter' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'pulp.tex', u'pulp Documentation', + u'pulp documentation team', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_use_modindex = True + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'http://docs.python.org/': None} diff --git a/doc/source/constants.rst b/doc/source/constants.rst new file mode 100644 index 0000000..ddfd448 --- /dev/null +++ b/doc/source/constants.rst @@ -0,0 +1,89 @@ + +:mod:`pulp.constants` +===================== + +.. automodule:: pulp.constants + :members: + :undoc-members: + :inherited-members: + :show-inheritance: + +.. data:: LpStatus + + Return status from solver: + + +-----------------------------+---------------+-----------------+ + | LpStatus key | string value | numerical value | + +=============================+===============+=================+ + | :data:`LpStatusOptimal` | "Optimal" | 1 | + +-----------------------------+---------------+-----------------+ + | :data:`LpStatusNotSolved` | "Not Solved" | 0 | + +-----------------------------+---------------+-----------------+ + | :data:`LpStatusInfeasible` | "Infeasible" | -1 | + +-----------------------------+---------------+-----------------+ + | :data:`LpStatusUnbounded` | "Unbounded" | -2 | + +-----------------------------+---------------+-----------------+ + | :data:`LpStatusUndefined` | "Undefined" | -3 | + +-----------------------------+---------------+-----------------+ + +.. data:: LpStatusOptimal + + LpStatusOptimal = 1 + +.. data:: LpStatusNotSolved + + LpStatusNotSolved = 0 + + .. data:: LpStatusInfeasible + + LpStatusInfeasible = -1 + + .. data:: LpStatusUnbounded + + LpStatusUnbounded = -2 + + .. data:: LpStatusUndefined + + LpStatusUndefined = -3 + + + .. data:: LpSenses + + Dictionary of values for :attr:`~pulp.pulp.LpProblem.sense`: + + LpSenses = + {:data:`LpMaximize`:"Maximize", :data:`LpMinimize`:"Minimize"} + + .. data:: LpMinimize + + LpMinimize = 1 + + .. data:: LpMaximize + + LpMaximize = -1 + + .. data:: LpConstraintEQ + + LpConstraintEQ = 0 + + .. data:: LpConstraintLE + + LpConstraintLE = -1 + + .. data:: LpConstraintGE + + LpConstraintGE = 1 + + .. data:: LpConstraintSenses + + +--------------------------+----------------+-----------------+ + | LpConstraint key | symbolic value | numerical value | + +==========================+================+=================+ + | :data:`LpConstraintEQ` | "==" | 0 | + +--------------------------+----------------+-----------------+ + | :data:`LpConstraintLE` | "<=" | -1 | + +--------------------------+----------------+-----------------+ + | :data:`LpConstraintGE` | ">=" | 1 | + +--------------------------+----------------+-----------------+ + + diff --git a/doc/source/documentation/elastic.rst b/doc/source/documentation/elastic.rst new file mode 100644 index 0000000..628f871 --- /dev/null +++ b/doc/source/documentation/elastic.rst @@ -0,0 +1,62 @@ +Elastic Constraints +^^^^^^^^^^^^^^^^^^^ +.. currentmodule:: pulp + +A constraint :math:`C(x) = c` (equality may be replaced by :math:`\le` +or :math:`\ge`) +can be elasticized to the form + +.. math:: C(x) \in D + +where :math:`D` denotes some interval containing the value +:math:`c`. + +Define the constraint in two steps: + + #. instantiate constraint (subclass of :class:`LpConstraint`) with target :math:`c`. + #. call its :meth:`~LpConstraint.makeElasticSubProblem` method which returns + an object of type :class:`FixedElasticSubProblem` + (subclass of :class:`LpProblem`) - its objective is the minimization + of the distance of :math:`C(x)` from :math:`D`. + +.. code-block:: python + + constraint = LpConstraint(..., rhs = c) + elasticProblem = constraint.makeElasticSubProblem( + penalty = , + proportionFreeBound = , + proportionFreeBoundList = , + ) + +where: + * ```` is a real number + * ```` :math:`a \in [0,1]` specifies a symmetric + target interval :math:`D = (c(1-a),c(1+a))` about :math:`c` + * `` = [a,b]``, a list of + proportions :math:`a, b \in [0,1]` specifying an asymmetric target + interval :math:`D = (c(1-a),c(1+b))` about :math:`c` + +The penalty applies to the constraint at points :math:`x` where +:math:`C(x) \not \in D`. +The magnitude of ```` can be assessed by examining +the final objective function in the ``.lp`` file written by +:meth:`LpProblem.writeLP`. + +Example: + +>>> constraint_1 = LpConstraint('ex_1',sense=1,rhs=200) +>>> elasticProblem_1 = constraint_1.makeElasticSubproblem(penalty=1, proportionFreeBound = 0.01) +>>> constraint_2 = LpConstraint('ex_2',sense=0,rhs=500) +>>> elasticProblem_2 = constraint_2.makeElasticSubproblem(penalty=1, +proportionFreeBoundList = [0.02, 0.05]) + +#. constraint_1 has a penalty-free target interval of 1% either side of the rhs value, 200 +#. constraint_2 has a penalty-free target interval of + - 2% on left and 5% on the right side of the rhs value, 500 + +.. image:: _static/freebound.* + :height: 5in + :alt: Freebound interval + +Following are the methods of the return-value: + diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 0000000..db0a68c --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,63 @@ +.. pulp_sphinx documentation master file, created by + sphinx-quickstart on Sun Nov 1 14:59:49 2009. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Optimization with PuLP +---------------------- + +You can begin learning Python and using PuLP +by looking at the content below. We recommend that you read The Optimisation Process, +Optimisation Concepts, and the Introduction to Python +before beginning the case-studies. For instructions for the installation of PuLP +see :ref:`installation`. + +The full PuLP function documentation is available, and useful functions +will be explained in the case studies. +The case studies are in order, so the later case studies will assume you have +(at least) read the earlier case studies. However, we will provide links to any +relevant information you will need. + +.. toctree:: + :maxdepth: 2 + + main/index + CaseStudies/index +.. comment block + main/Concepts + main/Introduction + main/Installing%20Python InstallingPythonatHome + main/BasicPythonCoding + main/Introduction to PuLP + main/InstallingPulpatHome + main/CaseStudies/ABLendingProblem + main/UsingElasticConstraints + main/Using FractionalConstraints + main/SequentialOptimisation + main/ExtraCaseStudies + +PuLP Internal Documentation +=========================== +.. toctree:: + :maxdepth: 1 + + constants + pulp + solvers + +Authors +======= + +The authors of this documentation (the pulp documentation team) include: + +.. include:: AUTHORS.txt + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/doc/source/main/amply.rst b/doc/source/main/amply.rst new file mode 100644 index 0000000..9eb6baa --- /dev/null +++ b/doc/source/main/amply.rst @@ -0,0 +1,531 @@ +Amply +====== + + +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 +---------------- + +.. testsetup:: + + >>> from pulp import Amply + +Import the class: :: + + >>> from pulp import Amply + +A simple set. Sets behave a lot like lists. + +.. doctest:: + + >>> 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: + +.. doctest:: + + >>> 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. + +.. doctest:: + + >>> 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): + +.. doctest:: + + >>> 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: + +.. doctest:: + + >>> 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. + +.. doctest:: + + >>> 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. + +.. doctest:: + + >>> data=Amply(""" + ... set ROUTES dimen 2; + ... set ROUTES : A B C D := + ... E + - - + + ... F + + - - + ... ; + ... """) + >>> print data.ROUTES + + +Matrices can also be transposed: + +.. doctest:: + + >>> 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: + +.. doctest:: + + >>> 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: + +.. doctest:: + + >>> 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. + +.. doctest:: + + >>> 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: + +.. doctest:: + + >>> 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: + +.. doctest:: + + >>> 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: + +.. doctest:: + + >>> 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: + +.. doctest:: + + >>> 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: + + +.. doctest:: + + >>> 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: + +.. doctest:: + + >>> 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: + +.. doctest:: + + >>> 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 + `_ +or there are a host of `Beginners Guides `_ +to Python on the Python Website. Follow the links to either: + ++ `BeginnersGuide/NonProgrammers `_ ++ `BeginnersGuide/Programmers `_ + +depending on your level of current programming knowledge. The code sections +below assume a knowledge of fundamental programming principles and mainly +focus on Syntax specific to programming in Python. + +Note: >>> represents a Python command line prompt. + + +Loops in Python +--------------- + +for Loop +~~~~~~~~ + +The general format is: + + +.. code-block:: python + + for variable in sequence: + #some commands + #other commands after for loop + + +Note that the formatting (indents and new lines) governs the end of the for +loop, whereas the start of the loop is the colon :. + +Observe the loop below, which is similar to loops you will be using in the +course. The variable i moves through the string list becoming each string in +turn. The top section is the code in a .py file and the bottom section shows +the output + +.. code-block:: python + + #The following code demonstrates a list with strings + ingredientslist = ["Rice","Water","Jelly"] + for i in ingredientslist: + print i + print "No longer in the loop" + +gives + +.. code-block:: python + + Rice + Water + Jelly + No longer in the loop + + +while Loop +~~~~~~~~~~ + +These are similar to for loops except they continue to loop until the specified +condition is no longer true. A while loop is not told to work through any +specific sequence. + + +.. code-block:: python + + i = 3 + while i <= 15: + # some commands + i = i + 1 # a command that will eventually end the loop is naturally + required + # other commands after while loop + + +For this specific simple while loop, it would have been better to do as a for +loop but it demonstrates the syntax. while loops are useful if the number of +iterations before the loop needs to end, is unknown. + +The if statement +~~~~~~~~~~~~~~~~ + +This is performed in much the same way as the loops above. The key identifier is +the colon : to start the statements and the end of indentation to end it. + + +.. code-block:: python + + if j in testlist: + # some commands + elif j == 5: + # some commands + else: + # some commands + + +Here it is shown that "elif" (else if) and "else" can also be used after an if +statement. "else" can in fact be used after both the loops in the same way. + +Array types in python +-------------------------- + +Lists +~~~~~ + +A list is simply a sequence of variables grouped together. The range function is +often used to create lists of integers, with the general format of +range(start,stop,step). The default for start is 0 and the default for step is +1. + + +>>> range(3,8) +[3,4,5,6,7] + + +This is a list/sequence. As well as integers, lists can also have strings in +them or a mix of integers, floats and strings. They can be created by a loop +(as shown in the next section) or by explicit creation (below). Note that the +print statement will display a string/variable/list/... to the user + + +>>> a = [5,8,"pt"] +>>> print a +[5,8,'pt'] +>>> print a[0] +5 + +Tuples +~~~~~~ + +Tuples are basically the same as lists, but with the important difference that +they cannot be modified once they have been created. They are assigned by: + + +>>> x = (4,1,8,"string",[1,0],("j",4,"o"),14) + + +Tuples can have any type of number, strings, lists, other tuples, functions and +objects, inside them. Also note that the first element in the tuple is numbered +as element "zero". Accessing this data is done by: + + +>>> x[0] +4 +>>> x[3] +"string" + + +Dictionaries +~~~~~~~~~~~~ + +A Dictionary is a list of reference keys each with associated data, whereby the +order does not affect the operation of the dictionary at all. With dictionaries, +the keys are not consecutive integers (unlike lists), and instead could be +integers, floats or strings. This will become clear: + + +>>> x = {} # creates a new empty dictionary - note the curly brackets denoting the creation of a dictionary +>>> x[4] = "programming" # the string "programming" is added to the dictionary x, with "4" as it's reference +>>> x["games"] = 12 +>>> print x["games"] +12 + + +In a dictionary, the reference keys and the stored values can be any type of +input. New dictionary elements are added as they are created (with a list, you +cannot access or write to a place in the list that exceeds the initially defined +list dimensions). + + +.. code-block:: python + + costs = {"CHICKEN": 1.3, "BEEF": 0.8, "MUTTON": 12} + print "Cost of Meats" + for i in costs: + print i + print costs[i] + costs["LAMB"] = 5 + print "Updated Costs of Meats" + for i in costs: + print i + print costs[i] + +gives + +.. code-block:: python + + Cost of Meats + CHICKEN + 1.3 + MUTTON + 12 + BEEF + 0.8 + Updated Costs of Meats + LAMB + 5 + CHICKEN + 1.3 + MUTTON + 12 + BEEF + 0.8 + + +In the above example, the dictionary is created using curly brackets and colons +to represent the assignment of data to the dictionary keys. The variable i is +assigned to each of the keys in turn (in the same way it would be for a list +with + +>>> for i in range(1,10) + +). Then the dictionary is called with this key, and +it returns the data stored under that key name. These types of for loops using +dictionaries will be highly relevant in using PuLP to model LPs in this course. + +List/Tuple/Dictionary Syntax Note +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Note that the creation of a: + +- list is done with square brackets []; +- tuple is done with round brackets and a comma (,); +- dictionary is done with parentheses{}. + +After creation however, when accessing elements in the list/tuple/dictionary, +the operation is always performed with square brackets (i.e a[3]?). If a was a +list or tuple, this would return the fourth element. If a was a dictionary it +would return the data stored with a reference key of 3. + +List Comprehensions +~~~~~~~~~~~~~~~~~~~ + +Python supports List Comprehensions which are a fast and concise way to create +lists without using multiple lines. They are easily understandable when simple, +and you will be using them in your code for this course. + + +>>> a = [i for i in range(5)] +>>> a +[0, 1, 2, 3, 4] + + +This statement above will create the list [0,1,2,3,4] and assign it to the +variable "a". + + +>>> odds = [i for i in range(25) if i%2==1] +>>> odds +[1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23] + +This statement above uses the if statement and the modulus operator(%) so that +only odd numbers are included in the list: [1,3,5,...,19,21,23]. (Note: The +modulus operator calculates the remainder from an integer division.) + + +>>> fifths = [i for i in range(25) if i%5==0] +>>> fifths +[0, 5, 10, 15, 20] + +This will create a list with every fifth value in it [0,5,10,15,20]. Existing +lists can also be used in the creation of new lists below: + +>>> a = [i for i in range(25) if (i in odds and i not in fifths)] + + +Note that this could also have been done in one step from scratch: + +>>> a = [i for i in range(25) if (i%2==1 and i%5==0)] + +For a challenge you can try creating + +a. a list of prime numbers up to 100, and +#. a list of all "perfect" numbers. + + +`More List Comprehensions Examples `_ + +`Wikipedia: Perfect Numbers `_. + +Other important language features +--------------------------------- + +Commenting in Python +~~~~~~~~~~~~~~~~~~~~ +Commenting at the top of a file is done using """ to start and to end the +comment section. Commenting done throughout the code is done using the hash +# symbol at the start of the line. + +Import Statement +~~~~~~~~~~~~~~~~ + +At the top of all your Python coding that you intend to use PuLP to model, you +will need the import statement. This statement makes the contents of another +module (file of program code) available in the module you are currently writing +i.e. functions and values defined in pulp.py that you will be required to call, +become usable. In this course you will use: + +>>> from pulp import * + +The asterisk represents that you are importing all names from the module of +pulp. Calling a function defined in pulp.py now can be done as if they were +defined in your own module. + +Functions +~~~~~~~~~ + +Functions in Python are defined by: (def is short for define) + +.. code-block:: python + + def name(inputparameter1, inputparameter2, . . .): + #function body + +For a real example, note that if inputs are assigned a value in the function +definition, that is the default value, and will be used only if no other value +is passed in. The order of the input parameters (in the definition) does not +matter at all, as long as when the function is called, the positional parameters +are entered in the corresponding order. If keywords are used the order of the +parameters does not matter at all: + +.. code-block:: python + + def string_appender(head='begin', tail='end', end_message='EOL'): + result = head + tail + end_message + return result + +>>> string_appender('newbegin', end_message = 'StringOver') +newbeginendStringOver + +In the above example, the output from the function call is printed. The default +value for head is 'begin', but instead the input of 'newbegin' was used. The +default value for tail of 'end' was used. And the input value of endmessage was +used. Note that end_message must be specified as a key word argument as no +value is given for tail + +Classes +~~~~~~~ + +To demonstrate how classes work in Python, look at the following class structure. + +The class name is Pattern, and it contains several class variables which are +relevant to any instance of the Pattern class (i.e a Pattern). The functions are + +__init__ + function which creates an instance of the Pattern + class and assigns the attributes of name and lengthsdict using self. + +__str__ + function defines what to return if the class instance is printed. + +trim + function acts like any normal function, except as with all class functions, + self must be in the input brackets. + +.. code-block:: python + + class Pattern: + """ + Information on a specific pattern in the SpongeRoll Problem + """ + cost = 1 + trimValue = 0.04 + totalRollLength = 20 + lenOpts = [5, 7, 9] + + def __init__(self,name,lengths = None): + self.name = name + self.lengthsdict = dict(zip(self.lenOpts,lengths)) + + def __str__(self): + return self.name + + def trim(self): + return Pattern.totalRollLength - sum([int(i)*self.lengthsdict[i] for i in self.lengthsdict]) + +This class could be used as follows: + +>>> Pattern.cost # The class attributes can be accessed without making an instance of the class +1 +>>> a = Pattern("PatternA",[1,0,1]) +>>> a.cost # a is now an instance of the Pattern class and is associated with Pattern class variables +1 +>>> print a # This calls the Pattern.__str__() function +"PatternA" +>>> a.trim() # This calls the Pattern.trim() function. Note that no input is required. + +The self in the function definition is an implied input diff --git a/doc/source/main/images/bandb.jpg b/doc/source/main/images/bandb.jpg new file mode 100644 index 0000000..92d8fcf Binary files /dev/null and b/doc/source/main/images/bandb.jpg differ diff --git a/doc/source/main/images/modeling_process.jpg b/doc/source/main/images/modeling_process.jpg new file mode 100644 index 0000000..2748a9b Binary files /dev/null and b/doc/source/main/images/modeling_process.jpg differ diff --git a/doc/source/main/images/or_methodology.jpg b/doc/source/main/images/or_methodology.jpg new file mode 100644 index 0000000..1b240be Binary files /dev/null and b/doc/source/main/images/or_methodology.jpg differ diff --git a/doc/source/main/images/sensitivity_excel.jpg b/doc/source/main/images/sensitivity_excel.jpg new file mode 100644 index 0000000..cad5c5b Binary files /dev/null and b/doc/source/main/images/sensitivity_excel.jpg differ diff --git a/doc/source/main/index.rst b/doc/source/main/index.rst new file mode 100644 index 0000000..2c2de03 --- /dev/null +++ b/doc/source/main/index.rst @@ -0,0 +1,16 @@ + +Main Topics +===================== + + + +.. toctree:: + :maxdepth: 2 + + the_optimisation_process + optimisation_concepts + basic_python_coding + installing_pulp_at_home + amply + + diff --git a/doc/source/main/installing_pulp_at_home.rst b/doc/source/main/installing_pulp_at_home.rst new file mode 100644 index 0000000..11d2e94 --- /dev/null +++ b/doc/source/main/installing_pulp_at_home.rst @@ -0,0 +1,13 @@ +.. _installation: + +Installing PuLP at Home +======================= + +PuLP is a free open source software written in Python. It is used to describe +optimisation problems as mathematical models. PuLP can then call any of numerous +external LP solvers (CBC, GLPK, CPLEX, Gurobi etc) to solve this model and then +use python commands to manipulate and display the solution. + +.. include:: ../../../INSTALL + + diff --git a/doc/source/main/optimisation_concepts.rst b/doc/source/main/optimisation_concepts.rst new file mode 100644 index 0000000..d741661 --- /dev/null +++ b/doc/source/main/optimisation_concepts.rst @@ -0,0 +1,42 @@ +Optimisation Concepts +===================== + +Linear Programing +----------------- +The simplest type of mathematical program is a linear program. For your +mathematical program to be a linear program you need the following +conditions to be true: + +* The decision variables must be real variables; +* The objective must be a linear expression; +* The constraints must be linear expressions. + +Linear expressions are any expression of the form + +.. math:: + + a_1 x_1 + a_2 x_2 + a_3 x_3 + ... a_n x_n \{<= , =, >=\} b + +where the :math:`a_i` and :math:`b` are known constants and :math:`x_i` are variables. The process +of solving a linear program is called linear programing. Linear programing +is done via the Revised Simplex Method (also known as the Primal Simplex Method), +the Dual Simplex Method or an Interior Point Method. Some solvers like cplex +allow you to specify which method you use, but we won’t go into further detail +here. + +Integer Programing +------------------ + +Integer programs are almost identical to linear programs with one very +important exception. Some of the decision variables in integer programs may +need to have only integer values. The variables are known as integer variables. +Since most integer programs contain a mix of continuous variables and integer +variables they are often known as mixed integer programs. While the change +from linear programing is a minor one, the effect on the solution process is +enormous. Integer programs can be very difficult problems to solve and there +is a lot of current research finding “good” ways to solve integer programs. +Integer programs can be solved using the branch-and-bound process. + +Note For MIPs of any reasonable size the solution time grows +exponentially as the number of integer variables increases. + diff --git a/doc/source/main/the_optimisation_process.rst b/doc/source/main/the_optimisation_process.rst new file mode 100644 index 0000000..67098ff --- /dev/null +++ b/doc/source/main/the_optimisation_process.rst @@ -0,0 +1,113 @@ +The Optimisation Process +======================== + +Solving an optimisation problem is not a linear process, but the process can be +broken down into five general steps: + ++ Getting the problem description ++ Formulating the mathematical program ++ Solving the mathematical program ++ Performing some post-optimal analysis ++ Presenting the solution and analysis + +However, there are often "feedback loops" within this process. For example, +after formulating and solving an optimisation problem, you will often want to +consider the validity of your solution (often consulting with the person who +provided the problem description). If your solution is invalid you may need to +alter or update your formulation to incorporate your new understanding of the +actual problem. This process is shown in the Operations Research Methodology +Diagram. + +.. image:: images/or_methodology.jpg + +The modeling process starts with a well-defined model description, then uses +mathematics to formulate a mathematical program. Next, the modeler enters the +mathematical program into some solver software, e.g., Excel and solves the +model. Finally, the solution is translated into a decision in terms of the +original model description. + +Using Python gives you a "shortcut" through the modeling process. By formulating +the mathematical program in Python you have already put it into a form that +can be used easily by PuLP the modeller to call many solvers, e.g. CPLEX, COIN, +gurobi so you don't need to enter the mathematical program into the solver +software. However, you usually don't put any "hard" numbers into your +formulation, instead you "populate" your model using data files, so there is +some work involved in creating the appropriate data file. The advantage of using +data files is that the same model may used many times with different data sets. + +The Modeling Process +-------------------- + +The modeling process is a "neat and tidy" simplification of the optimisation +process. Let's consider the five steps of the optimisation process in more +detail: + +Getting the Problem Description +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The aim of this step is to come up with a formal, rigourous model description. +Usually you start an optimisation project with an abstract description of a +problem and some data. Often you need to spend some time talking with the +person providing the problem (usually known as the client). By talking with the +client and considering the data available you can come up with the more +rigourous model description you are used to. Sometimes not all the data will be +relevant or you will need to ask the client if they can provide some other +data. Sometimes the limitations of the available data may change your model +description and subsequent formulation significantly. + +Formulating the mathematical program +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +In this step we identify the key quantifiable decisions, restrictions and goals +from the problem description, and capture their interdependencies in a +mathematical model. We can break the formulation process into 4 key steps: + +* Identify the Decision Variables paying particular attention to units (for example: we need to decide how many hours per week each process will run for). +* Formulate the Objective Function using the decision variables, we can construct a minimise or maximise objective function. The objective function typically reflects the total cost, or total profit, for a given value of the decision variables. +* Formulate the Constraints, either logical (for example, we cannot work for a negative number of hours), or explicit to the problem description. Again, the constraints are expressed in terms of the decision variables. +* Identify the Data needed for the objective function and constraints. To solve your mathematical program you will need to have some "hard numbers" as variable bounds and/or variable coefficients in your objective function and/or constraints. + +Solving the mathematical program +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +For relatively simple or well understood problems the mathematical model can +often be solved to optimality (i.e., the best possible solution is identified). +This is done using algorithms such as the Revised Simplex Method +or Interior Point Methods. However, many +industrial problems would take too long to solve to optimality using these +techniques, and so are solved using heuristic methods which do not guarantee optimality. + +Performing some post-optimal analysis +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Often there is uncertainty in the problem description (either with the accuracy +of the data provided, or with the value(s) of data in the future). In this +situation the robustness of our solution can be examined by performing +post-optimal analysis. This involves identifying how the optimal solution would +change under various changes to the formulation (for example, what would be the +effect of a given cost increasing, or a particular machine failing?). This sort +of analysis can also be useful for making tactical or strategic decisions (for +example, if we invested in opening another factory, what effect would this have on our revenue?). + +Another important consideration in this step (and the next) is the validation of +the mathematical program's solution. You should carefully consider what the +solution's variable values mean in terms of the original problem description. +Make sure they make sense to you and, more importantly, your client (which is +why the next step, presenting the solution and analysis is important). + +Presenting the solution and analysis +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +A crucial step in the optimisation process is the presentation of the solution +and any post-optimal analysis. The translation from a mathematical program's +solution back into a concise and comprehensible summary is as important as the +translation from the problem description into the mathematical program. Key +observations and decisions generated via optimisation must be presented in an +easily understandable way for the client or project stakeholders. + +Your presentation is a crucial first step in the implementation of the decisions +generated by your mathematical program. If the decisions and their consequences +(often determined by the mathematical program constraints) are not presented +clearly and intelligently your optimal decision will never be used. + +This step is also your chance to suggest other work in the future. This could include: + +* Periodic monitoring of the validity of your mathematical program; +* Further analysis of your solution, looking for other benefits for your client; +* Identification of future optimisation opportunities. diff --git a/doc/source/pulp.rst b/doc/source/pulp.rst new file mode 100644 index 0000000..0ed443c --- /dev/null +++ b/doc/source/pulp.rst @@ -0,0 +1,107 @@ +===================================== +:mod:`pulp`: Pulp classes +===================================== +.. module:: pulp + +.. currentmodule:: pulp + +.. autosummary:: + + LpProblem + LpVariable + LpAffineExpression + LpConstraint + LpConstraint.makeElasticSubProblem + FixedElasticSubProblem + +.. todo:: LpFractionConstraint, FractionElasticSubProblem + +The LpProblem Class +------------------- + +.. autoclass:: LpProblem + :show-inheritance: + + Three important attributes of the problem are: + + .. attribute:: objective + + The objective of the problem, an :obj:`LpAffineExpression` + + .. attribute:: constraints + + An :class:`ordered dictionary` of + :class:`constraints` of the problem - indexed by their names. + + .. attribute:: status + + The return :data:`status ` + of the problem from the solver. + + Some of the more important methods: + + .. automethod:: solve + .. automethod:: roundSolution + .. automethod:: setObjective + .. automethod:: writeLP + +Variables and Expressions +------------------------- + +.. autoclass:: LpElement + :members: + +---- + +.. autoclass:: LpVariable + :members: + +Example: + +>>> x = LpVariable('x',lowBound = 0, cat='Continuous') +>>> y = LpVariable('y', upBound = 5, cat='Integer') + +gives :math:`x \in [0,\infty)`, :math:`y \in (-\infty, 5]`, an +integer. + +---- + +.. autoclass:: LpAffineExpression + :show-inheritance: + + In brief, :math:`\textsf{LpAffineExpression([(x[i],a[i]) for i in + I])} = \sum_{i \in I} a_i x_i` where (note the order): + + * ``x[i]`` is an :class:`LpVariable` + * ``a[i]`` is a numerical coefficient. + +---- + +.. autofunction:: lpSum + + +Constraints +----------- +.. autoclass:: LpConstraint + :show-inheritance: + :members: makeElasticSubProblem + +.. include:: documentation/elastic.rst + +.. autoclass:: FixedElasticSubProblem + :show-inheritance: + :members: + +Combinations and Permutations +------------------------------ + +.. autofunction:: combination + +.. autofunction:: allcombinations + +.. autofunction:: permutation + +.. autofunction:: allpermutations + +.. autofunction:: value + diff --git a/doc/source/solvers.rst b/doc/source/solvers.rst new file mode 100644 index 0000000..b459911 --- /dev/null +++ b/doc/source/solvers.rst @@ -0,0 +1,11 @@ + +:mod:`pulp.solvers` Interface to Solvers +======================================== + + +.. automodule:: pulp.solvers + :members: + :undoc-members: + :show-inheritance: + + diff --git a/docs.cfg b/docs.cfg new file mode 100644 index 0000000..f1cd26b --- /dev/null +++ b/docs.cfg @@ -0,0 +1,11 @@ +[buildout] +extends = buildout.cfg +parts += sphinx + +[sphinx] +recipe = collective.recipe.sphinxbuilder +source = ${buildout:directory}/doc/source +build = ${buildout:directory}/doc +interpreter = ${buildout:directory}/bin/pythonpulp +outputs = html + diff --git a/examples/AmericanSteelProblem.py b/examples/AmericanSteelProblem.py new file mode 100644 index 0000000..caea200 --- /dev/null +++ b/examples/AmericanSteelProblem.py @@ -0,0 +1,100 @@ +""" +The American Steel Problem for the PuLP Modeller + +Authors: Antony Phillips, Dr Stuart Mitchell 2007 +""" + +# Import PuLP modeller functions +from pulp import * + +# List of all the nodes +Nodes = ["Youngstown", + "Pittsburgh", + "Cincinatti", + "Kansas City", + "Chicago", + "Albany", + "Houston", + "Tempe", + "Gary"] + +nodeData = {# NODE Supply Demand + "Youngstown": [10000,0], + "Pittsburgh": [15000,0], + "Cincinatti": [0,0], + "Kansas City": [0,0], + "Chicago": [0,0], + "Albany": [0,3000], + "Houston": [0,7000], + "Tempe": [0,4000], + "Gary": [0,6000]} + +# List of all the arcs +Arcs = [("Youngstown","Albany"), + ("Youngstown","Cincinatti"), + ("Youngstown","Kansas City"), + ("Youngstown","Chicago"), + ("Pittsburgh","Cincinatti"), + ("Pittsburgh","Kansas City"), + ("Pittsburgh","Chicago"), + ("Pittsburgh","Gary"), + ("Cincinatti","Albany"), + ("Cincinatti","Houston"), + ("Kansas City","Houston"), + ("Kansas City","Tempe"), + ("Chicago","Tempe"), + ("Chicago","Gary")] + +arcData = { # ARC Cost Min Max + ("Youngstown","Albany"): [0.5,0,1000], + ("Youngstown","Cincinatti"): [0.35,0,3000], + ("Youngstown","Kansas City"): [0.45,1000,5000], + ("Youngstown","Chicago"): [0.375,0,5000], + ("Pittsburgh","Cincinatti"): [0.35,0,2000], + ("Pittsburgh","Kansas City"): [0.45,2000,3000], + ("Pittsburgh","Chicago"): [0.4,0,4000], + ("Pittsburgh","Gary"): [0.45,0,2000], + ("Cincinatti","Albany"): [0.35,1000,5000], + ("Cincinatti","Houston"): [0.55,0,6000], + ("Kansas City","Houston"): [0.375,0,4000], + ("Kansas City","Tempe"): [0.65,0,4000], + ("Chicago","Tempe"): [0.6,0,2000], + ("Chicago","Gary"): [0.12,0,4000]} + +# Splits the dictionaries to be more understandable +(supply, demand) = splitDict(nodeData) +(costs, mins, maxs) = splitDict(arcData) + +# Creates the boundless Variables as Integers +vars = LpVariable.dicts("Route",Arcs,None,None,LpInteger) + +# Creates the upper and lower bounds on the variables +for a in Arcs: + vars[a].bounds(mins[a], maxs[a]) + +# Creates the 'prob' variable to contain the problem data +prob = LpProblem("American Steel Problem",LpMinimize) + +# Creates the objective function +prob += lpSum([vars[a]* costs[a] for a in Arcs]), "Total Cost of Transport" + +# Creates all problem constraints - this ensures the amount going into each node is at least equal to the amount leaving +for n in Nodes: + prob += (supply[n]+ lpSum([vars[(i,j)] for (i,j) in Arcs if j == n]) >= + demand[n]+ lpSum([vars[(i,j)] for (i,j) in Arcs if i == n])), "Steel Flow Conservation in Node %s"%n + +# The problem data is written to an .lp file +prob.writeLP("AmericanSteelProblem.lp") + +# The problem is solved using PuLP's choice of Solver +prob.solve() + +# The status of the solution is printed to the screen +print "Status:", LpStatus[prob.status] + +# Each of the variables is printed with it's resolved optimum value +for v in prob.variables(): + print v.name, "=", v.varValue + +# The optimised objective function value is printed to the screen +print "Total Cost of Transportation = ", value(prob.objective) diff --git a/examples/BeerDistributionProblem.py b/examples/BeerDistributionProblem.py new file mode 100644 index 0000000..50c0e90 --- /dev/null +++ b/examples/BeerDistributionProblem.py @@ -0,0 +1,71 @@ +""" +The Beer Distribution Problem for the PuLP Modeller + +Authors: Antony Phillips, Dr Stuart Mitchell 2007 +""" + +# Import PuLP modeler functions +from pulp import * + +# Creates a list of all the supply nodes +Warehouses = ["A", "B"] + +# Creates a dictionary for the number of units of supply for each supply node +supply = {"A": 1000, + "B": 4000} + +# Creates a list of all demand nodes +Bars = ["1", "2", "3", "4", "5"] + +# Creates a dictionary for the number of units of demand for each demand node +demand = {"1":500, + "2":900, + "3":1800, + "4":200, + "5":700,} + +# Creates a list of costs of each transportation path +costs = [ #Bars + #1 2 3 4 5 + [2,4,5,2,1],#A Warehouses + [3,1,3,2,3] #B + ] + +# The cost data is made into a dictionary +costs = makeDict([Warehouses,Bars],costs,0) + +# Creates the 'prob' variable to contain the problem data +prob = LpProblem("Beer Distribution Problem",LpMinimize) + +# Creates a list of tuples containing all the possible routes for transport +Routes = [(w,b) for w in Warehouses for b in Bars] + +# A dictionary called 'Vars' is created to contain the referenced variables(the routes) +vars = LpVariable.dicts("Route",(Warehouses,Bars),0,None,LpInteger) + +# The objective function is added to 'prob' first +prob += lpSum([vars[w][b]*costs[w][b] for (w,b) in Routes]), "Sum_of_Transporting_Costs" + +# The supply maximum constraints are added to prob for each supply node (warehouse) +for w in Warehouses: + prob += lpSum([vars[w][b] for b in Bars])<=supply[w], "Sum_of_Products_out_of_Warehouse_%s"%w + +# The demand minimum constraints are added to prob for each demand node (bar) +for b in Bars: + prob += lpSum([vars[w][b] for w in Warehouses])>=demand[b], "Sum_of_Products_into_Bar%s"%b + +# The problem data is written to an .lp file +prob.writeLP("BeerDistributionProblem.lp") + +# The problem is solved using PuLP's choice of Solver +prob.solve() + +# The status of the solution is printed to the screen +print "Status:", LpStatus[prob.status] + +# Each of the variables is printed with it's resolved optimum value +for v in prob.variables(): + print v.name, "=", v.varValue + +# The optimised objective function value is printed to the screen +print "Total Cost of Transportation = ", value(prob.objective) diff --git a/examples/BeerDistributionProblem_resolve.py b/examples/BeerDistributionProblem_resolve.py new file mode 100644 index 0000000..7561133 --- /dev/null +++ b/examples/BeerDistributionProblem_resolve.py @@ -0,0 +1,82 @@ +""" +The Beer Distribution Problem for the PuLP Modeller +Illustrates changing a constraint and solving + +Authors: Antony Phillips, Dr Stuart Mitchell 2007 +""" + +# Import PuLP modeler functions +from pulp import * + +# Creates a list of all the supply nodes +Warehouses = ["A", "B"] + +# Creates a dictionary for the number of units of supply for each supply node +supply = {"A": 1000, + "B": 4000} + +# Creates a list of all demand nodes +Bars = ["1", "2", "3", "4", "5"] + +# Creates a dictionary for the number of units of demand for each demand node +demand = {"1":500, + "2":900, + "3":1800, + "4":200, + "5":700,} + +# Creates a list of costs of each transportation path +costs = [ #Bars + #1 2 3 4 5 + [2,4,5,2,1],#A Warehouses + [3,1,3,2,3] #B + ] + +# The cost data is made into a dictionary +costs = makeDict([Warehouses,Bars],costs,0) + +# Creates the 'prob' variable to contain the problem data +prob = LpProblem("Beer Distribution Problem",LpMinimize) + +# Creates a list of tuples containing all the possible routes for transport +Routes = [(w,b) for w in Warehouses for b in Bars] + +# A dictionary called 'Vars' is created to contain the referenced variables(the routes) +vars = LpVariable.dicts("Route",(Warehouses,Bars),0,None,LpInteger) + +# The objective function is added to 'prob' first +prob += lpSum([vars[w][b]*costs[w][b] for (w,b) in Routes]), "Sum_of_Transporting_Costs" + +# The supply maximum constraints are added to prob for each supply node (warehouse) +for w in Warehouses: + prob += lpSum([vars[w][b] for b in Bars])<=supply[w], "Sum_of_Products_out_of_Warehouse_%s"%w + +# The demand minimum constraints are added to prob for each demand node (bar) +# These constraints are stored for resolve later +bar_demand_constraint = {} +for b in Bars: + constraint = lpSum([vars[w][b] for w in Warehouses])>=demand[b] + prob += constraint, "Sum_of_Products_into_Bar_%s"%b + bar_demand_constraint[b] = constraint + +# The problem data is written to an .lp file +prob.writeLP("BeerDistributionProblem.lp") +for demand in range(500, 601, 10): + # reoptimise the problem by increasing demand at bar '1' + # note the constant is stored as the LHS constant not the RHS of the constraint + bar_demand_constraint['1'].constant = - demand + #or alternatively, + #prob.constraints["Sum_of_Products_into_Bar_1"].constant = - demand + + # The problem is solved using PuLP's choice of Solver + prob.solve() + + # The status of the solution is printed to the screen + print "Status:", LpStatus[prob.status] + + # Each of the variables is printed with it's resolved optimum value + for v in prob.variables(): + print v.name, "=", v.varValue + + # The optimised objective function value is printed to the screen + print "Total Cost of Transportation = ", value(prob.objective) diff --git a/examples/CG.py b/examples/CG.py new file mode 100644 index 0000000..0cc71d5 --- /dev/null +++ b/examples/CG.py @@ -0,0 +1,119 @@ +""" +Column Generation Functions + +Authors: Antony Phillips, Dr Stuart Mitchell 2008 +""" + +# Import PuLP modeler functions +from pulp import * + +class Pattern: + """ + Information on a specific pattern in the SpongeRoll Problem + """ + cost = 1 + trimValue = 0.04 + totalRollLength = 20 + lenOpts = ["5", "7", "9"] + + def __init__(self, name, lengths = None): + self.name = name + self.lengthsdict = dict(zip(self.lenOpts,lengths)) + + def __str__(self): + return self.name + + def trim(self): + return Pattern.totalRollLength - sum([int(i)*int(self.lengthsdict[i]) for i in self.lengthsdict]) + +def masterSolve(Patterns, rollData, relax = True): + + # The rollData is made into separate dictionaries + (rollDemand,surplusPrice) = splitDict(rollData) + + # The variable 'prob' is created + prob = LpProblem("Cutting Stock Problem",LpMinimize) + + # vartype represents whether or not the variables are relaxed + if relax: + vartype = LpContinuous + else: + vartype = LpInteger + + # The problem variables are created + pattVars = LpVariable.dicts("Pattern", Patterns, 0, None, vartype) + surplusVars = LpVariable.dicts("Surplus", Pattern.lenOpts, 0, None, vartype) + + # The objective function is entered: (the total number of large rolls used * the cost of each) - + # (the value of the surplus stock) - (the value of the trim) + prob += lpSum([pattVars[i]*Pattern.cost for i in Patterns]) - lpSum([surplusVars[i]*surplusPrice[i]\ + for i in Pattern.lenOpts]) - lpSum([pattVars[i]*i.trim()*Pattern.trimValue for i in Patterns]) + + # The demand minimum constraint is entered + for j in Pattern.lenOpts: + prob += lpSum([pattVars[i]*i.lengthsdict[j] for i in Patterns]) - surplusVars[j]>=rollDemand[j],"Min%s"%j + + # The problem is solved + prob.solve() + + # The variable values are rounded + prob.roundSolution() + + if relax: + # Creates a dual variables list + duals = {} + for name,i in zip(['Min5','Min7','Min9'],Pattern.lenOpts): + duals[i] = prob.constraints[name].pi + + return duals + + else: + # Creates a dictionary of the variables and their values + varsdict = {} + for v in prob.variables(): + varsdict[v.name] = v.varValue + + # The number of rolls of each length in each pattern is printed + for i in Patterns: + print i, " = %s"%[i.lengthsdict[j] for j in Pattern.lenOpts] + + return value(prob.objective), varsdict + +def subSolve(Patterns, duals): + + # The variable 'prob' is created + prob = LpProblem("SubProb",LpMinimize) + + # The problem variables are created + vars = LpVariable.dicts("Roll Length", Pattern.lenOpts, 0, None, LpInteger) + + trim = LpVariable("Trim", 0 ,None,LpInteger) + + # The objective function is entered: the reduced cost of a new pattern + prob += (Pattern.cost - Pattern.trimValue*trim) - lpSum([vars[i]*duals[i] for i in Pattern.lenOpts]), "Objective" + + # The conservation of length constraint is entered + prob += lpSum([vars[i]*int(i) for i in Pattern.lenOpts]) + trim == Pattern.totalRollLength, "lengthEquate" + + # The problem is solved + prob.solve() + + # The variable values are rounded + prob.roundSolution() + + # The new pattern is written to a dictionary + varsdict = {} + newPattern = {} + for v in prob.variables(): + varsdict[v.name] = v.varValue + for i,j in zip(Pattern.lenOpts,["Roll_Length_5","Roll_Length_7","Roll_Length_9"]): + newPattern[i] = int(varsdict[j]) + + # Check if there are more patterns which would reduce the master LP objective function further + if value(prob.objective) < -10**-5: + morePatterns = True # continue adding patterns + Patterns += [Pattern("P" + str(len(Patterns)), [newPattern[i] for i in ["5","7","9"]])] + else: + morePatterns = False # all patterns have been added + + return Patterns, morePatterns diff --git a/examples/CGcolumnwise.py b/examples/CGcolumnwise.py new file mode 100644 index 0000000..28e4183 --- /dev/null +++ b/examples/CGcolumnwise.py @@ -0,0 +1,143 @@ +""" +Columnwise Column Generation Functions + +Authors: Antony Phillips, Dr Stuart Mitchell 2008 +""" + +# Import PuLP modeler functions +from pulp import * + +class Pattern: + """ + Information on a specific pattern in the SpongeRoll Problem + """ + cost = 1 + trimValue = 0.04 + totalRollLength = 20 + lenOpts = ["5", "7", "9"] + numPatterns = 0 + + def __init__(self, name, lengths = None): + self.name = name + self.lengthsdict = dict(zip(self.lenOpts,lengths)) + Pattern.numPatterns += 1 + + def __str__(self): + return self.name + + def trim(self): + return Pattern.totalRollLength - sum([int(i)*int(self.lengthsdict[i]) for i in self.lengthsdict]) + +def createMaster(): + + rollData = {#Length Demand SalePrice + "5": [150, 0.25], + "7": [200, 0.33], + "9": [300, 0.40]} + + (rollDemand,surplusPrice) = splitDict(rollData) + + # The variable 'prob' is created + prob = LpProblem("MasterSpongeRollProblem",LpMinimize) + + # The variable 'obj' is created and set as the LP's objective function + obj = LpConstraintVar("Obj") + prob.setObjective(obj) + + # The constraints are initialised and added to prob + constraints = {} + for l in Pattern.lenOpts: + constraints[l]= LpConstraintVar("Min" + str(l), LpConstraintGE, rollDemand[l]) + prob += constraints[l] + + # The surplus variables are created + surplusVars = [] + for i in Pattern.lenOpts: + surplusVars += [LpVariable("Surplus "+ i,0,None,LpContinuous, -surplusPrice[i] * obj - constraints[i])] + + return prob,obj,constraints + +def addPatterns(obj,constraints,newPatterns): + + # A list called Patterns is created to contain all the Pattern class + # objects created in this function call + Patterns = [] + for i in newPatterns: + + # The new patterns are checked to see that their length does not exceed + # the total roll length + lsum = 0 + for j,k in zip(i,Pattern.lenOpts): + lsum += j * int(k) + if lsum > Pattern.totalRollLength: + raise "Length Options too large for Roll" + + # The number of rolls of each length in each new pattern is printed + print "P"+str(Pattern.numPatterns),"=",i + + # The patterns are instantiated as Pattern objects + Patterns += [Pattern("P" + str(Pattern.numPatterns),i)] + + # The pattern variables are created + pattVars = [] + for i in Patterns: + pattVars += [LpVariable("Pattern "+i.name,0,None,LpContinuous, (i.cost - Pattern.trimValue*i.trim()) * obj\ + + lpSum([constraints[l]*i.lengthsdict[l] for l in Pattern.lenOpts]))] + +def masterSolve(prob,relax = True): + + # Unrelaxes the Integer Constraint + if not relax: + for v in prob.variables(): + v.cat = LpInteger + + # The problem is solved and rounded + prob.solve() + prob.roundSolution() + + if relax: + # A dictionary of dual variable values is returned + duals = {} + for i,name in zip(Pattern.lenOpts,["Min5","Min7","Min9"]): + duals[i] = prob.constraints[name].pi + return duals + else: + # A dictionary of variable values and the objective value are returned + varsdict = {} + for v in prob.variables(): + varsdict[v.name] = v.varValue + + return value(prob.objective), varsdict + +def subSolve(duals): + + # The variable 'prob' is created + prob = LpProblem("SubProb",LpMinimize) + + # The problem variables are created + vars = LpVariable.dicts("Roll Length", Pattern.lenOpts, 0, None, LpInteger) + + trim = LpVariable("Trim", 0 ,None,LpInteger) + + # The objective function is entered: the reduced cost of a new pattern + prob += (Pattern.cost - Pattern.trimValue*trim) - lpSum([vars[i]*duals[i] for i in Pattern.lenOpts]), "Objective" + + # The conservation of length constraint is entered + prob += lpSum([vars[i]*int(i) for i in Pattern.lenOpts]) + trim == Pattern.totalRollLength, "lengthEquate" + + # The problem is solved + prob.solve() + + # The variable values are rounded + prob.roundSolution() + + newPatterns = [] + # Check if there are more patterns which would reduce the master LP objective function further + if value(prob.objective) < -10**-5: + varsdict = {} + for v in prob.variables(): + varsdict[v.name] = v.varValue + # Adds the new pattern to the newPatterns list + newPatterns += [[int(varsdict["Roll_Length_5"]),int(varsdict["Roll_Length_7"]),int(varsdict["Roll_Length_9"])]] + + return newPatterns diff --git a/examples/ComputerPlantProblem.py b/examples/ComputerPlantProblem.py new file mode 100644 index 0000000..6835208 --- /dev/null +++ b/examples/ComputerPlantProblem.py @@ -0,0 +1,91 @@ +""" +The Computer Plant Problem for the PuLP Modeller + +Authors: Antony Phillips, Dr Stuart Mitchell 2007 +""" + +# Import PuLP modeler functions +from pulp import * + +# Creates a list of all the supply nodes +Plants = ["San Francisco", + "Los Angeles", + "Phoenix", + "Denver"] + +# Creates a dictionary of lists for the number of units of supply at +# each plant and the fixed cost of running each plant +supplyData = {#Plant Supply Fixed Cost + "San Francisco":[1700, 70000], + "Los Angeles" :[2000, 70000], + "Phoenix" :[1700, 65000], + "Denver" :[2000, 70000] + } + +# Creates a list of all demand nodes +Stores = ["San Diego", + "Barstow", + "Tucson", + "Dallas"] + +# Creates a dictionary for the number of units of demand at each store +demand = { #Store Demand + "San Diego":1700, + "Barstow" :1000, + "Tucson" :1500, + "Dallas" :1200 + } + +# Creates a list of costs for each transportation path +costs = [ #Stores + #SD BA TU DA + [5, 3, 2, 6], #SF + [4, 7, 8, 10],#LA Plants + [6, 5, 3, 8], #PH + [9, 8, 6, 5] #DE + ] + +# Creates a list of tuples containing all the possible routes for transport +Routes = [(p,s) for p in Plants for s in Stores] + +# Splits the dictionaries to be more understandable +(supply,fixedCost) = splitDict(supplyData) + +# The cost data is made into a dictionary +costs = makeDict([Plants,Stores],costs,0) + +# Creates the problem variables of the Flow on the Arcs +flow = LpVariable.dicts("Route",(Plants,Stores),0,None,LpInteger) + +# Creates the master problem variables of whether to build the Plants or not +build = LpVariable.dicts("BuildaPlant",Plants,0,1,LpInteger) + +# Creates the 'prob' variable to contain the problem data +prob = LpProblem("Computer Plant Problem",LpMinimize) + +# The objective function is added to prob - The sum of the transportation costs and the building fixed costs +prob += lpSum([flow[p][s]*costs[p][s] for (p,s) in Routes])+lpSum([fixedCost[p]*build[p] for p in Plants]),"Total Costs" + +# The Supply maximum constraints are added for each supply node (plant) +for p in Plants: + prob += lpSum([flow[p][s] for s in Stores])<=supply[p]*build[p], "Sum of Products out of Plant %s"%p + +# The Demand minimum constraints are added for each demand node (store) +for s in Stores: + prob += lpSum([flow[p][s] for p in Plants])>=demand[s], "Sum of Products into Stores %s"%s + +# The problem data is written to an .lp file +prob.writeLP("ComputerPlantProblem.lp") + +# The problem is solved using PuLP's choice of Solver +prob.solve() + +# The status of the solution is printed to the screen +print "Status:", LpStatus[prob.status] + +# Each of the variables is printed with it's resolved optimum value +for v in prob.variables(): + print v.name, "=", v.varValue + +# The optimised objective function value is printed to the screen +print "Total Costs = ", value(prob.objective) \ No newline at end of file diff --git a/examples/SpongeRollProblem1.py b/examples/SpongeRollProblem1.py new file mode 100644 index 0000000..b7bc1c7 --- /dev/null +++ b/examples/SpongeRollProblem1.py @@ -0,0 +1,61 @@ +""" +The Simplified Sponge Roll Problem for the PuLP Modeller + +Authors: Antony Phillips, Dr Stuart Mitchell 2007 +""" + +# Import PuLP modeler functions +from pulp import * + +# A list of all the roll lengths is created +LenOpts = ["5","7","9"] + +# A dictionary of the demand for each roll length is created +rollDemand = {"5":150, + "7":200, + "9":300} + +# A list of all the patterns is created +PatternNames = ["A","B","C"] + +# Creates a list of the number of rolls in each pattern for each different roll length +patterns = [#A B C + [0,2,2],# 5 + [1,1,0],# 7 + [1,0,1] # 9 + ] + +# The cost of each 20cm long sponge roll used +cost = 1 + +# The pattern data is made into a dictionary +patterns = makeDict([LenOpts,PatternNames],patterns,0) + +# The problem variables of the number of each pattern to make are created +vars = LpVariable.dicts("Patt",PatternNames,0,None,LpInteger) + +# The variable 'prob' is created +prob = LpProblem("Cutting Stock Problem",LpMinimize) + +# The objective function is entered: the total number of large rolls used * the fixed cost of each +prob += lpSum([vars[i]*cost for i in PatternNames]),"Production Cost" + +# The demand minimum constraint is entered +for i in LenOpts: + prob += lpSum([vars[j]*patterns[i][j] for j in PatternNames])>=rollDemand[i],"Ensuring enough %s cm rolls"%i + +# The problem data is written to an .lp file +prob.writeLP("SpongeRollProblem.lp") + +# The problem is solved using PuLP's choice of Solver +prob.solve() + +# The status of the solution is printed to the screen +print "Status:", LpStatus[prob.status] + +# Each of the variables is printed with it's resolved optimum value +for v in prob.variables(): + print v.name, "=", v.varValue + +# The optimised objective function value is printed to the screen +print "Production Costs = ", value(prob.objective) diff --git a/examples/SpongeRollProblem2.py b/examples/SpongeRollProblem2.py new file mode 100644 index 0000000..f9ad2ab --- /dev/null +++ b/examples/SpongeRollProblem2.py @@ -0,0 +1,78 @@ +""" +The Simplified Sponge Roll Problem with Surplus and Trim for the PuLP Modeller + +Authors: Antony Phillips, Dr Stuart Mitchell 2007 +""" + +# Import PuLP modeler functions +from pulp import * + +# A list of all the roll lengths is created +LenOpts = ["5","7","9"] + +rollData = {#Length Demand SalePrice + "5": [150, 0.25], + "7": [200, 0.33], + "9": [300, 0.40]} + +# A list of all the patterns is created +PatternNames = ["A","B","C"] + +# Creates a list of the number of rolls in each pattern for each different roll length +patterns = [#A B C + [0,2,2],# 5 + [1,1,0],# 7 + [1,0,1] # 9 + ] + +# A dictionary of the number of cms of trim in each pattern is created +trim = {"A": 4, + "B": 2, + "C": 1} + +# The cost of each 20cm long sponge roll used +cost = 1 + +# The sale value of each cm of trim +trimValue = 0.04 + +# The rollData is made into separate dictionaries +(rollDemand,surplusPrice) = splitDict(rollData) + +# The pattern data is made into a dictionary +patterns = makeDict([LenOpts,PatternNames],patterns,0) + +# The problem variables of the number of each pattern to make are created +pattVars = LpVariable.dicts("Patt",PatternNames,0,None,LpInteger) + +# The problem variables of the number of surplus rolls for each length are created +surplusVars = LpVariable.dicts("Surp",LenOpts,0,None,LpInteger) + +# The variable 'prob' is created +prob = LpProblem("Cutting Stock Problem",LpMinimize) + +# The objective function is entered: the total number of large rolls used * the fixed cost of each minus the surplus +# sales and the trim sales +prob += lpSum([pattVars[i]*cost for i in PatternNames]) - lpSum([surplusVars[i]*surplusPrice[i] for i in LenOpts]) \ +- lpSum([pattVars[i]*trim[i]*trimValue for i in PatternNames]),"Net Production Cost" + +# The demand minimum constraint is entered +for i in LenOpts: + prob += lpSum([pattVars[j]*patterns[i][j] for j in PatternNames]) - surplusVars[i]\ + >= rollDemand[i],"Ensuring enough %s cm rolls"%i + +# The problem data is written to an .lp file +prob.writeLP("SpongeRollProblem.lp") + +# The problem is solved using PuLP's choice of Solver +prob.solve() + +# The status of the solution is printed to the screen +print "Status:", LpStatus[prob.status] + +# Each of the variables is printed with it's resolved optimum value +for v in prob.variables(): + print v.name, "=", v.varValue + +# The optimised objective function value is printed to the screen +print "Production Costs = ", value(prob.objective) diff --git a/examples/SpongeRollProblem3.py b/examples/SpongeRollProblem3.py new file mode 100644 index 0000000..e45d2fc --- /dev/null +++ b/examples/SpongeRollProblem3.py @@ -0,0 +1,135 @@ +""" +The Full Sponge Roll Problem for the PuLP Modeller + +Authors: Antony Phillips, Dr Stuart Mitchell 2007 +""" + +def calculatePatterns(totalRollLength,lenOpts,head): + """ + Recursively calculates the list of options lists for a cutting stock problem. The input + 'tlist' is a pointer, and will be the output of the function call. + + The inputs are: + totalRollLength - the length of the roll + lenOpts - a list of the sizes of remaining cutting options + head - the current list that has been passed down though the recusion + + Returns the list of patterns + + Authors: Bojan Blazevic, Dr Stuart Mitchell 2007 + """ + if lenOpts: + patterns =[] + #take the first option off lenOpts + opt = lenOpts[0] + for rep in range(int(totalRollLength/opt)+1): + #reduce the length + l = totalRollLength - rep*opt + h = head[:] + h.append(rep) + + patterns.extend(calculatePatterns(l, lenOpts[1:], h)) + else: + #end of the recursion + patterns = [head] + return patterns + +def makePatterns(totalRollLength,lenOpts): + """ + Makes the different cutting patterns for a cutting stock problem. + + The inputs are: + totalRollLength : the length of the roll + lenOpts: a list of the sizes of cutting options as strings + + Authors: Antony Phillips, Dr Stuart Mitchell 2007 + """ + + # calculatePatterns is called to create a list of the feasible cutting options in 'tlist' + patterns = calculatePatterns(totalRollLength,lenOpts,[]) + + # The list 'PatternNames' is created + PatternNames = [] + for i in range(len(patterns)): + PatternNames += ["P"+str(i)] + + # The amount of trim (unused material) for each pattern is calculated and added to the dictionary + # 'trim', with the reference key of the pattern name. + trim = {} + for name,pattern in zip(PatternNames,patterns): + ssum = 0 + for rep,l in zip(pattern,lenOpts): + ssum += rep*l + trim[name] = totalRollLength - ssum + # The different cutting lengths are printed, and the number of each roll of that length in each + # pattern is printed below. This is so the user can see what each pattern contains. + print "Lens: %s" %lenOpts + for name,pattern in zip(PatternNames,patterns): + print name + " = %s"%pattern + + return (PatternNames,patterns,trim) + + +# Import PuLP modeler functions +from pulp import * + +# The Total Roll Length is entered +totalRollLength = 20 + +# The cost of each 20cm long sponge roll used +cost = 1 + +# The sale value of each cm of trim +trimValue = 0.04 + +# A list of all the roll lengths is created +LenOpts = ["5","7","9"] + +rollData = {#Length Demand SalePrice + "5": [150, 0.25], + "7": [200, 0.33], + "9": [300, 0.40]} + +# The pattern names and the patterns are created as lists, and the associated trim with each pattern +# is created as a dictionary. The inputs are the total roll length and the list (as integers) of +# cutting options. +(PatternNames,patterns,trim) = makePatterns(totalRollLength,[int(l) for l in LenOpts]) + +# The RollData is made into separate dictionaries +(rollDemand,surplusPrice) = splitDict(rollData) + +# The pattern data is made into a dictionary so it can be called by patterns["7"]["P3"] for example. +# This will return the number of rolls of length "7" in pattern "P3" +patterns = makeDict([PatternNames,LenOpts],patterns,0) + +# The variable 'prob' is created +prob = LpProblem("Cutting Stock Problem",LpMinimize) + +# The problem variables of the number of each pattern to make are created +pattVars = LpVariable.dicts("Patt",PatternNames,0,None,LpInteger) + +# The problem variables of the number of surplus rolls for each length are created +surplusVars = LpVariable.dicts("Surp",LenOpts,0,None,LpInteger) + +# The objective function is entered: (the total number of large rolls used * the cost of each) - (the value of the surplus stock) - (the value of the trim) +prob += lpSum([pattVars[i]*cost for i in PatternNames]) - lpSum([surplusVars[i]*surplusPrice[i] for i in LenOpts]) - lpSum([pattVars[i]*trim[i]*trimValue for i in PatternNames]),"Net Production Cost" + +# The demand minimum constraint is entered +for j in LenOpts: + prob += lpSum([pattVars[i]*patterns[i][j] for i in PatternNames]) - surplusVars[j]>=rollDemand[j],"Ensuring enough %s cm rolls"%j + +# The problem data is written to an .lp file +prob.writeLP("SpongeRollProblem.lp") + +# The problem is solved using PuLP's choice of Solver +prob.solve() + +# The status of the solution is printed to the screen +print "Status:", LpStatus[prob.status] + +# Each of the variables is printed with it's resolved optimum value +for v in prob.variables(): + print v.name, "=", v.varValue + +# The optimised objective function value is printed to the screen +print "Production Costs = ", value(prob.objective) diff --git a/examples/SpongeRollProblem4.py b/examples/SpongeRollProblem4.py new file mode 100644 index 0000000..7ab7d4c --- /dev/null +++ b/examples/SpongeRollProblem4.py @@ -0,0 +1,137 @@ +""" +The Full Sponge Roll Problem using Classes for the PuLP Modeller + +Authors: Antony Phillips, Dr Stuart Mitchell 2007 +""" + +def calculatePatterns(totalRollLength,lenOpts,head): + """ + Recursively calculates the list of options lists for a cutting stock problem. The input + 'tlist' is a pointer, and will be the output of the function call. + + The inputs are: + totalRollLength - the length of the roll + lenOpts - a list of the sizes of remaining cutting options + head - the current list that has been passed down though the recusion + + Returns the list of patterns + + Authors: Bojan Blazevic, Dr Stuart Mitchell 2007 + """ + if lenOpts: + patterns =[] + #take the first option off lenOpts + opt = lenOpts[0] + for rep in range(int(totalRollLength/opt)+1): + #reduce the length + l = totalRollLength - rep*opt + h = head[:] + h.append(rep) + + patterns.extend(calculatePatterns(l, lenOpts[1:], h)) + else: + #end of the recursion + patterns = [head] + return patterns + +def makePatterns(totalRollLength,lenOpts): + """ + Makes the different cutting patterns for a cutting stock problem. + + The inputs are: + totalRollLength : the length of the roll + lenOpts: a list of the sizes of cutting options as strings + + Authors: Antony Phillips, Dr Stuart Mitchell 2007 + """ + + # calculatePatterns is called to create a list of the feasible cutting options in 'tlist' + patternslist = calculatePatterns(totalRollLength,lenOpts,[]) + + # The list 'PatternNames' is created + PatternNames = [] + for i in range(len(patternslist)): + PatternNames += ["P"+str(i)] + + #Patterns = [0 for i in range(len(PatternNames))] + Patterns = [] + + for i,j in enumerate(PatternNames): + Patterns += [Pattern(j, patternslist[i])] + + # The different cutting lengths are printed, and the number of each roll of that length in each + # pattern is printed below. This is so the user can see what each pattern contains. + print "Lens: %s" %lenOpts + for i in Patterns: + print i, " = %s"%[i.lengthsdict[j] for j in lenOpts] + + return Patterns + + +class Pattern: + """ + Information on a specific pattern in the SpongeRoll Problem + """ + cost = 1 + trimValue = 0.04 + totalRollLength = 20 + lenOpts = [5, 7, 9] + + def __init__(self,name,lengths = None): + self.name = name + self.lengthsdict = dict(zip(self.lenOpts,lengths)) + + def __str__(self): + return self.name + + def trim(self): + return Pattern.totalRollLength - sum([int(i)*self.lengthsdict[i] for i in self.lengthsdict]) + + +# Import PuLP modeler functions +from pulp import * + +rollData = {#Length Demand SalePrice + 5: [150, 0.25], + 7: [200, 0.33], + 9: [300, 0.40]} + +# The pattern names and the patterns are created as lists, and the associated trim with each pattern +# is created as a dictionary. The inputs are the total roll length and the list (as integers) of +# cutting options. +Patterns = makePatterns(Pattern.totalRollLength,Pattern.lenOpts) + +# The rollData is made into separate dictionaries +(rollDemand,surplusPrice) = splitDict(rollData) + +# The variable 'prob' is created +prob = LpProblem("Cutting Stock Problem",LpMinimize) + +# The problem variables of the number of each pattern to make are created +pattVars = LpVariable.dicts("Patt",Patterns,0,None,LpInteger) + +# The problem variables of the number of surplus rolls for each length are created +surplusVars = LpVariable.dicts("Surp",Pattern.lenOpts,0,None,LpInteger) + +# The objective function is entered: (the total number of large rolls used * the cost of each) - (the value of the surplus stock) - (the value of the trim) +prob += lpSum([pattVars[i]*Pattern.cost for i in Patterns]) - lpSum([surplusVars[i]*surplusPrice[i] for i in Pattern.lenOpts]) - lpSum([pattVars[i]*i.trim()*Pattern.trimValue for i in Patterns]),"Net Production Cost" + +# The demand minimum constraint is entered +for j in Pattern.lenOpts: + prob += lpSum([pattVars[i]*i.lengthsdict[j] for i in Patterns]) - surplusVars[j] >= rollDemand[j],"Ensuring enough %s cm rolls"%j + +# The problem data is written to an .lp file +prob.writeLP("SpongeRollProblem.lp") + +# The problem is solved +prob.solve() + +# The status of the solution is printed to the screen +print "Status:", LpStatus[prob.status] + +# Each of the variables is printed with it's resolved optimum value +for v in prob.variables(): + print v.name, "=", v.varValue + +# The optimised objective function value is printed to the screen +print "Production Costs = ", value(prob.objective) diff --git a/examples/SpongeRollProblem5.py b/examples/SpongeRollProblem5.py new file mode 100644 index 0000000..c4776f6 --- /dev/null +++ b/examples/SpongeRollProblem5.py @@ -0,0 +1,43 @@ +""" +The Sponge Roll Problem with Column Generation for the PuLP Modeller + +Authors: Antony Phillips, Dr Stuart Mitchell 2008 +""" + +# Import Column Generation functions +from CG import * + +# The roll data is created +rollData = {#Length Demand SalePrice + "5": [150, 0.25], + "7": [200, 0.33], + "9": [300, 0.40]} + +# The boolean variable morePatterns is set to True to test for more patterns +morePatterns = True + +# A list of starting patterns is created +patternslist = [[4,0,0],[0,2,0],[0,0,2]] + +# The starting patterns are instantiated with the Pattern class +Patterns = [] +for i in patternslist: + Patterns += [Pattern("P" + str(len(Patterns)), i)] + +# This loop will be repeated until morePatterns is set to False +while morePatterns == True: + + # Solve the problem as a Relaxed LP + duals = masterSolve(Patterns, rollData) + + # Find another pattern + Patterns, morePatterns = subSolve(Patterns, duals) + +# Re-solve as an Integer Problem +solution, varsdict = masterSolve(Patterns, rollData, relax = False) + +# Display Solution +for i,j in varsdict.items(): + print i, "=", j + +print "objective = ", solution \ No newline at end of file diff --git a/examples/SpongeRollProblem6.py b/examples/SpongeRollProblem6.py new file mode 100644 index 0000000..21ed22f --- /dev/null +++ b/examples/SpongeRollProblem6.py @@ -0,0 +1,33 @@ +""" +The Sponge Roll Problem with Columnwise Column Generation for the PuLP Modeller + +Authors: Antony Phillips, Dr Stuart Mitchell 2008 +""" + +# Import Column Generation functions +from CGcolumnwise import * + +# The Master Problem is created +prob, obj, constraints = createMaster() + +# A list of starting patterns is created +newPatterns = [[1,0,0],[0,1,0],[0,0,1]] + +# New patterns will be added until newPatterns is an empty list +while newPatterns: + # The new patterns are added to the problem + addPatterns(obj,constraints,newPatterns) + # The master problem is solved, and the dual variables are returned + duals = masterSolve(prob) + # The sub problem is solved and a new pattern will be returned if there is one + # which can reduce the master objective function + newPatterns = subSolve(duals) + +# The master problem is solved with Integer Constraints not relaxed +solution, varsdict = masterSolve(prob,relax = False) + +# Display Solution +for i,j in varsdict.items(): + print i, "=", j + +print "objective = ", solution \ No newline at end of file diff --git a/examples/Sudoku1.py b/examples/Sudoku1.py new file mode 100644 index 0000000..980debf --- /dev/null +++ b/examples/Sudoku1.py @@ -0,0 +1,111 @@ +""" +The Sudoku Problem Formulation for the PuLP Modeller + +Authors: Antony Phillips, Dr Stuart Mitcehll +""" + +# Import PuLP modeler functions +from pulp import * + +# A list of strings from "1" to "9" is created +Sequence = ["1", "2", "3", "4", "5", "6", "7", "8", "9"] + +# The Vals, Rows and Cols sequences all follow this form +Vals = Sequence +Rows = Sequence +Cols = Sequence + +# The boxes list is created, with the row and column index of each square in each box +Boxes =[] +for i in range(3): + for j in range(3): + Boxes += [[(Rows[3*i+k],Cols[3*j+l]) for k in range(3) for l in range(3)]] + +# The prob variable is created to contain the problem data +prob = LpProblem("Sudoku Problem",LpMinimize) + +# The problem variables are created +choices = LpVariable.dicts("Choice",(Vals,Rows,Cols),0,1,LpInteger) + +# The arbitrary objective function is added +prob += 0, "Arbitrary Objective Function" + +# A constraint ensuring that only one value can be in each square is created +for r in Rows: + for c in Cols: + prob += lpSum([choices[v][r][c] for v in Vals]) == 1, "" + +# The row, column and box constraints are added for each value +for v in Vals: + for r in Rows: + prob += lpSum([choices[v][r][c] for c in Cols]) == 1,"" + + for c in Cols: + prob += lpSum([choices[v][r][c] for r in Rows]) == 1,"" + + for b in Boxes: + prob += lpSum([choices[v][r][c] for (r,c) in b]) == 1,"" + +# The starting numbers are entered as constraints +prob += choices["5"]["1"]["1"] == 1,"" +prob += choices["6"]["2"]["1"] == 1,"" +prob += choices["8"]["4"]["1"] == 1,"" +prob += choices["4"]["5"]["1"] == 1,"" +prob += choices["7"]["6"]["1"] == 1,"" +prob += choices["3"]["1"]["2"] == 1,"" +prob += choices["9"]["3"]["2"] == 1,"" +prob += choices["6"]["7"]["2"] == 1,"" +prob += choices["8"]["3"]["3"] == 1,"" +prob += choices["1"]["2"]["4"] == 1,"" +prob += choices["8"]["5"]["4"] == 1,"" +prob += choices["4"]["8"]["4"] == 1,"" +prob += choices["7"]["1"]["5"] == 1,"" +prob += choices["9"]["2"]["5"] == 1,"" +prob += choices["6"]["4"]["5"] == 1,"" +prob += choices["2"]["6"]["5"] == 1,"" +prob += choices["1"]["8"]["5"] == 1,"" +prob += choices["8"]["9"]["5"] == 1,"" +prob += choices["5"]["2"]["6"] == 1,"" +prob += choices["3"]["5"]["6"] == 1,"" +prob += choices["9"]["8"]["6"] == 1,"" +prob += choices["2"]["7"]["7"] == 1,"" +prob += choices["6"]["3"]["8"] == 1,"" +prob += choices["8"]["7"]["8"] == 1,"" +prob += choices["7"]["9"]["8"] == 1,"" +prob += choices["3"]["4"]["9"] == 1,"" +prob += choices["1"]["5"]["9"] == 1,"" +prob += choices["6"]["6"]["9"] == 1,"" +prob += choices["5"]["8"]["9"] == 1,"" + +# The problem data is written to an .lp file +prob.writeLP("Sudoku.lp") + +# The problem is solved using PuLP's choice of Solver +prob.solve() + +# The status of the solution is printed to the screen +print "Status:", LpStatus[prob.status] + +# A file called sudokuout.txt is created/overwritten for writing to +sudokuout = open('sudokuout.txt','w') + +# The solution is written to the sudokuout.txt file +for r in Rows: + if r == "1" or r == "4" or r == "7": + sudokuout.write("+-------+-------+-------+\n") + for c in Cols: + for v in Vals: + if value(choices[v][r][c])==1: + + if c == "1" or c == "4" or c =="7": + sudokuout.write("| ") + + sudokuout.write(v + " ") + + if c == "9": + sudokuout.write("|\n") +sudokuout.write("+-------+-------+-------+") +sudokuout.close() + +# The location of the solution is give to the user +print "Solution Written to sudokuout.txt" diff --git a/examples/Sudoku2.py b/examples/Sudoku2.py new file mode 100644 index 0000000..a9ef06e --- /dev/null +++ b/examples/Sudoku2.py @@ -0,0 +1,115 @@ +""" +The Looping Sudoku Problem Formulation for the PuLP Modeller + +Authors: Antony Phillips, Dr Stuart Mitcehll +""" +# Import PuLP modeler functions +from pulp import * + +# A list of strings from "1" to "9" is created +Sequence = ["1", "2", "3", "4", "5", "6", "7", "8", "9"] + +# The Vals, Rows and Cols sequences all follow this form +Vals = Sequence +Rows = Sequence +Cols = Sequence + +# The boxes list is created, with the row and column index of each square in each box +Boxes =[] +for i in range(3): + for j in range(3): + Boxes += [[(Rows[3*i+k],Cols[3*j+l]) for k in range(3) for l in range(3)]] + +# The prob variable is created to contain the problem data +prob = LpProblem("Sudoku Problem",LpMinimize) + +# The problem variables are created +choices = LpVariable.dicts("Choice",(Vals,Rows,Cols),0,1,LpInteger) + +# The arbitrary objective function is added +prob += 0, "Arbitrary Objective Function" + +# A constraint ensuring that only one value can be in each square is created +for r in Rows: + for c in Cols: + prob += lpSum([choices[v][r][c] for v in Vals]) == 1, "" + +# The row, column and box constraints are added for each value +for v in Vals: + for r in Rows: + prob += lpSum([choices[v][r][c] for c in Cols]) == 1,"" + + for c in Cols: + prob += lpSum([choices[v][r][c] for r in Rows]) == 1,"" + + for b in Boxes: + prob += lpSum([choices[v][r][c] for (r,c) in b]) == 1,"" + +# The starting numbers are entered as constraints +prob += choices["5"]["1"]["1"] == 1,"" +prob += choices["6"]["2"]["1"] == 1,"" +prob += choices["8"]["4"]["1"] == 1,"" +prob += choices["4"]["5"]["1"] == 1,"" +prob += choices["7"]["6"]["1"] == 1,"" +prob += choices["3"]["1"]["2"] == 1,"" +prob += choices["9"]["3"]["2"] == 1,"" +prob += choices["6"]["7"]["2"] == 1,"" +prob += choices["8"]["3"]["3"] == 1,"" +prob += choices["1"]["2"]["4"] == 1,"" +prob += choices["8"]["5"]["4"] == 1,"" +prob += choices["4"]["8"]["4"] == 1,"" +prob += choices["7"]["1"]["5"] == 1,"" +prob += choices["9"]["2"]["5"] == 1,"" +prob += choices["6"]["4"]["5"] == 1,"" +prob += choices["2"]["6"]["5"] == 1,"" +prob += choices["1"]["8"]["5"] == 1,"" +prob += choices["8"]["9"]["5"] == 1,"" +prob += choices["5"]["2"]["6"] == 1,"" +prob += choices["3"]["5"]["6"] == 1,"" +prob += choices["9"]["8"]["6"] == 1,"" +prob += choices["2"]["7"]["7"] == 1,"" +prob += choices["6"]["3"]["8"] == 1,"" +prob += choices["8"]["7"]["8"] == 1,"" +prob += choices["7"]["9"]["8"] == 1,"" +prob += choices["3"]["4"]["9"] == 1,"" +prob += choices["1"]["5"]["9"] == 1,"" +prob += choices["6"]["6"]["9"] == 1,"" +prob += choices["5"]["8"]["9"] == 1,"" + +# The problem data is written to an .lp file +prob.writeLP("Sudoku.lp") + +# A file called sudokuout.txt is created/overwritten for writing to +sudokuout = open('sudokuout.txt','w') + +while True: + prob.solve() + # The status of the solution is printed to the screen + print "Status:", LpStatus[prob.status] + # The solution is printed if it was deemed "optimal" i.e met the constraints + if LpStatus[prob.status] == "Optimal": + # The solution is written to the sudokuout.txt file + for r in Rows: + if r == "1" or r == "4" or r == "7": + sudokuout.write("+-------+-------+-------+\n") + for c in Cols: + for v in Vals: + if value(choices[v][r][c])==1: + if c == "1" or c == "4" or c =="7": + sudokuout.write("| ") + sudokuout.write(v + " ") + if c == "9": + sudokuout.write("|\n") + sudokuout.write("+-------+-------+-------+\n\n") + # The constraint is added that the same solution cannot be returned again + prob += lpSum([choices[v][r][c] for v in Vals + for r in Rows + for c in Cols + if value(choices[v][r][c])==1]) <= 80 + # If a new optimal solution cannot be found, we end the program + else: + break +sudokuout.close() + +# The location of the solutions is give to the user +print "Solutions Written to sudokuout.txt" diff --git a/examples/WhiskasModel1.py b/examples/WhiskasModel1.py new file mode 100644 index 0000000..6911ca6 --- /dev/null +++ b/examples/WhiskasModel1.py @@ -0,0 +1,41 @@ +""" +The Simplified Whiskas Model Python Formulation for the PuLP Modeller + +Authors: Antony Phillips, Dr Stuart Mitchell 2007 +""" + +# Import PuLP modeler functions +from pulp import * + +# Create the 'prob' variable to contain the problem data +prob = LpProblem("The Whiskas Problem",LpMinimize) + +# The 2 variables Beef and Chicken are created with a lower limit of zero +x1=LpVariable("ChickenPercent",0,None,LpInteger) +x2=LpVariable("BeefPercent",0) + +# The objective function is added to 'prob' first +prob += 0.013*x1 + 0.008*x2, "Total Cost of Ingredients per can" + +# The five constraints are entered +prob += x1 + x2 == 100, "PercentagesSum" +prob += 0.100*x1 + 0.200*x2 >= 8.0, "ProteinRequirement" +prob += 0.080*x1 + 0.100*x2 >= 6.0, "FatRequirement" +prob += 0.001*x1 + 0.005*x2 <= 2.0, "FibreRequirement" +prob += 0.002*x1 + 0.005*x2 <= 0.4, "SaltRequirement" + +# The problem data is written to an .lp file +prob.writeLP("WhiskasModel.lp") + +# The problem is solved using PuLP's choice of Solver +prob.solve() + +# The status of the solution is printed to the screen +print "Status:", LpStatus[prob.status] + +# Each of the variables is printed with it's resolved optimum value +for v in prob.variables(): + print v.name, "=", v.varValue + +# The optimised objective function value is printed to the screen +print "Total Cost of Ingredients per can = ", value(prob.objective) diff --git a/examples/WhiskasModel2.py b/examples/WhiskasModel2.py new file mode 100644 index 0000000..5517a5e --- /dev/null +++ b/examples/WhiskasModel2.py @@ -0,0 +1,83 @@ +""" +The Full Whiskas Model Python Formulation for the PuLP Modeller + +Authors: Antony Phillips, Dr Stuart Mitchell 2007 +""" + +# Import PuLP modeler functions +from pulp import * + +# Creates a list of the Ingredients +Ingredients = ['CHICKEN', 'BEEF', 'MUTTON', 'RICE', 'WHEAT', 'GEL'] + +# A dictionary of the costs of each of the Ingredients is created +costs = {'CHICKEN': 0.013, + 'BEEF': 0.008, + 'MUTTON': 0.010, + 'RICE': 0.002, + 'WHEAT': 0.005, + 'GEL': 0.001} + +# A dictionary of the protein percent in each of the Ingredients is created +proteinPercent = {'CHICKEN': 0.100, + 'BEEF': 0.200, + 'MUTTON': 0.150, + 'RICE': 0.000, + 'WHEAT': 0.040, + 'GEL': 0.000} + +# A dictionary of the fat percent in each of the Ingredients is created +fatPercent = {'CHICKEN': 0.080, + 'BEEF': 0.100, + 'MUTTON': 0.110, + 'RICE': 0.010, + 'WHEAT': 0.010, + 'GEL': 0.000} + +# A dictionary of the fibre percent in each of the Ingredients is created +fibrePercent = {'CHICKEN': 0.001, + 'BEEF': 0.005, + 'MUTTON': 0.003, + 'RICE': 0.100, + 'WHEAT': 0.150, + 'GEL': 0.000} + +# A dictionary of the salt percent in each of the Ingredients is created +saltPercent = {'CHICKEN': 0.002, + 'BEEF': 0.005, + 'MUTTON': 0.007, + 'RICE': 0.002, + 'WHEAT': 0.008, + 'GEL': 0.000} + +# Create the 'prob' variable to contain the problem data +prob = LpProblem("The Whiskas Problem", LpMinimize) + +# A dictionary called 'ingredient_vars' is created to contain the referenced Variables +ingredient_vars = LpVariable.dicts("Ingr",Ingredients,0) + +# The objective function is added to 'prob' first +prob += lpSum([costs[i]*ingredient_vars[i] for i in Ingredients]), "Total Cost of Ingredients per can" + +# The five constraints are added to 'prob' +prob += lpSum([ingredient_vars[i] for i in Ingredients]) == 100, "PercentagesSum" +prob += lpSum([proteinPercent[i] * ingredient_vars[i] for i in Ingredients]) >= 8.0, "ProteinRequirement" +prob += lpSum([fatPercent[i] * ingredient_vars[i] for i in Ingredients]) >= 6.0, "FatRequirement" +prob += lpSum([fibrePercent[i] * ingredient_vars[i] for i in Ingredients]) <= 2.0, "FibreRequirement" +prob += lpSum([saltPercent[i] * ingredient_vars[i] for i in Ingredients]) <= 0.4, "SaltRequirement" + +# The problem data is written to an .lp file +prob.writeLP("WhiskasModel2.lp") + +# The problem is solved using PuLP's choice of Solver +prob.solve() + +# The status of the solution is printed to the screen +print "Status:", LpStatus[prob.status] + +# Each of the variables is printed with it's resolved optimum value +for v in prob.variables(): + print v.name, "=", v.varValue + +# The optimised objective function value is printed to the screen +print "Total Cost of Ingredients per can = ", value(prob.objective) diff --git a/examples/furniture.py b/examples/furniture.py new file mode 100644 index 0000000..9e30255 --- /dev/null +++ b/examples/furniture.py @@ -0,0 +1,31 @@ +""" + The Furniture problem from EngSci391 for the PuLP Modeller + Author: Dr Stuart Mitchell 2007 +""" +from pulp import * +Chairs = ["A","B"] +costs = {"A":100, + "B":150} +Resources = ["Lathe","Polisher"] +capacity = {"Lathe" : 40, + "Polisher" : 48} +activity = [ #Chairs + #A B + [1, 2], #Lathe + [3, 1.5] #Polisher + ] +activity = makeDict([Resources,Chairs],activity) +prob = LpProblem("Furniture Manufacturing Problem", LpMaximize) +vars = LpVariable.dicts("Number of Chairs",Chairs, lowBound = 0) +#objective +prob += lpSum([costs[c]*vars[c] for c in Chairs]) +for r in Resources: + prob += lpSum([activity[r][c]*vars[c] for c in Chairs]) <= capacity[r], \ + "capacity_of_%s"%r +prob.writeLP("furniture.lp") +prob.solve() +# Each of the variables is printed with it's value +for v in prob.variables(): + print v.name, "=", v.varValue +# The optimised objective function value is printed to the screen +print "Total Revenue from Production = ", value(prob.objective) \ No newline at end of file diff --git a/examples/test1.py b/examples/test1.py new file mode 100644 index 0000000..6c57b88 --- /dev/null +++ b/examples/test1.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python +# @(#) $Jeannot: test1.py,v 1.11 2005/01/06 21:22:39 js Exp $ + +# Import PuLP modeler functions +from pulp import * + +# A new LP problem +prob = LpProblem("test1", LpMinimize) + +# Variables +# 0 <= x <= 4 +x = LpVariable("x", 0, 4) +# -1 <= y <= 1 +y = LpVariable("y", -1, 1) +# 0 <= z +z = LpVariable("z", 0) +# Use None for +/- Infinity, i.e. z <= 0 -> LpVariable("z", None, 0) + +# Objective +prob += x + 4*y + 9*z, "obj" +# (the name at the end is facultative) + +# Constraints +prob += x+y <= 5, "c1" +prob += x+z >= 10, "c2" +prob += -y+z == 7, "c3" +# (the names at the end are facultative) + +# Write the problem as an LP file +prob.writeLP("test1.lp") + +# Solve the problem using the default solver +prob.solve() +# Use prob.solve(GLPK()) instead to choose GLPK as the solver +# Use GLPK(msg = 0) to suppress GLPK messages +# If GLPK is not in your path and you lack the pulpGLPK module, +# replace GLPK() with GLPK("/path/") +# Where /path/ is the path to glpsol (excluding glpsol itself). +# If you want to use CPLEX, use CPLEX() instead of GLPK(). +# If you want to use XPRESS, use XPRESS() instead of GLPK(). +# If you want to use COIN, use COIN() instead of GLPK(). In this last case, +# two paths may be provided (one to clp, one to cbc). + +# Print the status of the solved LP +print "Status:", LpStatus[prob.status] + +# Print the value of the variables at the optimum +for v in prob.variables(): + print v.name, "=", v.varValue + +# Print the value of the objective +print "objective=", value(prob.objective) diff --git a/examples/test2.py b/examples/test2.py new file mode 100644 index 0000000..66d3fa3 --- /dev/null +++ b/examples/test2.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python +# @(#) $Jeannot: test2.py,v 1.16 2004/03/20 17:06:54 js Exp $ + +# This example is a PuLP rendition of the todd.mod problem included in the GLPK +# 4.4 distribution. It's a hard knapsack problem. + +# Import PuLP modeler functions +from pulp import * + +# Import math functions +from math import * + +# A new LP problem +prob = LpProblem("test2", LpMaximize) + +# Parameters +# Size of the problem +n = 15 +k = floor(log(n)/log(2)); + +# A vector of n binary variables +x = LpVariable.matrix("x", range(n), 0, 1, LpInteger) + +# A vector of weights +a = [pow(2,k + n + 1) + pow(2,k + n + 1 - j) + 1 for j in range(1,n+1)] +# The maximum weight +b = 0.5 * floor(sum(a)) + +# The total weight +weight = lpDot(a, x) + +# Objective +prob += weight +# Constraint +prob += weight <= b + +# Resolution +prob.solve() + +# Print the status of the solved LP +print "Status:", LpStatus[prob.status] + +# Print the value of the variables at the optimum +for v in prob.variables(): + print v.name, "=", v.varValue + +# Print the value of the objective +print "objective=", value(prob.objective) diff --git a/examples/test3.py b/examples/test3.py new file mode 100644 index 0000000..666871b --- /dev/null +++ b/examples/test3.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python +# @(#) $Jeannot: test3.py,v 1.3 2004/03/20 17:06:54 js Exp $ + +# Deterministic generation planning using mixed integer linear programming. + +# The goal is to minimise the cost of generation while satisfaying demand +# using a few thermal units and an hydro unit. +# The thermal units have a proportional cost and a startup cost. +# The hydro unit has an initial storage. + +from pulp import * +from math import * + +prob = LpProblem("test3", LpMinimize) + +# The number of time steps +tmax = 9 +# The number of thermal units +units = 5 +# The minimum demand +dmin = 10.0 +# The maximum demand +dmax = 150.0 +# The maximum thermal production +tpmax = 150.0 +# The maximum hydro production +hpmax = 100.0 +# Initial hydro storage +sini = 50.0 + +# Time range +time = range(tmax) +# Time range (and one more step for the last state of plants) +xtime = range(tmax+1) +# Units range +unit = range(units) +# The demand +demand = [dmin+(dmax-dmin)*0.5 + 0.5*(dmax-dmin)*sin(4*t*2*3.1415/tmax) for t in time] +# Maximum output for the thermal units +pmax = [tpmax / units for i in unit] +# Minimum output for the thermal units +pmin = [tpmax / (units*3.0) for i in unit] +# Proportional cost of the thermal units +costs = [i+1 for i in unit] +# Startup cost of the thermal units. +startupcosts = [100*(i+1) for i in unit] + +# Production variables for each time step and each thermal unit. +p = LpVariable.matrix("p", (time, unit), 0) +for t in time: + for i in unit: + p[t][i].upBound = pmax[i] + +# State (started/stopped) variables for each time step and each thermal unit +d = LpVariable.matrix("d", (xtime, unit), 0, 1, LpInteger) + +# Production constraint relative to the unit state (started/stoped) +for t in time: + for i in unit: + # If the unit is not started (d==0) then p<=0 else p<=pmax + prob += p[t][i] <= pmax[i]*d[t][i] + # If the unit is not started then p>=0 else p>= pmin + prob += p[t][i] >= pmin[i]*d[t][i] + +# Startup variables: 1 if the unit will be started next time step +u = LpVariable.matrix("u", (time, unit), 0) + +# Dynamic startup constraints +# Initialy, all groups are started +for t in time: + for i in unit: + # u>=1 if the unit is started next time step + prob += u[t][i] >= d[t+1][i] - d[t][i] + +# Storage for the hydro plant (must not go below 0) +s = LpVariable.matrix("s", xtime, 0) + +# Initial storage +s[0] = sini + +# Hydro production +ph = [s[t]-s[t+1] for t in time] +for t in time: + # Must be positive (no pumping) + prob += ph[t] >= 0 + # And lower than hpmax + prob += ph[t] <= hpmax + +# Total production must equal demand +for t in time: + prob += demand[t] == lpSum(p[t]) + ph[t] + +# Thermal production cost +ctp = lpSum([lpSum([p[t][i] for t in time])*costs[i] for i in unit]) +# Startup costs +cts = lpSum([lpSum([u[t][i] for t in time])*startupcosts[i] for i in unit]) +# The objective is the total cost +prob += ctp + cts + +# Solve the problem +prob.solve() + +print "Minimum total cost:", prob.objective.value() + +# Print the results +print " D S U ", +for i in unit: print " T%d " %i, +print + +for t in time: + # Demand, hydro storage, hydro production + print "%5.1f" % demand[t], "%5.1f" % value(s[t]), "%5.1f" % value(ph[t]), + for i in unit: + # Thermal production + print "%4.1f" % value(p[t][i]), + # The state of the unit + if value(d[t][i]): print "+", + else: print "-", + # Wether the unit will be started + if value(u[t][i]): print "*", + else: print " ", + print diff --git a/examples/test4.py b/examples/test4.py new file mode 100644 index 0000000..b8a85c4 --- /dev/null +++ b/examples/test4.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python +# @(#) $Jeannot: test4.py,v 1.5 2004/03/20 17:06:54 js Exp $ + +# A two stage stochastic planification problem + +# Example taken from: +# "On Optimal Allocation of Indivisibles under Incertainty" +# Vladimir I. Norkin, Yuri M. Ermoliev, Andrzej Ruszczynski +# IIASA, WP-94-021, April 1994 (revised October 1995). + +from pulp import * +from random import * + +C = 50 +B = 500 # Resources available for the two years +s = 20 # Number of scenarios +n = 10 # Number of projects + +N = range(n) +S = range(s) + +# First year costs +c = [randint(0,C) for i in N] +# First year resources +d = [randint(0,C) for i in N] +# a=debut, b=taille +interval = [[(randint(0,C), randint(0,C)) for i in N] for j in S] +# Final earnings +q = [[randint(ai, ai+bi) for ai,bi in ab] for ab in interval] +# Second year resources +delta = [[randint(ai, ai+bi) for ai,bi in ab] for ab in interval] + +# Variables +# x : Whether or not to start a project +x = LpVariable.matrix("x", (N,), 0, 1, LpInteger) +# y : Whether or not to finish it, in each scenario +y = LpVariable.matrix("y", (S, N), 0, 1, LpInteger) + +# Problem +lp = LpProblem("Planification", LpMinimize) + +# Objective: expected earnings +lp += lpDot(x, c) - lpDot(q, y)/float(s) + +# Resources constraints for each scenario +for j in S: + lp += lpDot(d, x) + lpDot(delta[j], y[j]) <= B + +# We can only finish a project that was started +for i in N: + for j in S: + lp += y[j][i] <= x[i] + +# Resolution +lp.solve() + +# Solution printing +for i in N: + print x[i], "=", x[i].value() diff --git a/examples/test5.py b/examples/test5.py new file mode 100644 index 0000000..51a2f46 --- /dev/null +++ b/examples/test5.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python +# @(#) $Jeannot: test5.py,v 1.2 2004/03/20 17:06:54 js Exp $ + +# Market splitting problems from: +# G. Cornuejols, M. Dawande, A class of hard small 0-1 programs, 1998. + +# With m>=4, these problems are often *very* difficult. + +# Import PuLP modeler functions +from pulp import * + +# Import random number generation functions +from random import * + +# A new LP problem +prob = LpProblem("test5", LpMinimize) + +# Parameters +# Number of constraints +m = 3 +# Size of the integers involved +D = 100 + +# Number of variables +n = 10*(m-1) + +# A vector of n binary variables +x = LpVariable.matrix("x", range(n), 0, 1, LpInteger) + +# Slacks +s = LpVariable.matrix("s", range(m), 0) +w = LpVariable.matrix("w", range(m), 0) + +# Objective +prob += lpSum(s) + lpSum(w) + +# Constraints +d = [[randint(0,D) for i in range(n)] for j in range(m)] +for j in range(m): + prob += lpDot(d[j], x) + s[j] - w[j] == lpSum(d[j])/2 + +# Resolution +prob.solve() + +# Print the status of the solved LP +print "Status:", LpStatus[prob.status] + +# Print the value of the variables at the optimum +for v in prob.variables(): + print v.name, "=", v.varValue + +# Print the value of the objective +print "objective=", value(prob.objective) diff --git a/examples/test6.py b/examples/test6.py new file mode 100644 index 0000000..467008f --- /dev/null +++ b/examples/test6.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +# @(#) $Jeannot: test1.py,v 1.11 2005/01/06 21:22:39 js Exp $ +# Copywrite 2007 Stuart Mitchell +# Columnwise modelling + +# Import PuLP modeler functions +from pulp import * + +# A new LP problem +prob = LpProblem("test6", LpMinimize) + +# objective +obj = LpConstraintVar("obj") + +# constraints + +a = LpConstraintVar("Ca", LpConstraintLE, 5) + +b = LpConstraintVar("Cb", LpConstraintGE, 10) + +c = LpConstraintVar("Cc", LpConstraintEQ, 7) + +prob.setObjective(obj) +prob += a +prob += b +prob += c + +# Variables +# 0 <= x <= 4 +x = LpVariable("x", 0, 4, LpContinuous, obj + a + b) +# -1 <= y <= 1 +y = LpVariable("y", -1, 1, LpContinuous, 4*obj + a - c) +# 0 <= z +z = LpVariable("z", 0, None, LpContinuous, 9*obj + b + c) +# Use None for +/- Infinity, i.e. z <= 0 -> LpVariable("z", None, 0) + + +# Write the problem as an LP file +prob.writeLP("test6.lp") + +# Solve the problem using the default solver +prob.solve() +# Use prob.solve(GLPK()) instead to choose GLPK as the solver +# Use GLPK(msg = 0) to suppress GLPK messages +# If GLPK is not in your path and you lack the pulpGLPK module, +# replace GLPK() with GLPK("/path/") +# Where /path/ is the path to glpsol (excluding glpsol itself). +# If you want to use CPLEX, use CPLEX() instead of GLPK(). +# If you want to use XPRESS, use XPRESS() instead of GLPK(). +# If you want to use COIN, use COIN() instead of GLPK(). In this last case, +# two paths may be provided (one to clp, one to cbc). + +# Print the status of the solved LP +print "Status:", LpStatus[prob.status] + +# Print the value of the variables at the optimum +for v in prob.variables(): + print v.name, "=", v.varValue + +# Print the value of the objective +print "objective=", value(prob.objective) \ No newline at end of file diff --git a/examples/test7.py b/examples/test7.py new file mode 100644 index 0000000..e03d4b7 --- /dev/null +++ b/examples/test7.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +# Test for output of dual variables + +# Import PuLP modeler functions +from pulp import * + +# A new LP problem +prob = LpProblem("test7", LpMinimize) + +x = LpVariable("x", 0, 4) + +y = LpVariable("y", -1, 1) + +z = LpVariable("z", 0) + +prob += x + 4*y + 9*z, "obj" + +prob += x + y <= 5, "c1" +prob += x + z >= 10,"c2" +prob += -y+ z == 7,"c3" + +prob.writeLP("test7.lp") + +prob.solve() + +print "Status:", LpStatus[prob.status] + +for v in prob.variables(): + print v.name, "=", v.varValue, "\tReduced Cost =", v.dj + +print "objective=", value(prob.objective) + +print "\nSensitivity Analysis\nConstraint\t\tShadow Price\tSlack" +for name, c in prob.constraints.items(): + print name, ":", c, "\t", c.pi, "\t\t", c.slack diff --git a/examples/wedding.py b/examples/wedding.py new file mode 100644 index 0000000..7417b60 --- /dev/null +++ b/examples/wedding.py @@ -0,0 +1,50 @@ +""" +A set partitioning model of a wedding seating problem + +Authors: Stuart Mitchell 2009 +""" + +import pulp + +max_tables = 5 +max_table_size = 4 +guests = 'A B C D E F G I J K L M N O P Q R'.split() + +def happiness(table): + """ + Find the happiness of the table + - by calculating the maximum distance between the letters + """ + return abs(ord(table[0]) - ord(table[-1])) + +#create list of all possible tables +possible_tables = [tuple(c) for c in pulp.allcombinations(guests, + max_table_size)] + +#create a binary variable to state that a table setting is used +x = pulp.LpVariable.dicts('table', possible_tables, + lowBound = 0, + upBound = 1, + cat = pulp.LpInteger) + +seating_model = pulp.LpProblem("Wedding Seating Model", pulp.LpMinimize) + +seating_model += sum([happiness(table) * x[table] for table in possible_tables]) + +#specify the maximum number of tables +seating_model += sum([x[table] for table in possible_tables]) <= max_tables, \ + "Maximum_number_of_tables" + +#A guest must seated at one and only one table +for guest in guests: + seating_model += sum([x[table] for table in possible_tables + if guest in table]) == 1, "Must_seat_%s"%guest + +seating_model.solve() + +print "The choosen tables are out of a total of %s:"%len(possible_tables) +for table in possible_tables: + if x[table].value() == 1.0: + print table + + diff --git a/ez_setup.py b/ez_setup.py new file mode 100644 index 0000000..e14ac7e --- /dev/null +++ b/ez_setup.py @@ -0,0 +1,278 @@ +#!python +"""Bootstrap setuptools installation + +If you want to use setuptools in your package's setup.py, just include this +file in the same directory with it, and add this to the top of your setup.py:: + + from ez_setup import use_setuptools + use_setuptools() + +If you want to require a specific version of setuptools, set a download +mirror, or use an alternate download directory, you can do so by supplying +the appropriate options to ``use_setuptools()``. + +This file can also be run as a script to install or upgrade setuptools. +""" +import sys +DEFAULT_VERSION = "0.6c11" +DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3] + +md5_data = { + 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca', + 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb', + 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b', + 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a', + 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618', + 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac', + 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5', + 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4', + 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c', + 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b', + 'setuptools-0.6c10-py2.3.egg': 'ce1e2ab5d3a0256456d9fc13800a7090', + 'setuptools-0.6c10-py2.4.egg': '57d6d9d6e9b80772c59a53a8433a5dd4', + 'setuptools-0.6c10-py2.5.egg': 'de46ac8b1c97c895572e5e8596aeb8c7', + 'setuptools-0.6c10-py2.6.egg': '58ea40aef06da02ce641495523a0b7f5', + 'setuptools-0.6c11-py2.3.egg': '2baeac6e13d414a9d28e7ba5b5a596de', + 'setuptools-0.6c11-py2.4.egg': 'bd639f9b0eac4c42497034dec2ec0c2b', + 'setuptools-0.6c11-py2.5.egg': '64c94f3bf7a72a13ec83e0b24f2749b2', + 'setuptools-0.6c11-py2.6.egg': 'bfa92100bd772d5a213eedd356d64086', + 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27', + 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277', + 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa', + 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e', + 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e', + 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f', + 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2', + 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc', + 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167', + 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64', + 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d', + 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20', + 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab', + 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53', + 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2', + 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e', + 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372', + 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902', + 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de', + 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b', + 'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03', + 'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a', + 'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6', + 'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a', +} + +import sys, os +try: from hashlib import md5 +except ImportError: from md5 import md5 + +def _validate_md5(egg_name, data): + if egg_name in md5_data: + digest = md5(data).hexdigest() + if digest != md5_data[egg_name]: + print >>sys.stderr, ( + "md5 validation of %s failed! (Possible download problem?)" + % egg_name + ) + sys.exit(2) + return data + +def use_setuptools( + version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, + download_delay=15 +): + """Automatically find/download setuptools and make it available on sys.path + + `version` should be a valid setuptools version number that is available + as an egg for download under the `download_base` URL (which should end with + a '/'). `to_dir` is the directory where setuptools will be downloaded, if + it is not already available. If `download_delay` is specified, it should + be the number of seconds that will be paused before initiating a download, + should one be required. If an older version of setuptools is installed, + this routine will print a message to ``sys.stderr`` and raise SystemExit in + an attempt to abort the calling script. + """ + was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules + def do_download(): + egg = download_setuptools(version, download_base, to_dir, download_delay) + sys.path.insert(0, egg) + import setuptools; setuptools.bootstrap_install_from = egg + try: + import pkg_resources + except ImportError: + return do_download() + try: + pkg_resources.require("setuptools>="+version); return + except pkg_resources.VersionConflict, e: + if was_imported: + print >>sys.stderr, ( + "The required version of setuptools (>=%s) is not available, and\n" + "can't be installed while this script is running. Please install\n" + " a more recent version first, using 'easy_install -U setuptools'." + "\n\n(Currently using %r)" + ) % (version, e.args[0]) + sys.exit(2) + else: + del pkg_resources, sys.modules['pkg_resources'] # reload ok + return do_download() + except pkg_resources.DistributionNotFound: + return do_download() + +def download_setuptools( + version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, + delay = 15 +): + """Download setuptools from a specified location and return its filename + + `version` should be a valid setuptools version number that is available + as an egg for download under the `download_base` URL (which should end + with a '/'). `to_dir` is the directory where the egg will be downloaded. + `delay` is the number of seconds to pause before an actual download attempt. + """ + import urllib2, shutil + egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3]) + url = download_base + egg_name + saveto = os.path.join(to_dir, egg_name) + src = dst = None + if not os.path.exists(saveto): # Avoid repeated downloads + try: + from distutils import log + if delay: + log.warn(""" +--------------------------------------------------------------------------- +This script requires setuptools version %s to run (even to display +help). I will attempt to download it for you (from +%s), but +you may need to enable firewall access for this script first. +I will start the download in %d seconds. + +(Note: if this machine does not have network access, please obtain the file + + %s + +and place it in this directory before rerunning this script.) +---------------------------------------------------------------------------""", + version, download_base, delay, url + ); from time import sleep; sleep(delay) + log.warn("Downloading %s", url) + src = urllib2.urlopen(url) + # Read/write all in one block, so we don't create a corrupt file + # if the download is interrupted. + data = _validate_md5(egg_name, src.read()) + dst = open(saveto,"wb"); dst.write(data) + finally: + if src: src.close() + if dst: dst.close() + return os.path.realpath(saveto) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +def main(argv, version=DEFAULT_VERSION): + """Install or upgrade setuptools and EasyInstall""" + try: + import setuptools + except ImportError: + egg = None + try: + egg = download_setuptools(version, delay=0) + sys.path.insert(0,egg) + from setuptools.command.easy_install import main + return main(list(argv)+[egg]) # we're done here + finally: + if egg and os.path.exists(egg): + os.unlink(egg) + else: + if setuptools.__version__ == '0.0.1': + print >>sys.stderr, ( + "You have an obsolete version of setuptools installed. Please\n" + "remove it from your system entirely before rerunning this script." + ) + sys.exit(2) + + req = "setuptools>="+version + import pkg_resources + try: + pkg_resources.require(req) + except pkg_resources.VersionConflict: + try: + from setuptools.command.easy_install import main + except ImportError: + from easy_install import main + main(list(argv)+[download_setuptools(delay=0)]) + sys.exit(0) # try to force an exit + else: + if argv: + from setuptools.command.easy_install import main + main(argv) + else: + print "Setuptools version",version,"or greater has been installed." + print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)' + +def update_md5(filenames): + """Update our built-in md5 registry""" + + import re + + for name in filenames: + base = os.path.basename(name) + f = open(name,'rb') + md5_data[base] = md5(f.read()).hexdigest() + f.close() + + data = [" %r: %r,\n" % it for it in md5_data.items()] + data.sort() + repl = "".join(data) + + import inspect + srcfile = inspect.getsourcefile(sys.modules[__name__]) + f = open(srcfile, 'rb'); src = f.read(); f.close() + + match = re.search("\nmd5_data = {\n([^}]+)}", src) + if not match: + print >>sys.stderr, "Internal error!" + sys.exit(2) + + src = src[:match.start(1)] + repl + src[match.end(1):] + f = open(srcfile,'w') + f.write(src) + f.close() + + +if __name__=='__main__': + if len(sys.argv)>2 and sys.argv[1]=='--md5update': + update_md5(sys.argv[2:]) + else: + main(sys.argv[1:]) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..f6c68c6 --- /dev/null +++ b/setup.py @@ -0,0 +1,55 @@ +#!/usr/bin/env/python +""" +Setup script for PuLP added by Stuart Mitchell 2007 +Copyright 2007 Stuart Mitchell +""" +from ez_setup import use_setuptools +use_setuptools() +from setuptools import setup + +Description = open('README').read() + +License = open('LICENSE').read() + +Version = open('VERSION').read().strip() + +setup(name="PuLP", + version=Version, + description=""" +PuLP is an LP modeler written in python. PuLP can generate MPS or LP files +and call GLPK, COIN CLP/CBC, CPLEX, and GUROBI to solve linear +problems. +""", + long_description = Description, + license = License, + keywords = ["Optimization", "Linear Programming", "Operations Research"], + author="J.S. Roy and S.A. Mitchell", + author_email="s.mitchell@auckland.ac.nz", + url="http://pulp-or.googlecode.com/", + classifiers = ['Development Status :: 5 - Production/Stable', + 'Environment :: Console', + 'Intended Audience :: Science/Research', + 'License :: OSI Approved :: BSD License', + 'Natural Language :: English', + 'Programming Language :: Python', + 'Topic :: Scientific/Engineering :: Mathematics', + ], + #ext_modules = [pulpCOIN], + package_dir={'':'src'}, + packages = ['pulp', 'pulp.solverdir'], + package_data = {'pulp' : ["AUTHORS","LICENSE", + "pulp.cfg.linux", + "pulp.cfg.win", + "LICENSE.CoinMP.txt", + "AUTHORS.CoinMP.txt", + "README.CoinMP.txt", + ], + 'pulp.solverdir' : ['*','*.*']}, + install_requires = ['pyparsing>=1.5.2'], + entry_points = (""" + [console_scripts] + pulptest = pulp:pulpTestAll + pulpdoctest = pulp:pulpDoctest + """ + ), +) diff --git a/solvers.cfg b/solvers.cfg new file mode 100644 index 0000000..d425f3c --- /dev/null +++ b/solvers.cfg @@ -0,0 +1,21 @@ +[buildout] +extends = buildout.cfg + +[glpk] +recipe = zc.recipe.cmmi +url = http://ftp.gnu.org/gnu/glpk/glpk-4.42.tar.gz + +[cbc] +recipe = zc.recipe.cmmi +url = http://www.coin-or.org/download/source/Cbc/Cbc-2.4.0.tgz + +[coinMP] +recipe = zc.recipe.cmmi +#url = http://pulp-or.googlecode.com/files/CoinMP-1.4.0patched.tar.gz +url = http://www.coin-or.org/download/source/CoinMP/CoinMP-1.4.0.tgz +#patch = ${buildout:directory}/patches/CoinMP-1.4.0.patch + +[install-coinMP] +recipe = plone.recipe.command +command = cp ${buildout:directory}/parts/coinMP/lib/* ${buildout:directory}/src/pulp/solverdir/ +update-command = cp ${buildout:directory}/parts/coinMP/lib/* ${buildout:directory}/src/pulp/solverdir/ diff --git a/src/pulp/__init__.py b/src/pulp/__init__.py new file mode 100644 index 0000000..a92374b --- /dev/null +++ b/src/pulp/__init__.py @@ -0,0 +1,37 @@ +# PuLP : Python LP Modeler +# Version 1.20 + +# Copyright (c) 2002-2005, Jean-Sebastien Roy (js@jeannot.org) +# Modifications Copyright (c) 2007- Stuart Anthony Mitchell (s.mitchell@auckland.ac.nz) +# $Id: __init__.py 1791 2008-04-23 22:54:34Z smit023 $ + +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: + +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +Module file that imports all of the pulp functions + +Copyright 2007- Stuart Mitchell (s.mitchell@auckland.ac.nz) +""" + +from pulp import * +from amply import * +__doc__ = pulp.__doc__ + +import tests diff --git a/src/pulp/amply.py b/src/pulp/amply.py new file mode 100644 index 0000000..b159419 --- /dev/null +++ b/src/pulp/amply.py @@ -0,0 +1,782 @@ +#! /usr/bin/env python +# Amply: a GNU MathProg data-parser + +# Copyright (c) 2010, Q. Lim (qlim001@aucklanduni.ac.nz) + +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: + +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +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 + >>> print a['T'] + 3 + + 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. +""" +try: + import pyparsing +except ImportError: + pass +else: + from pyparsing import alphas, nums, alphanums, delimitedList, oneOf + from pyparsing import Combine, Dict, Forward, Group, Literal, NotAny + from pyparsing import OneOrMore, Optional, ParseResults, QuotedString + from pyparsing import StringEnd, Suppress, Word, ZeroOrMore + + from itertools import chain + + __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 = data.keys() + cols = set() + for d in data.values(): + cols.update(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) + + 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: + assert len(self.records) == 1 + 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 = self.it.next() + 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[1] + 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 + num_subscripts = sum(_getDimen(s) for s in self.subscripts) + 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 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 == None: + # infer dimension from records + self.dimen = inferred_dimen + + if self.current_slice == 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 + + symbol = Word(alphas, alphanums + "_") + 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('-') + + single = number | symbol | QuotedString('"') | QuotedString("'") + tuple_ = Group(LPAREN + delimitedList(single) + RPAREN) + subscript_domain = LBRACE + Group(delimitedList(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 = ~Literal('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 = "set" + symbol + Optional(subscript_domain) + \ + Optional("dimen" + integer.setResultsName('dimen')) + END + set_def_stmt.setParseAction(SetDefStmt) + + set_member = LBRACKET + delimitedList(data) + RBRACKET + + set_stmt = "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("default" + data.setResultsName('default')) + + param_stmt = "param" + symbol.setResultsName('name') + param_default + \ + Group(OneOrMore(param_record)).setResultsName('records') + END + param_stmt.setParseAction(ParamStmt) + + param_tabbing_stmt = "param" + param_default + ':' + Optional(symbol + ':') + \ + OneOrMore(data).setResultsName('params') \ + + ':=' + OneOrMore(single).setResultsName('data') + END + param_tabbing_stmt.setParseAction(ParamTabbingStmt) + + param_def_stmt = "param" + symbol + Optional(subscript_domain) + \ + param_default + END + param_def_stmt.setParseAction(ParamDefStmt) + + stmts = set_stmt | set_def_stmt | param_stmt | param_def_stmt | \ + param_tabbing_stmt + grammar = ZeroOrMore(stmts) + StringEnd() + + + 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 accesed 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 + """ + for obj in grammar.parseString(string): + obj.eval(self) + + 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()) + diff --git a/src/pulp/constants.py b/src/pulp/constants.py new file mode 100644 index 0000000..5ee56de --- /dev/null +++ b/src/pulp/constants.py @@ -0,0 +1,77 @@ +# PuLP : Python LP Modeler +# Version 1.20 + +# Copyright (c) 2002-2005, Jean-Sebastien Roy (js@jeannot.org) +# Modifications Copyright (c) 2007- Stuart Anthony Mitchell (s.mitchell@auckland.ac.nz) +# $Id:constants.py 1791 2008-04-23 22:54:34Z smit023 $ + +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: + +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.""" + +""" +This file contains the constant definitions for PuLP +Note that hopefully these will be changed into something more pythonic +""" + +EPS = 1e-7 + +# variable categories +LpContinuous = "Continuous" +LpInteger = "Integer" +LpBinary = "Binary" +LpCategories = {LpContinuous: "Continuous", LpInteger: "Integer", + LpBinary: "Binary"} + +# objective sense +LpMinimize = 1 +LpMaximize = -1 +LpSenses = {LpMaximize:"Maximize", LpMinimize:"Minimize"} + +# problem status +LpStatusNotSolved = 0 +LpStatusOptimal = 1 +LpStatusInfeasible = -1 +LpStatusUnbounded = -2 +LpStatusUndefined = -3 +LpStatus = { LpStatusNotSolved:"Not Solved", + LpStatusOptimal:"Optimal", + LpStatusInfeasible:"Infeasible", + LpStatusUnbounded:"Unbounded", + LpStatusUndefined:"Undefined", + } + +# constraint sense +LpConstraintLE = -1 +LpConstraintEQ = 0 +LpConstraintGE = 1 +LpConstraintSenses = {LpConstraintEQ:"=", LpConstraintLE:"<=", LpConstraintGE:">="} + +# LP line size +LpCplexLPLineSize = 78 + +def isiterable(obj): + try: obj=iter(obj) + except: return False + else: return True + +class PulpError(Exception): + """ + Pulp Exception Class + """ + pass diff --git a/src/pulp/pulp.cfg.buildout b/src/pulp/pulp.cfg.buildout new file mode 100644 index 0000000..dcadc05 --- /dev/null +++ b/src/pulp/pulp.cfg.buildout @@ -0,0 +1,15 @@ +# The configuration file that holds locations for 3rd party solvers +# This is an appropriate configuration file for linux uses and in this case is assuming that the +# needed libraries are in the same directory as the config file (note this is not ideal and +# may change in later versions) +# Libraries are ordered in the needed order to resolve dependancies and CoinMP is loaded last +# a windows specific configuation file is pulp.cfg.win +#$Id: pulp.cfg 1704 2007-12-20 21:56:14Z smit023 $ +[locations] +CoinMPPath = %(here)s/../../parts/coinMP/lib/libCoinUtils.so, %(here)s/../../parts/coinMP/lib/libClp.so, %(here)s/../../parts/coinMP/lib/libOsi.so, %(here)s/../../parts/coinMP/lib/libOsiClp.so, %(here)s/../../parts/coinMP/lib/libCgl.so, %(here)s/../../parts/coinMP/lib/libCbc.so, %(here)s/../../parts/coinMP/lib/libOsiCbc.so, %(here)s/../../parts/coinMP/lib/libCbcSolver.so, %(here)s/../../parts/coinMP/lib/libCoinMP.so +CplexPath = /usr/ilog/cplex/bin/x86_rhel4.0_3.4/libcplex110.so +#note the recommended gurobi changes must be made to your PATH +#and LD_LIBRARY_PATH enviroment variables +GurobiPath = /opt/gurobi201/linux32/lib/python2.5 +CbcPath = %(here)s/../../parts/cbc/bin/cbc +GlpkPath = %(here)s/../../parts/glpk/bin/glpsol diff --git a/src/pulp/pulp.cfg.linux b/src/pulp/pulp.cfg.linux new file mode 100644 index 0000000..0d337c8 --- /dev/null +++ b/src/pulp/pulp.cfg.linux @@ -0,0 +1,18 @@ +# The configuration file that holds locations for 3rd party solvers +# This is an appropriate configuration file for linux uses and in this case is assuming that the +# needed libraries are in the same directory as the config file (note this is not ideal and +# may change in later versions) +# Libraries are ordered in the needed order to resolve dependancies and CoinMP is loaded last +# a windows specific configuation file is pulp.cfg.win +#$Id: pulp.cfg 1704 2007-12-20 21:56:14Z smit023 $ +[locations] +CplexPath = /usr/ilog/cplex/bin/x86_rhel4.0_3.4/libcplex110.so +#note the recommended gurobi changes must be made to your PATH +#and LD_LIBRARY_PATH environment variables +GurobiPath = /opt/gurobi201/linux32/lib/python2.5 +CbcPath = cbc +GlpkPath = glpsol +PulpCbcPath = %(here)s/solverdir/cbc +[licenses] +ilm_cplex_license = "LICENSE your-enterprise\nRUNTIME NEVER ..." +ilm_cplex_license_signature = 0 diff --git a/src/pulp/pulp.cfg.win b/src/pulp/pulp.cfg.win new file mode 100644 index 0000000..afd3a5b --- /dev/null +++ b/src/pulp/pulp.cfg.win @@ -0,0 +1,16 @@ +# The configuration file that holds locations for 3rd party solvers +# for CoinMp.dll it is assumed that no location will place the file in the +# same directory as the configuration file. (note this is not ideal and +# may change in later versions) +# a linux file is provided in pulp.cfg.linux +#$Id: pulp.cfg 1704 2007-12-20 21:56:14Z smit023 $ +[locations] +CplexPath = cplex100.dll +#must have gurobi.dll somewhere on the search path +GurobiPath = C:\gurobi10\win32\python2.5\lib +CbcPath = cbc +GlpkPath = glpsol +PulpCbcPath = %(here)s\solverdir\cbc.exe +[licenses] +ilm_cplex_license = "LICENSE your-enterprise\nRUNTIME NEVER ..." +ilm_cplex_license_signature = 0 diff --git a/src/pulp/pulp.py b/src/pulp/pulp.py new file mode 100755 index 0000000..9ba9afb --- /dev/null +++ b/src/pulp/pulp.py @@ -0,0 +1,2255 @@ +#! /usr/bin/env python +# PuLP : Python LP Modeler +# Version 1.5.1 + +# Copyright (c) 2002-2005, Jean-Sebastien Roy (js@jeannot.org) +# Modifications Copyright (c) 2007- Stuart Anthony Mitchell (s.mitchell@auckland.ac.nz) +# $Id: pulp.py 1791 2008-04-23 22:54:34Z smit023 $ + +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: + +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +PuLP is an LP modeler written in python. PuLP can generate MPS or LP files +and call GLPK[1], COIN CLP/CBC[2], CPLEX[3], and GUROBI[4] to solve linear +problems. + +See the examples directory for examples. + +PuLP requires Python >= 2.5. + +The examples require at least a solver in your PATH or a shared library file. + +Documentation is found on https://www.coin-or.org/PuLP/. +A comprehensive wiki can be found at https://www.coin-or.org/PuLP/ + +Use LpVariable() to create new variables. To create a variable 0 <= x <= 3 +>>> x = LpVariable("x", 0, 3) + +To create a variable 0 <= y <= 1 +>>> y = LpVariable("y", 0, 1) + +Use LpProblem() to create new problems. Create "myProblem" +>>> prob = LpProblem("myProblem", LpMinimize) + +Combine variables to create expressions and constraints and add them to the +problem. +>>> prob += x + y <= 2 + +If you add an expression (not a constraint), it will +become the objective. +>>> prob += -4*x + y + +Choose a solver and solve the problem. ex: +>>> status = prob.solve(GLPK(msg = 0)) + +Display the status of the solution +>>> LpStatus[status] +'Optimal' + +You can get the value of the variables using value(). ex: +>>> value(x) +2.0 + +Exported Classes: + - LpProblem -- Container class for a Linear programming problem + - LpVariable -- Variables that are added to constraints in the LP + - LpConstraint -- A constraint of the general form + a1x1+a2x2 ...anxn (<=, =, >=) b + - LpConstraintVar -- Used to construct a column of the model in column-wise + modelling + +Exported Functions: + - value() -- Finds the value of a variable or expression + - lpSum() -- given a list of the form [a1*x1, a2x2, ..., anxn] will construct + a linear expression to be used as a constraint or variable + - lpDot() --given two lists of the form [a1, a2, ..., an] and + [ x1, x2, ..., xn] will construct a linear epression to be used + as a constraint or variable + +Comments, bug reports, patches and suggestions are welcome. +pulp-or-discuss@googlegroups.com + +References: +[1] http://www.gnu.org/software/glpk/glpk.html +[2] http://www.coin-or.org/ +[3] http://www.cplex.com/ +[4] http://www.gurobi.com/ +""" + +import types +import string +import itertools + +from constants import * +from solvers import * +from types import GeneratorType + +_DICT_TYPE = dict + +if sys.platform not in ['cli']: + # iron python does not like an OrderedDict + try: + from odict import OrderedDict + _DICT_TYPE = OrderedDict + except ImportError: + pass + try: + #python 2.7 or 3.1 + from collections import OrderedDict + _DICT_TYPE = OrderedDict + except ImportError: + pass + + +def setConfigInformation(**keywords): + """ + set the data in the configuration file + at the moment will only edit things in [locations] + the keyword value pairs come from the keywords dictionary + """ + #TODO: extend if we ever add another section in the config file + #read the old configuration + config = ConfigParser.SafeConfigParser() + config.read(config_filename) + #set the new keys + for (key,val) in keywords.items(): + config.set("locations",key,val) + #write the new configuration + fp = open(config_filename,"w") + config.write(fp) + fp.close() + + +# Default solver selection +if PULP_CBC_CMD().available(): + LpSolverDefault = PULP_CBC_CMD() +elif GLPK_CMD().available(): + LpSolverDefault = GLPK_CMD() +elif COIN_CMD().available(): + LpSolverDefault = COIN_CMD() +else: + LpSolverDefault = None + +class LpElement(object): + """Base class for LpVariable and LpConstraintVar + """ + #to remove illegal characters from the names + trans = string.maketrans("-+[] ->/","________") + def setName(self,name): + if name: + self.__name = str(name).translate(self.trans) + else: + self.__name = None + def getName(self): + return self.__name + name = property(fget = getName,fset = setName) + + def __init__(self, name): + self.name = name + # self.hash MUST be different for each variable + # else dict() will call the comparison operators that are overloaded + self.hash = id(self) + self.modified = True + + def __hash__(self): + return self.hash + + def __str__(self): + return self.name + def __repr__(self): + return self.name + + def __neg__(self): + return - LpAffineExpression(self) + + def __pos__(self): + return self + + def __nonzero__(self): + return 1 + + def __add__(self, other): + return LpAffineExpression(self) + other + + def __radd__(self, other): + return LpAffineExpression(self) + other + + def __sub__(self, other): + return LpAffineExpression(self) - other + + def __rsub__(self, other): + return other - LpAffineExpression(self) + + def __mul__(self, other): + return LpAffineExpression(self) * other + + def __rmul__(self, other): + return LpAffineExpression(self) * other + + def __div__(self, other): + return LpAffineExpression(self)/other + + def __rdiv__(self, other): + raise TypeError, "Expressions cannot be divided by a variable" + + def __le__(self, other): + return LpAffineExpression(self) <= other + + def __ge__(self, other): + return LpAffineExpression(self) >= other + + def __eq__(self, other): + return LpAffineExpression(self) == other + + def __ne__(self, other): + if isinstance(other, LpVariable): + return self.name is not other.name + elif isinstance(other, LpAffineExpression): + if other.isAtomic(): + return self is not other.atom() + else: + return 1 + else: + return 1 + + +class LpVariable(LpElement): + """ + This class models an LP Variable with the specified associated parameters + + :param name: The name of the variable used in the output .lp file + :param lowbound: The lower bound on this variable's range. + Default is negative infinity + :param upBound: The upper bound on this variable's range. + Default is positive infinity + :param cat: The category this variable is in, Integer, Binary or + Continuous(default) + :param e: Used for column based modelling: relates to the variable's + existence in the objective function and constraints + """ + def __init__(self, name, lowBound = None, upBound = None, + cat = LpContinuous, e = None): + LpElement.__init__(self,name) + self.lowBound = lowBound + self.upBound = upBound + self.cat = cat + self.varValue = None + self.init = 0 + #code to add a variable to constraints for column based + # modelling + if cat == LpBinary: + self.lowBound = 0 + self.upBound = 1 + self.cat = LpInteger + if e: + self.add_expression(e) + + def add_expression(self,e): + self.expression = e + self.addVariableToConstraints(e) + + def matrix(self, name, indexs, lowBound = None, upBound = None, cat = LpContinuous, + indexStart = []): + if not isinstance(indexs, tuple): indexs = (indexs,) + if "%" not in name: name += "_%s" * len(indexs) + + index = indexs[0] + indexs = indexs[1:] + if len(indexs) == 0: + return [LpVariable(name % tuple(indexStart + [i]), + lowBound, upBound, cat) + for i in index] + else: + return [LpVariable.matrix(name, indexs, lowBound, + upBound, cat, indexStart + [i]) + for i in index] + matrix = classmethod(matrix) + + def dicts(self, name, indexs, lowBound = None, upBound = None, cat = LpContinuous, + indexStart = []): + """ + Creates a dictionary of LP variables + + This function creates a dictionary of LP Variables with the specified + associated parameters. + + :param name: The prefix to the name of each LP variable created + :param indexs: A list of strings of the keys to the dictionary of LP + variables, and the main part of the variable name itself + :param lowbound: The lower bound on these variables' range. Default is + negative infinity + :param upBound: The upper bound on these variables' range. Default is + positive infinity + :param cat: The category these variables are in, Integer or + Continuous(default) + + :return: A dictionary of LP Variables + """ + if not isinstance(indexs, tuple): indexs = (indexs,) + if "%" not in name: name += "_%s" * len(indexs) + + index = indexs[0] + indexs = indexs[1:] + d = {} + if len(indexs) == 0: + for i in index: + d[i] = LpVariable(name % tuple(indexStart + [str(i)]), lowBound, upBound, cat) + else: + for i in index: + d[i] = LpVariable.dicts(name, indexs, lowBound, upBound, cat, indexStart + [i]) + return d + dicts = classmethod(dicts) + + def dict(self, name, indexs, lowBound = None, upBound = None, cat = LpContinuous): + if not isinstance(indexs, tuple): indexs = (indexs,) + if "%" not in name: name += "_%s" * len(indexs) + + lists = indexs + + if len(indexs)>1: + # Cartesian product + res = [] + while len(lists): + first = lists[-1] + nres = [] + if res: + if first: + for f in first: + nres.extend([[f]+r for r in res]) + else: + nres = res + res = nres + else: + res = [[f] for f in first] + lists = lists[:-1] + index = [tuple(r) for r in res] + elif len(indexs) == 1: + index = indexs[0] + else: + return {} + + d = {} + for i in index: + d[i] = self(name % i, lowBound, upBound, cat) + return d + dict = classmethod(dict) + + def getLb(self): + return self.lowBound + + def getUb(self): + return self.upBound + + def bounds(self, low, up): + self.lowBound = low + self.upBound = up + + def positive(self): + self.lowBound = 0 + self.upBound = None + + def value(self): + return self.varValue + + def round(self, epsInt = 1e-5, eps = 1e-7): + if self.varValue is not None: + if self.upBound != None and self.varValue > self.upBound and self.varValue <= self.upBound + eps: + self.varValue = self.upBound + elif self.lowBound != None and self.varValue < self.lowBound and self.varValue >= self.lowBound - eps: + self.varValue = self.lowBound + if self.cat == LpInteger and abs(round(self.varValue) - self.varValue) <= epsInt: + self.varValue = round(self.varValue) + + def roundedValue(self, eps = 1e-5): + if self.cat == LpInteger and self.varValue != None \ + and abs(self.varValue - round(self.varValue)) <= eps: + return round(self.varValue) + else: + return self.varValue + + def valueOrDefault(self): + if self.varValue != None: + return self.varValue + elif self.lowBound != None: + if self.upBound != None: + if 0 >= self.lowBound and 0 <= self.upBound: + return 0 + else: + if self.lowBound >= 0: + return self.lowBound + else: + return self.upBound + else: + if 0 >= self.lowBound: + return 0 + else: + return self.lowBound + elif self.upBound != None: + if 0 <= self.upBound: + return 0 + else: + return self.upBound + else: + return 0 + + def valid(self, eps): + if self.varValue == None: return False + if self.upBound != None and self.varValue > self.upBound + eps: + return False + if self.lowBound != None and self.varValue < self.lowBound - eps: + return False + if self.cat == LpInteger and abs(round(self.varValue) - self.varValue) > eps: + return False + return True + + def infeasibilityGap(self, mip = 1): + if self.varValue == None: raise ValueError, "variable value is None" + if self.upBound != None and self.varValue > self.upBound: + return self.varValue - self.upBound + if self.lowBound != None and self.varValue < self.lowBound: + return self.varValue - self.lowBound + if mip and self.cat == LpInteger and round(self.varValue) - self.varValue != 0: + return round(self.varValue) - self.varValue + return 0 + + def isBinary(self): + return self.cat == LpInteger and self.lowBound == 0 and self.upBound == 1 + + def isInteger(self): + return self.cat == LpInteger + + def isFree(self): + return self.lowBound == None and self.upBound == None + + def isConstant(self): + return self.lowBound != None and self.upBound == self.lowBound + + def isPositive(self): + return self.lowBound == 0 and self.upBound == None + + def asCplexLpVariable(self): + if self.isFree(): return self.name + " free" + if self.isConstant(): return self.name + " = %.12g" % self.lowBound + if self.lowBound == None: + s= "-inf <= " + # Note: XPRESS and CPLEX do not interpret integer variables without + # explicit bounds + elif (self.lowBound == 0 and self.cat == LpContinuous): + s = "" + else: + s= "%.12g <= " % self.lowBound + s += self.name + if self.upBound != None: + s+= " <= %.12g" % self.upBound + return s + + def asCplexLpAffineExpression(self, name, constant = 1): + return LpAffineExpression(self).asCplexLpAffineExpression(name, constant) + + def __ne__(self, other): + if isinstance(other, LpElement): + return self.name is not other.name + elif isinstance(other, LpAffineExpression): + if other.isAtomic(): + return self is not other.atom() + else: + return 1 + else: + return 1 + + def addVariableToConstraints(self,e): + """adds a variable to the constraints indicated by + the LpConstraintVars in e + """ + for constraint, coeff in e.items(): + constraint.addVariable(self,coeff) + + def setInitialValue(self,val): + """sets the initial value of the Variable to val + may of may not be supported by the solver + """ + raise NotImplementedError + + +class LpAffineExpression(_DICT_TYPE): + """ + A linear combination of :class:`LpVariables`. + Can be initialised with the following: + + #. e = None: an empty Expression + #. e = dict: gives an expression with the values being the coefficients of the keys (order of terms is undetermined) + #. e = list or generator of 2-tuples: equivalent to dict.items() + #. e = LpElement: an expression of length 1 with the coefficient 1 + #. e = other: the constant is initialised as e + + Examples: + + >>> f=LpAffineExpression(LpElement('x')) + >>> f + 1*x + 0 + >>> x_name = ['x_0', 'x_1', 'x_2'] + >>> x = [LpVariable(x_name[i], lowBound = 0, upBound = 10) for i in range(3) ] + >>> c = LpAffineExpression([ (x[0],1), (x[1],-3), (x[2],4)]) + >>> c + 1*x_0 + -3*x_1 + 4*x_2 + 0 + """ + #to remove illegal characters from the names + trans = string.maketrans("-+[] ","_____") + def setName(self,name): + if name: + self.__name = str(name).translate(self.trans) + else: + self.__name = None + + def getName(self): + return self.__name + + name = property(fget=getName, fset=setName) + + def __init__(self, e = None, constant = 0, name = None): + self.name = name + #TODO remove isinstance usage + if e is None: + e = {} + if isinstance(e, LpAffineExpression): + # Will not copy the name + self.constant = e.constant + super(LpAffineExpression, self).__init__(e.items()) + elif isinstance(e, dict): + self.constant = constant + super(LpAffineExpression, self).__init__(e.items()) + elif isinstance(e, list) or isinstance(e, GeneratorType): + self.constant = constant + super(LpAffineExpression, self).__init__(e) + elif isinstance(e,LpElement): + self.constant = 0 + super(LpAffineExpression, self).__init__( [(e, 1)]) + else: + self.constant = e + super(LpAffineExpression, self).__init__() + + # Proxy functions for variables + + def isAtomic(self): + return len(self) == 1 and self.constant == 0 and self.values()[0] == 1 + + def isNumericalConstant(self): + return len(self) == 0 + + def atom(self): + return self.keys()[0] + + # Functions on expressions + + def __nonzero__(self): + return float(self.constant) != 0 or len(self) + + def value(self): + s = self.constant + for v,x in self.iteritems(): + if v.varValue is None: + return None + s += v.varValue * x + return s + + def valueOrDefault(self): + s = self.constant + for v,x in self.iteritems(): + s += v.valueOrDefault() * x + return s + + def addterm(self, key, value): + y = self.get(key, 0) + if y: + y += value + self[key] = y + else: + self[key] = value + + def emptyCopy(self): + return LpAffineExpression() + + def copy(self): + """Make a copy of self except the name which is reset""" + # Will not copy the name + return LpAffineExpression(self) + + def __str__(self, constant = 1): + s = "" + for v in self.sorted_keys(): + val = self[v] + if val<0: + if s != "": s += " - " + else: s += "-" + val = -val + elif s != "": s += " + " + if val == 1: s += str(v) + else: s += str(val) + "*" + str(v) + if constant: + if s == "": + s = str(self.constant) + else: + if self.constant < 0: s += " - " + str(-self.constant) + elif self.constant > 0: s += " + " + str(self.constant) + elif s == "": + s = "0" + return s + + def sorted_keys(self): + """ + returns the list of keys sorted by name + """ + result = [(v.name, v) for v in self.keys()] + result.sort() + result = [v for _, v in result] + return result + + def __repr__(self): + l = [str(self[v]) + "*" + str(v) + for v in self.sorted_keys()] + l.append(str(self.constant)) + s = " + ".join(l) + return s + + @staticmethod + def _count_characters(line): + #counts the characters in a list of strings + return sum(len(t) for t in line) + + def asCplexVariablesOnly(self, name): + """ + helper for asCplexLpAffineExpression + """ + result = [] + line = ["%s:" % name] + notFirst = 0 + variables = self.sorted_keys() + for v in variables: + val = self[v] + if val < 0: + sign = " -" + val = -val + elif notFirst: + sign = " +" + else: + sign = "" + notFirst = 1 + if val == 1: + term = "%s %s" %(sign, v.name) + else: + term = "%s %.12g %s" % (sign, val, v.name) + + if self._count_characters(line) + len(term) > LpCplexLPLineSize: + result += ["".join(line)] + line = [term] + else: + line += [term] + return result, line + + def asCplexLpAffineExpression(self, name, constant = 1): + """ + returns a string that represents the Affine Expression in lp format + """ + #refactored to use a list for speed in iron python + result, line = self.asCplexVariablesOnly(name) + if not self: + term = " %s" % self.constant + else: + term = "" + if constant: + if self.constant < 0: + term = " - %s" % (-self.constant) + elif self.constant > 0: + term = " + %s" % self.constant + if self._count_characters(line) + len(term) > LpCplexLPLineSize: + result += ["".join(line)] + line += [term] + else: + line += [term] + result += ["".join(line)] + result = "%s\n" % "\n".join(result) + return result + + def addInPlace(self, other): + if other is 0: return self + if other is None: return self + if isinstance(other,LpElement): + self.addterm(other, 1) + elif (isinstance(other,list) + or isinstance(other,types.GeneratorType)): + for e in other: + self.addInPlace(e) + elif isinstance(other,LpAffineExpression): + self.constant += other.constant + for v,x in other.iteritems(): + self.addterm(v, x) + elif isinstance(other,dict): + for e in other.itervalues(): + self.addInPlace(e) + else: + self.constant += other + return self + + def subInPlace(self, other): + if other is 0: return self + if other is None: return self + if isinstance(other,LpElement): + self.addterm(other, -1) + elif (isinstance(other,list) + or isinstance(other,types.GeneratorType)): + for e in other: + self.subInPlace(e) + elif isinstance(other,LpAffineExpression): + self.constant -= other.constant + for v,x in other.iteritems(): + self.addterm(v, -x) + elif isinstance(other,dict): + for e in other.itervalues(): + self.subInPlace(e) + else: + self.constant -= other + return self + + def __neg__(self): + e = self.emptyCopy() + e.constant = - self.constant + for v,x in self.iteritems(): + e[v] = - x + return e + + def __pos__(self): + return self + + def __add__(self, other): + return self.copy().addInPlace(other) + + def __radd__(self, other): + return self.copy().addInPlace(other) + + def __sub__(self, other): + return self.copy().subInPlace(other) + + def __rsub__(self, other): + return (-self).addInPlace(other) + + def __mul__(self, other): + e = self.emptyCopy() + if isinstance(other,LpAffineExpression): + e.constant = self.constant * other.constant + if len(other): + if len(self): + raise TypeError, "Non-constant expressions cannot be multiplied" + else: + c = self.constant + if c != 0: + for v,x in other.iteritems(): + e[v] = c * x + else: + c = other.constant + if c != 0: + for v,x in self.iteritems(): + e[v] = c * x + elif isinstance(other,LpVariable): + return self * LpAffineExpression(other) + else: + if other != 0: + e.constant = self.constant * other + for v,x in self.iteritems(): + e[v] = other * x + return e + + def __rmul__(self, other): + return self * other + + def __div__(self, other): + if isinstance(other,LpAffineExpression) or isinstance(other,LpVariable): + if len(other): + raise TypeError, "Expressions cannot be divided by a non-constant expression" + other = other.constant + e = self.emptyCopy() + e.constant = self.constant / other + for v,x in self.iteritems(): + e[v] = x / other + return e + + def __rdiv__(self, other): + e = self.emptyCopy() + if len(self): + raise TypeError, "Expressions cannot be divided by a non-constant expression" + c = self.constant + if isinstance(other,LpAffineExpression): + e.constant = other.constant / c + for v,x in other.iteritems(): + e[v] = x / c + else: + e.constant = other / c + return e + + def __le__(self, other): + return LpConstraint(self - other, LpConstraintLE) + + def __ge__(self, other): + return LpConstraint(self - other, LpConstraintGE) + + def __eq__(self, other): + return LpConstraint(self - other, LpConstraintEQ) + +class LpConstraint(LpAffineExpression): + """An LP constraint""" + def __init__(self, e = None, sense = LpConstraintEQ, + name = None, rhs = None): + """ + :param e: an instance of :class:`LpAffineExpression` + :param sense: one of :data:`~pulp.constants.LpConstraintEQ`, :data:`~pulp.constants.LpConstraintGE`, :data:`~pulp.constants.LpConstraintLE` (0, 1, -1 respectively) + :param name: identifying string + :param rhs: numerical value of constraint target + """ + LpAffineExpression.__init__(self, e, name = name) + if rhs is not None: + self.constant = - rhs + self.sense = sense + self.modified = True + + def getLb(self): + if ( (self.sense == LpConstraintGE) or + (self.sense == LpConstraintEQ) ): + return -self.constant + else: + return None + + def getUb(self): + if ( (self.sense == LpConstraintLE) or + (self.sense == LpConstraintEQ) ): + return -self.constant + else: + return None + + def __str__(self): + s = LpAffineExpression.__str__(self, 0) + if self.sense: + s += " " + LpConstraintSenses[self.sense] + " " + str(-self.constant) + return s + + def asCplexLpConstraint(self, name): + """ + Returns a constraint as a string + """ + result, line = self.asCplexVariablesOnly(name) + if not self.keys(): + line += ["0"] + c = -self.constant + if c == 0: + c = 0 # Supress sign + term = " %s %.12g" % (LpConstraintSenses[self.sense], c) + if self._count_characters(line)+len(term) > LpCplexLPLineSize: + result += ["".join(line)] + line = [term] + else: + line += [term] + result += ["".join(line)] + result = "%s\n" % "\n".join(result) + return result + + def changeRHS(self, RHS): + """ + alters the RHS of a constraint so that it can be modified in a resolve + """ + self.constant = -RHS + self.modified = True + + def __repr__(self): + s = LpAffineExpression.__repr__(self) + if self.sense is not None: + s += " " + LpConstraintSenses[self.sense] + " 0" + return s + + def copy(self): + """Make a copy of self""" + return LpConstraint(self, self.sense) + + def emptyCopy(self): + return LpConstraint(sense = self.sense) + + def addInPlace(self, other): + if isinstance(other,LpConstraint): + if self.sense * other.sense >= 0: + LpAffineExpression.addInPlace(self, other) + self.sense |= other.sense + else: + LpAffineExpression.subInPlace(self, other) + self.sense |= - other.sense + elif isinstance(other,list): + for e in other: + self.addInPlace(e) + else: + LpAffineExpression.addInPlace(self, other) + #raise TypeError, "Constraints and Expressions cannot be added" + return self + + def subInPlace(self, other): + if isinstance(other,LpConstraint): + if self.sense * other.sense <= 0: + LpAffineExpression.subInPlace(self, other) + self.sense |= - other.sense + else: + LpAffineExpression.addInPlace(self, other) + self.sense |= other.sense + elif isinstance(other,list): + for e in other: + self.subInPlace(e) + else: + LpAffineExpression.subInPlace(self, other) + #raise TypeError, "Constraints and Expressions cannot be added" + return self + + def __neg__(self): + c = LpAffineExpression.__neg__(self) + c.sense = - c.sense + return c + + def __add__(self, other): + return self.copy().addInPlace(other) + + def __radd__(self, other): + return self.copy().addInPlace(other) + + def __sub__(self, other): + return self.copy().subInPlace(other) + + def __rsub__(self, other): + return (-self).addInPlace(other) + + def __mul__(self, other): + if isinstance(other,LpConstraint): + c = LpAffineExpression.__mul__(self, other) + if c.sense == 0: + c.sense = other.sense + elif other.sense != 0: + c.sense *= other.sense + return c + else: + return LpAffineExpression.__mul__(self, other) + + def __rmul__(self, other): + return self * other + + def __div__(self, other): + if isinstance(other,LpConstraint): + c = LpAffineExpression.__div__(self, other) + if c.sense == 0: + c.sense = other.sense + elif other.sense != 0: + c.sense *= other.sense + return c + else: + return LpAffineExpression.__mul__(self, other) + + def __rdiv__(self, other): + if isinstance(other,LpConstraint): + c = LpAffineExpression.__rdiv__(self, other) + if c.sense == 0: + c.sense = other.sense + elif other.sense != 0: + c.sense *= other.sense + return c + else: + return LpAffineExpression.__mul__(self, other) + + def valid(self, eps = 0): + val = self.value() + if self.sense == LpConstraintEQ: return abs(val) <= eps + else: return val * self.sense >= - eps + + def makeElasticSubProblem(self, *args, **kwargs): + """ + Builds an elastic subproblem by adding variables to a hard constraint + + uses FixedElasticSubProblem + """ + return FixedElasticSubProblem(self, *args, **kwargs) + +class LpFractionConstraint(LpConstraint): + """ + Creates a constraint that enforces a fraction requirement a/b = c + """ + def __init__(self, numerator, denominator = None, sense = LpConstraintEQ, + RHS = 1.0, name = None, + complement = None): + """ + creates a fraction Constraint to model constraints of + the nature + numerator/denominator {==, >=, <=} RHS + numerator/(numerator + complement) {==, >=, <=} RHS + + :param numerator: the top of the fraction + :param denominator: as described above + :param sense: the sense of the relation of the constraint + :param RHS: the target fraction value + :param complement: as described above + """ + self.numerator = numerator + if denominator is None and complement is not None: + self.complement = complement + self.denominator = numerator + complement + elif denominator is not None and complement is None: + self.denominator = denominator + self.complement = denominator - numerator + else: + self.denominator = denominator + self.complement = complement + lhs = self.numerator - RHS * self.denominator + LpConstraint.__init__(self, lhs, + sense = sense, rhs = 0, name = name) + self.RHS = RHS + + def findLHSValue(self): + """ + Determines the value of the fraction in the constraint after solution + """ + if abs(value(self.denominator))>= EPS: + return value(self.numerator)/value(self.denominator) + else: + if abs(value(self.numerator))<= EPS: + #zero divided by zero will return 1 + return 1.0 + else: + raise ZeroDivisionError + + def makeElasticSubProblem(self, *args, **kwargs): + """ + Builds an elastic subproblem by adding variables and splitting the + hard constraint + + uses FractionElasticSubProblem + """ + return FractionElasticSubProblem(self, *args, **kwargs) + +class LpConstraintVar(LpElement): + """A Constraint that can be treated as a variable when constructing + a LpProblem by columns + """ + def __init__(self, name = None ,sense = None, + rhs = None, e = None): + LpElement.__init__(self,name) + self.constraint = LpConstraint(name = self.name, sense = sense, + rhs = rhs , e = e) + + def addVariable(self, var, coeff): + """ + Adds a variable to the constraint with the + activity coeff + """ + self.constraint.addterm(var, coeff) + + def value(self): + return self.constraint.value() + +class LpProblem(object): + """An LP Problem""" + def __init__(self, name = "NoName", sense = LpMinimize): + """ + Creates an LP Problem + + This function creates a new LP Problem with the specified associated parameters + + :param name: name of the problem used in the output .lp file + :param sense: of the LP problem objective. \ + Either :data:`~pulp.constants.LpMinimize` (default) \ + or :data:`~pulp.constants.LpMaximize`. + :return: An LP Problem + """ + self.objective = None + self.constraints = _DICT_TYPE() + self.name = name + self.sense = sense + self.sos1 = {} + self.sos2 = {} + self.status = LpStatusNotSolved + self.noOverlap = 1 + self.solver = None + self.initialValues = {} + self.modifiedVariables = [] + self.modifiedConstraints = [] + self.resolveOK = False + self._variables = [] + self._variable_ids = {} #old school using dict.keys() for a set + self.dummyVar = None + + + # locals + self.lastUnused = 0 + + def __repr__(self): + string = self.name+":\n" + if self.sense == 1: + string += "MINIMIZE\n" + else: + string += "MAXIMIZE\n" + string += repr(self.objective) +"\n" + + if self.constraints: + string += "SUBJECT TO\n" + for n, c in self.constraints.iteritems(): + string += c.asCplexLpConstraint(n) +"\n" + string += "VARIABLES\n" + for v in self.variables(): + string += v.asCplexLpVariable() + " " + LpCategories[v.cat] + "\n" + return string + + def copy(self): + """Make a copy of self. Expressions are copied by reference""" + lpcopy = LpProblem(name = self.name, sense = self.sense) + lpcopy.objective = self.objective + lpcopy.constraints = self.constraints.copy() + lpcopy.sos1 = self.sos1.copy() + lpcopy.sos2 = self.sos2.copy() + return lpcopy + + def deepcopy(self): + """Make a copy of self. Expressions are copied by value""" + lpcopy = LpProblem(name = self.name, sense = self.sense) + if lpcopy.objective != None: + lpcopy.objective = self.objective.copy() + lpcopy.constraints = {} + for k,v in self.constraints.iteritems(): + lpcopy.constraints[k] = v.copy() + lpcopy.sos1 = self.sos1.copy() + lpcopy.sos2 = self.sos2.copy() + return lpcopy + + def normalisedNames(self): + constraintsNames = {} + i = 0 + for k in self.constraints: + constraintsNames[k] = "C%07d" % i + i += 1 + variablesNames = {} + i = 0 + for k in self.variables(): + variablesNames[k.name] = "X%07d" % i + i += 1 + return constraintsNames, variablesNames, "OBJ" + + def isMIP(self): + for v in self.variables(): + if v.cat == LpInteger: return 1 + return 0 + + def roundSolution(self, epsInt = 1e-5, eps = 1e-7): + """ + Rounds the lp variables + + Inputs: + - none + + Side Effects: + - The lp variables are rounded + """ + for v in self.variables(): + v.round(epsInt, eps) + + def unusedConstraintName(self): + self.lastUnused += 1 + while 1: + s = "_C%d" % self.lastUnused + if s not in self.constraints: break + self.lastUnused += 1 + return s + + def valid(self, eps = 0): + for v in self.variables(): + if not v.valid(eps): return False + for c in self.constraints.itervalues(): + if not c.valid(eps): return False + else: + return True + + def infeasibilityGap(self, mip = 1): + gap = 0 + for v in self.variables(): + gap = max(abs(v.infeasibilityGap(mip)), gap) + for c in self.constraints.itervalues(): + if not c.valid(0): + gap = max(abs(c.value()), gap) + return gap + + def addVariable(self, variable): + """ + Adds a variable to the problem before a constraint is added + + @param variable: the variable to be added + """ + if id(variable) not in self._variable_ids: + self._variables.append(variable) + self._variable_ids[id(variable)] = variable + + def addVariables(self, variables): + """ + Adds variables to the problem before a constraint is added + + @param variables: the variables to be added + """ + for v in variables: + self.addVariable(v) + + def variables(self): + """ + Returns a list of the problem variables + + Inputs: + - none + + Returns: + - A list of the problem variables + """ + if self.objective: + self.addVariables(self.objective.keys()) + for c in self.constraints.itervalues(): + self.addVariables(c.keys()) + variables = self._variables + #sort the varibles DSU + variables = [[v.name, v] for v in variables] + variables.sort() + variables = [v for _, v in variables] + return variables + + def variablesDict(self): + variables = {} + if self.objective: + for v in self.objective: + variables[v.name] = v + for c in self.constraints.values(): + for v in c: + variables[v.name] = v + return variables + + def add(self, constraint, name = None): + self.addConstraint(constraint, name) + + def addConstraint(self, constraint, name = None): + if not isinstance(constraint, LpConstraint): + raise TypeError, "Can only add LpConstraint objects" + if name: + constraint.name = name + try: + if constraint.name: + name = constraint.name + else: + name = self.unusedConstraintName() + except AttributeError: + raise TypeError, "Can only add LpConstraint objects" + #removed as this test fails for empty constraints +# if len(constraint) == 0: +# if not constraint.valid(): +# raise ValueError, "Cannot add false constraints" + if name in self.constraints: + if self.noOverlap: + raise PulpError, "overlapping constraint names: " + name + else: + print "Warning: overlapping constraint names:", name + self.constraints[name] = constraint + self.modifiedConstraints.append(constraint) + self.addVariables(constraint.keys()) + + def setObjective(self,obj): + """ + Sets the input variable as the objective function. Used in Columnwise Modelling + + :param obj: the objective function of type :class:`LpConstraintVar` + + Side Effects: + - The objective function is set + """ + if isinstance(obj, LpVariable): + # allows the user to add a LpVariable as an objective + obj = obj + 0.0 + try: + obj = obj.constraint + name = obj.name + except AttributeError: + name = None + self.objective = obj + self.objective.name = name + self.resolveOK = False + + def __iadd__(self, other): + if isinstance(other, tuple): + other, name = other + else: + name = None + if other is True: + return self + if isinstance(other, LpConstraintVar): + self.addConstraint(other.constraint) + elif isinstance(other, LpConstraint): + self.addConstraint(other, name) + elif isinstance(other, LpAffineExpression): + self.objective = other + self.objective.name = name + elif isinstance(other, LpVariable) or type(other) in [int, float]: + self.objective = LpAffineExpression(other) + self.objective.name = name + else: + raise TypeError, "Can only add LpConstraintVar, LpConstraint, LpAffineExpression or True objects" + return self + + def extend(self, other, use_objective = True): + """ + extends an LpProblem by adding constraints either from a dictionary + a tuple or another LpProblem object. + + @param use_objective: determines whether the objective is imported from + the other problem + + For dictionaries the constraints will be named with the keys + For tuples an unique name will be generated + For LpProblems the name of the problem will be added to the constraints + name + """ + if isinstance(other, dict): + for name in other: + self.constraints[name] = other[name] + elif isinstance(other, LpProblem): + for v in set(other.variables()).difference(self.variables()): + v.name = other.name + v.name + for name,c in other.constraints.iteritems(): + c.name = other.name + name + self.addConstraint(c) + if use_objective: + self.objective += other.objective + else: + for c in other: + if isinstance(c,tuple): + name = c[0] + c = c[1] + else: + name = None + if not name: name = c.name + if not name: name = self.unusedConstraintName() + self.constraints[name] = c + + def coefficients(self, translation = None): + coefs = [] + if translation == None: + for c in self.constraints: + cst = self.constraints[c] + coefs.extend([(v.name, c, cst[v]) for v in cst]) + else: + for c in self.constraints: + ctr = translation[c] + cst = self.constraints[c] + coefs.extend([(translation[v.name], ctr, cst[v]) for v in cst]) + return coefs + + def writeMPS(self, filename, mpsSense = 0, rename = 0, mip = 1): + wasNone, dummyVar = self.fixObjective() + f = file(filename, "w") + if mpsSense == 0: mpsSense = self.sense + cobj = self.objective + if mpsSense != self.sense: + n = cobj.name + cobj = - cobj + cobj.name = n + if rename: + constraintsNames, variablesNames, cobj.name = self.normalisedNames() + f.write("*SENSE:"+LpSenses[mpsSense]+"\n") + n = self.name + if rename: n = "MODEL" + f.write("NAME "+n+"\n") + vs = self.variables() + # constraints + f.write("ROWS\n") + objName = cobj.name + if not objName: objName = "OBJ" + f.write(" N %s\n" % objName) + mpsConstraintType = {LpConstraintLE:"L", LpConstraintEQ:"E", LpConstraintGE:"G"} + for k,c in self.constraints.iteritems(): + if rename: k = constraintsNames[k] + f.write(" "+mpsConstraintType[c.sense]+" "+k+"\n") + # matrix + f.write("COLUMNS\n") + # Creation of a dict of dict: + # coefs[nomVariable][nomContrainte] = coefficient + coefs = {} + for k,c in self.constraints.iteritems(): + if rename: k = constraintsNames[k] + for v in c: + n = v.name + if rename: n = variablesNames[n] + if n in coefs: + coefs[n][k] = c[v] + else: + coefs[n] = {k:c[v]} + + for v in vs: + if mip and v.cat == LpInteger: + f.write(" MARK 'MARKER' 'INTORG'\n") + n = v.name + if rename: n = variablesNames[n] + if n in coefs: + cv = coefs[n] + # Most of the work is done here + for k in cv: f.write(" %-8s %-8s % .5e\n" % (n,k,cv[k])) + + # objective function + if v in cobj: f.write(" %-8s %-8s % .5e\n" % (n,objName,cobj[v])) + if mip and v.cat == LpInteger: + f.write(" MARK 'MARKER' 'INTEND'\n") + # right hand side + f.write("RHS\n") + for k,c in self.constraints.iteritems(): + c = -c.constant + if rename: k = constraintsNames[k] + if c == 0: c = 0 + f.write(" RHS %-8s % .5e\n" % (k,c)) + # bounds + f.write("BOUNDS\n") + for v in vs: + n = v.name + if rename: n = variablesNames[n] + if v.lowBound != None and v.lowBound == v.upBound: + f.write(" FX BND %-8s % .5e\n" % (n, v.lowBound)) + elif v.lowBound == 0 and v.upBound == 1 and mip and v.cat == LpInteger: + f.write(" BV BND %-8s\n" % n) + else: + if v.lowBound != None: + # In MPS files, variables with no bounds (i.e. >= 0) + # are assumed BV by COIN and CPLEX. + # So we explicitly write a 0 lower bound in this case. + if v.lowBound != 0 or (mip and v.cat == LpInteger and v.upBound == None): + f.write(" LO BND %-8s % .5e\n" % (n, v.lowBound)) + else: + if v.upBound != None: + f.write(" MI BND %-8s\n" % n) + else: + f.write(" FR BND %-8s\n" % n) + if v.upBound != None: + f.write(" UP BND %-8s % .5e\n" % (n, v.upBound)) + f.write("ENDATA\n") + f.close() + self.restoreObjective(wasNone, dummyVar) + # returns the variables, in writing order + if rename == 0: + return vs + else: + return vs, variablesNames, constraintsNames, cobj.name + + def writeLP(self, filename, writeSOS = 1, mip = 1): + """ + Write the given Lp problem to a .lp file. + + This function writes the specifications (objective function, + constraints, variables) of the defined Lp problem to a file. + + :param filename: the name of the file to be created. + + Side Effects: + - The file is created. + """ + f = file(filename, "w") + f.write("\\* "+self.name+" *\\\n") + if self.sense == 1: + f.write("Minimize\n") + else: + f.write("Maximize\n") + wasNone, dummyVar = self.fixObjective() + objName = self.objective.name + if not objName: objName = "OBJ" + f.write(self.objective.asCplexLpAffineExpression(objName, constant = 0)) + f.write("Subject To\n") + ks = self.constraints.keys() + ks.sort() + for k in ks: + constraint = self.constraints[k] + if not constraint.keys(): + #empty constraint add the dummyVar + constraint += self.get_dummyVar() + f.write(constraint.asCplexLpConstraint(k)) + vs = self.variables() + # check if any names are longer than 100 characters + long_names = [v.name for v in vs if len(v.name) > 100] + if long_names: + raise PulpError('Variable names too long for Lp format\n' + + str(long_names)) + # check for repeated names + repeated_names = {} + for v in vs: + repeated_names[v.name] = repeated_names.get(v.name, 0) + 1 + repeated_names = [(key, value) for key, value in repeated_names.items() + if value >= 2] + if repeated_names: + raise PulpError('Repeated variable names in Lp format\n' + + str(repeated_names)) + # Bounds on non-"positive" variables + # Note: XPRESS and CPLEX do not interpret integer variables without + # explicit bounds + if mip: + vg = [v for v in vs if not (v.isPositive() and v.cat == LpContinuous) \ + and not v.isBinary()] + else: + vg = [v for v in vs if not v.isPositive()] + if vg: + f.write("Bounds\n") + for v in vg: + f.write("%s\n" % v.asCplexLpVariable()) + # Integer non-binary variables + if mip: + vg = [v for v in vs if v.cat == LpInteger and not v.isBinary()] + if vg: + f.write("Generals\n") + for v in vg: f.write("%s\n" % v.name) + # Binary variables + vg = [v for v in vs if v.isBinary()] + if vg: + f.write("Binaries\n") + for v in vg: f.write("%s\n" % v.name) + # Special Ordered Sets + if writeSOS and (self.sos1 or self.sos2): + f.write("SOS\n") + if self.sos1: + for sos in self.sos1.itervalues(): + f.write("S1:: \n") + for v,val in sos.iteritems(): + f.write(" %s: %.12g\n" % (v.name, val)) + if self.sos2: + for sos in self.sos2.itervalues(): + f.write("S2:: \n") + for v,val in sos.iteritems(): + f.write(" %s: %.12g\n" % (v.name, val)) + f.write("End\n") + f.close() + self.restoreObjective(wasNone, dummyVar) + + def assignVarsVals(self, values): + variables = self.variablesDict() + for name in values: + if name != '__dummy': + variables[name].varValue = values[name] + + def assignVarsDj(self,values): + variables = self.variablesDict() + for name in values: + if name != '__dummy': + variables[name].dj = values[name] + + def assignConsPi(self, values): + for name in values: + self.constraints[name].pi = values[name] + + def assignConsSlack(self, values, activity=False): + for name in values: + if activity: + #reports the activitynot the slack + self.constraints[name].slack = -1 * ( + self.constraints[name].constant + float(values[name])) + else: + self.constraints[name].slack = float(values[name]) + + def get_dummyVar(self): + if self.dummyVar is None: + self.dummyVar = LpVariable("__dummy", 0, 0) + return self.dummyVar + + def fixObjective(self): + if self.objective is None: + self.objective = 0 + wasNone = 1 + else: + wasNone = 0 + if not isinstance(self.objective, LpAffineExpression): + self.objective = LpAffineExpression(self.objective) + if self.objective.isNumericalConstant(): + dummyVar = self.get_dummyVar() + self.objective += dummyVar + else: + dummyVar = None + return wasNone, dummyVar + + def restoreObjective(self, wasNone, dummyVar): + if wasNone: + self.objective = None + elif not dummyVar is None: + self.objective -= dummyVar + + def solve(self, solver = None, **kwargs): + """ + Solve the given Lp problem. + + This function changes the problem to make it suitable for solving + then calls the solver.actualSolve() method to find the solution + + :param solver: Optional: the specific solver to be used, defaults to the + default solver. + + Side Effects: + - The attributes of the problem object are changed in + :meth:`~pulp.solver.LpSolver.actualSolve()` to reflect the Lp solution + """ + + if not(solver): solver = self.solver + if not(solver): solver = LpSolverDefault + wasNone, dummyVar = self.fixObjective() + #time it + self.solutionTime = -clock() + status = solver.actualSolve(self, **kwargs) + self.solutionTime += clock() + self.restoreObjective(wasNone, dummyVar) + self.solver = solver + return status + + def sequentialSolve(self, objectives, absoluteTols = None, + relativeTols = None, solver = None, debug = False): + """ + Solve the given Lp problem with several objective functions. + + This function sequentially changes the objective of the problem + and then adds the objective function as a constraint + + :param objectives: the list of objectives to be used to solve the problem + :param absoluteTols: the list of absolute tolerances to be applied to + the constraints should be +ve for a minimise objective + :param relativeTols: the list of relative tolerances applied to the constraints + :param solver: the specific solver to be used, defaults to the default solver. + + """ + #TODO Add a penalty variable to make problems elastic + #TODO add the ability to accept different status values i.e. infeasible etc + + if not(solver): solver = self.solver + if not(solver): solver = LpSolverDefault + if not(absoluteTols): + absoluteTols = [0] * len(objectives) + if not(relativeTols): + relativeTols = [1] * len(objectives) + #time it + self.solutionTime = -clock() + statuses = [] + for i,(obj,absol,rel) in enumerate(zip(objectives, + absoluteTols, relativeTols)): + self.setObjective(obj) + status = solver.actualSolve(self) + statuses.append(status) + if debug: self.writeLP("%sSequence.lp"%i) + if self.sense == LpMinimize: + self += obj <= value(obj)*rel + absol,"%s_Sequence_Objective"%i + elif self.sense == LpMaximize: + self += obj >= value(obj)*rel + absol,"%s_Sequence_Objective"%i + self.solutionTime += clock() + self.solver = solver + return statuses + + def resolve(self, solver = None, **kwargs): + """ + resolves an Problem using the same solver as previously + """ + if not(solver): solver = self.solver + if self.resolveOK: + return self.solver.actualResolve(self, **kwargs) + else: + return self.solve(solver = solver, **kwargs) + + def setSolver(self,solver = LpSolverDefault): + """Sets the Solver for this problem useful if you are using + resolve + """ + self.solver = solver + + def setInitial(self,values): + self.initialValues = values + +class FixedElasticSubProblem(LpProblem): + """ + Contains the subproblem generated by converting a fixed constraint + :math:`\sum_{i}a_i x_i = b` into an elastic constraint. + + :param constraint: The LpConstraint that the elastic constraint is based on + :param penalty: penalty applied for violation (+ve or -ve) of the constraints + :param proportionFreeBound: + the proportional bound (+ve and -ve) on + constraint violation that is free from penalty + :param proportionFreeBoundList: the proportional bound on \ + constraint violation that is free from penalty, expressed as a list\ + where [-ve, +ve] + """ + def __init__(self, constraint, penalty = None, + proportionFreeBound = None, + proportionFreeBoundList = None): + subProblemName = "%s_elastic_SubProblem" % constraint.name + LpProblem.__init__(self, subProblemName, LpMinimize) + self.objective = LpAffineExpression() + self.constraint = constraint + self.constant = constraint.constant + self.RHS = - constraint.constant + self.objective = LpAffineExpression() + self += constraint, "_Constraint" + #create and add these variables but disabled + self.freeVar = LpVariable("_free_bound", + upBound = 0, lowBound = 0) + self.upVar = LpVariable("_pos_penalty_var", + upBound = 0, lowBound = 0) + self.lowVar = LpVariable("_neg_penalty_var", + upBound = 0, lowBound = 0) + constraint.addInPlace(self.freeVar + self.lowVar + self.upVar) + if proportionFreeBound: + proportionFreeBoundList = [proportionFreeBound, proportionFreeBound] + if proportionFreeBoundList: + #add a costless variable + self.freeVar.upBound = abs(constraint.constant * + proportionFreeBoundList[0]) + self.freeVar.lowBound = -abs(constraint.constant * + proportionFreeBoundList[1]) + # Note the reversal of the upbound and lowbound due to the nature of the + # variable + if penalty is not None: + #activate these variables + self.upVar.upBound = None + self.lowVar.lowBound = None + self.objective = penalty*self.upVar - penalty*self.lowVar + + def _findValue(self, attrib): + """ + safe way to get the value of a variable that may not exist + """ + var = getattr(self, attrib, 0) + if var: + if value(var) is not None: + return value(var) + else: + return 0.0 + else: + return 0.0 + + def isViolated(self): + """ + returns true if the penalty variables are non-zero + """ + upVar = self._findValue("upVar") + lowVar = self._findValue("lowVar") + freeVar = self._findValue("freeVar") + result = abs(upVar + lowVar) >= EPS + if result: + logging.debug("isViolated %s, upVar %s, lowVar %s, freeVar %s result %s"%( + self.name, upVar, lowVar, freeVar, result)) + logging.debug("isViolated value lhs %s constant %s"%( + self.findLHSValue(), self.RHS)) + return result + + def findDifferenceFromRHS(self): + """ + The amount the actual value varies from the RHS (sense: LHS - RHS) + """ + return self.findLHSValue() - self.RHS + + + def findLHSValue(self): + """ + for elastic constraints finds the LHS value of the constraint without + the free variable and or penalty variable assumes the constant is on the + rhs + """ + upVar = self._findValue("upVar") + lowVar = self._findValue("lowVar") + freeVar = self._findValue("freeVar") + return self.constraint.value() - self.constant - \ + upVar - lowVar - freeVar + + def deElasticize(self): + """ de-elasticize constraint """ + self.upVar.upBound = 0 + self.lowVar.lowBound = 0 + + def reElasticize(self): + """ + Make the Subproblem elastic again after deElasticize + """ + self.upVar.lowBound = 0 + self.upVar.upBound = None + self.lowVar.upBound = 0 + self.lowVar.lowBound = None + + def alterName(self, name): + """ + Alters the name of anonymous parts of the problem + + """ + self.name = "%s_elastic_SubProblem" % name + if hasattr(self, 'freeVar'): + self.freeVar.name = self.name + "_free_bound" + if hasattr(self, 'upVar'): + self.upVar.name = self.name + "_pos_penalty_var" + if hasattr(self, 'lowVar'): + self.lowVar.name = self.name + "_neg_penalty_var" + + +class FractionElasticSubProblem(FixedElasticSubProblem): + """ + Contains the subproblem generated by converting a Fraction constraint + numerator/(numerator+complement) = b + into an elastic constraint + + :param name: The name of the elastic subproblem + :param penalty: penalty applied for violation (+ve or -ve) of the constraints + :param proportionFreeBound: the proportional bound (+ve and -ve) on + constraint violation that is free from penalty + :param proportionFreeBoundList: the proportional bound on + constraint violation that is free from penalty, expressed as a list + where [-ve, +ve] + """ + def __init__(self, name, numerator, RHS, sense, + complement = None, + denominator = None, + penalty = None, + proportionFreeBound = None, + proportionFreeBoundList = None): + subProblemName = "%s_elastic_SubProblem" % name + self.numerator = numerator + if denominator is None and complement is not None: + self.complement = complement + self.denominator = numerator + complement + elif denominator is not None and complement is None: + self.denominator = denominator + self.complement = denominator - numerator + else: + raise PulpError, 'only one of denominator and complement must be specified' + self.RHS = RHS + self.lowTarget = self.upTarget = None + LpProblem.__init__(self, subProblemName, LpMinimize) + self.freeVar = LpVariable("_free_bound", + upBound = 0, lowBound = 0) + self.upVar = LpVariable("_pos_penalty_var", + upBound = 0, lowBound = 0) + self.lowVar = LpVariable("_neg_penalty_var", + upBound = 0, lowBound = 0) + if proportionFreeBound: + proportionFreeBoundList = [proportionFreeBound, proportionFreeBound] + if proportionFreeBoundList: + upProportionFreeBound, lowProportionFreeBound = \ + proportionFreeBoundList + else: + upProportionFreeBound, lowProportionFreeBound = (0, 0) + #create an objective + self += LpAffineExpression() + #There are three cases if the constraint.sense is ==, <=, >= + if sense in [LpConstraintEQ, LpConstraintLE]: + #create a constraint the sets the upper bound of target + self.upTarget = RHS + upProportionFreeBound + self.upConstraint = LpFractionConstraint(self.numerator, + self.complement, + LpConstraintLE, + self.upTarget, + denominator = self.denominator) + if penalty is not None: + self.lowVar.lowBound = None + self.objective += -1* penalty * self.lowVar + self.upConstraint += self.lowVar + self += self.upConstraint, '_upper_constraint' + if sense in [LpConstraintEQ, LpConstraintGE]: + #create a constraint the sets the lower bound of target + self.lowTarget = RHS - lowProportionFreeBound + self.lowConstraint = LpFractionConstraint(self.numerator, + self.complement, + LpConstraintGE, + self.lowTarget, + denominator = self.denominator) + if penalty is not None: + self.upVar.upBound = None + self.objective += penalty * self.upVar + self.lowConstraint += self.upVar + self += self.lowConstraint, '_lower_constraint' + + def findLHSValue(self): + """ + for elastic constraints finds the LHS value of the constraint without + the free variable and or penalty variable assumes the constant is on the + rhs + """ + # uses code from LpFractionConstraint + if abs(value(self.denominator))>= EPS: + return value(self.numerator)/value(self.denominator) + else: + if abs(value(self.numerator))<= EPS: + #zero divided by zero will return 1 + return 1.0 + else: + raise ZeroDivisionError + + def isViolated(self): + """ + returns true if the penalty variables are non-zero + """ + if abs(value(self.denominator))>= EPS: + if self.lowTarget is not None: + if self.lowTarget > self.findLHSValue(): + return True + if self.upTarget is not None: + if self.findLHSValue() > self.upTarget: + return True + else: + #if the denominator is zero the constraint is satisfied + return False + +class LpVariableDict(dict): + """An LP variable generator""" + def __init__(self, name, data = {}, lowBound = None, upBound = None, cat = LpContinuous): + self.name = name + dict.__init__(self, data) + + def __getitem__(self, key): + if key in self: + return dict.__getitem__(self, key) + else: + self[key] = LpVariable(name % key, lowBound, upBound, cat) + return self[key] + +# Utility functions + +def lpSum(vector): + """ + Calculate the sum of a list of linear expressions + + :param vector: A list of linear expressions + """ + return LpAffineExpression().addInPlace(vector) + +def lpDot(v1, v2): + """Calculate the dot product of two lists of linear expressions""" + if not isiterable(v1) and not isiterable(v2): + return v1 * v2 + elif not isiterable(v1): + return lpDot([v1]*len(v2),v2) + elif not isiterable(v2): + return lpDot(v1,[v2]*len(v1)) + else: + return lpSum([lpDot(e1,e2) for e1,e2 in zip(v1,v2)]) + +def isNumber(x): + """Returns true if x is an int of a float""" + return type(x) in [int, float] + +def value(x): + """Returns the value of the variable/expression x, or x if it is a number""" + if isNumber(x): return x + else: return x.value() + +def valueOrDefault(x): + """Returns the value of the variable/expression x, or x if it is a number + Variable without value (None) are affected a possible value (within their + bounds).""" + if isNumber(x): return x + else: return x.valueOrDefault() + +def combination(orgset, k = None): + """ + returns an iterator that lists the combinations of orgset of + length k + + :param orgset: the list to be iterated + :param k: the cardinality of the subsets + + :return: an iterator of the subsets + + example: + + >>> c = combination([1,2,3,4],2) + >>> for s in c: + ... print s + (1, 2) + (1, 3) + (1, 4) + (2, 3) + (2, 4) + (3, 4) + """ + try: + import probstat + return probstat.Combination(orgset,k) + except(ImportError): + return __combination(orgset,k) + +def __combination(orgset,k): + """ + fall back if probstat is not installed note it is GPL so cannot + be included + """ + if k == 1: + for i in orgset: + yield (i,) + elif k>1: + for i,x in enumerate(orgset): + #iterates though to near the end + for s in __combination(orgset[i+1:],k-1): + yield (x,) + s + +def permutation(orgset, k = None): + """ + returns an iterator that lists the permutations of orgset of + length k + + :param orgset: the list to be iterated + :param k: the cardinality of the subsets + + :return: an iterator of the subsets + + example: + + >>> c = permutation([1,2,3,4],2) + >>> for s in c: + ... print s + (1, 2) + (1, 3) + (1, 4) + (2, 1) + (2, 3) + (2, 4) + (3, 1) + (3, 2) + (3, 4) + (4, 1) + (4, 2) + (4, 3) + """ + try: + import probstat + return probstat.Permutation(orgset, k) + except(ImportError): + return __permutation(orgset, k) + +def __permutation(orgset, k): + """ + fall back if probstat is not installed note it is GPL so cannot + be included + """ + if k == 1: + for i in orgset: + yield (i,) + elif k>1: + for i,x in enumerate(orgset): + #iterates though to near the end + for s in __permutation(orgset[:i] + orgset[i+1:],k-1): + yield (x,)+ s + +def allpermutations(orgset,k): + """ + returns all permutations of orgset with up to k items + + :param orgset: the list to be iterated + :param k: the maxcardinality of the subsets + + :return: an iterator of the subsets + + example: + + >>> c = allpermutations([1,2,3,4],2) + >>> for s in c: + ... print s + (1,) + (2,) + (3,) + (4,) + (1, 2) + (1, 3) + (1, 4) + (2, 1) + (2, 3) + (2, 4) + (3, 1) + (3, 2) + (3, 4) + (4, 1) + (4, 2) + (4, 3) + """ + return itertools.chain(*[permutation(orgset,i) for i in range(1,k+1)]) + +def allcombinations(orgset,k): + """ + returns all permutations of orgset with up to k items + + :param orgset: the list to be iterated + :param k: the maxcardinality of the subsets + + :return: an iterator of the subsets + + example: + + >>> c = allcombinations([1,2,3,4],2) + >>> for s in c: + ... print s + (1,) + (2,) + (3,) + (4,) + (1, 2) + (1, 3) + (1, 4) + (2, 3) + (2, 4) + (3, 4) + """ + return itertools.chain(*[combination(orgset,i) for i in range(1,k+1)]) + +def makeDict(headers, array, default = None): + """ + makes a list into a dictionary with the headings given in headings + headers is a list of header lists + array is a list with the data + """ + result, defdict = __makeDict(headers, array, default) + return result + +def __makeDict(headers, array, default = None): + #this is a recursive function so end the recursion as follows + result ={} + returndefaultvalue = None + if len(headers) == 1: + result.update(dict(zip(headers[0],array))) + defaultvalue = default + else: + for i,h in enumerate(headers[0]): + result[h],defaultvalue = __makeDict(headers[1:],array[i],default) + if default != None: + f = lambda :defaultvalue + defresult = collections.defaultdict(f) + defresult.update(result) + result = defresult + returndefaultvalue = collections.defaultdict(f) + return result, returndefaultvalue + +def splitDict(Data): + """ + Split a dictionary with lists as the data, into smaller dictionaries + + :param Data: A dictionary with lists as the values + + :return: A tuple of dictionaries each containing the data separately, + with the same dictionary keys + """ + # find the maximum number of items in the dictionary + maxitems = max([len(values) for values in Data.values()]) + output =[dict() for i in range(maxitems)] + for key, values in Data.items(): + for i, val in enumerate(values): + output[i][key] = val + + return tuple(output) + +def read_table(data, coerce_type, transpose=False): + ''' + Reads in data from a simple table and forces it to be a particular type + + This is a helper function that allows data to be easily constained in a + simple script + ::return: a dictionary of with the keys being a tuple of the strings + in the first row and colum of the table + ::param data: the multiline string containing the table data + ::param coerce_type: the type that the table data is converted to + ::param transpose: reverses the data if needed + + Example: + >>> table_data = """ + ... L1 L2 L3 L4 L5 L6 + ... C1 6736 42658 70414 45170 184679 111569 + ... C2 217266 227190 249640 203029 153531 117487 + ... C3 35936 28768 126316 2498 130317 74034 + ... C4 73446 52077 108368 75011 49827 62850 + ... C5 174664 177461 151589 153300 59916 135162 + ... C6 186302 189099 147026 164938 149836 286307 + ... """ + >>> table = read_table(table_data, int) + >>> table[("C1","L1")] + 6736 + >>> table[("C6","L5")] + 149836 + ''' + lines = data.splitlines() + headings = lines[1].split() + result = {} + for row in lines[2:]: + items = row.split() + for i, item in enumerate(items[1:]): + if transpose: + key = (headings[i], items[0]) + else: + key = (items[0], headings[i]) + result[key] = coerce_type(item) + return result + +def configSolvers(): + """ + Configure the path the the solvers on the command line + + Designed to configure the file locations of the solvers from the + command line after installation + """ + configlist = [(cplex_dll_path,"cplexpath","CPLEX: "), + (coinMP_path, "coinmppath","CoinMP dll (windows only): ")] + print ("Please type the full path including filename and extension \n" + + "for each solver available") + configdict = {} + for (default, key, msg) in configlist: + value = raw_input(msg + "[" + str(default) +"]") + if value: + configdict[key] = value + setConfigInformation(**configdict) + + +def pulpTestAll(): + from tests import pulpTestSolver + solvers = [PULP_CBC_CMD, + CPLEX_DLL, + CPLEX_CMD, + CPLEX_PY, + COIN_CMD, + COINMP_DLL, + GLPK_CMD, + XPRESS, + GUROBI, + GUROBI_CMD, + PYGLPK, + YAPOSIB + ] + + for s in solvers: + if s().available(): + #~ try: + pulpTestSolver(s) + print "* Solver", s, "passed." + #~ except Exception, e: + #~ print e + #~ print "* Solver", s, "failed." + else: + print "Solver", s, "unavailable." + +def pulpDoctest(): + """ + runs all doctests + """ + import doctest + if __name__ != '__main__': + import pulp + doctest.testmod(pulp) + else: + doctest.testmod() + + +if __name__ == '__main__': + # Tests + pulpTestAll() + pulpDoctest() diff --git a/src/pulp/solverdir/CoinMP.dll b/src/pulp/solverdir/CoinMP.dll new file mode 100644 index 0000000..83db207 Binary files /dev/null and b/src/pulp/solverdir/CoinMP.dll differ diff --git a/src/pulp/solverdir/__init__.py b/src/pulp/solverdir/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pulp/solverdir/cbc-32 b/src/pulp/solverdir/cbc-32 new file mode 100755 index 0000000..44f09dd Binary files /dev/null and b/src/pulp/solverdir/cbc-32 differ diff --git a/src/pulp/solverdir/cbc-64 b/src/pulp/solverdir/cbc-64 new file mode 100755 index 0000000..f612eba Binary files /dev/null and b/src/pulp/solverdir/cbc-64 differ diff --git a/src/pulp/solverdir/cbc.exe b/src/pulp/solverdir/cbc.exe new file mode 100644 index 0000000..32a4e9b Binary files /dev/null and b/src/pulp/solverdir/cbc.exe differ diff --git a/src/pulp/solvers.py b/src/pulp/solvers.py new file mode 100644 index 0000000..2e1711b --- /dev/null +++ b/src/pulp/solvers.py @@ -0,0 +1,2275 @@ +# PuLP : Python LP Modeler +# Version 1.4.2 + +# Copyright (c) 2002-2005, Jean-Sebastien Roy (js@jeannot.org) +# Modifications Copyright (c) 2007- Stuart Anthony Mitchell (s.mitchell@auckland.ac.nz) +# $Id:solvers.py 1791 2008-04-23 22:54:34Z smit023 $ + +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: + +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.""" + +""" +This file contains the solver classes for PuLP +Note that the solvers that require a compiled extension may not work in +the current version +""" + +import os +import subprocess +import sys +from time import clock +import ConfigParser +import sparse +import collections +import warnings +from tempfile import mktemp +from constants import * + +import logging +log = logging.getLogger(__name__) + +class PulpSolverError(PulpError): + """ + Pulp Solver-related exceptions + """ + pass + +#import configuration information +def initialize(filename): + """ reads the configuration file to initialise the module""" + here = os.path.dirname(filename) + config = ConfigParser.SafeConfigParser({'here':here}) + config.read(filename) + try: + cplex_dll_path = config.get("locations", "CplexPath") + except ConfigParser.NoOptionError: + cplex_dll_path = 'libcplex110.so' + try: + ilm_cplex_license = config.get("licenses", + "ilm_cplex_license").decode("string-escape").replace('"','') + except ConfigParser.NoOptionError: + ilm_cplex_license = '' + try: + ilm_cplex_license_signature = config.getint("licenses", + "ilm_cplex_license_signature") + except ConfigParser.NoOptionError: + ilm_cplex_license_signature = 0 + try: + coinMP_path = config.get("locations", "CoinMPPath").split(', ') + except ConfigParser.NoOptionError: + coinMP_path = ['libCoinMP.so'] + try: + gurobi_path = config.get("locations", "GurobiPath") + except ConfigParser.NoOptionError: + gurobi_path = '/opt/gurobi201/linux32/lib/python2.5' + try: + cbc_path = config.get("locations", "CbcPath") + except ConfigParser.NoOptionError: + cbc_path = 'cbc' + try: + glpk_path = config.get("locations", "GlpkPath") + except ConfigParser.NoOptionError: + glpk_path = 'glpsol' + try: + pulp_cbc_path = config.get("locations", "PulpCbcPath") + except ConfigParser.NoOptionError: + pulp_cbc_path = 'cbc' + for i,path in enumerate(coinMP_path): + if not os.path.dirname(path): + #if no pathname is supplied assume the file is in the same directory + coinMP_path[i] = os.path.join(os.path.dirname(config_filename),path) + return cplex_dll_path, ilm_cplex_license, ilm_cplex_license_signature,\ + coinMP_path, gurobi_path, cbc_path, glpk_path, pulp_cbc_path + +#pick up the correct config file depending on operating system +PULPCFGFILE = "pulp.cfg" +if sys.platform in ['win32', 'cli']: + PULPCFGFILE += ".win" +else: + PULPCFGFILE += ".linux" + +if __name__ != '__main__': + DIRNAME = os.path.dirname(__file__) + config_filename = os.path.join(DIRNAME, + PULPCFGFILE) +else: #run as a script + from pulp import __file__ as fname + DIRNAME = os.path.dirname(fname) + config_filename = os.path.join(DIRNAME, + PULPCFGFILE) +cplex_dll_path, ilm_cplex_license, ilm_cplex_license_signature, \ + coinMP_path, gurobi_path, cbc_path, glpk_path, pulp_cbc_path = \ + initialize(config_filename) + + +# See later for LpSolverDefault definition +class LpSolver: + """A generic LP Solver""" + + def __init__(self, mip = True, msg = True, options = [], *args, **kwargs): + self.mip = mip + self.msg = msg + self.options = options + + def available(self): + """True if the solver is available""" + raise NotImplementedError + + def actualSolve(self, lp): + """Solve a well formulated lp problem""" + raise NotImplementedError + + def actualResolve(self,lp, **kwargs): + """ + uses existing problem information and solves the problem + If it is not implelemented in the solver + just solve again + """ + self.actualSolve(lp, **kwargs) + + def copy(self): + """Make a copy of self""" + + aCopy = self.__class__() + aCopy.mip = self.mip + aCopy.msg = self.msg + aCopy.options = self.options + return aCopy + + def solve(self, lp): + """Solve the problem lp""" + # Always go through the solve method of LpProblem + return lp.solve(self) + + #TODO: Not sure if this code should be here or in a child class + def getCplexStyleArrays(self,lp, + senseDict={LpConstraintEQ:"E", LpConstraintLE:"L", LpConstraintGE:"G"}, + LpVarCategories = {LpContinuous: "C",LpInteger: "I"}, + LpObjSenses = {LpMaximize : -1, + LpMinimize : 1}, + infBound = 1e20 + ): + """returns the arrays suitable to pass to a cdll Cplex + or other solvers that are similar + + Copyright (c) Stuart Mitchell 2007 + """ + rangeCount = 0 + variables=list(lp.variables()) + numVars = len(variables) + #associate each variable with a ordinal + self.v2n=dict(((variables[i],i) for i in range(numVars))) + self.vname2n=dict(((variables[i].name,i) for i in range(numVars))) + self.n2v=dict((i,variables[i]) for i in range(numVars)) + #objective values + objSense = LpObjSenses[lp.sense] + NumVarDoubleArray = ctypes.c_double * numVars + objectCoeffs=NumVarDoubleArray() + #print "Get objective Values" + for v,val in lp.objective.iteritems(): + objectCoeffs[self.v2n[v]]=val + #values for variables + objectConst = ctypes.c_double(0.0) + NumVarStrArray = ctypes.c_char_p * numVars + colNames = NumVarStrArray() + lowerBounds = NumVarDoubleArray() + upperBounds = NumVarDoubleArray() + initValues = NumVarDoubleArray() + for v in lp.variables(): + colNames[self.v2n[v]] = str(v.name) + initValues[self.v2n[v]] = 0.0 + if v.lowBound != None: + lowerBounds[self.v2n[v]] = v.lowBound + else: + lowerBounds[self.v2n[v]] = -infBound + if v.upBound != None: + upperBounds[self.v2n[v]] = v.upBound + else: + upperBounds[self.v2n[v]] = infBound + #values for constraints + numRows =len(lp.constraints) + NumRowDoubleArray = ctypes.c_double * numRows + NumRowStrArray = ctypes.c_char_p * numRows + NumRowCharArray = ctypes.c_char * numRows + rhsValues = NumRowDoubleArray() + rangeValues = NumRowDoubleArray() + rowNames = NumRowStrArray() + rowType = NumRowCharArray() + self.c2n = {} + self.n2c = {} + i = 0 + for c in lp.constraints: + rhsValues[i] = -lp.constraints[c].constant + #for ranged constraints a<= constraint >=b + rangeValues[i] = 0.0 + rowNames[i] = str(c) + rowType[i] = senseDict[lp.constraints[c].sense] + self.c2n[c] = i + self.n2c[i] = c + i = i+1 + #return the coefficient matrix as a series of vectors + coeffs = lp.coefficients() + sparseMatrix = sparse.Matrix(range(numRows), range(numVars)) + for var,row,coeff in coeffs: + sparseMatrix.add(self.c2n[row], self.vname2n[var], coeff) + (numels, mystartsBase, mylenBase, myindBase, + myelemBase) = sparseMatrix.col_based_arrays() + elemBase = ctypesArrayFill(myelemBase, ctypes.c_double) + indBase = ctypesArrayFill(myindBase, ctypes.c_int) + startsBase = ctypesArrayFill(mystartsBase, ctypes.c_int) + lenBase = ctypesArrayFill(mylenBase, ctypes.c_int) + #MIP Variables + NumVarCharArray = ctypes.c_char * numVars + columnType = NumVarCharArray() + if lp.isMIP(): + for v in lp.variables(): + columnType[self.v2n[v]] = LpVarCategories[v.cat] + self.addedVars = numVars + self.addedRows = numRows + return (numVars, numRows, numels, rangeCount, + objSense, objectCoeffs, objectConst, + rhsValues, rangeValues, rowType, startsBase, lenBase, indBase, + elemBase, lowerBounds, upperBounds, initValues, colNames, + rowNames, columnType, self.n2v, self.n2c) + + +class LpSolver_CMD(LpSolver): + """A generic command line LP Solver""" + def __init__(self, path=None, keepFiles=0, mip=1, msg=1, options=[]): + LpSolver.__init__(self, mip, msg, options) + if path is None: + self.path = self.defaultPath() + else: + self.path = path + self.keepFiles = keepFiles + self.setTmpDir() + + def copy(self): + """Make a copy of self""" + + aCopy = LpSolver.copy(self) + aCopy.path = self.path + aCopy.keepFiles = self.keepFiles + aCopy.tmpDir = self.tmpDir + return aCopy + + def setTmpDir(self): + """Set the tmpDir attribute to a reasonnable location for a temporary + directory""" + if os.name != 'nt': + # On unix use /tmp by default + self.tmpDir = os.environ.get("TMPDIR", "/tmp") + self.tmpDir = os.environ.get("TMP", self.tmpDir) + else: + # On Windows use the current directory + self.tmpDir = os.environ.get("TMPDIR", "") + self.tmpDir = os.environ.get("TMP", self.tmpDir) + self.tmpDir = os.environ.get("TEMP", self.tmpDir) + if not os.path.isdir(self.tmpDir): + self.tmpDir = "" + elif not os.access(self.tmpDir, os.F_OK + os.W_OK): + self.tmpDir = "" + + def defaultPath(self): + raise NotImplementedError + + def executableExtension(name): + if os.name != 'nt': + return name + else: + return name+".exe" + executableExtension = staticmethod(executableExtension) + + def executable(command): + """Checks that the solver command is executable, + And returns the actual path to it.""" + + if os.path.isabs(command): + if os.path.exists(command) and os.access(command, os.X_OK): + return command + for path in os.environ.get("PATH", []).split(os.pathsep): + new_path = os.path.join(path, command) + if os.path.exists(new_path) and os.access(new_path, os.X_OK): + return os.path.join(path, command) + return False + executable = staticmethod(executable) + +class GLPK_CMD(LpSolver_CMD): + """The GLPK LP solver""" + def defaultPath(self): + return self.executableExtension(glpk_path) + + def available(self): + """True if the solver is available""" + return self.executable(self.path) + + def actualSolve(self, lp): + """Solve a well formulated lp problem""" + if not self.executable(self.path): + raise PulpSolverError, "PuLP: cannot execute "+self.path + if not self.keepFiles: + pid = os.getpid() + tmpLp = os.path.join(self.tmpDir, "%d-pulp.lp" % pid) + tmpSol = os.path.join(self.tmpDir, "%d-pulp.sol" % pid) + else: + tmpLp = lp.name+"-pulp.lp" + tmpSol = lp.name+"-pulp.sol" + lp.writeLP(tmpLp, writeSOS = 0) + proc = ["glpsol", "--cpxlp", tmpLp, "-o", tmpSol] + if not self.mip: proc.append('--nomip') + proc.extend(self.options) + + self.solution_time = clock() + if not self.msg: + proc[0] = self.path + pipe = open(os.devnull, 'w') + rc = subprocess.call(proc, stdout = pipe, + stderr = pipe) + if rc: + raise PulpSolverError, "PuLP: Error while trying to execute "+self.path + else: + if os.name != 'nt': + rc = os.spawnvp(os.P_WAIT, self.path, proc) + else: + rc = os.spawnv(os.P_WAIT, self.executable(self.path), proc) + if rc == 127: + raise PulpSolverError, "PuLP: Error while trying to execute "+self.path + self.solution_time += clock() + + if not os.path.exists(tmpSol): + raise PulpSolverError, "PuLP: Error while executing "+self.path + lp.status, values = self.readsol(tmpSol) + lp.assignVarsVals(values) + if not self.keepFiles: + try: os.remove(tmpLp) + except: pass + try: os.remove(tmpSol) + except: pass + return lp.status + + def readsol(self,filename): + """Read a GLPK solution file""" + f = file(filename) + f.readline() + rows = int(f.readline().split()[1]) + cols = int(f.readline().split()[1]) + f.readline() + statusString = f.readline()[12:-1] + glpkStatus = { + "INTEGER OPTIMAL":LpStatusOptimal, + "INTEGER NON-OPTIMAL":LpStatusOptimal, + "OPTIMAL":LpStatusOptimal, + "INFEASIBLE (FINAL)":LpStatusInfeasible, + "INTEGER UNDEFINED":LpStatusUndefined, + "UNBOUNDED":LpStatusUnbounded, + "UNDEFINED":LpStatusUndefined, + "INTEGER EMPTY":LpStatusInfeasible + } + #print "statusString ",statusString + if statusString not in glpkStatus: + raise PulpSolverError, "Unknown status returned by GLPK" + status = glpkStatus[statusString] + isInteger = statusString in ["INTEGER NON-OPTIMAL","INTEGER OPTIMAL","INTEGER UNDEFINED"] + values = {} + for i in range(4): f.readline() + for i in range(rows): + line = f.readline().split() + if len(line) ==2: f.readline() + for i in range(3): + f.readline() + for i in range(cols): + line = f.readline().split() + name = line[1] + if len(line) ==2: line = [0,0]+f.readline().split() + if isInteger: + if line[2] == "*": value = int(line[3]) + else: value = float(line[2]) + else: + value = float(line[3]) + values[name] = value + return status, values +GLPK = GLPK_CMD + +class CPLEX_CMD(LpSolver_CMD): + """The CPLEX LP solver""" + def defaultPath(self): + return self.executableExtension("cplex") + + def available(self): + """True if the solver is available""" + return self.executable(self.path) + + def actualSolve(self, lp): + """Solve a well formulated lp problem""" + if not self.executable(self.path): + raise PulpSolverError, "PuLP: cannot execute "+self.path + if not self.keepFiles: + pid = os.getpid() + tmpLp = os.path.join(self.tmpDir, "%d-pulp.lp" % pid) + tmpSol = os.path.join(self.tmpDir, "%d-pulp.sol" % pid) + else: + tmpLp = lp.name+"-pulp.lp" + tmpSol = lp.name+"-pulp.sol" + lp.writeLP(tmpLp, writeSOS = 1) + try: os.remove(tmpSol) + except: pass + if not self.msg: + cplex = subprocess.Popen(self.path, stdin = subprocess.PIPE, + stdout = subprocess.PIPE, stderr = subprocess.PIPE) + else: + cplex = subprocess.Popen(self.path, stdin = subprocess.PIPE) + cplex_cmds = "read "+tmpLp+"\n" + for option in self.options: + cplex_cmds += option+"\n" + if lp.isMIP(): + if self.mip: + cplex_cmds += "mipopt\n" + cplex_cmds += "change problem fixed\n" + else: + cplex_cmds += "change problem lp\n" + + cplex_cmds += "optimize\n" + cplex_cmds += "write "+tmpSol+"\n" + cplex_cmds += "quit\n" + cplex.communicate(cplex_cmds) + if cplex.returncode != 0: + raise PulpSolverError, "PuLP: Error while trying to execute "+self.path + if not self.keepFiles: + try: os.remove(tmpLp) + except: pass + if not os.path.exists(tmpSol): + status = LpStatusInfeasible + else: + status, values, reducedCosts, shadowPrices, slacks = self.readsol(tmpSol) + if not self.keepFiles: + try: os.remove(tmpSol) + except: pass + try: os.remove("cplex.log") + except: pass + if status != LpStatusInfeasible: + lp.assignVarsVals(values) + lp.assignVarsDj(reducedCosts) + lp.assignConsPi(shadowPrices) + lp.assignConsSlack(slacks) + lp.status = status + return status + + def readsol(self,filename): + """Read a CPLEX solution file""" + try: + import xml.etree.ElementTree as et + except ImportError: + import elementtree.ElementTree as et + solutionXML = et.parse(filename).getroot() + solutionheader = solutionXML.find("header") + statusString = solutionheader.get("solutionStatusString") + cplexStatus = { + "optimal":LpStatusOptimal, + } + if statusString not in cplexStatus: + raise PulpSolverError, "Unknown status returned by CPLEX: "+statusString + status = cplexStatus[statusString] + + shadowPrices = {} + slacks = {} + shadowPrices = {} + slacks = {} + constraints = solutionXML.find("linearConstraints") + for constraint in constraints: + name = constraint.get("name") + shadowPrice = constraint.get("dual") + slack = constraint.get("slack") + shadowPrices[name] = float(shadowPrice) + slacks[name] = float(slack) + + values = {} + reducedCosts = {} + for variable in solutionXML.find("variables"): + name = variable.get("name") + value = variable.get("value") + reducedCost = variable.get("reducedCost") + values[name] = float(value) + reducedCosts[name] = float(reducedCost) + + return status, values, reducedCosts, shadowPrices, slacks + +def CPLEX_DLL_load_dll(path): + """ + function that loads the DLL useful for debugging installation problems + """ + import ctypes + if os.name in ['nt','dos']: + lib = ctypes.windll.LoadLibrary(path) + else: + lib = ctypes.cdll.LoadLibrary(path) + return lib + +try: + import ctypes + class CPLEX_DLL(LpSolver): + """ + The CPLEX LP/MIP solver (via a Dynamic library DLL - windows or SO - Linux) + + This solver wraps the c library api of cplex. + It has been tested against cplex 11. + For api functions that have not been wrapped in this solver please use + the ctypes library interface to the cplex api in CPLEX_DLL.lib + """ + lib = CPLEX_DLL_load_dll(cplex_dll_path) + #parameters manually found in solver manual + CPX_PARAM_EPGAP = 2009 + CPX_PARAM_MEMORYEMPHASIS = 1082 # from Cplex 11.0 manual + CPX_PARAM_TILIM = 1039 + #argtypes for CPLEX functions + lib.CPXsetintparam.argtypes = [ctypes.c_void_p, + ctypes.c_int, ctypes.c_int] + lib.CPXsetdblparam.argtypes = [ctypes.c_void_p, ctypes.c_int, + ctypes.c_double] + lib.CPXfopen.argtypes = [ctypes.c_char_p, + ctypes.c_char_p] + lib.CPXfopen.restype = ctypes.c_void_p + lib.CPXsetlogfile.argtypes = [ctypes.c_void_p, + ctypes.c_void_p] + + def __init__(self, + mip = True, + msg = True, + timeLimit = None, + epgap = None, + logfilename = None, + emphasizeMemory = False): + """ + Initializes the CPLEX_DLL solver. + + @param mip: if False the solver will solve a MIP as an LP + @param msg: displays information from the solver to stdout + @param epgap: sets the integer bound gap + @param logfilename: sets the filename of the cplex logfile + @param emphasizeMemory: makes the solver emphasize Memory over + solution time + """ + LpSolver.__init__(self, mip, msg) + self.timeLimit = timeLimit + self.grabLicence() + self.setMemoryEmphasis(emphasizeMemory) + if epgap is not None: + self.changeEpgap(epgap) + if timeLimit is not None: + self.setTimeLimit(timeLimit) + if logfilename is not None: + self.setlogfile(logfilename) + else: + self.logfile = None + + def setlogfile(self, filename): + """ + sets the logfile for cplex output + """ + self.logfilep = CPLEX_DLL.lib.CPXfopen(filename, "w") + CPLEX_DLL.lib.CPXsetlogfile(self.env, self.logfilep) + + def changeEpgap(self, epgap = 10**-4): + """ + Change cplex solver integer bound gap tolerence + """ + CPLEX_DLL.lib.CPXsetdblparam(self.env,CPLEX_DLL.CPX_PARAM_EPGAP, + epgap) + + def setTimeLimit(self, timeLimit = 0.0): + """ + Make cplex limit the time it takes --added CBM 8/28/09 + """ + CPLEX_DLL.lib.CPXsetdblparam(self.env,CPLEX_DLL.CPX_PARAM_TILIM, + float(timeLimit)) + + def setMemoryEmphasis(self, yesOrNo = False): + """ + Make cplex try to conserve memory at the expense of + performance. + """ + CPLEX_DLL.lib.CPXsetintparam(self.env, + CPLEX_DLL.CPX_PARAM_MEMORYEMPHASIS,yesOrNo) + + def findSolutionValues(self, lp, numcols, numrows): + byref = ctypes.byref + solutionStatus = ctypes.c_int() + objectiveValue = ctypes.c_double() + x = (ctypes.c_double * numcols)() + pi = (ctypes.c_double * numrows)() + slack = (ctypes.c_double * numrows)() + dj = (ctypes.c_double * numcols)() + status= CPLEX_DLL.lib.CPXsolwrite(self.env, self.hprob, + "CplexTest.sol") + if lp.isMIP(): + solutionStatus.value = CPLEX_DLL.lib.CPXgetstat(self.env, + self.hprob) + status = CPLEX_DLL.lib.CPXgetobjval(self.env, self.hprob, + byref(objectiveValue)) + if status != 0 and status != 1217: #no solution exists + raise PulpSolverError, ("Error in CPXgetobjval status=" + + str(status)) + + status = CPLEX_DLL.lib.CPXgetx(self.env, self.hprob, + byref(x), 0, numcols - 1) + if status != 0 and status != 1217: + raise PulpSolverError, "Error in CPXgetx status=" + str(status) + else: + status = CPLEX_DLL.lib.CPXsolution(self.env, self.hprob, + byref(solutionStatus), + byref(objectiveValue), + byref(x), byref(pi), + byref(slack), byref(dj)) + # 102 is the cplex return status for + # integer optimal within tolerance + # and is useful for breaking symmetry. + CplexLpStatus = {1: LpStatusOptimal, 3: LpStatusInfeasible, + 2: LpStatusUnbounded, 0: LpStatusNotSolved, + 101: LpStatusOptimal, 102: LpStatusOptimal, + 103: LpStatusInfeasible} + #populate pulp solution values + variablevalues = {} + variabledjvalues = {} + constraintpivalues = {} + constraintslackvalues = {} + for i in range(numcols): + variablevalues[self.n2v[i].name] = x[i] + variabledjvalues[self.n2v[i].name] = dj[i] + lp.assignVarsVals(variablevalues) + lp.assignVarsDj(variabledjvalues) + #put pi and slack variables against the constraints + for i in range(numrows): + constraintpivalues[self.n2c[i]] = pi[i] + constraintslackvalues[self.n2c[i]] = slack[i] + lp.assignConsPi(constraintpivalues) + lp.assignConsSlack(constraintslackvalues) + #TODO: clear up the name of self.n2c + if self.msg: + print "Cplex status=", solutionStatus.value + lp.resolveOK = True + for var in lp.variables(): + var.isModified = False + lp.status = CplexLpStatus.get(solutionStatus.value, + LpStatusUndefined) + return lp.status + + def __del__(self): + #LpSolver.__del__(self) + self.releaseLicence() + + def available(self): + """True if the solver is available""" + return True + + def grabLicence(self): + """ + Returns True if a CPLEX licence can be obtained. + The licence is kept until releaseLicence() is called. + """ + status = ctypes.c_int() + # If the config file allows to do so (non null params), try to + # grab a runtime license. + if ilm_cplex_license and ilm_cplex_license_signature: + runtime_status = CPLEX_DLL.lib.CPXsetstaringsol( + ilm_cplex_license, + ilm_cplex_license_signature) + # if runtime_status is not zero, running with a runtime + # license will fail. However, no error is thrown (yet) + # because the second call might still succeed if the user + # has another license. Let us forgive bad user + # configuration: + if not (runtime_status == 0) and self.msg: + print ( + "CPLEX library failed to load the runtime license" + + "the call returned status=%s" % str(runtime_status) + + "Please check the pulp config file.") + self.env = CPLEX_DLL.lib.CPXopenCPLEX(ctypes.byref(status)) + if not(status.value == 0): + raise PulpSolverError, ("CPLEX library failed on " + + "CPXopenCPLEX status=" + str(status)) + + + def releaseLicence(self): + """Release a previously obtained CPLEX licence""" + if getattr(self,"env",False): + status=CPLEX_DLL.lib.CPXcloseCPLEX(self.env) + else: + raise PulpSolverError, "No CPLEX enviroment to close" + + def callSolver(self, isMIP): + """Solves the problem with cplex + """ + #solve the problem + self.cplexTime = -clock() + if isMIP and self.mip: + status= CPLEX_DLL.lib.CPXmipopt(self.env, self.hprob) + if status != 0: + raise PulpSolverError, ("Error in CPXmipopt status=" + + str(status)) + else: + status = CPLEX_DLL.lib.CPXlpopt(self.env, self.hprob) + if status != 0: + raise PulpSolverError, ("Error in CPXlpopt status=" + + str(status)) + self.cplexTime += clock() + + def actualSolve(self, lp): + """Solve a well formulated lp problem""" + #TODO alter so that msg parameter is handled correctly + status = ctypes.c_int() + byref = ctypes.byref #shortcut to function + self.hprob = CPLEX_DLL.lib.CPXcreateprob(self.env, + byref(status), lp.name) + if status.value != 0: + raise PulpSolverError, ("Error in CPXcreateprob status=" + + str(status)) + (numcols, numrows, numels, rangeCount, + objSense, obj, objconst, + rhs, rangeValues, rowSense, matbeg, matcnt, matind, + matval, lb, ub, initValues, colname, + rowname, xctype, n2v, n2c )= self.getCplexStyleArrays(lp) + status.value = CPLEX_DLL.lib.CPXcopylpwnames (self.env, self.hprob, + numcols, numrows, + objSense, obj, rhs, rowSense, matbeg, matcnt, + matind, matval, lb, ub, None, colname, rowname) + if status.value != 0: + raise PulpSolverError, ("Error in CPXcopylpwnames status=" + + str(status)) + if lp.isMIP() and self.mip: + status.value = CPLEX_DLL.lib.CPXcopyctype(self.env, + self.hprob, + xctype) + if status.value != 0: + raise PulpSolverError, ("Error in CPXcopyctype status=" + + str(status)) + #set the initial solution + self.callSolver(lp.isMIP()) + #get the solution information + solutionStatus = self.findSolutionValues(lp, numcols, numrows) + for var in lp.variables(): + var.modified = False + return solutionStatus + + + def actualResolve(self,lp): + """looks at which variables have been modified and changes them + """ + #TODO: Add changing variables not just adding them + #TODO: look at constraints + modifiedVars = [var for var in lp.variables() if var.modified] + #assumes that all variables flagged as modified + #need to be added to the problem + newVars = modifiedVars + #print newVars + self.v2n.update([(var, i+self.addedVars) + for i,var in enumerate(newVars)]) + self.n2v.update([(i+self.addedVars, var) + for i,var in enumerate(newVars)]) + self.vname2n.update([(var.name, i+self.addedVars) + for i,var in enumerate(newVars)]) + oldVars = self.addedVars + self.addedVars += len(newVars) + (ccnt,nzcnt,obj,cmatbeg, + cmatlen, cmatind,cmatval, + lb,ub, initvals, + colname, coltype) = self.getSparseCols(newVars, lp, oldVars, + defBound = 1e20) + CPXaddcolsStatus = CPLEX_DLL.lib.CPXaddcols(self.env, self.hprob, + ccnt, nzcnt, + obj,cmatbeg, + cmatind,cmatval, + lb,ub,colname) + #add the column types + if lp.isMIP() and self.mip: + indices = (ctypes.c_int * len(newVars))() + for i,var in enumerate(newVars): + indices[i] = oldVars +i + CPXchgctypeStatus = CPLEX_DLL.lib.CPXchgctype (self.env, + self.hprob, + ccnt, indices, coltype); + #solve the problem + self.callSolver(lp.isMIP()) + #get the solution information + solutionStatus = self.findSolutionValues(lp, self.addedVars, + self.addedRows) + for var in modifiedVars: + var.modified = False + return solutionStatus + + def getSparseCols(self, vars, lp, offset = 0, defBound = 1e20): + """ + outputs the variables in var as a sparse matrix, + suitable for cplex and Coin + + Copyright (c) Stuart Mitchell 2007 + """ + numVars = len(vars) + obj = (ctypes.c_double * numVars)() + cmatbeg = (ctypes.c_int * numVars)() + mycmatind = [] + mycmatval = [] + rangeCount = 0 + #values for variables + colNames = (ctypes.c_char_p * numVars)() + lowerBounds = (ctypes.c_double * numVars)() + upperBounds = (ctypes.c_double * numVars)() + initValues = (ctypes.c_double * numVars)() + i=0 + for v in vars: + colNames[i] = str(v.name) + initValues[i] = v.init + if v.lowBound != None: + lowerBounds[i] = v.lowBound + else: + lowerBounds[i] = -defBound + if v.upBound != None: + upperBounds[i] = v.upBound + else: + upperBounds[i] = defBound + i+= 1 + #create the new variables + #values for constraints + #return the coefficient matrix as a series of vectors + myobjectCoeffs = {} + numRows = len(lp.constraints) + sparseMatrix = sparse.Matrix(range(numRows), range(numVars)) + for var in vars: + for row,coeff in var.expression.iteritems(): + if row.name == lp.objective.name: + myobjectCoeffs[var] = coeff + else: + sparseMatrix.add(self.c2n[row.name], self.v2n[var] - offset, coeff) + #objective values + objectCoeffs = (ctypes.c_double * numVars)() + for var in vars: + objectCoeffs[self.v2n[var]-offset] = myobjectCoeffs[var] + (numels, mystartsBase, mylenBase, myindBase, + myelemBase) = sparseMatrix.col_based_arrays() + elemBase = ctypesArrayFill(myelemBase, ctypes.c_double) + indBase = ctypesArrayFill(myindBase, ctypes.c_int) + startsBase = ctypesArrayFill(mystartsBase, ctypes.c_int) + lenBase = ctypesArrayFill(mylenBase, ctypes.c_int) + #MIP Variables + NumVarCharArray = ctypes.c_char * numVars + columnType = NumVarCharArray() + if lp.isMIP(): + CplexLpCategories = {LpContinuous: "C", + LpInteger: "I"} + for v in vars: + columnType[self.v2n[v] - offset] = CplexLpCategories[v.cat] + return numVars, numels, objectCoeffs, \ + startsBase, lenBase, indBase, \ + elemBase, lowerBounds, upperBounds, initValues, colNames, \ + columnType + + + + CPLEX = CPLEX_DLL +except (ImportError,OSError): + class CPLEX_DLL(LpSolver): + """The CPLEX LP/MIP solver PHANTOM Something went wrong!!!!""" + def available(self): + """True if the solver is available""" + return False + def actualSolve(self, lp): + """Solve a well formulated lp problem""" + raise PulpSolverError, "CPLEX_DLL: Not Available" + CPLEX = CPLEX_CMD + +try: + import cplex +except (ImportError): + class CPLEX_PY(LpSolver): + """The CPLEX LP/MIP solver from python PHANTOM Something went wrong!!!!""" + def available(self): + """True if the solver is available""" + return False + def actualSolve(self, lp): + """Solve a well formulated lp problem""" + raise PulpSolverError, "CPLEX_PY: Not Available" +else: + class CPLEX_PY(LpSolver): + """ + The CPLEX LP/MIP solver (via a Python Binding) + + This solver wraps the python api of cplex. + It has been tested against cplex 12.3. + For api functions that have not been wrapped in this solver please use + the base cplex classes + """ + CplexLpStatus = {cplex.Cplex.solution.status.MIP_optimal: LpStatusOptimal, + cplex.Cplex.solution.status.optimal: LpStatusOptimal, + cplex.Cplex.solution.status.optimal_tolerance: LpStatusOptimal, + cplex.Cplex.solution.status.infeasible: LpStatusInfeasible, + cplex.Cplex.solution.status.infeasible_or_unbounded: LpStatusInfeasible, + cplex.Cplex.solution.status.MIP_infeasible: LpStatusInfeasible, + cplex.Cplex.solution.status.MIP_infeasible_or_unbounded: LpStatusInfeasible, + cplex.Cplex.solution.status.unbounded: LpStatusUnbounded, + cplex.Cplex.solution.status.MIP_unbounded: LpStatusUnbounded, + cplex.Cplex.solution.status.abort_dual_obj_limit: LpStatusNotSolved, + cplex.Cplex.solution.status.abort_iteration_limit: LpStatusNotSolved, + cplex.Cplex.solution.status.abort_obj_limit: LpStatusNotSolved, + cplex.Cplex.solution.status.abort_relaxed: LpStatusNotSolved, + cplex.Cplex.solution.status.abort_time_limit: LpStatusNotSolved, + cplex.Cplex.solution.status.abort_user: LpStatusNotSolved, + } + + def __init__(self, + mip = True, + msg = True, + timeLimit = None, + epgap = None, + logfilename = None): + """ + Initializes the CPLEX_PY solver. + + @param mip: if False the solver will solve a MIP as an LP + @param msg: displays information from the solver to stdout + @param epgap: sets the integer bound gap + @param logfilename: sets the filename of the cplex logfile + """ + LpSolver.__init__(self, mip, msg) + self.timeLimit = timeLimit + self.epgap = epgap + self.logfilename = logfilename + + def available(self): + """True if the solver is available""" + return True + + def actualSolve(self, lp, callback = None): + """ + Solve a well formulated lp problem + + creates a gurobi model, variables and constraints and attaches + them to the lp model which it then solves + """ + self.buildSolverModel(lp) + #set the initial solution + log.debug("Solve the Model using cplex") + self.callSolver(lp) + #get the solution information + solutionStatus = self.findSolutionValues(lp) + for var in lp.variables(): + var.modified = False + for constraint in lp.constraints.values(): + constraint.modified = False + return solutionStatus + + def buildSolverModel(self, lp): + """ + Takes the pulp lp model and translates it into a cplex model + """ + self.n2v = dict((var.name, var) for var in lp.variables()) + if len(self.n2v) != len(lp.variables()): + raise PulpSolverError( + 'Variables must have unique names for cplex solver') + log.debug("create the cplex model") + self.solverModel = lp.solverModel = cplex.Cplex() + log.debug("set the name of the problem") + if not self.mip: + self.solverModel.set_problem_name(lp.name) + log.debug("set the sense of the problem") + if lp.sense == LpMaximize: + lp.solverModel.objective.set_sense( + lp.solverModel.objective.sense.maximize) + obj = [float(lp.objective.get(var, 0.0)) for var in lp.variables()] + def cplex_var_lb(var): + if var.lowBound is not None: + return float(var.lowBound) + else: + return -cplex.infinity + lb = [cplex_var_lb(var) for var in lp.variables()] + def cplex_var_ub(var): + if var.upBound is not None: + return float(var.upBound) + else: + return cplex.infinity + ub = [cplex_var_ub(var) for var in lp.variables()] + colnames = [var.name for var in lp.variables()] + def cplex_var_types(var): + if var.cat == LpInteger: + return 'I' + else: + return 'C' + ctype = [cplex_var_types(var) for var in lp.variables()] + ctype = "".join(ctype) + lp.solverModel.variables.add(obj=obj, lb=lb, ub=ub, types=ctype, + names=colnames) + rows = [] + senses = [] + rhs = [] + rownames = [] + for name,constraint in lp.constraints.items(): + #build the expression + expr = [(var.name, float(coeff)) for var, coeff in constraint.items()] + if not expr: + #if the constraint is empty + rows.append(([],[])) + else: + rows.append(zip(*expr)) + if constraint.sense == LpConstraintLE: + senses.append('L') + elif constraint.sense == LpConstraintGE: + senses.append('G') + elif constraint.sense == LpConstraintEQ: + senses.append('E') + else: + raise PulpSolverError, 'Detected an invalid constraint type' + rownames.append(name) + rhs.append(float(-constraint.constant)) + lp.solverModel.linear_constraints.add(lin_expr=rows, senses=senses, + rhs=rhs, names=rownames) + log.debug("set the type of the problem") + if not self.mip: + self.solverModel.set_problem_type(cplex.Cplex.problem_type.LP) + log.debug("set the logging") + if not self.msg: + self.solverModel.set_error_stream(None) + self.solverModel.set_log_stream(None) + self.solverModel.set_warning_stream(None) + self.solverModel.set_results_stream(None) + if self.logfilename is not None: + self.setlogfile(self.logfilename) + if self.epgap is not None: + self.changeEpgap(self.epgap) + if self.timeLimit is not None: + self.setTimeLimit(self.timeLimit) + + def setlogfile(self, filename): + """ + sets the logfile for cplex output + """ + self.solverModel.set_log_stream(filename) + + def changeEpgap(self, epgap = 10**-4): + """ + Change cplex solver integer bound gap tolerence + """ + self.solverModel.parameters.mip.tolerances.mipgap.set(epgap) + + def setTimeLimit(self, timeLimit = 0.0): + """ + Make cplex limit the time it takes --added CBM 8/28/09 + """ + self.solverModel.parameters.timelimit.set(timeLimit) + + def callSolver(self, isMIP): + """Solves the problem with cplex + """ + #solve the problem + self.solveTime = -clock() + self.solverModel.solve() + self.solveTime += clock() + + def findSolutionValues(self, lp): + lp.cplex_status = lp.solverModel.solution.get_status() + lp.status = self.CplexLpStatus.get(lp.cplex_status, LpStatusUndefined) + var_names = [var.name for var in lp.variables()] + con_names = [con for con in lp.constraints] + try: + objectiveValue = lp.solverModel.solution.get_objective_value() + variablevalues = dict(zip(var_names, lp.solverModel.solution.get_values(var_names))) + lp.assignVarsVals(variablevalues) + constraintslackvalues = dict(zip(con_names, lp.solverModel.solution.get_linear_slacks(con_names))) + lp.assignConsSlack(constraintslackvalues) + if lp.solverModel.get_problem_type == cplex.Cplex.problem_type.LP: + variabledjvalues = dict(zip(var_names, lp.solverModel.solution.get_reduced_costs(var_names))) + lp.assignVarsDj(variabledjvalues) + constraintpivalues = dict(zip(con_names, lp.solverModel.solution.get_dual_values(con_names))) + lp.assignConsPi(constraintpivalues) + except cplex.exceptions.CplexSolverError: + #raises this error when there is no solution + pass + #put pi and slack variables against the constraints + #TODO: clear up the name of self.n2c + if self.msg: + print "Cplex status=", lp.cplex_status + lp.resolveOK = True + for var in lp.variables(): + var.isModified = False + return lp.status + + def actualResolve(self,lp): + """ + looks at which variables have been modified and changes them + """ + raise NotImplementedError("Resolves in CPLEX_PY not yet implemented") + + CPLEX = CPLEX_PY + + +class XPRESS(LpSolver_CMD): + """The XPRESS LP solver""" + def defaultPath(self): + return self.executableExtension("optimizer") + + def available(self): + """True if the solver is available""" + return self.executable(self.path) + + def actualSolve(self, lp): + """Solve a well formulated lp problem""" + if not self.executable(self.path): + raise PulpSolverError, "PuLP: cannot execute "+self.path + if not self.keepFiles: + pid = os.getpid() + tmpLp = os.path.join(self.tmpDir, "%d-pulp.lp" % pid) + tmpSol = os.path.join(self.tmpDir, "%d-pulp.prt" % pid) + else: + tmpLp = lp.name+"-pulp.lp" + tmpSol = lp.name+"-pulp.prt" + lp.writeLP(tmpLp, writeSOS = 1, mip = self.mip) + if not self.msg: + xpress = os.popen(self.path+" "+lp.name+" > /dev/null 2> /dev/null", "w") + else: + xpress = os.popen(self.path+" "+lp.name, "w") + xpress.write("READPROB "+tmpLp+"\n") + if lp.sense == LpMaximize: + xpress.write("MAXIM\n") + else: + xpress.write("MINIM\n") + if lp.isMIP() and self.mip: + xpress.write("GLOBAL\n") + xpress.write("WRITEPRTSOL "+tmpSol+"\n") + xpress.write("QUIT\n") + if xpress.close() != None: + raise PulpSolverError, "PuLP: Error while executing "+self.path + status, values = self.readsol(tmpSol) + if not self.keepFiles: + try: os.remove(tmpLp) + except: pass + try: os.remove(tmpSol) + except: pass + lp.status = status + lp.assignVarsVals(values) + if abs(lp.infeasibilityGap(self.mip)) > 1e-5: # Arbitrary + lp.status = LpStatusInfeasible + return lp.status + + def readsol(self,filename): + """Read an XPRESS solution file""" + f = file(filename) + for i in range(6): f.readline() + l = f.readline().split() + + rows = int(l[2]) + cols = int(l[5]) + for i in range(3): f.readline() + statusString = f.readline().split()[0] + xpressStatus = { + "Optimal":LpStatusOptimal, + } + if statusString not in xpressStatus: + raise PulpSolverError, "Unknow status returned by XPRESS: "+statusString + status = xpressStatus[statusString] + values = {} + while 1: + l = f.readline() + if l == "": break + line = l.split() + if len(line) and line[0] == 'C': + name = line[2] + value = float(line[4]) + values[name] = value + return status, values + +class COIN_CMD(LpSolver_CMD): + """The COIN CLP/CBC LP solver + now only uses cbc + """ + + def defaultPath(self): + return self.executableExtension(cbc_path) + + def __init__(self, path = None, keepFiles = 0, mip = 1, + msg = 0, cuts = None, presolve = None, dual = None, + strong = None, options = [], + fracGap = None, maxSeconds = None, threads = None): + LpSolver_CMD.__init__(self, path, keepFiles, mip, msg, options) + self.cuts = cuts + self.presolve = presolve + self.dual = dual + self.strong = strong + self.fracGap = fracGap + self.maxSeconds = maxSeconds + self.threads = threads + #TODO hope this gets fixed in cbc as it does not like the c:\ in windows paths + if os.name == 'nt': + self.tmpDir = '' + + def copy(self): + """Make a copy of self""" + aCopy = LpSolver_CMD.copy(self) + aCopy.cuts = self.cuts + aCopy.presolve = self.presolve + aCopy.dual = self.dual + aCopy.strong = self.strong + return aCopy + + def actualSolve(self, lp, **kwargs): + """Solve a well formulated lp problem""" + return self.solve_CBC(lp, **kwargs) + + def available(self): + """True if the solver is available""" + return self.executable(self.path) + + def solve_CBC(self, lp, use_mps=True): + """Solve a MIP problem using CBC""" + if not self.executable(self.path): + raise PulpSolverError, "Pulp: cannot execute %s cwd: %s"%(self.path, + os.getcwd()) + if not self.keepFiles: + pid = os.getpid() + tmpLp = os.path.join(self.tmpDir, "%d-pulp.lp" % pid) + tmpMps = os.path.join(self.tmpDir, "%d-pulp.mps" % pid) + tmpSol = os.path.join(self.tmpDir, "%d-pulp.sol" % pid) + else: + tmpLp = lp.name+"-pulp.lp" + tmpMps = lp.name+"-pulp.mps" + tmpSol = lp.name+"-pulp.sol" + if use_mps: + vs, variablesNames, constraintsNames, objectiveName = lp.writeMPS( + tmpMps, rename = 1) + cmds = ' '+tmpMps+" " + if lp.sense == LpMaximize: + cmds += 'max ' + else: + lp.writeLP(tmpLp) + cmds = ' '+tmpLp+" " + if self.threads: + cmds += "threads %s "%self.threads + if self.fracGap is not None: + cmds += "ratio %s "%self.fracGap + if self.maxSeconds is not None: + cmds += "sec %s "%self.maxSeconds + if self.presolve: + cmds += "presolve on " + if self.strong: + cmds += "strong %d " % self.strong + if self.cuts: + cmds += "gomory on " + #cbc.write("oddhole on " + cmds += "knapsack on " + cmds += "probing on " + for option in self.options: + cmds += option+" " + if self.mip: + cmds += "branch " + else: + cmds += "initialSolve " + if lp.isMIP: + cmds += "printingOptions rows " + cmds += "solution "+tmpSol+" " + if self.msg: + pipe = None + else: + pipe = open(os.devnull, 'w') + logging.debug(self.path + cmds) + cbc = subprocess.Popen((self.path + cmds).split(), stdout = pipe, + stderr = pipe) + if cbc.wait() != 0: + raise PulpSolverError, "Pulp: Error while trying to execute " + \ + self.path + if not os.path.exists(tmpSol): + raise PulpSolverError, "Pulp: Error while executing "+self.path + if use_mps: + lp.status, values, reducedCosts, shadowPrices, slacks = self.readsol_MPS( + tmpSol, lp, lp.variables(), + variablesNames, constraintsNames, objectiveName) + else: + lp.status, values, reducedCosts, shadowPrices, slacks = self.readsol_LP( + tmpSol, lp, lp.variables()) + lp.assignVarsVals(values) + lp.assignVarsDj(reducedCosts) + lp.assignConsPi(shadowPrices) + lp.assignConsSlack(slacks, activity=True) + if not self.keepFiles: + try: + os.remove(tmpLp) + except: + pass + try: + os.remove(tmpSol) + except: + pass + return lp.status + + def readsol_MPS(self, filename, lp, vs, variablesNames, constraintsNames, + objectiveName): + """ + Read a CBC solution file generated from an mps file (different names) + """ + values = {} + + reverseVn = {} + for k, n in variablesNames.iteritems(): + reverseVn[n] = k + reverseCn = {} + for k, n in constraintsNames.iteritems(): + reverseCn[n] = k + + + for v in vs: + values[v.name] = 0.0 + + reducedCosts = {} + shadowPrices = {} + slacks = {} + cbcStatus = {'Optimal': LpStatusOptimal, + 'Infeasible': LpStatusInfeasible, + 'Unbounded': LpStatusUnbounded, + 'Stopped': LpStatusNotSolved} + f = file(filename) + statusstr = f.readline().split()[0] + status = cbcStatus.get(statusstr, LpStatusUndefined) + for l in f: + if len(l)<=2: + break + l = l.split() + vn = l[1] + val = l[2] + dj = l[3] + if vn in reverseVn: + values[reverseVn[vn]] = float(val) + reducedCosts[reverseVn[vn]] = float(dj) + if vn in reverseCn: + slacks[reverseCn[vn]] = float(val) + shadowPrices[reverseCn[vn]] = float(dj) + return status, values, reducedCosts, shadowPrices, slacks + + def readsol_LP(self, filename, lp, vs): + """ + Read a CBC solution file generated from an lp (good names) + """ + values = {} + reducedCosts = {} + shadowPrices = {} + slacks = {} + for v in vs: + values[v.name] = 0.0 + cbcStatus = {'Optimal': LpStatusOptimal, + 'Infeasible': LpStatusInfeasible, + 'Unbounded': LpStatusUnbounded, + 'Stopped': LpStatusNotSolved} + f = file(filename) + statusstr = f.readline().split()[0] + status = cbcStatus.get(statusstr, LpStatusUndefined) + for l in f: + if len(l)<=2: + break + l = l.split() + vn = l[1] + val = l[2] + dj = l[3] + if vn in values: + values[vn] = float(val) + reducedCosts[vn] = float(dj) + if vn in lp.constraints: + slacks[vn] = float(val) + shadowPrices[vn] = float(dj) + return status, values, reducedCosts, shadowPrices, slacks + +COIN = COIN_CMD + +class PULP_CBC_CMD(COIN_CMD): + """ + This solver uses a precompiled version of cbc provided with the package + """ + arch_pulp_cbc_path = pulp_cbc_path + try: + if os.name != 'nt': + #not windows + is_64bits = sys.maxsize > 2**32 + if is_64bits: + arch_pulp_cbc_path = pulp_cbc_path + '-64' + else: + arch_pulp_cbc_path = pulp_cbc_path + '-32' + if not os.access(arch_pulp_cbc_path, os.X_OK): + import stat + os.chmod(arch_pulp_cbc_path, stat.S_IXUSR + stat.S_IXOTH) + except: #probably due to incorrect permissions + def available(self): + """True if the solver is available""" + return False + def actualSolve(self, lp, callback = None): + """Solve a well formulated lp problem""" + raise PulpSolverError, "PULP_CBC_CMD: Not Available (check permissions on %s)" % arch_pulp_cbc_path + else: + def __init__(self, path=None, *args, **kwargs): + """ + just loads up COIN_CMD with the path set + """ + if path is not None: + raise PulpSolverError('Use COIN_CMD if you want to set a path') + #check that the file is executable + COIN_CMD.__init__(self, path=self.arch_pulp_cbc_path, *args, **kwargs) + +def COINMP_DLL_load_dll(path): + """ + function that loads the DLL useful for debugging installation problems + """ + import ctypes + if os.name == 'nt': + lib = ctypes.windll.LoadLibrary(path[-1]) + else: + #linux hack to get working + mode = ctypes.RTLD_GLOBAL + for libpath in path[:-1]: + #RTLD_LAZY = 0x00001 + ctypes.CDLL(libpath, mode = mode) + lib = ctypes.CDLL(path[-1], mode = mode) + return lib + +class COINMP_DLL(LpSolver): + """ + The COIN_MP LP MIP solver (via a DLL or linux so) + + :param timeLimit: The number of seconds before forcing the solver to exit + :param epgap: The fractional mip tolerance + """ + try: + lib = COINMP_DLL_load_dll(coinMP_path) + except (ImportError, OSError): + @classmethod + def available(cls): + """True if the solver is available""" + return False + def actualSolve(self, lp): + """Solve a well formulated lp problem""" + raise PulpSolverError, "COINMP_DLL: Not Available" + else: + COIN_INT_LOGLEVEL = 7 + COIN_REAL_MAXSECONDS = 16 + COIN_REAL_MIPMAXSEC = 19 + COIN_REAL_MIPFRACGAP = 34 + lib.CoinGetInfinity.restype = ctypes.c_double + lib.CoinGetVersionStr.restype = ctypes.c_char_p + lib.CoinGetSolutionText.restype=ctypes.c_char_p + lib.CoinGetObjectValue.restype=ctypes.c_double + lib.CoinGetMipBestBound.restype=ctypes.c_double + + def __init__(self, mip = 1, msg = 1, cuts = 1, presolve = 1, dual = 1, + crash = 0, scale = 1, rounding = 1, integerPresolve = 1, strong = 5, + timeLimit = None, epgap = None): + LpSolver.__init__(self, mip, msg) + self.maxSeconds = None + if timeLimit is not None: + self.maxSeconds = float(timeLimit) + self.fracGap = None + if epgap is not None: + self.fracGap = float(epgap) + #Todo: these options are not yet implemented + self.cuts = cuts + self.presolve = presolve + self.dual = dual + self.crash = crash + self.scale = scale + self.rounding = rounding + self.integerPresolve = integerPresolve + self.strong = strong + + def copy(self): + """Make a copy of self""" + + aCopy = LpSolver.copy() + aCopy.cuts = self.cuts + aCopy.presolve = self.presolve + aCopy.dual = self.dual + aCopy.crash = self.crash + aCopy.scale = self.scale + aCopy.rounding = self.rounding + aCopy.integerPresolve = self.integerPresolve + aCopy.strong = self.strong + return aCopy + + @classmethod + def available(cls): + """True if the solver is available""" + return True + + def getSolverVersion(self): + """ + returns a solver version string + + example: + >>> COINMP_DLL().getSolverVersion() # doctest: +ELLIPSIS + '...' + """ + return self.lib.CoinGetVersionStr() + + def actualSolve(self, lp): + """Solve a well formulated lp problem""" + #TODO alter so that msg parameter is handled correctly + self.debug = 0 + #initialise solver + self.lib.CoinInitSolver("") + #create problem + self.hProb = hProb = self.lib.CoinCreateProblem(lp.name); + #set problem options + if self.maxSeconds: + if self.mip: + self.lib.CoinSetRealOption(hProb, self.COIN_REAL_MIPMAXSEC, + ctypes.c_double(self.maxSeconds)) + else: + self.lib.CoinSetRealOption(hProb, self.COIN_REAL_MAXSECONDS, + ctypes.c_double(self.maxSeconds)) + if self.fracGap: + #Hopefully this is the bound gap tolerance + self.lib.CoinSetRealOption(hProb, self.COIN_REAL_MIPFRACGAP, + ctypes.c_double(self.fracGap)) + #CoinGetInfinity is needed for varibles with no bounds + coinDblMax = self.lib.CoinGetInfinity() + if self.debug: print "Before getCoinMPArrays" + (numVars, numRows, numels, rangeCount, + objectSense, objectCoeffs, objectConst, + rhsValues, rangeValues, rowType, startsBase, + lenBase, indBase, + elemBase, lowerBounds, upperBounds, initValues, colNames, + rowNames, columnType, n2v, n2c) = self.getCplexStyleArrays(lp) + self.lib.CoinLoadProblem(hProb, + numVars, numRows, numels, rangeCount, + objectSense, objectConst, objectCoeffs, + lowerBounds, upperBounds, rowType, + rhsValues, rangeValues, startsBase, + lenBase, indBase, elemBase, + colNames, rowNames, "Objective") + if lp.isMIP() and self.mip: + self.lib.CoinLoadInteger(hProb,columnType) + if self.msg == 0: + #close stdout to get rid of messages + tempfile = open(mktemp(),'w') + savestdout = os.dup(1) + os.close(1) + if os.dup(tempfile.fileno()) != 1: + raise PulpSolverError, "couldn't redirect stdout - dup() error" + self.coinTime = -clock() + self.lib.CoinOptimizeProblem(hProb, 0); + self.coinTime += clock() + + if self.msg == 0: + #reopen stdout + os.close(1) + os.dup(savestdout) + os.close(savestdout) + + CoinLpStatus = {0:LpStatusOptimal, + 1:LpStatusInfeasible, + 2:LpStatusInfeasible, + 3:LpStatusNotSolved, + 4:LpStatusNotSolved, + 5:LpStatusNotSolved, + -1:LpStatusUndefined + } + solutionStatus = self.lib.CoinGetSolutionStatus(hProb) + solutionText = self.lib.CoinGetSolutionText(hProb,solutionStatus) + objectValue = self.lib.CoinGetObjectValue(hProb) + + #get the solution values + NumVarDoubleArray = ctypes.c_double * numVars + NumRowsDoubleArray = ctypes.c_double * numRows + cActivity = NumVarDoubleArray() + cReducedCost = NumVarDoubleArray() + cSlackValues = NumRowsDoubleArray() + cShadowPrices = NumRowsDoubleArray() + self.lib.CoinGetSolutionValues(hProb, ctypes.byref(cActivity), + ctypes.byref(cReducedCost), + ctypes.byref(cSlackValues), + ctypes.byref(cShadowPrices)) + + variablevalues = {} + variabledjvalues = {} + constraintpivalues = {} + constraintslackvalues = {} + if lp.isMIP() and self.mip: + lp.bestBound = self.lib.CoinGetMipBestBound(hProb) + for i in range(numVars): + variablevalues[self.n2v[i].name] = cActivity[i] + variabledjvalues[self.n2v[i].name] = cReducedCost[i] + lp.assignVarsVals(variablevalues) + lp.assignVarsDj(variabledjvalues) + #put pi and slack variables against the constraints + for i in range(numRows): + constraintpivalues[self.n2c[i]] = cShadowPrices[i] + constraintslackvalues[self.n2c[i]] = \ + rhsValues[i] - cSlackValues[i] + lp.assignConsPi(constraintpivalues) + lp.assignConsSlack(constraintslackvalues) + + self.lib.CoinFreeSolver() + lp.status = CoinLpStatus[self.lib.CoinGetSolutionStatus(hProb)] + return lp.status + +if COINMP_DLL.available(): + COIN = COINMP_DLL + +# to import the gurobipy name into the module scope +gurobipy = None +class GUROBI(LpSolver): + """ + The Gurobi LP/MIP solver (via its python interface) + + The Gurobi variables are available (after a solve) in var.solverVar + Constriaints in constraint.solverConstraint + and the Model is in prob.solverModel + """ + try: + sys.path.append(gurobi_path) + # to import the name into the module scope + global gurobipy + import gurobipy + except: #FIXME: Bug because gurobi returns + #a gurobi exception on failed imports + def available(self): + """True if the solver is available""" + return False + def actualSolve(self, lp, callback = None): + """Solve a well formulated lp problem""" + raise PulpSolverError, "GUROBI: Not Available" + else: + def __init__(self, + mip = True, + msg = True, + timeLimit = None, + epgap = None, + **solverParams): + """ + Initializes the Gurobi solver. + + @param mip: if False the solver will solve a MIP as an LP + @param msg: displays information from the solver to stdout + @param timeLimit: sets the maximum time for solution + @param epgap: sets the integer bound gap + """ + LpSolver.__init__(self, mip, msg) + self.timeLimit = timeLimit + self.epgap = epgap + #set the output of gurobi + if not self.msg: + gurobipy.setParam("OutputFlag", 0) + #set the gurobi parameter values + for key,value in solverParams.items(): + gurobipy.setParam(key, value) + + def findSolutionValues(self, lp): + model = lp.solverModel + solutionStatus = model.Status + GRB = gurobipy.GRB + gurobiLpStatus = {GRB.OPTIMAL: LpStatusOptimal, + GRB.INFEASIBLE: LpStatusInfeasible, + GRB.INF_OR_UNBD: LpStatusInfeasible, + GRB.UNBOUNDED: LpStatusUnbounded, + GRB.ITERATION_LIMIT: LpStatusNotSolved, + GRB.NODE_LIMIT: LpStatusNotSolved, + GRB.TIME_LIMIT: LpStatusNotSolved, + GRB.SOLUTION_LIMIT: LpStatusNotSolved, + GRB.INTERRUPTED: LpStatusNotSolved, + GRB.NUMERIC: LpStatusNotSolved, + } + #populate pulp solution values + for var in lp.variables(): + try: + var.varValue = var.solverVar.X + except gurobipy.GurobiError: + pass + try: + var.dj = var.solverVar.RC + except gurobipy.GurobiError: + pass + #put pi and slack variables against the constraints + for constr in lp.constraints.values(): + try: + constr.pi = constr.solverConstraint.Pi + except gurobipy.GurobiError: + pass + try: + constr.slack = constr.solverConstraint.Slack + except gurobipy.GurobiError: + pass + if self.msg: + print "Gurobi status=", solutionStatus + lp.resolveOK = True + for var in lp.variables(): + var.isModified = False + lp.status = gurobiLpStatus.get(solutionStatus, LpStatusUndefined) + return lp.status + + def available(self): + """True if the solver is available""" + return True + + def callSolver(self, lp, callback = None): + """Solves the problem with gurobi + """ + #solve the problem + self.solveTime = -clock() + lp.solverModel.optimize(callback = callback) + self.solveTime += clock() + + def buildSolverModel(self, lp): + """ + Takes the pulp lp model and translates it into a gurobi model + """ + log.debug("create the gurobi model") + lp.solverModel = gurobipy.Model(lp.name) + log.debug("set the sense of the problem") + if lp.sense == LpMaximize: + lp.solverModel.setAttr("ModelSense", -1) + if self.timeLimit: + lp.solverModel.setParam("TimeLimit", self.timeLimit) + if self.epgap: + lp.solverModel.setParam("MIPGap", self.epgap) + log.debug("add the variables to the problem") + for var in lp.variables(): + lowBound = var.lowBound + if lowBound is None: + lowBound = -gurobipy.GRB.INFINITY + upBound = var.upBound + if upBound is None: + upBound = gurobipy.GRB.INFINITY + obj = lp.objective.get(var, 0.0) + varType = gurobipy.GRB.CONTINUOUS + if var.cat == LpInteger and self.mip: + varType = gurobipy.GRB.INTEGER + var.solverVar = lp.solverModel.addVar(lowBound, upBound, + vtype = varType, + obj = obj, name = var.name) + lp.solverModel.update() + log.debug("add the Constraints to the problem") + for name,constraint in lp.constraints.items(): + #build the expression + expr = gurobipy.LinExpr(constraint.values(), + [v.solverVar for v in constraint.keys()]) + if constraint.sense == LpConstraintLE: + relation = gurobipy.GRB.LESS_EQUAL + elif constraint.sense == LpConstraintGE: + relation = gurobipy.GRB.GREATER_EQUAL + elif constraint.sense == LpConstraintEQ: + relation = gurobipy.GRB.EQUAL + else: + raise PulpSolverError, 'Detected an invalid constraint type' + constraint.solverConstraint = lp.solverModel.addConstr(expr, + relation, -constraint.constant, name) + lp.solverModel.update() + + def actualSolve(self, lp, callback = None): + """ + Solve a well formulated lp problem + + creates a gurobi model, variables and constraints and attaches + them to the lp model which it then solves + """ + self.buildSolverModel(lp) + #set the initial solution + log.debug("Solve the Model using gurobi") + self.callSolver(lp, callback = callback) + #get the solution information + solutionStatus = self.findSolutionValues(lp) + for var in lp.variables(): + var.modified = False + for constraint in lp.constraints.values(): + constraint.modified = False + return solutionStatus + + def actualResolve(self, lp, callback = None): + """ + Solve a well formulated lp problem + + uses the old solver and modifies the rhs of the modified constraints + """ + log.debug("Resolve the Model using gurobi") + for constraint in lp.constraints.values(): + if constraint.modified: + constraint.solverConstraint.setAttr(gurobipy.GRB.Attr.RHS, + -constraint.constant) + lp.solverModel.update() + self.callSolver(lp, callback = callback) + #get the solution information + solutionStatus = self.findSolutionValues(lp) + for var in lp.variables(): + var.modified = False + for constraint in lp.constraints.values(): + constraint.modified = False + return solutionStatus + +class GUROBI_CMD(LpSolver_CMD): + """The GUROBI_CMD solver""" + def defaultPath(self): + return self.executableExtension("gurobi_cl") + + def available(self): + """True if the solver is available""" + return self.executable(self.path) + + def actualSolve(self, lp): + """Solve a well formulated lp problem""" + if not self.executable(self.path): + raise PulpSolverError, "PuLP: cannot execute "+self.path + if not self.keepFiles: + pid = os.getpid() + tmpLp = os.path.join(self.tmpDir, "%d-pulp.lp" % pid) + tmpSol = os.path.join(self.tmpDir, "%d-pulp.sol" % pid) + else: + tmpLp = lp.name+"-pulp.lp" + tmpSol = lp.name+"-pulp.sol" + lp.writeLP(tmpLp, writeSOS = 1) + try: os.remove(tmpSol) + except: pass + cmd = self.path + cmd += ' ' + ' '.join(['%s=%s' % (key, value) + for key, value in self.options]) + cmd += ' ResultFile=%s' % tmpSol + if lp.isMIP(): + if not self.mip: + warnings.warn('GUROBI_CMD does not allow a problem to be relaxed') + cmd += ' %s' % tmpLp + if self.msg: + pipe = None + else: + pipe = open(os.devnull, 'w') + + return_code = subprocess.call(cmd.split(), stdout = pipe, stderr = pipe) + + if return_code != 0: + raise PulpSolverError, "PuLP: Error while trying to execute "+self.path + if not self.keepFiles: + try: os.remove(tmpLp) + except: pass + if not os.path.exists(tmpSol): + warnings.warn('GUROBI_CMD does provide good solution status of non optimal solutions') + status = LpStatusNotSolved + else: + status, values, reducedCosts, shadowPrices, slacks = self.readsol(tmpSol) + if not self.keepFiles: + try: os.remove(tmpSol) + except: pass + try: os.remove("gurobi.log") + except: pass + if status != LpStatusInfeasible: + lp.assignVarsVals(values) + lp.assignVarsDj(reducedCosts) + lp.assignConsPi(shadowPrices) + lp.assignConsSlack(slacks) + lp.status = status + return status + + def readsol(self, filename): + """Read a Gurobi solution file""" + my_file = open(filename) + try: + my_file.next() # skip the objective value + except StopIteration: + # Empty file not solved + warnings.warn('GUROBI_CMD does provide good solution status of non optimal solutions') + status = LpStatusNotSolved + return status, {}, {}, {}, {} + #We have no idea what the status is assume optimal + status = LpStatusOptimal + + shadowPrices = {} + slacks = {} + shadowPrices = {} + slacks = {} + values = {} + reducedCosts = {} + for line in my_file: + name, value = line.split() + values[name] = float(value) + my_file.close() + return status, values, reducedCosts, shadowPrices, slacks + +#get the glpk name in global scope +glpk = None +class PYGLPK(LpSolver): + """ + The glpk LP/MIP solver (via its python interface) + + The glpk variables are available (after a solve) in var.solverVar + The glpk constraints are available in constraint.solverConstraint + The Model is in prob.solverModel + """ + try: + #import the model into the global scope + global glpk + import glpk + except ImportError: + def available(self): + """True if the solver is available""" + return False + def actualSolve(self, lp, callback = None): + """Solve a well formulated lp problem""" + raise PulpSolverError, "GLPK: Not Available" + else: + def __init__(self, + mip = True, + msg = True, + timeLimit = None, + epgap = None, + **solverParams): + """ + Initializes the glpk solver. + + @param mip: if False the solver will solve a MIP as an LP + @param msg: displays information from the solver to stdout + @param timeLimit: not handled by glpk + @param epgap: sets the integer bound gap + @param solverParams: not handled + """ + LpSolver.__init__(self, mip, msg) + self.timeLimit = timeLimit # time limits are not handled + self.epgap = epgap + if not self.msg: + glpk.env.term_on = False + + def findSolutionValues(self, lp): + model = lp.solverModel + solutionStatus = model.status + glpkLpStatus = {"opt": LpStatusOptimal, + "undef": LpStatusUndefined, + "feas": LpStatusNotSolved, + "infeas": LpStatusInfeasible, + "nofeas": LpStatusInfeasible, + "unbnd": LpStatusUnbounded + } + #populate pulp solution values + for var in lp.variables(): + var.varValue = var.solverVar.primal + try: + var.dj = var.solverVar.dual + except RuntimeError: + var.dj = None + #put pi and slack variables against the constraints + for constr in lp.constraints.values(): + try: + constr.pi = constr.solverConstraint.dual + except RuntimeError: + constr.pi = None + constr.slack = constr.solverConstraint.primal + if self.msg: + print "glpk status=", solutionStatus + lp.resolveOK = True + for var in lp.variables(): + var.isModified = False + lp.status = glpkLpStatus.get(solutionStatus, + LpStatusUndefined) + return lp.status + + def available(self): + """True if the solver is available""" + return True + + def callSolver(self, lp, callback = None): + """Solves the problem with glpk + """ + self.solveTime = -clock() + lp.solverModel.simplex() + if self.mip: + if (lp.solverModel.status != "infeas" + and lp.solverModel.status != "nofeas" + and lp.solverModel.status != "unbnd" + ): + lp.solverModel.integer() + self.solveTime += clock() + + def buildSolverModel(self, lp): + """ + Takes the pulp lp model and translates it into a glpk model + """ + log.debug("create the glpk model") + lp.solverModel = glpk.LPX() + lp.solverModel.name = lp.name + log.debug("set the sense of the problem") + if lp.sense == LpMaximize: + lp.solverModel.obj.maximize = True + log.debug("add the Constraints to the problem") + lp.solverModel.rows.add(len(lp.constraints.keys())) + i = 0 + for name, constraint in lp.constraints.items(): + row = lp.solverModel.rows[i] + row.name = name + if constraint.sense == LpConstraintLE: + row.bounds = None,-constraint.constant + elif constraint.sense == LpConstraintGE: + row.bounds = -constraint.constant, None + elif constraint.sense == LpConstraintEQ: + row.bounds = -constraint.constant,-constraint.constant + else: + raise PulpSolverError, 'Detected an invalid constraint type' + i += 1 + constraint.solverConstraint = row + log.debug("add the variables to the problem") + lp.solverModel.cols.add(len(lp.variables())) + j = 0 + for var in lp.variables(): + col = lp.solverModel.cols[j] + col.name = var.name + col.bounds = var.lowBound,var.upBound + if var.cat == LpInteger: + col.kind = int + var.solverVar = col + j += 1 + log.debug("set the objective function") + lp.solverModel.obj[:] = [lp.objective.get(var, 0.0) for var in + lp.variables()] + log.debug("set the problem matrix") + for name,constraint in lp.constraints.items(): + constraint.solverConstraint.matrix =[(var.solverVar.index, + value ) for var, value in constraint.items()] + + def actualSolve(self, lp, callback = None): + """ + Solve a well formulated lp problem + + creates a glpk model, variables and constraints and attaches + them to the lp model which it then solves + """ + self.buildSolverModel(lp) + #set the initial solution + log.debug("Solve the Model using glpk") + self.callSolver(lp, callback = callback) + #get the solution information + solutionStatus = self.findSolutionValues(lp) + for var in lp.variables(): + var.modified = False + for constraint in lp.constraints.values(): + constraint.modified = False + return solutionStatus + + def actualResolve(self, lp, callback = None): + """ + Solve a well formulated lp problem + + uses the old solver and modifies the rhs of the modified + constraints + """ + log.debug("Resolve the Model using glpk") + for constraint in lp.constraints.values(): + row = constraint.solverConstraint + if constraint.modified: + if constraint.sense == LpConstraintLE: + row.bounds = None,-constraint.constant + elif constraint.sense == LpConstraintGE: + row.bounds = -constraint.constant, None + elif constraint.sense == LpConstraintEQ: + row.bounds = -constraint.constant,-constraint.constant + else: + raise PulpSolverError, 'Detected an invalid constraint type' + self.callSolver(lp, callback = callback) + #get the solution information + solutionStatus = self.findSolutionValues(lp) + for var in lp.variables(): + var.modified = False + for constraint in lp.constraints.values(): + constraint.modified = False + return solutionStatus + +yaposib = None +class YAPOSIB(LpSolver): + """ + COIN OSI (via its python interface) + + The yaposib variables are available (after a solve) in var.solverVar + The yaposib constraints are available in constraint.solverConstraint + The Model is in prob.solverModel + """ + try: + #import the model into the global scope + global yaposib + import yaposib + except ImportError: + def available(self): + """True if the solver is available""" + return False + def actualSolve(self, lp, callback = None): + """Solve a well formulated lp problem""" + raise PulpSolverError, "YAPOSIB: Not Available" + else: + def __init__(self, + mip = True, + msg = True, + timeLimit = None, + epgap = None, + solverName = "Clp", + **solverParams): + """ + Initializes the yaposib solver. + + @param mip: if False the solver will solve a MIP as + an LP + @param msg: displays information from the solver to + stdout + @param timeLimit: not supported + @param epgap: not supported + @param solverParams: not supported + """ + LpSolver.__init__(self, mip, msg) + self.solverName = solverName + + def findSolutionValues(self, lp): + model = lp.solverModel + solutionStatus = model.status + yaposibLpStatus = {"optimal": LpStatusOptimal, + "undefined": LpStatusUndefined, + "abandoned": LpStatusInfeasible, + "infeasible": LpStatusInfeasible, + "limitreached": LpStatusInfeasible + } + #populate pulp solution values + for var in lp.variables(): + var.varValue = var.solverVar.solution + var.dj = var.solverVar.reducedcost + #put pi and slack variables against the constraints + for constr in lp.constraints.values(): + constr.pi = constr.solverConstraint.dual + constr.slack = constr.solverConstraint.activity + if self.msg: + print "yaposib status=", solutionStatus + lp.resolveOK = True + for var in lp.variables(): + var.isModified = False + lp.status = yaposibLpStatus.get(solutionStatus, + LpStatusUndefined) + return lp.status + + def available(self): + """True if the solver is available""" + return True + + def callSolver(self, lp, callback = None): + """Solves the problem with yaposib + """ + if self.msg == 0: + #close stdout to get rid of messages + tempfile = open(mktemp(),'w') + savestdout = os.dup(1) + os.close(1) + if os.dup(tempfile.fileno()) != 1: + raise PulpSolverError, "couldn't redirect stdout - dup() error" + self.solveTime = -clock() + lp.solverModel.solve(self.mip) + self.solveTime += clock() + if self.msg == 0: + #reopen stdout + os.close(1) + os.dup(savestdout) + os.close(savestdout) + + def buildSolverModel(self, lp): + """ + Takes the pulp lp model and translates it into a yaposib model + """ + log.debug("create the yaposib model") + lp.solverModel = yaposib.Problem(self.solverName) + prob = lp.solverModel + prob.name = lp.name + log.debug("set the sense of the problem") + if lp.sense == LpMaximize: + prob.obj.maximize = True + log.debug("add the variables to the problem") + for var in lp.variables(): + col = prob.cols.add(yaposib.vec([])) + col.name = var.name + if not var.lowBound is None: + col.lowerbound = var.lowBound + if not var.upBound is None: + col.upperbound = var.upBound + if var.cat == LpInteger: + col.integer = True + prob.obj[col.index] = lp.objective.get(var, 0.0) + var.solverVar = col + log.debug("add the Constraints to the problem") + for name, constraint in lp.constraints.items(): + row = prob.rows.add(yaposib.vec([(var.solverVar.index, + value) for var, value in constraint.items()])) + if constraint.sense == LpConstraintLE: + row.upperbound = -constraint.constant + elif constraint.sense == LpConstraintGE: + row.lowerbound = -constraint.constant + elif constraint.sense == LpConstraintEQ: + row.upperbound = -constraint.constant + row.lowerbound = -constraint.constant + else: + raise PulpSolverError, 'Detected an invalid constraint type' + row.name = name + constraint.solverConstraint = row + + def actualSolve(self, lp, callback = None): + """ + Solve a well formulated lp problem + + creates a yaposib model, variables and constraints and attaches + them to the lp model which it then solves + """ + self.buildSolverModel(lp) + #set the initial solution + log.debug("Solve the model using yaposib") + self.callSolver(lp, callback = callback) + #get the solution information + solutionStatus = self.findSolutionValues(lp) + for var in lp.variables(): + var.modified = False + for constraint in lp.constraints.values(): + constraint.modified = False + return solutionStatus + + def actualResolve(self, lp, callback = None): + """ + Solve a well formulated lp problem + + uses the old solver and modifies the rhs of the modified + constraints + """ + log.debug("Resolve the model using yaposib") + for constraint in lp.constraints.values(): + row = constraint.solverConstraint + if constraint.modified: + if constraint.sense == LpConstraintLE: + row.upperbound = -constraint.constant + elif constraint.sense == LpConstraintGE: + row.lowerbound = -constraint.constant + elif constraint.sense == LpConstraintEQ: + row.upperbound = -constraint.constant + row.lowerbound = -constraint.constant + else: + raise PulpSolverError, 'Detected an invalid constraint type' + self.callSolver(lp, callback = callback) + #get the solution information + solutionStatus = self.findSolutionValues(lp) + for var in lp.variables(): + var.modified = False + for constraint in lp.constraints.values(): + constraint.modified = False + return solutionStatus + +try: + import ctypes + def ctypesArrayFill(myList, type=ctypes.c_double): + """ + Creates a c array with ctypes from a python list + type is the type of the c array + """ + ctype= type * len(myList) + cList = ctype() + for i,elem in enumerate(myList): + cList[i] = elem + return cList +except(ImportError): + def ctypesArrayFill(myList, type = None): + return None + diff --git a/src/pulp/sparse.py b/src/pulp/sparse.py new file mode 100644 index 0000000..b214cb0 --- /dev/null +++ b/src/pulp/sparse.py @@ -0,0 +1,92 @@ +# Sparse : Python basic dictionary sparse matrix + +# Copyright (c) 2007, Stuart Mitchell (s.mitchell@auckland.ac.nz) +#$Id: sparse.py 1704 2007-12-20 21:56:14Z smit023 $ + +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: + +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +sparse this module provides basic pure python sparse matrix implementation +notably this allows the sparse matrix to be output in various formats +""" + +class Matrix(dict): + """ This is a dictionary based sparse matrix class + """ + def __init__(self,rows,cols): + """initialises the class by creating a matrix that will have the given + rows and columns + """ + self.rows = rows + self.cols = cols + self.rowdict = dict([(row, {}) for row in rows]) + self.coldict = dict([(col, {}) for col in cols]) + + def add(self,row,col,item,colcheck = False, rowcheck = False): + if (not(rowcheck and row not in self.rows)): + if (not(colcheck and col not in self.cols)): + dict.__setitem__(self,(row,col),item) + self.rowdict[row][col] = item + self.coldict[col][row] = item + else: + print self.cols + raise RuntimeError, "col %s is not in the matrix columns"%col + else: + raise RuntimeError, "row %s is not in the matrix rows"%row + + def addcol(self,col,rowitems): + """adds a column + """ + if col in self.cols: + for row,item in rowitems.iteritems(): + self.add(row, col, item, colcheck = False) + else: + raise RuntimeError, "col is not in the matrix columns" + + + + def get(self,k,d=0): + return dict.get(self,k,d) + + def col_based_arrays(self): + numEls = len(self) + elemBase = [] + startsBase = [] + indBase = [] + lenBase = [] + for i,col in enumerate(self.cols): + startsBase.append(len(elemBase)) + elemBase.extend(self.coldict[col].values()) + indBase.extend(self.coldict[col].keys()) + lenBase.append(len(elemBase) - startsBase[-1]) + startsBase.append(len(elemBase)) + return numEls, startsBase, lenBase, indBase, elemBase + +if __name__ == "__main__": + """ unit test + """ + rows = range(10) + cols = range(50,60) + mat = Matrix(rows,cols) + mat.add(1,52,"item") + mat.add(2,54,"stuff") + print mat.col_based_arrays() + + diff --git a/src/pulp/tests.py b/src/pulp/tests.py new file mode 100644 index 0000000..bb56126 --- /dev/null +++ b/src/pulp/tests.py @@ -0,0 +1,558 @@ +""" +Tests for pulp +""" +from pulp import * + +def pulpTestCheck(prob, solver, okstatus, sol = {}, + reducedcosts = None, + duals = None, + slacks = None, + eps = 10**-3, + status = None, + **kwargs): + + if status is None: + status = prob.solve(solver, **kwargs) + if status not in okstatus: + prob.writeLP("debug.lp") + prob.writeMPS("debug.mps") + print "Failure: status ==", status, "not in", okstatus + print "Failure: status ==", LpStatus[status], "not in", \ + [LpStatus[s] for s in okstatus] + raise PulpError, "Tests failed for solver %s"%solver + if sol: + for v,x in sol.iteritems(): + if abs(v.varValue - x) > eps: + prob.writeLP("debug.lp") + prob.writeMPS("debug.mps") + print "Test failed: var", v, "==", v.varValue, "!=", x + raise PulpError, "Tests failed for solver %s"%solver + if reducedcosts: + for v,dj in reducedcosts.iteritems(): + if abs(v.dj - dj) > eps: + prob.writeLP("debug.lp") + prob.writeMPS("debug.mps") + print "Test failed: var.dj", v, "==", v.dj, "!=", dj + raise PulpError, "Tests failed for solver %s"%solver + if duals: + for cname,p in duals.iteritems(): + c = prob.constraints[cname] + if abs(c.pi - p) > eps: + prob.writeLP("debug.lp") + prob.writeMPS("debug.mps") + print "Test failed: constraint.pi", cname , "==", c.pi, "!=", p + raise PulpError, "Tests failed for solver %s"%solver + if slacks: + for cname,slack in slacks.iteritems(): + c = prob.constraints[cname] + if abs(c.slack - slack) > eps: + prob.writeLP("debug.lp") + prob.writeMPS("debug.mps") + print ("Test failed: constraint.slack", cname , "==", + c.slack, "!=", slack) + raise PulpError, "Tests failed for solver %s"%solver + +def pulpTest001(solver): + """ + Test that a variable is deleted when it is suptracted to 0 + """ + x = LpVariable("x", 0, 4) + y = LpVariable("y", -1, 1) + z = LpVariable("z", 0) + c1 = x+y <= 5 + c2 = c1 + z -z + print "\t Testing zero subtraction" + assert str(c2) #will raise an exception + +def pulpTest010(solver): + # Continuous + prob = LpProblem("test010", LpMinimize) + x = LpVariable("x", 0, 4) + y = LpVariable("y", -1, 1) + z = LpVariable("z", 0) + w = LpVariable("w", 0) + prob += x + 4*y + 9*z, "obj" + prob += x+y <= 5, "c1" + prob += x+z >= 10, "c2" + prob += -y+z == 7, "c3" + prob += w >= 0, "c4" + print "\t Testing continuous LP solution" + pulpTestCheck(prob, solver, [LpStatusOptimal], {x:4, y:-1, z:6, w:0}) + +def pulpTest011(solver): + # Continuous Maximisation + prob = LpProblem("test011", LpMaximize) + x = LpVariable("x", 0, 4) + y = LpVariable("y", -1, 1) + z = LpVariable("z", 0) + w = LpVariable("w", 0) + prob += x + 4*y + 9*z, "obj" + prob += x+y <= 5, "c1" + prob += x+z >= 10, "c2" + prob += -y+z == 7, "c3" + prob += w >= 0, "c4" + print "\t Testing maximize continuous LP solution" + pulpTestCheck(prob, solver, [LpStatusOptimal], {x:4, y:1, z:8, w:0}) + +def pulpTest012(solver): + # Unbounded + prob = LpProblem("test012", LpMaximize) + x = LpVariable("x", 0, 4) + y = LpVariable("y", -1, 1) + z = LpVariable("z", 0) + w = LpVariable("w", 0) + prob += x + 4*y + 9*z + w, "obj" + prob += x+y <= 5, "c1" + prob += x+z >= 10, "c2" + prob += -y+z == 7, "c3" + prob += w >= 0, "c4" + print "\t Testing unbounded continuous LP solution" + if solver.__class__ in [GUROBI, CPLEX_CMD, YAPOSIB, CPLEX_PY]: + # These solvers report infeasible or unbounded + pulpTestCheck(prob, solver, [LpStatusInfeasible]) + elif solver.__class__ in [COINMP_DLL,]: + # COINMP_DLL is just plain wrong + print '\t\t Error in CoinMP it reports Optimal' + pulpTestCheck(prob, solver, [LpStatusOptimal]) + elif solver.__class__ is GLPK_CMD: + # GLPK_CMD Does not report unbounded problems, correctly + pulpTestCheck(prob, solver, [LpStatusUndefined]) + elif solver.__class__ in [CPLEX_DLL, GUROBI_CMD]: + # CPLEX_DLL Does not report unbounded problems, correctly + # GUROBI_CMD has a very simple interface + pulpTestCheck(prob, solver, [LpStatusNotSolved]) + else: + pulpTestCheck(prob, solver, [LpStatusUnbounded]) + +def pulpTest013(solver): + # Long name + prob = LpProblem("test013", LpMinimize) + x = LpVariable("x"*120, 0, 4) + y = LpVariable("y", -1, 1) + z = LpVariable("z", 0) + w = LpVariable("w", 0) + prob += x + 4*y + 9*z, "obj" + prob += x+y <= 5, "c1" + prob += x+z >= 10, "c2" + prob += -y+z == 7, "c3" + prob += w >= 0, "c4" + print "\t Testing Long Names" + if solver.__class__ in [CPLEX_CMD, GLPK_CMD, GUROBI_CMD]: + try: + pulpTestCheck(prob, solver, [LpStatusOptimal], {x:4, y:-1, z:6, w:0}) + except PulpError: + #these solvers should raise an error' + pass + else: + pulpTestCheck(prob, solver, [LpStatusOptimal], {x:4, y:-1, z:6, w:0}) + +def pulpTest014(solver): + # repeated name + prob = LpProblem("test014", LpMinimize) + x = LpVariable("x", 0, 4) + y = LpVariable("x", -1, 1) + z = LpVariable("z", 0) + w = LpVariable("w", 0) + prob += x + 4*y + 9*z, "obj" + prob += x+y <= 5, "c1" + prob += x+z >= 10, "c2" + prob += -y+z == 7, "c3" + prob += w >= 0, "c4" + print "\t Testing repeated Names" + if solver.__class__ in [COIN_CMD, PULP_CBC_CMD, CPLEX_CMD, CPLEX_PY, + GLPK_CMD, GUROBI_CMD]: + try: + pulpTestCheck(prob, solver, [LpStatusOptimal], {x:4, y:-1, z:6, w:0}) + except PulpError: + #these solvers should raise an error' + pass + else: + pulpTestCheck(prob, solver, [LpStatusOptimal], {x:4, y:-1, z:6, w:0}) + +def pulpTest015(solver): + # zero constraint + prob = LpProblem("test015", LpMinimize) + x = LpVariable("x", 0, 4) + y = LpVariable("y", -1, 1) + z = LpVariable("z", 0) + w = LpVariable("w", 0) + prob += x + 4*y + 9*z, "obj" + prob += x+y <= 5, "c1" + prob += x+z >= 10, "c2" + prob += -y+z == 7, "c3" + prob += w >= 0, "c4" + prob += lpSum([0, 0]) <= 0, "c5" + print "\t Testing zero constraint" + pulpTestCheck(prob, solver, [LpStatusOptimal], {x:4, y:-1, z:6, w:0}) + +def pulpTest016(solver): + # zero objective + prob = LpProblem("test016", LpMinimize) + x = LpVariable("x", 0, 4) + y = LpVariable("y", -1, 1) + z = LpVariable("z", 0) + w = LpVariable("w", 0) + prob += x+y <= 5, "c1" + prob += x+z >= 10, "c2" + prob += -y+z == 7, "c3" + prob += w >= 0, "c4" + prob += lpSum([0, 0]) <= 0, "c5" + print "\t Testing zero objective" + pulpTestCheck(prob, solver, [LpStatusOptimal]) + +def pulpTest017(solver): + # variable as objective + prob = LpProblem("test016", LpMinimize) + x = LpVariable("x", 0, 4) + y = LpVariable("y", -1, 1) + z = LpVariable("z", 0) + w = LpVariable("w", 0) + prob.setObjective(x) + prob += x+y <= 5, "c1" + prob += x+z >= 10, "c2" + prob += -y+z == 7, "c3" + prob += w >= 0, "c4" + prob += lpSum([0, 0]) <= 0, "c5" + print "\t Testing LpVariable (not LpAffineExpression) objective" + pulpTestCheck(prob, solver, [LpStatusOptimal]) + +def pulpTest018(solver): + # Long name in lp + prob = LpProblem("test013", LpMinimize) + x = LpVariable("x"*90, 0, 4) + y = LpVariable("y"*90, -1, 1) + z = LpVariable("z"*90, 0) + w = LpVariable("w"*90, 0) + prob += x + 4*y + 9*z, "obj" + prob += x+y <= 5, "c1" + prob += x+z >= 10, "c2" + prob += -y+z == 7, "c3" + prob += w >= 0, "c4" + if solver.__class__ in [COIN_CMD]: + print "\t Testing Long lines in LP" + pulpTestCheck(prob, solver, [LpStatusOptimal], {x:4, y:-1, z:6, w:0}, + use_mps=False) + +def pulpTest020(solver): + # MIP + prob = LpProblem("test020", LpMinimize) + x = LpVariable("x", 0, 4) + y = LpVariable("y", -1, 1) + z = LpVariable("z", 0, None, LpInteger) + prob += x + 4*y + 9*z, "obj" + prob += x+y <= 5, "c1" + prob += x+z >= 10, "c2" + prob += -y+z == 7.5, "c3" + print "\t Testing MIP solution" + pulpTestCheck(prob, solver, [LpStatusOptimal], {x:3, y:-0.5, z:7}) + +def pulpTest030(solver): + # relaxed MIP + prob = LpProblem("test030", LpMinimize) + x = LpVariable("x", 0, 4) + y = LpVariable("y", -1, 1) + z = LpVariable("z", 0, None, LpInteger) + prob += x + 4*y + 9*z, "obj" + prob += x+y <= 5, "c1" + prob += x+z >= 10, "c2" + prob += -y+z == 7.5, "c3" + solver.mip = 0 + print "\t Testing MIP relaxation" + if solver.__class__ in [GUROBI_CMD]: + #gurobi command does not let the problem be relaxed + pulpTestCheck(prob, solver, [LpStatusOptimal], {x:3.0, y:-0.5, z:7}) + else: + pulpTestCheck(prob, solver, [LpStatusOptimal], {x:3.5, y:-1, z:6.5}) + + +def pulpTest040(solver): + # Feasibility only + prob = LpProblem("test040", LpMinimize) + x = LpVariable("x", 0, 4) + y = LpVariable("y", -1, 1) + z = LpVariable("z", 0, None, LpInteger) + prob += x+y <= 5, "c1" + prob += x+z >= 10, "c2" + prob += -y+z == 7.5, "c3" + print "\t Testing feasibility problem (no objective)" + pulpTestCheck(prob, solver, [LpStatusOptimal]) + + +def pulpTest050(solver): + # Infeasible + prob = LpProblem("test050", LpMinimize) + x = LpVariable("x", 0, 4) + y = LpVariable("y", -1, 1) + z = LpVariable("z", 0, 10) + prob += x+y <= 5.2, "c1" + prob += x+z >= 10.3, "c2" + prob += -y+z == 17.5, "c3" + print "\t Testing an infeasible problem" + if solver.__class__ is GLPK_CMD: + # GLPK_CMD return codes are not informative enough + pulpTestCheck(prob, solver, [LpStatusUndefined]) + elif solver.__class__ in [CPLEX_DLL, GUROBI_CMD]: + # CPLEX_DLL Does not solve the problem + pulpTestCheck(prob, solver, [LpStatusNotSolved]) + else: + pulpTestCheck(prob, solver, [LpStatusInfeasible]) + +def pulpTest060(solver): + # Integer Infeasible + prob = LpProblem("test060", LpMinimize) + x = LpVariable("x", 0, 4, LpInteger) + y = LpVariable("y", -1, 1, LpInteger) + z = LpVariable("z", 0, 10, LpInteger) + prob += x+y <= 5.2, "c1" + prob += x+z >= 10.3, "c2" + prob += -y+z == 7.4, "c3" + print "\t Testing an integer infeasible problem" + if solver.__class__ in [GLPK_CMD, COIN_CMD, PULP_CBC_CMD]: + # GLPK_CMD returns InfeasibleOrUnbounded + pulpTestCheck(prob, solver, [LpStatusInfeasible, LpStatusUndefined]) + elif solver.__class__ in [COINMP_DLL]: + #Currently there is an error in COINMP for problems where + #presolve eliminates too many variables + print "\t\t Error in CoinMP to be fixed, reports Optimal" + pulpTestCheck(prob, solver, [LpStatusOptimal]) + elif solver.__class__ in [GUROBI_CMD]: + pulpTestCheck(prob, solver, [LpStatusNotSolved]) + else: + pulpTestCheck(prob, solver, [LpStatusInfeasible]) + +def pulpTest070(solver): + #Column Based modelling of PulpTest1 + prob = LpProblem("test070", LpMinimize) + obj = LpConstraintVar("obj") + # constraints + a = LpConstraintVar("C1", LpConstraintLE, 5) + b = LpConstraintVar("C2", LpConstraintGE, 10) + c = LpConstraintVar("C3", LpConstraintEQ, 7) + + prob.setObjective(obj) + prob += a + prob += b + prob += c + # Variables + x = LpVariable("x", 0, 4, LpContinuous, obj + a + b) + y = LpVariable("y", -1, 1, LpContinuous, 4*obj + a - c) + z = LpVariable("z", 0, None, LpContinuous, 9*obj + b + c) + print "\t Testing column based modelling" + pulpTestCheck(prob, solver, [LpStatusOptimal], {x:4, y:-1, z:6}) + +def pulpTest075(solver): + #Column Based modelling of PulpTest1 with empty constraints + prob = LpProblem("test075", LpMinimize) + obj = LpConstraintVar("obj") + # constraints + a = LpConstraintVar("C1", LpConstraintLE, 5) + b = LpConstraintVar("C2", LpConstraintGE, 10) + c = LpConstraintVar("C3", LpConstraintEQ, 7) + + prob.setObjective(obj) + prob += a + prob += b + prob += c + # Variables + x = LpVariable("x", 0, 4, LpContinuous, obj + b) + y = LpVariable("y", -1, 1, LpContinuous, 4*obj - c) + z = LpVariable("z", 0, None, LpContinuous, 9*obj + b + c) + if solver.__class__ in [CPLEX_DLL, CPLEX_CMD, COINMP_DLL]: + print "\t Testing column based modelling with empty constraints" + pulpTestCheck(prob, solver, [LpStatusOptimal], {x:4, y:-1, z:6}) + +def pulpTest080(solver): + """ + Test the reporting of dual variables slacks and reduced costs + """ + prob = LpProblem("test080", LpMinimize) + x = LpVariable("x", 0, 5) + y = LpVariable("y", -1, 1) + z = LpVariable("z", 0) + c1 = x+y <= 5 + c2 = x+z >= 10 + c3 = -y+z == 7 + + prob += x + 4*y + 9*z, "obj" + prob += c1, "c1" + prob += c2,"c2" + prob += c3,"c3" + + if solver.__class__ in [CPLEX_DLL, CPLEX_CMD, COINMP_DLL, PULP_CBC_CMD]: + print "\t Testing dual variables and slacks reporting" + pulpTestCheck(prob, solver, [LpStatusOptimal], + sol = {x:4, y:-1, z:6}, + reducedcosts = {x:0, y:12, z:0}, + duals = {"c1":0, "c2":1, "c3":8}, + slacks = {"c1":2, "c2":0, "c3":0}) + +def pulpTest090(solver): + #Column Based modelling of PulpTest1 with a resolve + prob = LpProblem("test090", LpMinimize) + obj = LpConstraintVar("obj") + # constraints + a = LpConstraintVar("C1", LpConstraintLE, 5) + b = LpConstraintVar("C2", LpConstraintGE, 10) + c = LpConstraintVar("C3", LpConstraintEQ, 7) + + prob.setObjective(obj) + prob += a + prob += b + prob += c + + prob.setSolver(solver)# Variables + x = LpVariable("x", 0, 4, LpContinuous, obj + a + b) + y = LpVariable("y", -1, 1, LpContinuous, 4*obj + a - c) + prob.resolve() + z = LpVariable("z", 0, None, LpContinuous, 9*obj + b + c) + if solver.__class__ in [CPLEX_DLL, COINMP_DLL]: + print "\t Testing resolve of problem" + prob.resolve() + #difficult to check this is doing what we want as the resolve is + #over ridden if it is not implemented + #pulpTestCheck(prob, solver, [LpStatusOptimal], {x:4, y:-1, z:6}) + +def pulpTest100(solver): + """ + Test the ability to sequentially solve a problem + """ + # set up a cubic feasible region + prob = LpProblem("test100", LpMinimize) + x = LpVariable("x", 0, 1) + y = LpVariable("y", 0, 1) + z = LpVariable("z", 0, 1) + + obj1 = x+0*y+0*z + obj2 = 0*x-1*y+0*z + prob += x <= 1, "c1" + + if solver.__class__ in [CPLEX_DLL, COINMP_DLL, GUROBI]: + print "\t Testing Sequential Solves" + status = prob.sequentialSolve([obj1,obj2], solver = solver) + pulpTestCheck(prob, solver, [[LpStatusOptimal,LpStatusOptimal]], + sol = {x:0, y:1}, + status = status) + +def pulpTest110(solver): + """ + Test the ability to use fractional constraints + """ + prob = LpProblem("test110", LpMinimize) + x = LpVariable("x", 0, 4) + y = LpVariable("y", -1, 1) + z = LpVariable("z", 0) + w = LpVariable("w", 0) + prob += x + 4*y + 9*z, "obj" + prob += x+y <= 5, "c1" + prob += x+z >= 10, "c2" + prob += -y+z == 7, "c3" + prob += w >= 0, "c4" + prob += LpFractionConstraint(x, z, LpConstraintEQ, 0.5, name = 'c5') + print "\t Testing fractional constraints" + pulpTestCheck(prob, solver, [LpStatusOptimal], + {x:10/3.0, y:-1/3.0, z:20/3.0, w:0}) + +def pulpTest120(solver): + """ + Test the ability to use Elastic constraints + """ + prob = LpProblem("test120", LpMinimize) + x = LpVariable("x", 0, 4) + y = LpVariable("y", -1, 1) + z = LpVariable("z", 0) + w = LpVariable("w") + prob += x + 4*y + 9*z + w, "obj" + prob += x+y <= 5, "c1" + prob += x+z >= 10, "c2" + prob += -y+z == 7, "c3" + prob.extend((w >= -1).makeElasticSubProblem()) + print "\t Testing elastic constraints (no change)" + pulpTestCheck(prob, solver, [LpStatusOptimal], + {x:4, y:-1, z:6, w:-1}) + +def pulpTest121(solver): + """ + Test the ability to use Elastic constraints + """ + prob = LpProblem("test121", LpMinimize) + x = LpVariable("x", 0, 4) + y = LpVariable("y", -1, 1) + z = LpVariable("z", 0) + w = LpVariable("w") + prob += x + 4*y + 9*z + w, "obj" + prob += x+y <= 5, "c1" + prob += x+z >= 10, "c2" + prob += -y+z == 7, "c3" + prob.extend((w >= -1).makeElasticSubProblem(proportionFreeBound = 0.1)) + print "\t Testing elastic constraints (freebound)" + pulpTestCheck(prob, solver, [LpStatusOptimal], + {x:4, y:-1, z:6, w:-1.1}) + +def pulpTest122(solver): + """ + Test the ability to use Elastic constraints (penalty unchanged) + """ + prob = LpProblem("test122", LpMinimize) + x = LpVariable("x", 0, 4) + y = LpVariable("y", -1, 1) + z = LpVariable("z", 0) + w = LpVariable("w") + prob += x + 4*y + 9*z + w, "obj" + prob += x+y <= 5, "c1" + prob += x+z >= 10, "c2" + prob += -y+z == 7, "c3" + prob.extend((w >= -1).makeElasticSubProblem(penalty = 1.1)) + print "\t Testing elastic constraints (penalty unchanged)" + prob.writeLP('debug.lp') + pulpTestCheck(prob, solver, [LpStatusOptimal], + {x:4, y:-1, z:6, w:-1.0}) + +def pulpTest123(solver): + """ + Test the ability to use Elastic constraints (penalty unbounded) + """ + prob = LpProblem("test123", LpMinimize) + x = LpVariable("x", 0, 4) + y = LpVariable("y", -1, 1) + z = LpVariable("z", 0) + w = LpVariable("w") + prob += x + 4*y + 9*z + w, "obj" + prob += x+y <= 5, "c1" + prob += x+z >= 10, "c2" + prob += -y+z == 7, "c3" + prob.extend((w >= -1).makeElasticSubProblem(penalty = 0.9)) + print "\t Testing elastic constraints (penalty unbounded)" + prob.writeLP('debug.lp') + if solver.__class__ in [COINMP_DLL, GUROBI, CPLEX_CMD, CPLEX_PY, YAPOSIB]: + # COINMP_DLL Does not report unbounded problems, correctly + pulpTestCheck(prob, solver, [LpStatusInfeasible]) + elif solver.__class__ is GLPK_CMD: + # GLPK_CMD Does not report unbounded problems, correctly + pulpTestCheck(prob, solver, [LpStatusUndefined]) + elif solver.__class__ in [CPLEX_DLL, GUROBI_CMD]: + # GLPK_CMD Does not report unbounded problems, correctly + pulpTestCheck(prob, solver, [LpStatusNotSolved]) + else: + pulpTestCheck(prob, solver, [LpStatusUnbounded]) + + +def pulpTestSolver(solver, msg = 0): + tests = [ + pulpTest001, + pulpTest010, pulpTest011, pulpTest012, pulpTest013, pulpTest014, + pulpTest015, pulpTest016, pulpTest017, + pulpTest018, + pulpTest020, + pulpTest030, + pulpTest040, + pulpTest050, + pulpTest060, + pulpTest070, pulpTest075, + pulpTest080, + pulpTest090, + pulpTest100, + pulpTest110, + pulpTest120, pulpTest121, pulpTest122, pulpTest123 + ] + for t in tests: + t(solver(msg=msg)) diff --git a/tests/amply_tests.py b/tests/amply_tests.py new file mode 100644 index 0000000..a443c72 --- /dev/null +++ b/tests/amply_tests.py @@ -0,0 +1,498 @@ +from pulp.amply import Amply, AmplyError +from StringIO import StringIO + +from nose.tools import assert_raises + +def test_data(): + result = Amply("param T := 4;")['T'] + assert result == 4 + result = Amply("param T := -4;")['T'] + assert result == -4 + result = Amply("param T := 0.04;")['T'] + assert result == 0.04 + result = Amply("param T := -0.04;")['T'] + assert result == -0.04 + +def test_set(): + result = Amply("set month := Jan Feb Mar Apr;")['month'] + assert result == ['Jan', 'Feb', 'Mar', 'Apr'] + + result = 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_param(): + result = Amply("param T := 4;")['T'] + assert result != [4] + result = Amply("param T{foo}; param T := 1 2;")['T'] + assert not (result == 2) + assert (result != 2) + +def test_attr_access(): + result = Amply("param T:= 4;").T + assert result == 4 + +def test_from_file(): + s = StringIO("param T:= 4;") + assert Amply.from_file(s).T == 4 + +def test_load_string(): + a = 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(): + a = Amply("param T:= 4; param X{foo};") + 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(): + a = Amply() + a.load_string("param T := 4;") + assert a.T == 4 + +def test_set_dimen2(): + result = 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(): + a = """ + set dim1 dimen 1; + set dim1 := (1, 2) (2, 3) (3, 2); + """ + assert_raises(AmplyError, lambda: Amply(a)) + +def test_set_dimen2_noparen(): + result = 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(): + result = 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(): + result = 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(): + result = 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(): + result = 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(): + result = 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(): + result = 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(): + result = 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(): + result = Amply("param T := 4;")['T'] + assert result == 4 + + +def test_sub1_params(): + result = 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(): + a = """ + param foo{s}; + param foo := 1 Jan 2 Feb 3; + """ + assert_raises(AmplyError, lambda :Amply(a)) + +def test_param_default(): + result = Amply( + """ + param foo {s} default 3; + param foo := Jan 1 Feb 2 Mar 3; + """ + ) + j = result['foo']['Jan'] + assert j == 1 + m = result['foo']['Mar'] + assert m == 3 + d = result['foo']['FOO'] + assert d == 3 + +def test_param_undefined(): + result = Amply( + """ + param foo {s} ; + param foo := Jan 1 Feb 2 Mar 3; + """ + ) + j = result['foo']['Jan'] + assert j == 1 + assert_raises(KeyError, lambda : result['foo']['Apr']) + +def test_sub2_params(): + result = 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(): + result = Amply( + """ + param demand {item, location}; + param demand + : FRA DET LAN := + spoons 200 100 30 + plates 30 120 90 + cups 666 13 29 ; + """ + )['demand'] + + s = result['spoons'] + assert s == { 'FRA': 200, 'DET': 100, 'LAN': 30 } + assert result['plates'] == { 'FRA': 30, 'DET': 120, 'LAN': 90 } + assert result['cups'] == { 'FRA': 666, 'DET': 13, 'LAN': 29 } + +def test_2d_numeric_param(): + result = 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(): + result = Amply( + """ + param demand {item, location}; + param demand default 42 + : FRA DET LAN := + spoons 200 . 30 + plates 30 120 . + cups . . 29 ; + """ + )['demand'] + + s = result['spoons'] + assert s == { 'FRA': 200, 'DET': 42, 'LAN': 30 } + assert result['plates'] == { 'FRA': 30, 'DET': 120, 'LAN': 42 } + assert result['cups'] == { 'FRA': 42, 'DET': 42, 'LAN': 29 } + +def test_2tables(): + result = 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'] + assert demand['spoons'] == {'FRA': 200, 'DET': 42, 'LAN': 30 } + assert demand['plates'] == { 'FRA': 30, 'DET': 120, 'LAN': 42 } + assert demand['cups'] == { 'FRA': 42, 'DET': 42, 'LAN': 29 } + + square = result['square'] + assert square['A'] == {'A': 1, 'B': 6} + assert square['B'] == {'A': 6, 'B': 36} + + +def test_2d_param_transpose(): + result = Amply( + """ + param demand {location, item}; + param demand default 42 (tr) + : FRA DET LAN := + spoons 200 . 30 + plates 30 120 . + cups . . 29 ; + """ + )['demand'] + + f = result['FRA'] + assert f == { 'spoons': 200, 'plates': 30, 'cups': 42 } + assert result['DET'] == { 'spoons': 42, 'plates': 120, 'cups': 42 } + assert result['LAN'] == { 'spoons': 30, 'plates': 42, 'cups': 29 } + +def test_2d_slice1(): + result = 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(): + result = 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(): + result = 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_single_tabbing_data(): + result = 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(): + result = 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(): + result = 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(): + assert_raises(AmplyError, lambda: Amply( + """ + param cost{elem}; + param : cost value := + 0 1 2 + 3 4 5 + ; + """ + )) + +def test_2dset_simpleparam(): + result = 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(): + result = 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 + +