Skip to content

Commit

Permalink
coverage, inconsistent units in NomialArrays, require ad (#1505)
Browse files Browse the repository at this point in the history
  • Loading branch information
bqpd committed Jul 15, 2020
1 parent f0dd8c9 commit f6d77aa
Show file tree
Hide file tree
Showing 28 changed files with 212 additions and 237 deletions.
2 changes: 1 addition & 1 deletion docs/source/examples/beam.py
Expand Up @@ -78,7 +78,7 @@ def setup(self, N=4):
assert max(abs(w_gp - w_exact)) <= 1.1*ureg.cm

PLOT = False
if PLOT:
if PLOT: # pragma: no cover
import matplotlib.pyplot as plt
x_exact = np.linspace(0, L, 1000)
w_exact = q/(24*EI) * x_exact**2 * (x_exact**2 - 4*L*x_exact + 6*L**2)
Expand Down
2 changes: 1 addition & 1 deletion docs/source/examples/boundschecking.py
Expand Up @@ -53,7 +53,7 @@ def setup(self):

try:
sol = gp.solve(verbosity=0) # Errors on mosek_cli
except UnknownInfeasible:
except UnknownInfeasible: # pragma: no cover
pass

bpl = ", but would gain it from any of these sets: "
Expand Down
2 changes: 1 addition & 1 deletion gpkit/__init__.py
Expand Up @@ -21,7 +21,7 @@
from .tools.docstring import parse_variables
from .tests.run_tests import run as run_unit_tests

if "just built!" in settings:
if "just built!" in settings: # pragma: no cover
run_unit_tests(verbosity=1)
print("""
GPkit is now installed with solver(s) %s
Expand Down
28 changes: 9 additions & 19 deletions gpkit/constraints/prog_factories.py
@@ -1,31 +1,23 @@
"Scripts for generating, solving and sweeping programs"
from time import time
import numpy as np
from ad import adnumber
from ..nomials import parse_subs
from ..solution_array import SolutionArray
from ..keydict import KeyDict
from ..small_scripts import maybe_flatten
from ..exceptions import Infeasible

try:
from ad import adnumber
except ImportError:
adnumber = None
print("Couldn't import ad; automatic differentiation of linked variables"
" is disabled.")


def evaluate_linked(constants, linked):
"Evaluates the values and gradients of linked variables."
if adnumber:
kdc = KeyDict({k: adnumber(maybe_flatten(v))
for k, v in constants.items()})
kdc.log_gets = True
kdc = KeyDict({k: adnumber(maybe_flatten(v))
for k, v in constants.items()})
kdc.log_gets = True
kdc_plain = None
array_calulated, logged_array_gets = {}, {}
for v, f in linked.items():
try:
assert adnumber # trigger exit if ad not found
if v.veckey and v.veckey.original_fn:
if v.veckey not in array_calulated:
ofn = v.veckey.original_fn
Expand All @@ -48,18 +40,16 @@ def evaluate_linked(constants, linked):
from .. import settings
if settings.get("ad_errors_raise", None):
raise
if adnumber:
print("Warning: skipped auto-differentiation of linked variable"
" %s because %s was raised. Set `gpkit.settings"
"[\"ad_errors_raise\"] = True` to raise such Exceptions"
" directly.\n" % (v, repr(exception)))
print("Warning: skipped auto-differentiation of linked variable"
" %s because %s was raised. Set `gpkit.settings"
"[\"ad_errors_raise\"] = True` to raise such Exceptions"
" directly.\n" % (v, repr(exception)))
if kdc_plain is None:
kdc_plain = KeyDict(constants)
constants[v] = f(kdc_plain)
v.descr.pop("gradients", None)
finally:
if adnumber:
kdc.logged_gets = set()
kdc.logged_gets = set()


def progify(program, return_attr=None):
Expand Down
28 changes: 13 additions & 15 deletions gpkit/globals.py
Expand Up @@ -4,31 +4,29 @@
from . import build


def load_settings(path=None, firstattempt=True):
def load_settings(path=None, trybuild=True):
"Load the settings file at SETTINGS_PATH; return settings dict"
if path is None:
path = os.sep.join([os.path.dirname(__file__), "env", "settings"])
try:
try: # if the settings file already exists, read it
with open(path) as settingsfile:
lines = [line[:-1].split(" : ") for line in settingsfile
if len(line.split(" : ")) == 2]
settings_ = {name: value.split(", ") for name, value in lines}
for name, value in settings_.items():
# hack to flatten 1-element lists,
# unless they're the solver list
# flatten 1-element lists unless they're the solver list
if len(value) == 1 and name != "installed_solvers":
settings_[name] = value[0]
except IOError:
settings_[name], = value
except IOError: # pragma: no cover
settings_ = {"installed_solvers": [""]}
if settings_["installed_solvers"] == [""]:
if firstattempt:
print("Found no installed solvers, beginning a build.")
build()
settings_ = load_settings(path, firstattempt=False)
if settings_["installed_solvers"] != [""]:
settings_["just built!"] = True
else:
print("""
if settings_["installed_solvers"] == [""] and trybuild: # pragma: no cover
print("Found no installed solvers, beginning a build.")
build()
settings_ = load_settings(path, trybuild=False)
if settings_["installed_solvers"] != [""]:
settings_["just built!"] = True
else:
print("""
=============
Build failed! :(
=============
Expand Down
3 changes: 1 addition & 2 deletions gpkit/keydict.py
Expand Up @@ -3,7 +3,7 @@
from collections.abc import Hashable
import numpy as np
from .small_classes import Numbers, Quantity, FixedScalar
from .small_scripts import is_sweepvar, isnan, SweepValue
from .small_scripts import is_sweepvar, isnan

DIMLESS_QUANTITY = Quantity(1, "dimensionless")
INT_DTYPE = np.dtype(int)
Expand Down Expand Up @@ -203,7 +203,6 @@ def __setitem__(self, key, value):
self.owned.add(key)
if idx:
if is_sweepvar(value):
value = SweepValue(value[1])
old = super().__getitem__(key)
super().__setitem__(key, np.array(old, "object"))
self.owned.add(key)
Expand Down
65 changes: 27 additions & 38 deletions gpkit/nomials/array.py
Expand Up @@ -10,11 +10,10 @@
from functools import reduce # pylint: disable=redefined-builtin
import numpy as np
from .map import NomialMap
from ..small_classes import Numbers, HashVector, EMPTY_HV
from ..small_scripts import try_str_without, mag
from ..small_classes import HashVector, EMPTY_HV
from ..small_scripts import try_str_without
from ..constraints import ArrayConstraint
from ..repr_conventions import ReprMixin
from ..exceptions import DimensionalityError

@np.vectorize
def vec_recurse(element, function, *args, **kwargs):
Expand Down Expand Up @@ -147,56 +146,46 @@ def sub(self, subs, require_positive=True):
"Substitutes into the array"
return self.vectorize(lambda nom: nom.sub(subs, require_positive))

@property
def units(self):
"""units must have same dimensions across the entire nomial array"""
units = None
for el in self.flat: # pylint: disable=not-an-iterable
el_units = getattr(el, "units", None)
if units is None:
units = el_units
elif ((el_units and units != el_units) or
(isinstance(el, Numbers) and not (el == 0 or np.isnan(el)))):
raise DimensionalityError(el_units, units)
return units

def sum(self, *args, **kwargs): # pylint: disable=arguments-differ
"Returns a sum. O(N) if no arguments are given."
if not self.size:
raise ValueError("cannot sum NomialArray of size 0")
if args or kwargs or not self.shape:
return np.ndarray.sum(self, *args, **kwargs)
hmap = NomialMap()
hmap.units = self.units
it = np.nditer(self, flags=["multi_index", "refs_ok"])
while not it.finished:
m = self[it.multi_index]
it.iternext()
if isinstance(mag(m), Numbers):
if mag(m):
hmap[EMPTY_HV] = mag(m) + hmap.get(EMPTY_HV, 0)
for p in self.flat: # pylint:disable=not-an-iterable
if not hmap and hasattr(p, "units"):
hmap.units = p.units
if hasattr(p, "hmap"):
hmap += p.hmap
else:
hmap += m.hmap
if hasattr(p, "units"):
p = p.to(hmap.units).magnitude
elif hmap.units and p and not np.isnan(p):
p /= float(hmap.units)
hmap[EMPTY_HV] = p + hmap.get(EMPTY_HV, 0)
out = Signomial(hmap)
out.ast = ("sum", (self, None))
return out

def prod(self, *args, **kwargs): # pylint: disable=arguments-differ
"Returns a product. O(N) if no arguments and only contains monomials."
if not self.size:
raise ValueError("cannot prod NomialArray of size 0")
if args or kwargs:
return np.ndarray.prod(self, *args, **kwargs)
c, unitpower = 1.0, 0
exp = HashVector()
it = np.nditer(self, flags=["multi_index", "refs_ok"])
while not it.finished:
m = self[it.multi_index]
it.iternext()
if not hasattr(m, "hmap") and len(m.hmap) == 1:
c, exp = 1.0, HashVector()
hmap = NomialMap()
for m in self.flat: # pylint:disable=not-an-iterable
try:
(mexp, mc), = m.hmap.items()
except (AttributeError, ValueError):
return np.ndarray.prod(self, *args, **kwargs)
c *= mag(m.c)
unitpower += 1
exp += m.exp
hmap = NomialMap({exp: c})
units = self.units
hmap.units = units**unitpower if units else None
c *= mc
exp += mexp
if m.units:
hmap.units = (hmap.units or 1) * m.units
hmap[exp] = c
out = Signomial(hmap)
out.ast = ("prod", (self, None))
return out
Expand Down
3 changes: 2 additions & 1 deletion gpkit/nomials/substitution.py
Expand Up @@ -42,7 +42,8 @@ def append_sub(sub, keys, constants, sweep, linkedsweep):
pywarnings.filterwarnings("error")
try:
sub = np.array(sub) if not hasattr(sub, "shape") else sub
except Warning: # ragged nested sequences, eg [[2]], [3, 4]]
except Warning: # pragma: no cover #TODO: coverage this
# ragged nested sequences, eg [[2]], [3, 4]], in py3.7+
sub = np.array(sub, dtype=object)
if key.shape == sub.shape:
value = sub[key.idx]
Expand Down
1 change: 1 addition & 0 deletions gpkit/nomials/variables.py
Expand Up @@ -171,6 +171,7 @@ def __new__(cls, shape, *args, **descr): # pylint: disable=too-many-branches, t

obj = np.asarray(vl).view(NomialArray)
obj.key = veckey
obj.units = obj.key.units
return obj


Expand Down
6 changes: 3 additions & 3 deletions gpkit/repr_conventions.py
Expand Up @@ -7,12 +7,12 @@

INSIDE_PARENS = re.compile(r"\(.*\)")

if sys.platform[:3] == "win":
if sys.platform[:3] == "win": # pragma: no cover
MUL = "*"
PI_STR = "PI"
UNICODE_EXPONENTS = False
UNIT_FORMATTING = ":~"
else:
else: # pragma: no cover
MUL = "·"
PI_STR = "π"
UNICODE_EXPONENTS = True
Expand All @@ -32,7 +32,7 @@ def unitstr(units, into="%s", options=UNIT_FORMATTING, dimless=""):
units = units.units
if not isinstance(units, Quantity):
return dimless
if options == ":~" and "ohm" in str(units.units):
if options == ":~" and "ohm" in str(units.units): # pragma: no cover
rawstr = str(units.units) # otherwise it'll be a capital Omega
else:
rawstr = ("{%s}" % options).format(units.units)
Expand Down
2 changes: 1 addition & 1 deletion gpkit/small_classes.py
Expand Up @@ -77,7 +77,7 @@ def write(self, writ):
if writ != "\n":
writ = writ.rstrip("\n")
self.append(str(writ))
if self.verbosity > 0:
if self.verbosity > 0: # pragma: no cover
self.output.write(writ)


Expand Down
13 changes: 0 additions & 13 deletions gpkit/small_scripts.py
Expand Up @@ -42,26 +42,13 @@ def mag(c):
return getattr(c, "magnitude", c)


class SweepValue:
"Object to represent a swept substitution."
def __init__(self, value):
self.value = value


def is_sweepvar(sub):
"Determines if a given substitution indicates a sweep."
return splitsweep(sub)[0]


def get_sweepval(sub):
"Returns a given substitution's indicated sweep, or None."
return splitsweep(sub)[1]


def splitsweep(sub):
"Splits a substitution into (is_sweepvar, sweepval)"
if isinstance(sub, SweepValue):
return True, sub.value
try:
sweep, value = sub
if sweep is "sweep" and (isinstance(value, Iterable) or # pylint: disable=literal-comparison
Expand Down
26 changes: 13 additions & 13 deletions gpkit/solvers/mosek_cli.py
Expand Up @@ -15,7 +15,7 @@
from ..exceptions import (UnknownInfeasible, InvalidLicense,
PrimalInfeasible, DualInfeasible)

def remove_read_only(func, path, exc):
def remove_read_only(func, path, exc): # pragma: no cover
"If we can't remove a file/directory, change permissions and try again."
if func in (os.rmdir, os.remove) and exc[1].errno == errno.EACCES:
# change the file to be readable,writable,executable: 0777
Expand Down Expand Up @@ -90,28 +90,28 @@ def optimize(*, c, A, p_idxs, **_):
# invalid license return codes:
# expired: 233 (linux)
# missing: 240 (linux)
if e.returncode in [233, 240]:
if e.returncode in [233, 240]: # pragma: no cover
raise InvalidLicense() from e
raise UnknownInfeasible() from e
with open(solution_filename) as f:
_, probsta = f.readline().split("PROBLEM STATUS : ")
if probsta == "PRIMAL_INFEASIBLE\n":
_, probsta = f.readline()[:-1].split("PROBLEM STATUS : ")
if probsta == "PRIMAL_INFEASIBLE":
raise PrimalInfeasible()
if probsta == "DUAL_INFEASIBLE\n":
if probsta == "DUAL_INFEASIBLE":
raise DualInfeasible()
if probsta != "PRIMAL_AND_DUAL_FEASIBLE\n":
raise UnknownInfeasible("PROBLEM STATUS: " + probsta[:-1])
if probsta != "PRIMAL_AND_DUAL_FEASIBLE":
raise UnknownInfeasible("PROBLEM STATUS: " + probsta)

_, solsta = f.readline().split("SOLUTION STATUS : ")
# line looks like "OBJECTIVE : 2.763550e+002"
objective_val = float(f.readline().split()[2])
assert_equal(f.readline(), "\n")
assert_equal(f.readline(), "PRIMAL VARIABLES\n")
assert_equal(f.readline(), "INDEX ACTIVITY\n")
assert_equal(f.readline(), "")
assert_equal(f.readline(), "PRIMAL VARIABLES")
assert_equal(f.readline(), "INDEX ACTIVITY")
primal_vals = read_vals(f)
# read_vals reads the dividing blank line as well
assert_equal(f.readline(), "DUAL VARIABLES\n")
assert_equal(f.readline(), "INDEX ACTIVITY\n")
assert_equal(f.readline(), "DUAL VARIABLES")
assert_equal(f.readline(), "INDEX ACTIVITY")
dual_vals = read_vals(f)

if tmpdir:
Expand Down Expand Up @@ -146,7 +146,7 @@ def write_output_file(filename, c, A, p_idxs):

def assert_equal(received, expected):
"Asserts that a file's next line is as expected."
if tuple(expected[:-1].split()) != tuple(received[:-1].split()):
if expected.rstrip() != received.rstrip(): # pragma: no cover
errstr = repr(expected)+" is not the same as "+repr(received)
raise RuntimeWarning("could not read mskexpopt output file: "+errstr)

Expand Down
4 changes: 2 additions & 2 deletions gpkit/solvers/mosek_conif.py
Expand Up @@ -200,7 +200,7 @@ def streamprinter(text):

try:
task.optimize()
except mosek.Error as e:
except mosek.Error as e: # pragma: no cover
if e.errno in [mosek.rescode.err_missing_license_file,
mosek.rescode.err_license_version,
mosek.rescode.err_license_expired]:
Expand All @@ -217,7 +217,7 @@ def streamprinter(text):
raise PrimalInfeasible()
if msk_solsta == mosek.solsta.dual_infeas_cer:
raise DualInfeasible()
if msk_solsta != mosek.solsta.optimal:
if msk_solsta != mosek.solsta.optimal: # pragma: no cover
raise UnknownInfeasible("solution status: ", msk_solsta)

# recover primal variables
Expand Down

0 comments on commit f6d77aa

Please sign in to comment.