pieterderycke / Jace

Jace.NET is a calculation engine for the .NET platform.
MIT License
436 stars 94 forks source link

Build Function with constants #43

Closed mrxrsd closed 4 years ago

mrxrsd commented 5 years ago

Hi,

Is it possible to add constant vars when build a function? something like that:

engine.build("a+b+c").Parameter("a",1").Parameter("b",2").Build();

Or I need to replace constant variable by myself before create a function? engine.build("1+2+c").Build();

tks

mrxrsd commented 5 years ago

Hi Pieter,

What do u think about adding a local constant registry in AstBuilder? Something like this:

CalculationEngine.cs

private Operation BuildAbstractSyntaxTree(string formulaText, IDictionary<string, double> constants = null)
        {
            TokenReader tokenReader = new TokenReader(cultureInfo);
            List<Token> tokens = tokenReader.Read(formulaText);

            var localConstantsRegistry = new ConstantRegistry(false);

            if (constants != null)
            {
                foreach (var localConst in constants)
                    localConstantsRegistry.RegisterConstant(localConst.Key, localConst.Value);
            }

            AstBuilder astBuilder = new AstBuilder(FunctionRegistry, adjustVariableCaseEnabled, localConstantsRegistry);
            Operation operation = astBuilder.Build(tokens);

            if (optimizerEnabled)
                return optimizer.Optimize(operation, this.FunctionRegistry, this.ConstantRegistry);
            else
                return operation;
        }

AstBuilder.cs (piece of public Operation Build(IList tokens))


                    case TokenType.Text:
                        if (functionRegistry.IsFunctionName((string)token.Value))
                        {
                            operatorStack.Push(token);
                            parameterCount.Push(1);
                        }
                        else
                        {
                            string tokenValue = (string)token.Value;
                            if (adjustVariableCaseEnabled)
                            {
                              tokenValue = tokenValue.ToLowerInvariant();
                            }

                            if (constantRegistry.IsConstantName(tokenValue)
                            {
                                resultStack.Push(new FloatingPointConstant(constantRegistry.GetConstantInfo(tokenValue).Value));
                            }
                            else
                            {
                                resultStack.Push(new Variable(tokenValue));
                            }

                        }
                        break;

CalculationEngineTest.cs


        [TestMethod]
        public void TestCalculationFormulaWithConstant()
        {
            CalculationEngine engine = new CalculationEngine(CultureInfo.InvariantCulture, ExecutionMode.Compiled);
            var fn = engine.Build("a+b+c",new Dictionary<string,double> { { "a", 1 }});
            double result = fn(new Dictionary<string, double> {{"b", 2}, {"c", 2}} );
          Assert.AreEqual(5.0, result);
}
pieterderycke commented 5 years ago

I am open for the idea to add support for it, but just for my understanding: for which use cases would you require this feature?

mrxrsd commented 5 years ago

In my case it's almost a linear regression model. It has two types of variables: generated by the system and by the input. System variables almost never change but I need to keep track all changes. So it's easier to think of them as variables instead of constants in the expression.

Another point it is I'm caching by my side, because I have other types of expressions that are not math and Jace can not process.

mrxrsd commented 5 years ago

Another example would be a tax engine. The way of calculating is always the same, but each State has its aliquot. Then we can read the aliquots from a database and apply in the formula for each State.

I think always we have variables that change little and need to be tracked, this functionality will be applied.

pieterderycke commented 5 years ago

And just to challenge you: why could you not build a function and then provide the variables for calculation? (It would really like to understand your use case in depth)

mrxrsd commented 5 years ago

Yes, it is possible but I will lose performance or I need to do as I showed in the first post by string replacement and dealing with all complexity to identify variables boundaries.

            var sw = new Stopwatch();

            var calcEngine = new CalculationEngine();
            var fn = calcEngine.Build("a*x+b");

            sw.Start();
            for (int i = 0; i < 10000000; i++)
            {
                fn(new Dictionary<string, double> {{"a", 2}, {"x", 3}, {"b", 4}});
            }

            sw.Stop();
            sw.Reset(); // 4774ms

            var fn1 = calcEngine.Build("2*x+4");
            sw.Start();
            for (int i = 0; i < 10000000; i++)
            {
                fn1(new Dictionary<string, double> { { "x", 3 } });
            }

            sw.Stop();
            sw.Reset(); //2939ms

Thinking as Functional programming is like curry function. I've already had my "function body" defined or in this case my math expressions. But I want to create sub-formulas with some variables pre-defined and caching this new functions.

About my user case is a compliance engine. There are a many of predefined rules created by user and we have sensors generating input for that "acceptance tree". This predefined rules rarely, or never change. But the tolerance values may change slightly depending on the quality of the raw material.

Example: Weight Acceptance Rule = "input - 1000 >= tolerance".

In this case my expression will never change but the tolerance value can change, or not, in a week for example.

It's like I said at the beginning, I can create the expression with all the values, except for the input of the sensor, but in this case would have to deal with the substitution by myself or I will always treating all variables as input losing performance.

Do you see any other way?

pieterderycke commented 5 years ago

You have convinced me 🙂 I agree with your use case. I will review your pull request this evening and add it as feature for Jace 1.0

mrxrsd commented 5 years ago

uow great news! looking forward to this new release! 😄

pieterderycke commented 5 years ago

I have merged pull request #44 into the dev branch

pieterderycke commented 5 years ago

I will also add support to define constants in formulas declarative:

CalculationEngine engine = new CalculationEngine(CultureInfo.InvariantCulture, ExecutionMode.Compiled);

Func<double, double, double> formula = (Func<double, double, double>)engine.Formula("a+b+c")
    .Parameter("b", DataType.FloatingPoint)
    .Parameter("c", DataType.FloatingPoint)
    .Constant("a", 1)
    .Result(DataType.FloatingPoint)
    .Build();

double result = formula(2.0, 2.0);
Assert.AreEqual(5.0, result);
pieterderycke commented 5 years ago

I have updated the wiki: https://github.com/pieterderycke/Jace/wiki/Compile-Time-constants

pieterderycke commented 4 years ago

This was released as part of Jace 1.0