Skip to content

Commit

Permalink
groot/rtree: implement a Formula interpreter
Browse files Browse the repository at this point in the history
Fixes #634.
  • Loading branch information
sbinet committed Apr 15, 2020
1 parent d27bd4f commit f6acb63
Show file tree
Hide file tree
Showing 6 changed files with 313 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -7,6 +7,7 @@ require (
github.com/apache/arrow/go/arrow v0.0.0-20200403134915-89ce1cadb678
github.com/astrogo/fitsio v0.1.0
github.com/campoy/embedmd v1.0.0
github.com/containous/yaegi v0.8.1
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/gonuts/binary v0.2.0
github.com/gonuts/commander v0.1.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Expand Up @@ -17,6 +17,8 @@ github.com/astrogo/fitsio v0.1.0/go.mod h1:AMazbBDPn8fcAglKAWIR5+5iDBnBv78pf6UHm
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/campoy/embedmd v1.0.0 h1:V4kI2qTJJLf4J29RzI/MAt2c3Bl4dQSYPuflzwFH2hY=
github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8=
github.com/containous/yaegi v0.8.1 h1:MYf4NuROUpNkcFBeRQ8qu4M34fRXNP7GCxTEVgcSrr0=
github.com/containous/yaegi v0.8.1/go.mod h1:Yj82MHpXQ9/h3ukzc2numJQ/Wr4+M3C9YLMzNjFtd3o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down
55 changes: 55 additions & 0 deletions groot/rtree/example_reader_test.go
Expand Up @@ -204,3 +204,58 @@ func ExampleReader_withReadVarsFromStruct() {
// evt[2]: 3, 3.3, tres
// evt[3]: 4, 4.4, quatro
}

func ExampleReader_withFormula() {
f, err := groot.Open("../testdata/simple.root")
if err != nil {
log.Fatalf("could not open ROOT file: %+v", err)
}
defer f.Close()

o, err := f.Get("tree")
if err != nil {
log.Fatalf("could not retrieve ROOT tree: %+v", err)
}
t := o.(rtree.Tree)

var (
data struct {
V1 int32 `groot:"one"`
V2 float32 `groot:"two"`
V3 string `groot:"three"`
}
rvars = rtree.ReadVarsFromStruct(&data)
)

r, err := rtree.NewReader(t, rvars)
if err != nil {
log.Fatalf("could not create tree reader: %+v", err)
}
defer r.Close()

f64, err := r.Formula("float64(two*10) + float64(1000*one) + float64(100*len(three))", nil)
if err != nil {
log.Fatalf("could not create formula: %+v", err)
}

fstr, err := r.Formula(`fmt.Sprintf("%q: %v, %q: %v, %q: %v", "one", one, "two", two, "three", three)`, []string{"fmt"})
if err != nil {
log.Fatalf("could not create formula: %+v", err)
}

err = r.Read(func(ctx rtree.RCtx) error {
valf64 := f64.Eval().(float64)
valstr := fstr.Eval().(string)
fmt.Printf("evt[%d]: %v, %v, %v -> %g | %s\n", ctx.Entry, data.V1, data.V2, data.V3, valf64, valstr)
return nil
})
if err != nil {
log.Fatalf("could not process tree: %+v", err)
}

// Output:
// evt[0]: 1, 1.1, uno -> 1311 | "one": 1, "two": 1.1, "three": uno
// evt[1]: 2, 2.2, dos -> 2322 | "one": 2, "two": 2.2, "three": dos
// evt[2]: 3, 3.3, tres -> 3433 | "one": 3, "two": 3.3, "three": tres
// evt[3]: 4, 4.4, quatro -> 4644 | "one": 4, "two": 4.4, "three": quatro
}
86 changes: 86 additions & 0 deletions groot/rtree/formula.go
@@ -0,0 +1,86 @@
// Copyright 2020 The go-hep Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package rtree

import (
"fmt"
"reflect"
"strings"

"github.com/containous/yaegi/interp"
"github.com/containous/yaegi/stdlib"
)

// Formula is a mathematical formula bound to variables (branches) of
// a given ROOT tree.
//
// Formulae are attached to a rtree.Reader.
type Formula struct {
r *Reader
expr string
prog string
eval *interp.Interpreter
fct func() interface{}
}

func newFormula(r *Reader, expr string, imports []string) (Formula, error) {
var (
eval = interp.New(interp.Options{})
pkg = "groot_rtree"
uses = interp.Exports{
pkg: make(map[string]reflect.Value),
}
prog = new(strings.Builder)
)

for _, name := range imports {
if _, ok := stdlib.Symbols[name]; !ok {
return Formula{}, fmt.Errorf("rtree: no known stdlib import for %q", name)
}
fmt.Fprintf(prog, "import %q\n", name)
}

fmt.Fprintf(prog, "import %q\n", pkg)
fmt.Fprintf(prog, "func _groot_rtree_func_eval() interface{} {\n")

for _, rvar := range r.rvars {
name := "Var_" + rvar.Name
uses[pkg][name] = reflect.ValueOf(rvar.Value)
// FIXME(sbinet): only load rvars that are actually used.
fmt.Fprintf(prog, "\t%s := *%s.%s // %T\n", rvar.Name, pkg, name, rvar.Value)
}

eval.Use(stdlib.Symbols)
eval.Use(uses)

fmt.Fprintf(prog,
"\t_groot_return := %s\n\treturn &_groot_return\n}",
expr,
)

_, err := eval.Eval(prog.String())
if err != nil {
return Formula{}, fmt.Errorf("rtree: could not define formula eval-func: %w", err)
}

f, err := eval.Eval("_groot_rtree_func_eval")
if err != nil {
return Formula{}, fmt.Errorf("rtree: could not retrieve formula eval-func: %w", err)
}

form := Formula{
r: r,
expr: expr,
prog: prog.String(),
eval: eval,
fct: f.Interface().(func() interface{}),
}

return form, nil
}

func (form *Formula) Eval() interface{} {
return reflect.ValueOf(form.fct()).Elem().Interface()
}
155 changes: 155 additions & 0 deletions groot/rtree/formula_test.go
@@ -0,0 +1,155 @@
// Copyright 2020 The go-hep Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package rtree

import (
"fmt"
"reflect"
"testing"

"go-hep.org/x/hep/groot/riofs"
"go-hep.org/x/hep/groot/root"
)

func TestFormula(t *testing.T) {
for _, tc := range []struct {
fname string
tname string
expr string
imports []string
want []interface{}
err error
}{
{
fname: "../testdata/simple.root",
tname: "tree",
expr: "one",
want: []interface{}{int32(1), int32(2)},
},
{
fname: "../testdata/simple.root",
tname: "tree",
expr: "one*one",
want: []interface{}{int32(1), int32(4)},
},
{
fname: "../testdata/simple.root",
tname: "tree",
expr: "math.Sqrt(float64(one*one))",
imports: []string{"math"},
want: []interface{}{float64(1), float64(2)},
},
{
fname: "../testdata/simple.root",
tname: "tree",
expr: `fmt.Sprintf("%d", one)`,
imports: []string{"fmt"},
want: []interface{}{"1", "2"},
},
{
fname: "../testdata/leaves.root",
tname: "tree",
expr: "ArrU64",
want: []interface{}{[10]uint64{}, [10]uint64{1, 1, 1, 1, 1, 1, 1, 1, 1, 1}},
},
{
fname: "../testdata/leaves.root",
tname: "tree",
expr: "ArrU64[0]",
want: []interface{}{uint64(0), uint64(1)},
},
{
fname: "../testdata/leaves.root",
tname: "tree",
expr: "D32",
want: []interface{}{root.Double32(0), root.Double32(1)},
},
{
fname: "../testdata/leaves.root",
tname: "tree",
expr: "float64(D32)+float64(len(SliI64))",
want: []interface{}{0.0, 2.0},
},
{
fname: "../testdata/simple.root",
tname: "tree",
expr: "ones",
err: fmt.Errorf("rtree: could not create Formula: rtree: could not define formula eval-func: 6:19: undefined: ones"),
},
{
fname: "../testdata/simple.root",
tname: "tree",
expr: "one",
imports: []string{"go-hep.org/x/hep/groot"},
err: fmt.Errorf(`rtree: could not create Formula: rtree: no known stdlib import for "go-hep.org/x/hep/groot"`),
},
{
fname: "../testdata/simple.root",
tname: "tree",
expr: "one+three",
err: fmt.Errorf(`rtree: could not create Formula: rtree: could not define formula eval-func: 6:19: mismatched types .int32 and .string`),
},
{
fname: "../testdata/simple.root",
tname: "tree",
expr: "math.Sqrt(float64(one))",
err: fmt.Errorf(`rtree: could not create Formula: rtree: could not define formula eval-func: 6:19: undefined: math`),
},
} {
t.Run(tc.expr, func(t *testing.T) {
f, err := riofs.Open(tc.fname)
if err != nil {
t.Fatal(err)
}
defer f.Close()

o, err := riofs.Dir(f).Get(tc.tname)
if err != nil {
t.Fatal(err)
}

tree := o.(Tree)

r, err := NewReader(tree, NewReadVars(tree), WithRange(0, 2))
if err != nil {
t.Fatal(err)
}
defer r.Close()

form, err := r.Formula(tc.expr, tc.imports)
switch {
case err != nil && tc.err != nil:
if got, want := err.Error(), tc.err.Error(); got != want {
t.Fatalf("invalid error.\ngot= %v\nwant=%v", got, want)
}
return
case err != nil && tc.err == nil:
t.Fatalf("unexpected error: %+v", err)
case err == nil && tc.err != nil:
t.Fatalf("expected an error: %v (got=nil)", tc.err)
case err == nil && tc.err == nil:
// ok.
}

defer func() {
e := recover()
if e != nil {
t.Fatalf("could not run form-eval:\n%s\n%+v", form.prog, e)
}
}()

err = r.Read(func(ctx RCtx) error {
got := form.Eval()
if got, want := got, tc.want[ctx.Entry]; !reflect.DeepEqual(got, want) {
return fmt.Errorf("entry[%d]: invalid form-eval:\ngot=%v (%T)\nwant=%v (%T)", ctx.Entry, got, got, want, want)
}
return nil
})
if err != nil {
t.Fatalf("error: %+v", err)
}
})
}
}
14 changes: 14 additions & 0 deletions groot/rtree/reader.go
Expand Up @@ -134,6 +134,8 @@ type Reader struct {
scan *Scanner
beg int64
end int64

evals []Formula
}

// ReadOption configures how a ROOT tree should be traversed.
Expand Down Expand Up @@ -213,6 +215,7 @@ func (r *Reader) Close() error {
}
err := r.scan.Close()
r.scan = nil
r.evals = nil
return err
}

Expand Down Expand Up @@ -249,3 +252,14 @@ func (r *Reader) Read(f func(ctx RCtx) error) error {

return nil
}

// Formula creates a new formula based on the provided expression and
// the list of stdlib imports.
func (r *Reader) Formula(expr string, imports []string) (Formula, error) {
f, err := newFormula(r, expr, imports)
if err != nil {
return Formula{}, fmt.Errorf("rtree: could not create Formula: %w", err)
}
r.evals = append(r.evals, f)
return f, nil
}

0 comments on commit f6acb63

Please sign in to comment.