WeBWorK Problems

Creating a special context?

Creating a special context?

by Joel Therrien -
Number of replies: 3
Hello,

After having spent many years authoring problems for some electrical engineering courses I have taught, I decided it was time to try and make the process easier and better looking (so immediately, you probably know that I so far have accomplished neither of those goals!).

There are a number of functions and constants that I use in these courses (I will focus on the quantum mechanics course I am writing problems for currently) which I don't wish to redefine for each and every problem. Therefore I figured this would be the perfect time to try making my own specialized context, which I titled 'Quantum'. I will show the 'contextQuantum.pl' file I created and show a test problem below.

I put the context file in the macros directory for my course and you can see that I load it and call it in the problem without that generating any errors. Though I did note that even if the file or context is spelled wrong, webwork does not throw any errors, so that isn't saying much by itself.

Now if I try to use a constant, say h_bar, directly in the problem, for example:

$test = h_bar;

Printing $test in the problem text just displays "h_bar" and not the value. But if I use Compute(h_bar); then the number comes through. But the built in constants, such as "pi" work just fine as one would expect.

Similar issue for functions, they only evaluate when called within functionParser(). I put comments next to the different scenarios I tried and what errors I got when processing the test.pg file

Any guidance would be greatly appreciated. Though there is ample documentation on writing your own contexts, there doesn't seem to be much out there on having this reside in it's own contextName.pl file, or not much that I could find. Github is a nice resource to see how others treat this, but I found a wide variation in styles, which makes it hard to figure out what is or isn't necessary to get this to work. And apologies in advance for rookie Perl mistakes; this is my first time diving this deep into it!

Here is the test.pg file I am using:

## DBsubject('Quantum Mechanics')
## DBchapter('Exam')
## DBsection('Wavefunctions')
## Date('03/18/2014')
## Author('Joel Therrien')
## Institution('UML')


#############################################
#  Initialization

DOCUMENT();

loadMacros(
"PGstandard.pl",
"MathObjects.pl",
"AnswerFormatHelp.pl",
"PGgraphmacros.pl",
"unionTables.pl",
"parserFunction.pl",
"contextQuantum.pl"
);

TEXT(beginproblem());

$refreshCachedImages = 1;


##############################################
#  Setup

Context("Quantum");

$tst = context::Quantum::Tunneling(1,2); # This one works just fine

# $tst = Compute(Tunneling(1,2)); # This one throws the following error: Undefined subroutine &main::Tunneling called at line 35 of (eval 2857)

parserFunction( "fTest(t)" => "context::Quantum::Tunneling(1,t)"); #This one throws the following error: 'context' is not defined in this context;

parserFunction( "fTest(t)" => "Tunneling(1,t)"); # This one throws the following error: Can't locate object method "weaken" via package "context::Quantum::Tunneling"

$tst2 = fTest(2); 

#############################################
#  Main text

Context()->texStrings;
BEGIN_TEXT

test

Test 1 = $tst

test 2 = $tst2

END_TEXT
Context()->normalStrings;

###############################################
#  Answer evaluation

$showPartialCorrectAnswers = 1;

############################
#  Solution

ENDDOCUMENT(); 

Here is the contextQuantum.pl file:

##########################################################################
#
# Hopefully this will become a library of quantum functions and constants
#

loadMacros("MathObjects.pl");

my $h = 6.6E-34; #Planck's constant
my $h_bar = 1.05E-34; #Planck's reduced constant
my $m_e = 9.11E-31; #electron rest mass

sub _contextQuantum_init {
my $context = $main::context{Quantum} = Parser::Context->getCopy("Numeric");

       $context->flags->set(
                            zeroLevel => 1E-50,
                            zeroLevelTol => 1E-50
 );
       $context->constants->add('h' => {value => $h, TeX => '\boldsymbol{h}'},
   'hbar' => {value => $h_bar, Tex => '\boldsymbol{\hbar}'},
 'me' => {value => $m_e, TeX => '\boldsymbol{m_{e}}'}
   );
    $context->functions->add(
                                                              Tunneling => {class => 'context::Quantum::Tunneling'}
                                                  );
}

package context::Quantum;
our @ISA = qw(Parser::Function::numeric2); 

sub Tunneling {
  my ($n,$r) = @_; my $T = 1;
  $T=2.3*$n;
  return $T
}

package main;

1;

~~~~
In reply to Joel Therrien

Re: Creating a special context?

by Davide Cervone -
I applaud you for trying to make your own Context, and you have done very well, but have made a couple of mistakes. The first is that
    $context->functions->add(
        Tunneling => {class => 'context::Quantum::Tunneling'}
    );
