diff --git a/.circleci/config.yml b/.circleci/config.yml index 3fa953d..bf99290 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -31,6 +31,12 @@ jobs: name: upload coverage to codecov command: bash <(curl -s https://codecov.io/bash) + - run: + name: run linting via flake8 + command: | + . venv/bin/activate + flake8 gilp + - save_cache: key: deps1-{{ .Branch }}-{{ checksum "test_requirements.txt" }} paths: diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..cd67370 --- /dev/null +++ b/.flake8 @@ -0,0 +1,13 @@ +[flake8] +ignore = + E121, E123, E133, E226, E24, ; default + E231, ; sometimes no whitespace between commas is clearer + W503, ; W503 conflicts with W504 standard +max-line-length = 79 +per-file-ignores = + ./__init__.py:F401 + gilp/__init__.py:F401 + ; standard to ignore F401 in __init__.py + ./_constants.py:E741 + gilp/_constants.py:E741 + ; Plotly's bad choice in variable names \ No newline at end of file diff --git a/.gitignore b/.gitignore index dc72b17..62e8d71 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,8 @@ ### Python ### test.py -test.ipynb +time_test.py +test*.ipynb # DS.Store **/.DS_Store diff --git a/gilp/_constants.py b/gilp/_constants.py new file mode 100644 index 0000000..ebed364 --- /dev/null +++ b/gilp/_constants.py @@ -0,0 +1,185 @@ +"""Constants. + +This module contains all constants for gilp (except for LP examples). +""" + +__author__ = 'Henry Robbins' + +# Color Theme -- Using Google's Material Design Color System +# https://material.io/design/color/the-color-system.html + +PRIMARY_COLOR = '#1565c0' +PRIMARY_LIGHT_COLOR = '#5e92f3' +PRIMARY_DARK_COLOR = '#003c8f' +SECONDARY_COLOR = '#d50000' +SECONDARY_LIGHT_COLOR = '#ff5131' +SECONDARY_DARK_COLOR = '#9b0000' +PRIMARY_FONT_COLOR = '#ffffff' +SECONDARY_FONT_COLOR = '#ffffff' +# Grayscale +TERTIARY_COLOR = '#DFDFDF' +TERTIARY_LIGHT_COLOR = 'white' # Jupyter Notebook: white, Sphinx: #FCFCFC +TERTIARY_DARK_COLOR = '#404040' + +# Figure Dimensions +FIG_HEIGHT = 500 +FIG_WIDTH = 950 # Jupyter Notebook: 950, Sphinx: 700 +LEGEND_WIDTH = 200 +COMP_WIDTH = (FIG_WIDTH - LEGEND_WIDTH) / 2 + +ISOPROFIT_STEPS = 25 +"""Number of isoprofit planes/lines plotted to the figure.""" + +# Plotly Default Attributes + +LAYOUT = dict(width=FIG_WIDTH, + height=FIG_HEIGHT, + title=dict(text="Geometric Interpretation of LPs", + font=dict(size=18, + color=TERTIARY_DARK_COLOR), + x=0, y=0.99, xanchor='left', yanchor='top'), + legend=dict(title=dict(text='Constraint(s)', + font=dict(size=14)), + font=dict(size=13), + x=(1 - LEGEND_WIDTH / FIG_WIDTH) / 2, y=1, + xanchor='left', yanchor='top'), + margin=dict(l=0, r=0, b=0, t=int(FIG_HEIGHT/15)), + font=dict(family='Arial', color=TERTIARY_DARK_COLOR), + paper_bgcolor=TERTIARY_LIGHT_COLOR, + plot_bgcolor=TERTIARY_LIGHT_COLOR, + hovermode='closest', + clickmode='none', + dragmode='turntable') +"""Layout attributes.""" + +AXIS_2D = dict(gridcolor=TERTIARY_COLOR, gridwidth=1, linewidth=2, + linecolor=TERTIARY_DARK_COLOR, tickcolor=TERTIARY_COLOR, + ticks='outside', rangemode='tozero', showspikes=False, + title_standoff=15, automargin=True, zerolinewidth=2) +"""2d axis attributes.""" + +AXIS_3D = dict(backgroundcolor=TERTIARY_LIGHT_COLOR, showbackground=True, + gridcolor=TERTIARY_COLOR, gridwidth=2, showspikes=False, + linecolor=TERTIARY_DARK_COLOR, zerolinecolor='white', + rangemode='tozero', ticks='') +"""3d axis attributes.""" + +SLIDER = dict(x=0.5 + ((LEGEND_WIDTH / FIG_WIDTH) / 2), xanchor="left", + yanchor="bottom", lenmode='fraction', len=COMP_WIDTH / FIG_WIDTH, + active=0, tickcolor='white', ticklen=0) +"""slider attributes.""" + +TABLE = dict(header_font_color=[SECONDARY_COLOR, 'black'], + header_fill_color=TERTIARY_LIGHT_COLOR, + cells_font_color=[['black', SECONDARY_COLOR, 'black'], + ['black', 'black', 'black']], + cells_fill_color=TERTIARY_LIGHT_COLOR, + visible=False) +"""table attributes.""" + +SCATTER = dict(mode='markers', + hoverinfo='none', + visible=True, + showlegend=False, + fillcolor=PRIMARY_COLOR, + line=dict(width=4, + color=PRIMARY_DARK_COLOR), + marker_line=dict(width=2, + color=SECONDARY_COLOR), + marker=dict(size=9, + color=TERTIARY_LIGHT_COLOR, + opacity=0.99)) +"""2d scatter attributes.""" + +SCATTER_3D = dict(mode='markers', + hoverinfo='none', + visible=True, + showlegend=False, + surfacecolor=PRIMARY_LIGHT_COLOR, + line=dict(width=6, + color=PRIMARY_COLOR), + marker_line=dict(width=1, + color=SECONDARY_COLOR), + marker=dict(size=5, + symbol='circle-open', + color=SECONDARY_LIGHT_COLOR, + opacity=0.99)) +"""3d scatter attributes.""" + +# Plotly Template Attributes + +CANONICAL_TABLE = dict(header=dict(height=30, + font_size=13, + line=dict(color='black', width=1)), + cells=dict(height=25, + font_size=13, + line=dict(color='black',width=1)), + columnwidth=[1,0.8]) +"""Template attributes for an LP table in canonical tableau form.""" + +DICTIONARY_TABLE = dict(header=dict(height=25, + font_size=14, + align=['left', 'right', 'left'], + line_color=TERTIARY_LIGHT_COLOR, + line_width=1), + cells=dict(height=25, + font_size=14, + align=['left', 'right', 'left'], + line_color=TERTIARY_LIGHT_COLOR, + line_width=1), + columnwidth=[50/COMP_WIDTH, + 25/COMP_WIDTH, + 1 - (75/COMP_WIDTH)]) +"""Template attributes for an LP table in dictionary tableau form.""" + +BFS_SCATTER = dict(marker=dict(size=20, color='gray', opacity=1e-7), + hoverinfo='text', + hoverlabel=dict(bgcolor=TERTIARY_LIGHT_COLOR, + bordercolor=TERTIARY_DARK_COLOR, + font_family='Arial', + font_color=TERTIARY_DARK_COLOR, + align='left')) +"""Template attributes for an LP basic feasible solutions (BFS).""" + +VECTOR = dict(mode='lines', line_color=SECONDARY_COLOR, visible=False) +"""Template attributes for a 2d or 3d vector.""" + +CONSTRAINT_LINE = dict(mode='lines', showlegend=True, + line=dict(width=2, dash='15,3,5,3')) +"""Template attributes for (2d) LP constraints.""" + +ISOPROFIT_LINE = dict(mode='lines', visible=False, + line=dict(color=SECONDARY_COLOR, width=4, dash=None)) +"""Template attributes for (2d) LP isoprofit lines.""" + +REGION_2D_POLYGON = dict(mode="lines", opacity=0.2, fill="toself", + line=dict(width=3, color=PRIMARY_DARK_COLOR)) +"""Template attributes for (2d) LP feasible region.""" + +REGION_3D_POLYGON = dict(mode="lines", opacity=0.2, + line=dict(width=5, color=PRIMARY_DARK_COLOR)) +"""Template attributes for (3d) LP feasible region.""" + +CONSTRAINT_POLYGON = dict(surfacecolor='gray', mode="none", + opacity=0.5, visible='legendonly', + showlegend=True) +"""Template attributes for (3d) LP constraints.""" + +ISOPROFIT_IN_POLYGON = dict(mode="lines+markers", + surfacecolor=SECONDARY_COLOR, + marker=dict(size=5, + symbol='circle', + color=SECONDARY_COLOR), + line=dict(width=5, + color=SECONDARY_COLOR), + visible=False) +"""Template attributes for (3d) LP isoprofit plane (interior).""" + +ISOPROFIT_OUT_POLYGON = dict(surfacecolor='gray', mode="none", + opacity=0.3, visible=False) +"""Template attributes for (3d) LP isoprofit plane (exterior).""" + +BNB_NODE = dict(visible=False, align="center", + bordercolor=TERTIARY_DARK_COLOR, borderwidth=2, borderpad=3, + font=dict(size=12, color=TERTIARY_DARK_COLOR), ax=0, ay=0) +"""Template attributes for a branch and bound node.""" diff --git a/gilp/_graphic.py b/gilp/_graphic.py index 4724a54..f178d8e 100644 --- a/gilp/_graphic.py +++ b/gilp/_graphic.py @@ -227,7 +227,8 @@ def linear_string(A: np.ndarray, str: String representation of the linear combination. """ # This function returns the correct sign (+ or -) prefix for a number - def sign(num: float): return {-1: ' - ', 0: ' + ', 1: ' + '}[np.sign(num)] + def sign(num: float): + return {-1: ' - ', 0: ' + ', 1: ' + '}[np.sign(num)] s = '' if constant is not None: diff --git a/gilp/simplex.py b/gilp/simplex.py index 2a109c7..a830fac 100644 --- a/gilp/simplex.py +++ b/gilp/simplex.py @@ -515,9 +515,9 @@ def _initial_solution(lp: LP, else: x = _validate(x, [lp.n, n], 'Initial solution') - if (np.allclose(np.dot(A,x), b, atol=feas_tol) and - all(x >= np.zeros((n,1)) - feas_tol) and - len(np.nonzero(x)[0]) <= m): + if (np.allclose(np.dot(A,x), b, atol=feas_tol) + and all(x >= np.zeros((n,1)) - feas_tol) + and len(np.nonzero(x)[0]) <= m): B = list(np.nonzero(x)[0]) N = list(set(range(lp.n+lp.m)) - set(B)) while len(B) < m: # if initial solution is degenerate diff --git a/gilp/tests/test_simplex.py b/gilp/tests/test_simplex.py index a82aad6..59215d9 100644 --- a/gilp/tests/test_simplex.py +++ b/gilp/tests/test_simplex.py @@ -399,10 +399,10 @@ def test_branch_and_bound_manual(): assert not iteration.fathomed assert iteration.incumbent is None assert iteration.best_bound is None - assert all(gilp.simplex(iteration.right_LP).x[:2] == - np.array([[1.8],[4]])) - assert all(gilp.simplex(iteration.left_LP).x[:2] == - np.array([[3],[3]])) + assert all(gilp.simplex(iteration.right_LP).x[:2] + == np.array([[1.8],[4]])) + assert all(gilp.simplex(iteration.left_LP).x[:2] + == np.array([[3],[3]])) @pytest.mark.parametrize("lp,x,val",[ diff --git a/gilp/visualize.py b/gilp/visualize.py index cdd8a5c..77a835a 100644 --- a/gilp/visualize.py +++ b/gilp/visualize.py @@ -16,199 +16,23 @@ import numpy as np import plotly.graph_objects as plt from typing import Union, List, Tuple +from ._constants import (AXIS_2D, AXIS_3D, BFS_SCATTER, BNB_NODE, + CANONICAL_TABLE, CONSTRAINT_LINE, CONSTRAINT_POLYGON, + DICTIONARY_TABLE, FIG_HEIGHT, FIG_WIDTH, + ISOPROFIT_IN_POLYGON, ISOPROFIT_LINE, + ISOPROFIT_OUT_POLYGON, ISOPROFIT_STEPS, LAYOUT, + LEGEND_WIDTH, PRIMARY_COLOR, PRIMARY_DARK_COLOR, + REGION_2D_POLYGON, REGION_3D_POLYGON, SCATTER, + SCATTER_3D, SECONDARY_COLOR, SLIDER, TABLE, + TERTIARY_DARK_COLOR, TERTIARY_LIGHT_COLOR, VECTOR) from ._geometry import (intersection, interior_point, NoInteriorPoint, - polytope_vertices, polytope_facets) + polytope_vertices, polytope_facets) from ._graphic import (num_format, equation_string, linear_string, plot_tree, Figure, label, table, vector, scatter, equation, polygon, polytope) from .simplex import (LP, simplex, branch_and_bound_iteration, UnboundedLinearProgram, Infeasible) -# COLOR THEME -- Using Google's Material Design Color System -# https://material.io/design/color/the-color-system.html - -PRIMARY_COLOR = '#1565c0' -PRIMARY_LIGHT_COLOR = '#5e92f3' -PRIMARY_DARK_COLOR = '#003c8f' -SECONDARY_COLOR = '#d50000' -SECONDARY_LIGHT_COLOR = '#ff5131' -SECONDARY_DARK_COLOR = '#9b0000' -PRIMARY_FONT_COLOR = '#ffffff' -SECONDARY_FONT_COLOR = '#ffffff' -# Grayscale -TERTIARY_COLOR = '#DFDFDF' -TERTIARY_LIGHT_COLOR = 'white' # Jupyter Notebook: white, Sphinx: #FCFCFC -TERTIARY_DARK_COLOR = '#404040' - -# FIGURE DIMENSIONS - -FIG_HEIGHT = 500 -"""Default figure height.""" -FIG_WIDTH = 950 # Jupyter Notebook: 950, Sphinx: 700 -"""Default figure width.""" -LEGEND_WIDTH = 200 -"""Default legend width.""" -COMP_WIDTH = (FIG_WIDTH - LEGEND_WIDTH) / 2 -"""Default width of the left and right component of a figure.""" -ISOPROFIT_STEPS = 25 -"""Number of isoprofit lines or plane to render.""" - -# PLOTLY LAYOUT, AXIS, AND SLIDER ATTRIBUTES - -LAYOUT = dict(width=FIG_WIDTH, - height=FIG_HEIGHT, - title=dict(text="Geometric Interpretation of LPs", - font=dict(size=18, - color=TERTIARY_DARK_COLOR), - x=0, y=0.99, xanchor='left', yanchor='top'), - legend=dict(title=dict(text='Constraint(s)', - font=dict(size=14)), - font=dict(size=13), - x=(1 - LEGEND_WIDTH / FIG_WIDTH) / 2, y=1, - xanchor='left', yanchor='top'), - margin=dict(l=0, r=0, b=0, t=int(FIG_HEIGHT/15)), - font=dict(family='Arial', color=TERTIARY_DARK_COLOR), - paper_bgcolor=TERTIARY_LIGHT_COLOR, - plot_bgcolor=TERTIARY_LIGHT_COLOR, - hovermode='closest', - clickmode='none', - dragmode='turntable') -"""Default layout attributes.""" - -AXIS_2D = dict(gridcolor=TERTIARY_COLOR, gridwidth=1, linewidth=2, - linecolor=TERTIARY_DARK_COLOR, tickcolor=TERTIARY_COLOR, - ticks='outside', rangemode='tozero', showspikes=False, - title_standoff=15, automargin=True, zerolinewidth=2) -"""Default 2d axis attributes.""" - -AXIS_3D = dict(backgroundcolor=TERTIARY_LIGHT_COLOR, showbackground=True, - gridcolor=TERTIARY_COLOR, gridwidth=2, showspikes=False, - linecolor=TERTIARY_DARK_COLOR, zerolinecolor='white', - rangemode='tozero', ticks='') -"""Default 3d axis attributes.""" - -SLIDER = dict(x=0.5 + ((LEGEND_WIDTH / FIG_WIDTH) / 2), xanchor="left", - yanchor="bottom", lenmode='fraction', len=COMP_WIDTH / FIG_WIDTH, - active=0, tickcolor='white', ticklen=0) -"""Default slider attributes.""" - -# PLOTLY DEFAULT TRACES - -TABLE = dict(header_font_color=[SECONDARY_COLOR, 'black'], - header_fill_color=TERTIARY_LIGHT_COLOR, - cells_font_color=[['black', SECONDARY_COLOR, 'black'], - ['black', 'black', 'black']], - cells_fill_color=TERTIARY_LIGHT_COLOR, - visible=False) -"""Default table attributes.""" - -SCATTER = dict(mode='markers', - hoverinfo='none', - visible=True, - showlegend=False, - fillcolor=PRIMARY_COLOR, - line=dict(width=4, - color=PRIMARY_DARK_COLOR), - marker_line=dict(width=2, - color=SECONDARY_COLOR), - marker=dict(size=9, - color=TERTIARY_LIGHT_COLOR, - opacity=0.99)) -"""Default 2d scatter attributes.""" - -SCATTER_3D = dict(mode='markers', - hoverinfo='none', - visible=True, - showlegend=False, - surfacecolor=PRIMARY_LIGHT_COLOR, - line=dict(width=6, - color=PRIMARY_COLOR), - marker_line=dict(width=1, - color=SECONDARY_COLOR), - marker=dict(size=5, - symbol='circle-open', - color=SECONDARY_LIGHT_COLOR, - opacity=0.99)) -"""Default 3d scatter attributes.""" - -# PLOTLY TRACE TEMPLATES - -CANONICAL_TABLE = dict(header=dict(height=30, - font_size=13, - line=dict(color='black', width=1)), - cells=dict(height=25, - font_size=13, - line=dict(color='black',width=1)), - columnwidth=[1,0.8]) -"""Template attributes for an LP table in canonical tableau form.""" - -DICTIONARY_TABLE = dict(header=dict(height=25, - font_size=14, - align=['left', 'right', 'left'], - line_color=TERTIARY_LIGHT_COLOR, - line_width=1), - cells=dict(height=25, - font_size=14, - align=['left', 'right', 'left'], - line_color=TERTIARY_LIGHT_COLOR, - line_width=1), - columnwidth=[50/COMP_WIDTH, - 25/COMP_WIDTH, - 1 - (75/COMP_WIDTH)]) -"""Template attributes for an LP table in dictionary tableau form.""" - -BFS_SCATTER = dict(marker=dict(size=20, color='gray', opacity=1e-7), - hoverinfo='text', - hoverlabel=dict(bgcolor=TERTIARY_LIGHT_COLOR, - bordercolor=TERTIARY_DARK_COLOR, - font_family='Arial', - font_color=TERTIARY_DARK_COLOR, - align='left')) -"""Template attributes for an LP basic feasible solutions (BFS).""" - -VECTOR = dict(mode='lines', line_color=SECONDARY_COLOR, visible=False) -"""Template attributes for a 2d or 3d vector.""" - -CONSTRAINT_LINE = dict(mode='lines', showlegend=True, - line=dict(width=2, dash='15,3,5,3')) -"""Template attributes for (2d) LP constraints.""" - -ISOPROFIT_LINE = dict(mode='lines', visible=False, - line=dict(color=SECONDARY_COLOR, width=4, dash=None)) -"""Template attributes for (2d) LP isoprofit lines.""" - -REGION_2D_POLYGON = dict(mode="lines", opacity=0.2, fill="toself", - line=dict(width=3, color=PRIMARY_DARK_COLOR)) -"""Template attributes for (2d) LP feasible region.""" - -REGION_3D_POLYGON = dict(mode="lines", opacity=0.2, - line=dict(width=5, color=PRIMARY_DARK_COLOR)) -"""Template attributes for (3d) LP feasible region.""" - -CONSTRAINT_POLYGON = dict(surfacecolor='gray', mode="none", - opacity=0.5, visible='legendonly', - showlegend=True) -"""Template attributes for (3d) LP constraints.""" - -ISOPROFIT_IN_POLYGON = dict(mode="lines+markers", - surfacecolor=SECONDARY_COLOR, - marker=dict(size=5, - symbol='circle', - color=SECONDARY_COLOR), - line=dict(width=5, - color=SECONDARY_COLOR), - visible=False) -"""Template attributes for (3d) LP isoprofit plane (interior).""" - -ISOPROFIT_OUT_POLYGON = dict(surfacecolor='gray', mode="none", - opacity=0.3, visible=False) -"""Template attributes for (3d) LP isoprofit plane (exterior).""" - -BNB_NODE = dict(visible=False, align="center", - bordercolor=TERTIARY_DARK_COLOR, borderwidth=2, borderpad=3, - font=dict(size=12, color=TERTIARY_DARK_COLOR), ax=0, ay=0) -"""Template attributes for a branch and bound node.""" - class InfiniteFeasibleRegion(Exception): """Raised when an LP is found to have an infinite feasible region and can @@ -654,7 +478,10 @@ def tableau_strings(lp: LP, header = ['(' + str(iteration) + ')', ' ', ' '] content = [] content.append(['max','s.t.']+[' ' for i in range(m - 1)]) - def x_sub(i: int): return 'x' + str(i) + '' + + def x_sub(i: int): + return 'x' + str(i) + '' + content.append(['z'] + [x_sub(B[i] + 1) for i in range(m)]) obj_func = ['= ' + linear_string(-T[0,1:n+m+1][N], list(np.array(N)+1), diff --git a/test_requirements.txt b/test_requirements.txt index 280de59..3f42733 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -6,4 +6,5 @@ plotly==4.9.0 pytest==6.1.2 scipy==1.5.3 tox==3.20.1 -typing==3.7.4.3 \ No newline at end of file +typing==3.7.4.3 +flake8==3.7.9 \ No newline at end of file