maths / moodle-qtype_stack

Stack question type for Moodle
GNU General Public License v3.0
140 stars 148 forks source link

No option to type in logarithms to different bases #243

Closed C0untC0unt closed 7 years ago

C0untC0unt commented 7 years ago

We tried to work on some tasks concerning logarithms. By doing that, we found out that students can only type in the ln() but no logarithms to other bases. Maybe an option can be included that allows the student to type in answers like log_3(7) (or log(3;7)) while STACK interprets ln(7)/ln(3).

sangwinc commented 7 years ago

Thanks for raising this issue. Actually, I've been wondering for some time how we might accommodate logarithms to other bases within STACK without overloading the input syntax. Maxima does not have a built-in function for logarithms for other bases, apart from base 10 logarithms are implemented with the contributed log10 package and directly as the function "lg" in STACK.

The function "log" can't be overloaded with a definition such as log(x,a) to take two arguments because Maxima does not allow the re-definition of core built-in functions (probably very sensibly really!). This is a bit of a shame in this one case.

We could overload the function "lg", so that if it is given one argument we assume it is base 10 (therefore no change in current behaviour) and if it accepts two arguments we convert this to an inert function "logbase(n, b)" which represents the logarithm.

The following code is my first attempt. Add the following code to replace lines around here: https://github.com/maths/moodle-qtype_stack/blob/master/stack/maxima/stackmaxima.mac#L82

lg([ex]) := block(
  if length(ex) = 1 then return(log10(first(ex))),
  if length(ex) = 2 then return(logbase(first(ex),second(ex))),
  error("STACK function 'lg' must have one or two arguments only.")
)$

logbasetex(ex):=block([n, b],
  [n, b]:args(ex),
  oldsimp:simp,
  return(concat("\\log_{", stack_disp_strip_dollars(tex(b, false)), "}\\left(", stack_disp_strip_dollars(tex(n, false)), "\\right)"))
)$

texput(logbase, logbasetex);

To use this in anger we can then evaluate expressions with the extra definition

/* Use of radcan to give canonical form. */
logbase(n,b):= radcan(log(n)/log(b));

which should establish equivalence as we would expect.

I'm on a train back to Edinburgh, so have not integrated this into the codebase. I expect there will be some wrinkles. Do you have any comments on either the input format or functionality?

sangwinc commented 7 years ago

This is now added to STACK. Thanks for reminding me to add this.

christianp commented 7 years ago

This isn't how I thought you'd do this! Don't you have a rewrite step on the student's answer, before Maxima tries to work with it? Then you could rewrite all occurrences of log(x,b) to log_base(x,b), and define a rule in Maxima simplifying log_base(x,%e) to log(x), and log(x,10) to lg(x).

Disclaimer: I have no idea how STACK goes from student input to a Maxima expression, and I haven't looked to find out.

sangwinc commented 7 years ago

Christian,

We already have "lg" as a wrapper for logs to base 10, and this extends that functionality. We could add something to the parser in PHP to intercept log( and replace it with log_base, but you basically end up in a similar place. The only advantage of your approach is that you overload the "log" function (which is core maxima) rather than "lg" which is not.

What do you do in Numbas?

christianp commented 7 years ago

We don't have log(x,b) in Numbas, but it would be trivial to add. The system supports function overloading: you give multiple definitions with different type signatures, and it picks the first one that matches the invocation.

I thought I'd see how Mathematica does it, and it looks like their calling convention is Log[b,z] - the base goes first. I think I prefer that - it's closer to log_b(z).

christianp commented 7 years ago

(Although I'd like the first argument to always mean the same thing - that's a much more common convention)

aharjula commented 7 years ago

Considering all the things Chris did with underscores lately I wonder how hard it would actually be to have that log_b(x), personally I like the looks of it more and it matches the LaTeX presentation better... Naturally, it would probably mean that #231 would need to be solved in a different way.

sangwinc commented 7 years ago

The difficulties with the underscore were due to (1) subscripts not really having any "meaning". I think the closest is indexing elements of a list/sequence, but often it is just a label. (2) trying to do all this in Maxima, and getting conflicts with what is and isn't an "atom". In Maxima AB is an atom, and we are trying to turn this into a operator "" and arguments A&B. Then comes issues of binding, associativity etc.

In this case I have been constrained by (1) not being able to overload "log" which is an internal Maxima function which can't be changed (2) not wanting to change existing behaviour!

I think the pattern "log_A(B)" should be robust to match and transform to "lg(A,B)" in PHP where A and B are just letters and numbers. We could add this syntactic sugar in this case for the benefit of students.

What do you think?

aharjula commented 7 years ago

Considering the way it would need to be done (when and the simplification issues) were it done in Maxima doing it on the PHP side sure makes more sense. Lets just hope that we don't start to make these special cases too often and that they keep some kind of an syntactically valid form in Maxima.

C0untC0unt commented 7 years ago

First of all, we want to say "Thank you"! The new way to type in logarithms (log_x(y)) works great and it is a joy to create some tasks with it.

By doing this, we found one (probably little) problem: When the student types a bracket within the log-bracket the interpretation switches back to the lg (log_10) Example: Answer: log_x(1/(x+b)) Interpretation: log_10(1/x) // String: (logbase(1/x,10)))

sangwinc commented 7 years ago

Thanks for reporting this. We'll look into it.

aharjula commented 7 years ago

Does this really match the parenthesis correctly? Just the outermost matching ones that is.

sangwinc commented 7 years ago

Spot on Matti! It doesn't work, and I'm just in the process of adding in some test cases...

sangwinc commented 7 years ago

Any comments Matti?

aharjula commented 7 years ago

Hmm... Not really as I am not that great at regexps. But maybe you can try these absurd examples with it:

log_pi((y+1)/x^(y))
log10(x/max(y,x))
log_5(1/(if ")" = ")" then x else y)) #Obviously this would have been fun without the string removal logic.
log_(-1/2)(x) #What would this even mean and I assume expression bases will not be supported anyway.
christianp commented 7 years ago

You definitely can't match nested parentheses with a regex - that's the classic limitation of regular languages. That substring_between function is giving me the horrors. Chris, your 'possible fix' doesn't cope with nested logs. I'm not sure why.

I've had a go at writing a couple of different ways of making a more robust replacement function. I've set up a script with some unit tests at https://gist.github.com/christianp/3a6badbc59100f6434b5df5f3c231191.

My replace_logs version works by tokenising first, splitting the string by the tokens we care about: this makes everything so much easier! I made it handle strings delimited by " because of Matti's test case above, but I don't know much about maxima strings: are there escape characters? Are double quotes the only allowed delimiters?

aharjula commented 7 years ago

You can pretty much ignore strings there as we already replace all strings earlier in the casstring processing with just empty strings i.e., ")whatever\"\\" -> "" the only escapes in maxima strings are \" and \\. And the delimiters are double quotes and nothing else.

I fully agree with you with the substring_between..., but can't really decide if the feeling of horror comes from the number of assumptions made or from just using the wrong tools. One thing is sure it does not really handle nesting.

As to nested parenthesis it is something I solved for matrices about a decade ago (the purest form of the nightmares I make is the modinput_tokenizer function here, note that mine is not even recursive) and someone has beautified that code to the form of stack_utils::list_to_array, but that only works for inputs that contain the whole nested structure. So in any case we still have the problem of finding the matching parenthesis for the opening one of the log.

We really should just invest time on building a general Maxima-syntax parser so that we would not need to worry about these nesting issues and could just act on trees renaming functions and moving arguments around without dealing with string indexes or replaces.