Free cookie consent management tool by TermsFeed Policy Generator

source: branches/GP.Symbols (TimeLag, Diff, Integral)/HeuristicLab.Problems.DataAnalysis/3.3/Symbolic/SymbolicSimplifier.cs @ 5551

Last change on this file since 5551 was 5109, checked in by mkommend, 14 years ago

Corrected SymbolicSimplifier.Negate in GP.Symbols branch (ticket #1340).

File size: 21.9 KB
Line 
1#region License Information
2/* HeuristicLab
3 * Copyright (C) 2002-2010 Heuristic and Evolutionary Algorithms Laboratory (HEAL)
4 *
5 * This file is part of HeuristicLab.
6 *
7 * HeuristicLab is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * HeuristicLab is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with HeuristicLab. If not, see <http://www.gnu.org/licenses/>.
19 */
20#endregion
21
22using System;
23using System.Collections.Generic;
24using System.Diagnostics;
25using System.Linq;
26using HeuristicLab.Common;
27using HeuristicLab.Encodings.SymbolicExpressionTreeEncoding;
28using HeuristicLab.Encodings.SymbolicExpressionTreeEncoding.Symbols;
29using HeuristicLab.Problems.DataAnalysis.Symbolic.Symbols;
30
31namespace HeuristicLab.Problems.DataAnalysis.Symbolic {
32  /// <summary>
33  /// Simplistic simplifier for arithmetic expressions
34  /// </summary>
35  public class SymbolicSimplifier {
36    private Addition addSymbol = new Addition();
37    private Multiplication mulSymbol = new Multiplication();
38    private Division divSymbol = new Division();
39    private Constant constSymbol = new Constant();
40    private Variable varSymbol = new Variable();
41
42    public SymbolicExpressionTree Simplify(SymbolicExpressionTree originalTree) {
43      var clone = (SymbolicExpressionTreeNode)originalTree.Root.Clone();
44      // macro expand (initially no argument trees)
45      var macroExpandedTree = MacroExpand(clone, clone.SubTrees[0], new List<SymbolicExpressionTreeNode>());
46      SymbolicExpressionTreeNode rootNode = (new ProgramRootSymbol()).CreateTreeNode();
47      rootNode.AddSubTree(GetSimplifiedTree(macroExpandedTree));
48      return new SymbolicExpressionTree(rootNode);
49    }
50
51    // the argumentTrees list contains already expanded trees used as arguments for invocations
52    private SymbolicExpressionTreeNode MacroExpand(SymbolicExpressionTreeNode root, SymbolicExpressionTreeNode node, IList<SymbolicExpressionTreeNode> argumentTrees) {
53      List<SymbolicExpressionTreeNode> subtrees = new List<SymbolicExpressionTreeNode>(node.SubTrees);
54      while (node.SubTrees.Count > 0) node.RemoveSubTree(0);
55      if (node.Symbol is InvokeFunction) {
56        var invokeSym = node.Symbol as InvokeFunction;
57        var defunNode = FindFunctionDefinition(root, invokeSym.FunctionName);
58        var macroExpandedArguments = new List<SymbolicExpressionTreeNode>();
59        foreach (var subtree in subtrees) {
60          macroExpandedArguments.Add(MacroExpand(root, subtree, argumentTrees));
61        }
62        return MacroExpand(root, defunNode, macroExpandedArguments);
63      } else if (node.Symbol is Argument) {
64        var argSym = node.Symbol as Argument;
65        // return the correct argument sub-tree (already macro-expanded)
66        return (SymbolicExpressionTreeNode)argumentTrees[argSym.ArgumentIndex].Clone();
67      } else {
68        // recursive application
69        foreach (var subtree in subtrees) {
70          node.AddSubTree(MacroExpand(root, subtree, argumentTrees));
71        }
72        return node;
73      }
74    }
75
76    private SymbolicExpressionTreeNode FindFunctionDefinition(SymbolicExpressionTreeNode root, string functionName) {
77      foreach (var subtree in root.SubTrees.OfType<DefunTreeNode>()) {
78        if (subtree.FunctionName == functionName) return subtree.SubTrees[0];
79      }
80
81      throw new ArgumentException("Definition of function " + functionName + " not found.");
82    }
83
84
85    #region symbol predicates
86    private bool IsDivision(SymbolicExpressionTreeNode original) {
87      return original.Symbol is Division;
88    }
89
90    private bool IsMultiplication(SymbolicExpressionTreeNode original) {
91      return original.Symbol is Multiplication;
92    }
93
94    private bool IsSubtraction(SymbolicExpressionTreeNode original) {
95      return original.Symbol is Subtraction;
96    }
97
98    private bool IsAddition(SymbolicExpressionTreeNode original) {
99      return original.Symbol is Addition;
100    }
101
102    private bool IsVariable(SymbolicExpressionTreeNode original) {
103      return original.Symbol is Variable;
104    }
105
106    private bool IsConstant(SymbolicExpressionTreeNode original) {
107      return original.Symbol is Constant;
108    }
109
110    private bool IsAverage(SymbolicExpressionTreeNode original) {
111      return original.Symbol is Average;
112    }
113    private bool IsLog(SymbolicExpressionTreeNode original) {
114      return original.Symbol is Logarithm;
115    }
116    private bool IsIfThenElse(SymbolicExpressionTreeNode original) {
117      return original.Symbol is IfThenElse;
118    }
119    #endregion
120
121    /// <summary>
122    /// Creates a new simplified tree
123    /// </summary>
124    /// <param name="original"></param>
125    /// <returns></returns>
126    public SymbolicExpressionTreeNode GetSimplifiedTree(SymbolicExpressionTreeNode original) {
127      if (IsConstant(original) || IsVariable(original)) {
128        return (SymbolicExpressionTreeNode)original.Clone();
129      } else if (IsAddition(original)) {
130        return SimplifyAddition(original);
131      } else if (IsSubtraction(original)) {
132        return SimplifySubtraction(original);
133      } else if (IsMultiplication(original)) {
134        return SimplifyMultiplication(original);
135      } else if (IsDivision(original)) {
136        return SimplifyDivision(original);
137      } else if (IsAverage(original)) {
138        return SimplifyAverage(original);
139      } else if (IsLog(original)) {
140        // TODO simplify logarithm
141        return SimplifyAny(original);
142      } else if (IsIfThenElse(original)) {
143        // TODO simplify conditionals
144        return SimplifyAny(original);
145      } else if (IsAverage(original)) {
146        return SimplifyAverage(original);
147      } else {
148        return SimplifyAny(original);
149      }
150    }
151
152    #region specific simplification routines
153    private SymbolicExpressionTreeNode SimplifyAny(SymbolicExpressionTreeNode original) {
154      // can't simplify this function but simplify all subtrees
155      List<SymbolicExpressionTreeNode> subTrees = new List<SymbolicExpressionTreeNode>(original.SubTrees);
156      while (original.SubTrees.Count > 0) original.RemoveSubTree(0);
157      var clone = (SymbolicExpressionTreeNode)original.Clone();
158      List<SymbolicExpressionTreeNode> simplifiedSubTrees = new List<SymbolicExpressionTreeNode>();
159      foreach (var subTree in subTrees) {
160        simplifiedSubTrees.Add(GetSimplifiedTree(subTree));
161        original.AddSubTree(subTree);
162      }
163      foreach (var simplifiedSubtree in simplifiedSubTrees) {
164        clone.AddSubTree(simplifiedSubtree);
165      }
166      if (simplifiedSubTrees.TrueForAll(t => IsConstant(t))) {
167        SimplifyConstantExpression(clone);
168      }
169      return clone;
170    }
171
172    private SymbolicExpressionTreeNode SimplifyConstantExpression(SymbolicExpressionTreeNode original) {
173      // not yet implemented
174      return original;
175    }
176
177    private SymbolicExpressionTreeNode SimplifyAverage(SymbolicExpressionTreeNode original) {
178      if (original.SubTrees.Count == 1) {
179        return GetSimplifiedTree(original.SubTrees[0]);
180      } else {
181        // simplify expressions x0..xn
182        // make sum(x0..xn) / n
183        Trace.Assert(original.SubTrees.Count > 1);
184        var sum = original.SubTrees
185          .Select(x => GetSimplifiedTree(x))
186          .Aggregate((a, b) => MakeSum(a, b));
187        return MakeFraction(sum, MakeConstant(original.SubTrees.Count));
188      }
189    }
190
191    private SymbolicExpressionTreeNode SimplifyDivision(SymbolicExpressionTreeNode original) {
192      if (original.SubTrees.Count == 1) {
193        return Invert(GetSimplifiedTree(original.SubTrees[0]));
194      } else {
195        // simplify expressions x0..xn
196        // make multiplication (x0 * 1/(x1 * x1 * .. * xn))
197        Trace.Assert(original.SubTrees.Count > 1);
198        var simplifiedTrees = original.SubTrees.Select(x => GetSimplifiedTree(x));
199        return
200          MakeProduct(simplifiedTrees.First(), Invert(simplifiedTrees.Skip(1).Aggregate((a, b) => MakeProduct(a, b))));
201      }
202    }
203
204    private SymbolicExpressionTreeNode SimplifyMultiplication(SymbolicExpressionTreeNode original) {
205      if (original.SubTrees.Count == 1) {
206        return GetSimplifiedTree(original.SubTrees[0]);
207      } else {
208        Trace.Assert(original.SubTrees.Count > 1);
209        return original.SubTrees
210          .Select(x => GetSimplifiedTree(x))
211          .Aggregate((a, b) => MakeProduct(a, b));
212      }
213    }
214
215    private SymbolicExpressionTreeNode SimplifySubtraction(SymbolicExpressionTreeNode original) {
216      if (original.SubTrees.Count == 1) {
217        return Negate(GetSimplifiedTree(original.SubTrees[0]));
218      } else {
219        // simplify expressions x0..xn
220        // make addition (x0,-x1..-xn)
221        Trace.Assert(original.SubTrees.Count > 1);
222        var simplifiedTrees = original.SubTrees.Select(x => GetSimplifiedTree(x));
223        return simplifiedTrees.Take(1)
224          .Concat(simplifiedTrees.Skip(1).Select(x => Negate(x)))
225          .Aggregate((a, b) => MakeSum(a, b));
226      }
227    }
228
229    private SymbolicExpressionTreeNode SimplifyAddition(SymbolicExpressionTreeNode original) {
230      if (original.SubTrees.Count == 1) {
231        return GetSimplifiedTree(original.SubTrees[0]);
232      } else {
233        // simplify expression x0..xn
234        // make addition (x0..xn)
235        Trace.Assert(original.SubTrees.Count > 1);
236        return original.SubTrees
237          .Select(x => GetSimplifiedTree(x))
238          .Aggregate((a, b) => MakeSum(a, b));
239      }
240    }
241    #endregion
242
243
244
245    #region low level tree restructuring
246    // MakeFraction, MakeProduct and MakeSum take two already simplified trees and create a new simplified tree
247
248    private SymbolicExpressionTreeNode MakeFraction(SymbolicExpressionTreeNode a, SymbolicExpressionTreeNode b) {
249      if (IsConstant(a) && IsConstant(b)) {
250        // fold constants
251        return MakeConstant(((ConstantTreeNode)a).Value / ((ConstantTreeNode)b).Value);
252      } if (IsConstant(a) && !((ConstantTreeNode)a).Value.IsAlmost(1.0)) {
253        return MakeFraction(MakeConstant(1.0), MakeProduct(b, Invert(a)));
254      } else if (IsVariable(a) && IsConstant(b)) {
255        // merge constant values into variable weights
256        var constB = ((ConstantTreeNode)b).Value;
257        ((VariableTreeNode)a).Weight /= constB;
258        return a;
259      } else if (IsAddition(a) && IsConstant(b)) {
260        return a.SubTrees
261          .Select(x => GetSimplifiedTree(x))
262         .Select(x => MakeFraction(x, b))
263         .Aggregate((c, d) => MakeSum(c, d));
264      } else if (IsMultiplication(a) && IsConstant(b)) {
265        return MakeProduct(a, Invert(b));
266      } else if (IsDivision(a) && IsConstant(b)) {
267        // (a1 / a2) / c => (a1 / (a2 * c))
268        Trace.Assert(a.SubTrees.Count == 2);
269        return MakeFraction(a.SubTrees[0], MakeProduct(a.SubTrees[1], b));
270      } else if (IsDivision(a) && IsDivision(b)) {
271        // (a1 / a2) / (b1 / b2) =>
272        Trace.Assert(a.SubTrees.Count == 2);
273        Trace.Assert(b.SubTrees.Count == 2);
274        return MakeFraction(MakeProduct(a.SubTrees[0], b.SubTrees[1]), MakeProduct(a.SubTrees[1], b.SubTrees[0]));
275      } else if (IsDivision(a)) {
276        // (a1 / a2) / b => (a1 / (a2 * b))
277        Trace.Assert(a.SubTrees.Count == 2);
278        return MakeFraction(a.SubTrees[0], MakeProduct(a.SubTrees[1], b));
279      } else if (IsDivision(b)) {
280        // a / (b1 / b2) => (a * b2) / b1
281        Trace.Assert(b.SubTrees.Count == 2);
282        return MakeFraction(MakeProduct(a, b.SubTrees[1]), b.SubTrees[0]);
283      } else {
284        var div = divSymbol.CreateTreeNode();
285        div.AddSubTree(a);
286        div.AddSubTree(b);
287        return div;
288      }
289    }
290
291    private SymbolicExpressionTreeNode MakeSum(SymbolicExpressionTreeNode a, SymbolicExpressionTreeNode b) {
292      if (IsConstant(a) && IsConstant(b)) {
293        // fold constants
294        ((ConstantTreeNode)a).Value += ((ConstantTreeNode)b).Value;
295        return a;
296      } else if (IsConstant(a)) {
297        // c + x => x + c
298        // b is not constant => make sure constant is on the right
299        return MakeSum(b, a);
300      } else if (IsConstant(b) && ((ConstantTreeNode)b).Value.IsAlmost(0.0)) {
301        // x + 0 => x
302        return a;
303      } else if (IsAddition(a) && IsAddition(b)) {
304        // merge additions
305        var add = addSymbol.CreateTreeNode();
306        for (int i = 0; i < a.SubTrees.Count - 1; i++) add.AddSubTree(a.SubTrees[i]);
307        for (int i = 0; i < b.SubTrees.Count - 1; i++) add.AddSubTree(b.SubTrees[i]);
308        if (IsConstant(a.SubTrees.Last()) && IsConstant(b.SubTrees.Last())) {
309          add.AddSubTree(MakeSum(a.SubTrees.Last(), b.SubTrees.Last()));
310        } else if (IsConstant(a.SubTrees.Last())) {
311          add.AddSubTree(b.SubTrees.Last());
312          add.AddSubTree(a.SubTrees.Last());
313        } else {
314          add.AddSubTree(a.SubTrees.Last());
315          add.AddSubTree(b.SubTrees.Last());
316        }
317        MergeVariablesInSum(add);
318        return add;
319      } else if (IsAddition(b)) {
320        return MakeSum(b, a);
321      } else if (IsAddition(a) && IsConstant(b)) {
322        // a is an addition and b is a constant => append b to a and make sure the constants are merged
323        var add = addSymbol.CreateTreeNode();
324        for (int i = 0; i < a.SubTrees.Count - 1; i++) add.AddSubTree(a.SubTrees[i]);
325        if (IsConstant(a.SubTrees.Last()))
326          add.AddSubTree(MakeSum(a.SubTrees.Last(), b));
327        else {
328          add.AddSubTree(a.SubTrees.Last());
329          add.AddSubTree(b);
330        }
331        return add;
332      } else if (IsAddition(a)) {
333        // a is already an addition => append b
334        var add = addSymbol.CreateTreeNode();
335        add.AddSubTree(b);
336        foreach (var subTree in a.SubTrees) {
337          add.AddSubTree(subTree);
338        }
339        MergeVariablesInSum(add);
340        return add;
341      } else {
342        var add = addSymbol.CreateTreeNode();
343        add.AddSubTree(a);
344        add.AddSubTree(b);
345        MergeVariablesInSum(add);
346        return add;
347      }
348    }
349
350    // makes sure variable symbols in sums are combined
351    // possible improvment: combine sums of products where the products only reference the same variable
352    private void MergeVariablesInSum(SymbolicExpressionTreeNode sum) {
353      var subtrees = new List<SymbolicExpressionTreeNode>(sum.SubTrees);
354      while (sum.SubTrees.Count > 0) sum.RemoveSubTree(0);
355      var groupedVarNodes = from node in subtrees.OfType<VariableTreeNode>()
356                            group node by node.VariableName into g
357                            select g;
358      var unchangedSubTrees = subtrees.Where(t => !(t is VariableTreeNode));
359
360      foreach (var variableNodeGroup in groupedVarNodes) {
361        var weightSum = variableNodeGroup.Select(t => t.Weight).Sum();
362        var representative = variableNodeGroup.First();
363        representative.Weight = weightSum;
364        sum.AddSubTree(representative);
365      }
366      foreach (var unchangedSubtree in unchangedSubTrees)
367        sum.AddSubTree(unchangedSubtree);
368    }
369
370
371    private SymbolicExpressionTreeNode MakeProduct(SymbolicExpressionTreeNode a, SymbolicExpressionTreeNode b) {
372      if (IsConstant(a) && IsConstant(b)) {
373        // fold constants
374        ((ConstantTreeNode)a).Value *= ((ConstantTreeNode)b).Value;
375        return a;
376      } else if (IsConstant(a)) {
377        // a * $ => $ * a
378        return MakeProduct(b, a);
379      } else if (IsConstant(b) && ((ConstantTreeNode)b).Value.IsAlmost(1.0)) {
380        // $ * 1.0 => $
381        return a;
382      } else if (IsConstant(b) && IsVariable(a)) {
383        // multiply constants into variables weights
384        ((VariableTreeNode)a).Weight *= ((ConstantTreeNode)b).Value;
385        return a;
386      } else if (IsConstant(b) && IsAddition(a)) {
387        // multiply constants into additions
388        return a.SubTrees.Select(x => MakeProduct(x, b)).Aggregate((c, d) => MakeSum(c, d));
389      } else if (IsDivision(a) && IsDivision(b)) {
390        // (a1 / a2) * (b1 / b2) => (a1 * b1) / (a2 * b2)
391        Trace.Assert(a.SubTrees.Count == 2);
392        Trace.Assert(b.SubTrees.Count == 2);
393        return MakeFraction(MakeProduct(a.SubTrees[0], b.SubTrees[0]), MakeProduct(a.SubTrees[1], b.SubTrees[1]));
394      } else if (IsDivision(a)) {
395        // (a1 / a2) * b => (a1 * b) / a2
396        Trace.Assert(a.SubTrees.Count == 2);
397        return MakeFraction(MakeProduct(a.SubTrees[0], b), a.SubTrees[1]);
398      } else if (IsDivision(b)) {
399        // a * (b1 / b2) => (b1 * a) / b2
400        Trace.Assert(b.SubTrees.Count == 2);
401        return MakeFraction(MakeProduct(b.SubTrees[0], a), b.SubTrees[1]);
402      } else if (IsMultiplication(a) && IsMultiplication(b)) {
403        // merge multiplications (make sure constants are merged)
404        var mul = mulSymbol.CreateTreeNode();
405        for (int i = 0; i < a.SubTrees.Count; i++) mul.AddSubTree(a.SubTrees[i]);
406        for (int i = 0; i < b.SubTrees.Count; i++) mul.AddSubTree(b.SubTrees[i]);
407        MergeVariablesAndConstantsInProduct(mul);
408        return mul;
409      } else if (IsMultiplication(b)) {
410        return MakeProduct(b, a);
411      } else if (IsMultiplication(a)) {
412        // a is already an multiplication => append b
413        a.AddSubTree(b);
414        MergeVariablesAndConstantsInProduct(a);
415        return a;
416      } else {
417        var mul = mulSymbol.CreateTreeNode();
418        mul.SubTrees.Add(a);
419        mul.SubTrees.Add(b);
420        MergeVariablesAndConstantsInProduct(mul);
421        return mul;
422      }
423    }
424    #endregion
425
426    // helper to combine the constant factors in products and to combine variables (powers of 2, 3...)
427    private void MergeVariablesAndConstantsInProduct(SymbolicExpressionTreeNode prod) {
428      var subtrees = new List<SymbolicExpressionTreeNode>(prod.SubTrees);
429      while (prod.SubTrees.Count > 0) prod.RemoveSubTree(0);
430      var groupedVarNodes = from node in subtrees.OfType<VariableTreeNode>()
431                            group node by node.VariableName into g
432                            orderby g.Count()
433                            select g;
434      var constantProduct = (from node in subtrees.OfType<VariableTreeNode>()
435                             select node.Weight)
436                            .Concat(from node in subtrees.OfType<ConstantTreeNode>()
437                                    select node.Value)
438                            .DefaultIfEmpty(1.0)
439                            .Aggregate((c1, c2) => c1 * c2);
440
441      var unchangedSubTrees = from tree in subtrees
442                              where !(tree is VariableTreeNode)
443                              where !(tree is ConstantTreeNode)
444                              select tree;
445
446      foreach (var variableNodeGroup in groupedVarNodes) {
447        var representative = variableNodeGroup.First();
448        representative.Weight = 1.0;
449        if (variableNodeGroup.Count() > 1) {
450          var poly = mulSymbol.CreateTreeNode();
451          for (int p = 0; p < variableNodeGroup.Count(); p++) {
452            poly.AddSubTree((SymbolicExpressionTreeNode)representative.Clone());
453          }
454          prod.AddSubTree(poly);
455        } else {
456          prod.AddSubTree(representative);
457        }
458      }
459
460      foreach (var unchangedSubtree in unchangedSubTrees)
461        prod.AddSubTree(unchangedSubtree);
462
463      if (!constantProduct.IsAlmost(1.0)) {
464        prod.AddSubTree(MakeConstant(constantProduct));
465      }
466    }
467
468
469    #region helper functions
470    /// <summary>
471    /// x => x * -1
472    /// Doesn't create new trees and manipulates x
473    /// </summary>
474    /// <param name="x"></param>
475    /// <returns>-x</returns>
476    private SymbolicExpressionTreeNode Negate(SymbolicExpressionTreeNode x) {
477      if (IsConstant(x)) {
478        ((ConstantTreeNode)x).Value *= -1;
479      } else if (IsVariable(x)) {
480        var variableTree = (VariableTreeNode)x;
481        variableTree.Weight *= -1.0;
482      } else if (IsAddition(x)) {
483        // (x0 + x1 + .. + xn) * -1 => (-x0 + -x1 + .. + -xn)       
484        for (int i = 0; i < x.SubTrees.Count; i++)
485          x.SubTrees[i] = Negate(x.SubTrees[i]);
486      } else if (IsMultiplication(x) || IsDivision(x)) {
487        // x0 * x1 * .. * xn * -1 => x0 * x1 * .. * -xn
488        x.SubTrees[x.SubTrees.Count - 1] = Negate(x.SubTrees.Last()); // last is maybe a constant, prefer to negate the constant
489      } else {
490        // any other function
491        return MakeProduct(x, MakeConstant(-1));
492      }
493      return x;
494    }
495
496    /// <summary>
497    /// x => 1/x
498    /// Doesn't create new trees and manipulates x
499    /// </summary>
500    /// <param name="x"></param>
501    /// <returns></returns>
502    private SymbolicExpressionTreeNode Invert(SymbolicExpressionTreeNode x) {
503      if (IsConstant(x)) {
504        return MakeConstant(1.0 / ((ConstantTreeNode)x).Value);
505      } else if (IsDivision(x)) {
506        Trace.Assert(x.SubTrees.Count == 2);
507        return MakeFraction(x.SubTrees[1], x.SubTrees[0]);
508      } else {
509        // any other function
510        return MakeFraction(MakeConstant(1), x);
511      }
512    }
513
514    private SymbolicExpressionTreeNode MakeConstant(double value) {
515      ConstantTreeNode constantTreeNode = (ConstantTreeNode)(constSymbol.CreateTreeNode());
516      constantTreeNode.Value = value;
517      return (SymbolicExpressionTreeNode)constantTreeNode;
518    }
519
520    private SymbolicExpressionTreeNode MakeVariable(double weight, string name) {
521      var tree = (VariableTreeNode)varSymbol.CreateTreeNode();
522      tree.Weight = weight;
523      tree.VariableName = name;
524      return tree;
525    }
526    #endregion
527  }
528}
Note: See TracBrowser for help on using the repository browser.