Showing posts with label Coding. Show all posts
Showing posts with label Coding. Show all posts

Sunday, 25 April 2021

Lyric Captions

Praise the Lord and Pass the Microphone

The Digital Audio Workstation "Ableton Live" works almost as well with full video files as it does with audio samples, but has no native facility to add lyric captions to a musical video, nor to generate the necessary caption format files to upload to video services like YouTube. Having produced a full square sixteen of Paul O'Brien's song recordings, and in the process almost accidentally created an equal number of "live" performance videos, I wanted the ability to add closed captions containing the song lyrics.

Oh, and it had to be free and quick and easy. There are commercial solutions available, but I didn't want to spend one cent. If you search the intertubes for "Ableton Lyrics" today, most of your results will be from American "worship sites", and a little thought will reveal the reason for that. Obviously this is a bit removed from my particular use case.

There are also automatic options. The AIs may be coming for all of our jobs, and speech recognition is certainly improving exponentially as we, erm, speak. But it's not quite there yet as far as the singing voice is concerned. I mean, just look at this effort.

So it has to be accurate too, but within limits. We're not building a karaoke machine complete with bouncing ball. One-second accuracy should be adequate for the display of each line of the lyrics, so the listener can follow along with the performance.

SubRip File Format

Most subtitles distributed on the internet, for example those ripped from movie DVDs, use a file format called - for obvious reasons - SubRip. Since this format is one of the two most popular currently supported for videos uploaded to YouTube or Google Drive (the other being SubViewer, support for which was added later) I settled on it initially for this project.

SubRip is a very simple text format: each text entity (line of dialog or song lyric) is preceded by a header containing its index (line) number and the start and stop times for its display on screen, and followed by a blank line. Obviously any text editor could be used to produce such a file, but look how fiddly it is, even after you've determined the full list of correct values, to incorporate these time marks into the format (the milliseconds separator is a comma because SubRip was originally written in France):

"Union Card" song lyrics - Copyright © 2021 Paul O'Brien

That's the little app I ended up with, after two hours in Visual Studio - one hour for the calculation engine, and one for the user interface. Here's the code, and here's how it works: 

  1. Specify the total duration of the video file, by either entering the minutes & seconds at the foot of the form, or selecting the video or associated audio file (via menu or drag & drop) and letting the code read the relevant duration from it. This feature uses the magic of TagLibSharp.
  2. Either drag the lyrics file into the window, or paste the lyrics from the clipboard into the left panel, or right-click and select the lyrics text file to load it.
  3. The captions file appears immediately in the right panel. This text may be copied to the clipboard, or saved with a menu command.
  4. Any alterations to the duration controls, or to the contents of the left lyrics panel, are immediately reflected in the right captions panel, so it's always kept up to date, ready to be copied or saved.

Tweaking the Timing

Given the above description of the tool's operation, you probably guessed that it's simply counting the number of lines in the lyrics, and allocating an equal time slice to each out of the total video duration. Sure, this isn't exactly how songs work, and without some degree of tweaking, the lyrics displayed will drift into and out of synchrony with the performance - that's if you're lucky, and they ever enter synchrony at all!

The one blunt weapon at our disposal is the blank line. It's usually enough to restore an adequate level of synchrony, without introducing complicated user operations, judiciously to insert one or more blank lines into the lyrics. For example, the above song Union Card has a classic 12-bar blues structure. If you don't know what that is, think of Led Zeppelin's Rock And Roll. And if you don't know what that it, get off of my lawn.

Union Card has a 4-bar instrumental introduction, during which we don't want any lyrics appearing, although we could use this to add the artist's name, song title, copyright notice etc. Assuming we don't want any of that, we just observe that each "line" of the song lyrics occupies two bars, and add two blank lines to the start of the lyrics to account for those four wordless bars.

Next, observe that here - as often in the 12-bar blues format - the first six bars of a verse are occupied by the first three lines of lyrics; the next two bars are instrumental; the next two hold the fourth "punch" line of the verse; and the last two bars are instrumental once again. Following our guide of one line of text equalling two bars, we see that inserting one blank line after the third and fourth line of each verse should align things very nicely. When my app sees a blank line, it just retains the previously displayed line of text, because why not? There's no advantage in blanking it. Incidentally if you do want to insert a blank line somewhere, just use a line containing only a backslash ('\') instead, and the program will oblige.

But wait - a glance at the Ableton project reveals there's actually a 5-bar outro after the 9th and final verse. If one blank line represents two bars, how can we add half a blank line to compensate for the final, odd-numbered bar? Well, we can't, at least not without complicating our beautifully simple timing scheme. Easier maybe just to add two blank lines, and truncate the final bar. Looking again at the score we see the tempo is 96bpm, the time signature is 4/4, so one bar is 4/96 minutes, or 2½ seconds. So, just clip 2½ seconds from the file duration using the up/down controls at the foot of the form.

Och That's Too Complicated

Okay, how about this then. You can add a half-length line by including an initial period ('.') in the lyrics. If this appears on a line on its own, it's equivalent to a blank line, but of just half the usual duration. If it appears at the start of a lyric line, then that line will occupy just one-half of the usual time for a line; so for example, if each line of lyrics so far has occupied two bars of music, this one will occupy just one bar.

Inspired by musical notation, I'll expand this a little further. So, a line starting with two consecutive periods ('..') will occupy a further 50% of the duration of the single period line, i.e. three quarters or 75% of the usual line length; while a line starting with a colon (':') will occupy just one quarter.

These markups can alternatively be appended to the end of a line, extending its duration by the given amount, so for example a period at the end of a line causes it to be displayed for 1½ times the usual interval, a colon 1¼, and so on. With a little thought, this is almost identical in effect to putting the punctuation on its own (otherwise blank) line, after the lyric. The "almost" covers the fact these trailing marks will be ignored if leading marks are also present.

Handy reminder from the Help menu or F1 key

