Part 6: Example Explorers#
An example explorer is a collection of Widgets designed to help users explore numerical examples of mathematical phenomena. You can find an introduction in this demo.
In Part 5: Doc Labels of this tutorial, we built a Proofscape deduction to represent the proof of the infinitude of primes, based on a version of that proof which we found in a PDF. We’ll now enrich our deduction by attaching an example explorer to one of its nodes.
A node deserving of enrichment#
Let’s start by reviewing the deduction we built in Part 5: Doc Labels.
Double-click Pf2
under the Primes
module in the “Structure” view in
the sidebar.
When the proof opens, find the node where we introduce the proof’s key construction, the number \(N\).
As we think about this construction, we might realize that it could motivate a nice little investigation. Instead of working under the reductio assumption active in the proof, that there be only finitely many primes, we could ask what happens in the real world, when we consider finite initial segments of the sequence of all prime numbers.
That is to say, we could investigate the numbers that are “primorials plus one” (a “primorial” being a factorial restricted to primes):
Are these numbers in fact always prime? The first three are.
We’ll build an annotation to help users explore this question. Whereas the annotations we built earlier in Part 2: Annotations were in some sense “standalone,” the one we build next will be explicitly attached to this node. This will allow users to discover it as an enrichment, from the node’s context menu. See Attachments.
Starting the annotation#
We’ll write our new annotation in the same source file where we defined the deduction.
You can open that now by right-clicking the same Pf2
you just double-clicked in
the sidebar, and selecting “Edit Source,” or you can right-click any node or deduction
in the chart panel and select the same.
When you’ve opened the source file, scroll to the bottom, and start a new annotation like this:
anno primorials_plus_one @@@
...
@@@
So far, this is nothing we haven’t seen before. However, we’ll now associate the annotation with the node we’re studying. In order to do that, we’ll need the node’s libpath. We could scroll up in the source file and locate the node’s definition, and determine the libpath that way, but there’s an easier method.
Right-click the tab of the chart panel, and select “Split and Move Right.”
Now both source panel and chart panel are visible.
Once again find the node of interest, where the number \(N\) is introduced in the
proof, right-click, and go to “Copy libpath.” In the tail selector that pops up
(we introduced tail selectors earlier in the tutorial),
move your mouse over the next-to-last libpath segment Pf2
, and then click,
to copy the tail Pf2.I30
to the clipboard.
You can now go back to the new anno definition, and add an on
clause, pasting
the copied libpath at the end:
anno primorials_plus_one on Pf2.I30 @@@
...
@@@
Important
Be sure you understand why we selected the libpath tail starting with Pf2
.
Since we are defining an annotation that is a sibling of the Pf2
deduction
(they have the same parent module, gh.example.tutorial.primes
),
we are working in a context where this relative libpath resolves correctly to
the node of interest.
Even though we haven’t yet written any actual contents in the new annotation, go ahead and build.
After the build completes, right-click the Pf2
deduction in the chart
panel, and select “Refresh links.”
This causes PISE to load any new enrichments – like our new annotation – that may have been built since the deduction was first opened. Having done this, we can now right-click the node of interest (where \(N\) is defined) one more time, and the context menu will now list our new annotation.
The on Pf2.I30
clause we added to the preamble of our annotation caused it to
be linked to the node in this way, as an available enrichment.
Building an example explorer#
We’ve started a new annotation, and linked it to a node, and we know what we want to do with it; namely, explore the numbers that are “primorials plus one,” and investigate the question of whether these numbers are in fact always prime or not.
In order to do that, we’re going to build an example explorer, using parameter and display widgets. See Examp Widgets in the reference manual, for the full details on how these widget types are coded.
Getting started with a parameter chooser#
We’ll start with a parameter widget to help the user select a prime number. Update your annotation to look like this:
anno primorials_plus_one on Pf2.I30 @@@
Under the *reductio* assumption of the proof, that there are only finitely many
primes, the number $N$ always turns out to be prime. But what happens in the real
world, where there are in fact infinitely many primes?
We'll investigate numbers of the form of $N$, and check whether they're actually
prime or not.
Start by picking a prime number $p$.
<param:eg1_p>[]{
ptype: "Prime",
tex: "p",
}
@@@
and build.
After the build completes, we should open the annotation we’re developing, so we can see how it’s progressing. We can of course open it from the “Structure” tab in the sidebar, as usual, but this time let’s instead take advantage of the fact that we linked it to a node in the chart. Right-click the target node, as we did earlier,
and this time click the name of our annotation in the context menu, in order to open it in a new notes panel.
Our annotation is shaping up nicely, with an introduction to explain its purpose, and a parameter “chooser panel” to help the user select a prime number \(p\). However, as you look over the chooser panel, you may feel that it offers too much. After the buttons offering all the primes less than 100, there is a dropdown offering all the three-digit primes, and even a text box in which even larger primes may be entered manually. Will we need to go so far? Maybe not.
Supposing we decide to start with something more modest, we can go back now and edit our
first param
widget, adding an upper bound:
anno primorials_plus_one on Pf2.I30 @@@
Under the *reductio* assumption of the proof, that there are only finitely many
primes, the number $N$ always turns out to be prime. But what happens in the real
world, where there are in fact infinitely many primes?
We'll investigate numbers of the form of $N$, and check whether they're actually
prime or not.
Start by picking a prime number $p$.
<param:eg1_p>[]{
ptype: "Prime",
tex: "p",
args: {
chooserUpperBound: 50,
}
}
@@@
After amending your param
widget as above, rebuild. The prime chooser panel now
offers a more limited selection.
See the reference on the Prime parameter type, for further details.
Building a display#
Now that we have a way to choose a prime number \(p\), we have to actually do something with it. This means it’s time to write a display widget. Display widgets are where you use parameter values to build mathematical displays.
As explained in the reference, we do this by writing Python code, using SymPy for mathematical calculations and symbolic manipulation. Our job is to build whatever string of HTML we want to be displayed in the page, and we can embed mathematical expressions in TeX syntax, to be typeset by MathJax.
Basic framework#
Before we write the Python code we want, however, let’s start by setting up a basic framework. Extend the annotation like this:
anno primorials_plus_one on Pf2.I30 @@@
Under the *reductio* assumption of the proof, that there are only finitely many
primes, the number $N$ always turns out to be prime. But what happens in the real
world, where there are in fact infinitely many primes?
We'll investigate numbers of the form of $N$, and check whether they're actually
prime or not.
Start by picking a prime number $p$.
<param:eg1_p>[]{
ptype: "Prime",
tex: "p",
args: {
chooserUpperBound: 50,
}
}
Now we can ask whether "$p$-primorial plus $1$" is a prime number or not,
where by "primorial" we mean a factorial featuring only prime factors.
<disp:>[]{
import: {
p: eg1_p,
},
build: """
html = f'<p>TO DO: Investigate $p$-primorial plus $1$ for $p = {p}$</p>'
return html
"""
}
@@@
We’ve added a bit of additional text after the parameter chooser, to explain to the user what
we plan to do, and we’ve started our disp
widget. Notice that we’ve used the import
field
in the disp
widget to import the value of the param
widget named eg1_p
under the
local variable p
. This will allow us to use the variable p
in the Python code we’re going
to write, to generate the display.
Mark as trusted#
Even though our disp
widget is still only a “stub,” go ahead and build now, in order to see
how the annotation page updates.
If your built page appears as in Fig. 5, with the display widget (blue box) showing its build code, instead of running that code to generate the desired display, don’t worry.
Because display widgets can contain arbitrary code, you have to formally trust the repo to which they belong, before they will run (except for repos for which trust is configred by default; see Trust settings). To do that, find the repo root module in the Sidebar, right-click it, and select “Trust…”.
Click “Grant trust” in the dialog that comes up.
Now that you’ve said you trust the repo we’re developing (at the WIP
or “work in progress” version),
the page should reload, and this time the display widget should run, generating the intended display,
as in Fig. 6.
Of course, this still isn’t the display we really want. In the build
field of our disp
widget, we’ve put some placeholder code, which is generating this simple “TO-DO” message, reminding
us of what we wanted to do.
However, you can already try clicking the buttons in the prime chooser panel, and see that the display box updates accordingly:
Building the desired display#
So far we’ve seen a nice, simple demonstration of how the display widget can use the value of the parameter widget, but it’s time now to build the display we actually want. We can do that using the Python code in Listing 22.
# Let M be p-primorial
primes = primerange(p + 1)
M = 1
for q in primes:
M *= q
# N = M + 1 is the number we want to investigate
N = M + 1
N_is_prime = isprime(N)
N_factors = None if N_is_prime else factorint(N)
def write_factorization(factors):
return r' \cdot '.join(
f'{b}^{{{e}}}' if e > 1 else str(b)
for b, e in factors.items()
)
def spell_out_primorial(Q):
primes = primerange(Q + 1)
s = ''
for k, q in enumerate(reversed(list(primes))):
if k == 0:
s += str(q)
elif k == 3 and q > 3:
s += r' \cdots 2'
break
else:
s += rf' \cdot {q}'
return s
# Now we can build our display HTML
simplify_M = f' = {M}' if M > 2 else ''
html = (
'$p$-primorial is:\n'
f'$$M = {spell_out_primorial(p)}{simplify_M}$$\n'
'Let $N$ be $p$-primorial plus $1$:\n'
f'$$N = M + 1 = {N}$$\n'
)
html += (
'<p>Is $N$ prime?</p>\n'
f'<p><b>Answer: </b>{"Yes" if N_is_prime else "No"}</p>\n'
)
if not N_is_prime:
html += f'$$N = {write_factorization(N_factors)}$$'
return html
In Listing 22 we have used the SymPy functions primerange(), isprime(), and factorint() to help us build the number \(p\)-primorial, to determine whether that number plus one is prime or not, and, if not, to produce a factorization of it. We’ve also defined a couple of custom functions to help us format things nicely, and we’ve built and returned the HTML string we want to see in the display.
Copy and paste this Python code now, replacing the existing value of the build
field in
the disp
widget, so that your annotation now looks like this:
anno primorials_plus_one on Pf2.I30 @@@
Under the *reductio* assumption of the proof, that there are only finitely many
primes, the number $N$ always turns out to be prime. But what happens in the real
world, where there are in fact infinitely many primes?
We'll investigate numbers of the form of $N$, and check whether they're actually
prime or not.
Start by picking a prime number $p$.
<param:eg1_p>[]{
ptype: "Prime",
tex: "p",
args: {
chooserUpperBound: 50,
}
}
Now we can ask whether "$p$-primorial plus $1$" is a prime number or not,
where by "primorial" we mean a factorial featuring only prime factors.
<disp:>[]{
import: {
p: eg1_p,
},
build: """
# Let M be p-primorial
primes = primerange(p + 1)
M = 1
for q in primes:
M *= q
# N = M + 1 is the number we want to investigate
N = M + 1
N_is_prime = isprime(N)
N_factors = None if N_is_prime else factorint(N)
def write_factorization(factors):
return r' \cdot '.join(
f'{b}^{{{e}}}' if e > 1 else str(b)
for b, e in factors.items()
)
def spell_out_primorial(Q):
primes = primerange(Q + 1)
s = ''
for k, q in enumerate(reversed(list(primes))):
if k == 0:
s += str(q)
elif k == 3 and q > 3:
s += r' \cdots 2'
break
else:
s += rf' \cdot {q}'
return s
# Now we can build our display HTML
simplify_M = f' = {M}' if M > 2 else ''
html = (
'$p$-primorial is:\n'
f'$$M = {spell_out_primorial(p)}{simplify_M}$$\n'
'Let $N$ be $p$-primorial plus $1$:\n'
f'$$N = M + 1 = {N}$$\n'
)
html += (
'<p>Is $N$ prime?</p>\n'
f'<p><b>Answer: </b>{"Yes" if N_is_prime else "No"}</p>\n'
)
if not N_is_prime:
html += f'$$N = {write_factorization(N_factors)}$$'
return html
"""
}
@@@
Then once again build, and observe how the page updates.
Finally, we’ve done enough to actually explore the question: Are primorials plus one always prime? By clicking the buttons in the prime chooser panel, you can explore different cases. What do you find?
Checking multiple cases at once#
Our first example explorer provides the user with a nice tool to begin examining primorials plus one, but, for more serious pattern seeking, having to click a button to check each number one at a time may be a bit slow. We’ll now provide a second explorer, enabling the user to examine many cases at once.
Extend your annotation by adding the following text at the end, and then build.
1Examining multiple cases at once can sometimes be a way to detect
2unexpected patterns. You can examine a range of primes $p$ below.
3
4<disp:>[]{
5 build: """
6 # BEGIN EDIT
7 # Check p-primorial + 1 for all primes p in the interval
8 # from a to b.
9 a, b = 0, 20
10 # END EDIT
11 html = '<pre>p, p-primorial + 1, prime or not (factorization below)</pre>\n'
12 html += '<pre>--------------------------------</pre>\n'
13 P = primerange(b)
14 M = 1
15 for p in P:
16 M *= p
17 if p >= a:
18 N = M + 1
19 ans = isprime(N)
20 html += f'<pre>{p}, {N}: {"" if ans else "not "}prime</pre>\n'
21 if not ans:
22 F = factorint(N)
23 html += f'<pre> {F}</pre>\n'
24 return html
25 """
26}
After building, you should see a new explorer at the end of the page.
Even though we only defined one display widget this time (no parameter widgets), we still
got both a yellow and a blue box. You can click in the yellow box, change the values of
the variables a
and b
, and then click the “hammer” button to re-run the display.
In order to understand where the yellow box came from, look again at
Listing 24, and notice lines 6 and 10 in particular,
which say # BEGIN EDIT
and # END EDIT
.
As explained in the reference, you can use this to place as
many editable sections into your display widgets’ build
field as you wish.
Exposing more code#
Question: Wouldn’t your users learn more if they could see all of the code, not just the editable section?
Answer: They can! They need only right-click the display widget, and they will get a “View source” option. (For you, it will say “Edit source.”)
While users can always view the full source code of any widget (disp
or any other type)
by right-clicking as just described, there may be cases in which you want them to have free
rein over all the code defining a disp
widget, right in the page.
To achieve that, you can always move the # END EDIT
line to the end, just before
the final return
statement that returns the HTML.
In the present case, that would look like this:
1Examining multiple cases at once can sometimes be a way to detect
2unexpected patterns. You can examine a range of primes $p$ below.
3
4<disp:>[]{
5 build: """
6 # BEGIN EDIT
7 # Check p-primorial + 1 for all primes p in the interval
8 # from a to b.
9 a, b = 0, 20
10
11 html = '<pre>p, p-primorial + 1, prime or not (factorization below)</pre>\n'
12 html += '<pre>--------------------------------</pre>\n'
13 P = primerange(b)
14 M = 1
15 for p in P:
16 M *= p
17 if p >= a:
18 N = M + 1
19 ans = isprime(N)
20 html += f'<pre>{p}, {N}: {"" if ans else "not "}prime</pre>\n'
21 if not ans:
22 F = factorint(N)
23 html += f'<pre> {F}</pre>\n'
24 # END EDIT
25 return html
26 """
27}
After making this change and building, you can see that the yellow box now exposes all of the code.
Summary#
You have learned how to:
Attach an annotation to a node, using an
on
clauseBuild a
param
widget, and customize its chooser panelImport the value of a
param
widget into adisp
widgetChange trust settings on a repo
Build an HTML display in a
disp
widgetMark a section of a
disp
widget’sbuild
code as user-editable