Skip to content

Commit 592f978

Browse files
authored
Merge pull request #651 from SasView/fix-use-webview-for-compare
Allow sasmodels.compare to use bumps webview instead of wx
2 parents e8a781e + 9073311 commit 592f978

1 file changed

Lines changed: 77 additions & 27 deletions

File tree

sasmodels/compare.py

Lines changed: 77 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,14 @@
2525
On Windows you will need to remove the quotes.
2626
"""
2727

28-
2928
import datetime
3029
import math
3130
import os
3231
import re
3332
import sys
3433
import traceback
35-
36-
# pylint: disable=unused-import
37-
from typing import Callable
34+
from dataclasses import dataclass
35+
from typing import TYPE_CHECKING, Protocol
3836

3937
import numpy as np # type: ignore
4038

@@ -44,16 +42,13 @@
4442
from .generate import FLOAT_RE, set_integration_size
4543
from .modelinfo import ModelInfo, ParameterSet
4644

47-
try:
48-
# With python 3.8+ we can indicate that calculator takes floats.
49-
from typing import Protocol
50-
class Calculator(Protocol):
51-
"""Kernel calculator takes *par=value* keyword arguments."""
52-
def __call__(self, **par: float) -> np.ndarray: ...
53-
except ImportError:
54-
#: Kernel calculator takes *par=value* keyword arguments.
55-
Calculator = Callable[..., np.ndarray]
56-
# pylint: enable=unused-import
45+
# Only use optional bumps dependency for type checking
46+
if TYPE_CHECKING:
47+
from bumps.names import Parameter
48+
49+
class Calculator(Protocol):
50+
"""Kernel calculator takes *par=value* keyword arguments."""
51+
def __call__(self, **par: float) -> np.ndarray: ...
5752

5853
USAGE = """
5954
usage: sascomp model [options...] [key=val]
@@ -108,7 +103,8 @@ def __call__(self, **par: float) -> np.ndarray: ...
108103
-profile shows the sld profile if the model has a plottable sld profile
109104
110105
=== output options ===
111-
-edit starts the parameter explorer
106+
-edit starts the parameter explorer (bumps webview)
107+
-wxedit starts the parameter explore (bumps wxgui)
112108
-help/-html shows the model docs instead of running the model
113109
114110
=== help ===
@@ -1039,7 +1035,7 @@ def plot_models(opts, result, limits=None, setnum=0):
10391035
'half', 'fast', 'single', 'double', 'single!', 'double!', 'quad!',
10401036

10411037
# Output options
1042-
'help', 'html', 'edit',
1038+
'help', 'html', 'edit', 'wxedit',
10431039

10441040
# Help options
10451041
'h', '?', 'models', 'models='
@@ -1194,7 +1190,7 @@ def parse_opts(argv):
11941190
'show_pars' : False,
11951191
'show_hist' : False,
11961192
'rel_err' : True,
1197-
'explore' : False,
1193+
'explore' : None,
11981194
'zero' : False,
11991195
'html' : False,
12001196
'title' : None,
@@ -1263,7 +1259,8 @@ def parse_opts(argv):
12631259
elif arg == '-single!': opts['engine'] = 'single!'
12641260
elif arg == '-double!': opts['engine'] = 'double!'
12651261
elif arg == '-quad!': opts['engine'] = 'quad!'
1266-
elif arg == '-edit': opts['explore'] = True
1262+
elif arg == '-edit': opts['explore'] = 'web'
1263+
elif arg == '-wxedit': opts['explore'] = 'wx'
12671264
elif arg == '-weights': opts['show_weights'] = True
12681265
elif arg == '-profile': opts['show_profile'] = True
12691266
elif arg == '-html': opts['html'] = True
@@ -1568,23 +1565,54 @@ def explore(opts):
15681565
"""
15691566
explore the model using the bumps gui.
15701567
"""
1568+
from bumps.names import FitProblem # type: ignore
1569+
1570+
model = Explore(opts)
1571+
problem = FitProblem(model)
1572+
name = ":".join(opts['name']) if opts['name'][0] == opts['name'][1] else opts['name'][0]
1573+
problem.name = name
1574+
1575+
if opts['explore'] == 'wx':
1576+
wx_explore(problem)
1577+
else:
1578+
webview_explore(problem)
1579+
1580+
def webview_explore(problem):
1581+
import logging
1582+
1583+
import bumps.webview.server.webserver as server
1584+
from aiohttp import web
1585+
from bumps.webview.server import api
1586+
from bumps.webview.server.cli import BumpsOptions
1587+
1588+
logging.getLogger("webview").setLevel(logging.WARNING)
1589+
1590+
server.init_web_app()
1591+
server.app.on_startup.append(lambda App: api.set_problem(problem))
1592+
options = BumpsOptions()
1593+
# CRUFT: options.threads does not have a default value
1594+
if not hasattr(options, 'threads'):
1595+
options.threads = False
1596+
sock = server.setup_app(options=options)
1597+
web.run_app(server.app, sock=sock)
1598+
1599+
def wx_explore(problem):
15711600
import wx # type: ignore
15721601
from bumps.gui import signal
15731602
from bumps.gui.app_frame import AppFrame # type: ignore
1574-
from bumps.names import FitProblem # type: ignore
15751603

