diff --git a/.pylintrc b/.pylintrc index 502405af..d4856d2d 100644 --- a/.pylintrc +++ b/.pylintrc @@ -83,7 +83,7 @@ attr-rgx=[a-z_][a-z0-9_]{2,30}$ attr-name-hint=[a-z_][a-z0-9_]{2,30}$ # Regular expression matching correct argument names -argument-rgx=[a-z_][a-z0-9_]{2,30}|nu|la|ax$ +argument-rgx=[a-z_][a-z0-9_]{2,30}|nu|la|ax|vk$ # Naming hint for argument names argument-name-hint=[a-z_][a-z0-9_]{2,30}$ diff --git a/docs/source/_static/css/custom.css b/docs/source/_static/css/custom.css new file mode 100644 index 00000000..f392ae94 --- /dev/null +++ b/docs/source/_static/css/custom.css @@ -0,0 +1,5 @@ +@import 'theme.css'; + +div[class^='highlight-breakdowns'] pre { + line-height: 1.15 !important; +} diff --git a/docs/source/conf.py b/docs/source/conf.py index 52cce0b9..78d3e7c5 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -139,6 +139,12 @@ # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] +html_css_files = [ + 'css/custom.css', +] + +html_style = 'css/custom.css' + # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. diff --git a/docs/source/debugging.rst b/docs/source/debugging.rst index b72902bd..e917219b 100644 --- a/docs/source/debugging.rst +++ b/docs/source/debugging.rst @@ -1,7 +1,7 @@ Debugging Models **************** -A number of errors and warnings may be raised when attempting to solve a model. +A number of errors and warnings may be raised when attempting to solve a model. A model may be primal infeasible: there is no possible solution that satisfies all constraints. A model may be dual infeasible: the optimal value of one or more variables is 0 or infinity (negative and positive infinity in logspace). For a GP model that does not solve, solvers may be able to prove its primal or dual infeasibility, or may return an unknown status. @@ -11,6 +11,7 @@ GPkit contains several tools for diagnosing which constraints and variables migh .. literalinclude:: examples/debug.py .. literalinclude:: examples/debug_output.txt + :language: breakdowns Note that certain modeling errors (such as omitting or forgetting a constraint) may be difficult to diagnose from this output. @@ -33,7 +34,7 @@ Potential errors and warnings - ``RuntimeWarning: Dual cost nan does not match primal cost 1.00122315152`` - Similarly to the above, this warning may be seen in dual infeasible models, see *Dual Infeasibility* below. -.. +.. note: remove the above when we match solver tolerance in GPkit (issue #753) @@ -54,6 +55,7 @@ For example, Mosek returns ``DUAL_INFEAS_CER`` when attempting to solve the foll Upon viewing the printed output, .. literalinclude:: examples/unbounded_output.txt + :language: breakdowns The problem, unsurprisingly, is that the cost ``1/x`` has no lower bound because ``x`` has no upper bound. @@ -84,3 +86,4 @@ If you suspect your model is primal infeasible, you can find the nearest primal .. literalinclude:: examples/relaxation.py .. literalinclude:: examples/relaxation_output.txt + :language: breakdowns diff --git a/docs/source/examples.rst b/docs/source/examples.rst index 9a970e78..b3a899b8 100644 --- a/docs/source/examples.rst +++ b/docs/source/examples.rst @@ -17,6 +17,7 @@ minimize :math:`x` subject to the constraint :math:`x \ge 1`. Of course, the optimal value is 1. Output: .. literalinclude:: examples/x_greaterthan_1_output.txt + :language: breakdowns Maximizing the Volume of a Box ============================== @@ -27,6 +28,7 @@ This example comes from Section 2.4 of the `GP tutorial ["0", "9"]: + # the code to create solar.p is in ./breakdowns/solartest.py + filepath = os.path.dirname(os.path.realpath(__file__)) + os.sep + "solar.p" + sol = pickle.load(open(filepath, "rb")) + bds = Breakdowns(sol) + + print("Cost breakdown (as seen in solution tables)") + print("==============") + bds.plot("cost") + + print("Variable breakdowns (note the two methods of access)") + print("===================") + varkey, = sol["variables"].keymap[("Mission.FlightSegment.AircraftPerf" + ".AircraftDrag.Poper")] + bds.plot(varkey) + bds.plot("AircraftPerf.AircraftDrag.MotorPerf.Q") + + print("Combining the two above by increasing maxwidth") + print("----------------------------------------------") + bds.plot("AircraftPerf.AircraftDrag.Poper", maxwidth=105) + + print("Model sensitivity breakdowns (note the two methods of access)") + print("============================") + bds.plot("model sensitivities") + bds.plot("Aircraft") + + print("Exhaustive variable breakdown traces (and configuration arguments)") + print("====================================") + # often useful as a reference point when reading traces + bds.plot("AircraftPerf.AircraftDrag.Poper", height=12) + # includes factors, can be useful for reading traces as well + bds.plot("AircraftPerf.AircraftDrag.Poper", showlegend=True) + print("\nPermissivity = 2 (the default)") + print("----------------") + bds.trace("AircraftPerf.AircraftDrag.Poper") + print("\nPermissivity = 1 (stops at Pelec = v·i)") + print("----------------") + bds.trace("AircraftPerf.AircraftDrag.Poper", permissivity=1) + + # you can also produce Plotly treemaps/icicle plots of your breakdowns + fig = bds.treemap("model sensitivities", returnfig=True) + fig = bds.icicle("cost", returnfig=True) + # uncommenting any of the below makes and shows the plot directly + # bds.icicle("model sensitivities") + # bds.treemap("cost") diff --git a/docs/source/examples/breakdowns/solartest.py b/docs/source/examples/breakdowns/solartest.py new file mode 100644 index 00000000..b9fe33fe --- /dev/null +++ b/docs/source/examples/breakdowns/solartest.py @@ -0,0 +1,6 @@ +from solar.solar import * +Vehicle = Aircraft(Npod=3, sp=True) +M = Mission(Vehicle, latitude=[20]) +M.cost = M[M.aircraft.Wtotal] + +M.localsolve().save("solar.p") diff --git a/docs/source/examples/breakdowns_output.txt b/docs/source/examples/breakdowns_output.txt new file mode 100644 index 00000000..3da69934 --- /dev/null +++ b/docs/source/examples/breakdowns_output.txt @@ -0,0 +1,210 @@ +Cost breakdown (you may be familiar with this from solution tables) +============== + + ┃┓ ┓ ┓ + ┃┃ ┃ ┃ + ┃┃ ┃ ┃ + ┃┃ ┃ ┃ + ┃┃ ┃ ┃ + ┃┃ ┣╸Battery.W ┣╸Battery.E╶⎨ + ┃┃ ┃ (370lbf) ┃ (165,913kJ) + ┃┃ ┃ ┃ + ┃┃ ┃ ┃ + Cost╺┫┃ ┃ ┃ + (699lbf) ┃┣╸Wtotal ┛ ┛ + ┃┃ (699lbf) ┓ ┓ + ┃┃ ┃ ┣╸Wing.BoxSpar.W╶⎨ + ┃┃ ┣╸Wing.W ┛ (96.1lbf) + ┃┃ ┛ (139lbf) ┣╸Wing.WingSecondStruct.W╶⎨ + ┃┃ ┣╸Motor.W╶⎨ + ┃┃ ┣╸SolarCells.W╶⎨ + ┃┃ ┣╸Empennage.W + ┃┃ ┣╸Wavn + ┃┛ ┣╸[6 terms] + +Variable breakdowns (note the two methods of access) +=================== + + ┃┓ ┓ ┓ + ┃┃ ┃ ┃ + ┃┃ ┃ ┃ + ┃┃ ┃ ┃ + ┃┃ ┃ ┃ + ┃┃ ┃ ┃ + ┃┃ ┃ ┣╸MotorPerf.Q╶⎨ + AircraftPerf.AircraftDrag.Poper╺┫┣╸MotorPerf.Pelec ┣╸MotorPerf.i ┃ (4.8N·m) + (3,194W) ┃┃ (0.685kW) ┃ (36.8A) ┃ + ┃┃ ┃ ┃ + ┃┃ ┃ ┃ + ┃┃ ┃ ┛ + ┃┃ ┃ ┣╸i0 + ┃┛ ┛ ┛ (4.5A, fixed) + ┃┣╸Pavn + ┃┣╸Ppay + + + ┃┓ + AircraftPerf.AircraftDrag.MotorPerf.Q╺┫┃ + (4.8N·m) ┃┣╸..ActuatorProp.CP + ┃┛ (0.00291) + +Combining the two above by increasing maxwidth +---------------------------------------------- + + ┃┓ ┓ ┓ ┓ + ┃┃ ┃ ┃ ┃ + ┃┃ ┃ ┃ ┃ + ┃┃ ┃ ┃ ┃ + ┃┃ ┃ ┃ ┃ + ┃┃ ┃ ┃ ┃ + ┃┃ ┃ ┣╸MotorPerf.Q ┣╸ActuatorProp.CP + AircraftPerf.AircraftDrag.Poper╺┫┣╸MotorPerf.Pelec ┣╸MotorPerf.i ┃ (4.8N·m) ┃ (0.00291) + (3,194W) ┃┃ (0.685kW) ┃ (36.8A) ┃ ┃ + ┃┃ ┃ ┃ ┃ + ┃┃ ┃ ┃ ┃ + ┃┃ ┃ ┛ ┛ + ┃┃ ┃ ┣╸i0 + ┃┛ ┛ ┛ (4.5A, fixed) + ┃┣╸Pavn + ┃┣╸Ppay + +Model sensitivity breakdowns (note the two methods of access) +============================ + + ┃┓ ┓ ┓ ┓ ┓ + ┃┃ ┃ ┃ ┃ ┃ + ┃┃ ┃ ┃ ┃ ┣╸ActuatorProp╶⎨ + ┃┃ ┃ ┃ ┃ ┛ + ┃┃ ┃ ┣╸AircraftPerf ┣╸AircraftDrag ┣╸MotorPerf╶⎨ + ┃┃ ┃ ┃ ┃ ┓ + ┃┣╸Mission ┣╸FlightSegment ┃ ┃ ┣╸[17 terms] + ┃┃ ┃ ┛ ┛ ┛ + ┃┃ ┃ ┣╸FlightState╶⎨ + Model╺┫┃ ┃ ┣╸GustL╶⎨ + ┃┃ ┃ ┣╸SteadyLevelFlight╶⎨ + ┃┃ ┛ ┣╸[49 terms] + ┃┛ ┣╸Climb╶⎨ + ┃┓ + ┃┃ + ┃┃ + ┃┣╸Aircraft╶⎨ + ┃┃ + ┃┛ + ┃┣╸g = 9.81m/s² + + + ┃┓ ┣╸etadischarge = 0.98 + ┃┃ ┛ + ┃┃ ┣╸W ≥ E·minSOC/hbatt/etaRTE/etapack·g + ┃┃ ┣╸etaRTE = 0.95 + ┃┣╸Battery ┣╸etapack = 0.85 + ┃┃ ┣╸hbatt = 350W·hr/kg + ┃┃ ┣╸minSOC = 1.03 + ┃┛ ┛ + ┃┓ + Aircraft╺┫┃ + ┃┣╸Wing╶⎨ + ┃┃ + ┃┛ + ┃┣╸Wtotal/mfac ≥ Fuselage.W[0,0] + Fuselage.W[1,0] + Fuselage.W[2,0] … + ┃┛ + ┃┣╸mfac = 1.05 + ┃┛ + ┃┣╸Empennage╶⎨ + ┃┣╸[23 terms] + ┃┛ + +Exhaustive variable breakdown traces (and configuration arguments) +==================================== + + ┃┓ ┓ ┓ + ┃┃ ┃ ┃ + ┃┃ ┃ ┃ + ┃┃ ┃ ┃ + ┃┃ ┃ ┃ + AircraftPerf.AircraftDrag.Poper╺┫┣╸MotorPerf.Pelec ┣╸MotorPerf.i ┣╸MotorPerf.Q╶⎨ + (3,194W) ┃┃ (0.685kW) ┃ (36.8A) ┃ (4.8N·m) + ┃┃ ┃ ┃ + ┃┃ ┃ ┃ + ┃┃ ┃ ┛ + ┃┛ ┛ ┣╸i0 + ┃┣╸Pavn + + + ┃╤╤┯╤┯╤┯┯╤┓ + ┃╎╎│╎│╎││╎┃ + ┃╎╎│╎│╎││╎┃ + ┃╎╎│╎│╎││╎┃ + ┃╎╎│╎│╎││╎┃ + ┃╎╎│╎│╎││╎┃ + ┃╎╎│╎│DCBA┣╸ActuatorProp.CP + AircraftPerf.AircraftDrag.Poper╺┫╎HGFE╎││╎┃ (0.00291) + (3,194W) ┃J╎│╎│╎││╎┃ + ┃╎╎│╎│╎││╎┃ + ┃╎╎│╎│╎││╎┃ + ┃╎╎│╎│╧┷┷╧┛ + ┃╎╎│╎│┣╸i0 (4.5A, fixed) + ┃╎╧┷╧┷┛ + ┃╎┣╸Pavn (200W, fixed) + ┃╧┣╸Ppay (100W, fixed) + + A 4.53e-05·FlightState.rho·ActuatorProp.omega²·Propeller.R⁵ ×1,653N·m [free factor] + B ActuatorProp.Q = 4.8N·m + C MotorPerf.Q = 4.8N·m + D Kv ×64.2rpm/V [free factor] + E MotorPerf.i = 36.8A + F MotorPerf.v ×18.6V [free factor] + G MotorPerf.Pelec = 0.685kW + H Nprop ×4, fixed + J mpower ×1.05, fixed + +Permissivity = 2 (the default) +---------------- + +AircraftPerf.AircraftDrag.Poper (3,194W) + which in: Poper/mpower ≥ Pavn + Ppay + Pelec·Nprop (sensitivity +5.6) + { through a factor of AircraftPerf.AircraftDrag.mpower (1.05, fixed) } + breaks down into 3 monomials: + 1) forming 90% of the RHS and 90% of the total: + { through a factor of Nprop (4, fixed) } + AircraftPerf.AircraftDrag.MotorPerf.Pelec (0.685kW) + which in: Pelec = v·i (sensitivity -5.1) + breaks down into: + { through a factor of AircraftPerf.AircraftDrag.MotorPerf.v (18.6V) } + AircraftPerf.AircraftDrag.MotorPerf.i (36.8A) + which in: i ≥ Q·Kv + i0 (sensitivity +5.4) + breaks down into 2 monomials: + 1) forming 87% of the RHS and 79% of the total: + { through a factor of Kv (64.2rpm/V) } + AircraftPerf.AircraftDrag.MotorPerf.Q (4.8N·m) + which in: Q = Q (sensitivity -4.7) + breaks down into: + AircraftPerf.AircraftDrag.ActuatorProp.Q (4.8N·m) + which in: CP ≤ Q·omega/(0.5·rho·(omega·R)³·π·R²) (sensitivity +4.7) + { through a factor of 4.53e-05·FlightState.rho·AircraftPerf.AircraftDrag.ActuatorProp.omega²·Propeller.R⁵ (1,653N·m) } + breaks down into: + AircraftPerf.AircraftDrag.ActuatorProp.CP (0.00291) + 2) forming 12% of the RHS and 11% of the total: + i0 (4.5A, fixed) + 2) forming 6% of the RHS and 6% of the total: + AircraftPerf.AircraftDrag.Pavn (200W, fixed) + 3) forming 3% of the RHS and 3% of the total: + AircraftPerf.AircraftDrag.Ppay (100W, fixed) + +Permissivity = 1 (stops at Pelec = v·i) +---------------- + +AircraftPerf.AircraftDrag.Poper (3,194W) + which in: Poper/mpower ≥ Pavn + Ppay + Pelec·Nprop (sensitivity +5.6) + { through a factor of AircraftPerf.AircraftDrag.mpower (1.05, fixed) } + breaks down into 3 monomials: + 1) forming 90% of the RHS and 90% of the total: + { through a factor of Nprop (4, fixed) } + AircraftPerf.AircraftDrag.MotorPerf.Pelec (0.685kW) + which in: Pelec = v·i (sensitivity -5.1) + breaks down into: + AircraftPerf.AircraftDrag.MotorPerf.i·AircraftPerf.AircraftDrag.MotorPerf.v (685A·V) + 2) forming 6% of the RHS and 6% of the total: + AircraftPerf.AircraftDrag.Pavn (200W, fixed) + 3) forming 3% of the RHS and 3% of the total: + AircraftPerf.AircraftDrag.Ppay (100W, fixed) diff --git a/docs/source/examples/docstringparsing_output.txt b/docs/source/examples/docstringparsing_output.txt index 48341482..dbc8d584 100644 --- a/docs/source/examples/docstringparsing_output.txt +++ b/docs/source/examples/docstringparsing_output.txt @@ -29,9 +29,38 @@ h = self.h = Variable('h', 1, 'm', 'minimum height') # from 'h 1 [m] min # way that makes the most sense to someone else reading your model. # -Optimal Cost ------------- - 1.465 + ┃┓ ┓ /┓ + ┃┃ ┃ ┃ + ┃┃ ┣╸s[0] /┣╸h + Cost╺┫┃ ┃ (0.316m) ┃ (1m, fixed) + (1.46m²) ┃┣╸A ┛ /┛ + ┃┃ (1.46m²) ┓ ┓ + ┃┃ ┣╸s[2] ┣╸h + ┃┛ ┛ (1m) ┛ + + + + ┃┓ ┓ + ┃┃ ┃ + ┃┃ ┃ + ┃┃ ┃ + ┃┃ ┣╸A ≥ 2·(s[0]·s[1] + s[1]·s[2] + s[2]·s[0]) + ┃┃ ┃ + ┃┃ ┃ + ┃┃ ┛ + ┃┃ ┓ + Model╺┫┃ ┃ + ┃┣╸Cube ┣╸V = 100l + ┃┃ ┛ + ┃┃ ┓ + ┃┃ ┃ + ┃┃ ┣╸V ≤ s[:].prod() + ┃┃ ┛ + ┃┃ ┣╸h = 1m + ┃┃ ┛ + ┃┃ ┣╸s[2] ≥ h + ┃┛ ┛ + Free Variables -------------- diff --git a/docs/source/examples/external_constraint.py b/docs/source/examples/external_constraint.py index debcd183..6199ac79 100644 --- a/docs/source/examples/external_constraint.py +++ b/docs/source/examples/external_constraint.py @@ -20,6 +20,5 @@ def as_gpconstr(self, x0): # Otherwise calls external code at the current position... x_star = x0[self.x] res = external_code(x_star) - # ...and returns a linearized posy <= 1 + # ...and returns a posynomial approximation around that position return (self.y >= res * self.x/x_star) - \ No newline at end of file diff --git a/docs/source/examples/external_sp_output.txt b/docs/source/examples/external_sp_output.txt index 90177398..e633369e 100644 --- a/docs/source/examples/external_sp_output.txt +++ b/docs/source/examples/external_sp_output.txt @@ -1,15 +1,23 @@ -Optimal Cost ------------- - 0.7071 + ┃┓ + Cost╺┫┃ + (0.707) ┃┣╸0.785 + ┃┛ + + + + ┃┓ + ┃┃ + ┃┣╸ + Model╺┫┛ + ┃┓ + ┃┃ + ┃┣╸x ≥ 0.785 + ┃┛ + Free Variables -------------- x : 0.7854 y : 0.7071 -Most Sensitive Constraints --------------------------- - +1 : - +1 : x ≥ 0.785 - diff --git a/docs/source/examples/gettingstarted_output.txt b/docs/source/examples/gettingstarted_output.txt index 32f10655..d07fd539 100644 --- a/docs/source/examples/gettingstarted_output.txt +++ b/docs/source/examples/gettingstarted_output.txt @@ -1,7 +1,20 @@ -Optimal Cost ------------- - 0.005511 + ┃/┓ + Cost╺┫ ┃ + (0.00551) ┃/┣╸y + ┃/┛ (4.08) + + + + ┃┓ + ┃┃ + ┃┃ + Model╺┫┣╸2·x·y + 2·x·z + 2·y·z ≤ 200 + ┃┃ + ┃┃ + ┃┛ + ┃┣╸x ≥ 2·y + Free Variables -------------- diff --git a/docs/source/examples/issue_1513_output.txt b/docs/source/examples/issue_1513_output.txt index cbf3d22b..e0b7f7af 100644 --- a/docs/source/examples/issue_1513_output.txt +++ b/docs/source/examples/issue_1513_output.txt @@ -1,12 +1,28 @@ -Optimal Cost ------------- - 1 + ┃┓ ┓ ┓ + ┃┃ ┃ ┃ + ┃┃ ┃ ┃ + Cost╺┫┃ ┣╸a[0,0] ┣╸1 + (1) ┃┣╸z[0] ┃ (1) ┃ + ┃┃ (1) ┛ ┛ + ┃┃ ┣╸a[1,0] ┣╸1 + ┃┛ ┛ (1) ┛ + + + + ┃┓ ┓ ┓ + ┃┃ ┃ ┃ + ┃┃ ┃ ┣╸a[0,0] ≥ 1 + ┃┃ ┣╸Vehicle ┛ + ┃┃ ┃ ┣╸a[1,0] ≥ 1 + Model╺┫┃ ┛ ┛ + ┃┣╸Fleet2 ┓ + ┃┃ ┃ + ┃┃ ┃ + ┃┃ ┣╸z[0] ≥ a[0,0]·y[0,0]/x[0] + y[1,0]/x[0]·a[1,0] + ┃┃ ┃ + ┃┛ ┛ -Model Sensitivities (sorts models in sections below) -------------------- - +3.0 : System.Fleet2 - +1.0 : System.Fleet2.Vehicle Free Variables -------------- @@ -30,21 +46,57 @@ y : [ - +0.25 ] Most Sensitive Constraints -------------------------- | System.Fleet2 - +1 : z[0] ≥ a[0,0]·x[0]^-1·y[0,0] + y[1,0]/x[0]·a[1,0] + +1 : z[0] ≥ a[0,0]·y[0,0]/x[0] + y[1,0]/x[0]·a[1,0] | System.Fleet2.Vehicle +0.75 : a[0,0] ≥ 1 +0.25 : a[1,0] ≥ 1 -Optimal Cost ------------- - 3 + ┃┓ ┓ ┓ + ┃┃ ┃ ┃ + ┃┃ ┣╸a[0,0] ┣╸1 + ┃┣╸z[0] ┃ (1) ┃ + ┃┃ (1) ┛ ┛ + ┃┃ ┣╸a[1,0] ┣╸1 + ┃┛ ┛ (1) ┛ + ┃┓ ┓ ┓ + ┃┃ ┃ ┃ + Cost╺┫┃ ┣╸a[0,1] ┣╸1 + (3) ┃┣╸z[1] ┃ (1) ┃ + ┃┃ (1) ┛ ┛ + ┃┃ ┣╸a[1,1] ┣╸1 + ┃┛ ┛ (1) ┛ + ┃┓ ┓ ┓ + ┃┃ ┃ ┃ + ┃┃ ┣╸a[0,2] ┣╸1 + ┃┣╸z[2] ┛ (1) ┛ + ┃┃ (1) ┣╸a[1,2] ┣╸1 + ┃┛ ┛ (1) ┛ + + + + ┃┓ ┓ ┓ + ┃┃ ┃ ┣╸a[0,0] ≥ 1 + ┃┃ ┃ ┛ + ┃┃ ┃ ┓ + ┃┃ ┃ ┣╸a[0,1] ≥ 1 + ┃┃ ┣╸Vehicle ┛ + ┃┃ ┃ ┣╸a[0,2] ≥ 1 + ┃┃ ┃ ┛ + ┃┃ ┃ ┣╸a[1,0] ≥ 1 + Model╺┫┃ ┃ ┣╸a[1,1] ≥ 1 + ┃┣╸Fleet2 ┛ ┣╸a[1,2] ≥ 1 + ┃┃ ┓ + ┃┃ ┣╸z[0] ≥ a[0,0]·y[0,0]/x[0] + y[1,0]/x[0]·a[1,0] + ┃┃ ┛ + ┃┃ ┓ + ┃┃ ┣╸z[1] ≥ a[0,1]·y[0,1]/x[1] + y[1,1]/x[1]·a[1,1] + ┃┃ ┛ + ┃┃ ┓ + ┃┃ ┣╸z[2] ≥ a[0,2]·y[0,2]/x[2] + y[1,2]/x[2]·a[1,2] + ┃┛ ┛ -Model Sensitivities (sorts models in sections below) -------------------- - +3.0 : System2.Fleet2 - +1.0 : System2.Fleet2.Vehicle Free Variables -------------- @@ -71,18 +123,51 @@ y : [ - - - Most Sensitive Constraints -------------------------- | System2.Fleet2 - +0.33 : z[0] ≥ a[0,0]·x[0]^-1·y[0,0] + y[1,0]/x[0]·a[1,0] - +0.33 : z[1] ≥ a[0,1]·x[1]^-1·y[0,1] + y[1,1]/x[1]·a[1,1] - +0.33 : z[2] ≥ a[0,2]·x[2]^-1·y[0,2] + y[1,2]/x[2]·a[1,2] + +0.33 : z[0] ≥ a[0,0]·y[0,0]/x[0] + y[1,0]/x[0]·a[1,0] + +0.33 : z[1] ≥ a[0,1]·y[0,1]/x[1] + y[1,1]/x[1]·a[1,1] + +0.33 : z[2] ≥ a[0,2]·y[0,2]/x[2] + y[1,2]/x[2]·a[1,2] | System2.Fleet2.Vehicle +0.25 : a[0,0] ≥ 1 +0.25 : a[0,1] ≥ 1 -Optimal Cost ------------- - 20 + ┃┓ ┓ + ┃┃ ┃ + ┃┃ ┃ + ┃┃ ┃ + ┃┃ ┣╸z[2] + ┃┃ ┃ (9, fixed) + ┃┣╸x[2] ┃ + ┃┃ (12) ┃ + ┃┃ ┛ + Cost╺┫┃ ┓ + (20) ┃┃ ┣╸y[2] + ┃┛ ┛ (3) + ┃┓ ┓ + ┃┃ ┃ + ┃┃ ┣╸z[1] + ┃┣╸x[1] ┛ (4, fixed) + ┃┃ (6) ┣╸y[1] + ┃┛ ┛ (2) + ┃┣╸x[0]╶⎨ + ┃┛ (2) + + + + ┃┓ ┓ + ┃┃ ┃ + ┃┃ ┃ + ┃┃ ┣╸x[2] ≥ y[2] + z[2] + ┃┃ ┃ + Model╺┫┃ ┃ + ┃┣╸Simple ┛ + ┃┃ ┓ + ┃┃ ┃ + ┃┃ ┣╸x[1] ≥ y[1] + z[1] + ┃┃ ┛ + ┃┛ ┣╸x[0] ≥ y[0] + z[0] + Swept Variables --------------- diff --git a/docs/source/examples/issue_1522_output.txt b/docs/source/examples/issue_1522_output.txt index 6505c64b..e4fa06b4 100644 --- a/docs/source/examples/issue_1522_output.txt +++ b/docs/source/examples/issue_1522_output.txt @@ -1,12 +1,48 @@ -Optimal Cost ------------- - 15 + ┃┓ ┓ + ┃┃ ┃ + ┃┣╸y[0] ┣╸x[1,0] + ┃┛ (3) ┛ (3, fixed) + ┃┓ ┓ + ┃┃ ┃ + ┃┣╸y[1] ┣╸x[1,1] + ┃┛ (3) ┛ (3, fixed) + ┃┓ ┓ + Cost╺┫┃ ┃ + (15) ┃┣╸y[2] ┣╸x[1,2] + ┃┛ (3) ┛ (3, fixed) + ┃┓ ┓ + ┃┃ ┃ + ┃┣╸y[3] ┣╸x[1,3] + ┃┛ (3) ┛ (3, fixed) + ┃┓ ┓ + ┃┃ ┃ + ┃┣╸y[4] ┣╸x[1,4] + ┃┛ (3) ┛ (3, fixed) + + + + ┃┓ ┓ + ┃┃ ┃ + ┃┃ ┣╸y[0] ≥ x[1,0] + ┃┃ ┛ + ┃┃ ┓ + ┃┃ ┃ + ┃┃ ┣╸y[1] ≥ x[1,1] + ┃┃ ┛ + ┃┃ ┓ + Model╺┫┃ ┃ + ┃┣╸Cake ┣╸y[2] ≥ x[1,2] + ┃┃ ┛ + ┃┃ ┓ + ┃┃ ┃ + ┃┃ ┣╸y[3] ≥ x[1,3] + ┃┃ ┛ + ┃┃ ┓ + ┃┃ ┃ + ┃┃ ┣╸y[4] ≥ x[1,4] + ┃┛ ┛ -Model Sensitivities (sorts models in sections below) -------------------- - +1.0 : Yum1.Cake - : Yum1.Cake.Pie Free Variables -------------- @@ -37,14 +73,18 @@ Most Sensitive Constraints +0.2 : y[4] ≥ x[1,4] -Optimal Cost ------------- - 3 + ┃┓ + Cost╺┫┃ + (3) ┃┣╸x[1,0] + ┃┛ (3, fixed) + + + + ┃┓ + Model╺┫┃ + ┃┣╸y[0] ≥ x[1,0] + ┃┛ -Model Sensitivities (sorts models in sections below) -------------------- - +1.0 : Yum2.Cake - : Yum2.Cake.Pie Free Variables -------------- diff --git a/docs/source/examples/performance_modeling_output.txt b/docs/source/examples/performance_modeling_output.txt index 633d1d6f..b9edeaac 100644 --- a/docs/source/examples/performance_modeling_output.txt +++ b/docs/source/examples/performance_modeling_output.txt @@ -13,76 +13,86 @@ Constraints FlightSegment AircraftP Wburn[:] ≥ 0.1·D[:] - Aircraft.W + Wfuel[:] ≤ 0.5·rho[:]·CL[:]·S·V[:]² + Aircraft.W + Wfuel[:] ≤ 0.5·Mission.FlightSegment.FlightState.rho[:]·CL[:]·S·V[:]² "performance": WingAero - D[:] ≥ 0.5·rho[:]·V[:]²·CD[:]·S - Re[:] = rho[:]·V[:]·c/mu[:] + D[:] ≥ 0.5·Mission.FlightSegment.FlightState.rho[:]·V[:]²·CD[:]·S + Re[:] = Mission.FlightSegment.FlightState.rho[:]·V[:]·c/mu[:] CD[:] ≥ 0.074/Re[:]^0.2 + CL[:]²/π/A/e[:] FlightState (no constraints) Aircraft - Aircraft.W ≥ Aircraft.Fuselage.W + Aircraft.Wing.W + Aircraft.W ≥ Fuselage.W + Wing.W Fuselage (no constraints) Wing c = (S/A)^0.5 - Aircraft.Wing.W ≥ S·Aircraft.Wing.rho + Wing.W ≥ S·Wing.rho + + ┃┓ ┓ ┓ ┓ ┓ + ┃┃ ┃ ┃ ┃ ┃ + ┃┃ ┃ ┃ ┣╸Wburn[2] ┣╸CD[2]╶⎨ + ┃┃ ┃ ┃ ┃ (0.272lbf) ┃ (0.0189) + ┃┃ ┃ ┃ ┛ ┛ + ┃┃ ┃ ┣╸Wfuel[2] ┓ ┓ + ┃┃ ┃ ┃ (0.544lbf) ┃ ┃ + ┃┃ ┣╸Wfuel[1] ┃ ┣╸Wfuel[3] ┣╸CD[3]╶⎨ + ┃┃ ┃ (0.817lbf) ┃ ┃ (0.272lbf) ┃ (0.0188) + Cost╺┫┃ ┃ ┛ ┛ ┛ + (1.09lbf) ┃┣╸Wfuel[0] ┃ ┓ ┓ ┓ + ┃┃ (1.09lbf) ┃ ┃ ┃ ┣╸CL[1]² + ┃┃ ┃ ┣╸Wburn[1] ┣╸CD[1] ┛ (1.01) + ┃┃ ┃ ┃ (0.273lbf) ┃ (0.0189) ┣╸1/Re[1]^0.2 + ┃┃ ┛ ┛ ┛ ┛ (0.0772) + ┃┃ ┓ ┓ ┓ + ┃┃ ┃ ┃ ┣╸CL[0]² + ┃┃ ┣╸Wburn[0] ┣╸CD[0] ┛ (1.01) + ┃┃ ┃ (0.274lbf) ┃ (0.019) ┣╸1/Re[0]^0.2 + ┃┛ ┛ ┛ ┛ (0.0772) + + + + ┃┓ ┓ ┓ + ┃┃ ┃ ┃ + ┃┃ ┃ ┃ + ┃┃ ┃ ┃ + ┃┃ ┣╸FlightSegment ┣╸AircraftP╶⎨ + ┃┃ ┃ ┃ + ┃┣╸Mission ┃ ┃ + ┃┃ ┃ ┃ + ┃┃ ┛ ┛ + Model╺┫┃ ┣╸Wfuel[0] ≥ Wfuel[1] + Wburn[0] + ┃┃ ┛ + ┃┃ ┣╸Wfuel[1] ≥ Wfuel[2] + Wburn[1] + ┃┛ ┣╸Wfuel[2] ≥ Wfuel[3] + Wburn[2] + ┃┓ ┓ + ┃┃ ┣╸Wing╶⎨ + ┃┃ ┛ + ┃┣╸Aircraft ┣╸W ≥ Fuselage.W + Wing.W + ┃┃ ┛ + ┃┃ ┣╸Fuselage ┣╸W = 100lbf + ┃┛ ┛ ┛ -Optimal Cost ------------- - 1.091 Free Variables -------------- - | Mission.FlightSegment.AircraftP.WingAero - D : [ 2.74 2.73 2.72 2.72 ] [lbf] drag force - - | Mission.FlightSegment.AircraftP -Wburn : [ 0.274 0.273 0.272 0.272 ] [lbf] segment fuel burn -Wfuel : [ 1.09 0.817 0.544 0.272 ] [lbf] fuel weight + | Aircraft + W : 144.1 [lbf] weight | Aircraft.Wing S : 44.14 [ft²] surface area W : 44.14 [lbf] weight c : 1.279 [ft] mean chord - | Aircraft - W : 144.1 [lbf] weight - -Variable Sensitivities ----------------------- - | Aircraft.Fuselage - W : +0.97 weight - - | Aircraft.Wing - A : -0.67 aspect ratio -rho : +0.43 areal density - -Next Most Sensitive Variables ------------------------------ - | Mission.FlightSegment.AircraftP.WingAero - e : [ -0.18 -0.18 -0.18 -0.18 ] Oswald efficiency - - | Mission.FlightSegment.FlightState - V : [ -0.22 -0.21 -0.21 -0.21 ] true airspeed -rho : [ -0.12 -0.11 -0.11 -0.11 ] air density - -Most Sensitive Constraints --------------------------- - | Aircraft - +1.4 : .W ≥ .Fuselage.W + .Wing.W - - | Mission - +1 : Wfuel[0] ≥ Wfuel[1] + Wburn[0] - +0.75 : Wfuel[1] ≥ Wfuel[2] + Wburn[1] - +0.5 : Wfuel[2] ≥ Wfuel[3] + Wburn[2] + | Mission.FlightSegment.AircraftP +Wburn : [ 0.274 0.273 0.272 0.272 ] [lbf] segment fuel burn +Wfuel : [ 1.09 0.817 0.544 0.272 ] [lbf] fuel weight - | Aircraft.Wing - +0.43 : .W ≥ S·.rho + | Mission.FlightSegment.AircraftP.WingAero + D : [ 2.74 2.73 2.72 2.72 ] [lbf] drag force Insensitive Constraints |below +1e-05| -------------------------------------- @@ -97,7 +107,7 @@ Constraint Differences @@ -31,3 +31,4 @@ Wing c = (S/A)^0.5 - Aircraft.Wing.W ≥ S·Aircraft.Wing.rho + Wing.W ≥ S·Wing.rho + Wburn[:] ≥ 0.2·D[:] ********************** diff --git a/docs/source/examples/relaxation.py b/docs/source/examples/relaxation.py index 8cf8e129..5a084ff9 100644 --- a/docs/source/examples/relaxation.py +++ b/docs/source/examples/relaxation.py @@ -21,6 +21,8 @@ mr1 = Model(allrelaxed.relaxvar, allrelaxed) print(mr1) print(mr1.solve(verbosity=0).table()) # solves with an x of 1.414 +from gpkit.breakdowns import Breakdowns +Breakdowns(mr1.solution).trace("cost") print("") print("With constraints relaxed individually") diff --git a/docs/source/examples/relaxation_output.txt b/docs/source/examples/relaxation_output.txt index 24f295d2..de29c43c 100644 --- a/docs/source/examples/relaxation_output.txt +++ b/docs/source/examples/relaxation_output.txt @@ -25,9 +25,30 @@ Constraints x ≤ C·x_max x_min ≤ C·x -Optimal Cost ------------- - 1.414 + ┃┓ + Cost╺┫┃ + (1.41) ┃┣╸x_min + ┃┛ (2, fixed) + + + + ┃┓ + ┃┃ + ┃┣╸x ≤ C·x_max + ┃┛ + ┃┓ + ┃┃ + ┃┣╸x_max = 1 + Model╺┫┛ + ┃┓ + ┃┃ + ┃┣╸x_min = 2 + ┃┛ + ┃┓ + ┃┃ + ┃┣╸x_min ≤ C·x + ┃┛ + ~~~~~~~~ WARNINGS @@ -60,6 +81,18 @@ Most Sensitive Constraints +0.5 : x_min ≤ C·x +C (1.41) + breaks down into: + C (1.41) + which in: x ≤ C·x_max (sensitivity +0.5) + { through a factor of 1/x_max (1, fixed) } + breaks down into: + x (1.41) + which in: x_min ≤ C·x (sensitivity +0.5) + breaks down into: + { through a factor of 1/C (0.707) } + x_min (2, fixed) + With constraints relaxed individually ===================================== @@ -75,9 +108,30 @@ Constraints x ≤ C[0]·x_max x_min ≤ C[1]·x -Optimal Cost ------------- - 2 + ┃┓ + Cost╺┫┃ + (2) ┃┣╸1/x + ┃┛ (1) + + + + ┃┓ + ┃┃ + ┃┣╸x_min = 2 + ┃┛ + ┃┓ + ┃┃ + ┃┣╸x_min ≤ C[1]·x + Model╺┫┛ + ┃┓ + ┃┃ + ┃┣╸x ≤ C[0]·x_max + ┃┛ + ┃┓ + ┃┃ + ┃┣╸x_max = 1 + ┃┛ + ~~~~~~~~ WARNINGS @@ -128,16 +182,41 @@ Constraints "relaxation constraints": "x_max": Relax2.x_max ≥ 1 - x_max ≥ Relax2.OriginalValues.x_max/Relax2.x_max - x_max ≤ Relax2.OriginalValues.x_max·Relax2.x_max + x_max ≥ OriginalValues.x_max/Relax2.x_max + x_max ≤ OriginalValues.x_max·Relax2.x_max "x_min": Relax2.x_min ≥ 1 - x_min ≥ Relax2.OriginalValues.x_min/Relax2.x_min - x_min ≤ Relax2.OriginalValues.x_min·Relax2.x_min + x_min ≥ OriginalValues.x_min/Relax2.x_min + x_min ≤ OriginalValues.x_min·Relax2.x_min + + ┃┓ + Cost╺┫┃ + (2) ┃┣╸1/Relax2.x_min + ┃┛ (0.5) + + + + ┃┓ + ┃┣╸x ≥ x_min + ┃┛ + ┃┓ + ┃┃ + ┃┣╸x_min = 1 + ┃┛ + ┃┓ + ┃┃ + Model╺┫┣╸x_min ≥ OriginalValues.x_min/Relax2.x_min + ┃┛ + ┃┓ + ┃┣╸x ≤ x_max + ┃┛ + ┃┓ + ┃┣╸x_max = 1 + ┃┛ + ┃┓ + ┃┣╸x_max ≤ OriginalValues.x_max·Relax2.x_max + ┃┛ -Optimal Cost ------------- - 2 ~~~~~~~~ WARNINGS @@ -147,11 +226,6 @@ Relaxed Constants x_min: relaxed from 2 to 1 ~~~~~~~~ -Model Sensitivities (sorts models in sections below) -------------------- - +2.0 : Relax2.OriginalValues - <1e-8 : Relax2 - Free Variables -------------- x : 1 @@ -176,8 +250,8 @@ x_max : -0.99 Most Sensitive Constraints -------------------------- +1 : x ≥ x_min - +1 : x_min ≥ Relax2.OriginalValues.x_min/Relax2.x_min + +1 : x_min ≥ OriginalValues.x_min/Relax2.x_min +0.99 : x ≤ x_max - +0.99 : x_max ≤ Relax2.OriginalValues.x_max·Relax2.x_max + +0.99 : x_max ≤ OriginalValues.x_max·Relax2.x_max diff --git a/docs/source/examples/simple_box_output.txt b/docs/source/examples/simple_box_output.txt index edc8b27e..aae5bf21 100644 --- a/docs/source/examples/simple_box_output.txt +++ b/docs/source/examples/simple_box_output.txt @@ -1,7 +1,28 @@ -Optimal Cost ------------- - 0.003674 + ┃/┓ + Cost╺┫ ┃ + (0.00367/m³) ┃/┣╸alpha + ┃/┛ (2, fixed) + + + + ┃┓ + ┃┃ + ┃┃ + ┃┣╸A_{wall} = 200m² + ┃┃ + ┃┛ + ┃┓ + Model╺┫┃ + ┃┃ + ┃┣╸A_{wall} ≥ 2·h·w + 2·h·d + ┃┃ + ┃┛ + ┃┣╸alpha = 2 + ┃┛ + ┃┣╸alpha ≤ h/w + ┃┛ + Free Variables -------------- diff --git a/docs/source/examples/simple_sp_output.txt b/docs/source/examples/simple_sp_output.txt index 14d6e7d9..bdec6ce3 100644 --- a/docs/source/examples/simple_sp_output.txt +++ b/docs/source/examples/simple_sp_output.txt @@ -1,17 +1,25 @@ -Optimal Cost ------------- - 0.9 + ┃┓ + Cost╺┫┃ + (0.9) ┃┣╸1/y^0.1 + ┃┛ (1.26) + + + + ┃┓ + ┃┃ + ┃┃ + Model╺┫┣╸1 - y ≤ x + ┃┃ + ┃┃ + ┃┛ + ┃┣╸y ≤ 0.1 + Free Variables -------------- x : 0.9 y : 0.1 -Most Sensitive Constraints --------------------------- - +1.1 : 1 - y ≤ x - +0.11 : y ≤ 0.1 - x values of each GP solve (note convergence) 2.50000, 0.92548, 0.90003, 0.90000 diff --git a/docs/source/examples/simpleflight_output.txt b/docs/source/examples/simpleflight_output.txt index 2878b89e..26adde6b 100644 --- a/docs/source/examples/simpleflight_output.txt +++ b/docs/source/examples/simpleflight_output.txt @@ -1,9 +1,38 @@ SINGLE ====== -Optimal Cost ------------- - 303.1 + ┃┓ ┓ + ┃┃ ┃ + ┃┃ ┣╸W_0 + Cost╺┫┃ ┃ (4,940N, fixed) + (303N) ┃┣╸W ┛ + ┃┃ (7,341N) ┓ ┓ + ┃┃ ┣╸W_w ┣╸S╶⎨ + ┃┛ ┛ (2,401N) ┛ (16.4m²) + + + + ┃┓ + ┃┣╸W ≥ W_0 + W_w + ┃┛ + ┃┣╸C_D ≥ (CDA0)/S + k·C_f·(\frac{S}{S_{wet}}) + C_L²/(π·A·e) + ┃┛ + ┃┣╸D ≥ 0.5·\rho·S·C_D·V² + ┃┛ + ┃┣╸W_0 = 4,940N + ┃┛ + Model╺┫┣╸W ≤ 0.5·\rho·S·C_L·V² + ┃┛ + ┃┣╸e = 0.95 + ┃┣╸(\frac{S}{S_{wet}}) = 2.05 + ┃┣╸C_f ≥ 0.074/Re^0.2 + ┃┣╸k = 1.2 + ┃┓ + ┃┃ + ┃┣╸[12 terms] + ┃┃ + ┃┛ + Free Variables -------------- @@ -18,22 +47,6 @@ C_f : 0.003599 skin friction coefficient W : 7341 [N] total aircraft weight W_w : 2401 [N] wing weight -Most Sensitive Variables ------------------------- - W_0 : +1 aircraft weight excluding wing - e : -0.48 Oswald efficiency factor -(\frac{S}{S_{wet}}) : +0.43 wetted area ratio - k : +0.43 form factor - V_{min} : -0.37 takeoff speed - -Most Sensitive Constraints --------------------------- - +1.3 : W ≥ W_0 + W_w - +1 : C_D ≥ (CDA0)/S + k·C_f·(\frac{S}{S_{wet}}) + C_L²/(π·A·e) - +1 : D ≥ 0.5·\rho·S·C_D·V² - +0.96 : W ≤ 0.5·\rho·S·C_L·V² - +0.43 : C_f ≥ 0.074/Re^0.2 - Solution Diff ============= (argument is the baseline solution) @@ -68,22 +81,6 @@ C_f : [ 0.00333 0.00314 0.00361 0.00342 ] skin friction coefficient W : [ 6.85e+03 6.4e+03 6.97e+03 6.44e+03 ] [N] total aircraft weight W_w : [ 1.91e+03 1.46e+03 2.03e+03 1.5e+03 ] [N] wing weight -Most Sensitive Variables ------------------------- - W_0 : [ +0.92 +0.85 +0.95 +0.85 ] aircraft weight excluding wing - V_{min} : [ -0.82 -1 -0.41 -0.71 ] takeoff speed - V : [ +0.59 +0.97 +0.25 +0.75 ] cruising speed -(\frac{S}{S_{wet}}) : [ +0.56 +0.63 +0.45 +0.54 ] wetted area ratio - k : [ +0.56 +0.63 +0.45 +0.54 ] form factor - -Most Sensitive Constraints (in last sweep) ------------------------------------------- - +1 : C_D ≥ (CDA0)/S + k·C_f·(\frac{S}{S_{wet}}) + C_L²/(π·A·e) - +1 : D ≥ 0.5·\rho·S·C_D·V² - +1 : W ≥ W_0 + W_w - +0.57 : W ≤ 0.5·\rho·S·C_L·V² - +0.54 : C_f ≥ 0.074/Re^0.2 - Solution Diff ============= (argument is the baseline solution) diff --git a/docs/source/examples/sin_approx_example_output.txt b/docs/source/examples/sin_approx_example_output.txt index bb27c809..bd6006eb 100644 --- a/docs/source/examples/sin_approx_example_output.txt +++ b/docs/source/examples/sin_approx_example_output.txt @@ -1,15 +1,23 @@ -Optimal Cost ------------- - 0.7854 + ┃┓ + Cost╺┫┃ + (0.785) ┃┣╸0.785 + ┃┛ + + + + ┃┓ + ┃┃ + ┃┣╸x ≥ 0.785 + Model╺┫┛ + ┃┓ + ┃┃ + ┃┣╸y ≥ x + ┃┛ + Free Variables -------------- x : 0.7854 y : 0.7854 -Most Sensitive Constraints --------------------------- - +1 : x ≥ 0.785 - +1 : y ≥ x - diff --git a/docs/source/examples/solar.p b/docs/source/examples/solar.p new file mode 100644 index 00000000..02e6c663 Binary files /dev/null and b/docs/source/examples/solar.p differ diff --git a/docs/source/examples/sp_to_gp_sweep_output.txt b/docs/source/examples/sp_to_gp_sweep_output.txt index 35dad72c..66227746 100644 --- a/docs/source/examples/sp_to_gp_sweep_output.txt +++ b/docs/source/examples/sp_to_gp_sweep_output.txt @@ -44,19 +44,3 @@ V_{f_{avail}} : [ 0.589 0.788 0.958 ] [m³] fuel volume available W_w_strc : [ 1.33e+03 269 151 ] [N] wing structural weight W_w_surf : [ 1.32e+03 1.78e+03 2.14e+03 ] [N] wing skin weight -Most Sensitive Variables ------------------------- - V_{min} : [ -1.4 - - ] takeoff speed - Range : [ +1.4 +1.1 +1.2 ] aircraft range - TSFC : [ +1.4 +1.1 +1.2 ] thrust specific fuel consumption -(\frac{S}{S_{wet}}) : [ +0.85 +0.71 +0.74 ] wetted area ratio - k : [ +0.85 +0.71 +0.74 ] form factor - -Most Sensitive Constraints (in last sweep) ------------------------------------------- - +1.2 : C_D ≥ (CDA0)/S + k·C_f·(\frac{S}{S_{wet}}) + C_L²/(π·A·e) - +1.2 : D ≥ 0.5·\rho·S·C_D·V² - +1.2 : T_{flight} ≥ Range/V - +1.2 : W_f ≥ TSFC·T_{flight}·D - +0.74 : C_f ≥ 0.074/Re^0.2 - diff --git a/docs/source/examples/unbounded_output.txt b/docs/source/examples/unbounded_output.txt index 9950c576..149911a3 100644 --- a/docs/source/examples/unbounded_output.txt +++ b/docs/source/examples/unbounded_output.txt @@ -1,7 +1,16 @@ -Optimal Cost ------------- - 1e-30 + ┃┓ + Cost╺┫┃ + (1e-30) ┃┣╸1/x + ┃┛ (1e-30) + + + + ┃┓ + Model╺┫┃ + ┃┣╸x ≤ 1e+30 + ┃┛ + ~~~~~~~~ WARNINGS @@ -16,7 +25,3 @@ Free Variables -------------- x : 1e+30 -Most Sensitive Constraints --------------------------- - +1 : x ≤ 1e+30 - diff --git a/docs/source/examples/vectorize_output.txt b/docs/source/examples/vectorize_output.txt index db2e0374..1aea6f4e 100644 --- a/docs/source/examples/vectorize_output.txt +++ b/docs/source/examples/vectorize_output.txt @@ -1,32 +1,48 @@ SCALAR -Optimal Cost ------------- - 1 + ┃┓ + Cost╺┫┃ + (1) ┃┣╸1 + ┃┛ + + + + ┃┓ + Model╺┫┃ + ┃┣╸x ≥ 1 + ┃┛ + Free Variables -------------- x : 1 -Most Sensitive Constraints --------------------------- - +1 : x ≥ 1 - __________ VECTORIZED -Optimal Cost ------------- - 2 + ┃┓ + Cost╺┫┃ + (2) ┃┣╸2 + ┃┛ + + + + ┃┓ ┓ + ┃┃ ┃ + ┃┃ ┣╸x[0] ≥ 1 + ┃┃ ┛ + ┃┣╸Test1 ┓ + Model╺┫┃ ┃ + ┃┃ ┣╸x[2] ≥ 1 + ┃┛ ┛ + ┃┓ + ┃┃ + ┃┣╸x[1] ≥ 2 + ┃┛ + Free Variables -------------- x : [ 1 2 1 ] -Most Sensitive Constraints --------------------------- - +1 : x[0] ≥ 1 - +1 : x[1] ≥ 2 - +1 : x[2] ≥ 1 - diff --git a/docs/source/examples/water_tank_output.txt b/docs/source/examples/water_tank_output.txt index 367bdadc..67234be1 100644 --- a/docs/source/examples/water_tank_output.txt +++ b/docs/source/examples/water_tank_output.txt @@ -1,8 +1,37 @@ Infeasible monomial equality: Cannot convert from 'V [m³]' to 'M [kg]' -Optimal Cost ------------- - 1.293 + ┃┓ ┓ ┓ + ┃┃ ┃ ┃ + ┃┃ ┣╸d[0] ┣╸M + Cost╺┫┃ ┃ (0.464m) ┃ (100kg, fixed) + (1.29m²) ┃┣╸A ┛ ┛ + ┃┃ (1.29m²) ┓ + ┃┃ ┣╸d[1]·d[2] + ┃┛ ┛ (0.215m²) + + + + ┃┓ + ┃┃ + ┃┣╸A ≥ 2·(d[0]·d[1] + d[0]·d[2] + d[1]·d[2]) + ┃┛ + ┃┓ + ┃┃ + ┃┣╸M = 100kg + ┃┛ + ┃┓ + Model╺┫┃ + ┃┣╸M = V·\rho + ┃┛ + ┃┓ + ┃┃ + ┃┣╸V = d[0]·d[1]·d[2] + ┃┛ + ┃┓ + ┃┃ + ┃┣╸\rho = 1,000kg/m³ + ┃┛ + Free Variables -------------- @@ -10,14 +39,3 @@ A : 1.293 [m²] Surface Area of the Tank V : 0.1 [m³] Volume of the Tank d : [ 0.464 0.464 0.464 ] [m] Dimension Vector -Variable Sensitivities ----------------------- - M : +0.67 Mass of Water in the Tank -\rho : -0.67 Density of Water in the Tank - -Most Sensitive Constraints --------------------------- - +1 : A ≥ 2·(d[0]·d[1] + d[0]·d[2] + d[1]·d[2]) - +0.67 : M = V·\rho - +0.67 : V = d[0]·d[1]·d[2] - diff --git a/docs/source/figures/solartest.py b/docs/source/figures/solartest.py new file mode 100644 index 00000000..b9fe33fe --- /dev/null +++ b/docs/source/figures/solartest.py @@ -0,0 +1,6 @@ +from solar.solar import * +Vehicle = Aircraft(Npod=3, sp=True) +M = Mission(Vehicle, latitude=[20]) +M.cost = M[M.aircraft.Wtotal] + +M.localsolve().save("solar.p") diff --git a/docs/source/modelbuilding.rst b/docs/source/modelbuilding.rst index 6f383b09..7b4fa487 100644 --- a/docs/source/modelbuilding.rst +++ b/docs/source/modelbuilding.rst @@ -36,6 +36,7 @@ These methods are illustrated in the following example. .. literalinclude:: examples/model_var_access.py .. literalinclude:: examples/model_var_access_output.txt + :language: breakdowns Vectorization ============= @@ -49,6 +50,7 @@ This allows models written with scalar constraints to be created with vector con .. literalinclude:: examples/vectorize.py .. literalinclude:: examples/vectorize_output.txt + :language: breakdowns @@ -70,3 +72,4 @@ The :ref:`sensitivity diagram ` which this code outputs shows how it is Note that the output table has been filtered above to show only variables of interest. .. literalinclude:: examples/performance_modeling_output.txt + :language: breakdowns diff --git a/docs/source/signomialprogramming.rst b/docs/source/signomialprogramming.rst index 61875dd8..2a2f8c2c 100644 --- a/docs/source/signomialprogramming.rst +++ b/docs/source/signomialprogramming.rst @@ -55,6 +55,7 @@ This problem is not GP compatible due to the :math:`sin(x)` constraint. One app .. literalinclude:: examples/sin_approx_example.py .. literalinclude:: examples/sin_approx_example_output.txt + :language: breakdowns Assume we have some external code which is capable of evaluating our incompatible function: @@ -69,6 +70,7 @@ and replace the incompatible constraint in our GP: .. literalinclude:: examples/external_sp.py .. literalinclude:: examples/external_sp_output.txt + :language: breakdowns which is the expected result. This method has been generalized to larger problems, such as calling XFOIL and AVL. diff --git a/docs/source/visint.rst b/docs/source/visint.rst index 1a917323..9d718125 100644 --- a/docs/source/visint.rst +++ b/docs/source/visint.rst @@ -3,6 +3,21 @@ Visualization and Interaction Code in this section uses the `CE solar model `_ except where noted otherwise. +Model and Variable Breakdowns +============================= + +Model breakdowns (similar to the sankey diagrams below) show the hierarchy of a model scaled by the sensitivity of its constraints and fixed variables. + +Variable breakdowns show how a variable "breaks down" into smaller expressions. +For example if the constraint ``x_total >= x1 + x2`` is tight (that is, has a sensitivity greater than zero, indicating that the right hand side is "pushing" against the left), then ``x_total`` can be said to "break down" into ``x1`` and ``x2``, each of which may have their own breakdowns. If multiple constraints break down a variable, the most sensitive one is chosen; if none do, than constraints such as ``1 >= x1/x_total + x2/x_total`` will be rearranged in an attempt to create a valid breakdown constraint like that above. + +.. literalinclude:: examples/breakdowns.py + +.. literalinclude:: examples/breakdowns_output.txt + :language: breakdowns + +If permissivity is greater than 1, the breakdown will always proceed if a breakdown variable is available in the monomial, and will choose the most sensitive one if multiple are available. If permissivity is 1, breakdowns will stop when there are multiple breakdown variables multiplying each other. If permissivity is 0, breakdowns will stop when any free variables multiply each other. If permissivity is between 0 and 1, it will follow the behavior for 1 if the monomial represents a fraction of the total greater than ``1 - permissivity``, and the behavior for 0 otherwise. + Model Hierarchy Treemaps ======================== diff --git a/gpkit/breakdowns.py b/gpkit/breakdowns.py new file mode 100644 index 00000000..044f662f --- /dev/null +++ b/gpkit/breakdowns.py @@ -0,0 +1,991 @@ +#TODO: cleanup weird conditionals +# add conversions to plotly/sankey + +# pylint: skip-file +import string +from collections import defaultdict, namedtuple, Counter +from gpkit.nomials import Monomial, Posynomial, Variable +from gpkit.nomials.map import NomialMap +from gpkit.small_scripts import mag, try_str_without +from gpkit.small_classes import FixedScalar, HashVector +from gpkit.exceptions import DimensionalityError +from gpkit.repr_conventions import unitstr as get_unitstr +from gpkit.repr_conventions import lineagestr +from gpkit.varkey import VarKey +import numpy as np + +Tree = namedtuple("Tree", ["key", "value", "branches"]) +Transform = namedtuple("Transform", ["factor", "power", "origkey"]) +def is_factor(key): + return (isinstance(key, Transform) and key.power == 1) +def is_power(key): + return (isinstance(key, Transform) and key.power != 1) + +def get_free_vks(posy, solution): + "Returns all free vks of a given posynomial for a given solution" + return set(vk for vk in posy.vks if vk not in solution["constants"]) + +def get_model_breakdown(solution): + breakdowns = {"|sensitivity|": 0} + for modelname, senss in solution["sensitivities"]["models"].items(): + senss = abs(senss) # for those monomial equalities + *namespace, name = modelname.split(".") + subbd = breakdowns + subbd["|sensitivity|"] += senss + for parent in namespace: + if parent not in subbd: + subbd[parent] = {parent: {}} + subbd = subbd[parent] + if "|sensitivity|" not in subbd: + subbd["|sensitivity|"] = 0 + subbd["|sensitivity|"] += senss + subbd[name] = {"|sensitivity|": senss} + # print(breakdowns["HyperloopSystem"]["|sensitivity|"]) + breakdowns = {"|sensitivity|": 0} + for constraint, senss in solution["sensitivities"]["constraints"].items(): + senss = abs(senss) # for those monomial + if senss <= 1e-5: + continue + subbd = breakdowns + subbd["|sensitivity|"] += senss + for parent in lineagestr(constraint).split("."): + if parent == "": + continue + if parent not in subbd: + subbd[parent] = {} + subbd = subbd[parent] + if "|sensitivity|" not in subbd: + subbd["|sensitivity|"] = 0 + subbd["|sensitivity|"] += senss + # treat vectors as namespace + constrstr = try_str_without(constraint, {"units", ":MAGIC:"+lineagestr(constraint)}) + if " at 0x" in constrstr: # don't print memory addresses + constrstr = constrstr[:constrstr.find(" at 0x")] + ">" + subbd[constrstr] = {"|sensitivity|": senss} + for vk in solution["sensitivities"]["variables"].keymap: # could this be done away with for backwards compatibility? + if not isinstance(vk, VarKey) or (vk.shape and not vk.index): + continue + senss = abs(solution["sensitivities"]["variables"][vk]) + if hasattr(senss, "shape"): + senss = np.nansum(senss) + if senss <= 1e-5: + continue + subbd = breakdowns + subbd["|sensitivity|"] += senss + for parent in vk.lineagestr().split("."): + if parent == "": + continue + if parent not in subbd: + subbd[parent] = {} + subbd = subbd[parent] + if "|sensitivity|" not in subbd: + subbd["|sensitivity|"] = 0 + subbd["|sensitivity|"] += senss + # treat vectors as namespace (indexing vectors above) + vk = vk.str_without({"lineage"}) + get_valstr(vk, solution, " = %s").replace(", fixed", "") + subbd[vk] = {"|sensitivity|": senss} + # TODO: track down in a live-solve environment why this isn't the same + # print(breakdowns["HyperloopSystem"]["|sensitivity|"]) + return breakdowns + +def crawl_modelbd(bd, lookup, name="Model"): + tree = Tree(name, bd.pop("|sensitivity|"), []) + if bd: + lookup[name] = tree + for subname, subtree in sorted(bd.items(), + key=lambda kv: (-float("%.2g" % kv[1]["|sensitivity|"]), kv[0])): + tree.branches.append(crawl_modelbd(subtree, lookup, subname)) + return tree + +def divide_out_vk(vk, pow, lt, gt): + hmap = NomialMap({HashVector({vk: 1}): 1.0}) + hmap.units = vk.units + var = Monomial(hmap)**pow + lt, gt = lt/var, gt/var + lt.ast = gt.ast = None + return lt, gt + +# @profile +def get_breakdowns(basically_fixed_variables, solution): + """Returns {key: (lt, gt, constraint)} for breakdown constrain in solution. + + A breakdown constraint is any whose "gt" contains a single free variable. + + (At present, monomial constraints check both sides as "gt") + """ + breakdowns = defaultdict(list) + beatout = defaultdict(set) + for constraint, senss in sorted(solution["sensitivities"]["constraints"].items(), key=lambda kv: (-abs(float("%.2g" % kv[1])), str(kv[0]))): + while getattr(constraint, "child", None): + constraint = constraint.child + while getattr(constraint, "generated", None): + constraint = constraint.generated + if abs(senss) <= 1e-5: # only tight-ish ones + continue + if constraint.oper == ">=": + gt, lt = (constraint.left, constraint.right) + elif constraint.oper == "<=": + lt, gt = (constraint.left, constraint.right) + elif constraint.oper == "=": + if senss > 0: # l_over_r is more sensitive - see nomials/math.py + lt, gt = (constraint.left, constraint.right) + else: # r_over_l is more sensitive - see nomials/math.py + gt, lt = (constraint.left, constraint.right) + for gtvk in gt.vks: # remove RelaxPCCP.C + if (gtvk.name == "C" and gtvk.lineage[0][0] == "RelaxPCCP" + and gtvk not in solution["freevariables"]): + lt, gt = lt.sub({gtvk: 1}), gt.sub({gtvk: 1}) + if len(gt.hmap) > 1: + continue + pos_gtvks = {vk for vk, pow in gt.exp.items() if pow > 0} + if len(pos_gtvks) > 1: + pos_gtvks &= get_free_vks(gt, solution) # remove constants + if len(pos_gtvks) == 1: + chosenvk, = pos_gtvks + while getattr(constraint, "parent", None): + constraint = constraint.parent + while getattr(constraint, "generated_by", None): + constraint = constraint.generated_by + breakdowns[chosenvk].append((lt, gt, constraint)) + for constraint, senss in sorted(solution["sensitivities"]["constraints"].items(), key=lambda kv: (-abs(float("%.2g" % kv[1])), str(kv[0]))): + if abs(senss) <= 1e-5: # only tight-ish ones + continue + while getattr(constraint, "child", None): + constraint = constraint.child + while getattr(constraint, "generated", None): + constraint = constraint.generated + if constraint.oper == ">=": + gt, lt = (constraint.left, constraint.right) + elif constraint.oper == "<=": + lt, gt = (constraint.left, constraint.right) + elif constraint.oper == "=": + if senss > 0: # l_over_r is more sensitive - see nomials/math.py + lt, gt = (constraint.left, constraint.right) + else: # r_over_l is more sensitive - see nomials/math.py + gt, lt = (constraint.left, constraint.right) + for gtvk in gt.vks: + if (gtvk.name == "C" and gtvk.lineage[0][0] == "RelaxPCCP" + and gtvk not in solution["freevariables"]): + lt, gt = lt.sub({gtvk: 1}), gt.sub({gtvk: 1}) + if len(gt.hmap) > 1: + continue + pos_gtvks = {vk for vk, pow in gt.exp.items() if pow > 0} + if len(pos_gtvks) > 1: + pos_gtvks &= get_free_vks(gt, solution) # remove constants + if len(pos_gtvks) != 1: # we'll choose our favorite vk + for vk, pow in gt.exp.items(): + if pow < 0: # remove all non-positive + lt, gt = divide_out_vk(vk, pow, lt, gt) + # bring over common factors from lt + lt_pows = defaultdict(set) + for exp in lt.hmap: + for vk, pow in exp.items(): + lt_pows[vk].add(pow) + for vk, pows in lt_pows.items(): + if len(pows) == 1: + pow, = pows + if pow < 0: # ...but only if they're positive + lt, gt = divide_out_vk(vk, pow, lt, gt) + # don't choose something that's already been broken down + candidatevks = {vk for vk in gt.vks if vk not in breakdowns} + if candidatevks: + vrisk = solution["sensitivities"]["variablerisk"] + chosenvk, *_ = sorted( + candidatevks, + key=lambda vk: (-float("%.2g" % (gt.exp[vk]*vrisk.get(vk, 0))), str(vk)) + ) + for vk, pow in gt.exp.items(): + if vk is not chosenvk: + lt, gt = divide_out_vk(vk, pow, lt, gt) + while getattr(constraint, "parent", None): + constraint = constraint.parent + while getattr(constraint, "generated_by", None): + constraint = constraint.generated_by + breakdowns[chosenvk].append((lt, gt, constraint)) + breakdowns = dict(breakdowns) # remove the defaultdict-ness + + prevlen = None + while len(basically_fixed_variables) != prevlen: + prevlen = len(basically_fixed_variables) + for key in breakdowns: + if key not in basically_fixed_variables: + get_fixity(basically_fixed_variables, key, breakdowns, solution) + return breakdowns + + +def get_fixity(basically_fixed, key, bd, solution, visited=set()): + lt, gt, _ = bd[key][0] + free_vks = get_free_vks(lt, solution).union(get_free_vks(gt, solution)) + for vk in free_vks: + if vk is key or vk in basically_fixed: + continue # currently checking or already checked + if vk not in bd: + return # a very free variable, can't even be broken down + if vk in visited: + return # tried it before, implicitly it didn't work out + # maybe it's basically fixed? + visited.add(key) + get_fixity(basically_fixed, vk, bd, solution, visited) + if vk not in basically_fixed: + return # ...well, we tried + basically_fixed.add(key) + +# @profile # ~84% of total last check # TODO: remove +def crawl(basically_fixed_variables, key, bd, solution, basescale=1, permissivity=2, verbosity=0, + visited_bdkeys=None, gone_negative=False, all_visited_bdkeys=None): + "Returns the tree of breakdowns of key in bd, sorting by solution's values" + if key != solution["cost function"] and hasattr(key, "key"): + key = key.key # clear up Variables + if key in bd: + # TODO: do multiple if sensitivities are quite close? + composition, keymon, constraint = bd[key][0] + elif isinstance(key, Posynomial): + composition = key + keymon = None + else: + raise TypeError("the `key` argument must be a VarKey or Posynomial.") + + if visited_bdkeys is None: + visited_bdkeys = set() + all_visited_bdkeys = set() + if verbosity == 1: + already_set = False #not solution._lineageset TODO + if not already_set: + solution.set_necessarylineage() + if verbosity: + indent = verbosity-1 # HACK: a bit of overloading, here + kvstr = "%s (%s)" % (key, get_valstr(key, solution)) + if key in all_visited_bdkeys: + print(" "*indent + kvstr + " [as broken down above]") + verbosity = 0 + else: + print(" "*indent + kvstr) + indent += 1 + orig_subtree = subtree = [] + tree = Tree(key, basescale, subtree) + visited_bdkeys.add(key) + all_visited_bdkeys.add(key) + if keymon is None: + scale = solution(key)/basescale + else: + if verbosity: + print(" "*indent + "which in: " + + constraint.str_without(["units", "lineage"]) + + " (sensitivity %+.2g)" % solution["sensitivities"]["constraints"][constraint]) + interesting_vks = {key} + subkey, = interesting_vks + power = keymon.exp[subkey] + boring_vks = set(keymon.vks) - interesting_vks + scale = solution(key)**power/basescale + # TODO: make method that can handle both kinds of transforms + if (power != 1 or boring_vks or mag(keymon.c) != 1 + or keymon.units != key.units): # some kind of transform here + units = 1 + exp = HashVector() + for vk in interesting_vks: + exp[vk] = keymon.exp[vk] + if vk.units: + units *= vk.units**keymon.exp[vk] + subhmap = NomialMap({exp: 1}) + try: + subhmap.units = None if units == 1 else units + except DimensionalityError: + # pints was unable to divide a unit by itself bc + # it has terrible floating-point errors. + # so let's assume it isn't dimensionless + # even though it probably is + subhmap.units = units + freemon = Monomial(subhmap) + factor = Monomial(keymon/freemon) + scale = scale * solution(factor) + if factor != 1: + factor = factor**(-1/power) # invert the transform + factor.ast = None + if verbosity: + print(" "*indent + "{ through a factor of %s (%s) }" % + (factor.str_without(["units"]), + get_valstr(factor, solution))) + subsubtree = [] + transform = Transform(factor, 1, keymon) + orig_subtree.append(Tree(transform, basescale, subsubtree)) + orig_subtree = subsubtree + if power != 1: + if verbosity: + print(" "*indent + "{ through a power of %.2g }" % power) + subsubtree = [] + transform = Transform(1, 1/power, keymon) # inverted bc it's on the gt side + orig_subtree.append(Tree(transform, basescale, subsubtree)) + orig_subtree = subsubtree + + # TODO: use ast_parsing instead of chop? + mons = composition.chop() + monsols = [solution(mon) for mon in mons] # ~20% of total last check # TODO: remove + parsed_monsols = [getattr(mon, "value", mon) for mon in monsols] + monvals = [float(mon/scale) for mon in parsed_monsols] # ~10% of total last check # TODO: remove + # sort by value, preserving order in case of value tie + sortedmonvals = sorted(zip([-float("%.2g" % mv) for mv in monvals], range(len(mons)), monvals, mons)) + # print([m.str_without({"units", "lineage"}) for m in mons]) + if verbosity: + if len(monsols) == 1: + print(" "*indent + "breaks down into:") + else: + print(" "*indent + "breaks down into %i monomials:" % len(monsols)) + indent += 1 + indent += 1 + for i, (_, _, scaledmonval, mon) in enumerate(sortedmonvals): + if not scaledmonval: + continue + subtree = orig_subtree # return to the original subtree + # time for some filtering + interesting_vks = mon.vks + potential_filters = [ + {vk for vk in interesting_vks if vk not in bd}, + mon.vks - get_free_vks(mon, solution), + {vk for vk in interesting_vks if vk in basically_fixed_variables} + ] + if scaledmonval < 1 - permissivity: # skip breakdown filter + potential_filters = potential_filters[1:] + potential_filters.insert(0, visited_bdkeys) + for filter in potential_filters: + if interesting_vks - filter: # don't remove the last one + interesting_vks = interesting_vks - filter + # if filters weren't enough and permissivity is high enough, sort! + if len(interesting_vks) > 1 and permissivity > 1: + csenss = solution["sensitivities"]["constraints"] + best_vks = sorted((vk for vk in interesting_vks if vk in bd), + key=lambda vk: (-abs(float("%.2g" % (mon.exp[vk]*csenss[bd[vk][0][2]]))), + -float("%.2g" % solution["variables"][vk]), + str(bd[vk][0][0]))) # ~5% of total last check # TODO: remove + # TODO: changing to str(vk) above does some odd stuff, why? + if best_vks: + interesting_vks = set([best_vks[0]]) + boring_vks = mon.vks - interesting_vks + + subkey = None + if len(interesting_vks) == 1: + subkey, = interesting_vks + if subkey in visited_bdkeys and len(sortedmonvals) == 1: + continue # don't even go there + if subkey not in bd: + power = 1 # no need for a transform + else: + power = mon.exp[subkey] + if power < 0 and gone_negative: + subkey = None # don't breakdown another negative + + if len(monsols) > 1 and verbosity: + indent -= 1 + print(" "*indent + "%s) forming %i%% of the RHS and %i%% of the total:" % (i+1, scaledmonval/basescale*100, scaledmonval*100)) + indent += 1 + + if subkey is None: + power = 1 + if scaledmonval > 1 - permissivity and not boring_vks: + boring_vks = interesting_vks + interesting_vks = set() + if not interesting_vks: + # prioritize showing some boring_vks as if they were "free" + if len(boring_vks) == 1: + interesting_vks = boring_vks + boring_vks = set() + else: + for vk in list(boring_vks): + if vk.units and not vk.units.dimensionless: + interesting_vks.add(vk) + boring_vks.remove(vk) + + if interesting_vks and (boring_vks or mag(mon.c) != 1): + units = 1 + exp = HashVector() + for vk in interesting_vks: + exp[vk] = mon.exp[vk] + if vk.units: + units *= vk.units**mon.exp[vk] + subhmap = NomialMap({exp: 1}) + subhmap.units = None if units is 1 else units + freemon = Monomial(subhmap) + factor = mon/freemon # autoconvert... + if (factor.units is None and isinstance(factor, FixedScalar) + and abs(factor.value - 1) <= 1e-4): + factor = 1 # minor fudge to clear numerical inaccuracies + if factor != 1 : + factor.ast = None + if verbosity: + keyvalstr = "%s (%s)" % (factor.str_without(["units"]), + get_valstr(factor, solution)) + print(" "*indent + "{ through a factor of %s }" % keyvalstr) + subsubtree = [] + transform = Transform(factor, 1, mon) + subtree.append(Tree(transform, scaledmonval, subsubtree)) + subtree = subsubtree + mon = freemon # simplifies units + if power != 1: + if verbosity: + print(" "*indent + "{ through a power of %.2g }" % power) + subsubtree = [] + transform = Transform(1, power, mon) + subtree.append(Tree(transform, scaledmonval, subsubtree)) + subtree = subsubtree + mon = mon**(1/power) + mon.ast = None + # TODO: make minscale an argument - currently an arbitrary 0.01 + if (subkey is not None and subkey not in visited_bdkeys + and subkey in bd and scaledmonval > 0.05): + subverbosity = indent + 1 if verbosity else 0 # slight hack + subsubtree = crawl(basically_fixed_variables, subkey, bd, solution, scaledmonval, + permissivity, subverbosity, set(visited_bdkeys), + gone_negative, all_visited_bdkeys) + subtree.append(subsubtree) + else: + if verbosity: + keyvalstr = "%s (%s)" % (mon.str_without(["units"]), + get_valstr(mon, solution)) + print(" "*indent + keyvalstr) + subtree.append(Tree(mon, scaledmonval, [])) + if verbosity == 1: + if not already_set: + solution.set_necessarylineage(clear=True) + return tree + +SYMBOLS = string.ascii_uppercase + string.ascii_lowercase +for ambiguous_symbol in "lILT": + SYMBOLS = SYMBOLS.replace(ambiguous_symbol, "") + +def get_spanstr(legend, length, label, leftwards, solution): + "Returns span visualization, collapsing labels to symbols" + if label is None: + return " "*length + spacer, lend, rend = "│", "┯", "┷" + if isinstance(label, Transform): + spacer, lend, rend = "╎", "╤", "╧" + if label.power != 1: + spacer = " " + lend = rend = "^" if label.power > 0 else "/" + # remove origkeys so they collide in the legends dictionary + label = Transform(label.factor, label.power, None) + if label.power == 1 and len(str(label.factor)) == 1: + legend[label] = str(label.factor) + + if label not in legend: + legend[label] = SYMBOLS[len(legend)] + shortname = legend[label] + + if length <= 1: + return shortname + shortside = int(max(0, length - 2)/2) + longside = int(max(0, length - 3)/2) + if leftwards: + if length == 2: + return lend + shortname + return lend + spacer*shortside + shortname + spacer*longside + rend + else: + if length == 2: + return shortname + rend + # HACK: no corners on long rightwards - only used for depth 0 + return "┃"*(longside+1) + shortname + "┃"*(shortside+1) + +def discretize(tree, extent, solution, collapse, depth=0, justsplit=False): + # TODO: add vertical simplification? + key, val, branches = tree + if collapse: # collapse Transforms with power 1 + while any(isinstance(branch.key, Transform) and branch.key.power > 0 for branch in branches): + newbranches = [] + for branch in branches: + # isinstance(branch.key, Transform) and branch.key.power > 0 + if isinstance(branch.key, Transform) and branch.key.power > 0: + newbranches.extend(branch.branches) + else: + newbranches.append(branch) + branches = newbranches + + scale = extent/val + values = [b.value for b in branches] + bkey_indexs = {} + for i, b in enumerate(branches): + k = get_keystr(b.key, solution) + if isinstance(b.key, Transform): + if len(b.branches) == 1: + k = get_keystr(b.branches[0].key, solution) + if k in bkey_indexs: + values[bkey_indexs[k]] += values[i] + values[i] = None + else: + bkey_indexs[k] = i + if any(v is None for v in values): + bvs = zip(*sorted(((-float("%.2g" % v), i, b, v) for i, (b, v) in enumerate(zip(branches, values)) if v is not None))) + _, _, branches, values = bvs + branches = list(branches) + values = list(values) + extents = [int(round(scale*v)) for v in values] + surplus = extent - sum(extents) + for i, b in enumerate(branches): + if isinstance(b.key, Transform): + subscale = extents[i]/b.value + if not any(round(subscale*subv) for _, subv, _ in b.branches): + extents[i] = 0 # transform with no worthy heirs: misc it + if not any(extents): + return Tree(key, extent, []) + if not all(extents): # create a catch-all + branches = branches.copy() + miscvkeys, miscval = [], 0 + for subextent in reversed(extents): + if not subextent or (branches[-1].value < miscval and surplus < 0): + extents.pop() + k, v, _ = branches.pop() + if isinstance(k, Transform): + k = k.origkey # TODO: this is the only use of origkey - remove it + if isinstance(k, tuple): + vkeys = [(-kv[1], str(kv[0]), kv[0]) for kv in k] + if not isinstance(k, tuple): + vkeys = [(-float("%.2g" % v), str(k), k)] + miscvkeys += vkeys + surplus -= (round(scale*(miscval + v)) + - round(scale*miscval) - subextent) + miscval += v + misckeys = tuple(k for _, _, k in sorted(miscvkeys)) + branches.append(Tree(misckeys, miscval, [])) + extents.append(int(round(scale*miscval))) + if surplus: + sign = int(np.sign(surplus)) + bump_priority = sorted((ext, sign*float("%.2g" % b.value), i) for i, (b, ext) + in enumerate(zip(branches, extents))) + # print(key, surplus, bump_priority) + while surplus: + try: + extents[bump_priority.pop()[-1]] += sign + surplus -= sign + except IndexError: + raise ValueError(val, [b.value for b in branches]) + + tree = Tree(key, extent, []) + # simplify based on how we're branching + branchfactor = len([ext for ext in extents if ext]) - 1 + if depth and not isinstance(key, Transform): + if extent == 1 or branchfactor >= max(extent-2, 1): + # if we'd branch too much, stop + return tree + if collapse and not branchfactor and not justsplit: + # if we didn't just split and aren't about to, skip through + return discretize(branches[0], extent, solution, collapse, + depth=depth+1, justsplit=False) + if branchfactor: + justsplit = True + elif not isinstance(key, Transform): # justsplit passes through transforms + justsplit = False + + for branch, subextent in zip(branches, extents): + if subextent: + branch = discretize(branch, subextent, solution, collapse, + depth=depth+1, justsplit=justsplit) + if (collapse and is_power(branch.key) + and all(is_power(b.key) for b in branch.branches)): + # combine stacked powers + power = branch.key.power + for b in branch.branches: + key = Transform(1, power*b.key.power, None) + if key.power == 1: # powers canceled, collapse both + tree.branches.extend(b.branches) + else: # collapse this level + tree.branches.append(Tree(key, b.value, b.branches)) + else: + tree.branches.append(branch) + return tree + +def layer(map, tree, maxdepth, depth=0): + "Turns the tree into a 2D-array" + key, extent, branches = tree + if depth <= maxdepth: + if len(map) <= depth: + map.append([]) + map[depth].append((key, extent)) + if not branches: + branches = [Tree(None, extent, [])] # pad it out + for branch in branches: + layer(map, branch, maxdepth, depth+1) + return map + +def plumb(tree, depth=0): + "Finds maximum depth of a tree" + maxdepth = depth + for branch in tree.branches: + maxdepth = max(maxdepth, plumb(branch, depth+1)) + return maxdepth + +def prune(tree, solution, maxlength, length=-1, prefix=""): + "Prune branches that are longer than a certain number of characters" + key, extent, branches = tree + keylength = max(len(get_valstr(key, solution, into="(%s)")), + len(get_keystr(key, solution, prefix))) + if length == -1 and isinstance(key, VarKey) and key.necessarylineage: + prefix = key.lineagestr() + length += keylength + 3 + for branch in branches: + keylength = max(len(get_valstr(branch.key, solution, into="(%s)")), + len(get_keystr(branch.key, solution, prefix))) + branchlength = length + keylength + 3 + if branchlength > maxlength: + return Tree(key, extent, []) + return Tree(key, extent, [prune(b, solution, maxlength, length, prefix) + for b in branches]) + +def simplify(tree, solution, extent, maxdepth, maxlength, collapse): + "Discretize, prune, and layer a tree to prepare for printing" + subtree = discretize(tree, extent, solution, collapse) + if collapse and maxlength: + subtree = prune(subtree, solution, maxlength) + return layer([], subtree, maxdepth) + +# @profile # ~16% of total last check # TODO: remove +def graph(tree, breakdowns, solution, basically_fixed_variables, *, + height=None, maxdepth=None, maxwidth=81, showlegend=False): + "Prints breakdown" + already_set = solution._lineageset + if not already_set: + solution.set_necessarylineage() + collapse = (not showlegend) # TODO: set to True while showlegend is True for first approx of receipts; autoinclude with trace? + if maxdepth is None: + maxdepth = plumb(tree) + if height is not None: + mt = simplify(tree, solution, height, maxdepth, maxwidth, collapse) + else: # zoom in from a default height of 20 to a height of 4 per branch + prev_height = None + height = 20 + while prev_height != height: + mt = simplify(tree, solution, height, maxdepth, maxwidth, collapse) + prev_height = height + height = min(height, max(*(4*len(at_depth) for at_depth in mt))) + + legend = {} + chararray = np.full((len(mt), height), "", "object") + for depth, elements_at_depth in enumerate(mt): + row = "" + for i, (element, length) in enumerate(elements_at_depth): + leftwards = depth > 0 and length > 2 + row += get_spanstr(legend, length, element, leftwards, solution) + chararray[depth, :] = list(row) + + # Format depth=0 + A_key, = [key for key, value in legend.items() if value == "A"] + prefix = "" + if A_key is solution["cost function"]: + A_str = "Cost" + else: + A_str = get_keystr(A_key, solution) + if isinstance(A_key, VarKey) and A_key.necessarylineage: + prefix = A_key.lineagestr() + A_valstr = get_valstr(A_key, solution, into="(%s)") + fmt = "{0:>%s}" % (max(len(A_str), len(A_valstr)) + 3) + for j, entry in enumerate(chararray[0,:]): + if entry == "A": + chararray[0,j] = fmt.format(A_str + "╺┫") + chararray[0,j+1] = fmt.format(A_valstr + " ┃") + else: + chararray[0,j] = fmt.format(entry) + # Format depths 1+ + labeled = set() + reverse_legend = {v: k for k, v in legend.items()} + legend = {} + for pos in range(height): + for depth in reversed(range(1,len(mt))): + char = chararray[depth, pos] + if char not in reverse_legend: + continue + key = reverse_legend[char] + if key not in legend and (isinstance(key, tuple) or (depth != len(mt) - 1 and chararray[depth+1, pos] != " ")): + legend[key] = SYMBOLS[len(legend)] + if collapse and is_power(key): + chararray[depth, pos] = "^" if key.power > 0 else "/" + del legend[key] + continue + if key in legend: + chararray[depth, pos] = legend[key] + if isinstance(key, tuple) and not isinstance(key, Transform): + chararray[depth, pos] = "*" + chararray[depth, pos] + del legend[key] + if showlegend: + continue + + keystr = get_keystr(key, solution, prefix) + if keystr in labeled: + valuestr = "" + else: + valuestr = get_valstr(key, solution, into=" (%s)") + if collapse: + fmt = "{0:<%s}" % max(len(keystr) + 3, len(valuestr) + 2) + else: + fmt = "{0:<1}" + span = 0 + tryup, trydn = True, True + while tryup or trydn: + span += 1 + if tryup: + if pos - span < 0: + tryup = False + else: + upchar = chararray[depth, pos-span] + if upchar == "│": + chararray[depth, pos-span] = fmt.format("┃") + elif upchar == "┯": + chararray[depth, pos-span] = fmt.format("┓") + else: + tryup = False + if trydn: + if pos + span >= height: + trydn = False + else: + dnchar = chararray[depth, pos+span] + if dnchar == "│": + chararray[depth, pos+span] = fmt.format("┃") + elif dnchar == "┷": + chararray[depth, pos+span] = fmt.format("┛") + else: + trydn = False + linkstr = "┣╸" + if not isinstance(key, FixedScalar): + labeled.add(keystr) + if span > 1 and (collapse or pos + 2 >= height + or chararray[depth, pos+1] == "┃"): + vallabel = chararray[depth, pos+1].rstrip() + valuestr + chararray[depth, pos+1] = fmt.format(vallabel) + elif showlegend: + keystr += valuestr + if key in breakdowns and not chararray[depth+1, pos].strip(): + keystr = keystr + "╶⎨" + chararray[depth, pos] = fmt.format(linkstr + keystr) + # Rotate and print + rowstrs = ["".join(row).rstrip() for row in chararray.T.tolist()] + print("\n" + "\n".join(rowstrs) + "\n") + + if showlegend: # create and print legend + legend_lines = [] + for key, shortname in sorted(legend.items(), key=lambda kv: kv[1]): + legend_lines.append(legend_entry(key, shortname, solution, prefix, + basically_fixed_variables)) + maxlens = [max(len(el) for el in col) for col in zip(*legend_lines)] + fmts = ["{0:<%s}" % L for L in maxlens] + for line in legend_lines: + line = "".join(fmt.format(cell) + for fmt, cell in zip(fmts, line) if cell).rstrip() + print(" " + line) + + if not already_set: + solution.set_necessarylineage(clear=True) + +def legend_entry(key, shortname, solution, prefix, basically_fixed_variables): + "Returns list of legend elements" + operator = note = "" + keystr = valuestr = " " + operator = "= " if shortname else " + " + if is_factor(key): + operator = " ×" + key = key.factor + free, quasifixed = False, False + if any(vk not in basically_fixed_variables + for vk in get_free_vks(key, solution)): + note = " [free factor]" + if is_power(key): + valuestr = " ^%.3g" % key.power + else: + valuestr = get_valstr(key, solution, into=" "+operator+"%s") + if not isinstance(key, FixedScalar): + keystr = get_keystr(key, solution, prefix) + return ["%-4s" % shortname, keystr, valuestr, note] + +def get_keystr(key, solution, prefix=""): + "Returns formatted string of the key in solution." + if hasattr(key, "str_without"): + out = key.str_without({"units", ":MAGIC:"+prefix}) + elif isinstance(key, tuple): + out = "[%i terms]" % len(key) + else: + out = str(key) + return out if len(out) <= 67 else out[:66]+"…" + +def get_valstr(key, solution, into="%s"): + "Returns formatted string of the value of key in solution." + # get valuestr + try: + value = solution(key) + except (ValueError, TypeError): + try: + value = sum(solution(subkey) for subkey in key) + except (ValueError, TypeError): + return " " + if isinstance(value, FixedScalar): + value = value.value + if 1e3 <= mag(value) < 1e6: + valuestr = "{:,.0f}".format(mag(value)) + else: + valuestr = "%-.3g" % mag(value) + # get unitstr + if hasattr(key, "unitstr"): + unitstr = key.unitstr() + else: + try: + if hasattr(value, "units"): + value.ito_reduced_units() + except DimensionalityError: + pass + unitstr = get_unitstr(value) + if unitstr[:2] == "1/": + unitstr = "/" + unitstr[2:] + if key in solution["constants"] or ( + hasattr(key, "vks") and key.vks + and all(vk in solution["constants"] for vk in key.vks)): + unitstr += ", fixed" + return into % (valuestr + unitstr) + + +import plotly.graph_objects as go +def plotlyify(tree, solution, minval=None): + """Plots model structure as Plotly TreeMap + + Arguments + --------- + model: Model + GPkit model object + + itemize (optional): string, either "variables" or "constraints" + Specify whether to iterate over the model varkeys or constraints + + sizebycount (optional): bool + Whether to size blocks by number of variables/constraints or use + default sizing + + Returns + ------- + plotly.graph_objects.Figure + Plot of model hierarchy + + """ + ids = [] + labels = [] + parents = [] + values = [] + + key, value, branches = tree + if isinstance(key, VarKey) and key.necessarylineage: + prefix = key.lineagestr() + else: + prefix = "" + + if minval is None: + minval = value/1000 + + parent_budgets = {} + + def crawl(tree, parent_id=None): + key, value, branches = tree + if value > minval: + if isinstance(key, Transform): + id = parent_id + else: + id = len(ids)+1 + ids.append(id) + labels.append(get_keystr(key, solution, prefix)) + if not isinstance(key, str): + labels[-1] = labels[-1] + "
" + get_valstr(key, solution) + parents.append(parent_id) + parent_budgets[id] = value + if parent_id is not None: # make sure there's no overflow + if parent_budgets[parent_id] < value: + value = parent_budgets[parent_id] # take remained + parent_budgets[parent_id] -= value + values.append(value) + for branch in branches: + crawl(branch, id) + + crawl(tree) + return ids, labels, parents, values + +def treemap(ids, labels, parents, values): + return go.Figure(go.Treemap( + ids=ids, + labels=labels, + parents=parents, + values=values, + branchvalues="total" + )) + +def icicle(ids, labels, parents, values): + return go.Figure(go.Icicle( + ids=ids, + labels=labels, + parents=parents, + values=values, + branchvalues="total" + )) + + +import functools + +class Breakdowns(object): + def __init__(self, sol): + self.sol = sol + self.mlookup = {} + self.mtree = crawl_modelbd(get_model_breakdown(sol), self.mlookup) + self.basically_fixed_variables = set() + self.bd = get_breakdowns(self.basically_fixed_variables, self.sol) + + def trace(self, key, *, permissivity=2): + print("") # a little padding to start + self.get_tree(key, permissivity=permissivity, verbosity=1) + + def get_tree(self, key, *, permissivity=2, verbosity=0): + tree = None + kind = "variable" + if isinstance(key, str): + if key == "model sensitivities": + tree = self.mtree + kind = "constraint" + elif key == "cost": + key = self.sol["cost function"] + elif key in self.mlookup: + tree = self.mlookup[key] + kind = "constraint" + else: + # TODO: support submodels + keys = [vk for vk in self.bd if key in str(vk)] + if not keys: + raise KeyError(key) + elif len(keys) > 1: + raise KeyError("There are %i keys containing '%s'." % (len(keys), key)) + key, = keys + if tree is None: + tree = crawl(self.basically_fixed_variables, key, self.bd, self.sol, + permissivity=permissivity, verbosity=verbosity) + return tree, kind + + def plot(self, key, *, height=None, permissivity=2, showlegend=False, + maxwidth=85): + tree, kind = self.get_tree(key, permissivity=permissivity) + lookup = self.bd if kind == "variable" else self.mlookup + graph(tree, lookup, self.sol, self.basically_fixed_variables, + height=height, showlegend=showlegend, maxwidth=maxwidth) + + def treemap(self, key, *, permissivity=2, returnfig=False, filename=None): + tree, _ = self.get_tree(key) + fig = treemap(*plotlyify(tree, self.sol)) + if returnfig: + return fig + if filename is None: + filename = str(key)+"_treemap.html" + keepcharacters = (".","_") + filename = "".join(c for c in filename if c.isalnum() + or c in keepcharacters).rstrip() + import plotly + plotly.offline.plot(fig, filename=filename) + + + def icicle(self, key, *, permissivity=2, returnfig=False, filename=None): + tree, _ = self.get_tree(key, permissivity=permissivity) + fig = icicle(*plotlyify(tree, self.sol)) + if returnfig: + return fig + if filename is None: + filename = str(key)+"_icicle.html" + keepcharacters = (".","_") + filename = "".join(c for c in filename if c.isalnum() + or c in keepcharacters).rstrip() + import plotly + plotly.offline.plot(fig, filename=filename) diff --git a/gpkit/constraints/gp.py b/gpkit/constraints/gp.py index a63865f7..4736d775 100644 --- a/gpkit/constraints/gp.py +++ b/gpkit/constraints/gp.py @@ -4,6 +4,7 @@ from time import time from collections import defaultdict import numpy as np +from ..nomials.map import NomialMap from ..repr_conventions import lineagestr from ..small_classes import CootMatrix, SolverLog, Numbers, FixedScalar from ..small_scripts import appendsolwarning, initsolwarning @@ -213,7 +214,7 @@ def solve(self, solver=None, *, verbosity=1, gen_result=True, **kwargs): except Exception as e: raise UnknownInfeasible("Something unexpected went wrong.") from e finally: - self.solve_log = "\n".join(sys.stdout) + self.solve_log = sys.stdout sys.stdout = original_stdout self.solver_out = solver_out @@ -307,7 +308,8 @@ def _generate_nula(self, solver_out): return la, nu_by_posy def _compile_result(self, solver_out): - result = {"cost": float(solver_out["objective"])} + result = {"cost": float(solver_out["objective"]), + "cost function": self.cost} primal = solver_out["primal"] if len(self.varlocs) != len(primal): raise RuntimeWarning("The primal solution was not returned.") @@ -334,7 +336,7 @@ def _compile_result(self, solver_out): " %s, but since the '%s' solver doesn't support discretization" " they were treated as continuous variables." % (sorted(self.choicevaridxs.keys()), solver_out["solver"]), - self.choicevaridxs)]} + self.choicevaridxs)]} # TODO: choicevaridxs seems unnecessary result["sensitivities"] = {"constraints": {}} la, self.nu_by_posy = self._generate_nula(solver_out) @@ -342,36 +344,45 @@ def _compile_result(self, solver_out): self.cost.hmap)) gpv_ss = cost_senss.copy() m_senss = defaultdict(float) + absv_ss = {vk: abs(x) for vk, x in cost_senss.items()} for las, nus, c in zip(la[1:], self.nu_by_posy[1:], self.hmaps[1:]): while getattr(c, "parent", None) is not None: - c = c.parent + if not isinstance(c, NomialMap): + c.parent.child = c + c = c.parent # parents get their sens_from_dual used... v_ss, c_senss = c.sens_from_dual(las, nus, result) for vk, x in v_ss.items(): gpv_ss[vk] = x + gpv_ss.get(vk, 0) + absv_ss[vk] = abs(x) + absv_ss.get(vk, 0) while getattr(c, "generated_by", None): - c = c.generated_by - result["sensitivities"]["constraints"][c] = abs(c_senss) + c.generated_by.generated = c + c = c.generated_by # ...while generated_bys are just labels + result["sensitivities"]["constraints"][c] = c_senss m_senss[lineagestr(c)] += abs(c_senss) - # add fixed variables sensitivities to models - for vk, senss in gpv_ss.items(): - m_senss[lineagestr(vk)] += abs(senss) result["sensitivities"]["models"] = dict(m_senss) # carry linked sensitivities over to their constants for v in list(v for v in gpv_ss if v.gradients): dlogcost_dlogv = gpv_ss.pop(v) + dlogcost_dlogabsv = absv_ss.pop(v) val = np.array(result["constants"][v]) for c, dv_dc in v.gradients.items(): with pywarnings.catch_warnings(): # skip pesky divide-by-zeros pywarnings.simplefilter("ignore") dlogv_dlogc = dv_dc * result["constants"][c]/val gpv_ss[c] = gpv_ss.get(c, 0) + dlogcost_dlogv*dlogv_dlogc + absv_ss[c] = (absv_ss.get(c, 0) + + abs(dlogcost_dlogabsv*dlogv_dlogc)) if v in cost_senss: - if c in self.cost.vks: + if c in self.cost.vks: # TODO: seems unnecessary dlogcost_dlogv = cost_senss.pop(v) before = cost_senss.get(c, 0) cost_senss[c] = before + dlogcost_dlogv*dlogv_dlogc + # add fixed variables sensitivities to models + for vk, senss in gpv_ss.items(): + m_senss[lineagestr(vk)] += abs(senss) result["sensitivities"]["cost"] = cost_senss result["sensitivities"]["variables"] = KeyDict(gpv_ss) + result["sensitivities"]["variablerisk"] = KeyDict(absv_ss) result["sensitivities"]["constants"] = \ result["sensitivities"]["variables"] # NOTE: backwards compat. return SolutionArray(result) diff --git a/gpkit/constraints/set.py b/gpkit/constraints/set.py index dc3dcf8f..b0ec926e 100644 --- a/gpkit/constraints/set.py +++ b/gpkit/constraints/set.py @@ -60,11 +60,12 @@ def flatiter(iterable, yield_if_hasattr=None): yield from flatiter(constraint, yield_if_hasattr) -class ConstraintSet(list, ReprMixin): +class ConstraintSet(list, ReprMixin): # pylint: disable=too-many-instance-attributes "Recursive container for ConstraintSets and Inequalities" unique_varkeys, idxlookup = frozenset(), {} _name_collision_varkeys = None _varkeys = None + _lineageset = False def __init__(self, constraints, substitutions=None, *, bonusvks=None): # pylint: disable=too-many-branches,too-many-statements if isinstance(constraints, dict): @@ -200,34 +201,67 @@ def __repr__(self): " and %i variable(s)>" % (self.__class__.__name__, len(self), len(self.varkeys))) - def name_collision_varkeys(self): + def set_necessarylineage(self, clear=False): # pylint: disable=too-many-branches "Returns the set of contained varkeys whose names are not unique" if self._name_collision_varkeys is None: - self._name_collision_varkeys = { - key for key in self.varkeys - if len(self.varkeys[key.str_without(["lineage", "vec"])]) > 1} - return self._name_collision_varkeys + self._name_collision_varkeys = {} + name_collisions = defaultdict(set) + for key in self.varkeys: + if hasattr(key, "key"): + if key.veckey and all(k.veckey == key.veckey + for k in self.varkeys[key.name]): + self._name_collision_varkeys[key] = 0 + self._name_collision_varkeys[key.veckey] = 0 + elif len(self.varkeys[key.name]) == 1: + self._name_collision_varkeys[key] = 0 + else: + shortname = key.str_without(["lineage", "vec"]) + if len(self.varkeys[shortname]) > 1: + name_collisions[shortname].add(key) + for varkeys in name_collisions.values(): + min_namespaced = defaultdict(set) + for vk in varkeys: + *_, mineage = vk.lineagestr().split(".") + min_namespaced[(mineage, 1)].add(vk) + while any(len(vks) > 1 for vks in min_namespaced.values()): + for key, vks in list(min_namespaced.items()): + if len(vks) <= 1: + continue + del min_namespaced[key] + mineage, idx = key + idx += 1 + for vk in vks: + lineages = vk.lineagestr().split(".") + submineage = lineages[-idx] + "." + mineage + min_namespaced[(submineage, idx)].add(vk) + for (_, idx), vks in min_namespaced.items(): + vk, = vks + self._name_collision_varkeys[vk] = idx + if clear: + self._lineageset = False + for vk in self._name_collision_varkeys: + del vk.descr["necessarylineage"] + else: + self._lineageset = True + for vk, idx in self._name_collision_varkeys.items(): + vk.descr["necessarylineage"] = idx def lines_without(self, excluded): "Lines representation of a ConstraintSet." excluded = frozenset(excluded) root, rootlines = "root" not in excluded, [] if root: - excluded = excluded.union(["root"]) - if "unnecessary lineage" in excluded: - for key in self.name_collision_varkeys(): - key.descr["necessarylineage"] = True + excluded = {"root"}.union(excluded) + self.set_necessarylineage() if hasattr(self, "_rootlines"): rootlines = self._rootlines(excluded) # pylint: disable=no-member lines = recursively_line(self, excluded) - indent = " " if getattr(self, "lineage", None) else "" - if root and "unnecessary lineage" in excluded: - indent += " " - for key in self.name_collision_varkeys(): - del key.descr["necessarylineage"] + indent = " " if root or getattr(self, "lineage", None) else "" + if root: + self.set_necessarylineage(clear=True) return rootlines + [(indent+line).rstrip() for line in lines] - def str_without(self, excluded=("unnecessary lineage", "units")): + def str_without(self, excluded=("units",)): "String representation of a ConstraintSet." return "\n".join(self.lines_without(excluded)) diff --git a/gpkit/constraints/sgp.py b/gpkit/constraints/sgp.py index ed3387b8..3068122d 100644 --- a/gpkit/constraints/sgp.py +++ b/gpkit/constraints/sgp.py @@ -207,8 +207,13 @@ def localsolve(self, solver=None, *, verbosity=1, x0=None, reltol=1e-4, del self.result["freevariables"][self.slack.key] # pylint: disable=no-member del self.result["variables"][self.slack.key] # pylint: disable=no-member del self.result["sensitivities"]["variables"][self.slack.key] # pylint: disable=no-member - slackconstraint = self.gpconstraints[0] - del self.result["sensitivities"]["constraints"][slackconstraint] + slcon = self.gpconstraints[0] + slconsenss = self.result["sensitivities"]["constraints"][slcon] + del self.result["sensitivities"]["constraints"][slcon] + # TODO: create constraint in RelaxPCCP namespace + self.result["sensitivities"]["models"][""] -= slconsenss + if not self.result["sensitivities"]["models"][""]: + del self.result["sensitivities"]["models"][""] return self.result @property @@ -233,6 +238,8 @@ def gp(self, x0=None, *, cleanx0=False): for sgpc in self.sgpconstraints: for hmaplt1 in sgpc.as_gpconstr(self._gp.x0).as_hmapslt1({}): approxc = self.approxconstraints[p_idx] + approxc.left = self.slack + approxc.right.hmap = hmaplt1 approxc.unsubbed = [Posynomial(hmaplt1)/self.slack] p_idx += 1 # p_idx=0 is the cost; sp constraints are after it hmap, = approxc.as_hmapslt1(self._gp.substitutions) diff --git a/gpkit/interactive/sankey.py b/gpkit/interactive/sankey.py index 24c7f286..a0303429 100644 --- a/gpkit/interactive/sankey.py +++ b/gpkit/interactive/sankey.py @@ -90,7 +90,7 @@ def linkfixed(self, cset, target): return total_sens # pylint: disable=too-many-branches - def link(self, cset, target, var, *, labeled=False, subarray=False): + def link(self, cset, target, vk, *, labeled=False, subarray=False): "adds links of a given constraint set to self.links" total_sens = 0 switchedtarget = False @@ -98,7 +98,7 @@ def link(self, cset, target, var, *, labeled=False, subarray=False): if cset is not self.cset: # top-level, no need to switch targets switchedtarget = target target = self.add_node(target, cset.lineage[-1][0]) - if var is None: + if vk is None: total_sens += self.linkfixed(cset, target) elif isinstance(cset, ArrayConstraint) and cset.constraints.size > 1: switchedtarget = target @@ -111,21 +111,21 @@ def link(self, cset, target, var, *, labeled=False, subarray=False): if isinstance(cset, dict): for label, c in cset.items(): source = self.add_node(target, label) - subtotal_sens = self.link(c, source, var, labeled=True) + subtotal_sens = self.link(c, source, vk, labeled=True) self.links[source, target] += subtotal_sens total_sens += subtotal_sens elif isinstance(cset, Iterable): for c in cset: - total_sens += self.link(c, target, var, subarray=subarray) + total_sens += self.link(c, target, vk, subarray=subarray) else: - if var is None and cset in self.csenss: + if vk is None and cset in self.csenss: total_sens = -abs(self.csenss[cset]) or -EPS - elif var is not None: + elif vk is not None: if cset.v_ss is None: - if var.key in cset.varkeys: + if vk in cset.varkeys: total_sens = EPS - elif var.key in cset.v_ss: - total_sens = cset.v_ss[var.key] or EPS + elif vk in cset.v_ss: + total_sens = cset.v_ss[vk] or EPS if not labeled: cstr = cset.str_without(["lineage", "units"]) label = cstr if len(cstr) <= 30 else "%s ..." % cstr[:30] @@ -153,20 +153,20 @@ def diagram(self, variable=None, varlabel=None, *, minsenss=0, maxlinks=20, self.maxlinks = maxlinks self.showconstraints = showconstraints - for key in self.solution.name_collision_varkeys(): - key.descr["necessarylineage"] = True + self.solution.set_necessarylineage() if variable: + variable = variable.key if not varlabel: - varlabel = variable.str_without(["unnecessary lineage"]) + varlabel = str(variable) if len(varlabel) > 20: varlabel = variable.str_without(["lineage"]) self.nodes[varlabel] = {"id": varlabel, "title": varlabel} csetnode = self.add_node(varlabel, self.csetlabel) - if variable.key in self.solution["sensitivities"]["cost"]: + if variable in self.solution["sensitivities"]["cost"]: costnode = self.add_node(varlabel, "[cost function]") self.links[costnode, varlabel] = \ - self.solution["sensitivities"]["cost"][variable.key] + self.solution["sensitivities"]["cost"][variable] else: csetnode = self.csetlabel self.nodes[self.csetlabel] = {"id": self.csetlabel, @@ -181,8 +181,7 @@ def diagram(self, variable=None, varlabel=None, *, minsenss=0, maxlinks=20, filename = self.csetlabel if variable: - filename += "_" + variable.str_without(["unnecessary lineage", - "units"]) + filename += "_%s" % variable if not os.path.isdir("sankey_autosaves"): os.makedirs("sankey_autosaves") filename = "sankey_autosaves" + os.path.sep + cleanfilename(filename) @@ -191,8 +190,7 @@ def diagram(self, variable=None, varlabel=None, *, minsenss=0, maxlinks=20, out.on_node_clicked(self.onclick) out.on_link_clicked(self.onclick) - for key in self.solution.name_collision_varkeys(): - del key.descr["necessarylineage"] + self.solution.set_necessarylineage(clear=True) return out def _links_and_nodes(self, top_node=None): diff --git a/gpkit/keydict.py b/gpkit/keydict.py index d6bbd948..6215344f 100644 --- a/gpkit/keydict.py +++ b/gpkit/keydict.py @@ -103,7 +103,6 @@ def __contains__(self, key): # pylint:disable=too-many-return-statements raise IndexError("key %s with idx %s is out of bounds" " for value %s" % (key, idx, super().__getitem__(key))) # pylint: disable=no-member - return True return key in self.keymap def update_keymap(self): diff --git a/gpkit/nomials/core.py b/gpkit/nomials/core.py index 9ab109ca..12d0af23 100644 --- a/gpkit/nomials/core.py +++ b/gpkit/nomials/core.py @@ -44,10 +44,16 @@ def str_without(self, excluded=()): return self.parse_ast(excluded) + units mstrs = [] for exp, c in self.hmap.items(): - varstrs = [] - for (var, x) in exp.items(): + pvarstrs, nvarstrs = [], [] + for (var, x) in sorted(exp.items(), + key=lambda vx: (vx[1], str(vx[0]))): if not x: continue + if x > 0: + varstrlist = pvarstrs + else: + x = -x + varstrlist = nvarstrs varstr = var.str_without(excluded) if UNICODE_EXPONENTS and int(x) == x and 2 <= x <= 9: x = int(x) @@ -57,14 +63,18 @@ def str_without(self, excluded=()): varstr += chr(8304+x) elif x != 1: varstr += "^%.2g" % x - varstrs.append(varstr) - varstrs.sort() + varstrlist.append(varstr) + numerator_strings = pvarstrs cstr = "%.3g" % c - if cstr == "-1" and varstrs: - mstrs.append("-" + "·".join(varstrs)) + if cstr == "-1": + cstr = "-" + if numerator_strings and cstr == "1": + mstr = MUL.join(pvarstrs) else: - cstr = [cstr] if (cstr != "1" or not varstrs) else [] - mstrs.append(MUL.join(cstr + varstrs)) + mstr = MUL.join([cstr] + pvarstrs) + if nvarstrs: + mstr = mstr + "/" + "/".join(nvarstrs) + mstrs.append(mstr) return " + ".join(sorted(mstrs)) + units def latex(self, excluded=()): # TODO: add ast parsing here diff --git a/gpkit/nomials/data.py b/gpkit/nomials/data.py index 29bb2761..a28cb9de 100644 --- a/gpkit/nomials/data.py +++ b/gpkit/nomials/data.py @@ -38,6 +38,7 @@ def cs(self): if self._cs is None: self._cs = np.array(list(self.hmap.values())) if self.hmap.units: + # TODO: treat vars as dimensionless, it's a hack self._cs = self._cs*self.hmap.units return self._cs diff --git a/gpkit/nomials/math.py b/gpkit/nomials/math.py index 789f197a..140275ce 100644 --- a/gpkit/nomials/math.py +++ b/gpkit/nomials/math.py @@ -262,7 +262,7 @@ def chop(self): monmaps = [NomialMap({exp: c}) for exp, c in self.hmap.items()] for monmap in monmaps: monmap.units = self.hmap.units - return [Monomial(monmap) for monmap in monmaps] + return [Monomial(monmap) for monmap in sorted(monmaps, key=str)] class Posynomial(Signomial): "A Signomial with strictly positive cs" diff --git a/gpkit/repr_conventions.py b/gpkit/repr_conventions.py index 1f07615b..02e6be96 100644 --- a/gpkit/repr_conventions.py +++ b/gpkit/repr_conventions.py @@ -89,16 +89,15 @@ class ReprMixin: cached_strs = None ast = None # pylint: disable=too-many-branches, too-many-statements - def parse_ast(self, excluded=("units")): + def parse_ast(self, excluded=()): "Turns the AST of this object's construction into a faithful string" + excluded = frozenset({"units"}.union(excluded)) if self.cached_strs is None: self.cached_strs = {} - elif frozenset(excluded) in self.cached_strs: - return self.cached_strs[frozenset(excluded)] - aststr = None + elif excluded in self.cached_strs: + return self.cached_strs[excluded] oper, values = self.ast # pylint: disable=unpacking-non-sequence - excluded = set(excluded) - excluded.add("units") + if oper == "add": left = strify(values[0], excluded) right = strify(values[1], excluded) @@ -167,7 +166,7 @@ def parse_ast(self, excluded=("units")): aststr = "%s[%s]" % (left, idx) else: raise ValueError(oper) - self.cached_strs[frozenset(excluded)] = aststr + self.cached_strs[excluded] = aststr return aststr def __repr__(self): diff --git a/gpkit/small_classes.py b/gpkit/small_classes.py index 96c0077f..241d35c6 100644 --- a/gpkit/small_classes.py +++ b/gpkit/small_classes.py @@ -11,7 +11,7 @@ class FixedScalarMeta(type): "Metaclass to implement instance checking for fixed scalars" def __instancecheck__(cls, obj): - return hasattr(obj, "hmap") and len(obj.hmap) == 1 and not obj.vks + return getattr(obj, "hmap", None) and len(obj.hmap) == 1 and not obj.vks class FixedScalar(metaclass=FixedScalarMeta): # pylint: disable=no-init @@ -65,10 +65,10 @@ def dot(self, arg): return self.tocsr().dot(arg) -class SolverLog(list): +class SolverLog: "Adds a `write` method to list so it's file-like and can replace stdout." def __init__(self, output=None, *, verbosity=0): - list.__init__(self) + self.written = "" self.verbosity = verbosity self.output = output @@ -76,11 +76,17 @@ def write(self, writ): "Append and potentially write the new line." if writ[:2] == "b'": writ = writ[2:-1] - if writ != "\n": - self.append(writ.rstrip("\n")) + self.written += writ if self.verbosity > 0: # pragma: no cover self.output.write(writ) + def lines(self): + "Returns the lines presently written." + return self.written.split("\n") + + def flush(self): + "Dummy function for I/O api compatibility" + class DictOfLists(dict): "A hierarchy of dicionaries, with lists at the bottom." diff --git a/gpkit/solution_array.py b/gpkit/solution_array.py index 6d8fb817..3feced2a 100644 --- a/gpkit/solution_array.py +++ b/gpkit/solution_array.py @@ -1,4 +1,5 @@ """Defines SolutionArray class""" +import sys import re import json import difflib @@ -7,11 +8,13 @@ import pickle import gzip import pickletools +from collections import defaultdict import numpy as np from .nomials import NomialArray -from .small_classes import DictOfLists, Strings +from .small_classes import DictOfLists, Strings, SolverLog from .small_scripts import mag, try_str_without from .repr_conventions import unitstr, lineagestr +from .breakdowns import Breakdowns CONSTRSPLITPATTERN = re.compile(r"([^*]\*[^*])|( \+ )|( >= )|( <= )|( = )") @@ -77,7 +80,7 @@ def msenss_table(data, _, **kwargs): if (msenss < 0.1).all(): msenss = np.max(msenss) if msenss: - msenssstr = "%6s" % ("<1e%i" % np.log10(msenss)) + msenssstr = "%6s" % ("<1e%i" % max(-3, np.log10(msenss))) else: msenssstr = " =0 " else: @@ -144,12 +147,13 @@ def tight_table(self, _, ntightconstrs=5, tight_senss=1e-2, **kwargs): title = "Most Sensitive Constraints" if len(self) > 1: title += " (in last sweep)" - data = sorted(((-float("%+6.2g" % s[-1]), str(c)), - "%+6.2g" % s[-1], id(c), c) + data = sorted(((-float("%+6.2g" % abs(s[-1])), str(c)), + "%+6.2g" % abs(s[-1]), id(c), c) for c, s in self["sensitivities"]["constraints"].items() if s[-1] >= tight_senss)[:ntightconstrs] else: - data = sorted(((-float("%+6.2g" % s), str(c)), "%+6.2g" % s, id(c), c) + data = sorted(((-float("%+6.2g" % abs(s)), str(c)), + "%+6.2g" % abs(s), id(c), c) for c, s in self["sensitivities"]["constraints"].items() if s >= tight_senss)[:ntightconstrs] return constraint_table(data, title, **kwargs) @@ -173,15 +177,14 @@ def loose_table(self, _, min_senss=1e-5, **kwargs): def constraint_table(data, title, sortbymodel=True, showmodels=True, **_): "Creates lines for tables where the right side is a constraint." # TODO: this should support 1D array inputs from sweeps - excluded = ("units", "unnecessary lineage") - if not showmodels: - excluded = ("units", "lineage") # hide all of it + excluded = {"units"} if showmodels else {"units", "lineage"} models, decorated = {}, [] for sortby, openingstr, _, constraint in sorted(data): model = lineagestr(constraint) if sortbymodel else "" if model not in models: models[model] = len(models) - constrstr = try_str_without(constraint, excluded) + constrstr = try_str_without( + constraint, {":MAGIC:"+lineagestr(constraint)}.union(excluded)) if " at 0x" in constrstr: # don't print memory addresses constrstr = constrstr[:constrstr.find(" at 0x")] + ">" decorated.append((models[model], model, sortby, constrstr, openingstr)) @@ -195,7 +198,6 @@ def constraint_table(data, title, sortbymodel=True, showmodels=True, **_): if model or lines: lines.append([("newmodelline",), model]) previous_model = model - constrstr = constrstr.replace(model, "") minlen, maxlen = 25, 80 segments = [s for s in CONSTRSPLITPATTERN.split(constrstr) if s] constraintlines = [] @@ -281,6 +283,23 @@ def warnings_table(self, _, **kwargs): lines[-1] = "~~~~~~~~" return lines + [""] +def bdtable_gen(key): + "Generator for breakdown tablefns" + + def bdtable(self, _showvars, **_): + "Cost breakdown plot" + bds = Breakdowns(self) + original_stdout = sys.stdout + try: + sys.stdout = SolverLog(original_stdout, verbosity=0) + bds.plot(key) + finally: + lines = sys.stdout.lines() + sys.stdout = original_stdout + return lines + + return bdtable + TABLEFNS = {"sensitivities": senss_table, "top sensitivities": topsenss_table, @@ -289,6 +308,8 @@ def warnings_table(self, _, **kwargs): "tightest constraints": tight_table, "loose constraints": loose_table, "warnings": warnings_table, + "model sensitivities breakdown": bdtable_gen("model sensitivities"), + "cost breakdown": bdtable_gen("cost") } def unrolled_absmax(values): @@ -354,23 +375,55 @@ class SolutionArray(DictOfLists): """ modelstr = "" _name_collision_varkeys = None + _lineageset = False table_titles = {"choicevariables": "Choice Variables", "sweepvariables": "Swept Variables", "freevariables": "Free Variables", "constants": "Fixed Variables", # TODO: change everywhere "variables": "Variables"} - def name_collision_varkeys(self): + def set_necessarylineage(self, clear=False): # pylint: disable=too-many-branches "Returns the set of contained varkeys whose names are not unique" if self._name_collision_varkeys is None: + self._name_collision_varkeys = {} self["variables"].update_keymap() keymap = self["variables"].keymap - self._name_collision_varkeys = set() - for key in list(keymap): + name_collisions = defaultdict(set) + for key in keymap: if hasattr(key, "key"): - if len(keymap[key.str_without(["lineage", "vec"])]) > 1: - self._name_collision_varkeys.add(key) - return self._name_collision_varkeys + if len(keymap[key.name]) == 1: # very unique + self._name_collision_varkeys[key] = 0 + else: + shortname = key.str_without(["lineage", "vec"]) + if len(keymap[shortname]) > 1: + name_collisions[shortname].add(key) + for varkeys in name_collisions.values(): + min_namespaced = defaultdict(set) + for vk in varkeys: + *_, mineage = vk.lineagestr().split(".") + min_namespaced[(mineage, 1)].add(vk) + while any(len(vks) > 1 for vks in min_namespaced.values()): + for key, vks in list(min_namespaced.items()): + if len(vks) <= 1: + continue + del min_namespaced[key] + mineage, idx = key + idx += 1 + for vk in vks: + lineages = vk.lineagestr().split(".") + submineage = lineages[-idx] + "." + mineage + min_namespaced[(submineage, idx)].add(vk) + for (_, idx), vks in min_namespaced.items(): + vk, = vks + self._name_collision_varkeys[vk] = idx + if clear: + self._lineageset = False + for vk in self._name_collision_varkeys: + del vk.descr["necessarylineage"] + else: + self._lineageset = True + for vk, idx in self._name_collision_varkeys.items(): + vk.descr["necessarylineage"] = idx def __len__(self): try: @@ -532,26 +585,23 @@ def varnames(self, showvars, exclude): "Returns list of variables, optionally with minimal unique names" if showvars: showvars = self._parse_showvars(showvars) - for key in self.name_collision_varkeys(): - key.descr["necessarylineage"] = True + self.set_necessarylineage() names = {} for key in showvars or self["variables"]: for k in self["variables"].keymap[key]: names[k.str_without(exclude)] = k - for key in self.name_collision_varkeys(): - del key.descr["necessarylineage"] + self.set_necessarylineage(clear=True) return names def savemat(self, filename="solution.mat", *, showvars=None, - excluded=("unnecessary lineage", "vec")): + excluded=("vec")): "Saves primal solution as matlab file" from scipy.io import savemat savemat(filename, {name.replace(".", "_"): np.array(self["variables"][key], "f") for name, key in self.varnames(showvars, excluded).items()}) - def todataframe(self, showvars=None, - excluded=("unnecessary lineage", "vec")): + def todataframe(self, showvars=None, excluded=("vec")): "Returns primal solution as pandas dataframe" import pandas as pd # pylint:disable=import-error rows = [] @@ -596,8 +646,8 @@ def savetxt(self, filename="solution.txt", *, printmodel=True, **kwargs): def savejson(self, filename="solution.json", showvars=None): "Saves solution table as a json file" sol_dict = {} - for key in self.name_collision_varkeys(): - key.descr["necessarylineage"] = True + if self._lineageset: + self.set_necessarylineage(clear=True) data = self["variables"] if showvars: showvars = self._parse_showvars(showvars) @@ -610,8 +660,6 @@ def savejson(self, filename="solution.json", showvars=None): else: val = {"v": v, "u": k.unitstr()} sol_dict[key] = val - for key in self.name_collision_varkeys(): - del key.descr["necessarylineage"] with open(filename, "w") as f: json.dump(sol_dict, f) @@ -668,7 +716,7 @@ def subinto(self, posy): return NomialArray([self.atindex(i).subinto(posy) for i in range(len(self))]) - return posy.sub(self["variables"]) + return posy.sub(self["variables"], require_positive=False) def _parse_showvars(self, showvars): showvars_out = set() @@ -678,27 +726,16 @@ def _parse_showvars(self, showvars): showvars_out.update(keys) return showvars_out - def summary(self, showvars=(), ntopsenss=5, **kwargs): - "Print summary table, showing top sensitivities and no constants" - showvars = self._parse_showvars(showvars) - out = self.table(showvars, ["cost", "warnings", "sweepvariables", - "freevariables"], **kwargs) - constants_in_showvars = showvars.intersection(self["constants"]) - senss_tables = [] - if len(self["constants"]) < ntopsenss+2 or constants_in_showvars: - senss_tables.append("sensitivities") - if len(self["constants"]) >= ntopsenss+2: - senss_tables.append("top sensitivities") - senss_tables.append("tightest constraints") - senss_str = self.table(showvars, senss_tables, nvars=ntopsenss, - **kwargs) - if senss_str: - out += "\n" + senss_str - return out + def summary(self, showvars=(), **kwargs): + "Print summary table, showing no sensitivities or constants" + return self.table(showvars, + ["cost breakdown", "model sensitivities breakdown", + "warnings", "sweepvariables", "freevariables"], + **kwargs) def table(self, showvars=(), - tables=("cost", "warnings", "model sensitivities", - "sweepvariables", "freevariables", + tables=("cost breakdown", "model sensitivities breakdown", + "warnings", "sweepvariables", "freevariables", "constants", "sensitivities", "tightest constraints"), sortmodelsbysenss=False, **kwargs): """A table representation of this SolutionArray @@ -732,11 +769,13 @@ def table(self, showvars=(), break if has_only_one_model: kwargs["sortbymodel"] = False - for key in self.name_collision_varkeys(): - key.descr["necessarylineage"] = True + self.set_necessarylineage() showvars = self._parse_showvars(showvars) strs = [] for table in tables: + if len(self) > 1 and "breakdown" in table: + # no breakdowns for sweeps + table = table.replace(" breakdown", "") if "sensitivities" not in self and ("sensitivities" in table or "constraints" in table): continue @@ -768,8 +807,7 @@ def table(self, showvars=(), "% \\usepackage{amsmath}", "% \\begin{document}\n")) strs = [preamble] + strs + ["% \\end{document}"] - for key in self.name_collision_varkeys(): - del key.descr["necessarylineage"] + self.set_necessarylineage(clear=True) return "\n".join(strs) def plot(self, posys=None, axes=None): @@ -833,9 +871,10 @@ def var_table(data, title, *, printunits=True, latex=False, rawlines=False, if minval and hidebelowminval and getattr(v, "shape", None): v[np.abs(v) <= minval] = np.nan model = lineagestr(k.lineage) if sortbymodel else "" - msenss = -sortmodelsbysenss.get(model, 0) if sortmodelsbysenss else 0 - if hasattr(msenss, "shape"): - msenss = np.mean(msenss) + if not sortmodelsbysenss: + msenss = 0 + else: # sort should match that in msenss_table above + msenss = -round(np.mean(sortmodelsbysenss.get(model, 0)), 4) models.add(model) b = bool(getattr(v, "shape", None)) s = k.str_without(("lineage", "vec")) diff --git a/gpkit/tests/t_examples.py b/gpkit/tests/t_examples.py index a81991a2..e4a21282 100644 --- a/gpkit/tests/t_examples.py +++ b/gpkit/tests/t_examples.py @@ -49,6 +49,9 @@ def test_dummy_example(self, example): # import matplotlib.pyplot as plt # plt.close("all") + def test_breakdowns(self, example): + pass + def test_issue_1513(self, example): pass diff --git a/gpkit/tests/t_keydict.py b/gpkit/tests/t_keydict.py index 3bf6b131..d54865da 100644 --- a/gpkit/tests/t_keydict.py +++ b/gpkit/tests/t_keydict.py @@ -52,7 +52,7 @@ def test_vector(self): v = VectorVariable(3, "v") kd = KeyDict() kd[v] = np.array([2, 3, 4]) - self.assertTrue(all(kd[v] == kd[v.key])) + self.assertTrue(all(kd[v] == kd[v.key])) # pylint:disable=no-member self.assertTrue(all(kd["v"] == np.array([2, 3, 4]))) self.assertEqual(v[0].key.idx, (0,)) self.assertEqual(kd[v][0], kd[v[0]]) diff --git a/gpkit/varkey.py b/gpkit/varkey.py index 4fe35dbe..fc137502 100644 --- a/gpkit/varkey.py +++ b/gpkit/varkey.py @@ -63,10 +63,32 @@ def __setstate__(self, state): def str_without(self, excluded=()): "Returns string without certain fields (such as 'lineage')." name = self.name - if ("lineage" not in excluded and self.lineage - and ("unnecessary lineage" not in excluded - or self.necessarylineage)): - name = self.lineagestr("modelnums" not in excluded) + "." + name + if "lineage" not in excluded and self.lineage: + namespace = self.lineagestr("modelnums" not in excluded).split(".") + for ex in excluded: + if ex[0:7] == ":MAGIC:": + to_replace = ex[7:] + if not to_replace: + continue + to_replace = to_replace.split(".") + replaced = 0 + for modelname in to_replace: + if not namespace or namespace[0] != modelname: + break + replaced += 1 + namespace = namespace[1:] + if len(to_replace) > replaced: + namespace.insert(0, "."*(len(to_replace)-replaced)) + necessarylineage = self.necessarylineage + if necessarylineage is None and self.veckey: + necessarylineage = self.veckey.necessarylineage + if necessarylineage is not None: + if necessarylineage > 0: + namespace = namespace[-necessarylineage:] + else: + namespace = None + if namespace: + name = ".".join(namespace) + "." + name if "idx" not in excluded: if self.idx: name += "[%s]" % ",".join(map(str, self.idx)) @@ -92,9 +114,7 @@ def latex(self, excluded=()): name = "\\vec{%s}" % name if "idx" not in excluded and self.idx: name = "{%s}_{%s}" % (name, ",".join(map(str, self.idx))) - if ("lineage" not in excluded and self.lineage - and ("unnecessary lineage" not in excluded - or self.necessarylineage)): + if "lineage" not in excluded and self.lineage: name = "{%s}_{%s}" % (name, self.lineagestr("modelnums" not in excluded)) return name