JuliaWeBWorK.jl

(source) 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 shown 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 a Plots object into a pg file. Plots are encoded and embedded.
  • File allows for inclusion of images stored in files into a pg file. Images are encoded and embedded.
  • JuliaWeBWorK.QUESTIONS() creates a container for questions that can be easily pushed 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_onlyConstant
numbers_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.

source
JuliaWeBWorK.AbstractChoiceQType
 AbstractChoiceQ

The choices questions don't readily lend themselves to fit with the AbstractRandomizedQ setup, so the choice questions push randomization on to WeBWorK.

source
JuliaWeBWorK.AbstractQType

AbstractQ

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 throughhint`.

source
JuliaWeBWorK.AbstractRandomizedQType
AbstractRandomizedQ

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.

source
JuliaWeBWorK.INCLUDEMethod
INCLUDE(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")
source
JuliaWeBWorK.PAGEMethod
PAGE(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
source
JuliaWeBWorK.PlotMethod
Plot(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.

source
JuliaWeBWorK.essayqMethod
essayq(question; width=60, height=6)

WeBWorK allows for one essay question per page. These will be graded by the instructor.

source
JuliaWeBWorK.iframeFunction
iframe(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)
source
JuliaWeBWorK.jsxgraphMethod
jsxgraph(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.

source
JuliaWeBWorK.multiplechoiceqMethod
 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
source
JuliaWeBWorK.radioqFunction
radioq(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)
source
JuliaWeBWorK.randomizerMethod
randomizer(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)
source
JuliaWeBWorK.randomqFunction
randomq(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-flavored Markdown

    • 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. Use raw if dollar signs have no meaning.
    • References to randomized variables are through Mustache variables numbered sequentially {{:a1}}, {{:a2}}, {{:a3}}, ... up to 16 (by default).
  • ans_fn: the answer function is an n-ary function of the randomized parameters

  • random: 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.

source
JuliaWeBWorK.stringqFunction
stringq(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.

source
JuliaWeBWorK.yesnoqFunction
yesnoq(question, yes::Bool, r=(), solution="")

A question with non-computed answer "yes" (yes=true) or "no" (yes=false)

source
JuliaWeBWorK.@MT_strMacro
MT

Use <<...>> or <<{...}>> for substitution before randomimization substitution. Useful for plots

source

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...)