15761604
is_mac = "cocoa" in wx.version()
15771605
# Create an app if not running embedded
15781606
app = wx.App() if wx.GetApp() is None else None
1579-
model = Explore(opts)
1580-
problem = FitProblem(model)
15811607
frame = AppFrame(parent=None, title="explore", size=(1000, 700))
15821608
if not is_mac:
15831609
frame.Show()
15841610
frame.panel.set_model(model=problem)
15851611
frame.panel.Layout()
15861612
frame.panel.aui.Split(0, wx.TOP)
1613+
# Hack to revert parameters to initial values: override the reload model tool
15871614
def _reset_parameters(event):
1615+
model = next(problem.models)
15881616
model.revert_values()
15891617
signal.update_parameters(problem)
15901618
frame.Bind(wx.EVT_TOOL, _reset_parameters, frame.ToolBar.GetToolByPos(1))
@@ -1594,18 +1622,23 @@ def _reset_parameters(event):
15941622
if app:
15951623
app.MainLoop()
15961624

1625+
@dataclass
15971626
class Explore:
15981627
"""
15991628
Bumps wrapper for a SAS model comparison.
16001629
16011630
The resulting object can be used as a Bumps fit problem so that
16021631
parameters can be adjusted in the GUI, with plots updated on the fly.
16031632
"""
1633+
pars: dict[str, "Parameter"]
1634+
name = "sasmodels"
1635+
16041636
def __init__(self, opts):
16051637
# type: (Dict[str, Any]) -> None
16061638
from bumps.cli import config_matplotlib # type: ignore
16071639

16081640
from . import bumps_model
1641+
16091642
config_matplotlib()
16101643
self.opts = opts
16111644
opts['pars'] = list(opts['pars'])
@@ -1673,19 +1706,36 @@ def plot(self, view='log'):
16731706
"""
16741707
Plot the data and residuals.
16751708
"""
1709+
import matplotlib.pyplot as plt
1710+
16761711
pars = dict((k, v.value) for k, v in self.pars.items())
16771712
pars.update(self.pd_types)
16781713
self.opts['pars'][0] = pars
16791714
if not self.fix_p2:
16801715
self.opts['pars'][1] = pars
1681-
result = run_models(self.opts)
1716+
try:
1717+
result = run_models(self.opts)
1718+
except Exception as exc:
1719+
print("Exception %s while evaluating"%(exc.__class__.__name__))
1720+
import traceback
1721+
traceback.print_exc()
1722+
plt.clf()
1723+
return
16821724
limits = plot_models(self.opts, result, limits=self.limits)
1683-
if self.limits is None:
1684-
vmin, vmax = limits
1685-
self.limits = vmax*1e-7, 1.3*vmax
1686-
import pylab
1687-
pylab.clf()
1725+
1726+
is_sf = self.opts['info'][0].structure_factor
1727+
if 0 and self.limits is None:
1728+
# Set limits based on first plot
1729+
if is_sf:
1730+
self.limits = 0.01, 4
1731+
else:
1732+
vmin, vmax = limits
1733+
self.limits = vmax*1e-7, 1.3*vmax
1734+
plt.clf()
16881735
plot_models(self.opts, result, limits=self.limits)
1736+
if is_sf:
1737+
plt.xscale('linear')
1738+
plt.yscale('linear')
16891739

16901740

16911741
def main(*argv):

0 commit comments

Comments
 (0)