Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Units at Runtime #268

Merged
merged 19 commits into from May 19, 2016

Conversation

Projects
None yet
3 participants
@goehle
Copy link
Member

commented Jan 18, 2016

This pull requests modifies NumberWithUnits so that you can add units at runtime. You can see the documentation in the header portion of parserNumberWithUnits (see file changes). The short version is you can add new units, along with conversion info, at runtime.

$newUnits = ['apple',{name=>'apples',conversion=>{factor=>1,apple=>1}}];
$a = NumberWithUnits("3 apples",{newUnit=>$newUnits});

Davide, since I'm mucking about in MathObjects, let me know if I've stepped on anything I shouldn't have or if there is something I should do to improve things. You can use the following problem to test:

DOCUMENT();      
loadMacros(
   "PGstandard.pl",
   "MathObjects.pl",
   "parserNumberWithUnits.pl",
);
TEXT(beginproblem());
Context("Numeric");
$newUnit = {name => 'bear',                                            
                   conversion => {factor =>3, m=>1}};                       
$pi = NumberWithUnits("3 bear", {newUnit=>$newUnit});   
$pi2 = NumberWithUnits("pi", "Spoon",{newUnit=>"Spoon"});
$newUnits = ['apple',{name=>'apples',conversion=>{factor=>1,apple=>1}}];
$pi3 = NumberWithUnits("3 apples",{newUnit=>$newUnits});  
$pi4 = NumberWithUnits("pi ft");
WARN_MESSAGE(join(',',keys %Units::known_units));
Context()->texStrings;
BEGIN_TEXT
\($pi\) and \{$pi->ans_rule\} $BR
\($pi2\) and \{$pi2->ans_rule\} $BR
\($pi3\) and \{$pi3->ans_rule\} $BR
\($pi4\) and \{$pi4->ans_rule\} $BR
END_TEXT
Context()->normalStrings;
ANS($pi->with(tolerance=>.0001)->cmp);
ANS($pi2->cmp);
ANS($pi3->cmp);
ANS($pi4->cmp);
ENDDOCUMENT();       
@dpvc

This comment has been minimized.

Copy link
Member

commented Jan 18, 2016

The problem that I see with this, and the reason I never did something like it myself, is that the Units namespace is global to the httpd child process, so any chanced you make to it will be persistent to other problems that are processed by the same child. That means that, after your problem is processed, other problems that use units will have bear, spoon, and apples as defined units. But only those that use the same child process. This will lead to intermittent and apparently random behavior of problems after such a question is assigned to anyone on the server.

In order to make this work properly, you need to add the units locally to the problem, so they are part of the safe compartment, and will disappear between problems. The current organization of the Units package doesn't make the easy to do.

It has been a while since I tested the details of how the packages work, and I didn't test this change specifically, so perhaps things have changed since then. But last time I looked, this would have caused leakage of problem-secific changes to other problems.

@goehle

This comment has been minimized.

Copy link
Member Author

commented Jan 18, 2016

Hmm. You are right, there is leakage to other problems. You need to be able to override the known_units and fundamental_units hashes to point to something local to the problem. I could do that and then add the ability to pass a custom known_units and fundamental_units hash to the Units package routines.

@dpvc

This comment has been minimized.

Copy link
Member

commented Jan 18, 2016

Ok, sounds like a plan.

@goehle

This comment has been minimized.

Copy link
Member Author

commented Jan 23, 2016

Ok, so I added code in the parserNumberWithUnits.pl and parserFormulaWithUnits.pl files that spins off a copy of the relevent hashes in Units and initializes NumberWithUnits with them. These are only loaded once so unless someone does something crazy you should get one copy of the unit hashes per problem. I also added overrides to the relevent functions in NumberWithUnits and Units so that if a reference to alternative unit hashes are provided then those are used instead. Again, this is reasonably subtle PG territory so let me know if I am doing something clumsy.

Things to test

  • Test that normal units work. I.E. test
