Coverage for gpkit/interactive/widgets.py: 0%
155 statements
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-07 22:15 -0500
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-07 22:15 -0500
1"Interactive GPkit widgets for iPython notebook"
2#pylint: disable=import-error
3import ipywidgets as widgets
4from traitlets import link
5from .plot_sweep import plot_1dsweepgrid
6from ..small_scripts import is_sweepvar
7from ..small_classes import Numbers
8from ..exceptions import InvalidGPConstraint
11# pylint: disable=too-many-locals
12def modelinteract(model, fns_of_sol, ranges=None, **solvekwargs):
13 """Easy model interaction in IPython / Jupyter
15 By default, this creates a model with sliders for every constant
16 which prints a new solution table whenever the sliders are changed.
18 Arguments
19 ---------
20 fn_of_sol : function
21 The function called with the solution after each solve that
22 displays the result. By default prints a table.
24 ranges : dictionary {str: Slider object or tuple}
25 Determines which sliders get created. Tuple values may contain
26 two or three floats: two correspond to (min, max), while three
27 correspond to (min, step, max)
29 **solvekwargs
30 kwargs which get passed to the solve()/localsolve() method.
31 """
32 ranges_out = {}
33 if ranges:
34 if not isinstance(ranges, dict):
35 ranges = {k: None for k in ranges}
36 slider_vars = set()
37 for k in list(ranges):
38 if k in model.substitutions: # only if already a constant
39 for key in model.varkeys[k]:
40 slider_vars.add(key)
41 ranges[key] = ranges[k]
42 del ranges[k]
43 else:
44 slider_vars = model.substitutions
45 for k in slider_vars:
46 v = model.substitutions[k]
47 if is_sweepvar(v) or isinstance(v, Numbers):
48 if is_sweepvar(v):
49 # By default, sweep variables become sliders, so we
50 # need to to update the model's substitutions such that
51 # the first solve (with no substitutions) reflects
52 # the substitutions created on slider movement
53 _, sweep = v
54 v = sweep[0]
55 model.substitutions.update({k: v})
56 vmin, vmax = v/2.0, v*2.0
57 if is_sweepvar(v):
58 # pylint: disable=nested-min-max
59 vmin = min(vmin, min(sweep))
60 vmax = max(vmax, min(sweep))
61 if ranges and ranges[k]:
62 vmin, vmax = ranges[k]
63 vstep = (vmax-vmin)/24.0
64 varkey_latex = "$"+k.latex(excluded=["lineage"])+"$"
65 floatslider = widgets.FloatSlider(min=vmin, max=vmax,
66 step=vstep, value=v,
67 description=varkey_latex)
68 floatslider.varkey = k
69 ranges_out[str(k)] = floatslider
71 if not hasattr(fns_of_sol, "__iter__"):
72 fns_of_sol = [fns_of_sol]
74 solvekwargs["verbosity"] = 0
76 def resolve(**subs):
77 "This method gets called each time the user changes something"
78 model.substitutions.update(subs)
79 try:
80 try:
81 model.solve(**solvekwargs)
82 except InvalidGPConstraint:
83 # TypeError raised by as_hmapslt1 in non-GP-compatible models
84 model.localsolve(**solvekwargs)
85 if hasattr(model, "solution"):
86 sol = model.solution
87 else:
88 sol = model.result
89 for fn in fns_of_sol:
90 fn(sol)
91 except RuntimeWarning as e:
92 print("RuntimeWarning:", str(e).split("\n", maxsplit=1))
93 print("\n> Running model.debug()")
94 model.debug()
96 resolve()
98 return widgets.interactive(resolve, **ranges_out)
101# pylint: disable=too-many-locals, too-many-statements
102def modelcontrolpanel(model, showvars=(), fns_of_sol=None, **solvekwargs):
103 """Easy model control in IPython / Jupyter
105 Like interact(), but with the ability to control sliders and their showvars
106 live. args and kwargs are passed on to interact()
107 """
109 freevars = set(model.varkeys).difference(model.substitutions)
110 freev_in_showvars = False
111 for var in showvars:
112 if var in model.varkeys and freevars.intersection(model.varkeys[var]):
113 freev_in_showvars = True
114 break
116 if fns_of_sol is None:
117 def __defaultfn(solution):
118 "Display function to run when a slider is moved."
119 # NOTE: if there are some freevariables in showvars, filter
120 # the table to show only those and the slider constants
121 print(solution.summary(showvars if freev_in_showvars else ()))
123 __defaultfntable = __defaultfn
125 fns_of_sol = [__defaultfntable]
127 sliders = model.interact(fns_of_sol, showvars, **solvekwargs)
128 sliderboxes = []
129 for sl in sliders.children:
130 cb = widgets.Checkbox(value=True, width="3ex")
131 unit_latex = sl.varkey.latex_unitstr()
132 if unit_latex:
133 unit_latex = r"$"+unit_latex+"$"
134 units = widgets.Label(value=unit_latex)
135 units.font_size = "1.16em"
136 box = widgets.HBox(children=[cb, sl, units])
137 link((box, 'visible'), (cb, 'value'))
138 sliderboxes.append(box)
140 widgets_css = widgets.HTML("""<style>
141 [style="font-size: 1.16em;"] { padding-top: 0.25em; }
142 [style="width: 3ex; font-size: 1.165em;"] { padding-top: 0.2em; }
143 .widget-numeric-text { width: auto; }
144 .widget-numeric-text .widget-label { width: 20ex; }
145 .widget-numeric-text .form-control { background: #fbfbfb; width: 8.5ex; }
146 .widget-slider .widget-label { width: 20ex; }
147 .widget-checkbox .widget-label { width: 15ex; }
148 .form-control { border: none; box-shadow: none; }
149 </style>""")
150 settings = [widgets_css]
151 for sliderbox in sliderboxes:
152 settings.append(create_settings(sliderbox))
153 sweep = widgets.Checkbox(value=False, width="3ex")
154 label = "Plot top sliders against: (separate with two spaces)"
155 boxlabel = widgets.Label(value=label, width="200ex")
156 y_axes = widgets.Text(value="none", width="20ex")
158 def append_plotfn():
159 "Creates and adds plotfn to fn_of_sols"
160 yvars = [model.cost]
161 for varname in y_axes.value.split(" "): # pylint: disable=no-member
162 varname = varname.strip()
163 try:
164 yvars.append(model[varname])
165 except: # pylint: disable=bare-except
166 break
167 ranges = {}
168 for sb in sliderboxes[1:]:
169 if sb.visible and len(ranges) < 3:
170 slider = sb.children[1]
171 ranges[slider.varkey] = (slider.min, slider.max)
173 def __defaultfn(sol):
174 "Plots a 1D sweep grid, starting from a single solution"
175 fig, _ = plot_1dsweepgrid(model, ranges, yvars, origsol=sol,
176 verbosity=0, solver="mosek_cli")
177 fig.show()
178 fns_of_sol.append(__defaultfn)
180 def redo_plots(_):
181 "Refreshes the plotfn"
182 if fns_of_sol and fns_of_sol[-1].__name__ == "__defaultfn":
183 fns_of_sol.pop() # get rid of the old one!
184 if sweep.value:
185 append_plotfn()
186 if not fns_of_sol:
187 fns_of_sol.append(__defaultfntable)
188 sl.value = sl.value*(1.000001) # pylint: disable=undefined-loop-variable
190 sweep.observe(redo_plots, "value")
191 y_axes.on_submit(redo_plots)
192 sliderboxes.insert(0, widgets.HBox(children=[sweep, boxlabel, y_axes]))
193 tabs = widgets.Tab(children=[widgets.VBox(children=sliderboxes,
194 padding="1.25ex")])
196 tabs.set_title(0, 'Sliders')
198 return tabs
201def create_settings(box):
202 "Creates a widget Container for settings and info of a particular slider."
203 _, slider, sl_units = box.children
205 enable = widgets.Checkbox(value=box.visible, width="3ex")
206 link((box, 'visible'), (enable, 'value'))
208 def slider_link(obj, attr):
209 "Function to link one object to an attr of the slider."
210 # pylint: disable=unused-argument
211 def link_fn(name, new_value):
212 "How to update the object's value given min/max on the slider. "
213 if new_value >= slider.max:
214 slider.max = new_value
215 # if any value is greater than the max, the max slides up
216 # however, this is not held true for the minimum, because
217 # during typing the max or value will grow, and we don't want
218 # to permanently anchor the minimum to unfinished typing
219 if attr == "max" and new_value <= slider.value:
220 if slider.max >= slider.min:
221 slider.value = new_value
222 else:
223 pass # bounds nonsensical, probably because we picked up
224 # a small value during user typing.
225 elif attr == "min" and new_value >= slider.value:
226 slider.value = new_value
227 setattr(slider, attr, new_value)
228 slider.step = (slider.max - slider.min)/24.0
229 obj.on_trait_change(link_fn, "value")
230 link((slider, attr), (obj, "value"))
232 text_html = "<span class='form-control' style='width: auto;'>"
233 setvalue = widgets.FloatText(value=slider.value,
234 description=slider.description)
235 slider_link(setvalue, "value")
236 fromlabel = widgets.HTML(text_html + "from")
237 setmin = widgets.FloatText(value=slider.min, width="10ex")
238 slider_link(setmin, "min")
239 tolabel = widgets.HTML(text_html + "to")
240 setmax = widgets.FloatText(value=slider.max, width="10ex")
241 slider_link(setmax, "max")
243 units = widgets.Label()
244 units.width = "6ex"
245 units.font_size = "1.165em"
246 link((sl_units, 'value'), (units, 'value'))
247 descr = widgets.HTML(text_html + slider.varkey.descr.get("label", ""))
248 descr.width = "40ex"
250 return widgets.HBox(children=[enable, setvalue, units, descr,
251 fromlabel, setmin, tolabel, setmax],
252 width="105ex")