But what if my lyrics... end with a haunting ellipsis? If you want to incorporate leading or trailing punctuation in the displayed text of a particular lyric line, no problem, just pad the text with a leading or trailing space, so that your punctuation symbols don't actually appear right at the very start or end of the line. The program strips all leading and trailing markup and whitespace, before adding a single space for legibility to the start and end of each line, so your trailing ellipsis will be preserved without altering the line's display duration.

More general extensions are possible, but I'll reserve those until the need arises. Paul's written some songs in 3:4 time, so that shouldn't be too far in the future.


Here's the final result. Note how it changes text precisely on the first beat of the bar throughout. A little distracting of course when Paul's singing anticipates this point, but that's by design, and it's doing just what I asked. Precisely positioned lyric captions with the absolute minimum of time, cost, effort and fuss.

Friday, 14 June 2019

ToyGraf

Taylored Polynomials

Previously:
Differentiating f(x)^g(x)
The Differentiator
Graphing Derivatives
The Complexities Of The Simplifier
Formula Translation: The Error Function
Expression Parser
Note: As always, the latest ToyGraf code can be found at https://github.com/dogbiscuituk/Sid/.

What better use for a graphing differentiator, than to generate Taylor polynomials for arbitrary functions, in the fewest possible keystrokes?

  • Run ToyGraf;
  • Press F2 (or click the green + button) to add a new function;
  • Type sin x + cos x
  • Click the little yellow pencil in the top row to bring up the Trace Properties dialog;
  • Type Alt+T, or click the big friendly Taylor Polynomial... button;
  • Use the spin edit to select the desired degree of your polynomial, then click OK.
The Legend Box that appears will obscure most of the new graph, while the Property Table hides the rest. But that's your own fault for selecting such a ridiculously high degree of polynomial. There are both menu options and toolbar buttons to control both if and where stuff like the toolbar, legend and property table appear - have a hunt for them!

Save some files in the ToyGraf .tgf format, then open them in Notepad++ to read what they contain. Don't worry, you won't injure yourself on the sharp brackets, they're not XML (thanks to my code reviewer and tester Stuart for convincing me JSON was the way to go).

Next time: Using ToyGraf to make real analysis demo animations...

Thursday, 18 April 2019

Expression Parser

Modus Operandi

Previously:
Differentiating f(x)^g(x)
The Differentiator
Graphing Derivatives
The Complexities Of The Simplifier
Formula Translation: The Error Function
The Differentiator's fluent expression-building syntax is great when you have source code access, but it's easy to build a standard, recursive-descent parser for everyone else to enjoy. As you'll see below, the best thing about that is the sheer pleasure of creating your own precedence and associativity rules.

Our little language of arithmetic expressions in a single real variable x has quite the simple grammar, which in mostly-EBNF, looks like this:

    Parameter       = "x" ;

    Number          = ? \d*\.?\d*([eE][+-]?\d+)? ? ;

    NamedConstant   = "e" | "π" | "pi" | "ϕ" | "phi" ;

    UnaryOperator   = "+" | "-" | "!" | "~" | "√" ;

    BinaryOperator  = "||" | "&&" | "|" | "&" | "=" | "==" | "≠" | "<>" | "!=" | "<" | ">" | "≮ " | "≯ " | "<=" | ">="
                    | "+" | "-" | "*" | "/" | "^" ;

    Function        = "Abs" | "Acos" | "Acosh" | "Acot" | "Acoth" | "Acsc" | "Acsch" | "Asec" | "Asech" | "Asin"
                    | "Asinh" | "Atan" | "Atanh" | "Ceiling" | "Cos" | "Cosh" | "Cot" | "Coth" | "Csc" | "Csch"
                    | "Erf" | "Exp" | "Floor" | "Ln" | "Log10" | "Round" | "Sec" | "Sech" | "Sign" | "Sin"
                    | "Sinh" | "Sqrt" | "Step" | "Tan" | "Tanh" ;

    Operand         = Parameter
                    | Number
                    | NamedConstant
                    | Operand, "'" (* Form the derivative *)
                    | "(", Expression, ")"
                    | Function, Operand
                    | UnaryOperator, Operand ;

    Expression      = Operand, { BinaryOperator, Operand }
                    | Expression, "?", Expression, ":", Expression ;

The mutual recursion visible between the above Expression and Operand nonterminals is the hallmark of a classic infix-notation, parentheses-laden syntax.

Notes on the Grammar
  • The Parameter 'x' is case insensitive, but will be converted to lower case during the parsing process.
  • The Number element is defined above using a regular expression pattern. This pattern overreaches, since for simplicity's sake, none of its character groups are mandatory. So the empty string is regarded as a valid number; as is, a single dot; or for example, the sequence '.E-'. But that's OK, as long as the tokeniser is just greedy enough, but doesn't steal characters from subsequent tokens. The actual parsing of constants will be done later by the double.Parse() method, so if the tokeniser passes through any such malformed number, the error will be detected.
  • A Number starts with either a digit or a decimal point; any preceding '+' or '-' signs will be treated as unary operators (although the parser will later collapse all such operators into a single Constant node).
  • The NamedConstant class does allow you to type the names of (some of) the few supported values as Greek letters (π, ϕ)... but you're wasting your time! This code collapses (combines) constants eagerly, and everything soon ends up as a double-precision, floating point number. NamedConstant elements are case insensitive.
  • The UnaryOperator elements '!' (nothing to do with factorials) and '~' both represent a logical not operation. The number zero is regarded as false, anything else as true. So if x=0, then !x (or equivalently ~x) will return 1; otherwise 0.
  • UnaryOperator '√' performs the same function as 'Sqrt'.
  • The BinaryOperator elements '&' and '&&' both perform a logical And operation, likewise '|' and '||' a logical Or. They differ only in their levels of precedence: as in C#, the order of evaluation is '&', '|', '&&', '||' (obviously you can just stick to one preferred style, as the same effect can be achieved using parentheses). Similarly, '=' and '==' perform the same equality test; '≠', '<>' and '!=' the same inequality test; and so on for the comparison operators.
  • The available Function terminal elements are just the names of the 35 functions made available in the previous article.
  • Function names are case insensitive, but will be converted to Title Case (initial capital) during the parsing process. Notice that the Function syntax doesn't require parentheses; abs x works just as well as Abs(x).
  • The apostrophe "'" is the only postfix operator in the grammar. It has the effect of differentiating the preceding Operand, so for example, (Sin(x))' evaluates to Cos(x).