DOCUMENT();      
loadMacros(
"PGstandard.pl",     # Standard macros for PG language
"MathObjects.pl",
"parserNumberWithUnits.pl",
);
TEXT(beginproblem());
Context("Numeric");
$pi = NumberWithUnits("pi ft");
Context()->texStrings;
BEGIN_TEXT
Enter a value for \(\pi\)
\{$pi->ans_rule\}
END_TEXT
Context()->normalStrings;
ANS($pi->with(tolerance=>.0001)->cmp);
ENDDOCUMENT();        

This should work even under the following conditions:

DOCUMENT();      
loadMacros(
"PGstandard.pl",
"MathObjects.pl",
"parserNumberWithUnits.pl",
);
TEXT(beginproblem());
Context("Numeric");
$newUnit = {name => 'bear',                                            
               conversion => {factor =>3, m=>1}};                       
$pi = NumberWithUnits("3 bear", {newUnit=>$newUnit});   
$pi2 = NumberWithUnits("pi", "Spoon",{newUnit=>"Spoon"});
$newUnits = ['apple',{name=>'apples',conversion=>{factor=>1,apple=>1}}];
$pi3 = NumberWithUnits("3 apples",{newUnit=>$newUnits});  
$pi4 = NumberWithUnits("pi ft");
Context()->texStrings;
BEGIN_TEXT
\($pi\) and \{$pi->ans_rule\} $BR
\($pi2\) and \{$pi2->ans_rule\} $BR
\($pi3\) and \{$pi3->ans_rule\} $BR
\($pi4\) and \{$pi4->ans_rule\} $BR
END_TEXT
Context()->normalStrings;
ANS($pi->with(tolerance=>.0001)->cmp);
ANS($pi2->cmp);
ANS($pi3->cmp);
ANS($pi4->cmp);
ENDDOCUMENT();        

In particular this should work with a first answer of 9 m or 3 bear since one bear is 3 m. The second answer should just be pi Spoon and the third answer should work as either 3 apples or 3 apple. Finally the fourth answer should work as pi ft or 0.319186 bear.

  • Test the same functionality with parserFormulaWithUnits.pl. You can use the problem
DOCUMENT();      
loadMacros(
"PGstandard.pl",
"MathObjects.pl",
"parserFormulaWithUnits.pl",
);
TEXT(beginproblem());
Context("Numeric");
$newUnit = {name => 'bear',                                            
               conversion => {factor =>3, m=>1}};                       
$pi = FormulaWithUnits("3 x bear", {newUnit=>$newUnit});   
$pi2 = FormulaWithUnits("pi x", "Spoon",{newUnit=>"Spoon"});
$newUnits = ['apple',{name=>'apples',conversion=>{factor=>1,apple=>1}}];
$pi3 = FormulaWithUnits("3x apples",{newUnit=>$newUnits});  
$pi4 = FormulaWithUnits("pi*x ft");

Context()->texStrings;
BEGIN_TEXT
\($pi\) and \{$pi->ans_rule\} $BR
\($pi2\) and \{$pi2->ans_rule\} $BR
\($pi3\) and \{$pi3->ans_rule\} $BR
\($pi4\) and \{$pi4->ans_rule\} $BR
END_TEXT
Context()->normalStrings;
ANS($pi->with(tolerance=>.0001)->cmp);
ANS($pi2->cmp);
ANS($pi3->cmp);
ANS($pi4->cmp);
ENDDOCUMENT();        

The answers are all the same as the ones for the parserNumberWithUnits.pl test problem but they have x in them. In particular the conversions mentioned above should still work.

  • Test that there isn't any leakage in another problem. You can add WARN_MESSAGE(join(',',keys %Units::known_units)); to a problem to show all of the possible units. Run one of the above problems then check another problem for unintended units.
if ($known_units) {
$options->{known_units} = $known_units;
}
my %Units = Units::evaluate_units($units,{fundamental_units => $fundamental_units, known_units => $known_units});

This comment has been minimized.

Copy link
@dpvc

dpvc Jan 23, 2016

Member

Does this want to use $options rather than the explicit hash. It looks like you are setting up $options for that, but never use them.

This comment has been minimized.

Copy link
@goehle

goehle Jan 23, 2016

Author Member

Good catch. I was adding and removing stuff for testing purposes and likely added the wrong thing back in.

