Coverage for gpkit/interactive/widgets.py: 0%

155 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-01-07 22:13 -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 

9 

10 

11# pylint: disable=too-many-locals 

12def modelinteract(model, fns_of_sol, ranges=None, **solvekwargs): 

13 """Easy model interaction in IPython / Jupyter 

14 

15 By default, this creates a model with sliders for every constant 

16 which prints a new solution table whenever the sliders are changed. 

17 

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. 

23 

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) 

28 

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 

70 

71 if not hasattr(fns_of_sol, "__iter__"): 

72 fns_of_sol = [fns_of_sol] 

73 

74 solvekwargs["verbosity"] = 0 

75 

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() 

95 

96 resolve() 

97 

98 return widgets.interactive(resolve, **ranges_out) 

99 

100 

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 

104 

105 Like interact(), but with the ability to control sliders and their showvars 

106 live. args and kwargs are passed on to interact() 

107 """ 

108 

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 

115 

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 ())) 

122 

123 __defaultfntable = __defaultfn 

124 

125 fns_of_sol = [__defaultfntable] 

126 

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) 

139 

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") 

157 

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) 

172 

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) 

179 

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 

189 

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")]) 

195 

196 tabs.set_title(0, 'Sliders') 

197 

198 return tabs 

199 

200 

201def create_settings(box): 

202 "Creates a widget Container for settings and info of a particular slider." 

203 _, slider, sl_units = box.children 

204 

205 enable = widgets.Checkbox(value=box.visible, width="3ex") 

206 link((box, 'visible'), (enable, 'value')) 

207 

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")) 

231 

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") 

242 

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" 

249 

250 return widgets.HBox(children=[enable, setvalue, units, descr, 

251 fromlabel, setmin, tolabel, setmax], 

252 width="105ex")