Turning Japanese

It's always at this point, turning from the syntax diagram to the semantics of coding, that I inevitably get seduced by this cool feature, first encountered in my first couple of pocket computers: the Sharp PC-1211 and PC-1500. The cool feature is implied products. TL;DR: every memory category was in such short supply back then (see: millennium bug), even BASIC variables were meted out individually, and their names were A..Z. The unexpected benefit of this rationing was that products of variables (variables which individually could only have single-character names like A or B) could now be expressed unambiguously in the form AB, saving a whole byte by omitting the multiplication symbol.

At first glimpse this implied multiplication seems no big deal, but wait. Does it necessarily have the same associativity and precedence as normal multiplication? If so, then
A/BC would be parsed as (A/B)*C = (A*C)/B.
But this seems wrong! The expression looks so much more like a quantity A being divided by a quantity BC, in other words,
A/BC = A/(B*C).
Now the C has landed below the line; before, it was above. Which is correct?

Utility is monarch, and the solution turns out to be the introduction of a new kind of implied multiplication with a higher precedence than the usual one. By analogy: the associativity of '^' was once settled to be right, unlike its additive and multiplicative colleagues which all tended left.
So, while subtraction goes like this: A-B-C = (A-B)-C = A-(B+C),

by contrast exponentiation does this: A^B^C = A^(B^C)

rather than the much less useful (A^B)^C,
because the real action's in the exponent. In the case of the implicit multiplier, the new operator was given a higher precedence than its peers, so for example
A^BC = A^(B*C), rather than (A^B)*C.
In this way multiplication, if it just remain hidden, can leapfrog other more powerful operators, perhaps even unary operators, perhaps even function calls:
sin AB = sin (A * B), compared to sin A * B = (sin A) * B.

White Feather

So why did I hesitate to implement such an operator in Parser?

A recurring subject in the C# language designers' blogs and comment responses is cost. Nothing comes for free! There's always at least a test budget to consider, and almost always, myriad further hidden design and development costs. Implied products are almost too attractive a feature. Sure, (x+2)(x-2) and its ilk are compelling, but to get the full story, you must go back to the language's grammar definition and look at all newly emerging cases.

What about
2 sin x cos x
for example. The familiar sine of a double angle formula, obviously and unambiguously intended to be read as
2*sin(x)*cos(x).
But should the precedence of "implicit multiply" exceed even unary and function calls, the interpretation becomes
2*sin(x*cos(x)),
which, simply, ouch.

Then again, why all this detail, if I never intended to implement such an operator? Because I knew I would. I knew that about myself, and so wanted all this background material to be available for review, when I finally revisited the code to smoosh it up a bit. Also, the very inevitability of that revisit is why I made this code as clean and readily understandable as I could. Part of this process is of course eliminating all but the most vital of comments from the source; hence their home in this article.

That Precedence enumeration was a proper godsend when smooshing time arrived! Anyway, here comes the parser code:

namespace FormulaBuilder
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Text.RegularExpressions;

    public class Parser
    {
        private enum Precedence
        {
            RightParenthesis,
            Additive,
            Multiplicative,
            Exponential,
            Functional,
            ImpliedProduct,
            SuperscriptPower
        }

        const char
            SquareRoot = '√';

        const string
            ImpliedProduct = "i*",
            SuperscriptPower = "s^",
            UnaryMinus = "u-",
            UnaryPlus = "u+";

        private string Formula;
        private int Index;
        private Stack<Expression> Operands;
        private Stack<string> Operators;

        public Expression Parse(string formula)
        {
            Formula = formula;
            Index = 0;
            Operands = new Stack<Expression>();
            Operators = new Stack<string>(new[] { "(" });
            ParseExpression();
            if (Operators.Any())
                throw new FormatException(
                    $"Unexpected end of expression, input='{Formula}'");
            return Operands.Peek();
        }

        public bool TryParse(string formula, out object result)
        {
            try
            {
                result = Parse(formula);
                return true;
            }
            catch (Exception e)
            {
                result = e.Message;
                return false;
            }
        }

        private static double GetNamedConstantValue(string constant)
        {
            switch (constant.ToLower())
            {
                case "e":
                    return Math.E;
                case "π":
                case "pi":
                    return Math.PI;
                case "ϕ":
                case "phi":
                    return (1 + Math.Sqrt(5)) / 2;
            }
            return 0;
        }

        private static ExpressionType GetExpressionType(string op)
        {
            switch (op)
            {
                case "+":
                    return ExpressionType.Add;
                case "-":
                    return ExpressionType.Subtract;
                case "*":
                case "i*":
                    return ExpressionType.Multiply;
                case "/":
                    return ExpressionType.Divide;
                case "^":
                case "s^":
                    return ExpressionType.Power;
                case UnaryPlus:
                    return ExpressionType.UnaryPlus;
                case UnaryMinus:
                    return ExpressionType.Negate;
            }
            throw new FormatException();
        }

        private static Precedence GetPrecedence(string op)
        {
            switch (op)
            {
                case ")":
                    return Precedence.RightParenthesis;
                case "+":
                case "-":
                    return Precedence.Additive;
                case "*":
                case "/":
                    return Precedence.Multiplicative;
                case "^":
                    return Precedence.Exponential;
                case ImpliedProduct:
                    return Precedence.ImpliedProduct;
                case SuperscriptPower:
                    return Precedence.SuperscriptPower;
            }
            return Precedence.Functional;
        }

        private Expression MakeBinary(string op, Expression lhs, Expression rhs) =>
            Expression.MakeBinary(GetExpressionType(op), lhs, rhs);

        private Expression MakeFunction(string f, Expression operand)
        {
            f = $"{char.ToUpper(f[0])}{f.ToLower().Substring(1)}";
            var result = Expressions.Function(f, operand);
            if (operand is ConstantExpression c)
                return result.AsDouble((double)c.Value).Constant();
            return result;
        }

        private Expression MakeUnary(string op, Expression operand)
        {
            if (operand is ConstantExpression c)
                switch (op)
                {
                    case UnaryPlus: return operand;
                    case UnaryMinus: return (-(double)c.Value).Constant();
                }
            return Expression.MakeUnary(GetExpressionType(op), operand, null);
        }

        private string MatchFunction() => MatchRegex(@"^[\p{Lu}\p{Ll}\d]+").ToLower();

        private string MatchNumber() => MatchRegex(@"^\d*\.?\d*([eE][+-]?\d+)?");

        private string MatchRegex(string pattern)
        {
            var match = Regex.Match(Formula.Substring(Index), pattern);
            return Formula.Substring(Index + match.Index, match.Length);
        }

        private string MatchSubscript() => MatchRegex($"^[{StringUtilities.Subscripts}]+");
        private string MatchSuperscript() => MatchRegex($"^[{StringUtilities.Superscripts}]+");

        private char NextChar()
        {
            var count = Formula.Length;
            while (Index < count && Formula[Index] == ' ')
                Index++;
            return Index < count ? Formula[Index] : Index == count ? ')' : '$';
        }

        private string NextToken()
        {
            var nextChar = NextChar();
            switch (nextChar)
            {
                case '+':
                case '-':
                case '*':
                case '/':
                case '^':
                case '(':
                case ')':
                case SquareRoot:
                    return nextChar.ToString();
                case char c when char.IsDigit(c):
                case '.':
                    return MatchNumber();
                case char c when c.IsSuperscript():
                    return MatchSuperscript();
                case char c when char.IsLetter(c):
                    return MatchFunction();
            }
            throw new FormatException(
                $"Unexpected character '{nextChar}', input='{Formula}', index={Index}");
        }

        private void ParseExpression()
        {
            do
            {
                ParseOperand();
                var op = NextChar();
                switch (op)
                {
                    case '+':
                    case '-':
                    case '*':
                    case '/':
                    case '^':
                    case ')':
                        ParseOperator(op.ToString());
                        if (op == ')')
                            return;
                        break;
                    case '$' when Index == Formula.Length + 2: // End of input (normal)
                        return;
                    case '$' when Index < Formula.Length + 2: // End of input (unexpected)
                        throw new FormatException(
                            $"Unexpected end of text, input='{Formula}', index={Index}");
                    case char c when c.IsSuperscript():
                        ParseOperator(SuperscriptPower); //
                        break;
                    default:
                        if (Operands.Peek() is ConstantExpression)
                        {
                            ParseOperator(ImpliedProduct); // Implied multiplication
                            break;
                        }
                        throw new FormatException(
                            $"Unexpected character '{op}', input='{Formula}', index={Index}");
                }
            }
            while (true);
        }

        private void ParseFunction(string function)
        {
            Operators.Push(function);
            ReadPast(function);
        }

        private bool ParseNamedConstant(string constant)
        {
            var value = GetNamedConstantValue(constant);
            if (value == 0)
                return false;
            Operands.Push(value.Constant());
            ReadPast(constant);
            return true;
        }

        private void ParseNumber(string number)
        {
            try
            {
                Operands.Push(double.Parse(number).Constant());
            }
            catch (FormatException)
            {
                throw new FormatException(
                    $"Invalid number format '{number}', input='{Formula}', index={Index}");
            }
            catch (OverflowException)
            {
                throw new FormatException(
                    $"Numerical overflow '{number}', input='{Formula}', index={Index}");
            }
            ReadPast(number);
        }

        private void ParseOperand()
        {
            var token = NextToken();
            switch (char.ToLower(token[0]))
            {
                case 'x' when token.Length == 1:
                    ParseParameter(token);
                    break;
                case char c when char.IsDigit(c):
                case '.':
                    ParseNumber(token);
                    break;
                case char c when c.IsSuperscript():
                    ParseSuperscript(token);
                    break;
                case '(':
                    Operators.Push(token);
                    ReadPast(token);
                    ParseExpression();
                    break;
                case '+':
                case '-':
                    ParseUnary(token);
                    ParseOperand();
                    break;
                case char c when char.IsLetter(c):
                    if (!ParseNamedConstant(token))
                    {
                        ParseFunction(token);
                        ParseOperand();
                    }
                    break;
                case SquareRoot:
                    ParseSquareRoot();
                    ParseOperand();
                    break;
                default:
                    throw new FormatException(
                        $"Missing operand, input='{Formula}', index={Index}");
            }
        }

        private void ParseOperator(string op)
        {
            do
            {
                var ours = GetPrecedence(op);
                var pending = Operators.Peek();
                if (pending == "(")
                    break;
                var theirs = GetPrecedence(pending);
                // Operator '^' is right associative: a^b^c = a^(b^c).
                if (theirs > ours || theirs == ours && op != "^")
                {
                    Operators.Pop();
                    var operand = Operands.Pop();
                    if (theirs == Precedence.Functional)
                        switch (pending)
                        {
                            case UnaryPlus:
                                break;
                            case UnaryMinus:
                                operand = MakeUnary(pending, operand);
                                break;
                            default:
                                try
                                {
                                    operand = MakeFunction(pending, operand);
                                }
                                catch (ArgumentNullException)
                                {
                                    throw new FormatException(
                                        $"Unrecognised function '{pending}', input='{Formula}'");
                                };
                                break;
                        }
                    else
                    {
                        if (pending == ImpliedProduct)
                            pending = "*";
                        operand = MakeBinary(pending, Operands.Pop(), operand);
                    }
                    Operands.Push(operand);
                }
                else
                    break;
            }
            while (true);
            if (op == ")")
                Operators.Pop();
            else
                Operators.Push(op);
            if (op != ImpliedProduct && op != SuperscriptPower)
                ReadPast(op);
        }

        private void ParseParameter(string token)
        {
            Operands.Push(Expressions.x);
            ReadPast(token);
        }

        private void ParseSquareRoot()
        {
            Operators.Push("Sqrt");
            ReadPast(SquareRoot.ToString());
        }

        private void ParseSuperscript(string superscript)
        {
            Operands.Push(new Parser().Parse(superscript.SuperscriptToNormal()));
            ReadPast(superscript);
        }

        private void ParseUnary(string unary)
        {
            Operators.Push($"u{unary}");
            ReadPast(unary);
        }

        private void ReadPast(string token) => Index += token.Length;
    }
}