@dpvc

This comment has been minimized.

Copy link
Member

commented Jan 23, 2016

Just FYI, to check for leakage, you probably need to run the second problem several times, since you have to make sure it runs in the same child process as the original problem. Since the children are parceled out in various orders, you probably won't get the same child for the second problem on your first try. I'm sure you know that, but just in case anyone else it doing testing.

I haven't had a chance to run tests by hand yet, but just looked through the code by hand. It looks good. I'll see if I can get some time this weekend to give it a try.

@@ -0,0 +1,60 @@
#!/usr/bin/perl

This comment has been minimized.

Copy link
@dpvc

dpvc Jan 31, 2016

Member

Did you mean to add these ~ files?

# the hashes for these local copies to the NumberWithUnits package to use
# for all of its stuff.
%fundamental_units = %Units::fundamental_units;
%known_units = %Units::known_units;

This comment has been minimized.

Copy link
@dpvc

dpvc Jan 31, 2016

Member

It looks like these are setting global values in the main:: namespace. Should these be my variables?

# the hashes for these local copies to the NumberWithUnits package to use
# for all of its stuff.
%fundamental_units = %Units::fundamental_units;
%known_units = %Units::known_units;

This comment has been minimized.

Copy link
@dpvc

dpvc Jan 31, 2016

Member

Same here.

This comment has been minimized.

Copy link
@goehle

goehle Jan 31, 2016

Author Member

I didn't mean to add the ~ files. They don't actually work right now, but
I had to update my personal repo so I could transfer between machines and
those changes showed up here. Its what I get for working on something in
consideration for pull requests. I'll get rid of them.

Its not always clear to me why some variables are local and some are global
in PG. For example %Units::known_units is available as a variable
directly. If I add my's should I also add accessors? Its not impossible
that someone would want to be able to see what units are available. If I
do leave them as global then I could also give them better names.

This comment has been minimized.

Copy link
@dpvc

dpvc Jan 31, 2016

Member

Its not always clear to me why some variables are local and some are global in PG

Yes, I understand. I think much of that is left over from before PG used packages, and that was basically the only option.

If I add my's should I also add accessors?

The actually variables used are the my variables in Parcer::Legacy::ObjectWithUnits that you added in the first file at the top of the page. The ones here are only used to get copies of the ones from Units:: to pass to initializeUnits() in the line below. That is the routine that actually saves the values in the my variables that are used for the rest of the problem. So these definitely should be local variables, not global ones.

If you want to give access to the ones in Parser::Legacy::ObjectWithUnits, then that is a different question. I'm not sure it is necessary. The available units are the the ones in Units::known_units plus the ones defined in the problem, so there shouldn't be much question about that. I suppose other macro packages could define some that you didn't know about.

@goehle

This comment has been minimized.

Copy link
Member Author

commented Jan 31, 2016

Good point. I've made the suggested changes.

@goehle

This comment has been minimized.

Copy link
Member Author

commented Feb 1, 2016

I've cleaned up the unit tests so they work. You can try them out by also checking out the Automated Testing branch (openwebwork/webwork2#681) and getting Selenium set up. Then you can just run

perl /opt/webwork/pg/t/Selenium/Tests/parserNumberWithUnit/parser.t

Again this is a bit of a proof of concept to see if these tests are useful. The basic idea is that you can keep a pg file in the testing folder and use the existing utilities to easy make a problem to test. Then you would use the Selenium IDE to generate some tests on the rendered version of the problem. In theory having these tests, and providing them with pull requests will speed things up, but it depends on if people use them and if they are robust enough.

@mgage

This comment has been minimized.

Copy link
Member

commented May 19, 2016

I've verified that there is no leakage in this code but I have some suggestions before
we pull it. For the file
parserNumberWithUnits.pl how about changing it to:

our %fundamental_units = %Units::fundamental_units;
our %known_units = %Units::known_units;

