github.com/vlifesystems/rulehunter@v0.0.0-20180501090014-673078aa4a83/report/report.go (about)

     1  // Copyright (C) 2016-2018 vLife Systems Ltd <http://vlifesystems.com>
     2  // Licensed under an MIT licence.  Please see LICENSE.md for details.
     3  
     4  package report
     5  
     6  import (
     7  	"encoding/json"
     8  	"errors"
     9  	"fmt"
    10  	"io/ioutil"
    11  	"math"
    12  	"os"
    13  	"path/filepath"
    14  	"sort"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/lawrencewoodman/dexpr"
    19  	"github.com/lawrencewoodman/dlit"
    20  	rhkaggregator "github.com/vlifesystems/rhkit/aggregator"
    21  	rhkassessment "github.com/vlifesystems/rhkit/assessment"
    22  	"github.com/vlifesystems/rhkit/description"
    23  	"github.com/vlifesystems/rhkit/rule"
    24  	"github.com/vlifesystems/rulehunter/config"
    25  	"github.com/vlifesystems/rulehunter/internal"
    26  )
    27  
    28  type Aggregator struct {
    29  	Name          string `json:"name"`
    30  	OriginalValue string `json:"originalValue"`
    31  	RuleValue     string `json:"ruleValue"`
    32  	Difference    string `json:"difference"`
    33  }
    34  
    35  type Goal struct {
    36  	Expr           string `json:"expr"`
    37  	OriginalPassed bool   `json:"originalPassed"`
    38  	RulePassed     bool   `json:"rulePassed"`
    39  }
    40  
    41  type Assessment struct {
    42  	Rule        string        `json:"rule"`
    43  	Aggregators []*Aggregator `json:"aggregators"`
    44  	Goals       []*Goal       `json:"goals"`
    45  }
    46  
    47  func (a *Assessment) String() string {
    48  	return fmt.Sprintf("{Rule: %s, Aggregators: %v, Goals: %v}",
    49  		a.Rule, a.Aggregators, a.Goals)
    50  }
    51  
    52  func (g *Goal) String() string {
    53  	return fmt.Sprintf("{Expr: %s, OriginalPassed: %t, RulePassed: %t}",
    54  		g.Expr, g.OriginalPassed, g.RulePassed)
    55  }
    56  
    57  func (a *Aggregator) String() string {
    58  	return fmt.Sprintf(
    59  		"{Name: %s, OriginalValue: %s, RuleValue: %s, Difference: %s}",
    60  		a.Name, a.OriginalValue, a.RuleValue, a.Difference,
    61  	)
    62  }
    63  
    64  type Report struct {
    65  	Mode               ModeKind                  `json:"mode"`
    66  	Title              string                    `json:"title"`
    67  	Tags               []string                  `json:"tags"`
    68  	Category           string                    `json:"category"`
    69  	Stamp              time.Time                 `json:"stamp"`
    70  	ExperimentFilename string                    `json:"experimentFilename"`
    71  	NumRecords         int64                     `json:"numRecords"`
    72  	SortOrder          []rhkassessment.SortOrder `json:"sortOrder"`
    73  	Aggregators        []AggregatorDesc          `json:"aggregators"`
    74  	Description        *description.Description  `json:"description"`
    75  	Assessments        []*Assessment             `json:"assessments"`
    76  }
    77  
    78  type AggregatorDesc struct {
    79  	Name string `json:"name"`
    80  	Kind string `json:"kind"`
    81  	Arg  string `json:"arg"`
    82  }
    83  
    84  // Which mode was the report run in
    85  type ModeKind int
    86  
    87  const (
    88  	Train ModeKind = iota
    89  	Test
    90  )
    91  
    92  func (m ModeKind) String() string {
    93  	if m == Train {
    94  		return "train"
    95  	}
    96  	return "test"
    97  }
    98  
    99  func New(
   100  	mode ModeKind,
   101  	title string,
   102  	desc *description.Description,
   103  	assessment *rhkassessment.Assessment,
   104  	aggregators []rhkaggregator.Spec,
   105  	sortOrder []rhkassessment.SortOrder,
   106  	experimentFilename string,
   107  	tags []string,
   108  	category string,
   109  ) *Report {
   110  	assessment.Sort(sortOrder)
   111  	assessment.Refine()
   112  
   113  	aggregatorDescs := make([]AggregatorDesc, len(aggregators))
   114  	for i, as := range aggregators {
   115  		aggregatorDescs[i] = AggregatorDesc{
   116  			Name: as.Name(),
   117  			Kind: as.Kind(),
   118  			Arg:  as.Arg(),
   119  		}
   120  	}
   121  
   122  	return &Report{
   123  		Mode:               mode,
   124  		Title:              title,
   125  		Tags:               tags,
   126  		Category:           category,
   127  		Stamp:              time.Now(),
   128  		ExperimentFilename: experimentFilename,
   129  		NumRecords:         assessment.NumRecords,
   130  		SortOrder:          sortOrder,
   131  		Aggregators:        aggregatorDescs,
   132  		Assessments:        makeAssessments(assessment),
   133  		Description:        desc,
   134  	}
   135  }
   136  
   137  func (r *Report) WriteJSON(config *config.Config) error {
   138  	// File mode permission:
   139  	// No special permission bits
   140  	// User: Read, Write
   141  	// Group: Read
   142  	// Other: None
   143  	const modePerm = 0640
   144  	json, err := json.Marshal(r)
   145  	if err != nil {
   146  		return err
   147  	}
   148  	buildFilename :=
   149  		internal.MakeBuildFilename(r.Mode.String(), r.Category, r.Title)
   150  	reportFilename := filepath.Join(config.BuildDir, "reports", buildFilename)
   151  	return ioutil.WriteFile(reportFilename, json, modePerm)
   152  }
   153  
   154  // LoadJSON loads a report from the specified reportFilename in
   155  // reports directory of cfg.BuildDir. Following the reportFilename you
   156  // can specify an optional number of times to try to decode the JSON file.
   157  // This can be useful in situations where you might try to read the
   158  // JSON file before it has been completely written.
   159  func LoadJSON(
   160  	cfg *config.Config,
   161  	reportFilename string,
   162  	args ...int,
   163  ) (*Report, error) {
   164  	const sleep = 200 * time.Millisecond
   165  	var report Report
   166  	filename := filepath.Join(cfg.BuildDir, "reports", reportFilename)
   167  
   168  	maxTries := 1
   169  	if len(args) == 1 {
   170  		maxTries = args[0]
   171  	} else if len(args) > 1 {
   172  		panic("too many arguments for function")
   173  	}
   174  	for tries := 1; ; tries++ {
   175  		f, err := os.Open(filename)
   176  		if err != nil {
   177  			return nil, err
   178  		}
   179  		defer f.Close()
   180  
   181  		dec := json.NewDecoder(f)
   182  		if err = dec.Decode(&report); err != nil {
   183  			if tries > maxTries {
   184  				return nil, fmt.Errorf("can't decode JSON file: %s, %s", filename, err)
   185  			} else {
   186  				time.Sleep(sleep)
   187  			}
   188  		} else {
   189  			break
   190  		}
   191  	}
   192  	return &report, nil
   193  }
   194  
   195  func makeAssessments(assessment *rhkassessment.Assessment) []*Assessment {
   196  	trueRuleAssessment, err := getTrueRuleAssessment(assessment)
   197  	if err != nil {
   198  		panic(err)
   199  	}
   200  
   201  	trueAggregators := trueRuleAssessment.Aggregators
   202  	trueGoals := trueRuleAssessment.Goals
   203  	assessments := make([]*Assessment, len(assessment.RuleAssessments))
   204  	for i, ruleAssessment := range assessment.RuleAssessments {
   205  		assessments[i] = &Assessment{
   206  			Rule: ruleAssessment.Rule.String(),
   207  			Aggregators: makeAggregators(
   208  				trueAggregators,
   209  				ruleAssessment.Aggregators,
   210  			),
   211  			Goals: makeGoals(trueGoals, ruleAssessment.Goals),
   212  		}
   213  	}
   214  	return assessments
   215  }
   216  
   217  func makeAggregators(
   218  	trueAggregators map[string]*dlit.Literal,
   219  	ruleAggregators map[string]*dlit.Literal,
   220  ) []*Aggregator {
   221  	aggregatorNames := getSortedAggregatorNames(ruleAggregators)
   222  	aggregators := make([]*Aggregator, len(ruleAggregators))
   223  	for j, aggregatorName := range aggregatorNames {
   224  		aggregator := ruleAggregators[aggregatorName]
   225  		difference :=
   226  			calcTrueAggregatorDiff(trueAggregators, aggregatorName, aggregator)
   227  		aggregators[j] = &Aggregator{
   228  			Name:          aggregatorName,
   229  			OriginalValue: trueAggregators[aggregatorName].String(),
   230  			RuleValue:     aggregator.String(),
   231  			Difference:    difference,
   232  		}
   233  	}
   234  	return aggregators
   235  }
   236  
   237  func makeGoals(
   238  	trueGoals []*rhkassessment.GoalAssessment,
   239  	ruleGoals []*rhkassessment.GoalAssessment,
   240  ) []*Goal {
   241  	goals := make([]*Goal, len(ruleGoals))
   242  	for i, g := range ruleGoals {
   243  		goals[i] = &Goal{
   244  			Expr:           g.Expr,
   245  			OriginalPassed: trueGoals[i].Passed,
   246  			RulePassed:     g.Passed,
   247  		}
   248  	}
   249  	return goals
   250  }
   251  
   252  func getSortedAggregatorNames(aggregators map[string]*dlit.Literal) []string {
   253  	aggregatorNames := make([]string, len(aggregators))
   254  	j := 0
   255  	for aggregatorName, _ := range aggregators {
   256  		aggregatorNames[j] = aggregatorName
   257  		j++
   258  	}
   259  	sort.Strings(aggregatorNames)
   260  	return aggregatorNames
   261  }
   262  
   263  func getTrueRuleAssessment(
   264  	assessment *rhkassessment.Assessment,
   265  ) (*rhkassessment.RuleAssessment, error) {
   266  	for _, ra := range assessment.RuleAssessments {
   267  		if _, isTrueRule := ra.Rule.(rule.True); isTrueRule {
   268  			return ra, nil
   269  		}
   270  	}
   271  
   272  	return nil, errors.New("can't find true() rule")
   273  }
   274  
   275  func numDecPlaces(s string) int {
   276  	i := strings.IndexByte(s, '.')
   277  	if i > -1 {
   278  		s = strings.TrimRight(s, "0")
   279  		return len(s) - i - 1
   280  	}
   281  	return 0
   282  }
   283  
   284  type CantConvertToTypeError struct {
   285  	Kind  string
   286  	Value *dlit.Literal
   287  }
   288  
   289  func (e CantConvertToTypeError) Error() string {
   290  	return fmt.Sprintf("can't convert to %s: %s", e.Kind, e.Value)
   291  }
   292  
   293  // roundTo returns a number n,  rounded to a number of decimal places dp.
   294  // This uses round half-up to tie-break
   295  func roundTo(n *dlit.Literal, dp int) (*dlit.Literal, error) {
   296  
   297  	if _, isInt := n.Int(); isInt {
   298  		return n, nil
   299  	}
   300  
   301  	x, isFloat := n.Float()
   302  	if !isFloat {
   303  		if err := n.Err(); err != nil {
   304  			return n, err
   305  		}
   306  		err := CantConvertToTypeError{Kind: "float", Value: n}
   307  		r := dlit.MustNew(err)
   308  		return r, err
   309  	}
   310  
   311  	// Prevent rounding errors where too high dp is used
   312  	xNumDP := numDecPlaces(n.String())
   313  	if dp > xNumDP {
   314  		dp = xNumDP
   315  	}
   316  	shift := math.Pow(10, float64(dp))
   317  	return dlit.New(math.Floor(.5+x*shift) / shift)
   318  }
   319  
   320  func calcTrueAggregatorDiff(
   321  	trueAggregators map[string]*dlit.Literal,
   322  	aggregatorName string,
   323  	aggregatorValue *dlit.Literal,
   324  ) string {
   325  	funcs := map[string]dexpr.CallFun{}
   326  	maxDP := numDecPlaces(aggregatorValue.String())
   327  	trueAggregatorValueDP := numDecPlaces(trueAggregators[aggregatorName].String())
   328  	if trueAggregatorValueDP > maxDP {
   329  		maxDP = trueAggregatorValueDP
   330  	}
   331  	diffExpr := dexpr.MustNew("r - t", funcs)
   332  	vars := map[string]*dlit.Literal{
   333  		"r": aggregatorValue,
   334  		"t": trueAggregators[aggregatorName],
   335  	}
   336  	differenceL := diffExpr.Eval(vars)
   337  	if err := differenceL.Err(); err != nil {
   338  		return "N/A"
   339  	}
   340  	roundedDifferenceL, err := roundTo(differenceL, maxDP)
   341  	if err != nil {
   342  		return "N/A"
   343  	}
   344  	return roundedDifferenceL.String()
   345  }