Going Wild!

It's a fair cop. If you've read through the source code above, you'll have spotted vestiges of not only implied multiplication, but also (implied) superscript exponentiation. Like a lot of subsequent developments, they're not visible in the grammar spec at the head of this article. But to start with, you can omit the multiplication sign after any constant number, and before the next operand, which may be "x", or a function, or "(" introducing a nested expression, or even another number (separated from the first by at least one space).

And what of the over-eager precedence problem? This new implicit multiplication operator needs its own, new precedence level, Implied, above Unary, so stuff like sin 2x works properly. But that's also higher than Exponential precedence, so 2x^3 becomes (2*x)^3, whereas in the arena of polynomials we'd rather see 2*(x^3). This can be fixed, luckily, quickly, and quirkily: introduce Unicode superscripts, and give them their own implicit exponentiation operator! This has the highest precedence of all, and causes 2x³ to get dressed as the wanted 2*(x^3).

Just how far can we go with superscripts? Unicode characters include numbers and common mathematical symbols ⁰¹²³⁴⁵⁶⁷⁸⁹⁺⁻⁼⁽⁾, a full superscript Latin lowercase alphabet ᵃᵇᶜᵈᵉᶠᵍʰⁱʲᵏˡᵐⁿᵒᵖʳˢᵗᵘᵛʷˣʸᶻ (well, full except for 'q'), a limited uppercase Latin alphabet ᴬᴮᴰᴱᴳᴴᴵᴶᴷᴸᴹᴺᴼᴾᴿᵀᵁⱽᵂ, and some Greek letters ᵅᵝᵞᵟᵋᶿᶥᶲᵠᵡ. These limitations already suggest we should use some other type of markup language - remember, this series started with a look at LaTeX - and the fact that even these available glyphs come from different ranges, so depending on your typeface can vary in size and position, surely cements that opinion. But until we make that switch, we can enjoy the novelty of chucking expressions like x⁴-4x³+6x²-4x+1, or eᶜᵒˢ⁽ˣ⁾, into our mathematical stockpot.

You don't have superscripts on your keyboard? Sorry, I'm just a code monkey. That's definitely a hardware problem.

namespace FormulaBuilder
{
    using System;
    using System.Text;

    public static class StringUtilities
    {
        public const string
            Subscripts = "₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎ₐₑₕᵢⱼₖₗₘₙₒₚᵣₛₜᵤᵥₓᵦᵧᵨᵩᵪ",
            Transcripts = "0123456789+-=()aehijklmnoprstuvxβγρψχbcdfgwyzABDEGHIJKLMNOPRTUVWαδεθιφ",
            Superscripts = "⁰¹²³⁴⁵⁶⁷⁸⁹⁺⁻⁼⁽⁾ᵃᵉʰⁱʲᵏˡᵐⁿᵒᵖʳˢᵗᵘᵛˣᵝᵞρᵠᵡᵇᶜᵈᶠᵍʷʸᶻᴬᴮᴰᴱᴳᴴᴵᴶᴷᴸᴹᴺᴼᴾᴿᵀᵁⱽᵂᵅᵟᵋᶿᶥᶲ";

        public static bool IsSubscript(this char c) => Subscripts.IndexOf(c) >= 0;
        public static bool IsSuperscript(this char c) => Superscripts.IndexOf(c) >= 0;

        public static string NormalToSubscript(this string number) => Transcribe(number, Transcripts, Subscripts);
        public static string NormalToSuperscript(this string number) => Transcribe(number, Transcripts, Superscripts);
        public static string SubscriptToNormal(this string number) => Transcribe(number, Subscripts, Transcripts);
        public static string SubscriptToSuperscript(this string number) => Transcribe(number, Subscripts, Superscripts);
        public static string SuperscriptToNormal(this string number) => Transcribe(number, Superscripts, Transcripts);
        public static string SuperscriptToSubscript(this string number) => Transcribe(number, Superscripts, Subscripts);

        public static string Transcribe(this string number, string source, string target)
        {
            var stringBuilder = new StringBuilder(number);
            for (var index = 0; index < Math.Min(source.Length, target.Length); index++)
                stringBuilder.Replace(source[index], target[index]);
            return stringBuilder.ToString();
        }
    }
}

But Does It Parse The Test?

