From 3250290c7a1620dba5f012bc23a094518b1d47e9 Mon Sep 17 00:00:00 2001 From: "Davide P. Cervone" Date: Sat, 11 Jan 2025 07:13:08 -0500 Subject: [PATCH 1/3] Update units context to include differentiation, and fix some issues with absolute value --- lib/Parser/Differentiation.pm | 38 ++-- macros/contexts/contextExtensions.pl | 11 ++ macros/contexts/contextUnits.pl | 251 ++++++++++++++++++++++----- 3 files changed, 238 insertions(+), 62 deletions(-) diff --git a/lib/Parser/Differentiation.pm b/lib/Parser/Differentiation.pm index b4cb96a178..a65313c6f3 100644 --- a/lib/Parser/Differentiation.pm +++ b/lib/Parser/Differentiation.pm @@ -12,28 +12,26 @@ # sub Parser::D { my $self = shift; - my $d; - my @x = @_; - my $x; - if (defined($x[0]) && $x[0] =~ m/^\d+$/) { - $d = shift(@x); - $self->Error("You can only specify one variable when you give a derivative count") - unless scalar(@x) <= 1; - return ($self) if $d == 0; - } - if (scalar(@x) == 0) { - my @vars = keys(%{ $self->{variables} }); - my $n = scalar(@vars); - if ($n == 0) { - return $self->new('0') if $self->{isNumber}; - $x = 'x'; - } else { - $self->Error("You must specify a variable to differentiate by") unless $n == 1; - $x = $vars[0]; + my @vars = keys %{ $self->{variables} }; + my $X = @vars == 1 ? $vars[0] : undef; + push(@_, $X) if @_ == 0 && defined $X; + $self->Error("You must specify a variable to differentiate by") unless @_; + + my @x; + while (@_) { + my $d = 1; + my $x = shift; + ($d, $x) = ($x, shift) if $x =~ m/^\d+$/; + if (!defined $x) { + $self->Error([ "You must specify a variable to differentiate by following %s", $d ]) + unless defined $X; + $x = $X; } - CORE::push(@x, $x); + my $def = $self->context->variables->get($x); + $self->Error([ "Variable of differentiation not defined: %s", $x ]) unless $def; + push(@x, ($x) x $d) if $d; } - @x = ($x[0]) x $d if $d; + my $f = $self->{tree}; foreach $x (@x) { return (0 * $self)->reduce('0*x' => 1) unless defined $self->{variables}{$x}; diff --git a/macros/contexts/contextExtensions.pl b/macros/contexts/contextExtensions.pl index 72833110eb..c9f8700dbe 100644 --- a/macros/contexts/contextExtensions.pl +++ b/macros/contexts/contextExtensions.pl @@ -243,6 +243,17 @@ sub extend { } } + if ($options{lists}) { + my $lists = $context->lists; + my $listClasses = $options{lists}; + for my $list (keys %{$listClasses}) { + my $def = $lists->get($list); + Value->Error("Context '%s' does not have a definition for '%s'", $from, $op) unless $list; + $data->{$list} = $def->{class}; + $lists->set($list => { class => "context::$context->{baseName}::$listClasses->{$list}" }); + } + } + # # Replace any Parser/Value classes that are needed, saving the # originals in the class data for the context diff --git a/macros/contexts/contextUnits.pl b/macros/contexts/contextUnits.pl index fc9fa4459b..e6fb8ea200 100644 --- a/macros/contexts/contextUnits.pl +++ b/macros/contexts/contextUnits.pl @@ -14,8 +14,8 @@ =head1 DESCRIPTION loadMacros('contextUnits.pl'); -and then select the Units or LimitedUnits context and enable the units -that you want to use. E.g., +and then select the C or C context and enable the +units that you want to use. E.g., Context("Units")->withUnitsFor("length"); @@ -65,7 +65,7 @@ =head1 DESCRIPTION or $context = Context("Units"); - $context->addCategories("volume"); + $context->withUnitsFor("volume"); $context->addUnits("m", "cm"); to get a units context with units for volume as well as C and C @@ -73,6 +73,27 @@ =head1 DESCRIPTION Use C in place of C to add just the named units without adding any aliases for them. +You can remove individual units from the context using the +C method of the context. For example + + Context("Units")->withUnitsFor("length")->removeUnits("ft", "inch", "mile", "furlong"); + +removes the English units and their aliases, leaving only the metric +units. To remove a unit without removing its aliases, use C +instead. + +Note that the units are stored in the context as constants, so to list +all the units, together with other constants, use + + Context()->constants->names; + +The constants that are units have the C property set. So + + grep {Context()->constants->get($_)->{isUnit}} (Context()->constants->names); + +will get the list of units. + + =head2 Custom units You can define your own units in terms of the fundamental units. @@ -120,30 +141,13 @@ =head2 Custom units oranges => { fruit => 1, aliases => ["orange"], factor => 2 } ); - Compute("1 apple") == Compute("2 oranges"); # returns 1 + Compute("2 apples") == Compute("1 orange"); # returns 1 -will make an apple equivalent to two oranges by making both C -and C be examples of the fundamental unit C. - -You can remove individual units from the context using the -C method of the context. For example - - Context("Units")->withUnitsFor("length")->removeUnits("ft", "inch", "mile", "furlong"); - -removes the English units and their aliases, leaving only the metric -units. To remove a unit without removing its aliases, use C -instead. - -Note that the units are stored in the context as constants, so to list -all the units, together with other constants, use - - Context()->constants->names; - -The constants that are units have the C property set. So - - grep {Context()->constants->get($_)->{isUnit}} (Context()->constants->names); - -will get the list of units. +will make an orange equivalent to two apples by making both C +and C be examples of the fundamental unit C, but with +C having a factor of 2 times the fundamental unit, so an +C is considered to be 2 C, while an C is one +C. =head2 Adding units to other contexts @@ -257,7 +261,8 @@ =head2 Creating unit and number-with-unit objects makes C<$f> be a Formula returning a Number-with-Unit. Note, however, that since the space before the unit has the same precedence as multiplication (just as it does within a formula), if the expression -before the unit includes addition, you need to enclose it in parentheses: +before the unit includes addition or subtraction, you need to enclose +it in parentheses: $n = Compute("(1+4) meters"); $f = Compute("(1+2x) meters"); @@ -348,7 +353,7 @@ =head2 Working with numbers with units Using C<< $m->quantity >> is equivalent to calling C<< $m->toBaseUnits->number >>. Finally, you can get the factor by which the given units must be -multiplied to obtain the quantity in the fundamental base uses using +multiplied to obtain the quantity in the fundamental base units using the C method: $f = Compute("3 ft")->factor; # returns 0.3048 @@ -357,7 +362,7 @@ =head2 Working with numbers with units the factor for that unit. Most functions, such as C and C, will report an error if -hey are passed a number with units (or a bare unit). Important +they are passed a number with units (or a bare unit). Important exceptions are the trigonometric and hyperbolic functions, which accept a number with units provided the units are angular units. For example, @@ -377,6 +382,43 @@ =head2 Working with numbers with units quantity is the absolute value of the original quantity. +=head2 Differentiation of numbers with units + +In order to be able to differentiate a formula that returns a number +with units, the MathObjects library needs to know the units of the +variable you are differentiating by. For example, if you have + + $s = Compute("(3t^2 - 2x) m"); + +as a function of time, C, then you would like + + $v = $s->D("t"); + +to be equivalent to C. + +To enable this, you must tell the C context that C has units +of seconds. That is done using the C function of the +context: + + Context("Units") + ->withUnitsFor("length", "time") + ->assignUnits(t => "s"); + +You can pass as many unit assignments to a single C +call as you like. E.g. + + Context("Units") + ->withUnitsFor("length", "time") + ->assignUnits( + t => "s", + s => "m" + ); + +to assign the variable C units of seconds and C units of meters. +These values are only used in differentiation, so don't affect other +formulas, and aren't involved in type-checking or other operations. + + =head2 Answer checking for units and numbers with units You can use units and numbers with units within PGML or C calls @@ -389,7 +431,7 @@ =head2 Answer checking for units and numbers with units Here, the student can answer any equivalent units, such as C or even C, and get full credit. If you wish to require the units to being the same as the correct answer, you can use the -C option on the answer checker (to set the C +C option on the answer checker (or set the C flag in the units context): $u = Compute("m/s^2"); @@ -592,6 +634,9 @@ sub extending { isCommand => 1 } }, + lists => { + AbsoluteValue => 'AbsoluteValue', + }, functions => 'trig|hyperbolic|numeric', value => [ 'Real()', 'Formula' ], parser => ['Formula'], @@ -875,6 +920,26 @@ sub removeUnitsNotAliases { $self->constants->remove(@units); } +# +# Assigns units to a list of variables (for differentiation) +# +sub assignUnits { + my ($self, %vars) = @_; + my $constants = $self->constants; + my $variables = $self->variables; + for my $x (keys %vars) { + Value->Error("Unit for '%s' is not defined in this context", $x) + unless ($constants->get($vars{$x}) || {})->{isUnit}; + my $units = context::Units::Unit->new($vars{$x}); + if ($variables->get($x)) { + $variables->set($x => { units => $units }); + } else { + $variables->add($x => [ 'Real', units => $units ]); + } + } + return $self; +} + ################################################################################################# ################################################################################################# @@ -1160,7 +1225,8 @@ sub power { $self->Error("A Unit can't be raised to %s", Value::showClass($r)) unless $l->type eq 'Unit' && $r->type eq 'Number'; my $n = $r->value; - $self->Error("A Unit can only be raised to a non-zero integer value") if $n == 0 || CORE::int($n) != $n; + $self->Error("A Unit can only be raised to a non-zero integer value") + if $n == 0 || CORE::int($n) != $n; return $l->raiseUnit($n); } @@ -1176,6 +1242,39 @@ sub compare { return $l->{factor} <=> $r->{factor}; } +# +# Get the list of variables to differentiate by +# +sub DiffVars { + my $self = shift; + my @x; + while (@_) { + my $d = 1; + my $x = shift; + ($d, $x) = ($x, shift) if $x =~ m/^\d+$/; + $self->Error([ "You must specify a variable to differentiate by following %s", $d ]) + unless defined $x; + my $def = $self->context->variables->get($x); + $self->Error([ "Variable of differentiation not defined: %s", $x ]) + unless $def; + $self->Error("You can't differentiate by a variable that is not assigned a unit") + unless $def->{units}; + push(@x, ($x) x $d) if $d; + } + return @x; +} + +# +# Differentiate by the given variables (provided they have assigned units). +# +sub D { + my $self = shift; + my $vars = $self->context->variables; + my $units = $self->copy; + map { $units = $units->perUnit($vars->get($_)->{units}) } $self->DiffVars(@_); + return $units; +} + ############################################################# # @@ -1518,7 +1617,7 @@ sub mult { return $self->new($l->number->copy, $l->unit->appendUnit($r)) if $lUnitN && $rUnit; return $self->new($l->number * $r->number, $l->unit->appendUnit($r->unit)) if $lUnitN && $rUnitN; return $self->new($l * $r->number, $r->unit->copy) if $l->type eq 'Number'; - return $self->new($l->number * $r, $l->unit->copy) if $$r->type eq 'Number'; + return $self->new($l->number * $r, $l->unit->copy) if $r->type eq 'Number'; $self->Error("A Unit can't be multiplied by %s", Value::showClass($r)) if $lUnit; $self->Error("Can't multiply %s by a Unit", Value::showClass($l)); } @@ -1566,6 +1665,14 @@ sub compare { return $l->quantity <=> $r->quantity; } +# +# Differentiate the number and unts by the given variables. +# +sub D { + my $self = shift; + return $self->new($self->number->D(@_), $self->unit->D(@_)); +} + ############################################################# # @@ -1833,6 +1940,7 @@ package context::Units::BOP::add; sub _check { (shift)->checkNumberUnits } sub _eval { $_[1] + $_[2] } +sub D { Parser::BOP::add::D(@_) } ############################################################# @@ -1841,6 +1949,7 @@ package context::Units::BOP::subtract; sub _check { (shift)->checkNumberUnits } sub _eval { $_[1] - $_[2] } +sub D { Parser::BOP::subtract::D(@_) } ############################################################# @@ -1855,6 +1964,21 @@ sub _check { } sub _eval { $_[1] * $_[2] } +sub D { + my ($self, $x) = @_; + return $self->Diff($self->{lop}, $self->{rop}, $x) + if $self->{rop}->type eq 'Unit'; + return $self->Diff($self->{rop}, $self->{lop}, $x) + if $self->{lop}->type eq 'Unit'; + return Parser::BOP::multiply::D($self, $x); +} + +sub Diff { + my ($self, $op, $units, $x) = @_; + $units = $self->Item('Value')->new($self->{equation}, $units->eval->D($x)); + return $self->new($self->{equation}, $self->{bop}, $op->D($x), $units); +} + ############################################################# package context::Units::BOP::Space; @@ -1872,6 +1996,7 @@ sub _check { $self->checkCancelledUnits() if ref($self) eq $class; } sub _eval { $_[1] / $_[2] } +sub D { Parser::BOP::divide::D(@_) } ############################################################# @@ -1894,6 +2019,7 @@ sub _check { } sub _eval { $_[1]**$_[2] } +sub D { Parser::BOP::power::D(@_) } ############################################################# @@ -1941,15 +2067,17 @@ sub isAngle { } # -# Check whether degrees or other units are allowed, and do the usual -# check (for error reporting) if not. +# Check whether degrees or other units are allowed, +# otherwise convert to the usual function and do its check. # sub _check { my $self = shift; return if &{ $self->super('checkArgCount') }($self, 1); my $arg = $self->{params}->[0]; - if (($self->allowDegrees && $self->isAngle($arg)) || ($self->allowUnits && $arg->type eq $context::Units::NUNIT)) { + if ($self->allowDegrees && $self->isAngle($arg)) { $self->{type} = $Value::Type{number}; + } elsif ($self->allowUnits && $arg->type eq $context::Units::NUNIT) { + $self->{type} = $NUMBER_WITH_UNIT; } else { $self->mutate->_check; } @@ -1961,11 +2089,7 @@ sub _check { sub _eval { my ($self, $arg) = @_; my $name = $self->{name}; - $arg = $arg->quantity - if $self->allowDegrees - && Value::isValue($arg) - && $arg->type eq $context::Units::NUNIT - && $arg->fString eq 'rad'; + $arg = $arg->quantity if $self->allowDegrees && $arg->fString eq 'rad'; return &{ $self->super($name) }($self, $arg); } @@ -1981,10 +2105,26 @@ sub _call { Value::Error("Function '%s' has too many inputs", $name) if scalar(@_) > 1; Value::Error("Function '%s' has too few inputs", $name) if scalar(@_) == 0; $n = $n->quantity if $self->allowDegrees && $n->fString eq 'rad'; - Value::Error("The input to '%s' must be a number", $name) unless $n->isNumber || $self->allowUnits; + Value::Error("The input to '%s' must be a number", $name) unless $n->isNumber || $self->allowUnits($name); return &{ $self->super($name) }($self, $n); } +# +# Differentiate a function with a number-with-units as an argument. +# +# Get the argument as a FOrmula. +# If the the argument is an angle, get its quantity (which includes +# the unit factor) and differentiate that. +# Otherwise, remove the unit from the function call and differentiate that. +# +sub D { + my $self = shift; + my $arg = $self->Package('Formula')->new($self->{params}[0]); + return &{ $self->super($self->{name}) }($self, $arg->quantity)->D(@_) + if $self->allowDegrees && $arg->fString eq 'rad'; + return (&{ $self->super($self->{name}) }($self, $arg->number) * $arg->unit)->D(@_); +} + ############################################################# package context::Units::Function::trig; @@ -2001,7 +2141,26 @@ package context::Units::Function::numeric; our @ISA = ('context::Units::Function::common'); sub allowDegrees {0} -sub allowUnits { (shift)->{name} eq 'abs' } +sub allowUnits { ((shift)->{name} || shift) eq 'abs' } + +############################################################# + +package context::Units::AbsoluteValue; +our @ISA = ('context::Units::Super', 'Parser::List::AbsoluteValue'); + +sub _check { + my $self = shift; + return $self->mutate->_check + unless $self->{type}{length} == 1 + && $self->{coords}[0]->type eq $context::Units::NUNIT; + $self->{type} = $context::Units::NUMBER_WITH_UNIT; +} + +sub D { + my $self = shift; + my $arg = $self->Package('Formula')->new($self->{coords}[0]); + return (abs($arg->number) * $arg->unit)->D(@_); +} ################################################################################################# ################################################################################################# @@ -2053,6 +2212,14 @@ sub number { return $self / $self->getTypicalValue($self)->unit; } +sub quantity { + my $self = shift; + $self->checkNumberWithUnits('number'); + my $number = $self->number; + my $factor = $self->unit->factor; + return $factor == 1 ? $number : $factor * $number; +} + package context::Units::Parser::Formula; our @ISA = ('context::Units::Value::Formula'); From cd498280dd5767f7667a37749c35dea20c2eccea Mon Sep 17 00:00:00 2001 From: "Davide P. Cervone" Date: Mon, 3 Feb 2025 06:51:43 -0500 Subject: [PATCH 2/3] Update documentation and make changes requested in review --- lib/Parser/Differentiation.pm | 4 ++-- macros/contexts/contextUnits.pl | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/Parser/Differentiation.pm b/lib/Parser/Differentiation.pm index a65313c6f3..80300c438c 100644 --- a/lib/Parser/Differentiation.pm +++ b/lib/Parser/Differentiation.pm @@ -14,7 +14,7 @@ sub Parser::D { my $self = shift; my @vars = keys %{ $self->{variables} }; my $X = @vars == 1 ? $vars[0] : undef; - push(@_, $X) if @_ == 0 && defined $X; + CORE::push(@_, $X) if @_ == 0 && defined $X; $self->Error("You must specify a variable to differentiate by") unless @_; my @x; @@ -29,7 +29,7 @@ sub Parser::D { } my $def = $self->context->variables->get($x); $self->Error([ "Variable of differentiation not defined: %s", $x ]) unless $def; - push(@x, ($x) x $d) if $d; + CORE::push(@x, ($x) x $d) if $d; } my $f = $self->{tree}; diff --git a/macros/contexts/contextUnits.pl b/macros/contexts/contextUnits.pl index e6fb8ea200..9d9f5c0e01 100644 --- a/macros/contexts/contextUnits.pl +++ b/macros/contexts/contextUnits.pl @@ -388,7 +388,7 @@ =head2 Differentiation of numbers with units with units, the MathObjects library needs to know the units of the variable you are differentiating by. For example, if you have - $s = Compute("(3t^2 - 2x) m"); + $s = Compute("(3t^2 - 2t) m"); as a function of time, C, then you would like @@ -397,7 +397,7 @@ =head2 Differentiation of numbers with units to be equivalent to C. To enable this, you must tell the C context that C has units -of seconds. That is done using the C function of the +of seconds. That is done using the C function of the context: Context("Units") @@ -418,6 +418,10 @@ =head2 Differentiation of numbers with units These values are only used in differentiation, so don't affect other formulas, and aren't involved in type-checking or other operations. +If you assign units to a variable that hasn't yet been added to the +context, C will first add the variable as a real-valued +one and then assign the units to it. + =head2 Answer checking for units and numbers with units @@ -2112,7 +2116,7 @@ sub _call { # # Differentiate a function with a number-with-units as an argument. # -# Get the argument as a FOrmula. +# Get the argument as a Formula. # If the the argument is an angle, get its quantity (which includes # the unit factor) and differentiate that. # Otherwise, remove the unit from the function call and differentiate that. From 94d142eb2dd7ec0e1d8a4b0b9d5ad2247f1f506e Mon Sep 17 00:00:00 2001 From: "Davide P. Cervone" Date: Mon, 3 Feb 2025 07:04:06 -0500 Subject: [PATCH 3/3] Fix POD error reported in review --- macros/contexts/contextUnits.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macros/contexts/contextUnits.pl b/macros/contexts/contextUnits.pl index 9d9f5c0e01..1494615f2c 100644 --- a/macros/contexts/contextUnits.pl +++ b/macros/contexts/contextUnits.pl @@ -168,7 +168,7 @@ =head2 Adding units to other contexts $context = Context(context::Units::extending("LimitedFraction", limited => 1)); $context->addUnitsFor("length"); -In this case, the C 1> option indicates that no operations +In this case, the C< 1>> option indicates that no operations are allowed between numbers with units, and since the C context doesn't allow operations otherwise, you will only be able to enter fractions or whole numbers, with or without