should be
    $context->functions->add(
        Tunneling => {class => 'context::Quantum'}
    );
The class must be a Perl package name, not a Perl function name. The class in your case is context::Quantum. By setting the class to context::Quantum::Tunneling, you have implied that there is a package with that name, and when MathObjects tries to use that package, it runs into trouble (this is the error message about weaken since it tries to call the weaken method of the class you have specified for the function).

Here is the explanation of the errors you have received:

    $tst = context::Quantum::Tunneling(1,2); # This one works just fine
This is because you are calling the function directly from Perl. MathObjects are not involved here.

    $tst = Compute(Tunneling(1,2));
       # This one throws the following error: Undefined subroutine &main::Tunneling
This is because Perl first must call Tunneling(1,2) and then pass that value to Compute(). But there is no Perl function called Tunneling() (only one called context::Quantum::Tunneling(), and a MathObject one called Tunneling, but that is unknown to native Perl expressions), so Perl complains that it doesn't know what to do.

You may have meant to do

    $tst = Compute("Tunneling(1,2)");
which would have asked MathObjects to use its definition of Tunneling and create a value from that. (This would also fail because of the problem with how you added Tunneling to the context, but that is a different issue.)

    parserFunction( "fTest(t)" => "context::Quantum::Tunneling(1,t)");
       #This one throws the following error: 'context' is not defined in this context;
The parserFunction command defines a new MathObject function by using a MathObject formula. So the things that appear in the second string can only be things that are meaningfull in the current Context(), as that string will be parsed by MathObjects (via Compute()). The error is because context::Quantum::Tunneling is a Perl function, not a MathObjects one. When MathObject tries to parse the string you have given it, it identifies context as the first token to be analyzed, but since it has no definition for that, it gives the error message you report. It has correctly told you that this identifier is unknown to it, and so it can't create the function you are requesting.

    parserFunction( "fTest(t)" => "Tunneling(1,t)");
       # This one throws the following error:
             Can't locate object method "weaken" via package "context::Quantum::Tunneling"
This is a correct use of parserFunction(), since Tunneling is defined in the context. The problem is that it is not defined correctly, and so when MathObjects tries to use the definition, it fails (as described above).

    $tst2 = fTest(2); 
if the parserFunction() call succeeds, then this should also work, as parserFunction also defines the needed Perl function that calls the MathObject one so that the function you are defining can be used in both MathObject and Perl expressions.

So fixing the main problem with the class of the Tunneling function is the first thing you need to fix. There is also a second problem, however. The context::Quantum::Tunneling function is actually a method of a Perl object, and so the first parameter passed to it will be the object itself, not the values of $n and $r (these are the second and third arguments). So the definition should be

    sub Tunneling {
      my $self = shift;
      my ($n,$r) = @_;
      my $T=2.3*$n;
      return $T;
    }
or just
    sub Tunneling {
      shift; my ($n,$r) = @_;
      my $T=2.3*$n;
      return $T;
    }
Of course, the function can be simplified as
    sub Tunneling {
      shift; my ($n,$r) = @_;
      return 2.3*$n;
    }
but perhaps there is more to come with the function (what do I know of quantum tunneling?).

This allows the Tunneling subroutine to be used properly as a MathObject function. Note, however, that this will cause your first example to fail, since your direct call to the function doesn't include an object as its first parameter. You can correct that by using

    $tst = context::Quantum->Tunneling(1,2);
instead. This passes the class as the first parameter.

Hope that clears things up.

In reply to Davide Cervone

Re: Creating a special context?

by Joel Therrien -
Hi Davide,

This has been so very helpful! I can't thank you enough... funny how a small mistake can cause so much frustration!

I have a follow up question. One means that i found which seems to enable the use of "Tunneling()" directly in the pg code without needing to use compute (not that I'm against that), but in terms of making the code as readable as possible for the future me that will inevitably forget why I wrote the problem in a certain manner, I inserted the following line into the pg code:

sub Tunneling {Parser::Function->call('Tunneling',@_)}

Now I can call the function directly anywhere in the pg code without issue. But, pushing my luck, I wanted to see if I could put that line into the context definition file. Is that possible?


In reply to Joel Therrien

Re: Creating a special context?

by Davide Cervone -
I wanted to see if I could put that line into the context definition file. Is that possible?

Yes, you can, but you should do it in the _init routine for your macro file. For example, if your macro file is called myMacros.pl, then you could use

    sub _myMacros_init {
      PG_restricted_eval('sub Tunneling {Parser::Function->call("Tunneling",@_)}');
    }
to define your function. it is possible to put it in directly, but it is safer to do this, so that your macro file can be used in more general situations.