Obviously we need some tests for all this, and in fact the following lines have just appeared in Expressions.Tests.cs. Despite giving the visual appearance of comparing one string with another one rather like it, these tests actually embody quite the round trip. The left string is pumped into the parser, generating an expression tree as its output. This is then fed through a stage 2 amplifier, namely the AsString() extension method, where we hope to see a similarly structured string emerge, but sporting at the most binary operations, and all the other minor effects and transformations expected.

        public static void TestParse(string input, string output) =>
            Check(output, new Parser().Parse(input).AsString());

        public static void TestParseFail(string input, string error)
        {
            try
            {
                new Parser().Parse(input);
                Check("Exception thrown", "no Exception thrown");
            }
            catch (Exception ex)
            {
                Check(error, ex.Message);
            }
        }

        public static void TestParser()
        {
            TestParserFailure();
            TestParserSuccess();
        }

        public static void TestParserFailure()
        {
            TestParseFail("x~2", "Unexpected character '~', input='x~2', index=1");
            TestParseFail("x+123,456", "Unexpected character ',', input='x+123,456', index=5");
            TestParseFail("x+1$2", "Unexpected end of text, input='x+1$2', index=3");
            TestParseFail("x+1e999", "Numerical overflow '1e999', input='x+1e999', index=2");
            TestParseFail("x+.", "Invalid number format '.', input='x+.', index=2");
            TestParseFail("x+.E+1", "Invalid number format '.E+1', input='x+.E+1', index=2");
            TestParseFail("x+", "Missing operand, input='x+', index=2");
            TestParseFail("(x+(2*(x+(3)))", "Unexpected end of text, input='(x+(2*(x+(3)))', index=15");
        }

        public static void TestParserSuccess()
        {
            TestParse("0", "0");
            TestParse("e", "2.71828182845905");
            TestParse("π", "3.14159265358979");
            TestParse("pi", "3.14159265358979");
            TestParse("ϕ", "1.61803398874989");
            TestParse("phi", "1.61803398874989");
            TestParse("X+1", "(x+1)");
            TestParse("((x+1))", "(x+1)");
            TestParse("x+x*x^x/x-x", "((x+((x*(x^x))/x))-x)");
            TestParse("3*x+x/5", "((3*x)+(x/5))");
            TestParse("x-2-x", "((x-2)-x)");                             // Subtraction is left associative
            TestParse("x^2^x", "(x^(2^x))");                             // Exponentiation is right associative
            TestParse("(x-3)*(5-x)/10", "(((x-3)*(5-x))/10)");
            TestParse("sin x * cos x", "(Sin(x)*Cos(x))");
            TestParse("Ln(sin x - tanh(x)) - 1", "(Ln((Sin(x)-Tanh(x)))-1)");
            TestParse("Abs Cos Sin Tan 1.5", "0.540839774154307");
            TestParse("Abs Cos Sin Tan (x/2)", "Abs(Cos(Sin(Tan((x/2)))))");
            TestParse("2*(sin x + cos x ^ 3 - tan(x^3))/3", "((2*((Sin(x)+(Cos(x)^3))-Tan((x^3))))/3)");
            TestParse("2*(x+3*(x-4^x)-5)/6", "((2*((x+(3*(x-(4^x))))-5))/6)");
            TestParse("1/5x", "(1/(5*x))");                              // Implied products have higher precedence
            TestParse("1/2sqrt(x)", "(1/(2*Sqrt(x)))");
            TestParse("2 sin 3x", "(2*Sin((3*x)))");
            TestParse("2 sin 3x * 5 cos 7x", "((2*Sin((3*x)))*(5*Cos((7*x))))");
            TestParse("2 3", "(2*3)");
            TestParse("2(x+3)", "(2*(x+3))");
            TestParse("2x^3)", "((2*x)^3)");
            TestParse("2(x^3)", "(2*(x^3))");
            TestParse("x⁴-4x³+6x²-4x+1", "(((((x^4)-(4*(x^3)))+(6*(x^2)))-(4*x))+1)");
            TestParse("1/2√(1-x²)", "(1/(2*Sqrt((1-(x^2)))))");
            TestParse("eˣ", "(2.71828182845905^x)");
            TestParse("eᶜᵒˢ⁽ˣ⁾", "(2.71828182845905^Cos(x))");
        }

Wednesday, 17 April 2019

Formula Translation: The Error Function

Not At All Elementary, Watson

Previously:
Differentiating f(x)^g(x)
The Differentiator
Graphing Derivatives
The Complexities Of The Simplifier
Project Differentiator is growing rapidly (!) into a customizable calculus teaching tool. Such resources are sometimes more valuable than any generally available mathematics code library, utility, or even online suite like Wolfram Alpha or Mathematica. Just ask Grant Sanderson, whose magnificent YouTube channel 3blue1brown - in addition to its inestimable educational payload - delivers and promotes beautiful mathematics as an art form, rolling along atop its very own powerful, handcrafted, dynamic, mathematical graphics engine. When you want to explain things in great detailed graphics, and especially in animations, nothing beats running your own source code.

Waiting for the dust to settle on the latest UI iteration, I thought I'd mention this short function here: a three-line translation, based on some old 1992 code found in a Fortran77 book, of the error function, y=erf(x), as defined in the integral equation alongside. That FORmula TRANslation provenance remains visible in my choice of variable names in the code below: well of course n is the integer loop variable.

Incidentally, you should never use the Fortran-recommended I as your loop variable. If a single pin breaks in your dot matrix printer, the resultant printout on your music ruled paper might be mistaken for the digit 1, crashing your lander on the moon.

The interesting thing about erf(x) is that it's non-elementary, making it actually one of the uninteresting ones, globally speaking.

What's Special About Error?

    double t = 1 / (1 + Math.Abs(x) / 2), u = 1, v = 0;
    for (var n = 0; n < 10; v += erf_a[n] * u, n++, u *= t) ;
    return (1 - t * Math.Exp(v - x * x)) * Math.Sign(x);

