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 vib3
directory.
Inspecting function signatures.
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
.
Generating the model.
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.)
Generating the view.
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).
Generating the template.
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 the read 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_forms.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>
A somewhat generic solution for rendering the results can be
provided by default. In the file templates/view_results_default.html
we
offer a code that checks if results
is a string ending in '.png'
or other typical file extensions for HTML images, and then writes out
the code for an image, and otherwise a string version of the results
object is dumped to the web page:
<p>
{% if result != None %}
{% if type(result) == type("") and
result[:-4] == '.png' or result[:-4] == '.gif' or
result[:-4] == '.jpg' %}
<img src="{{ result }}">
{% else %}
{{ str(result) }}
{% endif %}
{% endif %}
</p>
The index
function must combine the two pieces of templates.
The complete, generic form of this function is then
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
# Concatenate view_forms.html and view_results.html
forms_html = os.path.join('templates', 'view_forms.html')
results_html = os.path.join('templates', 'view_results.html')
view_html = os.path.join('templates', 'view.html')
f_forms = open(forms_html, 'r')
f_res = open(results_html, 'r')
f_view = open(view_html, 'w')
f_view.write(f_forms.read() + f_res.read())
f_forms.close(); f_res.close(); f_view.close()
return render_template(os.path.basename(view_html),
form=form, result=result)
if __name__ == '__main__':
app.run(debug=True)
Application. 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))
if not os.path.isdir('static'):
os.mkdir('static')
else:
# Remove old plot files
for filename in glob.glob(os.path.join('static', '*.png')):
os.remove(filename)
# Use time since Jan 1, 1970 in filename in order make
# a unique filename that the browser has not chached
t = str(time.time())
plotfile1 = os.path.join('static', 'density_%s.png' % t)
plotfile2 = os.path.join('static', 'cumulative_%s.png' % t)
plt.savefig(plotfile1)
y = gamma_cumulative(x, a, h, A)
plt.figure()
plt.plot(x, y)
plt.grid(True)
plt.savefig(plotfile2)
return plotfile1, plotfile2, mean, stdev
The compute_gamma
function returns a tuple of four values.
We want output as displayed in Figure 13.
Figure 13: Design of a web page illustrating the gamma probability functions.
The design is realized in the file view_results.html
shown below.
{% if result != None %}
<p>
<table>
<tr>
<td><img src="{{ result[0] }}" width="400"></td>
<td><img src="{{ result[1] }}" width="400"></td>
</tr>
<tr><td>
Mean value: {{ result[2] }} <br>
Standard deviation value: {{ result[3] }}
</td></tr>
</table>
</p>
{% endif %}
To create the web application, we just copy the generic controller.py
,
model.py
,
and templates/view_forms.html
files, add the
application-specific
compute.py
and templates/view_results.html
files given above,
and write one line in
model.py
:
from compute import compute_gamma as compute
That's it! Running controller.py
file starts the app.