JuliaWeBWorK.jl
The JuliaWeBWorK
package is a means to author .pg
file for WeBWorK from a Julia
script.
Elements of a page
The script should call the package
using JuliaWeBWorK
The basic flow is a "page" is defined in the script which when show
n writes out the pg
text.
A page consists of
- an introduction
- questions
- metadata
A "page" is created by a call like:
p = Page(intro, qs; [context], [answer_context], meta...)
The show
method for a page writes out the page in pg
format for saving and uploading into WeBWorK
.
A page has a context
and answer_context
instructing WeBWorK
as to how it should process the student's answer. The value numbers_only
for answer_context
is used to turn off the simplification pass by WeBWorK
(so students answers like 2+2
are distinct from 4
).
An introduction
A introduction is just markdown text. Typically this is done in a raw
text block so that backslashes need not be escaped. However, it can be useful to interpolate Julia
values, in which case raw
would not be used.
Questions
Questions consist of a question and a means to grade student answers.
Questions come in a few different types:
randomq
Most questions can be randomized. As we expect answers to be computed using Julia
code, the resulting pg
file contains all possible combinations and WeBWorK
simply chooses a random index. (Hence, the number of possible random outcomes shouldn't be too big.)
The randomization is specified using a tuple of iterables, as in (1:5,)
or (1:5, 1:5)
. (Note the trailing comma in the first to make a tuple.) Randomization can be shared amongst questions using a randomizer
object.
Within a question, the randomized variables are referred to by Mustache
variables numbered {{:a1}}
, {{:a2}}
, etc. (upto 16).
The answer to be graded is computed by an n
-ary function with n
the number of randomized variables (0 to 16).
The randomq(question, answer_fn, randomizer; ...)
constructor allows this. This first example has no randomization (as specified by ()
for the third position argument).
using SpecialFunctions
randomq("What is the *value* of `airy(pi)`?", () -> airyai(pi), ())
JuliaWeBWorK.FixedRandomQ("13428534535398526724", "What is the *value* of `airy(pi)`?", "", 0.005089353479594496, 0.0001, false)
This example has randomization over two variables:
randomq("What is ``{{:a1}} + {{:a2}}``?", (a,b) -> a+b, (1:5, 1:5))
JuliaWeBWorK.RandomQ("14153604185152056358", (1:5, 1:5), Main.var"#3#4"(), "What is ``{{:a1}} + {{:a2}}``?", "", 0.0001, false)
The above two examples expect a numeric output. For the first, a tolerance would be expected. The numericq
constructor has the keyword argument tolerance
defaulting to 1e-4
for an absolute tolerance.
For students, answers have:
- scientific notation in answers must use an E (not
e
) Inf
is used for infinity
Other answer types than numbers can be specified:
Lists
Student answers can be comma separated lists of numbers. The List
function is used to specify the list.
question = raw"""What are the elements of ``\{1,2, {{:a1}} \}``?"""
answer(a) = List(1,2, a)
rnd = (3:5,)
randomq(question, answer, rnd)
JuliaWeBWorK.RandomQ("17433813610060648664", (3:5,), Main.answer, "What are the elements of ``\\{1,2, {{:a1}} \\}``?", "", 0.0001, false)
The keyword argument ordered::Bool
can be specified if the list should be in some specific order, otherwise these are graded as sets.
Intervals
An interval or list of intervals may be specified as an answer. When indicating an interval, we have Interval(a,b)
. This will match regardless of open or closed, except when infinities are involved.
question = raw"On what intervals is ``f(x)=(x+1) \cdot x \cdot (x-1)`` positive?"
answer() = List([Interval(-1, 0), Interval(1,Inf)])
rnd = ()
randomq(question, answer, rnd)
JuliaWeBWorK.FixedRandomQ("4972416264372034989", "On what intervals is ``f(x)=(x+1) \\cdot x \\cdot (x-1)`` positive?", "", List(Interval(-1, 0), Interval(1, infinity)), 0.0001, false)
stringq
To fill in from a limited set of strings, as computed by the possible range of the answer function over the random set.
Choice questions
Choice questions only have their selection of answer(s) randomized. The questions do not have any templated values for substitution, as randomq
questions may.
radioq
For multiple choice questions (1 of many). Also yesnoq(questions, answer::Bool)
The choices to choose from are specified as an iterable of choices. If that iterable contains nested iterables, those will be shuffled. The correct answer is specified by index relative to the flattened collection:
radioq("Pick \"three\"", ("one", "two","three"), 3) # none randomized
radioq("Pick \"three\"", (("one", "two","three"),), 3) # all randomized
radioq("Pick third", (("one", "two"),"three"), 3) # "three" at end
radioq("Pick third", (("one","two"), ("three", "four")), 3) # randomized each pair
multiplechoiceq
for multiple choice questions (1 or more of many) the answers should be a tuple of needed selections.
multiplechoiceq("Select all three", (raw"\\(1\\)", "**two**", "3"), (1,2,3)) # not randomised
multiplechoiceq("Some question", (("one","two","three"),"four"), 4) # first three randomized
multiplechoiceq("Some question", (("one","two","three"),("four","five")), (4,5)) # randomized first three, last two
Essayq
For longer form text answers that are graded individually. Only 1 per page is allowed.
Output only
A WeBWorK question has 3 possible places of inclusion: the answer, the question or what the student sees, and a solution. Sometimes just output is needed.
plotq
For randomized plots in a question, plotq
can be used to display the plots. Another question must be used to ask the question and gather the answer. The randomizer
must be used to share randomization between the two.
jsxgraph
The page can include interactive graphics using jsxgraph
. While not as interactive as the geogebra use within WeBWorK, this does allows interactive demonstrations. The JuliaWeBWorK.INCLUDE
declaration creates a function which can make working with separate .js
files easier within a script.
hint
A hint shows a little inline popup.
There are a few helpers for questions:
Plot
allows for inclusion of aPlots
object into apg
file. Plots are encoded and embedded.File
allows for inclusion of images stored in files into apg
file. Images are encoded and embedded.JuliaWeBWorK.QUESTIONS()
creates a container for questions that can be easilypush
ed onto via a pipe.letters = JuliaWeBWorK.letters()
creates a function,letters
which returns an incremented letter each time it is called. Useful to multi-part questions.- The
jmt
string macro allows interpolation using$
; does not need backslashes escaped; and parses to Mustache tokens.
Meta data
Each pg
file may have meta data in its contents. Such data is passed to the Page
constructor through keyword arguments. For example, the following could be splatted into the call to Page
.
meta = (
KEYWORDS = "Julia, WeBWorK",
DBChapter = "Sample questions",
DBSection = "section 1",
Section = "1",
Problem = "1"
)
Reference
JuliaWeBWorK.numbers_only
— Constantnumbers_only
Dictionary to pass to answer_context
to turn off WeBWorK's simplification pass. There is no means to turn this off per problem, only per page.
JuliaWeBWorK.AbstractChoiceQ
— Type AbstractChoiceQ
The choices questions don't readily lend themselves to fit with the AbstractRandomizedQ
setup, so the choice questions push randomization on to WeBWorK.
JuliaWeBWorK.AbstractOutputQ
— TypeAbstractOutputQ
Type for output only things (Plots, hint, label ...)
JuliaWeBWorK.AbstractQ
— TypeAbstractQ
A question has atleast two part: a question (marked up in julia-flavored markdown) and an answer, which is typically randomized. In WeBWorK, there are tpyically 3 places in the file where a question needs defintions: in the preamble the values are defined (written by create_answer
); between BEGIN_TEXT
and END_TEXT
the question is asked (written by show_answer
); and the grading area (written by show_answer). Hints can be added through
hint`.
JuliaWeBWorK.AbstractRandomizedQ
— TypeAbstractRandomizedQ
A question where randomization is done by creating an array of all possible values for the sample space in Julia
and having WeBWorK select one of the values. The randomizer
function can be used to share this random selection amongst questions.
JuliaWeBWorK.File
— MethodFile(p)
run Base64.base64encode
; wrap for inclusion into img
tag.
JuliaWeBWorK.INCLUDE
— MethodINCLUDE(DIR)
Returns a function that will includes the text of a file found relative to the specified directory (which would usually be @__DIR__
). Intended for use with jsxgraph
to keeps JavaScript files separate from .jl
files.
INCLUDE = JuliaWeBWorK.INCLUDE(@__DIR__)
INCLUDE("fname.js")
JuliaWeBWorK.LETTERS
— Methodreturn iterator over the letters (a)
, (b)
, ... Calling function increments letters
JuliaWeBWorK.MathObject
— MethodMathObject(r)
What type of MathObject to create? Defaults to "List", but "" (PlotQ
) or "String" (StringQ
) are useful.
JuliaWeBWorK.PAGE
— MethodPAGE(SCRIPTNAME)
Write a page to a file name based on the value of SCRIPTNAME
. Returns an anonymous function which can be called repeatedly to write a page with a filename based on SCRIPTNAME
.
This is designed to be used as PAGE = JuliaWeBWorK.PAGE(@__FILE__)
. Then from one script file several related pg
files can be generated. This might be useful for authoring exams where it is a good practice to have many separate problems and not one big one with many parts.
using JuliaWeBWorK
PAGE = write_page(@__FILE__)
q = numericq(raw"What is \({{:a1}} + {{:a2}}\)?", (a,b) -> a+b, (1:4, 2:5))
PAGE("Addition", (q,)) # writes to SCRIPT_BASE_NAME-1.pg
q = numericq(raw"What is \({{:a1}} - {{:a2}}\)?", (a,b) -> a-b, (1:4, 2:5))
PAGE("subtraction", (q,)) # writes to SCRIPT_BASE_NAME-2.pg
q = numericq(raw"What is \({{:a1}} * {{:a2}}\)?", (a,b) -> a*b, (1:4, 2:5))
PAGE("multiplication", (q,)) # writes to SCRIPT_BASE_NAME-3.pg
JuliaWeBWorK.Plot
— MethodPlot(p)
Convert plot to png
object; run Base64.base64encode
; wrap for inclusion into img
tag.
Works for Plots
, and would work for other graphing backends with a show(io, MIME("text/png"), p)
method.
JuliaWeBWorK.create_answer_partial
— Methodcreate_answer_partial
Ability to modify just part of the create_answer_tpl
for "AbstractRandomizedQ" for a given type. (e.g., StringQ
)
JuliaWeBWorK.essayq
— Methodessayq(question; width=60, height=6)
WeBWorK allows for one essay question per page. These will be graded by the instructor.
JuliaWeBWorK.hint
— Functionhint(text, tag="hint...")
Little inline popup. docs
JuliaWeBWorK.iframe
— Functioniframe(url, [alt]; [width], [height])
Embed the web page specified in url
in the page.
Example (from https://webwork.maa.org/wiki/IframeEmbedding1)
r = iframe("https://docs.google.com/presentation/d/1pk0FxsamBuZsVh1WGGmHGEb5AlfC68KUlz7zRRIYAUg/embed#slide=id.i0";
width=555, height=451)
JuliaWeBWorK.jsxgraph
— Methodjsxgraph(commands; domid="jxgbox", width=600, height=400)
Insert a graphic built using the jsxgraph javascript library.
The javascript commands below have a DOM id passed to initBoard
which is specified to domid
, with default of jxgbox
. This would need adjusting were two or more graphs in the same page desired.
Example (https://jsxgraph.uni-bayreuth.de/wiki/index.php/Drag_Polygons):
q = jsxgraph("""
var brd = JXG.JSXGraph.initBoard('jxgbox', {boundingbox: [-10, 10, 10, -10]});
var a = brd.create('point', [-2, 1]);
var b = brd.create('point', [-4, -5]);
var c = brd.create('point', [3, -6]);
var d = brd.create('point', [2, 3]);
var p = brd.create('polygon', [a, b, c, d], {hasInnerPoints: true});
"""; domid="jxgbox")
p = Page("Dragging polygons", (q,))
Most of the examples in the jsxgraph wiki work simply by copying the commands into a multi-line string, as in the example.
The site jsfiddle.net allows for easy testing of js code.
JuliaWeBWorK.label
— Methodlabel(text)
Add text area to a set or questions
Example
Click [here](www.google.com)
JuliaWeBWorK.multiplechoiceq
— Method multiplechoiceq(question, choices, answer; [instruction])
choices
A collection of answers. An answer may be a collection, in which case it will be shuffled.answer
: a tuple or vector of indices of the correct answers. The indices refer to the components stacked in random then fixed order.
Example:
multiplechoiceq("Select all three", (raw"\(1\)", "**two**", "3"), (1,2,3)) # not randomised
multiplechoiceq("Some question", (("one","two","three"),"four"), 4) # first three randomized
multiplechoiceq("Some question", (("one","two","three"),("four","five")), (4,5)) # randomized first three, last two
JuliaWeBWorK.numericq
— Functionnumericq
Alias for randomq
.
JuliaWeBWorK.radioq
— Functionradioq(question, choices, answer, [solution])
- choices. A collection of possible answers. These may be nested collections, in which case the second level is randomized
- answer. The index, within the flattened choices, of the answer (1-based)
Examples
radioq("Pick "three"", ("one", "two","three"), 3) # none randomized
radioq("Pick "three"", (("one", "two","three"),), 3) # all randomized
radioq("Pick third", (("one", "two"),"three"), 3) # "three" at end
radioq("Pick third", (("one","two"), ("three", "four")), 3) # randomized each pair
choices = ("one", "two","three")
radioq("Pick "three"", [choices], 3)
JuliaWeBWorK.randomizer
— Methodrandomizer(vars...)
A means to share the randomization across questions.
Example
qs = JuliaWeBWorK.QUESTIONS()
r = randomizer(1:3) |> qs
q1 = randomq("What is ``2-{{:a1}}?``", (a) -> 2-a, r) |> qs
q2 = randomq("What is ``3-{{:a1}}?``", (a) -> 3-a, r) |> qs
Page("test", qs)
JuliaWeBWorK.randomq
— Functionrandomq(question, ans_fn, random; solution, tolerance=1e-4, ordered=false)
Means to ask questions which are randomized within Julia
. The basic usage expects one or more numeric values as the answer. The answers may be randomized by specifying random parameter values and an answer function which returns the answer for the range of values specified by the randomized parameters. Besides numeric values, the Formula
type can be used to specify an expresion for the answer; the Interval
type can be used to specify one or more intervals for an answer (all intervals are assumed open).
The function numericq
is an alias.
Arguments:
question
is a string processed through julia-flavoredMarkdown
- LaTeX can be included: Use
\(, \)
for inline math and\[,\]
for display math. Alternatively, enclosing values in double back ticks indicates inline LaTeX markup, and the math literal block syntax ("math ...
") can be used for display math. - use regular markdown for other markup. Eg, code, bold, italics, sectioning, lists.
- The
jmt
string macro is helful to avoid escaping backslashes. It allows for string interpolation. Useraw
if dollar signs have no meaning. - References to randomized variables are through Mustache variables numbered sequentially
{{:a1}}
,{{:a2}}
,{{:a3}}
, ... up to 16 (by default).
- LaTeX can be included: Use
ans_fn
: the answer function is an n-ary function of the randomized parametersrandom
: the random parameters are specified by 0,1,2,or more (up
to 16) iterable objects (e.g., 1:5
or [1,2,3,5]
) combined in a tuple (grouped with parentheses; use (itr,)
if only 1 randomized parameter). Alternatively, a randomizer
object may be passed allowing shared randomization amongst questions.
The collection of all possible outputs for the given random parameterizations are generated and WeBWorK
selects an index from among them.
tolerance is an absolute tolerance, when the output is numeric.
ordered
is only for the case where the output is a list and you want an exact order
Examples
using SymPy, SpecialFunctions
# markdown
randomq("What is the *value* of `airy(pi)`?", () -> airyai(pi), ())
# latex via back ticks
randomq("What is ``{{:a1}} + {{:a2}}``?", (a,b) -> a+b, (1:5, 1:5))
randomq("What is ``{{:a1}}*{{:a2}}+{{:a3}}``?", (a,b,c) -> a*b+c, (1:5, 1:5,1:5))
# latex via \(, \)
randomq(raw"What is \({{:a1}}\cdot{{:a2}} + {{:a3}}\)?", (a,b,c) -> a*b+c, (1:5, 1:5,1:5))
randomq("Estimate from your graph the \(x\)-intercept.", ()-> 2.3, (); tolerance=0.5)
## Dispaly math
randomq("What is \[ \infty \]?", () -> Inf, ())
randomq("What is \( {1,2,{{:a1}} } \)?", (a) -> List(1,2,a), (3:6), ordered=true)
randomq("What is the derivative of \( \sin(x) \)?", () -> (@syms x; Formula(diff(sin(x),x))), ())
Plots may be included in different manners (see the example), but typically include via the Plot
function as follows:
using Plots
p = plot(sin, 0, 2pi);
plot!(zero);
q = randomq("![A Plot]($(Plot(p))) This is a plot of ``sin`` over what interval?", ()->Interval(0, 2pi),())
Plots may be randomized too. See Plot
, though they will not show in a hard copy.
!! note "TODO" Should consolidate arguments to cmp
(tolerance
, ordered
) For Interval
types, may need to set the context.
JuliaWeBWorK.stringq
— Functionstringq(question, answer, values)
Answer among limited set of strings. The strings available are all the possible outputs of answer
(a function) over all possible values in the sample space.
Examples:
q1 = stringq(raw"Is \({{:a1}} > 0\)? (yes/no)", (a) -> ("no","yes")[(a>0) + 1], (-3:3,))
q2 = stringq("Spell out {{:a1}}", (a) -> ("one","two","three")[a], (1:3,))
!!! Note: Using yes/no
or true/false
is common, so for these cases all 4 names are available, even if some do not appear in the collection of all possible outputs.
!!! Note: If the answers don't include all likely choices, then the student will not have the option of choosing the distractors.... This is not so great.
JuliaWeBWorK.yesnoq
— Functionyesnoq(question, yes::Bool, r=(), solution="")
A question with non-computed answer "yes" (yes=true) or "no" (yes=false)
JuliaWeBWorK.@MT_str
— MacroMT
Use <<...>>
or <<{...}>>
for substitution before randomimization substitution. Useful for plots
Example
A full example script might look like the following:
using JuliaWeBWorK
meta = (
KEYWORDS = "Sample questions",
)
qs = JuliaWeBWorK.QUESTIONS()
letters = JuliaWeBWorK.LETTERS()
intro = """
![WeBWorK](https://webwork.maa.org/images/webwork_logo.svg)
A simple page.
"""
numericq("$(letters()) What is ``{{:a1}} + 2``?",
(a) -> a + 2, (1:3,)) |> cs
radioq("$(letters()) Which is better?",
("*Dark* chocolate", "*White* chocolate"), 1) |> qs
p = Page(intro, qs; meta...)