sub _parserNumberWithUnits_init {
  # We make copies of these hashes here because these copies will be unique to  # the problem.  The hashes in Units are shared between problems.  We pass
  # the hashes for these local copies to the NumberWithUnits package to use
  # for all of its stuff.  


  Parser::Legacy::ObjectWithUnits::initializeUnits(\%fundamental_units,\%known_units);
  # main::PG_restricted_eval('sub NumberWithUnits {Parser::Legacy::NumberWithUnits->new(@_)}');

}
sub NumberWithUnits {Parser::Legacy::NumberWithUnits->new(@_)};
sub parserNumberWithUnits::fundamental_units {
    return \%fundamental_units;
}
sub parserNumberWithUnits::known_units {
    return \%known_units;
}
sub parserNumberWithUnits::add_unit {
    my $newUnit = shift;
    my $Units= Parser::Legacy::ObjectWithUnits::add_unit($newUnit->{name}, $newUnit->{conversion});
    return %$Units;
}
  1. the accessor's are simply so that we can grab the current value of %known_units and %fundamental_units as stored in parserNumberWithUnits.pl. %Units::known_units didn't
    update as items were added.
  2. The add_unit subroutine is so that you can add the units to the files environment directly without
    having to create an answer evaluator. This seemed more natural to me although there is no reason
    to remove the automatic adding of a unit to the environment when creating an evaluator.

This is the test code I put in problems to determine which new units were defined:

@check_these_units = qw(apple apples Spoon bear rabbit foobear);
DEBUG_MESSAGE("checking for ", join(' ', @check_these_units), $BR);
$string='';
$fund = parserNumberWithUnits::fundamental_units();
$known = parserNumberWithUnits::known_units;
foreach my $key (@check_these_units) {
  $string .="$key is a fundamental unit$BR" if exists($fund->{$key});
  $string .="$key is a known unit$BR" if exists($known->{$key});
  $string.= "$key is an undefined unit $BR" unless exists($known->{$key});
}
DEBUG_MESSAGE( $string);


Change the formula file in the same way. Comments?

@goehle

This comment has been minimized.

Copy link
Member Author

commented May 19, 2016

If you make a pull request against my feature branch I will merge it.

Cheers
Geoff
On May 18, 2016 10:39 PM, "Michael Gage" notifications@github.com wrote:

I've verified that there is no leakage in this code but I have some
suggestions before
we pull it. For the file
parserNumberWithUnits.pl how about changing it to:

our %fundamental_units = %Units::fundamental_units;
our %known_units = %Units::known_units;

sub _parserNumberWithUnits_init {

We make copies of these hashes here because these copies will be unique to # the problem. The hashes in Units are shared between problems. We pass

the hashes for these local copies to the NumberWithUnits package to use

for all of its stuff.

Parser::Legacy::ObjectWithUnits::initializeUnits(%fundamental_units,%known_units);

main::PG_restricted_eval('sub NumberWithUnits {Parser::Legacy::NumberWithUnits->new(@_)}');

}
sub NumberWithUnits {Parser::Legacy::NumberWithUnits->new(@_)};
sub parserNumberWithUnits::fundamental_units {
return %fundamental_units;
}
sub parserNumberWithUnits::known_units {
return %known_units;
}
sub parserNumberWithUnits::add_unit {
my $newUnit = shift;
my $Units= Parser::Legacy::ObjectWithUnits::add_unit($newUnit->{name}, $newUnit->{conversion});
return %$Units;
}

  1. the accessor's are simply so that we can grab the current value of
    %known_units and %fundamental_units as stored in parserNumberWithUnits.pl.
    %Units::known_units didn't update as items were added.
  2. The add_unit subroutine is so that you can add the units to the
    files environment directly without having to create an answer evaluator.
    This seemed more natural to me although there is no reason to remove the
    automatic adding of a unit to the environment when creating an evaluator.

This is the test code I put in problems to determine which new units were
defined:

@check_these_units = qw(apple apples Spoon bear rabbit foobear);
DEBUG_MESSAGE("checking for ", join(' ', @check_these_units), $BR);
$string='';
$fund = parserNumberWithUnits::fundamental_units();
$known = parserNumberWithUnits::known_units;
foreach my $key (@check_these_units) {
$string .="$key is a fundamental unit$BR" if exists($fund->{$key});
$string .="$key is a known unit$BR" if exists($known->{$key});
$string.= "$key is an undefined unit $BR" unless exists($known->{$key});
}
DEBUG_MESSAGE( $string);

