APSIMInitiative / ApsimX

ApsimX is the next generation of APSIM
http://www.apsim.info
Other
129 stars 159 forks source link

Make Report able to do things like NHA += sward.Sum(species => species.HerbageGrowthWt) #7675

Closed sno036 closed 1 year ago

sno036 commented 1 year ago

but for zones, patches, plant models, ...

hol353 commented 1 year ago

This would be difficult. I made a start on thinking about how to do this. One approach is to create a class that implements IFunction that compiles a LINQ function at runtime, using our ScriptCompiler, and evaluates it when required. This could then be created and called from the Locator.cs class. This would be better (more flexible) than modifying REPORT. The REPORT line would look something like this:

sum of PastureSpecies.Sum(ps => ps.AboveGround.Wt) from [clock].StartOfSimulation to [clock].EndOfSimulation as SwardAboveGroundWt

I wonder whether this approach could also be used for the current expression handling that Locator does. I guess the syntax would be a bit different, requiring a .apsimx converter.

Q: Is this a step too far for APSIM? The reason for wanting to do this is so that users don't need to write so many manager scripts that aggregate raw APSIM variables e.g. adding up the above ground biomass in all pasture species.

Prototype of LinqFunction.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using Models.Core;
using APSIM.Shared.Utilities;

namespace Models.Functions
{
    /// <summary>
    /// A c# linq expression is evaluated.
    /// </summary>
    public class LinqFunction
    {
        /// <summary>
        /// Compile the expression and return the compiled function.
        /// </summary>
        /// <param name="expression">The expression to compile.</param>
        /// <param name="relativeTo">The model the expression is for.</param>
        /// <param name="compiler">An instance of the script compiler.</param>
        /// <param name="function">The returned function or null if not compilable.</param>
        /// <param name="errorMessages">The error messages from the compiler.</param>
        public static bool Compile<T>(string expression, IModel relativeTo, ScriptCompiler compiler,
                                      out T function, out string errorMessages)
        {
            if (compiler != null)
            {
                // From a list of visible models in scope, create [Link] lines e.g.
                //    [Link] Clock Clock;
                //    [Link] Weather Weather;
                // and namespace lines e.g.
                //    using Models.Clock;
                //    using Models;
                var models = relativeTo.FindAllInScope().ToList().Where(model => !model.IsHidden &&
                                                                        model.GetType() != typeof(Graph) &&
                                                                        model.GetType() != typeof(Series) &&
                                                                        model.GetType().Name != "StorageViaSockets");
                var linkList = new List<string>();
                var namespaceList = new SortedSet<string>();
                foreach (var model in models)
                {
                    if (expression.Contains(model.Name))
                    {
                        linkList.Add($"        [Link(ByName=true)] {model.GetType().Name} {model.Name};");
                        namespaceList.Add("using " + model.GetType().Namespace + ";");
                    }
                }
                var namespaces = StringUtilities.BuildString(namespaceList.Distinct(), Environment.NewLine);
                var links = StringUtilities.BuildString(linkList.Distinct(), Environment.NewLine);

                // Get template c# script.
                string template;
                if (typeof(T) == typeof(IBooleanFunction))
                    template = ReflectionUtilities.GetResourceAsString("Models.Resources.Scripts.CSharpBooleanExpressionTemplate.cs");
                else
                    template = ReflectionUtilities.GetResourceAsString("Models.Resources.Scripts.CSharpExpressionTemplate.cs");

                // Replace the "using Models;" namespace place holder with the namesspaces above.
                template = template.Replace("using Models;", namespaces);

                var scriptName = Guid.NewGuid().ToString().Replace("-", "");
                template = template.Replace("class Script", $"class Script{scriptName}");

                // Replace the link place holder in the template with links created above.
                template = template.Replace("        [Link] Clock Clock = null;", links.ToString());

                // Replace the expression place holder in the template with the real expression.
                template = template.Replace("return Clock.FractionComplete;", "return " + expression + ";");

                // Create a new manager that will compile the expression.
                var result = compiler.Compile(template, relativeTo);
                if (result.ErrorMessages == null)
                {
                    errorMessages = null;
                    function = (T)result.Instance;

                    // Resolve links
                    var functionAsModel = function as IModel;
                    functionAsModel.Parent = relativeTo;
                    var linkResolver = new Links();
                    linkResolver.Resolve(functionAsModel, true);
                    return true;
                }
                else
                {
                    errorMessages = $"Cannot compile expression: {expression}{Environment.NewLine}" +
                                    $"{result.ErrorMessages}{Environment.NewLine}" +
                                    $"Generated code: {Environment.NewLine}{template}";
                    function = default;
                    return false;
                }
            }
            else
            {
                errorMessages = "Cannot find c# compiler";
                function = default;
                return false;
            }
        }
    }
}
hol353 commented 1 year ago

Closing this for now.