Yeah, absolutely nothing. Non-elementary functions might appear curious to engineers learning calculus, but the proportion of all possible continuous functions that are elementary is zero (assuming that's possible for the ratio of two uncountable infinities). An elementary function, as originally introduced by Joseph Liouville starting in 1833, is one built from certain basic functions (constant, algebraic, exponential and logarithmic, along with their inverses if they exist), by recursive composition and application of arithmetic operators, including root extraction.

On the other hand, despite its non-elementary nature, erf(x) is in great demand, mostly because it's literally the area under the normal distribution graph in statistics. And when you hear there's no way to integrate y=exp(x²), well, a constant multiple of erf(x) is the answer to that too. These reasons, together with its eminent differentiability (it's defined as an integral, after all), are why I wanted it in the Lego™ box.

Speaking of which, the list of elementary functions available in Sid, as the project is now dubbed, has more than doubled since you last looked. And if you ever needed an inverse trigonometric or hyperbolic function, but discovered it missing from the supplied System.Math set, you'll find the entire lot implemented here. Anyway, the erf code is the bottom third of this file:

namespace FormulaBuilder
{
    using System;

    /// <summary>
    /// Several math functions are either absent from System.Math (Asec, Sech, Asech,
    /// Step) or unsuitable (Sign, which has an integer output), so they are provided
    /// in this class. Might as well include the whole System.Math lot here too; then
    /// we never need perform two reflection operations to find any function by name.
    /// </summary>
    public static class Functions
    {
        #region Elementary Functions

        public static double Abs(double x) => Math.Abs(x);
        public static double Acos(double x) => Math.Acos(x);
        public static double Acosh(double x) => Math.Log(x + Math.Sqrt(Math.Pow(x, 2) - 1));
        public static double Acot(double x) => Math.Atan(1 / x);
        public static double Acoth(double x) => Math.Log((x + 1) / (x - 1)) / 2;
        public static double Acsc(double x) => Math.Asin(1 / x);
        public static double Acsch(double x) => Math.Log((1 + Math.Sqrt(1 + Math.Pow(x, 2))) / x);
        public static double Asec(double x) => Math.Acos(1 / x);
        public static double Asech(double x) => Math.Log((1 + Math.Sqrt(1 - Math.Pow(x, 2))) / x);
        public static double Asin(double x) => Math.Asin(x);
        public static double Asinh(double x) => Math.Log(x + Math.Sqrt(Math.Pow(x, 2) + 1));
        public static double Atan(double x) => Math.Atan(x);
        public static double Atanh(double x) => Math.Log((1 + x) / (1 - x)) / 2;
        public static double Ceiling(double x) => Math.Ceiling(x);
        public static double Cos(double x) => Math.Cos(x);
        public static double Cosh(double x) => Math.Cosh(x);
        public static double Cot(double x) => 1 / Math.Tan(x);
        public static double Coth(double x) => 1 / Math.Tanh(x);
        public static double Csc(double x) => 1 / Math.Sin(x);
        public static double Csch(double x) => 1 / Math.Sinh(x);
        public static double Exp(double x) => Math.Exp(x);
        public static double Floor(double x) => Math.Floor(x);
        public static double Ln(double x) => Math.Log(x);
        public static double Log10(double x) => Math.Log10(x);
        public static double Round(double x) => Math.Round(x);
        public static double Sec(double x) => 1 / Math.Cos(x);
        public static double Sech(double x) => 1 / Math.Cosh(x);
        public static double Sign(double x) => Math.Sign(x);
        public static double Sin(double x) => Math.Sin(x);
        public static double Sinh(double x) => Math.Sinh(x);
        public static double Sqrt(double x) => Math.Sqrt(x);
        public static double Step(double x) => x < 0 ? 0 : 1;
        public static double Tan(double x) => Math.Tan(x);
        public static double Tanh(double x) => Math.Tanh(x);

        #endregion

        #region Non-elementary Functions

        private static readonly double[] erf_a =
        {
            -1.26551223, +1.00002368, +0.37409196, +0.09678418, -0.18628806,
            +0.27886807, -1.13520398, +1.48851587, -0.82215223, +0.17087277
        };

        /// <summary>
        /// An approximation, with a worst case error of 1.2e-7, from the book
        /// "Numerical Recipes in Fortran 77: The Art of Scientific Computing"
        /// (ISBN 0-521-43064-X), 1992, page 214, Cambridge University Press.
        /// </summary>
        /// <param name="x">The input value to the function Erf(x).</param>
        /// <returns>An approximation to the value of erf(x).</returns>
        public static double Erf(double x)
        {
            double t = 1 / (1 + Math.Abs(x) / 2), u = 1, v = 0;
            for (var n = 0; n < 10; v += erf_a[n] * u, n++, u *= t) ;
            return (1 - t * Math.Exp(v - x * x)) * Math.Sign(x);
        }

        #endregion
    }
}

I'll confess to considering the elision of that redundant final u *= t in the for loop, before remembering the pipeline exception would certainly waste more picoseconds than a blast through the floating point silicon. Old optimisation rabbits die hard.

The algorithm first sets t=1/(1+|x|/2). Then in the for loop, it forms a ninth degree polynomial v(t), using ten supplied coefficients erf_a[0..9]. Finally, according to the original sign of x, your answer is either 1-t*e^(v(t)-x²), or its negative.

Next time: let's get that parser up and running...

Image credits: https://en.wikipedia.org/wiki/Error_function.

Monday, 8 April 2019

The Complexities Of The Simplifier

The Simplifier The Better

Previously:
Differentiating f(x)^g(x)
The Differentiator
Graphing Derivatives
Simplification of algebraic expressions was never the intended destination of this series, but it's where we've landed. The rules of differentiation guarantee an answer for any input function, subject to certain reasonable conditions of continuity. They just don't promise a pretty answer; that much is up to us! And generally, it requires more work.

Reduction of algebraic formulae is a big subject. Sometimes, to be fair, that's due to subjective constraints we place on answers. Take polynomials for example. It's natural to expect every power of x to be expressed as anxⁿ, rather than say (xⁿ)*an, albeit the algebra is indifferent to such distinctions. On the other hand, nobody ever ordered xⁿ+xⁿ when they actually wanted 2xⁿ.

Our Simplify() method is a fairly arbitrary piece of ad-hoc coding, which applies a specific set of transformation rules to the expression tree. These rules are motivated by the particular output from certain popular examples of differentiation, and are all applied during a single traversal of the tree. Here we will consider an alternative, where rules are applied one by one to the entire tree, starting at the root and proceeding depth-first recursively. These steps successively transform the tree such that new invariants become true upon each pass, resulting in an overall directed process of reduction.

Let's continue using our informal rule notation, where
  • s, t represent any subtrees of arbitrary size and complexity;
  • +, -, *, /, ^ are binary operators;
  • u+, u- are the unary plus (identity) and minus (negate) operators respectively;
  • c, d are numeric constants;
  • 0, 1, -1 are the particular numeric constants 0, ±1.
Then using the right arrow '→' to indicate the application of a transformation rule to its left operand, resulting in the new tree in its right operand, we have the following examples. First, the unary u+ operator represents the identity transformation, and can just be dropped from wherever it occurs:
u+(s) → s (Rule 1)
The unary u- operator is equivalent to the binary '-' with (0) on its left hand side. This is not strictly a reductive transformation, but it might be useful if we want to rid the tree of all unary operators, so that some other rule requiring this precondition can then be applied (note: the converse rule, which is reductive, was actually used in the original code):
u-(s) → 0 - s (Rule 2)
One And One Is One

Any binary operator with constants on both sides (constant siblings) can be replaced by a single, computed constant value. Let @ be any one of the five binary operators. Then we have this rule template:
c @ d → c@d (Rule 3)
where the notation c@d written without intervening spaces indicates a single constant node, comprising an immediate numerical calculation. Obviously checks against division by zero, and other indeterminate forms such as 0⁰, are assumed to raise the appropriate exceptions (DivideByZeroException, InvalidOperationException) when poked.

Similarly, certain constant niblings (nieces or nephews) can be combined, when their operators "match" - i.e., they are either both additive, or both multiplicative. For ease of legibility and comprehension in what follows, I'm going to let '+' represent the commutative operator in such a pair (which will actually be either '+' or '*'), and let '-' represent its noncommutative inverse (actually '-' or '/'). Then we have these four templates:
(s + c) + d → s + c+d (Rule 4a) (s + c) - d → s + c-d (Rule 4b) (s - c) + d → s - c-d (Rule 4c) (s - c) - d → s - c+d (Rule 4d)
Notice the pattern of operators in these templates. The first operator, following the s, is unchanged; while the second is set to either the commutative '+' if the original two operators were the same, or the noncommutative '-' if they were different. This logic was handled by local variables same & opps in the original SimplifyBinary() method.

Constant Craving

Everything up to this point, with the singular exception of the unary u- noted above, was included in the earlier single traversal code. However these four latest example templates assume all the constants are on the RHS of their parent binaries. That suggests another twelve such patterns exist, where either one or both constants are actually on the LHS:
(c + s) + d → s + c+d (Rule 5a) (c + s) - d → s + c-d (Rule 5b) (c - s) + d → c+d - s (Rule 5c) (c - s) - d → c-d - s (Rule 5d) c + (s + d) → s + c+d (Rule 6a) c + (s - d) → s + c-d (Rule 6b) c - (s + d) → c+d - s (Rule 6c) c - (s - d) → c-d - s (Rule 6d) c + (d + s) → s + c+d (Rule 7a) c + (d - s) → c+d - s (Rule 7b) c - (d + s) → c-d - s (Rule 7c) c - (d - s) → s + c-d (Rule 7d)
Almost half of these can be handled by preprocessing the tree to ensure that whenever possible, i.e. when a binary operator is commutative, its constant term appears on the RHS, using this supplementary rule:
c + s → s + c (Rule 8)
A single application of Rule 8 carries Rules 5a and 6a into 4a, 5b into 4b, and 6b into 4c, while two successive applications carry 7a into 4a. That's five out of these new twelve cases dealt with. We are frustrated in claiming a full 50% by Rule 7d, which requires recognising that s is governed by two '-' signs (or '/' signs in the multiplicative version).

Constant Cousins

Another important transformation which could have been included in the original sample code, but was omitted for purposes of clarity, is the combination of cousin constants. This is where a binary operation has two binary children, each of which has a constant operand. Since we are not handling distributivity here, the three binaries involved must be either all additive, or all multiplicative. This yields another 8 templates which can be expressed, reusing the previous operator definitions, like this:
(s + c) + (t + d) → (s + t) + c+d (Rule 9a) (s + c) + (t - d) → (s + t) + c-d (Rule 9b) (s + c) - (t + d) → (s - t) + c-d (Rule 9c) (s + c) - (t - d) → (s - t) + c+d (Rule 9d) (s - c) + (t + d) → (s + t) - c-d (Rule 9e) (s - c) + (t - d) → (s + t) - c+d (Rule 9f) (s - c) - (t + d) → (s - t) - c+d (Rule 9g) (s - c) - (t - d) → (s - t) - c-d (Rule 9h)
Again there's a pattern to the operators, before and after. This time the first and second operators swap places, while the third is set either to the commutative '+' if the original set of three operators contained an even number (0 or 2) of '-'s, or to the noncommutative '-' if it contained an odd number (1 or 3). And just as before, the possibility of constants appearing in the LHS leads to another proliferation of additional cases (initially 24), around half of which are handled by our commutativity code, with the rest requiring further special handling.

Finally, Semantix Rears Its Monstrous Head

Merged constants aside, all of the foregoing transformations have been syntactical, in that they have not relied upon any particular constant values, function calls, powers of x, etc. Now we've combined as many constants into single values as we feel inclined to do, we can take advantage of a few simple semantic rules, such as those pertaining to additive and multiplicative identities and inverses. Assuming we're on the final stretch, we presumably no longer need maintain the unary-free invariant property of our expression tree, and can also reintroduce negative versions of the multiplicative rules. So, we may have 0 or ±1 on the RHS:
s + 0 → s (Rule 10a) s - 0 → s (Rule 10b) s * 0 → 0 (Rule 10c) s * 1 → s (Rule 10d) s * -1 → -s (Rule 10e) s / 1 → s (Rule 10f) s / -1 → -s (Rule 10g) s ^ 0 → 1 (Rule 10h) s ^ 1 → s (Rule 10i)
Rules 10e & 10g are the negatives mentioned above. Finally, we may have 0 or 1 on the LHS, in these few cases which have not already been handled by commutativity:
0 / s → 0 (Rule 11a) 0 ^ s → 0 (Rule 11b) 1 ^ s → 1 (Rule 11c)
Next time: work continues on the above optimisation system, and a new UI is simultaneously being developed to allow user entry of formulae as plain text. While all that rages on, I'll report on several interesting design snippets just to keep the post count ticking over. The first of these will be the addition of the non-elementary error function Erf(x) to the library.
 
Anonymization by Anonymouse.org ~ Adverts
Anonymouse better ad-free, faster and with encryption?
X