Change the formula file in the same way. Comments?


You are receiving this because you authored the thread.
Reply to this email directly or view it on GitHub
#268 (comment)

Create ability to add new units to problem before calling NumberWithU…
…nits or FormulaWithUnits.

Simplify the way NumberWithUnits is called -- I don't believe the eval() is necessary.
Add accessor so the current value of units can be determined.
@mgage

This comment has been minimized.

Copy link
Member

commented May 19, 2016

OK. That’s done. (It took three tries to submit a clean PR. :-) )

Take care,

Mike

On May 19, 2016, at 8:20 AM, Geoff Goehle notifications@github.com wrote:

If you make a pull request against my feature branch I will merge it.

Cheers
Geoff
On May 18, 2016 10:39 PM, "Michael Gage" notifications@github.com wrote:

I've verified that there is no leakage in this code but I have some
suggestions before
we pull it. For the file
parserNumberWithUnits.pl how about changing it to:

our %fundamental_units = %Units::fundamental_units;
our %known_units = %Units::known_units;

sub _parserNumberWithUnits_init {

We make copies of these hashes here because these copies will be unique to # the problem. The hashes in Units are shared between problems. We pass

the hashes for these local copies to the NumberWithUnits package to use

for all of its stuff.

Parser::Legacy::ObjectWithUnits::initializeUnits(%fundamental_units,%known_units);

main::PG_restricted_eval('sub NumberWithUnits {Parser::Legacy::NumberWithUnits->new(@_)}');

}
sub NumberWithUnits {Parser::Legacy::NumberWithUnits->new(@_)};
sub parserNumberWithUnits::fundamental_units {
return %fundamental_units;
}
sub parserNumberWithUnits::known_units {
return %known_units;
}
sub parserNumberWithUnits::add_unit {
my $newUnit = shift;
my $Units= Parser::Legacy::ObjectWithUnits::add_unit($newUnit->{name}, $newUnit->{conversion});
return %$Units;
}

  1. the accessor's are simply so that we can grab the current value of
    %known_units and %fundamental_units as stored in parserNumberWithUnits.pl.
    %Units::known_units didn't update as items were added.
  2. The add_unit subroutine is so that you can add the units to the
    files environment directly without having to create an answer evaluator.
    This seemed more natural to me although there is no reason to remove the
    automatic adding of a unit to the environment when creating an evaluator.

This is the test code I put in problems to determine which new units were
defined:

@check_these_units = qw(apple apples Spoon bear rabbit foobear);
DEBUG_MESSAGE("checking for ", join(' ', @check_these_units), $BR);
$string='';
$fund = parserNumberWithUnits::fundamental_units();
$known = parserNumberWithUnits::known_units;
foreach my $key (@check_these_units) {
$string .="$key is a fundamental unit$BR" if exists($fund->{$key});
$string .="$key is a known unit$BR" if exists($known->{$key});
$string.= "$key is an undefined unit $BR" unless exists($known->{$key});
}
DEBUG_MESSAGE( $string);

Change the formula file in the same way. Comments?


You are receiving this because you authored the thread.
Reply to this email directly or view it on GitHub
#268 (comment)


You are receiving this because you commented.
Reply to this email directly or view it on GitHub https://urldefense.proofpoint.com/v2/url?u=https-3A__github.com_openwebwork_pg_pull_268-23issuecomment-2D220307749&d=CwMFaQ&c=kbmfwr1Yojg42sGEpaQh5ofMHBeTl9EI2eaqQZhHbOU&r=C6Pt5AGtImanmAdcooarL-JZO8M5dSFPfs3VweYXYkE&m=YJgEIMqhFYY59atC8y9dY2_WnJDBcXIju_8U9lhNE44&s=EGdsQNZpVthJI-fAp8WXuSxGEtEYZ2ySkggb_XkFy64&e=

Merge pull request #3 from mgage/newunit
Create ability to add new units to problem before calling NumberWithU…

@mgage mgage merged commit d78b2c9 into openwebwork:develop May 19, 2016

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.