1 |
from gi.repository import Gtk, Gdk |
2 |
from gi.repository import Pango |
3 |
import ascpy |
4 |
from celsiusunits import CelsiusUnits |
5 |
from preferences import Preferences |
6 |
|
7 |
from varentry import * |
8 |
from studyreporter import * |
9 |
from math import log, exp |
10 |
|
11 |
STEP_NUMBER = 0 |
12 |
STEP_INCREM = 1 |
13 |
STEP_RATIO = 2 |
14 |
DIST_LINEAR = "Linear" |
15 |
DIST_LOG = "Logarithmic" |
16 |
|
17 |
class StudyWin: |
18 |
def __init__(self, browser, instance): |
19 |
""" |
20 |
Study dialog: allow user to fill an Observer by varying the value in |
21 |
a particular column, and solving for each case (also there is the option |
22 |
of running a selected METHOD before each step) |
23 |
""" |
24 |
# we will be using the instance to determine valid units for bounds and step size |
25 |
self.instance = instance |
26 |
self.browser = browser |
27 |
self.browser.builder.add_objects_from_file(self.browser.glade_file,["list_of_dist","studywin"]) |
28 |
|
29 |
widgets = ["lowerb","upperb","step_menu","nsteps","methodrun","dist","check_dist","studywin","var_to_study"] |
30 |
for n in widgets: |
31 |
setattr(self,n,self.browser.builder.get_object(n)) |
32 |
|
33 |
self.checkbutton = self.browser.builder.get_object("on_fail_continue") |
34 |
self.method = None |
35 |
|
36 |
# TODO add an integer index to the ListStore as well, to avoid string conversion |
37 |
self.step_menu_model = Gtk.ListStore(int,str) |
38 |
self.step_menu_model.append([STEP_NUMBER,'No. of steps']) |
39 |
self.step_menu_model.append([STEP_INCREM,'Step size']) |
40 |
self.step_menu_model.append([STEP_RATIO, 'Step ratio']) |
41 |
renderer = Gtk.CellRendererText() |
42 |
self.step_menu.set_model(self.step_menu_model) |
43 |
self.step_menu.pack_start(renderer, True) |
44 |
self.step_menu.add_attribute(renderer, 'text',1) |
45 |
self.step_menu.set_active(0) |
46 |
#self.step_type="No. of steps" |
47 |
|
48 |
_p = self.browser.prefs |
49 |
_continue_on_fail = _p.getBoolPref("StudyReporter", "continue_on_fail", True) |
50 |
self.checkbutton.set_active(_continue_on_fail) |
51 |
|
52 |
# set up the distributions combobox |
53 |
dist_model = Gtk.ListStore(str) |
54 |
dist_model.append([DIST_LINEAR]) |
55 |
dist_model.append([DIST_LOG]) |
56 |
self.dist.set_model(dist_model) |
57 |
self.dist.set_active(0) |
58 |
|
59 |
# set up the methods combobox |
60 |
_methodstore = self.browser.methodstore |
61 |
_methodrenderer = Gtk.CellRendererText() |
62 |
self.methodrun.set_model(_methodstore) |
63 |
self.methodrun.pack_start(_methodrenderer, True) |
64 |
self.methodrun.add_attribute(_methodrenderer, 'text',0) |
65 |
|
66 |
# user preferences for this dialog |
67 |
self.nsteps_number = str(int(float(self.browser.prefs.getStringPref("Study","nsteps","10")))) |
68 |
self.nsteps_increm = self.browser.prefs.getStringPref("Study","nsteps_increm","") |
69 |
self.nsteps_ratio = self.browser.prefs.getStringPref("Study","nsteps_ratio","1.1") |
70 |
self.nsteps_type = int(self.browser.prefs.getStringPref("Study","step_type",str(STEP_NUMBER))) |
71 |
|
72 |
# fill in step type and default nsteps |
73 |
self.var_to_study.set_text(self.browser.sim.getInstanceName(self.instance)) |
74 |
self.set_step_type(self.nsteps_type) |
75 |
self.nsteps.set_text({ |
76 |
STEP_NUMBER:self.nsteps_number |
77 |
,STEP_INCREM:self.nsteps_increm |
78 |
,STEP_RATIO:self.nsteps_ratio |
79 |
}[self.nsteps_type]) |
80 |
# if using an increment by preference, only permit it if dimensionally compatible with |
81 |
# selected variable, else default back to number of steps |
82 |
if not self.validate_nsteps(): |
83 |
self.set_step_type(STEP_NUMBER) |
84 |
self.nsteps.set_text(self.nsteps_number) |
85 |
self.taint_entry(self.nsteps,good=1) |
86 |
|
87 |
# fill in upper/.lower bound |
88 |
_u = self.instance.getType().getPreferredUnits(); |
89 |
if _u is None: |
90 |
_conversion = 1 |
91 |
_u = self.instance.getDimensions().getDefaultUnits().getName().toString() |
92 |
else: |
93 |
_conversion = _u.getConversion() # displayvalue x conversion = SI |
94 |
_u = _u.getName().toString() |
95 |
|
96 |
_arr = {self.lowerb: self.instance.getRealValue() |
97 |
,self.upperb: self.instance.getUpperBound() # this upper bound is probably stoopid |
98 |
} |
99 |
|
100 |
##### CELSIUS TEMPERATURE WORKAROUND |
101 |
if self.instance.getType().isRefinedReal() and str(self.instance.getType().getDimensions()) == 'TMP': |
102 |
units = Preferences().getPreferredUnitsOrigin(str(self.instance.getType().getName())) |
103 |
if units == CelsiusUnits.get_celsius_sign(): |
104 |
self.convert = True |
105 |
##### CELSIUS TEMPERATURE WORKAROUND |
106 |
|
107 |
for _k,_v in _arr.iteritems(): |
108 |
_t = str(_v / _conversion)+" "+_u |
109 |
##### CELSIUS TEMPERATURE WORKAROUND |
110 |
if self.convert: |
111 |
_t = CelsiusUnits.convert_kelvin_to_celsius(_v, str(self.instance.getType())) + " " + CelsiusUnits.get_celsius_sign() |
112 |
##### CELSIUS TEMPERATURE WORKAROUND |
113 |
_k.set_text(_t) |
114 |
|
115 |
self.browser.builder.connect_signals(self) |
116 |
self.lowerb.select_region(0, -1) |
117 |
|
118 |
def get_step_type(self): |
119 |
_s = self.step_menu.get_active_iter() |
120 |
return self.step_menu_model.get_value(_s,0) |
121 |
|
122 |
def set_step_type(self,st): |
123 |
#FIXME this depends on the ordering being equal to the 'id' column values? |
124 |
self.step_menu.set_active(st) |
125 |
|
126 |
def set_dist(self,dist): |
127 |
#FIXME this depends on the ordering, is there a better way? |
128 |
self.dist.set_active({DIST_LINEAR:0, DIST_LOG:1}[dist]) |
129 |
|
130 |
def run(self): |
131 |
while 1: |
132 |
_res = self.studywin.run(); |
133 |
if _res == Gtk.ResponseType.OK: |
134 |
if self.validate_inputs(): |
135 |
# store inputs for later recall |
136 |
_p = self.browser.prefs |
137 |
_p.setStringPref("Study", "nsteps", self.nsteps_number) |
138 |
_p.setStringPref("Study", "nsteps_increm", self.nsteps_increm) |
139 |
_p.setStringPref("Study", "nsteps_ratio", self.nsteps_ratio) |
140 |
_p.setStringPref("Study", "nsteps_type", self.get_step_type()) |
141 |
# run the study |
142 |
self.solve() |
143 |
break |
144 |
else: |
145 |
self.browser.reporter.reportError("Please review input errors in Study dialog.") |
146 |
continue |
147 |
elif _res==Gtk.ResponseType.CANCEL: |
148 |
# cancel... exit Study |
149 |
break |
150 |
self.studywin.destroy() |
151 |
|
152 |
def on_studywin_close(self,*args): |
153 |
self.studywin.response(Gtk.ResponseType.CANCEL) |
154 |
|
155 |
def on_key_press_event(self,widget,event): |
156 |
keyname = Gdk.keyval_name(event.keyval) |
157 |
if keyname=="Return": |
158 |
self.studywin.response(Gtk.ResponseType.OK) |
159 |
return True |
160 |
elif keyname=="Escape": |
161 |
self.studywin.response(Gtk.ResponseType.CANCEL) |
162 |
return True; |
163 |
return False; |
164 |
|
165 |
def on_methodrun_changed(self, *args): |
166 |
index = self.methodrun.get_active() |
167 |
piter = self.methodrun.get_model().get_iter(Gtk.TreePath.new_from_string(str(index))) |
168 |
_sel = self.methodrun.get_model().get_value(piter, 0) |
169 |
if _sel: |
170 |
_methods = self.browser.sim.getType().getMethods() |
171 |
for _m in _methods: |
172 |
if _m.getName()==_sel: |
173 |
self.method = _m |
174 |
break |
175 |
|
176 |
def on_dist_changed(self, *args): |
177 |
_dist = self.dist.get_active_text() |
178 |
if _dist == DIST_LINEAR: |
179 |
if self.get_step_type() == STEP_RATIO: |
180 |
self.set_step_type(STEP_INCREM) |
181 |
self.nsteps.set_text(self.nsteps_number) |
182 |
elif _dist == DIST_LOG: |
183 |
if self.get_step_type() == STEP_INCREM: |
184 |
self.set_step_type(STEP_RATIO) |
185 |
self.nsteps.set_text(self.nsteps_ratio) |
186 |
self.validate_inputs() |
187 |
|
188 |
def validate_inputs(self): |
189 |
""" |
190 |
Check that inputs make sense in terms of log dist constraints & others. |
191 |
Returns 1 if all is valid. If all is not valid, relevant inputs are |
192 |
tainted for user correction. |
193 |
""" |
194 |
_dist = self.dist.get_active_text() |
195 |
|
196 |
_start = self.parse_entry(self.lowerb) |
197 |
_end = self.parse_entry(self.upperb) |
198 |
steps = self.validate_nsteps() |
199 |
|
200 |
if _start is None or _end is None: |
201 |
# error/empty start/end values will already have been tainted |
202 |
self.taint_dist() |
203 |
return 0 |
204 |
|
205 |
if _start == _end: |
206 |
# can't distribute over a zero-width range (and no point) |
207 |
_msg = "Bounds cannot not be equal." |
208 |
self.taint_dist(msg=_msg) |
209 |
self.taint_entry(self.lowerb,msg=_msg) |
210 |
self.taint_entry(self.upperb,msg=_msg) |
211 |
return 0 |
212 |
|
213 |
if _dist == DIST_LINEAR: |
214 |
flag = 0 |
215 |
if self.get_step_type() == STEP_RATIO: |
216 |
self.set_step_type(STEP_INCREM) |
217 |
self.taint_dist(good=1) |
218 |
return 1 |
219 |
if _dist == DIST_LOG: |
220 |
flag = 0 |
221 |
if self.get_step_type() == STEP_INCREM: |
222 |
self.set_step_type(STEP_RATIO) |
223 |
flag = 1 |
224 |
if flag == 1: |
225 |
self.step_menu.set_active(1) |
226 |
if _start == 0 or _end == 0: |
227 |
_msg = "Bounds cannot be 0 for logarithmic distribution." |
228 |
self.taint_dist(_msg) |
229 |
if _start == 0: |
230 |
self.taint_entry(self.lowerb,msg=_msg) |
231 |
else: |
232 |
self.taint_entry(self.upperb,msg=_msg) |
233 |
return 0 |
234 |
if (_start/_end) < 0: |
235 |
_msg = "Bounds cannot be of opposite sign for logarithmic distribution." |
236 |
self.taint_dist(_msg) |
237 |
self.taint_entry(self.lowerb,msg=_msg) |
238 |
self.taint_entry(self.upperb,msg=_msg) |
239 |
return 0 |
240 |
self.check_dist.set_from_stock('gtk-yes', Gtk.IconSize.BUTTON) |
241 |
self.check_dist.set_tooltip_text("") |
242 |
return 1 |
243 |
|
244 |
def on_step_menu_changed(self, *args): |
245 |
""" |
246 |
If the step menu is changed, recall previous 'sane' input for nsteps. |
247 |
""" |
248 |
_st = self.get_step_type() |
249 |
self.nsteps.set_text({ |
250 |
STEP_NUMBER:self.nsteps_number |
251 |
,STEP_INCREM:self.nsteps_increm |
252 |
,STEP_RATIO:self.nsteps_ratio |
253 |
}[_st]) |
254 |
_dist = self.dist.get_active_text() |
255 |
if _st==STEP_RATIO and _dist==DIST_LINEAR: |
256 |
self.set_dist(DIST_LOG) |
257 |
elif _st==STEP_INCREM and _dist==DIST_LOG: |
258 |
self.set_dist(DIST_LINEAR) |
259 |
self.validate_nsteps() |
260 |
|
261 |
def validate_nsteps(self): |
262 |
_st = self.get_step_type() |
263 |
_val = self.nsteps.get_text() |
264 |
if _st==STEP_RATIO: |
265 |
try: |
266 |
_fl = float(_val) |
267 |
except: |
268 |
_fl = None |
269 |
if _fl is None or _fl <= 0: |
270 |
self.taint_entry(self.nsteps,"Value must be positive") |
271 |
return 0 |
272 |
elif _st==STEP_INCREM: |
273 |
# will also handle the tainting, if required: |
274 |
return self.parse_entry(self.nsteps) |
275 |
elif _st==STEP_NUMBER: |
276 |
try: |
277 |
_int = int(float(_val)) |
278 |
except: |
279 |
_int = 0 |
280 |
if _val != _int or _int < 2: |
281 |
self.taint_entry(self.nsteps,"Number of steps must be positive integer >= 2") |
282 |
return 0 |
283 |
self.nsteps.set_text(_int) |
284 |
self.taint_entry(self.nsteps, good=1) |
285 |
|
286 |
def on_nsteps_changed(self, *args): |
287 |
self.validate_nsteps() |
288 |
setattr(self,("nsteps_number","nsteps_increm","nsteps_ratio")[self.get_step_type()],self.nsteps.get_text()) |
289 |
|
290 |
def on_check_toggled(self, *args): |
291 |
# update the preference for behaviour on solver fail |
292 |
_p = self.browser.prefs |
293 |
_p.setBoolPref("StudyReporter", "continue_on_fail", self.checkbutton.get_active()) |
294 |
|
295 |
def taint_entry(self, entry, good=0, msg=None): |
296 |
""" |
297 |
Color an input box red/white according to value of 'good'; add a further |
298 |
error message via the tooltip on the secondary icon, if provided. |
299 |
""" |
300 |
color = "white" |
301 |
if not good: |
302 |
color = "#FFBBBB" |
303 |
for s in [Gtk.StateType.NORMAL, Gtk.StateType.ACTIVE]: |
304 |
entry.modify_bg(s, Gdk.color_parse(color)) |
305 |
entry.modify_base(s, Gdk.color_parse(color)) |
306 |
# FIXME don't apply logic to hard-wired colour codes |
307 |
if not good: |
308 |
entry.set_property("secondary-icon-stock", 'gtk-dialog-error') |
309 |
else: |
310 |
entry.set_property("secondary-icon-stock", 'gtk-yes') |
311 |
|
312 |
# causes gtk-critical errors |
313 |
# entry.set_property("secondary-icon-tooltip-text", "") |
314 |
# entry.set_property("secondary-icon-tooltip-text", msg) |
315 |
|
316 |
def taint_dist(self, good=0, msg=None): |
317 |
""" |
318 |
Taint the distribution combobox, using the icon placed to its right. |
319 |
Add a message as tooltip on the error icon, if provided. |
320 |
""" |
321 |
if good: |
322 |
_icon = 'gtk-yes' |
323 |
else: |
324 |
_icon = 'gtk-dialog-error' |
325 |
self.check_dist.set_from_stock(_icon, Gtk.IconSize.BUTTON) |
326 |
if msg!=None: |
327 |
self.check_dist.set_tooltip_text(msg) |
328 |
|
329 |
def parse_entry(self, entry): |
330 |
""" |
331 |
Parse an input box and enforce dimensional agreement with self.instance. |
332 |
""" |
333 |
newtext = entry.get_text() |
334 |
##### CELSIUS TEMPERATURE WORKAROUND |
335 |
if self.convert: |
336 |
if len(newtext) > 0 and (len(newtext.split(" ")) == 1 or newtext.split(" ")[1] == CelsiusUnits.get_celsius_sign()): |
337 |
newtext = CelsiusUnits.convert_celsius_to_kelvin(newtext.split(" ")[0], str(self.instance.getType())) |
338 |
##### CELSIUS TEMPERATURE WORKAROUND |
339 |
# FIXME Add missing units if they have not been entered. |
340 |
i = RealAtomEntry(self.instance, newtext) |
341 |
_msg = None |
342 |
try: |
343 |
i.checkEntry() |
344 |
_value = i.getValue() |
345 |
except InputError, e: |
346 |
# FIXME does the following line actually clear out the entry box? |
347 |
_value = None |
348 |
_error = re.split('Input Error: ', str(e), 1) |
349 |
_msg = _error[1] |
350 |
|
351 |
if _value is not None: |
352 |
self.taint_entry(entry, good=1) |
353 |
else: |
354 |
self.taint_entry(entry, msg=_msg) |
355 |
return _value |
356 |
|
357 |
def solve(self): |
358 |
# we can assume that all the inputs have been validated |
359 |
_start = self.parse_entry(self.lowerb) |
360 |
_end = self.parse_entry(self.upperb) |
361 |
|
362 |
_dist = self.dist.get_active_text() |
363 |
_st = self.get_step_type() |
364 |
_step = None |
365 |
_nsteps = None |
366 |
if _dist==DIST_LINEAR: |
367 |
if _st==STEP_NUMBER: |
368 |
_nsteps = int(self.nsteps.get_text()) |
369 |
_step = (_end - _start)/(_nsteps) |
370 |
elif _st==STEP_INCREM: |
371 |
_step = self.parse_entry(self.nsteps) |
372 |
# TODO convert to real value without units? |
373 |
_nsteps = int((_end - _start)/_step) |
374 |
elif _dist==DIST_LOG: |
375 |
if _st==STEP_RATIO: |
376 |
_step = log(float(self.nsteps.get_text())) |
377 |
_nsteps = int((log(_end) - log(_start))/_step) |
378 |
elif _st==STEP_NUMBER: |
379 |
_nsteps = int(self.nsteps.get_text()) |
380 |
_step = (log(_end) - log(_start))/_nsteps |
381 |
|
382 |
if _step is None or _nsteps is None: |
383 |
raise RuntimeError("invalid step selection") |
384 |
|
385 |
_b = self.browser |
386 |
|
387 |
if not hasattr(self.browser,'solver'): |
388 |
_b.reporter.reportError("No solver assigned!") |
389 |
return |
390 |
|
391 |
if _b.no_built_system(): |
392 |
return |
393 |
_b.start_waiting("Solving with %s..." % _b.solver.getName()) |
394 |
self.studywin.destroy() |
395 |
reporter = StudyReporter(_b, _b.sim.getNumVars(), self.instance, _nsteps, self) |
396 |
|
397 |
# FIXME move following code to the StudyReporter class? |
398 |
i = 0 |
399 |
_val = _start |
400 |
while i <= _nsteps and reporter.guiinterrupt is False: |
401 |
# run a method, if requested |
402 |
if self.method: |
403 |
try: |
404 |
_b.sim.run(self.method) |
405 |
except RuntimeError,e: |
406 |
_b.reporter.reportError(str(e)) |
407 |
|
408 |
# set the value (do it inside the loop to avoid METHOD possibly unfixing) |
409 |
if self.instance.getType().isRefinedSolverVar(): |
410 |
# for solver vars, set the 'fixed' flag as well |
411 |
## FIXME this function seems to somehow be repeatedly parsing units: avoid doing that every step. |
412 |
self.instance.setFixedValue(_val) |
413 |
else: |
414 |
# what other kind of variable is it possible to study, if not a solver_var? integer? not suppported? |
415 |
self.instance.setRealValue(_val) |
416 |
|
417 |
#solve |
418 |
# FIXME where is the continue_on_fail thing? |
419 |
try: |
420 |
reporter.updateVarDetails(i) |
421 |
_b.sim.solve(_b.solver, reporter) |
422 |
except RuntimeError,e: |
423 |
_b.reporter.reportError(str(e)) |
424 |
|
425 |
i += 1 |
426 |
# any issue with accumulation of rounding errors here? |
427 |
if _dist==DIST_LOG: |
428 |
_val = exp(log(_start)+i*_step) |
429 |
else: |
430 |
_val = _start + i*_step |
431 |
|
432 |
if reporter.continue_on_fail == True: |
433 |
reporter.updateVarDetails(i) |
434 |
|
435 |
_b.stop_waiting() |
436 |
_b.modelview.refreshtree() |
437 |
|