We shall now present generic model.py
and controller.py
files that work with any compute
function (!). This example will
demonstrate some advanced, powerful features of Python. The source code
is found in the gen
directory.
The basic idea is that the Python module inspect
can be used to
retrieve the names of the arguments and the default values of
keyword arguments of any given compute
function. Say we have some
def mycompute(A, m=0, s=1, w=1, x_range=[-3,3]):
...
return result
Running
import inspect
arg_names = inspect.getargspec(mycompute).args
defaults = inspect.getargspec(mycompute).defaults
leads to
arg_names = ['A', 'm', 's', 'w', 'x_range']
defaults = (0, 1, 1, [-3, 3])
We have all the argument names in arg_names
and
defaults[i]
is the default value of keyword argument
arg_names[j]
, where j = len(arg_names) - len(defaults) + i
.
Knowing the name name
of some argument in the compute
function, we can make the corresponding class attribute
in the InputForm
class by
setattr(InputForm, name, FloatForm())
For name equal to 'A'
this is the same as hardcoding
class InputForm:
A = FloatForm()
Assuming that all arguments in compute
are floats, we could
do
class InputForm:
pass # Empty class
arg_names = inspect.getargspec(mycompute).args
for name in arg_names:
setattr(InputForm, name, FloatForm())
However, we can do better than this: for
keyword arguments the type of the default value can be used to
select the appropriate form class. The complete model.py
file
then goes as follows:
"""
Example on generic model.py file which inspects the arguments
of the compute function and automatically generates a relevant
InputForm class.
"""
import wtforms
from math import pi
from compute import compute_gamma as compute
import inspect
arg_names = inspect.getargspec(compute).args
defaults = inspect.getargspec(compute).defaults
class InputForm(wtforms.Form):
pass
# Augment defaults with None elements for the positional
# arguments
defaults = [None]*(len(arg_names)-len(defaults)) + list(defaults)
# Map type of default to right form field
type2form = {type(1.0): wtforms.FloatField,
type(1): wtforms.IntegerField,
type(''): wtforms.TextField,
}
for name, value in zip(arg_names, defaults):
if value is None:
setattr(InputForm, name, wtforms.FloatField(
validators=[wtforms.validators.InputRequired()]))
else:
if type(value) in type2form:
setattr(InputForm, name, type2form[type(value)](
default=value,
validators=[wtforms.validators.InputRequired()]))
else:
raise TypeError('argument %s %s not supported' %
name, type(value))
if __name__ == '__main__':
for item in dir(InputForm):
if item in arg_names:
print item, getattr(InputForm, item)
(The compute_gamma
function imported from compute
is the
only application-specific statement in this code and will be explained later.)
The call to compute
in the controller.py
file must also be expressed
in a general way such that the call handles any type and number of
parameters. This can be done in two ways, using either positional
or keyword arguments.
The technique with positional arguments
is explained first. It consists of collecting all parameters in
a list or tuple, called args
, and then calling compute(*args)
(which is equivalent to compute(args[0], args[1], ..., args[n])
if n
is len(args)-1
). The elements of args
are the values of the
form variables. We know the name of a form variable as a string
name
(from arg_names
), and if form
is the form object,
the construction getattr(form, name).data
extracts the value
that the user provided (getattr(obj, attr)
gets the attribute, with name
available as a string in attr
, in the object obj
).
For exampe, if name
is 'A'
, getattr(form, name).data
is the same as
form.A.data
.
Collecting all form variables, placing them in a list,
and calling compute
are done with
arg_names = inspect.getargspec(compute).args
args = [getattr(form, name).data for name in arg_names]
result = compute(*args)
Our InputForm
class guarantees that all arguments in compute
are present in the form, but to be absolutely safe we can
test if name
is present in the form
object:
args = [getattr(form, name).data for name in arg_names
if hasattr(form, name)]
A potential problem with the args
list is that the values might
be in wrong order. It appears, fortunately, that the order we
assign attributes to the form class is preserved when iterating over
the form. Nevertheless, using keyword arguments instead of positional
arguments provides a completely safe solution to calling compute
with the correct arguments. Keyword arguments are placed in a
dictionary kwargs
and compute
is called as compute(**kwargs)
.
The generic solution is
kwargs = {name: getattr(form, name).data for name in arg_names
if hasattr(form, name)}
result = compute(**kwargs)
The compute(**kwargs)
call is equivalent to compute(A=1, b=3, w=0.5)
in case kwargs = {'w'=0.5, 'A':1, 'b':3}
(recall that the order of
the keys in a Python dictionary is undetermined).
It remains to generate the right HTML template. The HTML code depends
on what the returned result
object from compute
contains. Only a
human who has read the compute
code knows the details of the returned
result. Therefore, we leave it to a human to provide the part
of the HTML template that renders the result. The file templates/view_results.html
contains this human-provided code, while templates/view.html
is a completely generic template for the forms:
<form method=post action="">
<table>
{% for field in form %}
<tr><td>{{ field.name }}</td> <td>{{ field }}</td>
<td>{% if field.errors %}
<ul class=errors>
{% for error in field.errors %}
<li>{{ error }}</li>
{% endfor %}</ul>
{% endif %}</td></tr>
{% endfor %}
</table>
<p><input type=submit value=Compute></form></p>
{% if result != None %}
{{ result|safe }}
{% endif %}
At the end of this code, an HTML text result
(string) is to be
inserted. This text is typically generated by calling Flask's
render_template
function, which uses templates/view_results.html
to turn the return object result
from the compute function into the
desired HTML code:
def index():
...
if result:
result = render_template('view_results.html', result=result)
# result is now rendered HTML text
return render_template('view.html', form=form, result=result)
view_forms.html
file and a user-specific
view_results.html
and explicitly combining them into a new
file. This requires file writing by the app, which one normally
wants to avoid. Especially if the web app gets multiple users,
the file writing may lead to corrupt files.
The complete, generic form of the index
function becomes
def index():
form = InputForm(request.form)
if request.method == 'POST' and form.validate():
arg_names = inspect.getargspec(compute).args
kwargs = {name: getattr(form, name).data
for name in arg_names if hasattr(form, name)}
result = compute(**kwargs)
else:
result = None
if result:
# result must be transformed to HTML and inserted as a
# string in the generic view.html file
result = render_template('view_results.html', result=result)
return render_template('view.html', form=form, result=result)
if __name__ == '__main__':
app.run(debug=True)
Let us apply the files above to plot the gamma probability density function
$$ g(x; a, h, A) = \frac{|h|}{\Gamma(a)A}\left(\frac{x}{A}\right)^{ah-1}
e^{-\left(\frac{x}{A}\right)^h},
$$
and its cumulative density
$$ G(x; a, h, A) = \int_0^x g(\tau; a, h, A)d\tau,$$
computed by numerically the Trapezoidal rule, for instance.
We also want to compute and display
the mean value \( A\Gamma(a + 1/h)/\Gamma(a) \) and
standard deviation
$$ \sigma = \frac{A}{\Gamma(a)}\sqrt{\Gamma(a + 2/h)\Gamma(a) - \Gamma(a+1/h)^2}.$$
Here, \( \Gamma(a) \) is the gamma function, which can be computed
by math.gamma(a)
in Python.
Below is a compute.py
file with the
relevant implementations of \( g(x;a,h,A) \) (gamma_density
),
\( G(x; a, h, A) \) (gamma_cumulative
), and a function compute_gamma
for
making a plot of \( g \) og \( G \) for \( x\in [0,7\sigma] \).
def gamma_density(x, a, h, A):
# http://en.wikipedia.org/wiki/Gamma_distribution
xA = x/float(A)
return abs(h)/(math.gamma(a)*A)*(xA)**(a*h-1)*exp(-xA**h)
def gamma_cumulative(x, a, h, A):
# Integrate gamma_density using the Trapezoidal rule.
# Assume x is array.
g = gamma_density(x, a, h, A)
r = zeros_like(x)
for i in range(len(r)-1):
r[i+1] = r[i] + 0.5*(g[i] + g[i+1])*(x[i+1] - x[i])
return r
def compute_gamma(a=0.5, h=2.0, A=math.sqrt(2), resolution=500):
"""Return plot and mean/st.dev. value of the gamma density."""
gah = math.gamma(a + 1./h)
mean = A*gah/math.gamma(a)
stdev = A/math.gamma(a)*math.sqrt(
math.gamma(a + 2./h)*math.gamma(a) - gah**2)
x = linspace(0, 7*stdev, resolution+1)
y = gamma_density(x, a, h, A)
plt.figure() # needed to avoid adding curves in plot
plt.plot(x, y)
plt.title('a=%g, h=%g, A=%g' % (a, h, A))
# Make Matplotlib write to BytesIO file object and grab
# return the object's string
from io import BytesIO
figfile = BytesIO()
plt.savefig(figfile, format='png')
figfile.seek(0) # rewind to beginning of file
import base64
figdata_density_png = base64.b64encode(figfile.getvalue())
figfile = BytesIO()
plt.savefig(figfile, format='svg')
figfile.seek(0)
figdata_density_svg = '<svg' + figfile.getvalue().split('<svg')[1]
figdata_density_svg = unicode(figdata_density_svg,'utf-8')
y = gamma_cumulative(x, a, h, A)
plt.figure()
plt.plot(x, y)
plt.grid(True)
figfile = BytesIO()
plt.savefig(figfile, format='png')
figfile.seek(0)
figdata_cumulative_png = base64.b64encode(figfile.getvalue())
figfile = BytesIO()
plt.savefig(figfile, format='svg')
figfile.seek(0)
figdata_cumulative_svg = '<svg' + figfile.getvalue().split('<svg')[1]
figdata_cumulative_svg = unicode(figdata_cumulative_svg,'utf-8')
return figdata_density_png, figdata_cumulative_png, \
figdata_density_svg, figdata_cumulative_svg, \
'%.2f' % mean, '%.2f' % stdev
The compute_gamma
function returns a tuple of six values.
We want output as displayed in Figure 10.
The design is realized in the file view_results.html
shown below.
<p>
<table>
<tr>
<td>
<img src="data:image/png;base64,{{ result[0] }}" width="400">
</td><td>
<img src="data:image/png;base64,{{ result[1] }}" width="400">
</td></tr>
<tr>
<td>{{ result[2]|safe }}</td>
<td>{{ result[3]|safe }}</td>
</tr>
<tr><td>
Mean value: {{ result[4] }} <br>
Standard deviation value: {{ result[5] }}
</td></tr>
</table>
</p>
To create the web application, we just perform the following steps:
controller.py
and model.py
files to a new directorycompute.py
controller.py
and model.py
to use the right name of the
compute function (from compute import name as compute
)templates/view_forms.html
file that visualizes
the returned value results
from the compute function