From 01376c0e1fd468884d3229a3da778dc38564bae7 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Sat, 17 Oct 2026 22:13:54 +0200 Subject: [PATCH 01/20] WIP: conversion layer, reflection binder, Stats --- conv.go | 394 +++++++++++++ dataframe.go | 5 + datafunc.go | 55 ++ doc.go | 29 + enums.go | 9 + enums_gen.go | 160 +++++ go.mod | 414 +++++++++++++ go.sum | 1176 +++++++++++++++++++++++++++++++++++++ integrate.go | 5 + internal/genenums/main.go | 206 +++++++ optimize.go | 5 + plot.go | 5 + reflect.go | 444 ++++++++++++++ smoke_test.go | 46 ++ special.go | 5 + stats.go | 516 ++++++++++++++++ 16 files changed, 3474 insertions(+) create mode 100644 conv.go create mode 100644 dataframe.go create mode 100644 datafunc.go create mode 100644 doc.go create mode 100644 enums.go create mode 100644 enums_gen.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 integrate.go create mode 100644 internal/genenums/main.go create mode 100644 optimize.go create mode 100644 plot.go create mode 100644 reflect.go create mode 100644 smoke_test.go create mode 100644 special.go create mode 100644 stats.go diff --git a/conv.go b/conv.go new file mode 100644 index 0000000..4ed4635 --- /dev/null +++ b/conv.go @@ -0,0 +1,394 @@ +package ruby + +import ( + "fmt" + "math/big" + "reflect" + "sort" + "strings" + "unicode" + + rb "github.com/go-embedded-ruby/ruby" + nd "github.com/go-ndarray/ndarray" +) + +// This file converts between Ruby values, as rbgo hands them to Go, and the +// Go types of the bound APIs. One rule set serves positional arguments, +// keyword arguments (which fill the fields of a Go option struct) and results. + +func argError(format string, args ...any) error { + return rb.NewError("ArgumentError", fmt.Sprintf(format, args...)) +} + +func typeError(x any, into string) error { + return rb.NewError("TypeError", fmt.Sprintf("no implicit conversion of %s into %s", describe(x), into)) +} + +// describe names a converted Ruby value (see rb.Value.Go) the way MRI's +// conversion errors do: nil, true and false by value, everything else by +// class. +func describe(x any) string { + switch v := x.(type) { + case nil: + return "nil" + case bool: + return fmt.Sprint(v) + case int64, *big.Int: + return "Integer" + case float64: + return "Float" + case complex128: + return "Complex" + case *big.Rat: + return "Rational" + case string: + return "String" + case rb.Symbol: + return "Symbol" + case []any: + return "Array" + case map[any]any: + return "Hash" + case *nd.Array: + return "NDArray" + case rb.Value: + return v.ClassName() + } + return fmt.Sprintf("%T", x) +} + +// goError is the Ruby exception for an error a Go library returned: an +// ArgumentError carrying its message, which is what SciPy raises (ValueError) +// for the same inputs. +func goError(err error) error { return rb.NewError("ArgumentError", err.Error()) } + +// toFloat reads a real number: an Integer, a Float, a big Integer or a +// Rational. +func toFloat(v any) (float64, error) { + switch x := v.(type) { + case float64: + return x, nil + case int64: + return float64(x), nil + case *big.Int: + f, _ := new(big.Float).SetInt(x).Float64() + return f, nil + case *big.Rat: + f, _ := x.Float64() + return f, nil + } + return 0, typeError(v, "Float") +} + +// toInt reads an Integer that fits an int. +func toInt(v any) (int64, error) { + if x, ok := v.(int64); ok { + return x, nil + } + return 0, typeError(v, "Integer") +} + +// toFloats reads a vector: an Array of real numbers, or an NDArray (any +// shape, read in row-major order). +func toFloats(v any) ([]float64, error) { + switch x := v.(type) { + case *nd.Array: + return arrayFloats(x), nil + case []any: + out := make([]float64, len(x)) + for i, e := range x { + f, err := toFloat(e) + if err != nil { + return nil, err + } + out[i] = f + } + return out, nil + } + return nil, typeError(v, "an Array of Floats") +} + +// arrayFloats copies an array's elements, row-major, as float64. +func arrayFloats(a *nd.Array) []float64 { + flat := a.Flatten() + out := make([]float64, flat.Size()) + for i := range out { + out[i] = flat.At(i) + } + return out +} + +// data reads the observations of a statistic: an NDArray is passed to Go as +// it is, an Array becomes a []float64. It is the Go side's +// []float64 | *ndarray.Array. +func data(v any) (any, error) { + if a, ok := v.(*nd.Array); ok { + return a, nil + } + return toFloats(v) +} + +// ndArray reads an NDArray, or an Array of numbers as a 1-d one. +func ndArray(v any) (*nd.Array, error) { + d, err := data(v) + if err != nil { + return nil, err + } + if a, ok := d.(*nd.Array); ok { + return a, nil + } + f := d.([]float64) + a, _ := nd.FromData(f, len(f)) + return a, nil +} + +// vec is a result vector as Ruby gets it: an NDArray. +func vec(x []float64) *nd.Array { + if x == nil { + return nil + } + a, _ := nd.FromData(append([]float64(nil), x...), len(x)) + return a +} + +// converters convert to a Go type that the general rules do not reach, such +// as optimize.Bound; the files binding those types register them. +var converters = map[reflect.Type]func(any) (reflect.Value, error){} + +// convert converts a Ruby value, as rb.Value.Go gives it, to the Go type t. +func convert(v any, t reflect.Type) (reflect.Value, error) { + if c, ok := converters[t]; ok { + return c(v) + } + if vals, ok := enumTable[t]; ok { + return enumValueOf(v, t, vals) + } + switch t.Kind() { + case reflect.Float64, reflect.Float32: + f, err := toFloat(v) + return reflect.ValueOf(f).Convert(t), err + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + i, err := toInt(v) + return reflect.ValueOf(i).Convert(t), err + case reflect.Bool: + b, ok := v.(bool) + if !ok { + return reflect.Value{}, typeError(v, "true or false") + } + return reflect.ValueOf(b), nil + case reflect.String: + s, err := toString(v) + return reflect.ValueOf(s).Convert(t), err + case reflect.Slice: + if t.Elem().Kind() == reflect.Float64 { + f, err := toFloats(v) + return reflect.ValueOf(f), err + } + return convertList(v, t) + case reflect.Array: + return convertList(v, t) + case reflect.Map: + return convertMap(v, t) + case reflect.Pointer: + if t == ndArrayType { + a, err := ndArray(v) + return reflect.ValueOf(a), err + } + } + if v != nil && reflect.TypeOf(v) == t { + return reflect.ValueOf(v), nil // a Go value that went through Ruby + } + return reflect.Value{}, typeError(v, t.String()) +} + +var ndArrayType = reflect.TypeFor[*nd.Array]() + +// toString reads a String or a Symbol. +func toString(v any) (string, error) { + switch s := v.(type) { + case string: + return s, nil + case rb.Symbol: + return string(s), nil + } + return "", typeError(v, "String") +} + +// convertList converts an Array element by element into a slice or a +// fixed-size array. +func convertList(v any, t reflect.Type) (reflect.Value, error) { + elems, ok := v.([]any) + if !ok { + return reflect.Value{}, typeError(v, "Array") + } + var out reflect.Value + if t.Kind() == reflect.Array { + if len(elems) != t.Len() { + return reflect.Value{}, argError("expected an Array of %d elements, got %d", t.Len(), len(elems)) + } + out = reflect.New(t).Elem() + } else { + out = reflect.MakeSlice(t, len(elems), len(elems)) + } + for i, e := range elems { + ev, err := convert(e, t.Elem()) + if err != nil { + return reflect.Value{}, err + } + out.Index(i).Set(ev) + } + return out, nil +} + +// convertMap converts a Hash whose keys are Strings or Symbols. +func convertMap(v any, t reflect.Type) (reflect.Value, error) { + h, ok := v.(map[any]any) + if !ok || t.Key().Kind() != reflect.String { + return reflect.Value{}, typeError(v, "Hash") + } + out := reflect.MakeMapWithSize(t, len(h)) + for k, e := range h { + ks, err := toString(k) + if err != nil { + return reflect.Value{}, err + } + ev, err := convert(e, t.Elem()) + if err != nil { + return reflect.Value{}, err + } + out.SetMapIndex(reflect.ValueOf(ks).Convert(t.Key()), ev) + } + return out, nil +} + +// enumValueOf reads a symbol (or a string) naming a value of an enumerated +// type: :two_sided, "two-sided" and :"L-BFGS-B" all match, case and +// punctuation aside, the symbol or the Go string value. +func enumValueOf(v any, t reflect.Type, vals []enumValue) (reflect.Value, error) { + s, err := toString(v) + if err != nil { + return reflect.Value{}, typeError(v, "Symbol") + } + want := normalize(s) + names := make([]string, len(vals)) + for i, e := range vals { + rv := reflect.ValueOf(e.value) + if normalize(e.name) == want || (rv.Kind() == reflect.String && normalize(rv.String()) == want) { + return rv, nil + } + names[i] = ":" + e.name + } + return reflect.Value{}, argError("unknown %s %q (expected one of %s)", snakeName(t.Name()), s, strings.Join(names, ", ")) +} + +// normalize folds case and drops punctuation, so names compare by their +// letters: "two_sided", "TwoSided" and "two-sided" are one. +func normalize(s string) string { + var b strings.Builder + for _, r := range s { + if unicode.IsLetter(r) || unicode.IsDigit(r) { + b.WriteRune(unicode.ToLower(r)) + } + } + return b.String() +} + +// options fills the option struct dst points to from keyword arguments: the +// keyword omit_nan sets the field OmitNaN, and so on, each converted to the +// field's type. An unknown keyword is an ArgumentError naming the known ones. +func options(dst any, kw map[string]rb.Value) error { + rv := reflect.ValueOf(dst).Elem() + fields := fieldsByName(rv.Type()) + for _, k := range sortedKeys(kw) { + f, ok := fields[normalize(k)] + if !ok { + return argError("unknown keyword: :%s (known: %s)", k, strings.Join(keywordNames(rv.Type()), ", ")) + } + val, err := convert(kw[k].Go(), f.Type) + if err != nil { + return err + } + rv.FieldByIndex(f.Index).Set(val) + } + return nil +} + +func sortedKeys(kw map[string]rb.Value) []string { + keys := make([]string, 0, len(kw)) + for k := range kw { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + +// fieldsByName indexes a struct's settable fields by normalized name. +func fieldsByName(t reflect.Type) map[string]reflect.StructField { + out := map[string]reflect.StructField{} + for _, f := range reflect.VisibleFields(t) { + if f.IsExported() && !f.Anonymous && f.Type.Kind() != reflect.Func { + out[normalize(f.Name)] = f + } + } + return out +} + +// keywordNames lists the keywords an option struct accepts. +func keywordNames(t reflect.Type) []string { + var out []string + for _, f := range reflect.VisibleFields(t) { + if f.IsExported() && !f.Anonymous && f.Type.Kind() != reflect.Func { + out = append(out, snakeName(f.Name)) + } + } + return out +} + +// split takes the keywords a binding handles itself out of kw, leaving the +// rest for an option struct. +func split(kw map[string]rb.Value, names ...string) (mine, rest map[string]rb.Value) { + mine, rest = map[string]rb.Value{}, map[string]rb.Value{} + for k, v := range kw { + if contains(names, k) { + mine[k] = v + } else { + rest[k] = v + } + } + return mine, rest +} + +func contains(list []string, s string) bool { + for _, e := range list { + if e == s { + return true + } + } + return false +} + +// snakeOverrides are names whose Ruby spelling is SciPy's rather than the +// mechanical one. +var snakeOverrides = map[string]string{ + "PValue": "pvalue", "ZStatistic": "zstatistic", "NObs": "nobs", "OmitNaN": "omit_nan", + "LogPDF": "logpdf", "LogCDF": "logcdf", "LogSF": "logsf", "LogPMF": "logpmf", + "DF": "df", "NaN": "nan", +} + +// snakeName is the Ruby name of a Go identifier: PValue is pvalue, NumIter +// num_iter, XArray x_array. +func snakeName(s string) string { + if o, ok := snakeOverrides[s]; ok { + return o + } + r := []rune(s) + var b strings.Builder + for i, c := range r { + if i > 0 && unicode.IsUpper(c) && (unicode.IsLower(r[i-1]) || unicode.IsDigit(r[i-1]) || + (i+1 < len(r) && unicode.IsLower(r[i+1]) && unicode.IsUpper(r[i-1]))) { + b.WriteByte('_') + } + b.WriteRune(unicode.ToLower(c)) + } + return b.String() +} diff --git a/dataframe.go b/dataframe.go new file mode 100644 index 0000000..3be470c --- /dev/null +++ b/dataframe.go @@ -0,0 +1,5 @@ +package ruby + +import rb "github.com/go-embedded-ruby/ruby" + +func registerDataFrame(in *rb.Interp) error { return nil } diff --git a/datafunc.go b/datafunc.go new file mode 100644 index 0000000..0c6bf90 --- /dev/null +++ b/datafunc.go @@ -0,0 +1,55 @@ +package ruby + +import ( + "reflect" + + rb "github.com/go-embedded-ruby/ruby" + nd "github.com/go-ndarray/ndarray" +) + +// dataFunc binds a generic Go function over stats.Data — []float64 or +// *ndarray.Array — given its two instantiations, overSlice (D = []float64) +// and overArray (D = *ndarray.Array). The parameters where the two differ are +// the data: Ruby passes an Array or an NDArray there. When every data argument +// is an NDArray the NDArray instantiation runs on them unchanged; otherwise +// the slice one runs, with each NDArray read row-major. A variadic data +// parameter takes the remaining positional arguments, one sample each. The +// other parameters convert as for any Go function (see invoke). +func dataFunc(overSlice, overArray any) rb.Func { + fs, fa := reflect.ValueOf(overSlice), reflect.ValueOf(overArray) + ts := fs.Type() + isData := make([]bool, ts.NumIn()) + for i := range isData { + isData[i] = ts.In(i) != fa.Type().In(i) + } + return func(c *rb.Call) (any, error) { + allArrays := true + for i, a := range c.Args { + if dataArg(isData, ts, i) { + if _, ok := a.Go().(*nd.Array); !ok { + allArrays = false + } + } + } + fn := fs + if allArrays { + fn = fa + } + return invoke(fn, c, c.Args) + } +} + +// dataArg reports whether positional argument i lands on a data parameter. +func dataArg(isData []bool, ft reflect.Type, i int) bool { + p := 0 + for j := range isData { + if filled(ft.In(j)) { + continue + } + if p == i || (ft.IsVariadic() && j == ft.NumIn()-1) { + return isData[j] + } + p++ + } + return false +} diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..c467401 --- /dev/null +++ b/doc.go @@ -0,0 +1,29 @@ +// Package ruby binds the go-scikit stack — go-statistics, go-numerics, +// go-dataframe and go-plot, over go-ndarray — into the pure-Go Ruby of +// go-embedded-ruby, so Ruby code gets SciPy-style statistics, special +// functions, optimisation, integration, data frames and plots with no native +// dependency: +// +// in := rb.New() +// ruby.Register(in) +// in.Eval(`Stats::Normal.new(loc: 0, scale: 1).pdf(0.5)`, "x.rb", 1) +// +// Every Ruby method calls the Go function it is named after, on the same +// inputs; the results are the Go library's, bit for bit. NDArray is rbgo's +// own: an array passed in or returned is the go-ndarray array itself. +package ruby + +import ( + rb "github.com/go-embedded-ruby/ruby" +) + +// Register defines the modules Stats, Special, Optimize, Integrate, +// DataFrame and Plot in the interpreter. +func Register(in *rb.Interp) error { + for _, f := range []func(*rb.Interp) error{registerStats, registerSpecial, registerOptimize, registerIntegrate, registerDataFrame, registerPlot} { + if err := f(in); err != nil { + return err + } + } + return nil +} diff --git a/enums.go b/enums.go new file mode 100644 index 0000000..d69787a --- /dev/null +++ b/enums.go @@ -0,0 +1,9 @@ +package ruby + +//go:generate go run ./internal/genenums + +// enumValue is one value of an enumerated Go type and its Ruby symbol. +type enumValue struct { + name string + value any +} diff --git a/enums_gen.go b/enums_gen.go new file mode 100644 index 0000000..68e66aa --- /dev/null +++ b/enums_gen.go @@ -0,0 +1,160 @@ +// Code generated by internal/genenums; DO NOT EDIT. + +package ruby + +import ( + "reflect" + + "github.com/go-numerics/integrate" + "github.com/go-numerics/optimize" + "github.com/go-statistics/stats" +) + +// enumTable lists, for each enumerated Go type, its values and the Ruby +// symbols that name them. +var enumTable = map[reflect.Type][]enumValue{ + reflect.TypeFor[stats.ADDist](): { + {"norm", stats.ADNorm}, + {"expon", stats.ADExpon}, + {"logistic", stats.ADLogistic}, + {"gumbel_l", stats.ADGumbelL}, + {"gumbel_r", stats.ADGumbelR}, + }, + reflect.TypeFor[stats.Alternative](): { + {"two_sided", stats.TwoSided}, + {"less", stats.Less}, + {"greater", stats.Greater}, + }, + reflect.TypeFor[stats.BandwidthRule](): { + {"scott", stats.BandwidthScott}, + {"silverman", stats.BandwidthSilverman}, + }, + reflect.TypeFor[stats.BinRule](): { + {"count", stats.BinsCount}, + {"auto", stats.BinsAuto}, + {"fd", stats.BinsFD}, + {"doane", stats.BinsDoane}, + {"scott", stats.BinsScott}, + {"stone", stats.BinsStone}, + {"rice", stats.BinsRice}, + {"sturges", stats.BinsSturges}, + {"sqrt", stats.BinsSqrt}, + }, + reflect.TypeFor[stats.BinStatistic](): { + {"mean", stats.BinMean}, + {"std", stats.BinStd}, + {"median", stats.BinMedian}, + {"count", stats.BinCount}, + {"sum", stats.BinSum}, + {"min", stats.BinMin}, + {"max", stats.BinMax}, + }, + reflect.TypeFor[stats.BootstrapMethod](): { + {"b_ca", stats.BootstrapBCa}, + {"percentile", stats.BootstrapPercentile}, + {"basic", stats.BootstrapBasic}, + }, + reflect.TypeFor[stats.DiffEntropyMethod](): { + {"auto", stats.DiffEntropyAuto}, + {"vasicek", stats.DiffEntropyVasicek}, + {"van_es", stats.DiffEntropyVanEs}, + {"ebrahimi", stats.DiffEntropyEbrahimi}, + {"correa", stats.DiffEntropyCorrea}, + }, + reflect.TypeFor[stats.KendallVariant](): { + {"b", stats.TauB}, + {"c", stats.TauC}, + }, + reflect.TypeFor[stats.LeveneCenter](): { + {"median", stats.CenterMedian}, + {"mean", stats.CenterMean}, + {"trimmed", stats.CenterTrimmed}, + }, + reflect.TypeFor[stats.Method](): { + {"auto", stats.MethodAuto}, + {"exact", stats.MethodExact}, + {"asymptotic", stats.MethodAsymptotic}, + {"approx", stats.MethodApprox}, + }, + reflect.TypeFor[stats.QuantileMethod](): { + {"linear", stats.QuantileLinear}, + {"inverted_cdf", stats.QuantileInvertedCDF}, + {"averaged_inverted_cdf", stats.QuantileAveragedInvertedCDF}, + {"closest_observation", stats.QuantileClosestObservation}, + {"interpolated_inverted_cdf", stats.QuantileInterpolatedInvertedCDF}, + {"hazen", stats.QuantileHazen}, + {"weibull", stats.QuantileWeibull}, + {"median_unbiased", stats.QuantileMedianUnbiased}, + {"normal_unbiased", stats.QuantileNormalUnbiased}, + {"lower", stats.QuantileLower}, + {"higher", stats.QuantileHigher}, + {"midpoint", stats.QuantileMidpoint}, + {"nearest", stats.QuantileNearest}, + }, + reflect.TypeFor[stats.RankMethod](): { + {"average", stats.RankAverage}, + {"min", stats.RankMin}, + {"max", stats.RankMax}, + {"dense", stats.RankDense}, + {"ordinal", stats.RankOrdinal}, + }, + reflect.TypeFor[stats.Tail](): { + {"right", stats.TailRight}, + {"left", stats.TailLeft}, + }, + reflect.TypeFor[stats.ZeroMethod](): { + {"wilcox", stats.ZeroWilcox}, + {"pratt", stats.ZeroPratt}, + {"zsplit", stats.ZeroZsplit}, + }, + reflect.TypeFor[optimize.FDMethod](): { + {"fd2_point", optimize.FD2Point}, + {"fd3_point", optimize.FD3Point}, + }, + reflect.TypeFor[optimize.LPMethod](): { + {"simplex", optimize.LPSimplex}, + {"interior_point", optimize.LPInteriorPoint}, + }, + reflect.TypeFor[optimize.LSMethod](): { + {"trf", optimize.TRF}, + {"lm", optimize.LM}, + }, + reflect.TypeFor[optimize.Loss](): { + {"linear", optimize.LossLinear}, + {"soft_l1", optimize.LossSoftL1}, + {"huber", optimize.LossHuber}, + {"cauchy", optimize.LossCauchy}, + {"arctan", optimize.LossArctan}, + }, + reflect.TypeFor[optimize.Method](): { + {"nelder_mead", optimize.NelderMead}, + {"powell", optimize.Powell}, + {"cg", optimize.CG}, + {"bfgs", optimize.BFGS}, + {"newton_cg", optimize.NewtonCG}, + {"lbfgsb", optimize.LBFGSB}, + }, + reflect.TypeFor[optimize.ScalarMethod](): { + {"brent", optimize.Brent}, + {"bounded", optimize.Bounded}, + {"golden", optimize.Golden}, + }, + reflect.TypeFor[integrate.Method](): { + {"rk45", integrate.RK45}, + {"rk23", integrate.RK23}, + {"dop853", integrate.DOP853}, + {"radau", integrate.Radau}, + {"bdf", integrate.BDF}, + {"lsoda", integrate.LSODA}, + }, + reflect.TypeFor[integrate.Weight](): { + {"none", integrate.WeightNone}, + {"cos", integrate.WeightCos}, + {"sin", integrate.WeightSin}, + {"alg", integrate.WeightAlg}, + {"alg_log_a", integrate.WeightAlgLogA}, + {"alg_log_b", integrate.WeightAlgLogB}, + {"alg_log", integrate.WeightAlgLog}, + {"cauchy", integrate.WeightCauchy}, + }, +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c576863 --- /dev/null +++ b/go.mod @@ -0,0 +1,414 @@ +module github.com/go-scikit/ruby + +go 1.27.2 + +require github.com/go-embedded-ruby/ruby v0.6.0 + +require ( + filippo.io/age v1.3.2 // indirect + filippo.io/edwards25519 v1.2.0 // indirect + filippo.io/hpke v0.4.0 // indirect + github.com/Azure/go-ntlmssp v0.1.1 // indirect + github.com/BurntSushi/toml v1.6.0 // indirect + github.com/RoaringBitmap/roaring/v2 v2.14.5 // indirect + github.com/abtreece/confd v0.41.2 // indirect + github.com/ajroetker/go-highway v0.0.12 // indirect + github.com/andybalholm/brotli v1.2.6 // indirect + github.com/apache/arrow-go/v18 v18.8.0 // indirect + github.com/apache/thrift v0.24.0 // indirect + github.com/armon/go-metrics v0.4.1 // indirect + github.com/aws/aws-sdk-go-v2 v1.41.7 // indirect + github.com/aws/aws-sdk-go-v2/config v1.32.17 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.16 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 // indirect + github.com/aws/aws-sdk-go-v2/service/acm v1.38.3 // indirect + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.57.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.23 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect + github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.6 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 // indirect + github.com/aws/aws-sdk-go-v2/service/ssm v1.68.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 // indirect + github.com/aws/smithy-go v1.25.1 // indirect + github.com/beevik/etree v1.8.1 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bits-and-blooms/bitset v1.24.2 // indirect + github.com/blevesearch/bleve/v2 v2.6.1 // indirect + github.com/blevesearch/bleve_index_api v1.4.1 // indirect + github.com/blevesearch/geo v0.2.6 // indirect + github.com/blevesearch/go-faiss v1.1.5 // indirect + github.com/blevesearch/go-porterstemmer v1.0.3 // indirect + github.com/blevesearch/gtreap v0.1.1 // indirect + github.com/blevesearch/mmap-go v1.2.0 // indirect + github.com/blevesearch/scorch_segment_api/v2 v2.4.10 // indirect + github.com/blevesearch/segment v0.9.1 // indirect + github.com/blevesearch/snowballstem v0.9.0 // indirect + github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect + github.com/blevesearch/vellum v1.2.0 // indirect + github.com/blevesearch/zapx/v11 v11.4.3 // indirect + github.com/blevesearch/zapx/v12 v12.4.3 // indirect + github.com/blevesearch/zapx/v13 v13.4.3 // indirect + github.com/blevesearch/zapx/v14 v14.4.3 // indirect + github.com/blevesearch/zapx/v15 v15.4.3 // indirect + github.com/blevesearch/zapx/v16 v16.3.4 // indirect + github.com/blevesearch/zapx/v17 v17.2.3 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/coder/websocket v1.8.15 // indirect + github.com/coreos/go-semver v0.3.1 // indirect + github.com/coreos/go-systemd/v22 v22.7.0 // indirect + github.com/crewjam/saml v0.5.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/fsnotify/fsnotify v1.10.1 // indirect + github.com/fxamacker/cbor/v2 v2.9.4 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.8 // indirect + github.com/go-augeas/augeas v0.0.0-20260830115849-a0db83a6594a // indirect + github.com/go-commonmark/commonmark v0.1.0 // indirect + github.com/go-composites/array v0.0.0-20260929012314-b34dd2208c46 // indirect + github.com/go-composites/bag v0.0.0-20261001005725-fcf324b93f95 // indirect + github.com/go-composites/error v0.0.0-20260926002113-8ebf8341ff74 // indirect + github.com/go-composites/null v0.0.0-20260903220223-c1d743488d23 // indirect + github.com/go-composites/result v0.0.0-20260927170344-f2c7344faeef // indirect + github.com/go-composites/time v0.0.0-20260929013028-aa3c57ae1371 // indirect + github.com/go-crdt/collab v0.74.0 // indirect + github.com/go-crdt/crdt v0.55.0 // indirect + github.com/go-dataframe/dataframe v0.1.0 // indirect + github.com/go-datetime/dates v0.1.0 // indirect + github.com/go-encryptions/unixcrypt v0.1.0 // indirect + github.com/go-facter/facter v0.0.0-20260830120958-454b72e642ab // indirect + github.com/go-fft/fft v0.23.0 // indirect + github.com/go-gfx/gfx v0.35.0 // indirect + github.com/go-hiera/hiera v0.0.0-20260830144306-f9304f6bec92 // indirect + github.com/go-hocon/hocon v0.0.0-20260831114632-08e716b40e6d // indirect + github.com/go-icons/iconoir v0.2.0 // indirect + github.com/go-images/gif v0.1.0 // indirect + github.com/go-images/images v0.0.0-20260927173152-87444e36aac4 // indirect + github.com/go-images/jpeg v0.2.0 // indirect + github.com/go-images/jpeg2000 v0.13.3 // indirect + github.com/go-images/png v0.1.0 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect + github.com/go-kramdown/kramdown v0.2.0 // indirect + github.com/go-ldap/ldap/v3 v3.4.14 // indirect + github.com/go-liquid/liquid v0.1.1 // indirect + github.com/go-logr/logr v1.4.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-mustache/mustache v0.1.0 // indirect + github.com/go-ndarray/ndarray v0.12.0 // indirect + github.com/go-nokogiri/nokogiri v0.1.0 // indirect + github.com/go-numerics/integrate v0.1.0 // indirect + github.com/go-numerics/optimize v0.1.0 // indirect + github.com/go-numerics/special v0.1.0 // indirect + github.com/go-opentype/fonts v0.12.0 // indirect + github.com/go-opentype/opentype v0.15.0 // indirect + github.com/go-opentype/shape v0.5.0 // indirect + github.com/go-pcore/pcore v0.0.0-20260831114716-f9c3e7f59eaa // indirect + github.com/go-plot/plot v0.1.1 // indirect + github.com/go-puppet/puppet v0.0.0-20260918012035-fc6b0424cdbd // indirect + github.com/go-regexp/engine v0.3.0 // indirect + github.com/go-richdoc/richdoc v0.4.0 // indirect + github.com/go-rouge/rouge v0.3.0 // indirect + github.com/go-ruby-aasm/aasm v0.0.0-20260717061120-cec0976ec205 // indirect + github.com/go-ruby-abbrev/abbrev v0.0.0-20260916090008-ac08b8471830 // indirect + github.com/go-ruby-acme/acme v0.0.0-20260910083506-2c4b2786f606 // indirect + github.com/go-ruby-actioncable/actioncable v0.0.0-20260916090125-dc336dcdde77 // indirect + github.com/go-ruby-actionmailer/actionmailer v0.0.0-20260927173231-984b22488078 // indirect + github.com/go-ruby-actionpack/actionpack v0.0.0-20260916090236-b987dbf76b25 // indirect + github.com/go-ruby-actionview/actionview v0.0.0-20260927173231-1207ea900433 // indirect + github.com/go-ruby-activejob/activejob v0.0.0-20260916090346-271f6f8a2a0f // indirect + github.com/go-ruby-activeldap/activeldap v0.0.0-20260916090418-6bcd3576fdda // indirect + github.com/go-ruby-activemodel/activemodel v0.0.0-20260923211229-79f6029378bf // indirect + github.com/go-ruby-activerecord/activerecord v0.0.0-20260916090523-27a767d9d502 // indirect + github.com/go-ruby-activestorage/activestorage v0.0.0-20260916090555-d36e120608b9 // indirect + github.com/go-ruby-activesupport/activesupport v0.0.0-20260916090626-10f09966f037 // indirect + github.com/go-ruby-addressable/addressable v0.0.0-20260916090658-0f4764ebcf4a // indirect + github.com/go-ruby-age/age v0.0.0-20260830121641-3a7438630806 // indirect + github.com/go-ruby-arrow/arrow v0.0.0-20260910084041-fd3a53122222 // indirect + github.com/go-ruby-async/async v0.0.0-20260717061939-b24a3ecc37bc // indirect + github.com/go-ruby-augeas/augeas v0.0.0-20260831125504-d45cc71b97b1 // indirect + github.com/go-ruby-base64/base64 v0.0.0-20260916090912-e1e211a81fd1 // indirect + github.com/go-ruby-bbolt/bbolt v0.0.0-20260717062205-7390a75a22b5 // indirect + github.com/go-ruby-bcrypt/bcrypt v0.0.0-20260916091024-e5b4ef398eb3 // indirect + github.com/go-ruby-benchmark/benchmark v0.0.0-20260916091057-fa497ecfe30c // indirect + github.com/go-ruby-bigdecimal/bigdecimal v0.0.0-20260916091144-b31c0a1caac2 // indirect + github.com/go-ruby-bleve/bleve v0.0.0-20260825110136-02ab15b86bd5 // indirect + github.com/go-ruby-builder/builder v0.0.0-20260916091257-7850ebb53ff7 // indirect + github.com/go-ruby-bundler/bundler v0.0.0-20260923211304-8585fc1ac0ac // indirect + github.com/go-ruby-cancancan/cancancan v0.0.0-20260916091405-8fac9e430552 // indirect + github.com/go-ruby-capistrano/capistrano v0.0.0-20260927173236-3b90af3c6888 // indirect + github.com/go-ruby-capybara/capybara v0.0.0-20260910084630-0731c9ea8d6d // indirect + github.com/go-ruby-cgi/cgi v0.0.0-20260916091525-3d328d3ff447 // indirect + github.com/go-ruby-chronic/chronic v0.0.0-20260916091614-bd47d560b6f4 // indirect + github.com/go-ruby-cmath/cmath v0.0.0-20260916091648-4963ab4145f7 // indirect + github.com/go-ruby-concurrent-ruby/concurrent-ruby v0.0.0-20260916091840-b820f7a57284 // indirect + github.com/go-ruby-confd/confd v0.0.0-20260825131031-1ac780d51c57 // indirect + github.com/go-ruby-connection-pool/connection-pool v0.0.0-20260916091940-5582ed60f8a6 // indirect + github.com/go-ruby-csv/csv v0.0.0-20260916092014-f49077ac37dd // indirect + github.com/go-ruby-date/date v0.0.0-20260916092103-cf7111f88dea // indirect + github.com/go-ruby-deep-merge/deep-merge v0.0.0-20260825131041-0389f358e6cf // indirect + github.com/go-ruby-devise/devise v0.0.0-20260927173238-e81f837e986c // indirect + github.com/go-ruby-did-you-mean/did-you-mean v0.0.0-20260916092307-ddd074d76c3c // indirect + github.com/go-ruby-digest/digest v0.0.0-20260916092355-76988b665a30 // indirect + github.com/go-ruby-dotenv/dotenv v0.0.0-20260916092517-652b01333e00 // indirect + github.com/go-ruby-dry-struct/dry-struct v0.0.0-20260923203755-f9de2d043c8d // indirect + github.com/go-ruby-dry-types/dry-types v0.0.0-20260917093022-4ace620129fe // indirect + github.com/go-ruby-dry-validation/dry-validation v0.0.0-20260923203804-e8968bd489d4 // indirect + github.com/go-ruby-erb/erb v0.0.0-20260916092736-2b6ca70e94b1 // indirect + github.com/go-ruby-erubi/erubi v0.0.0-20260916092808-e08929a3f546 // indirect + github.com/go-ruby-etcd/etcd v0.0.0-20260923210448-a14e302251b1 // indirect + github.com/go-ruby-excon/excon v0.0.0-20260916092904-2d7d3ea939bd // indirect + github.com/go-ruby-facter/facter v0.0.0-20260831125504-d8bb19e1e317 // indirect + github.com/go-ruby-factory-bot/factory-bot v0.0.0-20260717064748-e60c0663ca83 // indirect + github.com/go-ruby-faker/faker v0.0.0-20260916093030-7f41896b308e // indirect + github.com/go-ruby-faraday/faraday v0.0.0-20260916093104-ea49608c0ed0 // indirect + github.com/go-ruby-fast-gettext-locale/fast-gettext-locale v0.0.0-20260825110154-a53e0e3a41a7 // indirect + github.com/go-ruby-fast-gettext/fast-gettext v0.0.0-20260826125753-7e0d72f9a378 // indirect + github.com/go-ruby-find/find v0.0.0-20260916093229-03f8985707b5 // indirect + github.com/go-ruby-format/format v0.0.0-20260916093306-887300990e07 // indirect + github.com/go-ruby-friendly-id/friendly-id v0.0.0-20260910090127-4e0134da5c10 // indirect + github.com/go-ruby-getoptlong/getoptlong v0.0.0-20260916093430-ea78ae8c529b // indirect + github.com/go-ruby-grape/grape v0.0.0-20260916093510-22ee43652ee8 // indirect + github.com/go-ruby-graphql/graphql v0.0.0-20260717065229-c0355095acc2 // indirect + github.com/go-ruby-grpc/grpc v0.0.0-20260727143307-befa80ff22df // indirect + github.com/go-ruby-haml/haml v0.0.0-20260916093627-8783fea9fe40 // indirect + github.com/go-ruby-hanami/hanami v0.0.0-20260923211533-24f30a036b61 // indirect + github.com/go-ruby-hcl2/hcl2 v0.0.0-20260717065417-6b99e6076938 // indirect + github.com/go-ruby-hiera/hiera v0.0.0-20260831115655-7a9d33419f3e // indirect + github.com/go-ruby-hocon/hocon v0.0.0-20260901145201-d484ec155199 // indirect + github.com/go-ruby-http/http v0.0.0-20260916093911-fc825633cf27 // indirect + github.com/go-ruby-httparty/httparty v0.0.0-20260916093943-2584adf002b1 // indirect + github.com/go-ruby-i18n/i18n v0.0.0-20260916094015-15d33383fb6e // indirect + github.com/go-ruby-images/images v0.0.0-20260927173248-d1b4499ced38 // indirect + github.com/go-ruby-ipaddr/ipaddr v0.0.0-20260916094116-8c16b4082722 // indirect + github.com/go-ruby-irb/irb v0.0.0-20260916094148-b11dd34a8d63 // indirect + github.com/go-ruby-jbuilder/jbuilder v0.0.0-20260916094222-db456ca7e642 // indirect + github.com/go-ruby-jekyll/jekyll v0.0.0-20260907185853-82fc8b8e7b41 // indirect + github.com/go-ruby-json/json v0.0.0-20260916094324-3ddd0f57b467 // indirect + github.com/go-ruby-jwt/jwt v0.0.0-20260928112721-cf468b8ccb75 // indirect + github.com/go-ruby-kafka/kafka v0.0.0-20260929105502-b0b474169d7c // indirect + github.com/go-ruby-kaminari/kaminari v0.0.0-20260717070041-898c0896ede4 // indirect + github.com/go-ruby-ldap/ldap v0.0.0-20260808195309-d90a141d64f9 // indirect + github.com/go-ruby-logger/logger v0.0.0-20260916094709-0005da481f56 // indirect + github.com/go-ruby-mail/mail v0.0.0-20260916094742-929003e330ea // indirect + github.com/go-ruby-marshal/marshal v0.0.0-20260820215345-e25f276d2451 // indirect + github.com/go-ruby-matrix/matrix v0.0.0-20260916094843-418993c373b7 // indirect + github.com/go-ruby-mime-types/mime-types v0.0.0-20260916094916-b81f94313451 // indirect + github.com/go-ruby-minitest/minitest v0.0.0-20260916094951-890fe6bdb67a // indirect + github.com/go-ruby-money/money v0.0.0-20260916095024-7c6146302d19 // indirect + github.com/go-ruby-mongodb/mongodb v0.0.0-20260912084905-df8743006be1 // indirect + github.com/go-ruby-msgpack/msgpack v0.0.0-20260916095121-f81da43702e1 // indirect + github.com/go-ruby-multi-json/multi-json v0.0.0-20260825110324-bdea42ea11b6 // indirect + github.com/go-ruby-mysql/mysql v0.0.0-20260903192751-d2c199d0722c // indirect + github.com/go-ruby-nats/nats v0.0.0-20260920095939-743be351c042 // indirect + github.com/go-ruby-net-ftp/net-ftp v0.0.0-20260916095353-e39a60e507d2 // indirect + github.com/go-ruby-net-http/net-http v0.0.0-20260916095427-514f82c0c217 // indirect + github.com/go-ruby-net-imap/net-imap v0.0.0-20260916095458-5766481d6882 // indirect + github.com/go-ruby-net-pop/net-pop v0.0.0-20260916095531-fc392a73c587 // indirect + github.com/go-ruby-net-sftp/net-sftp v0.0.0-20260916095628-7e65f73bbebc // indirect + github.com/go-ruby-net-smtp/net-smtp v0.0.0-20260916095659-4ce00c942d53 // indirect + github.com/go-ruby-oauth2/oauth2 v0.0.0-20260916095803-9bbe291c2c47 // indirect + github.com/go-ruby-observer/observer v0.0.0-20260820220157-5e26c6317a28 // indirect + github.com/go-ruby-oidc/oidc v0.0.0-20260923211633-65f2c6985066 // indirect + github.com/go-ruby-omniauth/omniauth v0.0.0-20260923203916-25cee0673a57 // indirect + github.com/go-ruby-openbao/openbao v0.0.0-20260717071447-328a091965dd // indirect + github.com/go-ruby-openstack/openstack v0.0.0-20260923203924-9000c374d63c // indirect + github.com/go-ruby-opentelemetry/opentelemetry v0.0.0-20261003102038-c7cc0d3679e1 // indirect + github.com/go-ruby-opentype/opentype v0.3.0 // indirect + github.com/go-ruby-optparse/optparse v0.0.0-20260917100925-33b18da76c37 // indirect + github.com/go-ruby-ostruct/ostruct v0.0.0-20260927145508-48818b0ca96e // indirect + github.com/go-ruby-pagy/pagy v0.0.0-20260717071719-997d15eee011 // indirect + github.com/go-ruby-paper-trail/paper-trail v0.0.0-20260717071745-42a249656e5a // indirect + github.com/go-ruby-parquet/parquet v0.0.0-20260923211642-bc75bcb9031e // indirect + github.com/go-ruby-parser/parser v0.13.1 // indirect + github.com/go-ruby-pathname/pathname v0.0.0-20260916100446-0824484b665f // indirect + github.com/go-ruby-pg/pg v0.0.0-20260916100523-3831ec811a67 // indirect + github.com/go-ruby-prawn/prawn v0.0.0-20260927145511-370d5985305c // indirect + github.com/go-ruby-prettyprint/prettyprint v0.0.0-20260916100624-a502f95c3ad3 // indirect + github.com/go-ruby-prime/prime v0.0.0-20260916100658-bce9183a6c57 // indirect + github.com/go-ruby-protobuf/protobuf v0.0.0-20260820052513-ddc8d1652a89 // indirect + github.com/go-ruby-pstore/pstore v0.0.0-20260916100755-22600eaa8ef5 // indirect + github.com/go-ruby-public-suffix/public-suffix v0.0.0-20260916100829-97fcf5fc0909 // indirect + github.com/go-ruby-puma/puma v0.0.0-20260717072346-1d0625916636 // indirect + github.com/go-ruby-pundit/pundit v0.0.0-20260916100927-31bbe9228061 // indirect + github.com/go-ruby-puppet-resource-api/puppet-resource-api v0.0.0-20260901145207-1f42f39276b6 // indirect + github.com/go-ruby-puppet/puppet v0.0.0-20260923211650-6955c245474b // indirect + github.com/go-ruby-racc/racc v0.0.0-20260916101058-25c21ceace36 // indirect + github.com/go-ruby-rack/rack v0.0.0-20260916101131-d86b924dd331 // indirect + github.com/go-ruby-rails/rails v0.0.0-20260927173418-b7816aebfa00 // indirect + github.com/go-ruby-railties/railties v0.0.0-20260927173301-9af8a2d861f7 // indirect + github.com/go-ruby-rake/rake v0.0.0-20260916101401-3a87d07573fb // indirect + github.com/go-ruby-ransack/ransack v0.0.0-20260717072959-06ca1d7c6829 // indirect + github.com/go-ruby-rdoc/rdoc v0.0.0-20260916101528-7ed1d61f5088 // indirect + github.com/go-ruby-redis/redis v0.0.0-20260916101628-3b91d134e9dc // indirect + github.com/go-ruby-regexp/regexp v0.1.0 // indirect + github.com/go-ruby-reline/reline v0.0.0-20260916101734-37c7d2c11d74 // indirect + github.com/go-ruby-resolv/resolv v0.0.0-20260916101809-a2b1b027ae6d // indirect + github.com/go-ruby-resque/resque v0.0.0-20260903192756-dc5f8e3f2e80 // indirect + github.com/go-ruby-rexml/rexml v0.0.0-20260916101905-c8e846bf8b6e // indirect + github.com/go-ruby-roda/roda v0.0.0-20260923204143-337fa1a774a7 // indirect + github.com/go-ruby-rolify/rolify v0.0.0-20260717073459-4d2e717bab13 // indirect + github.com/go-ruby-rqrcode/rqrcode v0.0.0-20260916102108-c0062622e02f // indirect + github.com/go-ruby-rspec/rspec v0.0.0-20260916102141-613d88c1871f // indirect + github.com/go-ruby-rss/rss v0.0.0-20260923204152-16e2242a2336 // indirect + github.com/go-ruby-rubocop/rubocop v0.0.0-20261001114457-ff08439ed26a // indirect + github.com/go-ruby-rubygems/rubygems v0.0.0-20260916102314-9bd0bf6a00e5 // indirect + github.com/go-ruby-saml/saml v0.0.0-20260925100912-118142122e06 // indirect + github.com/go-ruby-sass/sass v0.0.0-20260906100410-777830f19847 // indirect + github.com/go-ruby-scanf/scanf v0.0.0-20260916102437-76341a9284aa // indirect + github.com/go-ruby-securerandom/securerandom v0.0.0-20260916102518-8f55919c115b // indirect + github.com/go-ruby-semantic-puppet/semantic-puppet v0.0.0-20260825110441-e577792d52e8 // indirect + github.com/go-ruby-sequel/sequel v0.0.0-20260916102612-ac9710f8a470 // indirect + github.com/go-ruby-shellwords/shellwords v0.0.0-20260916102720-dcaddc4ec112 // indirect + github.com/go-ruby-shrine/shrine v0.0.0-20260717074136-96ee44b6c6c8 // indirect + github.com/go-ruby-sidekiq/sidekiq v0.0.0-20260916102813-4c8927cd45b5 // indirect + github.com/go-ruby-simplecov/simplecov v0.0.0-20260717074233-00faa55e2495 // indirect + github.com/go-ruby-sinatra/sinatra v0.0.0-20260923204248-301a06b96ca0 // indirect + github.com/go-ruby-slim/slim v0.0.0-20260916102940-76e23809c696 // indirect + github.com/go-ruby-sodium/sodium v0.0.0-20260910094952-712708ed6e8d // indirect + github.com/go-ruby-sqlite3/sqlite3 v0.0.0-20260930114044-3861a7b679bd // indirect + github.com/go-ruby-strscan/strscan v0.0.0-20260916103346-94924cbd2f12 // indirect + github.com/go-ruby-thor/thor v0.0.0-20260916103450-ad4daa2c5591 // indirect + github.com/go-ruby-timecop/timecop v0.0.0-20260717074948-f619efc95b6b // indirect + github.com/go-ruby-toml/toml v0.0.0-20260916103606-531bde8e061c // indirect + github.com/go-ruby-tsort/tsort v0.0.0-20260916103639-a7d9f97ff914 // indirect + github.com/go-ruby-typhoeus/typhoeus v0.0.0-20260916103712-7a85bb37a39c // indirect + github.com/go-ruby-tzinfo/tzinfo v0.0.0-20260916103743-89200c78075f // indirect + github.com/go-ruby-unicode-normalize/unicode-normalize v0.0.0-20260916103816-ff4e6b030f1f // indirect + github.com/go-ruby-uri/uri v0.0.0-20260916103846-a8de1e4e5937 // indirect + github.com/go-ruby-vcr/vcr v0.0.0-20260717075257-3b3a803b4619 // indirect + github.com/go-ruby-warden/warden v0.0.0-20260927173355-64506bbab3be // indirect + github.com/go-ruby-webauthn/webauthn v0.0.0-20260921104542-2bb711883889 // indirect + github.com/go-ruby-webmock/webmock v0.0.0-20260717075423-a92c67f51b7f // indirect + github.com/go-ruby-webrick/webrick v0.0.0-20260927173350-a71c0afa01d4 // indirect + github.com/go-ruby-widgets/mvvm v0.2.0 // indirect + github.com/go-ruby-widgets/tui v0.4.0 // indirect + github.com/go-ruby-widgets/widgets v0.12.0 // indirect + github.com/go-ruby-yaml/yaml v0.0.0-20260916104302-910ced2db1c7 // indirect + github.com/go-ruby-zeitwerk/zeitwerk v0.0.0-20260916104335-29169305e951 // indirect + github.com/go-ruby-zlib/zlib v0.0.0-20260927173414-e9d862990b6d // indirect + github.com/go-scss/scss v0.0.0-20260905061546-39932e01faa4 // indirect + github.com/go-simd/adler32 v0.0.0-20260923204409-4db2a9281622 // indirect + github.com/go-simd/base64 v0.0.0-20260903220000-c04f5883bb18 // indirect + github.com/go-simd/crc32 v0.0.0-20260903220012-5f164e0e0487 // indirect + github.com/go-simd/hex v0.0.0-20260903220024-a8d22a843218 // indirect + github.com/go-sql-driver/mysql v1.10.1 // indirect + github.com/go-statistics/stats v0.1.0 // indirect + github.com/go-tex/math v0.50.0 // indirect + github.com/go-typeset/bidi v0.3.0 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/go-webauthn/webauthn v0.18.2 // indirect + github.com/go-webauthn/x v0.3.1 // indirect + github.com/go-widgets/mvvm v0.9.0 // indirect + github.com/go-widgets/painter v0.15.0 // indirect + github.com/go-widgets/toolkit v0.321.2 // indirect + github.com/go-widgets/tui v0.61.0 // indirect + github.com/go-xslt/xslt v0.1.0 // indirect + github.com/go-zookeeper/zk v1.0.4 // indirect + github.com/goccy/go-json v0.10.6 // indirect + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v1.0.0 // indirect + github.com/google/flatbuffers v25.12.19+incompatible // indirect + github.com/google/go-tpm v0.9.8 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gophercloud/gophercloud/v2 v2.15.0 // indirect + github.com/graphql-go/graphql v0.8.1 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect + github.com/hashicorp/consul/api v1.34.2 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-retryablehttp v0.7.8 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect + github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect + github.com/hashicorp/go-sockaddr v1.0.7 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/hashicorp/hcl v1.0.1-vault-7 // indirect + github.com/hashicorp/serf v0.10.1 // indirect + github.com/hashicorp/vault/api v1.23.0 // indirect + github.com/jonboulle/clockwork v0.5.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.20.1 // indirect + github.com/klauspost/cpuid/v2 v2.4.0 // indirect + github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.24 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mschoch/smat v0.2.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/nats-io/nats.go v1.54.0 // indirect + github.com/nats-io/nkeys v0.4.16 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/philhofer/fwd v1.2.0 // indirect + github.com/pierrec/lz4/v4 v4.1.30 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/redis/go-redis/v9 v9.22.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/russellhaering/goxmldsig v1.6.1 // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect + github.com/sergeymakinen/go-bmp v1.0.0 // indirect + github.com/sergeymakinen/go-ico v1.0.0 // indirect + github.com/tannevaled/gobig2 v0.2.0 // indirect + github.com/tinylib/msgp v1.6.4 // indirect + github.com/twmb/franz-go v1.22.1 // indirect + github.com/twmb/franz-go/pkg/kadm v1.19.0 // indirect + github.com/twmb/franz-go/pkg/kmsg v1.14.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.2.0 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + github.com/zeebo/xxh3 v1.1.0 // indirect + go.etcd.io/bbolt v1.5.0 // indirect + go.etcd.io/etcd/api/v3 v3.7.2 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.7.2 // indirect + go.etcd.io/etcd/client/v3 v3.7.2 // indirect + go.mongodb.org/mongo-driver/v2 v2.9.1 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.47.0 // indirect + go.opentelemetry.io/otel/log v1.47.0 // indirect + go.opentelemetry.io/otel/metric v1.47.0 // indirect + go.opentelemetry.io/otel/sdk v1.47.0 // indirect + go.opentelemetry.io/otel/trace v1.47.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.1 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/crypto v0.57.0 // indirect + golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect + golang.org/x/image v0.46.0 // indirect + golang.org/x/net v0.60.0 // indirect + golang.org/x/sync v0.23.0 // indirect + golang.org/x/sys v0.48.0 // indirect + golang.org/x/term v0.46.0 // indirect + golang.org/x/text v0.42.0 // indirect + golang.org/x/time v0.16.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect + google.golang.org/grpc v1.84.0 // indirect + google.golang.org/protobuf v1.36.12 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.77.1 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.12.1 // indirect + modernc.org/sqlite v1.60.1 // indirect +) + +replace github.com/go-embedded-ruby/ruby => ../../go-embedded-ruby/ruby.wt-ruby diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e9e8cbe --- /dev/null +++ b/go.sum @@ -0,0 +1,1176 @@ +c2sp.org/CCTV/age v0.0.0-20260829155415-4448f2097b2d h1:Blprhc2SbChNZtWcU+BLTM4YdoqYAS9V7cJgOwJKyAs= +c2sp.org/CCTV/age v0.0.0-20260829155415-4448f2097b2d/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo= +filippo.io/age v1.3.2 h1:r6RSZLFSMm6rzKepZ7ZAYkKCu14f3/Me8c7uKYh7C8c= +filippo.io/age v1.3.2/go.mod h1:TH/Yr2sSRhCKbaH4XPxpUV0Us8Gv6txYUpiZQWz8Evk= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A= +filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY= +github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw= +github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/RoaringBitmap/roaring/v2 v2.14.5 h1:ckd0o545JqDPeVJDgeFoaM21eBixUnlWfYgjE5VnyWw= +github.com/RoaringBitmap/roaring/v2 v2.14.5/go.mod h1:eq4wdNXxtJIS/oikeCzdX1rBzek7ANzbth041hrU8Q4= +github.com/abtreece/confd v0.41.2 h1:XN+ozNZmah6gIjJG2mN0iCr2JkE/KB5yEf2MDfLM2D8= +github.com/abtreece/confd v0.41.2/go.mod h1:OeUcf5L7ZLhPjqHY4r3hWkns7zPiwvqSwdgK7rJnbwE= +github.com/ajroetker/go-highway v0.0.12 h1:OcGp/l/W7WQtH5ELLuNnKXCuR7ePpWGh2RWZI/INtck= +github.com/ajroetker/go-highway v0.0.12/go.mod h1:un7fcrwix7UOHJrr18Lgh5eeF+1yaZiGWdTw3FbfDv4= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI= +github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= +github.com/alicebob/miniredis/v2 v2.39.0 h1:M7WbmV5BmV56L8KTG0rw6vEQ+woTOghpDgin2xv4A0g= +github.com/alicebob/miniredis/v2 v2.39.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= +github.com/andybalholm/brotli v1.2.6 h1:ftYnfj6usCp+UGV5kSJ3+chpMQgU+gJf/AxsUQ52REI= +github.com/andybalholm/brotli v1.2.6/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E= +github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= +github.com/apache/arrow-go/v18 v18.8.0 h1:BLOzbPv7bxMPgXPacAg6HQjnxupYsZzC4tf+FkqPU/M= +github.com/apache/arrow-go/v18 v18.8.0/go.mod h1:uJCFfCwq0KsxCmsCfQg4ft+LsW+iHYzAXiSDh5ug/8U= +github.com/apache/thrift v0.24.0 h1:zy31L1a49QTNB2bG1BBfMXol3yJrTH975G3pPubQVLQ= +github.com/apache/thrift v0.24.0/go.mod h1:zPt6WxgvTOM6hF92y8C+MkEM5LMxZuk4JcQOiU4Esvs= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= +github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8= +github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc= +github.com/aws/aws-sdk-go-v2/config v1.32.17 h1:FpL4/758/diKwqbytU0prpuiu60fgXKUWCpDJtApclU= +github.com/aws/aws-sdk-go-v2/config v1.32.17/go.mod h1:OXqUMzgXytfoF9JaKkhrOYsyh72t9G+MJH8mMRaexOE= +github.com/aws/aws-sdk-go-v2/credentials v1.19.16 h1:r3RJBuU7X9ibt8RHbMjWE6y60QbKBiII6wSrXnapxSU= +github.com/aws/aws-sdk-go-v2/credentials v1.19.16/go.mod h1:6cx7zqDENJDbBIIWX6P8s0h6hqHC8Avbjh9Dseo27ug= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 h1:UuSfcORqNSz/ey3VPRS8TcVH2Ikf0/sC+Hdj400QI6U= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23/go.mod h1:+G/OSGiOFnSOkYloKj/9M35s74LgVAdJBSD5lsFfqKg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23/go.mod h1:xYWD6BS9ywC5bS3sz9Xh04whO/hzK2plt2Zkyrp4JuA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE= +github.com/aws/aws-sdk-go-v2/service/acm v1.38.3 h1:Fzab84hCu3rw9R9Y3mH7SHfr/cSEHnCB0Mq1JCdr9t0= +github.com/aws/aws-sdk-go-v2/service/acm v1.38.3/go.mod h1:yCteizCNPaHt0SnNusoGGHvy0JDB0tvGDTVhEt5anZM= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.57.3 h1:XgjzLEE8CrNYnr4Xmi1W5PfKsKMjp4Pu1rWkJNO43JI= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.57.3/go.mod h1:r7sfLXEN8RUA89tAHy1E7lCtVOOWIkqVy/FbnUdxW1E= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.23 h1:3Eo/PBBnjFi1+gYfaL286dpmFSW3mTfodBIybq36Qv4= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.23/go.mod h1:3oh+5xGSd1iuxonVb3Qbm+WJYlbhczT9kbzr6doJLzY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.6 h1:XR42AXidhYs4HwH0I+yElLXVt7zb2hAyNHQJe6Blv7w= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.6/go.mod h1:nOTsSVQlAsgwVRdtZYtECSnsInF8IUhrpnclCPat7Fs= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI= +github.com/aws/aws-sdk-go-v2/service/ssm v1.68.5 h1:TY5Vh7uXQgJVuc6ahI6toLcRajG1aYSDCP3a0xsPvmo= +github.com/aws/aws-sdk-go-v2/service/ssm v1.68.5/go.mod h1:UkzShnbxHRIIL2cHi/7fBGLUAZIVTEADQjaA53bWWCE= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.17/go.mod h1:xNWknVi4Ezm1vg1QsB/5EWpAJURq22uqd38U8qKvOJc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 h1:+1Kl1zx6bWi4X7cKi3VYh29h8BvsCoHQEQ6ST9X8w7w= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21/go.mod h1:4vIRDq+CJB2xFAXZ+YgGUTiEft7oAQlhIs71xcSeuVg= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 h1:F/M5Y9I3nwr2IEpshZgh1GeHpOItExNM9L1euNuh/fk= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.1/go.mod h1:mTNxImtovCOEEuD65mKW7DCsL+2gjEH+RPEAexAzAio= +github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI= +github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/beevik/etree v1.8.1 h1:MchsAnqPGCGsfQezhwcouHPlAHlcAOqWpyCVZoyWfjU= +github.com/beevik/etree v1.8.1/go.mod h1:bh4zJxiIr62SOf9pRzN7UUYaEDa9HEKafK25+sLc0Gc= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bits-and-blooms/bitset v1.24.2 h1:M7/NzVbsytmtfHbumG+K2bremQPMJuqv1JD3vOaFxp0= +github.com/bits-and-blooms/bitset v1.24.2/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/blevesearch/bleve/v2 v2.6.1 h1:47vLskRTqxvQEtxVPYHjf5KpOgzD2msslXFjvUQCgWQ= +github.com/blevesearch/bleve/v2 v2.6.1/go.mod h1:Dvvx6ZoEBTOj6RSzfk0lEz0wce/qhe2yOUubXeuzd2c= +github.com/blevesearch/bleve_index_api v1.4.1 h1:CYIyecFlI+/RYjzUm+NmDjYbSvk870Bb7f+Vl4b12q8= +github.com/blevesearch/bleve_index_api v1.4.1/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko= +github.com/blevesearch/geo v0.2.6 h1:7K1oyQKYlauC+mJuo2AfNPyjN/4mihEoJMfyClVH1Mo= +github.com/blevesearch/geo v0.2.6/go.mod h1:6qzVUiB4BK47QkSZcRqiXEP2W3EeXuzM5XFTF8AdZ8A= +github.com/blevesearch/go-faiss v1.1.5 h1:/IU5lkOahH9Ghfk9n3F6N0XD7PYVXZJWmNDc9TtXuco= +github.com/blevesearch/go-faiss v1.1.5/go.mod h1:w3W9AiWsFRGVaMG+/cmJi7iHEAuGyC6blsgO1EzCK/M= +github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo= +github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M= +github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y= +github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk= +github.com/blevesearch/mmap-go v1.2.0 h1:l33nNKPFcBjJUMwem6sAYJPUzhUCABoK9FxZDGiFNBI= +github.com/blevesearch/mmap-go v1.2.0/go.mod h1:Vd6+20GBhEdwJnU1Xohgt88XCD/CTWcqbCNxkZpyBo0= +github.com/blevesearch/scorch_segment_api/v2 v2.4.10 h1:C3873+iWZ0YJM2ijaSHhJJzSvD4x1k+5UaQdGygZVhM= +github.com/blevesearch/scorch_segment_api/v2 v2.4.10/go.mod h1:WUUkAocbkDlNK/kgAE13NvS9oxe+u618mYZ8sOvcCc4= +github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU= +github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw= +github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s= +github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs= +github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A= +github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ= +github.com/blevesearch/vellum v1.2.0 h1:xkDiOEsHc2t3Cp0NsNZZ36pvc130sCzcGKOPMzXe+e0= +github.com/blevesearch/vellum v1.2.0/go.mod h1:uEcfBJz7mAOf0Kvq6qoEKQQkLODBF46SINYNkZNae4k= +github.com/blevesearch/zapx/v11 v11.4.3 h1:PTZOO5loKpHC/x/GzmPZNa9cw7GZIQxd5qRjwij9tHY= +github.com/blevesearch/zapx/v11 v11.4.3/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc= +github.com/blevesearch/zapx/v12 v12.4.3 h1:eElXvAaAX4m04t//CGBQAtHNPA+Q6A1hHZVrN3LSFYo= +github.com/blevesearch/zapx/v12 v12.4.3/go.mod h1:TdFmr7afSz1hFh/SIBCCZvcLfzYvievIH6aEISCte58= +github.com/blevesearch/zapx/v13 v13.4.3 h1:qsdhRhaSpVnqDFlRiH9vG5+KJ+dE7KAW9WyZz/KXAiE= +github.com/blevesearch/zapx/v13 v13.4.3/go.mod h1:knK8z2NdQHlb5ot/uj8wuvOq5PhDGjNYQQy0QDnopZk= +github.com/blevesearch/zapx/v14 v14.4.3 h1:GY4Hecx0C6UTmiNC2pKdeA2rOKiLR5/rwpU9WR51dgM= +github.com/blevesearch/zapx/v14 v14.4.3/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8= +github.com/blevesearch/zapx/v15 v15.4.3 h1:iJiMJOHrz216jyO6lS0m9RTCEkprUnzvqAI2lc/0/CU= +github.com/blevesearch/zapx/v15 v15.4.3/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw= +github.com/blevesearch/zapx/v16 v16.3.4 h1:hDAqA8qusZTNbPEL7//w5P65UZ2de6yhSeUaTbp0Po0= +github.com/blevesearch/zapx/v16 v16.3.4/go.mod h1:zqkPPqs9GS9FzVWzCO3Wf1X044yWAV17+4zb+FTiEHg= +github.com/blevesearch/zapx/v17 v17.2.3 h1:UYYJPAt5b2tVxldx5h0jmv23RMsg8/UZKFVya7v92po= +github.com/blevesearch/zapx/v17 v17.2.3/go.mod h1:r7mb4QWbDQSkbAnOjCb9iCfkcrzajB4yBdJpuBIo/fE= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA= +github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= +github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= +github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= +github.com/crewjam/saml v0.5.1 h1:g+mfp0CrLuLRZCK793PgJcZeg5dS/0CDwoeAX2zcwNI= +github.com/crewjam/saml v0.5.1/go.mod h1:r0fDkmFe5URDgPrmtH0IYokva6fac3AUdstiPhyEolQ= +github.com/cristalhq/base64 v0.1.2 h1:edsefYyYDiac7Ytdh2xdaiiSSJzcI2f0yIkdGEf1qY0= +github.com/cristalhq/base64 v0.1.2/go.mod h1:sy4+2Hale2KbtSqkzpdMeYTP/IrB+HCvxVHWsh2VSYk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 h1:u3PMzfF8RkKd3lB9pZ2bfn0qEG+1Gms9599cr0REMww= +github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2/go.mod h1:mIEZOHnFx4ZMQeawhw9rhsj+0zwQj7adVsnBX7t+eKY= +github.com/dolthub/go-icu-regex v0.0.0-20250327004329-6799764f2dad h1:66ZPawHszNu37VPQckdhX1BPPVzREsGgNxQeefnlm3g= +github.com/dolthub/go-icu-regex v0.0.0-20250327004329-6799764f2dad/go.mod h1:ylU4XjUpsMcvl/BKeRRMXSH7e7WBrPXdSLvnRJYrxEA= +github.com/dolthub/go-mysql-server v0.20.0 h1:oB1WXD5TwdjhdyJDbF6VgVxyEbCevDRok9yEXefpoyI= +github.com/dolthub/go-mysql-server v0.20.0/go.mod h1:5ZdrW0fHZbz+8CngT9gksqSX4H3y+7v1pns7tJCEpu0= +github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 h1:bMGS25NWAGTEtT5tOBsCuCrlYnLRKpbJVJkDbrTRhwQ= +github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71/go.mod h1:2/2zjLQ/JOOSbbSboojeg+cAwcRV0fDLzIiWch/lhqI= +github.com/dolthub/vitess v0.0.0-20250512224608-8fb9c6ea092c h1:imdag6PPCHAO2rZNsFoQoR4I/vIVTmO/czoOl5rUnbk= +github.com/dolthub/vitess v0.0.0-20250512224608-8fb9c6ea092c/go.mod h1:1gQZs/byeHLMSul3Lvl3MzioMtOW1je79QYGyi2fd70= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/emmansun/base64 v0.10.0 h1:sMW+wqusjjq6wPIhIS9+kAMq+UXL4/qRPy7dX9oLFNI= +github.com/emmansun/base64 v0.10.0/go.mod h1:BD7xc9SDECKNzG6q5kX4wMXmIdgHJ+HXWvQv3ImB1V4= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= +github.com/fxamacker/cbor/v2 v2.9.4 h1:xwjVlxEMR3S605oUlgBjKLTTeGFciYPGYCtF/35LKGo= +github.com/fxamacker/cbor/v2 v2.9.4/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/glauth/ldap v0.0.0-20260718202943-34c5f9b3cbf1 h1:897px/nmT3zCSW2aL9kspzl+laSereeYf5m0yZMSqak= +github.com/glauth/ldap v0.0.0-20260718202943-34c5f9b3cbf1/go.mod h1:5ueZMujvJ5nuYPyBj6uQTPV4R1YshboRa/osaP4Emfs= +github.com/go-asn1-ber/asn1-ber v1.5.8 h1:H9AZkK22UOmfX8J84ubyaZxKJZ3FMHVwn8swoMML7iQ= +github.com/go-asn1-ber/asn1-ber v1.5.8/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-augeas/augeas v0.0.0-20260830115849-a0db83a6594a h1:WnRzPthHV1yr9s1jPmUmdApc1M3iIA1ZfH/fwKmCzFg= +github.com/go-augeas/augeas v0.0.0-20260830115849-a0db83a6594a/go.mod h1:syDsgdbvlbD/zjgs6nJktc2araTaN5DEL3a/OvPCrkM= +github.com/go-commonmark/commonmark v0.1.0 h1:UUjE5rIPuYYxDBi2Nj2/fNoTGDyAirp3p/t8B4TzMjE= +github.com/go-commonmark/commonmark v0.1.0/go.mod h1:llCsI7zIAI9nbpH+zhewbQfL6WtyYxmkB4HWZ0YseD8= +github.com/go-composites/array v0.0.0-20260929012314-b34dd2208c46 h1:QKl9KoohqddvNC9PaNmYza9tXtMYznp9ijqj0q7WRxc= +github.com/go-composites/array v0.0.0-20260929012314-b34dd2208c46/go.mod h1:dTLkPz3urunXt2WkDGt8JOIg599mr7ngRUVZYpVbHdo= +github.com/go-composites/bag v0.0.0-20261001005725-fcf324b93f95 h1:gJGC5u3FF2HNf0/6aDQkr77HcZyLX5i12PnW1Zh8ziw= +github.com/go-composites/bag v0.0.0-20261001005725-fcf324b93f95/go.mod h1:AEFE1Z5FW16JnTQogPDauzzkTopgHNoexZafRdYUYLI= +github.com/go-composites/error v0.0.0-20260926002113-8ebf8341ff74 h1:rsAKabYMWmJmKKSfzwgGeWq04/bLz4LoPk7KU7qjOW8= +github.com/go-composites/error v0.0.0-20260926002113-8ebf8341ff74/go.mod h1:kDkZO/jA+1YjdNuH46mZs442IZrntOY70oiaX8hS8l8= +github.com/go-composites/null v0.0.0-20260903220223-c1d743488d23 h1:CrztNkLeRiOTNW7QIIASY7bjwUiK78iUxgYbUpoyIWI= +github.com/go-composites/null v0.0.0-20260903220223-c1d743488d23/go.mod h1:6cSkTkpvobmk2/FSOCRu928lYmWCp87HyCSffyQFhVk= +github.com/go-composites/result v0.0.0-20260927170344-f2c7344faeef h1:VLKCu1sUSOevMOPd1Oy3acH+Tw1LP/qX+RPWL8MobJY= +github.com/go-composites/result v0.0.0-20260927170344-f2c7344faeef/go.mod h1:9wrRj4pStriFW4blUZ25bGzJLc7WvKyOF0Oervq1REI= +github.com/go-composites/time v0.0.0-20260929013028-aa3c57ae1371 h1:P0q4hHWvrQSToC6aFg4OD+zrtN1uV3q2ngxfBEr1c6g= +github.com/go-composites/time v0.0.0-20260929013028-aa3c57ae1371/go.mod h1:lKO/VLgkkNd/FquGtrcO2e+WS/ZYyn1pHSlqQaEeeDU= +github.com/go-crdt/collab v0.74.0 h1:Q+fv5M57UOGzgse9khZEYNCDPxY2kH4CHhEXqSV/6qc= +github.com/go-crdt/collab v0.74.0/go.mod h1:o1aOUE4PI5EfvNPzetDO/cgoGStcuCQueoPaFTf4LiU= +github.com/go-crdt/crdt v0.55.0 h1:D/LHQ4OgSJRomMvEeYzRwwnP0hnXGngWevXglmpXIxI= +github.com/go-crdt/crdt v0.55.0/go.mod h1:vbfvns9B9FUaMi2sPKzMYttKmbxfgO7EK3Yu82SrlYw= +github.com/go-dataframe/dataframe v0.1.0 h1:l92liqTQ/StH5rurH2ipgvXunMbtCJAkDNWEvFwN6oI= +github.com/go-dataframe/dataframe v0.1.0/go.mod h1:rAJdylzFafSOhTAV6z+gSs4i0D9M9XKMKqhWUjIEolk= +github.com/go-datetime/dates v0.1.0 h1:Uj8TaTZ09DXdq75EGQ/teFdrXpfVAC3ZDHAPQvPTnaA= +github.com/go-datetime/dates v0.1.0/go.mod h1:KeZ1wUeMScJD6HOvj3hINwbsB+GCSFjJLWcJhrBlwIo= +github.com/go-encryptions/unixcrypt v0.1.0 h1:XWk4h9uCDd+T2hovOzgO0B2vOdJvAwi9nbOBVb3o62c= +github.com/go-encryptions/unixcrypt v0.1.0/go.mod h1:B1dJSjz3kdxvP7cpi5xSzyxezgBijmtNCUeCKqqDd3o= +github.com/go-facter/facter v0.0.0-20260830120958-454b72e642ab h1:bXPxQDhADpiF/S/8jlRFhVisFPRaxPtPgjibLnOzvsE= +github.com/go-facter/facter v0.0.0-20260830120958-454b72e642ab/go.mod h1:ghuoOHWXdq5dE3GLPDiz3Uy0ORrvK+1hB7QC997ez2U= +github.com/go-fft/fft v0.23.0 h1:SBQmOliIthFyRz1MbnrT/HnJejmLJxoWdLXkXsGMCis= +github.com/go-fft/fft v0.23.0/go.mod h1:GN5V4TWCjV05ECqH1ONfjFM6Z1m2shqCdssxKGqQ41s= +github.com/go-gfx/gfx v0.35.0 h1:AaXMxDMxNB1hROxzx+3EoO06KxsrQUvnw/10olmSgVo= +github.com/go-gfx/gfx v0.35.0/go.mod h1:qIZS9u2+VnBk5CX7o4f/fQ5uD8c+mPFTtAPE85s8V90= +github.com/go-hiera/hiera v0.0.0-20260830144306-f9304f6bec92 h1:eyAunbBKGExaT/qWTpapYsBw4lvXApxMixxGs+wek6k= +github.com/go-hiera/hiera v0.0.0-20260830144306-f9304f6bec92/go.mod h1:Xsrq+9d6EzW6Ju6DyJpIDDj4CYGka5ZAOWlBbQOOE5k= +github.com/go-hocon/hocon v0.0.0-20260831114632-08e716b40e6d h1:Lnoi3t2g9QiII2NGQlg+wL8/zmA/po6G3B0Vs1Dm6lI= +github.com/go-hocon/hocon v0.0.0-20260831114632-08e716b40e6d/go.mod h1:2231cAwqF+bztx0saIVdVSKtnvkx1VVxncHSNSEPu/4= +github.com/go-icons/iconoir v0.2.0 h1:I5AnDEIPgYfawTmwzemDRJjlYCUbFHuS8GJXrjADhXc= +github.com/go-icons/iconoir v0.2.0/go.mod h1:Td/M2vy4xdVv4aOudAskV0nXR9Wv0MlhWvyRIzPBz5U= +github.com/go-images/gif v0.1.0 h1:g2ZnRd41PutXIt1mCZSZsRpqqUiD0m+UbAjZ5ylC0RU= +github.com/go-images/gif v0.1.0/go.mod h1:8mk9sA/K3U61jL+rj4U+Yv3TRKeoGp2fMxF25ykOOwY= +github.com/go-images/images v0.0.0-20260927173152-87444e36aac4 h1:Likv0wVxr9vDo2ITd4A7SOMKCrN3efOHs9CsANj2y+0= +github.com/go-images/images v0.0.0-20260927173152-87444e36aac4/go.mod h1:bc99LrkZxVUjP+Di3JCmf4SoHzim/PNcHEJfKd1yUtg= +github.com/go-images/jpeg v0.2.0 h1:dn55cTczPdOJf+NQq+sETCFeov/KeP1haF1ES+FJa+4= +github.com/go-images/jpeg v0.2.0/go.mod h1:2C1DevAM+HaQlysTLbSdyol8X/RlvmsXmHAXs9teAqo= +github.com/go-images/jpeg2000 v0.13.3 h1:GSnkQWQ72ANwfi/xYWt3za9Nfss+C1iBbk4uubLWYHw= +github.com/go-images/jpeg2000 v0.13.3/go.mod h1:r07D0wUJxBPZcsLTkLFt3OkL3DubrH6DsmRjJRnZYlo= +github.com/go-images/png v0.1.0 h1:OI4yHotS0lA75sLnG2FTL2Vooh4PWpVWEidre1msSe0= +github.com/go-images/png v0.1.0/go.mod h1:V9vaEbr2nrZGorUWn4xv9F24Yuu1/qLVtmailM9GBVo= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.10.0 h1:dXFJfIHVvUcpSgDOV+Ne6t7jXri8Tfv2uOLHUZ2XNuo= +github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= +github.com/go-kramdown/kramdown v0.2.0 h1:ODNLcpqfagHV5s2KUC4lGThW8zZoC0GaO/Qre5aJ6zc= +github.com/go-kramdown/kramdown v0.2.0/go.mod h1:Ie4U1Gt85X9uf+eknh0G2YDc8bmzgxramzgJa3WHXIw= +github.com/go-ldap/ldap/v3 v3.4.14 h1:D6PYdEgsaVzsXyr6w/yDC06Ria4uUhWm+Rb+er8lfAs= +github.com/go-ldap/ldap/v3 v3.4.14/go.mod h1:S4eJUMUNjDkE0ZJtIZdybwyb03sGGLW6gxXT1Hs8VKA= +github.com/go-liquid/liquid v0.1.1 h1:gSOFKvaaF9LAqaF0SwD71TBflSO2U9RNuGQ9PJHC4DI= +github.com/go-liquid/liquid v0.1.1/go.mod h1:iFhRQrmLiwv4R9E29+Vdiz63wGcPGjrSq6n9ouSSMo0= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-mustache/mustache v0.1.0 h1:WvnpPlBn8/v1dwVzp7XvzF5dDC1agYMzPyCl1MdPVzY= +github.com/go-mustache/mustache v0.1.0/go.mod h1:jXns/eIBKrnaFbE8BXX44VrTLp92732R0oXBiZC+v/Q= +github.com/go-ndarray/ndarray v0.12.0 h1:4xfl9ZQo1m0UXopo06KypELg1wUYgQb5kdKaJPZAZus= +github.com/go-ndarray/ndarray v0.12.0/go.mod h1:ZcgNCBVLXt1FxeZjQWI5Z+k0SSWAT0/wR8slfat/gRA= +github.com/go-nokogiri/nokogiri v0.1.0 h1:3Q3BewGHK16Bt34ggJeyf9xeIMJyA3dmfbIxARPgR9U= +github.com/go-nokogiri/nokogiri v0.1.0/go.mod h1:zjghooYIK8Xfg+fL4/OUhROslx51xGsM3JKtB2TI0tg= +github.com/go-numerics/integrate v0.1.0 h1:m1HxKk9CjH8tPjd6rPNq6tdHEl95BHokGbdniG4qbBY= +github.com/go-numerics/integrate v0.1.0/go.mod h1:EGBYtIsMIeVcuVcf6LnLx5/p2lCwbAybdGMiHhwoh/c= +github.com/go-numerics/optimize v0.1.0 h1:SOA+QxTuIjeZSAsd5Dyj433lItnOKWyprCbHQoLxg/c= +github.com/go-numerics/optimize v0.1.0/go.mod h1:ieZ+/EsbBw/EfHl4W8JryPyCBB8n5a8xKk6lu/xh7zk= +github.com/go-numerics/special v0.1.0 h1:HcsebTwcNuAhFBP8RuOirO9XVmWJSrxARiOVkyeCfUk= +github.com/go-numerics/special v0.1.0/go.mod h1:T0QKLvh32lH5Bq9wiTEvtlaFd1vaOKRGrOPgvTeYfdM= +github.com/go-opentype/fonts v0.12.0 h1:gjYYkbDZBK3JHpraakgGNnXdkGq8oADerhDYjC22ILQ= +github.com/go-opentype/fonts v0.12.0/go.mod h1:Kiep7ojxxaR2ALFeB/akcFHQyl1gG5OLIobp8EzaZSA= +github.com/go-opentype/opentype v0.15.0 h1:ioE3eCbNZavlk/w073J3uX5FKjqhgOHQw+wZZdJ+gE4= +github.com/go-opentype/opentype v0.15.0/go.mod h1:ZB31jTJl2mhPESXYMuUW+p36UrOabw/2+6mtyh/vNnI= +github.com/go-opentype/shape v0.5.0 h1:jHNaOMHNBdDj5EixOevlrrsi92svMxvVMKN7GYaPfxo= +github.com/go-opentype/shape v0.5.0/go.mod h1:3ImRYNIj6zpwWQ/DV3BWhgMFfmzDITH6XpZw2auQHTo= +github.com/go-pcore/pcore v0.0.0-20260831114716-f9c3e7f59eaa h1:JNWdYslHUIBvlGE7aKJWtJ+Z6U2riJn+v8NquDVMy+U= +github.com/go-pcore/pcore v0.0.0-20260831114716-f9c3e7f59eaa/go.mod h1:MgngaL59lB3D41kl/VWYfkem1sJDBbh3JXG/y/Lrnjo= +github.com/go-plot/plot v0.1.1 h1:POie6aBWh06Ruo2mc6rV1kaUNxsPhSBIhDL36+yR/6U= +github.com/go-plot/plot v0.1.1/go.mod h1:XySYRAdJQbV9yNAkRNXcly3dVMj8iWTYmYWi5jfYbv0= +github.com/go-puppet/puppet v0.0.0-20260918012035-fc6b0424cdbd h1:0tExRGSb1rQsGZnJxhL//KAGsbe0kmH5EBHoHYYjzjc= +github.com/go-puppet/puppet v0.0.0-20260918012035-fc6b0424cdbd/go.mod h1:wdKZWNCMcBQA7Dgp7xu2QFDPF7tQ2fTlOG4cjGEMgxU= +github.com/go-regexp/engine v0.3.0 h1:yoJ9GtIr5YloEuQQ3bb7jjD0Qzmh+Exi+f6CUwBT8H8= +github.com/go-regexp/engine v0.3.0/go.mod h1:fWnPvSuE4HIVpBQIHMiaPMEE7InDc00KeoPr/QNFGwA= +github.com/go-richdoc/richdoc v0.4.0 h1:oLhMdRXRRZr3RW6Zqra3N6k5nOX6GFmvgyDpMKEBywM= +github.com/go-richdoc/richdoc v0.4.0/go.mod h1:aCX8ulqg5CoKqSWgWeEWul1Oj0d2VxQeaQNI0+jsm7c= +github.com/go-rouge/rouge v0.3.0 h1:tPEjmwlXWdtOCR04rBdSUe+lI75RfHx72F1RihOoTMY= +github.com/go-rouge/rouge v0.3.0/go.mod h1:T6XioFEPW65CmNseRxzjVf8XkxQOHlY8jBLVh3zCqbo= +github.com/go-ruby-aasm/aasm v0.0.0-20260717061120-cec0976ec205 h1:DWFY+nG1MTwCoG/zNblmxDC+ztjPoH85Sf1qY4+igH0= +github.com/go-ruby-aasm/aasm v0.0.0-20260717061120-cec0976ec205/go.mod h1:tngVDjDIQPDpiFh5UXZiMQlBJwVPvkK9viQTMbLH6HY= +github.com/go-ruby-abbrev/abbrev v0.0.0-20260916090008-ac08b8471830 h1:QfzmoOcfNgEUMX4d3yxHS+/xnujKd3gCnpL/qYknOv8= +github.com/go-ruby-abbrev/abbrev v0.0.0-20260916090008-ac08b8471830/go.mod h1:PgAtItdQBv87Wog0Q7rnbwhqsupDC0pDXDlZgSv6nvA= +github.com/go-ruby-acme/acme v0.0.0-20260910083506-2c4b2786f606 h1:5JaoDVykWuEEtG+1Ooaorb6GwUCXlY50PxhWMBfUK8g= +github.com/go-ruby-acme/acme v0.0.0-20260910083506-2c4b2786f606/go.mod h1:Hn0UnfDzZsc7TWigANkGOO3gSiMdmzRIKs9mo4uq5pQ= +github.com/go-ruby-actioncable/actioncable v0.0.0-20260916090125-dc336dcdde77 h1:/ANh9/ju7kClid4JVtvIIMQMww8QHWYlMJ7iayYDugI= +github.com/go-ruby-actioncable/actioncable v0.0.0-20260916090125-dc336dcdde77/go.mod h1:5DS3icNZKImRwvDP7Kf9IHv+9d6EYtcvRpBALgh/s/Y= +github.com/go-ruby-actionmailer/actionmailer v0.0.0-20260927173231-984b22488078 h1:1ABKpKrH5BCVgJg0lYC2HmhjcJ+2iVuSO2tn9EP6Bis= +github.com/go-ruby-actionmailer/actionmailer v0.0.0-20260927173231-984b22488078/go.mod h1:mYlizTQCH5jI2DujjNRQI3SWi3v93BUXnwOTi0bAHRU= +github.com/go-ruby-actionpack/actionpack v0.0.0-20260916090236-b987dbf76b25 h1:XRCqUUtIBECHpFKHThYPe3bNG2PhGzoQrDLKPtVEhtM= +github.com/go-ruby-actionpack/actionpack v0.0.0-20260916090236-b987dbf76b25/go.mod h1:3lXE8Ou6UitS4sOxQhn/KCI6eKLdREUcFxsky4oMEbk= +github.com/go-ruby-actionview/actionview v0.0.0-20260927173231-1207ea900433 h1:OagF2o9hsTuG68b7p8d67io5C9UQdGLrDCcrucMUpHA= +github.com/go-ruby-actionview/actionview v0.0.0-20260927173231-1207ea900433/go.mod h1:Kb3ZFsuy2ju6mK/fJBMikpyFsjlgPWH0NmtONpt22qw= +github.com/go-ruby-activejob/activejob v0.0.0-20260916090346-271f6f8a2a0f h1:YmIL/sOEuRF75LnL6K6uYKE3kS9grsm0j9VQNjZrw3M= +github.com/go-ruby-activejob/activejob v0.0.0-20260916090346-271f6f8a2a0f/go.mod h1:HBZ9KJKoGTzo4h6/5+gaYyzMYWQtucT660mE68zHZGw= +github.com/go-ruby-activeldap/activeldap v0.0.0-20260916090418-6bcd3576fdda h1:gY9DiUbe5W3pZX6wMmqJemw1+IfTBRr9GeRe5KSDrIM= +github.com/go-ruby-activeldap/activeldap v0.0.0-20260916090418-6bcd3576fdda/go.mod h1:EY+VvxoLxV18Qkh4FxFt7yeOVCHItmr070jb24F+zZQ= +github.com/go-ruby-activemodel/activemodel v0.0.0-20260923211229-79f6029378bf h1:fW8qT//lHnqe8xI1hRPmeVYzSN7GiFwT7QNDN4ptkWc= +github.com/go-ruby-activemodel/activemodel v0.0.0-20260923211229-79f6029378bf/go.mod h1:DhZXgevr0giSVgf1SQkhl02I17/nEYWwdRuSGeEAI7o= +github.com/go-ruby-activerecord/activerecord v0.0.0-20260916090523-27a767d9d502 h1:hvuletqfpN5lTWrVwx1aRUNfqpgOOgQkKbjP2yZeRRQ= +github.com/go-ruby-activerecord/activerecord v0.0.0-20260916090523-27a767d9d502/go.mod h1:0QY+Ri6/HhOkjRGhhyZ5NwtcDosKI7xwO5csmWEG0W0= +github.com/go-ruby-activestorage/activestorage v0.0.0-20260916090555-d36e120608b9 h1:+EbAyk1GQYnc6EsRC/H/fn++n28BFLaSXFmwiOYe1NI= +github.com/go-ruby-activestorage/activestorage v0.0.0-20260916090555-d36e120608b9/go.mod h1:+aVqjlgBiR8H5b1t7tPht4OVAAYCxa/KPQAbv7WzC/M= +github.com/go-ruby-activesupport/activesupport v0.0.0-20260916090626-10f09966f037 h1:GZDluekMWuN22PJu4FaLktJQJt1jsJwvM0kQ0QeBJd4= +github.com/go-ruby-activesupport/activesupport v0.0.0-20260916090626-10f09966f037/go.mod h1:ykSisArfgmpZFoV0y0irolshuOdSPyALFz/wWCJiaEE= +github.com/go-ruby-addressable/addressable v0.0.0-20260916090658-0f4764ebcf4a h1:9N4/yXfrbZbRlZiJSI2mXJYqJU6AZIpfXD1njaNjQUI= +github.com/go-ruby-addressable/addressable v0.0.0-20260916090658-0f4764ebcf4a/go.mod h1:4gG3enh3yJ4fpQWjNuzsLbB01vF8XINKkgqsHdfL9O4= +github.com/go-ruby-age/age v0.0.0-20260830121641-3a7438630806 h1:bCm0sLHhlJlmhzAkk2M19xGbQ3VW+9bFMnlVcWm/T0g= +github.com/go-ruby-age/age v0.0.0-20260830121641-3a7438630806/go.mod h1:gtMvY18J/t2NP3ZffE7kPMdH7ajVy/vu5srHQFggXeU= +github.com/go-ruby-arrow/arrow v0.0.0-20260910084041-fd3a53122222 h1:+8ZYpZMWdDaH+iBXnnuNJqpDs4RN/Yqcy1IXs66Fnxc= +github.com/go-ruby-arrow/arrow v0.0.0-20260910084041-fd3a53122222/go.mod h1:rsSzbHn8mE+/vsjlaWj9hPP8PHhb/jnLTUUm0Fozbn8= +github.com/go-ruby-async/async v0.0.0-20260717061939-b24a3ecc37bc h1:mapstuvSdHawVxubXE0+lsC0oW/lUo9ZluowCewcqJg= +github.com/go-ruby-async/async v0.0.0-20260717061939-b24a3ecc37bc/go.mod h1:461TqgFiDMx6SKwX1YFc+SSwSvI3Ny1Zj6TiZaFGtW0= +github.com/go-ruby-augeas/augeas v0.0.0-20260831125504-d45cc71b97b1 h1:sgZv7piFvY3pYQlbd+YYsKpNOjPpqtl3N09TnpcEOoI= +github.com/go-ruby-augeas/augeas v0.0.0-20260831125504-d45cc71b97b1/go.mod h1:uqNC/nDN7it2nY82wTXNEbzyq0Q/1jXxz0p3pEsERbU= +github.com/go-ruby-base64/base64 v0.0.0-20260916090912-e1e211a81fd1 h1:o67TKyiWELF07wklV220MjmFV6LBsGAC1XzxyZa+uSQ= +github.com/go-ruby-base64/base64 v0.0.0-20260916090912-e1e211a81fd1/go.mod h1:JZf/Kc4NucCOPGiikq72HcKqM81N5xcxZ5QWBqT7fyU= +github.com/go-ruby-bbolt/bbolt v0.0.0-20260717062205-7390a75a22b5 h1:qfM7C1mlpssN0z0LzJkziOedcJlXZIaGVryL7mHJDkI= +github.com/go-ruby-bbolt/bbolt v0.0.0-20260717062205-7390a75a22b5/go.mod h1:ZAYQsrvCyOiDqPRQiJ2201BerIWJ76e/HBb99Ontr38= +github.com/go-ruby-bcrypt/bcrypt v0.0.0-20260916091024-e5b4ef398eb3 h1:KuvmlVjrBKEN15ZCfIYWLqCZu/PrV2omGdLX7YdlwVY= +github.com/go-ruby-bcrypt/bcrypt v0.0.0-20260916091024-e5b4ef398eb3/go.mod h1:MYTkJdtdQ0WBH9Uy8zVusKqzh2vPhDIeRo2yoU7VDys= +github.com/go-ruby-benchmark/benchmark v0.0.0-20260916091057-fa497ecfe30c h1:K0hhBZ/OZAF4uZjLojkGOVo9vhcefKhwo8C/a2vdyhM= +github.com/go-ruby-benchmark/benchmark v0.0.0-20260916091057-fa497ecfe30c/go.mod h1:xTLuIsErHXTxzLBJkUz5J07NwY58QJ75j2Ww4DQGFEM= +github.com/go-ruby-bigdecimal/bigdecimal v0.0.0-20260916091144-b31c0a1caac2 h1:zo7WF/+KG9X0TeWzxNwKq5TBsIoBx9R39lyU05uhKH8= +github.com/go-ruby-bigdecimal/bigdecimal v0.0.0-20260916091144-b31c0a1caac2/go.mod h1:op9nu6Z2gvH8/phs7iJZQWNvUnT2DkJV8wBAl1l0kfE= +github.com/go-ruby-bleve/bleve v0.0.0-20260825110136-02ab15b86bd5 h1:1vj/ezjwS/+M1tglLxe0pQDJijor3B+XkvJ8Qs2mc/4= +github.com/go-ruby-bleve/bleve v0.0.0-20260825110136-02ab15b86bd5/go.mod h1:UouC7QsjL9Z7YSbS74+PzndoB2SxPEr6gPtQAMsFsDI= +github.com/go-ruby-builder/builder v0.0.0-20260916091257-7850ebb53ff7 h1:SQhfd2Qn8DTBLXbUs/+7Y7nUsgefc+EswCo789aixhM= +github.com/go-ruby-builder/builder v0.0.0-20260916091257-7850ebb53ff7/go.mod h1:wYtstpfGi/nHyyNdfTU+JACaSR5Vzd3FvCQd4Hchyrc= +github.com/go-ruby-bundler/bundler v0.0.0-20260923211304-8585fc1ac0ac h1:UcOZ2UVfZQnsO6wojJYMp/0E3dh3u+c47xVgvFG216M= +github.com/go-ruby-bundler/bundler v0.0.0-20260923211304-8585fc1ac0ac/go.mod h1:7NP2e2JVUw5XCdOsgXkf6ml8a03TccMmlIlmH23aI2Y= +github.com/go-ruby-cancancan/cancancan v0.0.0-20260916091405-8fac9e430552 h1:k9fA9QhNlllM1Gls6qscmTPwH1yYf9XMhmPmjgH7kS0= +github.com/go-ruby-cancancan/cancancan v0.0.0-20260916091405-8fac9e430552/go.mod h1:WvnY0uliy6W7kQ0Mpmjo3TbRGNQUwWwMufvdyT2V8wY= +github.com/go-ruby-capistrano/capistrano v0.0.0-20260927173236-3b90af3c6888 h1:8aPeHZ2iy8l/ZqpwT3clx181Kd11PJ5zeiPAL49PI8s= +github.com/go-ruby-capistrano/capistrano v0.0.0-20260927173236-3b90af3c6888/go.mod h1:i1DHs2O0B801YaM6gwPrrIclTecr+p4ycpf2ZT4KY/k= +github.com/go-ruby-capybara/capybara v0.0.0-20260910084630-0731c9ea8d6d h1:eDH85UsCrm3mz0Lr5q6ZHiCis9KoqgZ3cy7c10D4S7A= +github.com/go-ruby-capybara/capybara v0.0.0-20260910084630-0731c9ea8d6d/go.mod h1:igWLNrYKtKkssHOzlXm1YFoPH0Yi6LGazOSA4Obo6T0= +github.com/go-ruby-cgi/cgi v0.0.0-20260916091525-3d328d3ff447 h1:o6V4n2Td46cjbCwPN0pZnUVFfHbC648AnYtMQVU9O60= +github.com/go-ruby-cgi/cgi v0.0.0-20260916091525-3d328d3ff447/go.mod h1:TAIxiFMAj68dWClzEmDY1vCSNFiPfupD3HRADRXrkZw= +github.com/go-ruby-chronic/chronic v0.0.0-20260916091614-bd47d560b6f4 h1:Z2aDUPl3f6fQIJ4Y+9KCE16p77dAxJBBJjR3j974A5o= +github.com/go-ruby-chronic/chronic v0.0.0-20260916091614-bd47d560b6f4/go.mod h1:6H3V0VdtD8NB/WEt+8+bIM0n78g45H8HDAnWNwJByCM= +github.com/go-ruby-cmath/cmath v0.0.0-20260916091648-4963ab4145f7 h1:2tUvNHeR8mL8NcJyxTZxaaujvusucfBsnZAQg18D9Zg= +github.com/go-ruby-cmath/cmath v0.0.0-20260916091648-4963ab4145f7/go.mod h1:djGUM2WR7AdnZqeRg/OQ4xVzmsl8PAWQKILX2n/foas= +github.com/go-ruby-concurrent-ruby/concurrent-ruby v0.0.0-20260916091840-b820f7a57284 h1:hIdCXEwLLYl+NPKBCbI3aFul6ARXfgEczxlA0hIDOkY= +github.com/go-ruby-concurrent-ruby/concurrent-ruby v0.0.0-20260916091840-b820f7a57284/go.mod h1:Jj/1rtC6VpluKBpT0XN+9HT1x3xz+0m6gr69QV0Nnns= +github.com/go-ruby-confd/confd v0.0.0-20260825131031-1ac780d51c57 h1:L2Y93x9ntHelQ2hjb4PBTGEj6dBN5wl/KsBkgOcUv8Y= +github.com/go-ruby-confd/confd v0.0.0-20260825131031-1ac780d51c57/go.mod h1:36tzboufIATppUnfNlKSZnCvXh4WTVlTNoJ98K9xqNY= +github.com/go-ruby-connection-pool/connection-pool v0.0.0-20260916091940-5582ed60f8a6 h1:l/tTy+iy2uS2g2YMGaPIC5uHeh8ZVeBLXawqE7EpFW4= +github.com/go-ruby-connection-pool/connection-pool v0.0.0-20260916091940-5582ed60f8a6/go.mod h1:Ye6JGxrVgn959GWYYwL/D0mkdhcd5Vfeler13Fyelb0= +github.com/go-ruby-csv/csv v0.0.0-20260916092014-f49077ac37dd h1:plNVYjlP9BRlUbSP7FwmwPK+LVCY4BkYs+ZZZCv40ZQ= +github.com/go-ruby-csv/csv v0.0.0-20260916092014-f49077ac37dd/go.mod h1:vvfFJGcGOK37vnBikohtzboL6dIfbsr54+2Ri0HkWwk= +github.com/go-ruby-date/date v0.0.0-20260916092103-cf7111f88dea h1:77DSZqatTj70Rh+icq69nBFXTg4InqL1gRF9qQNimBU= +github.com/go-ruby-date/date v0.0.0-20260916092103-cf7111f88dea/go.mod h1:kn4XWlGthyqF+ogp9Tmj9EYLmvZa3Yb9Eqnfdsgu63Y= +github.com/go-ruby-deep-merge/deep-merge v0.0.0-20260825131041-0389f358e6cf h1:8L51aOEI7eQ093vBzsgdoU/8bYC9U3MxlHeJ5NzsKzg= +github.com/go-ruby-deep-merge/deep-merge v0.0.0-20260825131041-0389f358e6cf/go.mod h1:a4khGfrUTGA/kTuvjH2/LEcBcKrTFXziYPd2nlC4cHA= +github.com/go-ruby-devise/devise v0.0.0-20260927173238-e81f837e986c h1:kY8o8lcyS43sbhAC/5TXk6P9uYRGIPnK2LK2fBfL1wU= +github.com/go-ruby-devise/devise v0.0.0-20260927173238-e81f837e986c/go.mod h1:ztBiOVsOqGKLUImht8kFcybs8Odi9AYho7v155BY0uQ= +github.com/go-ruby-did-you-mean/did-you-mean v0.0.0-20260916092307-ddd074d76c3c h1:NSRJ7aPt1+FcTQFToesKCl/rzyL3QdynhvzKaHvGwZQ= +github.com/go-ruby-did-you-mean/did-you-mean v0.0.0-20260916092307-ddd074d76c3c/go.mod h1:8I30lj4s9N0MCztmdJdCyMRCfK5xBBQiWHUwgnK3pIY= +github.com/go-ruby-digest/digest v0.0.0-20260916092355-76988b665a30 h1:/WSzZcSMfffRitpZSctZ/5LVlRgEGyLrcbzCtf0hFcM= +github.com/go-ruby-digest/digest v0.0.0-20260916092355-76988b665a30/go.mod h1:PLkd1cts5/qufkr8BvU6UmcEcfnXWPqSmE/uR1gmaQ8= +github.com/go-ruby-dotenv/dotenv v0.0.0-20260916092517-652b01333e00 h1:ChIAUAXdel8PoA4XCPubBMtn308AXVCS25Zc5yjGjxE= +github.com/go-ruby-dotenv/dotenv v0.0.0-20260916092517-652b01333e00/go.mod h1:L5FvtAnqC4wHelxeOEPnWLz/BHoXA/dPD3KuDpzpcr4= +github.com/go-ruby-dry-struct/dry-struct v0.0.0-20260923203755-f9de2d043c8d h1:itX8BVA8Hd0MsjUhcc6ZGzIDdVmTgpvQqNURQBFr7zU= +github.com/go-ruby-dry-struct/dry-struct v0.0.0-20260923203755-f9de2d043c8d/go.mod h1:d3od/SMyhLAZwpQbtNPwTT3uppFFH1b+9NwBBKs4D1o= +github.com/go-ruby-dry-types/dry-types v0.0.0-20260917093022-4ace620129fe h1:1V04Ed0BQYqWITmnMZvlJB5wEyKGFx2Nk4DlI+cEdXQ= +github.com/go-ruby-dry-types/dry-types v0.0.0-20260917093022-4ace620129fe/go.mod h1:S+2r7EA5zp4E8JyaJ88koD8gT7k/JD9i8NGj6V6jIRM= +github.com/go-ruby-dry-validation/dry-validation v0.0.0-20260923203804-e8968bd489d4 h1:yCVdFC0eDHwsnM0UYYAVXiu9EtzMKmok8scvjHPxVFs= +github.com/go-ruby-dry-validation/dry-validation v0.0.0-20260923203804-e8968bd489d4/go.mod h1:6+Su0ClwyB2FNI98Xo8WEFdEh7a6UjHBRpPt1ulC53A= +github.com/go-ruby-erb/erb v0.0.0-20260916092736-2b6ca70e94b1 h1:W0IT8+iWj4qU0eZXiGkDLgyWqn8XR5pDwRj2oKZkqlE= +github.com/go-ruby-erb/erb v0.0.0-20260916092736-2b6ca70e94b1/go.mod h1:HEJVI7jQ/BnjiSiu2Wj934QboEAgdv47RNOqvZb+nRw= +github.com/go-ruby-erubi/erubi v0.0.0-20260916092808-e08929a3f546 h1:XdrSKXjEbbtDD5xKKt+/0OSznx7vvCJyUdxoWeO+xDc= +github.com/go-ruby-erubi/erubi v0.0.0-20260916092808-e08929a3f546/go.mod h1:jZJcpxqgDFkKAW7K6Z/WwrWdF7BQ78dYUopimi986ls= +github.com/go-ruby-etcd/etcd v0.0.0-20260923210448-a14e302251b1 h1:yh9Jbso2+Cel3G3LLt0gBAScaroE4tRRylk3y5MQ1wA= +github.com/go-ruby-etcd/etcd v0.0.0-20260923210448-a14e302251b1/go.mod h1:FoLMEbrgyh4WcLC4pPo2XqWtjcUjtm788uO/Sb8hjlQ= +github.com/go-ruby-excon/excon v0.0.0-20260916092904-2d7d3ea939bd h1:35pmqbRkGHpeNbUIvbMzJLwAfhW00YdFwoHDEssss5U= +github.com/go-ruby-excon/excon v0.0.0-20260916092904-2d7d3ea939bd/go.mod h1:oelovkSKi3ofbasu8dxitwGA53oPgoop1Ow04s8T5FA= +github.com/go-ruby-facter/facter v0.0.0-20260831125504-d8bb19e1e317 h1:pXXe2+DmhgxWS/Hd1rWEclPlIAy7i1CzceRFVRv/D9Q= +github.com/go-ruby-facter/facter v0.0.0-20260831125504-d8bb19e1e317/go.mod h1:G4VeX/6HVW30gF/Qzcd5CBTWVOChFF486iNIG8mR04I= +github.com/go-ruby-factory-bot/factory-bot v0.0.0-20260717064748-e60c0663ca83 h1:3DYqZAbXYUoA0lwalmJnhwbKwHElsuc5nOKjj4ZPOag= +github.com/go-ruby-factory-bot/factory-bot v0.0.0-20260717064748-e60c0663ca83/go.mod h1:XLVBvQlRhuvFMPbCgI97ifRnnLBbUal9vnpDsAXbRPY= +github.com/go-ruby-faker/faker v0.0.0-20260916093030-7f41896b308e h1:GVGtPSq+NmRHjtJo68Jl7A3SXVXH/mREpE5G7yxnIZE= +github.com/go-ruby-faker/faker v0.0.0-20260916093030-7f41896b308e/go.mod h1:7h2mxQ3mnWz9bg39nSDr0h/LxqnEnbrJxmNRDbu3zs8= +github.com/go-ruby-faraday/faraday v0.0.0-20260916093104-ea49608c0ed0 h1:XKY/K5mXU/SgxRfyN0bhkz7v6iQE/isJa+Gmrh++Fv0= +github.com/go-ruby-faraday/faraday v0.0.0-20260916093104-ea49608c0ed0/go.mod h1:t4H4KVmvxOQ4IwtcniaHeNluKbmXKBOF8TkaULJAxsU= +github.com/go-ruby-fast-gettext-locale/fast-gettext-locale v0.0.0-20260825110154-a53e0e3a41a7 h1:vKG4lEzAZX0MXc0RgIZvuxxjLm5GEnUiwgV711nEbhY= +github.com/go-ruby-fast-gettext-locale/fast-gettext-locale v0.0.0-20260825110154-a53e0e3a41a7/go.mod h1:uL9OXR02JFILIgyJTWs82PseC59F54CNWZVSEnPkRbs= +github.com/go-ruby-fast-gettext/fast-gettext v0.0.0-20260826125753-7e0d72f9a378 h1:y6IWVgyVz3C0gnr0jfyGSvgaGixN74DmnftD+jHu2BE= +github.com/go-ruby-fast-gettext/fast-gettext v0.0.0-20260826125753-7e0d72f9a378/go.mod h1:od3ty6Nvkv9duft61Qv8HL4UZMXcWlLyM6g3IuQVSW0= +github.com/go-ruby-find/find v0.0.0-20260916093229-03f8985707b5 h1:3UiJEH7DMbS7uJal9QdjTEQ0b1y7Gprpt2uT+8yO4bY= +github.com/go-ruby-find/find v0.0.0-20260916093229-03f8985707b5/go.mod h1:wpq0ciEFn8zUc1vfuHRjF2U8mH0krhJCRHHr+JOGaEY= +github.com/go-ruby-format/format v0.0.0-20260916093306-887300990e07 h1:jPTvLfwlvAoa6GTyU9dZ5EW102x+6GC+sC3b9qrS61A= +github.com/go-ruby-format/format v0.0.0-20260916093306-887300990e07/go.mod h1:1tuz12SHPOcnFSfdPYRFwVvRhMT4Xn4M4jfPFgM7pLA= +github.com/go-ruby-friendly-id/friendly-id v0.0.0-20260910090127-4e0134da5c10 h1:kDm6soIqA7t4wlUUGQpoNd3JK+VKExX0vi8JsfX/gkE= +github.com/go-ruby-friendly-id/friendly-id v0.0.0-20260910090127-4e0134da5c10/go.mod h1:bSvNqqNaK2vo7WYCKIYhO1l110XSdpiRLj5n+Nw9RRk= +github.com/go-ruby-getoptlong/getoptlong v0.0.0-20260916093430-ea78ae8c529b h1:P/UqpWSFofDBNygifbL2jHtJKR58U7LncVxwVpeKmMU= +github.com/go-ruby-getoptlong/getoptlong v0.0.0-20260916093430-ea78ae8c529b/go.mod h1:nGvVSBSIsYrhM2qs4yQ2k8uDbXCFPWQXXqkowleHMDs= +github.com/go-ruby-grape/grape v0.0.0-20260916093510-22ee43652ee8 h1:LEyuWSZ9LWh1HyGolB8Z7Z7Q7aaTlQ7BYfF2DvYaQsM= +github.com/go-ruby-grape/grape v0.0.0-20260916093510-22ee43652ee8/go.mod h1:N0dFV4BDu87D5qFusrC2jUb+hPlJpiAuuHRSMznMM7c= +github.com/go-ruby-graphql/graphql v0.0.0-20260717065229-c0355095acc2 h1:wRj6lMHHCM+R5DY05NWssAOubrsgzXQVELeLDkQGYGo= +github.com/go-ruby-graphql/graphql v0.0.0-20260717065229-c0355095acc2/go.mod h1:u2AOB5QQHSsWCoIZ3XnRcw98LQDIRHxzYYerDHuTsCw= +github.com/go-ruby-grpc/grpc v0.0.0-20260727143307-befa80ff22df h1:c+AGprCo4gL24pk1gDtBlggop+Ov0znIsABYA7h0SJM= +github.com/go-ruby-grpc/grpc v0.0.0-20260727143307-befa80ff22df/go.mod h1:4aUK5J8VX6Umk5/FgRqXRhe3tLJ9F/rhFXiUTcb81kc= +github.com/go-ruby-haml/haml v0.0.0-20260916093627-8783fea9fe40 h1:Pr22LNblqxs4K+/f4QIxf3NE+ITt+i27MSS8rvxcT5s= +github.com/go-ruby-haml/haml v0.0.0-20260916093627-8783fea9fe40/go.mod h1:D4xOrc0xIiN0u0Q+KqjfKx/f7up8ekpud/syYsHTrPY= +github.com/go-ruby-hanami/hanami v0.0.0-20260923211533-24f30a036b61 h1:RNK177r54XXOaD9wTPpyhBHBsSm7MGRAWWt27Wddynk= +github.com/go-ruby-hanami/hanami v0.0.0-20260923211533-24f30a036b61/go.mod h1:WydSW9NCTxvNG01/JXdrwuk+qoXqRA3kdAJiAtKQNVo= +github.com/go-ruby-hcl2/hcl2 v0.0.0-20260717065417-6b99e6076938 h1:2UWjr92i7i0wDyI0wb+atFBOjQEvFBjwl2q4ZWjvGNo= +github.com/go-ruby-hcl2/hcl2 v0.0.0-20260717065417-6b99e6076938/go.mod h1:zPgLGk8wYhXACwDsJqxkdK09WgWbJwalPxiw6qHMGQg= +github.com/go-ruby-hiera/hiera v0.0.0-20260831115655-7a9d33419f3e h1:6nfEsQkxOEsU/rKDv7/7jaPBPj81DRoF5RXHSp7VynA= +github.com/go-ruby-hiera/hiera v0.0.0-20260831115655-7a9d33419f3e/go.mod h1:P4MTzkaz17D/8MzLsDpEj5GEcuhodUQ9re4lyts9zpA= +github.com/go-ruby-hocon/hocon v0.0.0-20260901145201-d484ec155199 h1:SyImERv/2FrOTfdaC15PvdBx1JsJcpvj+GGgWPdNs/0= +github.com/go-ruby-hocon/hocon v0.0.0-20260901145201-d484ec155199/go.mod h1:vp1MeeH5cTM3x7K6U+szUL6qgTy0F2Ed2v/+DQKzQbE= +github.com/go-ruby-http/http v0.0.0-20260916093911-fc825633cf27 h1:xfsH8peGCWdDT7GQfhL+A18Kpsj+CPBFaleaeeQXqfs= +github.com/go-ruby-http/http v0.0.0-20260916093911-fc825633cf27/go.mod h1:i+ODhr0dqRa5IzaeLSVW52G0Fd2d7fVIQe5pKO2luDA= +github.com/go-ruby-httparty/httparty v0.0.0-20260916093943-2584adf002b1 h1:Q3QtR5v9OBna2I/ghtyvA4mWJwk1Qr1mIA0pDM2+E34= +github.com/go-ruby-httparty/httparty v0.0.0-20260916093943-2584adf002b1/go.mod h1:02LNZ7RDjMh536m57zJdVXN6VsSw/L1OjpGaTL+RRIo= +github.com/go-ruby-i18n/i18n v0.0.0-20260916094015-15d33383fb6e h1:4de+waIjDwapYg9ByMWd0imUpXem66oKc8L5esXSrfU= +github.com/go-ruby-i18n/i18n v0.0.0-20260916094015-15d33383fb6e/go.mod h1:EIn9QjMqN3zvyz37o1hU2Vqvqi2YTjf5mCKvSc+PMDA= +github.com/go-ruby-images/images v0.0.0-20260927173248-d1b4499ced38 h1:ksvnzLHkkZDMm4XLVY/8qece+OJLuYvT7Lmdm2NZXeA= +github.com/go-ruby-images/images v0.0.0-20260927173248-d1b4499ced38/go.mod h1:JbGl9PaWWKLybBNJYv/6uW3D/TxL5C4vrQEVa8YzWJo= +github.com/go-ruby-ipaddr/ipaddr v0.0.0-20260916094116-8c16b4082722 h1:aw9USM0ngYA7qPtdxqi7IVYkNP4kwytnqhCrvV5Kn/M= +github.com/go-ruby-ipaddr/ipaddr v0.0.0-20260916094116-8c16b4082722/go.mod h1:NbAjqDjT7Qx83yBMHYGCfPFzi+iAvQtrE3JT3nqS/+U= +github.com/go-ruby-irb/irb v0.0.0-20260916094148-b11dd34a8d63 h1:Om5udRNelippvDe1cquq5XRklHHuGxGIru1f02l5Eu0= +github.com/go-ruby-irb/irb v0.0.0-20260916094148-b11dd34a8d63/go.mod h1:/CCuNPTzkrA8aBkxPUB4CG0Km1YD55jlxw8V8o7XmpQ= +github.com/go-ruby-jbuilder/jbuilder v0.0.0-20260916094222-db456ca7e642 h1:A96jQXWQuTboaXCznkYqiAjTn6fYxYwdUvMn2Qq3RYk= +github.com/go-ruby-jbuilder/jbuilder v0.0.0-20260916094222-db456ca7e642/go.mod h1:EY+IHrYLDowQEX9Pj3OJ8u2bI5FfkTmMbMhmEmV6uUM= +github.com/go-ruby-jekyll/jekyll v0.0.0-20260907185853-82fc8b8e7b41 h1:2ZJ8l6pxTXauAPFIx77c8WzNTz3lIqIMjFVh0Az1XC0= +github.com/go-ruby-jekyll/jekyll v0.0.0-20260907185853-82fc8b8e7b41/go.mod h1:eNwInueehS0vb7y+Y9JWCxnyo9l52mEGicUtxTYM3kU= +github.com/go-ruby-json/json v0.0.0-20260916094324-3ddd0f57b467 h1:jkUiPjB8SVePsZW/wi+bJGsU8BquiEqrquBti58+7D4= +github.com/go-ruby-json/json v0.0.0-20260916094324-3ddd0f57b467/go.mod h1:nHXcH3zaGNmCslZStZVeylXkv1wLp+nzT6qNj+m+QXU= +github.com/go-ruby-jwt/jwt v0.0.0-20260928112721-cf468b8ccb75 h1:N3hl4wQh+f/rGXIUCOUSfpAqNFMRFqi7n0+FfyRks8Q= +github.com/go-ruby-jwt/jwt v0.0.0-20260928112721-cf468b8ccb75/go.mod h1:lXzMyvgfYye7tZm1cY9ej2SbXzAdJdA2IAiY+niM+nU= +github.com/go-ruby-kafka/kafka v0.0.0-20260929105502-b0b474169d7c h1:d3Q7vAPflMqwVd5p5ci6xAUr/EbyzD/M3PWcGfez9fI= +github.com/go-ruby-kafka/kafka v0.0.0-20260929105502-b0b474169d7c/go.mod h1:PU9R/wtWIw6RFDqS9rZY1RcvrTIg/5kDTorbIPC8N54= +github.com/go-ruby-kaminari/kaminari v0.0.0-20260717070041-898c0896ede4 h1:jFRRi4pF/+rhREfS6iYyhNw36++YTSVy5pPEdeMOddA= +github.com/go-ruby-kaminari/kaminari v0.0.0-20260717070041-898c0896ede4/go.mod h1:L57ZFGNQr2e0gnapvjfF7tSgD+p/ET0J1voQ2TBqGDE= +github.com/go-ruby-ldap/ldap v0.0.0-20260808195309-d90a141d64f9 h1:U5RkeM0ZPZ3am2ONBVjPDspn9dovDnc1zqwvyNTpS7w= +github.com/go-ruby-ldap/ldap v0.0.0-20260808195309-d90a141d64f9/go.mod h1:E+etdKNjaujhdka05RG11Nf4MmMIl1n7hWbxNtsrmVQ= +github.com/go-ruby-logger/logger v0.0.0-20260916094709-0005da481f56 h1:a3lxxOluGfZqog71gSiOwHnwUfImF0SQvo305Ui2YoY= +github.com/go-ruby-logger/logger v0.0.0-20260916094709-0005da481f56/go.mod h1:9+5E2AAIpErKRtzJ7zqZSfptjlD/XcD/ni6H9jpO37Y= +github.com/go-ruby-mail/mail v0.0.0-20260916094742-929003e330ea h1:J9NSCbebb2Sb/WipXN1lO1Be2YKTX3BKb4KiONkD7l4= +github.com/go-ruby-mail/mail v0.0.0-20260916094742-929003e330ea/go.mod h1:YJN6l0WpGBz87UAbyHGqnH14iwABfjvrZPUunn9OrNs= +github.com/go-ruby-marshal/marshal v0.0.0-20260820215345-e25f276d2451 h1:uuOqNDFJgvAU7OVFn9TLv5r7mMejq20gJRh7EzyW72k= +github.com/go-ruby-marshal/marshal v0.0.0-20260820215345-e25f276d2451/go.mod h1:UpjSgWGcQSNt0ARWuHKwf0P55LkwZ65vuqoRPSKctZ8= +github.com/go-ruby-matrix/matrix v0.0.0-20260916094843-418993c373b7 h1:ZK6Fj4ALwYDnEd7gOJLXNl+/1yVlxljyJgPzuAj9Dgc= +github.com/go-ruby-matrix/matrix v0.0.0-20260916094843-418993c373b7/go.mod h1:ZrmXlz95j9VE3j1bUeq8a5CTWAKoEiYrkh/ON9ns9iE= +github.com/go-ruby-mime-types/mime-types v0.0.0-20260916094916-b81f94313451 h1:TFdHYMs3nPjv0v9Oyp6vTI1TdbQFyByExY7cSuShOlA= +github.com/go-ruby-mime-types/mime-types v0.0.0-20260916094916-b81f94313451/go.mod h1:CxpFpr/gPj1iq9+xI8uyDtB+LiRzlY7FlytmXEE9z2s= +github.com/go-ruby-minitest/minitest v0.0.0-20260916094951-890fe6bdb67a h1:dOVtAfGwq8YcFq/CgHl7d71RmT8vUa6ySuknThzLZio= +github.com/go-ruby-minitest/minitest v0.0.0-20260916094951-890fe6bdb67a/go.mod h1:uTtdIEuNPKAm9oIvtKyG1LKO67WhE3luarsY5vQDiC8= +github.com/go-ruby-money/money v0.0.0-20260916095024-7c6146302d19 h1:DExfwGZDpPBIs9MIxvkA/FsNgtDN0hvkNr0z/m3qX00= +github.com/go-ruby-money/money v0.0.0-20260916095024-7c6146302d19/go.mod h1:jVG6K5w0MzXZlyEdBdzbUC/WqArBowo9hJd6PQZ/TPA= +github.com/go-ruby-mongodb/mongodb v0.0.0-20260912084905-df8743006be1 h1:IosTKjO1X1yK3iVDdpqCykamQSBM5AagInqpRnCPWrs= +github.com/go-ruby-mongodb/mongodb v0.0.0-20260912084905-df8743006be1/go.mod h1:q178/SaLLCLk6gMgpz4St+AcP/JiJnaXo1qwHbtOwKA= +github.com/go-ruby-msgpack/msgpack v0.0.0-20260916095121-f81da43702e1 h1:hanXx9/acNQnd715WYhdlKHXsSQU9HJIBDmMnSKc1p4= +github.com/go-ruby-msgpack/msgpack v0.0.0-20260916095121-f81da43702e1/go.mod h1:MkpM6qH0Zz8H6btdGOPBmnImw6BbpcMCvPHV9J2V2Rc= +github.com/go-ruby-multi-json/multi-json v0.0.0-20260825110324-bdea42ea11b6 h1:iwh/SskgVOE2DaD5rRzHLbNznuF0+h4ah6HbX2OynU8= +github.com/go-ruby-multi-json/multi-json v0.0.0-20260825110324-bdea42ea11b6/go.mod h1:/TjKZ4URypH/A7odknSIG+l4QaojOGv9dNEiyu8Y/oc= +github.com/go-ruby-mysql/mysql v0.0.0-20260903192751-d2c199d0722c h1:2XanUxeHmIIVShFbSVj6kePbAJFAOlroFDHk1S/1aj0= +github.com/go-ruby-mysql/mysql v0.0.0-20260903192751-d2c199d0722c/go.mod h1:+jCNDkKtns+zdvh27C7/PeI/L1OoloOiU7Jso30bSVA= +github.com/go-ruby-nats/nats v0.0.0-20260920095939-743be351c042 h1:q8QrtfAEtbVQHjxvxWSxKwv+eEB1kJ6g+b6p61VDOiQ= +github.com/go-ruby-nats/nats v0.0.0-20260920095939-743be351c042/go.mod h1:m7IE8tHF7KosxWVO+zy4A6UXxwEkVnxBL+H6Tf9Dr9Y= +github.com/go-ruby-net-ftp/net-ftp v0.0.0-20260916095353-e39a60e507d2 h1:IYLNQDtHx0q5LT9a5SdHUBxhVe0uPy/26DHW9J8B2Ts= +github.com/go-ruby-net-ftp/net-ftp v0.0.0-20260916095353-e39a60e507d2/go.mod h1:1H5yOndya/hONJxNjd0oAbRyCAyurxcbze0FXlF2ExA= +github.com/go-ruby-net-http/net-http v0.0.0-20260916095427-514f82c0c217 h1:6jgq2XLnWkigaLldPdW7SdOTr5Ar32yvdA3FreV0Lpo= +github.com/go-ruby-net-http/net-http v0.0.0-20260916095427-514f82c0c217/go.mod h1:PiNQDuyrSPZX8OzApvgyn1fTjRNwx0KNO1vrzdAguEc= +github.com/go-ruby-net-imap/net-imap v0.0.0-20260916095458-5766481d6882 h1:vgKYYJIkKd/N2MuqlvirfgiPX5fdImHWUSwbgHQVTBE= +github.com/go-ruby-net-imap/net-imap v0.0.0-20260916095458-5766481d6882/go.mod h1:jKlWC6EDNkTFr5BDzA7+UAMFXsKLwIy5r9zKqxXSM+U= +github.com/go-ruby-net-pop/net-pop v0.0.0-20260916095531-fc392a73c587 h1:1k5vum44yWIvjU9c3PYGJrS/jsYKJQ3LWxbZPjfdWFI= +github.com/go-ruby-net-pop/net-pop v0.0.0-20260916095531-fc392a73c587/go.mod h1:9+NyzWDudIY8eetFY066OKgrU8bk7ZVqDRKAjFBIwng= +github.com/go-ruby-net-sftp/net-sftp v0.0.0-20260916095628-7e65f73bbebc h1:9qNKQEgiFTVAwE0kOr4XCD3v9GInC9cNMlPKWLEQtFI= +github.com/go-ruby-net-sftp/net-sftp v0.0.0-20260916095628-7e65f73bbebc/go.mod h1:okvDFdQDwwx5ubNL6mmXTHGTutWhOTHSnUPq1JzhxoU= +github.com/go-ruby-net-smtp/net-smtp v0.0.0-20260916095659-4ce00c942d53 h1:MkBDEBKb8MtJCM0N9+tLmLt29YzQakTgOOInw1fxleY= +github.com/go-ruby-net-smtp/net-smtp v0.0.0-20260916095659-4ce00c942d53/go.mod h1:+XJbVnPobgkWUscH8bDSw9F3jW6t3QY35DEAyE9qE9Q= +github.com/go-ruby-oauth2/oauth2 v0.0.0-20260916095803-9bbe291c2c47 h1:kvG94brPmmFJaEEY5Bk/MUR8GKHd9ZbFgmciOe1Ov0A= +github.com/go-ruby-oauth2/oauth2 v0.0.0-20260916095803-9bbe291c2c47/go.mod h1:/yz3iBLTlChPKMqrP0uA5tBpXKh4a8Rwd46SAv/lMiw= +github.com/go-ruby-observer/observer v0.0.0-20260820220157-5e26c6317a28 h1:9Iiv/i4gMlFiWyJ1ltuX3mvFnaAnnL4/vyIbougjbNw= +github.com/go-ruby-observer/observer v0.0.0-20260820220157-5e26c6317a28/go.mod h1:QvbIKqCHbVYFCko+ZlLGzFh6h/K7pqSKGzADrzTUvls= +github.com/go-ruby-oidc/oidc v0.0.0-20260923211633-65f2c6985066 h1:hJITyJ8M6fe4PN8W8o+ecmjZNBBslfHXrQmlRb9m1zM= +github.com/go-ruby-oidc/oidc v0.0.0-20260923211633-65f2c6985066/go.mod h1:MhZlCLmM3CPAJsizDFHk5TyneSguhVyb0qNZnDJBzLE= +github.com/go-ruby-omniauth/omniauth v0.0.0-20260923203916-25cee0673a57 h1:vOt55+3B4n+9z8nrWUSAXPl/8jGErUjvQvp1RvSNASg= +github.com/go-ruby-omniauth/omniauth v0.0.0-20260923203916-25cee0673a57/go.mod h1:oRxffiY3vC8WWJ6YHQM9Wr84KBcEAkqV9sMwn/B/QaU= +github.com/go-ruby-openbao/openbao v0.0.0-20260717071447-328a091965dd h1:MsW8tMLHpWz/Inj8ESDr2xZFbtGck584qS8mgsFeuqk= +github.com/go-ruby-openbao/openbao v0.0.0-20260717071447-328a091965dd/go.mod h1:lOCs10/i8qtuP3cA+d8YpRrCSiUw+Bo8xffbPplrnCo= +github.com/go-ruby-openstack/openstack v0.0.0-20260923203924-9000c374d63c h1:UXHqT9dRMZsN01WCHuQr82BOHEOeG3iAoDkdUPcTRYc= +github.com/go-ruby-openstack/openstack v0.0.0-20260923203924-9000c374d63c/go.mod h1:mlq0xlrzxyJF2Kw/fSm/ZSm1D7H4WCv/18l1HSZITcs= +github.com/go-ruby-opentelemetry/opentelemetry v0.0.0-20261003102038-c7cc0d3679e1 h1:4oMsgcp7OsNM9z9GDSyqo9AZBWbo0787TUsJy/Or99g= +github.com/go-ruby-opentelemetry/opentelemetry v0.0.0-20261003102038-c7cc0d3679e1/go.mod h1:84SxI/twpjckb+Dv9BbkoXJVEvkRd7Z2HKEJClIK20g= +github.com/go-ruby-opentype/opentype v0.3.0 h1:cqdbaFq1z/uvNfjoMkxv7TUm/BiWUg/7dsTPUE40j90= +github.com/go-ruby-opentype/opentype v0.3.0/go.mod h1:I8WEDlwDOj8RPSs/zvGw9MSqLg2yMJxCom9zVIs5HOI= +github.com/go-ruby-optparse/optparse v0.0.0-20260917100925-33b18da76c37 h1:SZ2alHm5/WzH6f8ChbnUE2JrzmBOx402mQHuk7BhadY= +github.com/go-ruby-optparse/optparse v0.0.0-20260917100925-33b18da76c37/go.mod h1:BxhqkQFCqSMeYeWOHt7sdxTSkbaSJF19dWzspICZL1Q= +github.com/go-ruby-ostruct/ostruct v0.0.0-20260927145508-48818b0ca96e h1:Hq96vh/dQiurNCGYLFbYC/EAHqm56yNdT4jeyHxcvZY= +github.com/go-ruby-ostruct/ostruct v0.0.0-20260927145508-48818b0ca96e/go.mod h1:WkHaYzJJba7jbtNmSXYztEF9tPOWgUv32ox47v1+AZc= +github.com/go-ruby-pagy/pagy v0.0.0-20260717071719-997d15eee011 h1:VylRXFAZERgegXtWB/XwptxVCpvtpl2Ek8Qz9dyRwgA= +github.com/go-ruby-pagy/pagy v0.0.0-20260717071719-997d15eee011/go.mod h1:IVNVo8WkqSQbwf9BcfHxQr5vbx6CpC49f8G7C+C9NGI= +github.com/go-ruby-paper-trail/paper-trail v0.0.0-20260717071745-42a249656e5a h1:MkFighwO7LdEFJy1xZeFYKxP/6Uj0i7mhxqr/8UsWXM= +github.com/go-ruby-paper-trail/paper-trail v0.0.0-20260717071745-42a249656e5a/go.mod h1:z370KNJcUH8n39yLdlT0Y1QvHukxjsq1XjGpIzvfqWE= +github.com/go-ruby-parquet/parquet v0.0.0-20260923211642-bc75bcb9031e h1:pxhP3nIRxTpj99e74JAVpGj7JQtRSNqLb+B5x2WT5N4= +github.com/go-ruby-parquet/parquet v0.0.0-20260923211642-bc75bcb9031e/go.mod h1:wC+J6qfQ8C0aKgi5qsbR03DYrLLuXXezUBHl967am/E= +github.com/go-ruby-parser/parser v0.13.1 h1:gq7QVT7KdRp7nZt1UHsa73YDo77qrD8kGdQY/GVEA6g= +github.com/go-ruby-parser/parser v0.13.1/go.mod h1:Se+ZvnD/4Vm0RRe1U5kZi0GmPSvSczJJXdZLiB0PlJY= +github.com/go-ruby-pathname/pathname v0.0.0-20260916100446-0824484b665f h1:9pt89wlAp12vbCNGFYmbSqp/PJTHRwUx1yNDXW5hWRI= +github.com/go-ruby-pathname/pathname v0.0.0-20260916100446-0824484b665f/go.mod h1:IvW5q5Kyl5l3hrYwiHylOEmk5hpGSo7XqcmUXBHF5H0= +github.com/go-ruby-pg/pg v0.0.0-20260916100523-3831ec811a67 h1:X9MKhEAV60oYoL94tqb11lfLw3yW097mVqfjDdy5PK0= +github.com/go-ruby-pg/pg v0.0.0-20260916100523-3831ec811a67/go.mod h1:XIYL8wT1Thvoeh7y5v9ZrL+34ySHiieu1/GyDZauULk= +github.com/go-ruby-prawn/prawn v0.0.0-20260927145511-370d5985305c h1:VzQUCjVO86Ha+los1UAVmCnh8VEzGkKOvTqUTG8Cj34= +github.com/go-ruby-prawn/prawn v0.0.0-20260927145511-370d5985305c/go.mod h1:Jk81Fm/7UhOlBxAi0AcTVm3J+RjOws4PqU1EUNfNfPw= +github.com/go-ruby-prettyprint/prettyprint v0.0.0-20260916100624-a502f95c3ad3 h1:zdfvHMjuhc74ytM8B7VaRekQv1G8d7GDMKKS1cmATs8= +github.com/go-ruby-prettyprint/prettyprint v0.0.0-20260916100624-a502f95c3ad3/go.mod h1:SIQyfiIJRKJq0OD9gWYfmvQTdJClJf8z56BsfVVw7rY= +github.com/go-ruby-prime/prime v0.0.0-20260916100658-bce9183a6c57 h1:0REvk8nHvMGXavBnIoGfj5L/Fj55Dg/FCrMi57GgZPo= +github.com/go-ruby-prime/prime v0.0.0-20260916100658-bce9183a6c57/go.mod h1:dG/91hk8HlQ6YAdD64xiMFHTTmXjTWtapKWoG9JtcRA= +github.com/go-ruby-protobuf/protobuf v0.0.0-20260820052513-ddc8d1652a89 h1:t9TH4oLm6JQdgc+uNPL70QPgt/7zfQeXI1NR9SqeSTc= +github.com/go-ruby-protobuf/protobuf v0.0.0-20260820052513-ddc8d1652a89/go.mod h1:GWVem7HJaTvzGk0ACpdvHtiw2yDyD5qY2ulZjs7csKU= +github.com/go-ruby-pstore/pstore v0.0.0-20260916100755-22600eaa8ef5 h1:nfWXdTTDEVQAtL1ruCziiGW/BGPdRnxSAbLdgytMmrQ= +github.com/go-ruby-pstore/pstore v0.0.0-20260916100755-22600eaa8ef5/go.mod h1:RZGT1vhwpQ5b6jq94ghQkmTE6CMfM6AwsV8MtDA1UfE= +github.com/go-ruby-public-suffix/public-suffix v0.0.0-20260916100829-97fcf5fc0909 h1:aMAc1zTy7xkrHGNTpKlECYrzytRstWLef9ferAW0His= +github.com/go-ruby-public-suffix/public-suffix v0.0.0-20260916100829-97fcf5fc0909/go.mod h1:Wq3cgP6klsULbO4ee0DOavVIzifgmddOWnYpqljbER4= +github.com/go-ruby-puma/puma v0.0.0-20260717072346-1d0625916636 h1:iDy8EGm0o0rLQFh3yNRULhBeAi5wC3xvZz2e3loiKws= +github.com/go-ruby-puma/puma v0.0.0-20260717072346-1d0625916636/go.mod h1:auu7u2RjtfGu7yD4e9U67Pt+EbKWs8mYVV/tLWhBanA= +github.com/go-ruby-pundit/pundit v0.0.0-20260916100927-31bbe9228061 h1:bMgO3IPjWo4Sj0iJF+nKOmIHwMV+T1PNuO9dbLfj+uY= +github.com/go-ruby-pundit/pundit v0.0.0-20260916100927-31bbe9228061/go.mod h1:eEm7KJjGK32SW4hkOP97uB8lQfQwfxSS569Qqzo3vTY= +github.com/go-ruby-puppet-resource-api/puppet-resource-api v0.0.0-20260901145207-1f42f39276b6 h1:7hW+A6Fn/WbUk67e9feRde36XS8zx9VjHQaw6HloRT0= +github.com/go-ruby-puppet-resource-api/puppet-resource-api v0.0.0-20260901145207-1f42f39276b6/go.mod h1:sMvTquJqn8fZVOnWbIV58LRUq+eMdApNCo3nidfzZK8= +github.com/go-ruby-puppet/puppet v0.0.0-20260923211650-6955c245474b h1:kXLNWs/igXMXItMYbq/RA8mRDLQsra79VGH7uy9ro7w= +github.com/go-ruby-puppet/puppet v0.0.0-20260923211650-6955c245474b/go.mod h1:+oYBXr6CegvhXoFmTpkrbSC1h433ymENv1rOkM/W5Ec= +github.com/go-ruby-racc/racc v0.0.0-20260916101058-25c21ceace36 h1:rBzM9hAjT2N5DMuGfb3OVXceHeti8vgQ+a/7PuH9qjY= +github.com/go-ruby-racc/racc v0.0.0-20260916101058-25c21ceace36/go.mod h1:HeFcYYwifNZvQzF9jdaSFViHYT0mUNLwWEFPQj0j6V4= +github.com/go-ruby-rack/rack v0.0.0-20260916101131-d86b924dd331 h1:r+OCRLteRUid3hfcrC5Tah3yUITFF9hrWygvIZoily4= +github.com/go-ruby-rack/rack v0.0.0-20260916101131-d86b924dd331/go.mod h1:rKZ3vD81gJq61NvZ3XX9cr6AxtIV3+lFZKG7lPKV3PM= +github.com/go-ruby-rails/rails v0.0.0-20260927173418-b7816aebfa00 h1:YffI7pR42d6R26HmLpF28ht9zQkXapsYyig1wZfCrw4= +github.com/go-ruby-rails/rails v0.0.0-20260927173418-b7816aebfa00/go.mod h1:HRW25B6VQCE6I3V/pPiPdhtAmIReS6zDRs7gcFHJMlQ= +github.com/go-ruby-railties/railties v0.0.0-20260927173301-9af8a2d861f7 h1:Gy+A4ecwu66OaiS1iHgObgupNASh8W+JbVzzGs1Foa8= +github.com/go-ruby-railties/railties v0.0.0-20260927173301-9af8a2d861f7/go.mod h1:NsXSECy7iJyo+S2KA4w2wco4o+gr8P7etyE3Rl8Z+rY= +github.com/go-ruby-rake/rake v0.0.0-20260916101401-3a87d07573fb h1:5GkYOLzoLd2TwL72BrNURhh2dH+TB/ygrZXOcnX23xY= +github.com/go-ruby-rake/rake v0.0.0-20260916101401-3a87d07573fb/go.mod h1:xAIiEx09/OpalT2DMw3b7AUfbgMkVODTOEl9cexTLOI= +github.com/go-ruby-ransack/ransack v0.0.0-20260717072959-06ca1d7c6829 h1:dVvhIpL16vr5Z92EzHub69wjtbesLB7lYXzZq+WS4Fg= +github.com/go-ruby-ransack/ransack v0.0.0-20260717072959-06ca1d7c6829/go.mod h1:Ti6YI1QAM0JLaqqvbHqrNsPTVQAFq2jXo4fLiWt4KUM= +github.com/go-ruby-rdoc/rdoc v0.0.0-20260916101528-7ed1d61f5088 h1:ZM5ySs951YynvzW5i2zkFoUxHdUgiQd30GMyhERPxF4= +github.com/go-ruby-rdoc/rdoc v0.0.0-20260916101528-7ed1d61f5088/go.mod h1:NBlCj+x2SBtdDSKDvTTCVRoWd9tGPWcUwPvE2nKvYSM= +github.com/go-ruby-redis/redis v0.0.0-20260916101628-3b91d134e9dc h1:ilAGkAG47yhEemSIGBFLHpW8gJCNwSdIATOV1ANBYHI= +github.com/go-ruby-redis/redis v0.0.0-20260916101628-3b91d134e9dc/go.mod h1:gt67ft80L9J5DpRGe+x3qPy2hg1g75aWQ6h52inm7og= +github.com/go-ruby-regexp/regexp v0.1.0 h1:KtFfOt6tKiT/UhoZ8GH6liyIVbmqhXTD04uqAiUQBj8= +github.com/go-ruby-regexp/regexp v0.1.0/go.mod h1:ndhrN+oK4wdXKM6iOasSIzLSTCs9vD9jhi65gx13Xpg= +github.com/go-ruby-reline/reline v0.0.0-20260916101734-37c7d2c11d74 h1:DsVosBzp7T+U4Q3/UxcHVUl9mb5SprF9hvSkOQ/4Fdg= +github.com/go-ruby-reline/reline v0.0.0-20260916101734-37c7d2c11d74/go.mod h1:d89qnDguKRgCpUIFphMQnQrlalJBT4s/muXS/wEQdj0= +github.com/go-ruby-resolv/resolv v0.0.0-20260916101809-a2b1b027ae6d h1:yU7BQTgui2DiLBkp4mB6QnIjIepY7K9S2hLiiaNLpAM= +github.com/go-ruby-resolv/resolv v0.0.0-20260916101809-a2b1b027ae6d/go.mod h1:RD3wPeMjMThQ7QVnB9O15gCllRg+RJhSEJu9DtFaLLQ= +github.com/go-ruby-resque/resque v0.0.0-20260903192756-dc5f8e3f2e80 h1:XIWQFZxiy3ev9DRuFNki3VQ0HTjyxtAOHridfGe0RW4= +github.com/go-ruby-resque/resque v0.0.0-20260903192756-dc5f8e3f2e80/go.mod h1:EXVUn96NEwbtRqZhotiwXpdQ1ny9q2r/5AY5qn10a90= +github.com/go-ruby-rexml/rexml v0.0.0-20260916101905-c8e846bf8b6e h1:PwZ1F6Ia1XUXgwCjh7R+qsQVIU4pZmH7VmqLHkK/auQ= +github.com/go-ruby-rexml/rexml v0.0.0-20260916101905-c8e846bf8b6e/go.mod h1:UQGki9qXLiqaEOm4ih1CGznGNPUDcNGCsLVJ/y6G1G8= +github.com/go-ruby-roda/roda v0.0.0-20260923204143-337fa1a774a7 h1:BbbGOO08zNtuSd8RLnTe2Fbx+ysn1J0YRwOxifHWw2s= +github.com/go-ruby-roda/roda v0.0.0-20260923204143-337fa1a774a7/go.mod h1:D3LD6F4NDzg81cFXXcSpNrMILVf/e1KnxC+PA/QuCgs= +github.com/go-ruby-rolify/rolify v0.0.0-20260717073459-4d2e717bab13 h1:+1fDlQuVxjjsjGctjPl7pZq9rbN7QRUBTgwAyvwZvsU= +github.com/go-ruby-rolify/rolify v0.0.0-20260717073459-4d2e717bab13/go.mod h1:4dqBKwZf+wvpwA+2Eh+2Ow51YS6ma937wYJEZ1otLcE= +github.com/go-ruby-rqrcode/rqrcode v0.0.0-20260916102108-c0062622e02f h1:ATyJLCTrLGIy9nGCHG10yYO01ZZOb+pQxKlTTFU46tg= +github.com/go-ruby-rqrcode/rqrcode v0.0.0-20260916102108-c0062622e02f/go.mod h1:+MJBJltcK0b7tztRFluEcmlg4PVSA23h4LTbsBu7Aks= +github.com/go-ruby-rspec/rspec v0.0.0-20260916102141-613d88c1871f h1:m9PDLa3PK7Inmo5czL91w+xeciwRYpjofzUmbxXALKA= +github.com/go-ruby-rspec/rspec v0.0.0-20260916102141-613d88c1871f/go.mod h1:BYeb8EVnYLyl+ykaRyA1XAKJl3bOad/hJ7D+ZZCj2SQ= +github.com/go-ruby-rss/rss v0.0.0-20260923204152-16e2242a2336 h1:1DIDK6OLYSKVFTEPWjqLZlgoOTkUXmReTGpRZccd0Rs= +github.com/go-ruby-rss/rss v0.0.0-20260923204152-16e2242a2336/go.mod h1:zjFoQB1Tlt2ffTMKm88uwp/mR7oYTt7lxNkmLsjtF5c= +github.com/go-ruby-rubocop/rubocop v0.0.0-20261001114457-ff08439ed26a h1:2+6KDea5HHSH99pjNib7zRPevXGlKZ1kpkZ+sTtWhyI= +github.com/go-ruby-rubocop/rubocop v0.0.0-20261001114457-ff08439ed26a/go.mod h1:Pqzce62DYUMRWABwyRAIxg50GLd4JIBgQY1zxxSG8JY= +github.com/go-ruby-rubygems/rubygems v0.0.0-20260916102314-9bd0bf6a00e5 h1:JnnWSLv/hr2dPp/I7IlvxmMKdZ/znoq0GsnE18Grnvs= +github.com/go-ruby-rubygems/rubygems v0.0.0-20260916102314-9bd0bf6a00e5/go.mod h1:NuLiHni0BIuZk9tJvpfcqNgaSnFtH+wylUrz/tX9Vgo= +github.com/go-ruby-saml/saml v0.0.0-20260925100912-118142122e06 h1:WKsOTM/gZya1agE02Z9hl4cnCBJZEF1nCGZbESD6ciM= +github.com/go-ruby-saml/saml v0.0.0-20260925100912-118142122e06/go.mod h1:HReG7YVN49DUtQFbSDcrAec0dbb5wBe7hOSJ5QIxuLE= +github.com/go-ruby-sass/sass v0.0.0-20260906100410-777830f19847 h1:ELua5/LWhKtr4sCKpwpE9b+M5etlkgBnEPgHyvsJsrg= +github.com/go-ruby-sass/sass v0.0.0-20260906100410-777830f19847/go.mod h1:x1nYqayL1xpriwIyqXnvfJ5ptOVePxanJXjXFI6Zn/E= +github.com/go-ruby-scanf/scanf v0.0.0-20260916102437-76341a9284aa h1:HDPt4wjtK+VCeuoK1aWUODR78StZLilYB9CVTdiKMzA= +github.com/go-ruby-scanf/scanf v0.0.0-20260916102437-76341a9284aa/go.mod h1:UpBHz/Hf4Q8h14UpfJ5Cpp+OxVEqqNuICXGnUjg2dKc= +github.com/go-ruby-securerandom/securerandom v0.0.0-20260916102518-8f55919c115b h1:nSaBApt934aDq+ECaxf4nnrxj2Ga0uJ1+fKgbrOK8FM= +github.com/go-ruby-securerandom/securerandom v0.0.0-20260916102518-8f55919c115b/go.mod h1:AXmjWZDPb7iCmnVXsR16rEQdV2H6B46s8GewxL6HYCc= +github.com/go-ruby-semantic-puppet/semantic-puppet v0.0.0-20260825110441-e577792d52e8 h1:zWec4nwshrcw1X4ty1YjSbsyI8kO23bzxMUevtckbHg= +github.com/go-ruby-semantic-puppet/semantic-puppet v0.0.0-20260825110441-e577792d52e8/go.mod h1:bWZ13s/vD9dORGSNfbqXqHrsqPJjE1bV+M3Scu/N5zA= +github.com/go-ruby-sequel/sequel v0.0.0-20260916102612-ac9710f8a470 h1:WJTuO/1OElcTgRzjBho+U6ZpP7OI8nc3O8abt63/zOQ= +github.com/go-ruby-sequel/sequel v0.0.0-20260916102612-ac9710f8a470/go.mod h1:24vGiLxZE/YuFm5qNQpiXYloUQNg3w4AGW8zTmdI2Xw= +github.com/go-ruby-shellwords/shellwords v0.0.0-20260916102720-dcaddc4ec112 h1:kj85uTSB28WFuN0NsNUlr7q42ZDKVkB0M6dOEn/UNuI= +github.com/go-ruby-shellwords/shellwords v0.0.0-20260916102720-dcaddc4ec112/go.mod h1:gJ+9ba+y8+DKenwjZKbTLVSuX6Xp7V5R7pZ6NwtD6x0= +github.com/go-ruby-shrine/shrine v0.0.0-20260717074136-96ee44b6c6c8 h1:tolkKlBpIzRdBZ5yQUxcj0tn6ICRd1DQ5fEuhOHix14= +github.com/go-ruby-shrine/shrine v0.0.0-20260717074136-96ee44b6c6c8/go.mod h1:WTwiy8FfwQDh+uDa/HnFGaahCDeXu88cNAj2AFWWyYM= +github.com/go-ruby-sidekiq/sidekiq v0.0.0-20260916102813-4c8927cd45b5 h1:pcGsmdC/tfPLCaHP+o4hOz57UDuJ1EdDnHtobs7iScw= +github.com/go-ruby-sidekiq/sidekiq v0.0.0-20260916102813-4c8927cd45b5/go.mod h1:RogTfQdQmZzK9LfIAWOSMRvS8J6SXfdei/yTT3TgMG0= +github.com/go-ruby-simplecov/simplecov v0.0.0-20260717074233-00faa55e2495 h1:5q+V3HxHLLBpoJLv9EdYP99q095aRwmjLaC0o3HANos= +github.com/go-ruby-simplecov/simplecov v0.0.0-20260717074233-00faa55e2495/go.mod h1:cBtT0grdK86g1Rqbj9CJw/YPKg9teGlt6oH5J1axyF4= +github.com/go-ruby-sinatra/sinatra v0.0.0-20260923204248-301a06b96ca0 h1:fWlFn8VVlk8EX+xDJkFKZPIZA8NGXVnIWja/biUWhC8= +github.com/go-ruby-sinatra/sinatra v0.0.0-20260923204248-301a06b96ca0/go.mod h1:SkmmvxVArC0uaRey0s3WmydG5cUJe5b2mxtQ3A2qhDw= +github.com/go-ruby-slim/slim v0.0.0-20260916102940-76e23809c696 h1:qWaRnip/3lnU/F6ZPX8qmn7HRwGHnk5xaflCN9XzftU= +github.com/go-ruby-slim/slim v0.0.0-20260916102940-76e23809c696/go.mod h1:+y2RCJmyKesyDRfmIlOSHFbBzTgEIpch7rqkFqPunAw= +github.com/go-ruby-sodium/sodium v0.0.0-20260910094952-712708ed6e8d h1:WSd/cg1swqmApWQSjLLE9uTfocFyrm5IfZD4bImAieY= +github.com/go-ruby-sodium/sodium v0.0.0-20260910094952-712708ed6e8d/go.mod h1:dRS1KD9eSNqEhVK0prIVG/ddEy7yOoBsRQCD/fHWLKg= +github.com/go-ruby-sqlite3/sqlite3 v0.0.0-20260930114044-3861a7b679bd h1:HYJTSG6DVewPTdGCjEb87+Rtiy6KmCwrFZZ2LzSXc5U= +github.com/go-ruby-sqlite3/sqlite3 v0.0.0-20260930114044-3861a7b679bd/go.mod h1:zlMIvLKiV3vaCYQ8ty/mE5YWX6aognx4+J0bLcWV2gc= +github.com/go-ruby-strscan/strscan v0.0.0-20260916103346-94924cbd2f12 h1:xgWvfi1759PGBUHNk4cTsGOGknSzeTd+BQSIOzM1ZCc= +github.com/go-ruby-strscan/strscan v0.0.0-20260916103346-94924cbd2f12/go.mod h1:7ZDv0gKkkx+fIiUZfpMY8HIqebGzvXSrCIm0xEEg/ik= +github.com/go-ruby-thor/thor v0.0.0-20260916103450-ad4daa2c5591 h1:Phw9GME8NRnJPln/APQczfDq8ujzfuTXrVs3NnRTuFE= +github.com/go-ruby-thor/thor v0.0.0-20260916103450-ad4daa2c5591/go.mod h1:uDsL93m+Z6DbNVThgn0DD5VazEMVwpe5OJZuwyIhi9A= +github.com/go-ruby-timecop/timecop v0.0.0-20260717074948-f619efc95b6b h1:rDfkFjZx8vp0ZvzJcuADxw6wyVatfQzXK/cPmPLHiO4= +github.com/go-ruby-timecop/timecop v0.0.0-20260717074948-f619efc95b6b/go.mod h1:HEd4hTQ+8R4+ndhNdpjeO1c0KLZZz2zXCnpWLg5m3I0= +github.com/go-ruby-toml/toml v0.0.0-20260916103606-531bde8e061c h1:b/DVfv+B+t8/93ZJ9N+OZzRzCM3Bd+tj3w75UR9fMKg= +github.com/go-ruby-toml/toml v0.0.0-20260916103606-531bde8e061c/go.mod h1:Pt2V2/VchBYXJPEDkPxo5DZxyQivrLmXmovgbyDvU+U= +github.com/go-ruby-tsort/tsort v0.0.0-20260916103639-a7d9f97ff914 h1:7U4VT+U+8ugy0PTu0HpYUeRePPjg8MFh4jLRe9O9yiE= +github.com/go-ruby-tsort/tsort v0.0.0-20260916103639-a7d9f97ff914/go.mod h1:QMZn01XgIYihSr1pAeqHtM5DNuf7q92rJzO1Uj1diwE= +github.com/go-ruby-typhoeus/typhoeus v0.0.0-20260916103712-7a85bb37a39c h1:Ipjs3J6hLlHrNKMUCBVdoqhEqWctmaxMNjkfpJp4Ka4= +github.com/go-ruby-typhoeus/typhoeus v0.0.0-20260916103712-7a85bb37a39c/go.mod h1:CMo0EZS3P0ASQlJQfqn2dJg0Q4Fmi8+NpWEjkqT0yKg= +github.com/go-ruby-tzinfo/tzinfo v0.0.0-20260916103743-89200c78075f h1:TwVd1pT5m9d3rdX4GGd8yZqETTLd5wHRhVlC9Rry3eI= +github.com/go-ruby-tzinfo/tzinfo v0.0.0-20260916103743-89200c78075f/go.mod h1:fSH6OCwfjMpdO+vswG/7ZkzxqaA0G1TIKG1Zx6t093Q= +github.com/go-ruby-unicode-normalize/unicode-normalize v0.0.0-20260916103816-ff4e6b030f1f h1:Qqoz8+3zR1Rv8fLpY9xvLZIsOewozC3BDtf1c3erj7o= +github.com/go-ruby-unicode-normalize/unicode-normalize v0.0.0-20260916103816-ff4e6b030f1f/go.mod h1:h46HEDiNUPt0u4Ru5djqcljbtV0x0BWgKss+HVGaun4= +github.com/go-ruby-uri/uri v0.0.0-20260916103846-a8de1e4e5937 h1:WF22KcFLpI5wIIBh7pVbr2a13Ptex9Qth/6UkYbDjKM= +github.com/go-ruby-uri/uri v0.0.0-20260916103846-a8de1e4e5937/go.mod h1:/7+NF+4kESzcNHjRtNe/lAfhgHpgElX4C+VNo0xsb6Y= +github.com/go-ruby-vcr/vcr v0.0.0-20260717075257-3b3a803b4619 h1:YgHiWCLagMVGW9i6WlcJbbwVDYXhGF21XHoE2GP903Q= +github.com/go-ruby-vcr/vcr v0.0.0-20260717075257-3b3a803b4619/go.mod h1:YK1MSInsLFhUeYgeK9MJvzrmQmSU5wgmNHPX79mMFiA= +github.com/go-ruby-warden/warden v0.0.0-20260927173355-64506bbab3be h1:a/0sT+sg61Zg7tW1mSkKerJoTSpzHnA1E1Klg7LYmVg= +github.com/go-ruby-warden/warden v0.0.0-20260927173355-64506bbab3be/go.mod h1:km4VezGFAE7+LcPUcDJGkQB388b60fuk/pAhj9eDGXM= +github.com/go-ruby-webauthn/webauthn v0.0.0-20260921104542-2bb711883889 h1:e8XGAMGrWqyXEie0Iq0y8HU0/Slyt4eGcTHUXzah/Pw= +github.com/go-ruby-webauthn/webauthn v0.0.0-20260921104542-2bb711883889/go.mod h1:nFIGjCLJkRTE4KKKn0WhLvBaVJ1GQaZizHgaUYn8CAE= +github.com/go-ruby-webmock/webmock v0.0.0-20260717075423-a92c67f51b7f h1:xAyl4OGxw50gRFWG5FtrTpUYG2tuKgOX2fnTCvDR/QM= +github.com/go-ruby-webmock/webmock v0.0.0-20260717075423-a92c67f51b7f/go.mod h1:yUFQOsGseFaPfF9tjFaRXVAbdDL6FPuXhVmF+GLamcc= +github.com/go-ruby-webrick/webrick v0.0.0-20260927173350-a71c0afa01d4 h1:bX8iN2pjBtCvO3AFGtOolomouaKswjDmSVGyIcuv5OM= +github.com/go-ruby-webrick/webrick v0.0.0-20260927173350-a71c0afa01d4/go.mod h1:yhWqcwHkPnMcy4FJ5lI5rzaSLhFKAM7luqWsecUwdGY= +github.com/go-ruby-widgets/mvvm v0.2.0 h1:O6+5U5jRbyiAH5mR0Nq58lDWMA6VutBZBewuhk7UrR8= +github.com/go-ruby-widgets/mvvm v0.2.0/go.mod h1:di+jtvAHgJwXQkQk4uE1dnAgGqisUleVfkkfLeWOcdE= +github.com/go-ruby-widgets/tui v0.4.0 h1:zXQK4yJ4WnURunVRpUJaEsxaD2Y/UqoP9OmoUdoqmr8= +github.com/go-ruby-widgets/tui v0.4.0/go.mod h1:vwshS+pc0zB3CJtm6R7BikDgA0Iry1TVstj47+qtmKw= +github.com/go-ruby-widgets/widgets v0.12.0 h1:aCD0sFQhu36XsueHp0OU31/EAhq4z0a1LeWjsH56K+o= +github.com/go-ruby-widgets/widgets v0.12.0/go.mod h1:Ly3vcTZnmFGq/V24jEXjIwIHo/2YW79/xXNu6aNvGs0= +github.com/go-ruby-yaml/yaml v0.0.0-20260916104302-910ced2db1c7 h1:0p98W8xQ9WMGipkUVYZcgqvfVED1pEBk4IMjmjAej2s= +github.com/go-ruby-yaml/yaml v0.0.0-20260916104302-910ced2db1c7/go.mod h1:HtbQgnZpSTJBAhPg1jOKgHycZl3tXgf9ZdOu4dnHWjQ= +github.com/go-ruby-zeitwerk/zeitwerk v0.0.0-20260916104335-29169305e951 h1:z3nFdVxZdAyuPZekIkfqzxoJbIimfQTMRMcMH1UzE7M= +github.com/go-ruby-zeitwerk/zeitwerk v0.0.0-20260916104335-29169305e951/go.mod h1:e282nPoQTTtYK2TUfrIXVe5AZ80h0lgzYvx4xpL/aDA= +github.com/go-ruby-zlib/zlib v0.0.0-20260927173414-e9d862990b6d h1:HN8hfYpRteVZCUx4tcJU8fVWTRKNdsyzuHzf8GAuzJ4= +github.com/go-ruby-zlib/zlib v0.0.0-20260927173414-e9d862990b6d/go.mod h1:DU4Ow6jFNhybuffUGkKvOCW6rinG5RqpRugrHH/zydk= +github.com/go-scss/scss v0.0.0-20260905061546-39932e01faa4 h1:PpCxQnwQfCJXYKj1HdDjECB3u/c/c4wOawMPT5LCFVs= +github.com/go-scss/scss v0.0.0-20260905061546-39932e01faa4/go.mod h1:TcSRuwTSVas0+vXMRFdq4RiAmlaK+z5FQh1/6+4fWTo= +github.com/go-simd/adler32 v0.0.0-20260923204409-4db2a9281622 h1:CGnOevgOjgWKZjKfZfMMvCEkmBlnxoge1uUHQDH6/oM= +github.com/go-simd/adler32 v0.0.0-20260923204409-4db2a9281622/go.mod h1:Ev/S50A53N1VSPR82MDZ4kSRn+pnYMA0EaTR25L61xg= +github.com/go-simd/base64 v0.0.0-20260903220000-c04f5883bb18 h1:qRxVDQapJmkrxakah1XqYxnYc8nhbTUhb0R7ZF+ZkiU= +github.com/go-simd/base64 v0.0.0-20260903220000-c04f5883bb18/go.mod h1:P2YPxBiS0h0R01P7Cqrhj4cZHKiacpO3Z2qLE3RflOQ= +github.com/go-simd/crc32 v0.0.0-20260903220012-5f164e0e0487 h1:6JZ4TKf3j6y7xS9meu2MBQZHkkeebFWiQo8x2htKfd0= +github.com/go-simd/crc32 v0.0.0-20260903220012-5f164e0e0487/go.mod h1:lickteAl0z3L24ypOqBe0AwoHPMMxxpYcxJu72aO1kw= +github.com/go-simd/hex v0.0.0-20260903220024-a8d22a843218 h1:TymapuhKzqUf8LNtsv8L4PJY/gz5bAaEbD2cpot1vFI= +github.com/go-simd/hex v0.0.0-20260903220024-a8d22a843218/go.mod h1:ZwRN06+N0wTIjn+mxT2NF/CbzTMAlfH/c0CKG8q5cwU= +github.com/go-sql-driver/mysql v1.10.1 h1:arlSnNLq6a5yxGxV7qg9lF4j0C+KwD6NbQyKr9QL6ME= +github.com/go-sql-driver/mysql v1.10.1/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-statistics/stats v0.1.0 h1:m8/5lxOZEcbWp51T6evG3Yx0ZzMlOO2wIEZa8uBGryA= +github.com/go-statistics/stats v0.1.0/go.mod h1:hyKv4yhaJ7aDpNbR1MLNjiiBPeP5iZb8Q075y+q+FEw= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= +github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/go-tex/math v0.50.0 h1:YHJ3LYzS45zBoPKGBkZW4+LUQkOeHP30YWLsfXGZCpo= +github.com/go-tex/math v0.50.0/go.mod h1:cPm5juijQkhFj1MCByLF8tOfZ+UswAGUPBh+Wy6+vbw= +github.com/go-typeset/bidi v0.3.0 h1:4fjGjejvjE2LzLNzY4si8PkVO321NcsKIiANhWT3jF4= +github.com/go-typeset/bidi v0.3.0/go.mod h1:ct3cmYT8Qt1FGJQ+2QaakrUxqaP4ZysgdQQg2ym5+Xo= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-webauthn/webauthn v0.18.2 h1:0BeftmEHU7i3Dv0VFwBtidy/ba37Vcdjvqst9EYu8Sk= +github.com/go-webauthn/webauthn v0.18.2/go.mod h1:hEXaOuLxvZ3zG9miZe3ehlyeVso9AtklXG+kTn36k+A= +github.com/go-webauthn/x v0.3.1 h1:1ff37z3XfmTTomkhlURgGizLIDyOvPgTt2t9nlzKLRo= +github.com/go-webauthn/x v0.3.1/go.mod h1:ZInxAynYXfBPvvm5gzKZ7geBlL23K71xASMgohHl/Rg= +github.com/go-widgets/mvvm v0.9.0 h1:NwW/hAAsJUGlxmNt0ErQ7fUrud3w5Ys1RWm7hVugdJs= +github.com/go-widgets/mvvm v0.9.0/go.mod h1:1cUYVpDei59Ka7W3pMD1FWksBmBXfPnbPWHzBAJ/ebA= +github.com/go-widgets/painter v0.15.0 h1:0oeaRDTPcIe5/b0wHtYbAUsnReoidkSapr8y+iZJwDk= +github.com/go-widgets/painter v0.15.0/go.mod h1:FSLNdrtzfDF3md+vtaORyS4nrOKLW6eLaCT5BuO9EWg= +github.com/go-widgets/toolkit v0.321.2 h1:l36f1g9rrxEy/4OgM82V/uNLQxB1hDNAU5nrc91okno= +github.com/go-widgets/toolkit v0.321.2/go.mod h1:9nubMil78K/7phDy5hNMPFZn6z0bycqYchip020sFLs= +github.com/go-widgets/tui v0.61.0 h1:DmT5TOpCrnXBnG6t1060BvsaHLyJXEM23DWH4SpuGt8= +github.com/go-widgets/tui v0.61.0/go.mod h1:Y1CSyfhaqltz2D+khiAzUXWSyZPI03TvxaupubpsTIc= +github.com/go-xslt/xslt v0.1.0 h1:ivmior1Ea1XQSnGbI2F90cP7cJ33JPxf+nosJ0xHGwU= +github.com/go-xslt/xslt v0.1.0/go.mod h1:N6IdXZRclCsBtmDpdeVxciDpSrKMvYA3JaEDAflL6/w= +github.com/go-zookeeper/zk v1.0.4 h1:DPzxraQx7OrPyXq2phlGlNSIyWEsAox0RJmjTseMV6I= +github.com/go-zookeeper/zk v1.0.4/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= +github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= +github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs= +github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= +github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba h1:qJEJcuLzH5KDR0gKc0zcktin6KSAwL7+jWKBYceddTc= +github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba/go.mod h1:EFYHy8/1y2KfgTAsx7Luu7NGhoxtuVHnNo8jE7FikKc= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo= +github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gophercloud/gophercloud/v2 v2.15.0 h1:4zLiLYTFraZMlJ77FH1Kzq7itjfVP+BIbWcCurCrgic= +github.com/gophercloud/gophercloud/v2 v2.15.0/go.mod h1:4fs5I9VH6Wg2LyocDL9xf0ASb8VD63tyLA8sgAX/69U= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/graphql-go/graphql v0.8.1 h1:p7/Ou/WpmulocJeEx7wjQy611rtXGQaAcXGqanuMMgc= +github.com/graphql-go/graphql v0.8.1/go.mod h1:nKiHzRM0qopJEwCITUuIsxk9PlVlwIiiI8pnJEhordQ= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= +github.com/grpc-transports/websocket v0.2.0 h1:n4lN066PPcVTvTfLdwa84gfNiAzvS9k4SSQ1yWuG3+g= +github.com/grpc-transports/websocket v0.2.0/go.mod h1:2z7Qy11qgPaGDfqzImOt0iyLwH0wjfsvaiidCIIpzgg= +github.com/hashicorp/consul/api v1.34.2 h1:B5jqSSKwWyY8U8WiGS5vmPEPkkF0bAvrECykdZkDR80= +github.com/hashicorp/consul/api v1.34.2/go.mod h1:+gAdHQa2zvgYX3ZfcgITtnYCSj6AgS/cgotvCKaE+b8= +github.com/hashicorp/consul/sdk v0.18.1 h1:RDTeBvAeOveI2xI86sV+8WkaN7OkP4zz+cG3fOobDCM= +github.com/hashicorp/consul/sdk v0.18.1/go.mod h1:XdP2tEJmAvlK4jgoKTTtohGkRJlS4mU44mv9/sjU21s= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI= +github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= +github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I= +github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM= +github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0= +github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= +github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= +github.com/hashicorp/vault/api v1.23.0 h1:gXgluBsSECfRWTSW9niY2jwg2e9mMJc4WoHNv4g3h6A= +github.com/hashicorp/vault/api v1.23.0/go.mod h1:zransKiB9ftp+kgY8ydjnvCU7Wk8i9L0DYWpXeMj9ko= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= +github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= +github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= +github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= +github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lestrrat-go/strftime v1.0.4 h1:T1Rb9EPkAhgxKqbcMIPguPq8glqXTA1koF8n9BHElA8= +github.com/lestrrat-go/strftime v1.0.4/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g= +github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU= +github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= +github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mhr3/adler32-simd v0.3.0 h1:4CdhRQdivbqK8X1rGRsd50n48hUFtzQEd5pBzndgnC0= +github.com/mhr3/adler32-simd v0.3.0/go.mod h1:dWAnxNc3wSXy0lhxgkXMNj9xyY46iL8vZwkETDE2Gpw= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4= +github.com/minio/highwayhash v1.0.4/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= +github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= +github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nats-io/jwt/v2 v2.8.2 h1:XXRgB60MSTnqsRwejQurVDs/hcv2dkt+86GjI+I/bMc= +github.com/nats-io/jwt/v2 v2.8.2/go.mod h1:Ag/56sq9OblL4JgdYufDd16Egb17Kr/8WwwuO/forVc= +github.com/nats-io/nats-server/v2 v2.15.0 h1:M99yf0y05rTr46/qc/Is6ZAowI58Ryp2SjufLCUeVJc= +github.com/nats-io/nats-server/v2 v2.15.0/go.mod h1:5qLF4CDGzZVFt//3fUrY1ePpwbi05r7QHPNroSUtolk= +github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= +github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k= +github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= +github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/onsi/ginkgo/v2 v2.33.0 h1:C8gBA6Uc2ZEubiV+SXiu5tZnMTwEmXHgkJwGozKtZf8= +github.com/onsi/ginkgo/v2 v2.33.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= +github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ= +github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= +github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/pierrec/lz4/v4 v4.1.30 h1:cchX8N2DVP668WkElI9QMwVyoNabLkq1LofDHFeIrdg= +github.com/pierrec/lz4/v4 v4.1.30/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/redis/go-redis/v9 v9.22.0 h1:laDvpYXTJtZLloinw1fA5Kqd6HAEH2XKxOkG/PDq2F0= +github.com/redis/go-redis/v9 v9.22.0/go.mod h1:y2g0Wj8rQvuK0ELM+oxSudcLtC09JScs98I/X9gRWY4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g= +github.com/rogpeppe/go-internal v1.16.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs= +github.com/russellhaering/goxmldsig v1.6.1 h1:SB7R5ttvrGIDB2juJAK/i7DQ2Ivr7agG+ohfNJjwyYU= +github.com/russellhaering/goxmldsig v1.6.1/go.mod h1:haZkRcLs9W/Xp989fIjP3BrTdbFQveRF0QNZSYoH09w= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sergeymakinen/go-bmp v1.0.0 h1:SdGTzp9WvCV0A1V0mBeaS7kQAwNLdVJbmHlqNWq0R+M= +github.com/sergeymakinen/go-bmp v1.0.0/go.mod h1:/mxlAQZRLxSvJFNIEGGLBE/m40f3ZnUifpgVDlcUIEY= +github.com/sergeymakinen/go-ico v1.0.0 h1:uL3khgvKkY6WfAetA+RqsguClBuu7HpvBB/nq/Jvr80= +github.com/sergeymakinen/go-ico v1.0.0/go.mod h1:wQ47mTczswBO5F0NoDt7O0IXgnV4Xy3ojrroMQzyhUk= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo= +github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q= +github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= +github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +github.com/tannevaled/gobig2 v0.2.0 h1:yx97/ltljALrIWDFVqHjPUIjAp3bfUUhH8Z8UROvNLc= +github.com/tannevaled/gobig2 v0.2.0/go.mod h1:X0S1H+N35kg6zYgVYRqGMsGj3C35zs+iA2r1MXyPcV4= +github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4= +github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= +github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= +github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= +github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= +github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/twmb/franz-go v1.22.1 h1:J7Xixbb7k0Itl39eaBot5PIblZh9IL3ZKYgo2yzlf40= +github.com/twmb/franz-go v1.22.1/go.mod h1:b2qISbZgMTJRcIsltVqPz4+Bb2Lw/9bN+/Gd0C07kYw= +github.com/twmb/franz-go/pkg/kadm v1.19.0 h1:5Nx/WWFkpNUi8Z55Skxvn9x5HOCjw+BUntSNB1kLglk= +github.com/twmb/franz-go/pkg/kadm v1.19.0/go.mod h1:emmsx5J7YPU9A7UHcSoz0fBMYVmCcJO2etylJeU0VHU= +github.com/twmb/franz-go/pkg/kfake v0.0.0-20260927204940-b5a45ccfdf7e h1:LZ7xcGp2SCGhYdwecwWzwrXdNZwoyAMOREzomAEq8G4= +github.com/twmb/franz-go/pkg/kfake v0.0.0-20260927204940-b5a45ccfdf7e/go.mod h1:TG+7GhIS2HEiBNWJUb+2m0F+rB87IbU7WtWSWBDnOL4= +github.com/twmb/franz-go/pkg/kmsg v1.14.0 h1:gSxrBEKWl3qnsx3QKWol5OEVujuPmIoDkhMt3didFKM= +github.com/twmb/franz-go/pkg/kmsg v1.14.0/go.mod h1:+DPt4NC8RmI6hqb8G09+3giKObE6uD2Eya6CfqBpeJY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs= +github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chqDkyE9Z4N61UnQd+KOfgp5Iu53llk= +github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= +go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU= +go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk= +go.etcd.io/etcd/api/v3 v3.7.2 h1:xgt/6el1LsPWWYNLkhMAK4tZm6dF+1sCqDecpE5gdbk= +go.etcd.io/etcd/api/v3 v3.7.2/go.mod h1:RoRCBRt9BfBff1pIGZLUVMiz7wu3bY+b2qLysGu1HY4= +go.etcd.io/etcd/client/pkg/v3 v3.7.2 h1:SVtlR7tiSVAYOQ4nWPIyFXb4RMgEcnzeAG9RQ8MoNDU= +go.etcd.io/etcd/client/pkg/v3 v3.7.2/go.mod h1:HsSux/B3ahgyw/D5+d4YbZqicOi0mEbuxm6lIUdjAoI= +go.etcd.io/etcd/client/v3 v3.7.2 h1:Z66GqDQDI7zPDfVSsIBqGSK4mJYLtv8ESwXa4mPf+wY= +go.etcd.io/etcd/client/v3 v3.7.2/go.mod h1:x03t1qMs4tGZirCDJlMuzPBJdQffXJImIyEjLhNBCsY= +go.etcd.io/etcd/pkg/v3 v3.7.2 h1:bC8FAE6cWtbTS38kvkrbhcwqUpMDnSeNAIHgJ0ECB3s= +go.etcd.io/etcd/pkg/v3 v3.7.2/go.mod h1:XTscG8UUP11rTrHc3Den4gzTiabEh2AMp8vqNxswZiI= +go.etcd.io/etcd/server/v3 v3.7.2 h1:gfnwItZwsDFKUqCJocsBVMNNtWYGTl7/dHc+83qeYVo= +go.etcd.io/etcd/server/v3 v3.7.2/go.mod h1:tlvKX6r/kTEqRV9mydK2qzgI4WcojFEHKHHsZ6DG024= +go.etcd.io/raft/v3 v3.7.0 h1:BGzlwx07bLv8PW6OU5HObuz1y4hlPZUXA07pM1mPUh4= +go.etcd.io/raft/v3 v3.7.0/go.mod h1:6gX6T2X907DjnjsFLODnTxba77stjs84W9gTTI0GUNA= +go.mongodb.org/mongo-driver/v2 v2.9.1 h1:jewiFs2m1/VOQp8qhFshX6hWZ+EAXDhZHXExAUMcOgQ= +go.mongodb.org/mongo-driver/v2 v2.9.1/go.mod h1:SHKN0IWkKmEVGHLjXnni6s4wPKX4v86FTgOeJJFuXcA= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg= +go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM= +go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk= +go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI= +go.opentelemetry.io/otel/log v1.47.0/go.mod h1:9byitSQ5pLC6PpqwGXjqdMKya6ZTswHRZh2vvXT33nw= +go.opentelemetry.io/otel/metric v1.47.0 h1:4PptaldXx3Eat1XjMZ68pPJEs5wrhlemctZE9a3UdWY= +go.opentelemetry.io/otel/metric v1.47.0/go.mod h1:ADGSXxRrXM6bjbvLo535EstVFlPpPYZm4LBKixjDHwU= +go.opentelemetry.io/otel/sdk v1.47.0 h1:zWXEr4j2lFefG87TU6Yg8a7ngfohIKFZHKp0Hf5hC6I= +go.opentelemetry.io/otel/sdk v1.47.0/go.mod h1:VUc24kiOeoGsxG8G9ULx3fWKvB7jMhnGE8Oi607lgR0= +go.opentelemetry.io/otel/sdk/metric v1.47.0 h1:lfISg2j93VT6yqdk9OfUaZmw/GfcZqCCV3jdXtsPnKw= +go.opentelemetry.io/otel/sdk/metric v1.47.0/go.mod h1:ypLp+mW1Nt2x+Szt3b5/i1syodyts49lMOwxpDI3VGw= +go.opentelemetry.io/otel/trace v1.47.0 h1:JOjX/Oci8K94QHddo+bbfya/Ai/nf6/dt9ZfrFNWSrM= +go.opentelemetry.io/otel/trace v1.47.0/go.mod h1:jNaSLa2PZEYFG6fRjJABAu+bw4FS08uDmPg28lTghu0= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= +golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= +golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ= +golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= +golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= +golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= +golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/telemetry v0.0.0-20260908163034-4bcc4b2ee518 h1:F5BWKvW126NXR74uxkxuc1jQHhm/rwm/J3rSiFyuRs4= +golang.org/x/telemetry v0.0.0-20260908163034-4bcc4b2ee518/go.mod h1:i+ivNqjDnTF3WTElsdk5g9V5DTSBYgdNo7xTU9SDwYA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= +golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= +golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE= +golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU= +golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800 h1:admdQBe8jR3VWhBsUrAOaF2Qw6K/+p5pSm1GN8+6Fw4= +google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800/go.mod h1:FPk7EXUKMtImne7AmknoYjT4QXqKIzzRbeQIXzLk6fQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 h1:qEHAMpSaUhtD0p3NbEEI83HwNGFxEwaSJ1G9PLnCBZE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0= +google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/src-d/go-errors.v1 v1.0.0 h1:cooGdZnCjYbeS1zb1s6pVAAimTdKceRrpn7aKOnNIfc= +gopkg.in/src-d/go-errors.v1 v1.0.0/go.mod h1:q1cBlomlw2FnDBDNGlnh6X0jPihy+QxZfMMNxPCbdYg= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +k8s.io/utils v0.0.0-20260108192941-914a6e750570 h1:JT4W8lsdrGENg9W+YwwdLJxklIuKWdRm+BC+xt33FOY= +k8s.io/utils v0.0.0-20260108192941-914a6e750570/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +modernc.org/cc/v4 v4.29.7 h1:q+NXGJ0bK3b4TXFYQQVr9pYETGnmwFWkrUzJnMya/Tg= +modernc.org/cc/v4 v4.29.7/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= +modernc.org/ccgo/v4 v4.36.1 h1:ZNIUZAryN0UgnJwtyxrdEzcFc3yD4Cu4AzjfPXsLsIE= +modernc.org/ccgo/v4 v4.36.1/go.mod h1:rrtGc2QkS239nYb/mQNuBMyjq3/y3ZXWbBjPoV3wqzA= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.5 h1:21ldfPfRYE31Tb7B3mwAK8gy1AxP4+dKjrOQPfqakoc= +modernc.org/gc/v3 v3.1.5/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.77.1 h1:Ct8j47QtiZ1Enj2DtFXQtUqrPCAjdCmPjtCuvrYQ0Hs= +modernc.org/libc v1.77.1/go.mod h1:87/pZ4L6nD1zqW4nItuS12YO7hN1igAah34xjnQo/W0= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.12.1 h1:nFMiWrpStgZczNl6XI9GnIk/rWhYIyHGUaR04pGbp9g= +modernc.org/memory v1.12.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= +modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.60.1 h1:/blz53O951KWFOso4QQvEs/Fq6cDBKLtMVrYNSeJVKw= +modernc.org/sqlite v1.60.1/go.mod h1:1dIoEagfDE72QytD5scH1lxARtaUgKgHC/NuApA27r0= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/integrate.go b/integrate.go new file mode 100644 index 0000000..887e61f --- /dev/null +++ b/integrate.go @@ -0,0 +1,5 @@ +package ruby + +import rb "github.com/go-embedded-ruby/ruby" + +func registerIntegrate(in *rb.Interp) error { return nil } diff --git a/internal/genenums/main.go b/internal/genenums/main.go new file mode 100644 index 0000000..3da4653 --- /dev/null +++ b/internal/genenums/main.go @@ -0,0 +1,206 @@ +// Command genenums writes enums_gen.go: for every enumerated type of the bound +// Go packages — a named type with a block of constants, such as +// stats.Alternative or optimize.Method — the Ruby symbols that name its values. +// +// Constants carry no reflection information, so this is the one part of the +// bindings that has to be generated: everything else is read from the Go types +// at run time. Run it from the module root with `go run ./internal/genenums`; +// TestEnumsAreGenerated fails when the output is out of date. +package main + +import ( + "bytes" + "fmt" + "go/ast" + "go/format" + "go/parser" + "go/token" + "os" + "os/exec" + "path/filepath" + "sort" + "strings" + "unicode" +) + +// packages are the bound packages, by import path and the name the generated +// file imports them under. +var packages = []struct{ path, name string }{ + {"github.com/go-statistics/stats", "stats"}, + {"github.com/go-numerics/optimize", "optimize"}, + {"github.com/go-numerics/integrate", "integrate"}, +} + +type constant struct{ name string } + +func main() { + out := "enums_gen.go" + if len(os.Args) > 1 { + out = os.Args[1] + } + src, err := generate(moduleDir) + if err == nil { + err = os.WriteFile(out, src, 0o644) + } + if err != nil { + fmt.Fprintln(os.Stderr, "genenums:", err) + os.Exit(1) + } +} + +// moduleDir finds a module's source in the module cache; a seam for tests. +var moduleDir = func(path string) (string, error) { + cmd := exec.Command("go", "list", "-m", "-f", "{{.Dir}}", path) + cmd.Env = append(os.Environ(), "GOWORK=off") + b, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("go list -m %s: %v", path, err) + } + return strings.TrimSpace(string(b)), nil +} + +// generate returns the formatted source of enums_gen.go. +func generate(dir func(string) (string, error)) ([]byte, error) { + var b bytes.Buffer + b.WriteString("// Code generated by internal/genenums; DO NOT EDIT.\n\npackage ruby\n\nimport (\n\t\"reflect\"\n\n") + for _, p := range packages { + fmt.Fprintf(&b, "\t%q\n", p.path) + } + b.WriteString(")\n\n// enumTable lists, for each enumerated Go type, its values and the Ruby\n// symbols that name them.\nvar enumTable = map[reflect.Type][]enumValue{\n") + for _, p := range packages { + d, err := dir(p.path) + if err != nil { + return nil, err + } + groups, err := enums(d) + if err != nil { + return nil, err + } + types := make([]string, 0, len(groups)) + for t := range groups { + types = append(types, t) + } + sort.Strings(types) + for _, t := range types { + consts := groups[t] + fmt.Fprintf(&b, "\treflect.TypeFor[%s.%s](): {\n", p.name, t) + names := make([]string, len(consts)) + for i, c := range consts { + names[i] = c.name + } + strip := commonPrefix(names) + for _, c := range consts { + fmt.Fprintf(&b, "\t\t{%q, %s.%s},\n", snake(c.name[len(strip):]), p.name, c.name) + } + b.WriteString("\t},\n") + } + } + b.WriteString("}\n") + return format.Source(b.Bytes()) +} + +// enums returns the exported constants of each exported named type declared in +// the package in dir, in declaration order. A constant without a type of its +// own inherits the previous one's in its block, as iota constants do. +func enums(dir string) (map[string][]constant, error) { + fset := token.NewFileSet() + files, err := filepath.Glob(filepath.Join(dir, "*.go")) + if err != nil || len(files) == 0 { + return nil, fmt.Errorf("no Go files in %s", dir) + } + sort.Strings(files) + named := map[string]bool{} + var decls []*ast.GenDecl + for _, f := range files { + if strings.HasSuffix(f, "_test.go") { + continue + } + file, err := parser.ParseFile(fset, f, nil, 0) + if err != nil { + return nil, err + } + for _, d := range file.Decls { + gd, ok := d.(*ast.GenDecl) + if !ok { + continue + } + switch gd.Tok { + case token.TYPE: + for _, s := range gd.Specs { + ts := s.(*ast.TypeSpec) + if id, ok := ts.Type.(*ast.Ident); ok && ts.Name.IsExported() && ts.Assign == 0 && basic[id.Name] { + named[ts.Name.Name] = true + } + } + case token.CONST: + decls = append(decls, gd) + } + } + } + out := map[string][]constant{} + for _, gd := range decls { + typ := "" + for _, s := range gd.Specs { + vs := s.(*ast.ValueSpec) + if vs.Type != nil { + typ = "" + if id, ok := vs.Type.(*ast.Ident); ok { + typ = id.Name + } + } else if len(vs.Values) > 0 { + typ = "" // an untyped constant, or one typed by its value + } + if !named[typ] { + continue + } + for _, n := range vs.Names { + if n.IsExported() { + out[typ] = append(out[typ], constant{n.Name}) + } + } + } + } + return out, nil +} + +var basic = map[string]bool{"int": true, "int8": true, "int16": true, "int32": true, "int64": true, "uint": true, "uint8": true, "uint16": true, "uint32": true, "uint64": true, "string": true} + +// words splits a Go identifier into its CamelCase words, keeping an acronym +// whole: "TTestInd" is T Test Ind, "LBFGSB" is LBFGSB, "BinsSqrt" Bins Sqrt. +func words(s string) []string { + var out []string + r := []rune(s) + start := 0 + for i := 1; i < len(r); i++ { + lowerToUpper := unicode.IsUpper(r[i]) && (unicode.IsLower(r[i-1]) || unicode.IsDigit(r[i-1])) + acronymEnd := unicode.IsUpper(r[i]) && unicode.IsUpper(r[i-1]) && i+1 < len(r) && unicode.IsLower(r[i+1]) + if lowerToUpper || acronymEnd { + out = append(out, string(r[start:i])) + start = i + } + } + return append(out, string(r[start:])) +} + +// snake is the Ruby spelling of a Go name: "TwoSided" is two_sided. +func snake(s string) string { return strings.ToLower(strings.Join(words(s), "_")) } + +// commonPrefix is the run of whole words every name starts with, when that +// leaves something of each: CenterMedian and CenterMean share "Center". +func commonPrefix(names []string) string { + if len(names) < 2 { + return "" + } + first := words(names[0]) + prefix := "" + for i := 0; i < len(first)-1; i++ { + p := prefix + first[i] + for _, n := range names[1:] { + if !strings.HasPrefix(n, p) || len(n) == len(p) || !unicode.IsUpper([]rune(n[len(p):])[0]) && !unicode.IsDigit([]rune(n[len(p):])[0]) { + return prefix + } + } + prefix = p + } + return prefix +} diff --git a/optimize.go b/optimize.go new file mode 100644 index 0000000..bdf4649 --- /dev/null +++ b/optimize.go @@ -0,0 +1,5 @@ +package ruby + +import rb "github.com/go-embedded-ruby/ruby" + +func registerOptimize(in *rb.Interp) error { return nil } diff --git a/plot.go b/plot.go new file mode 100644 index 0000000..7198767 --- /dev/null +++ b/plot.go @@ -0,0 +1,5 @@ +package ruby + +import rb "github.com/go-embedded-ruby/ruby" + +func registerPlot(in *rb.Interp) error { return nil } diff --git a/reflect.go b/reflect.go new file mode 100644 index 0000000..6d8ba2c --- /dev/null +++ b/reflect.go @@ -0,0 +1,444 @@ +package ruby + +import ( + "errors" + "fmt" + "math" + "reflect" + "strings" + + rb "github.com/go-embedded-ruby/ruby" + nd "github.com/go-ndarray/ndarray" + "github.com/go-ndarray/ndarray/random" +) + +// This file binds Go types by reflection: a result struct becomes a Ruby class +// with a reader per field, and the methods of a Go type whose parameters and +// results convert are bound as they are, under their snake_case names. + +// definer defines modules and classes, keeping the first error so a register +// function can define everything and check once. +type definer struct { + in *rb.Interp + err error +} + +func (d *definer) module(name string) *rb.Module { + m, err := d.in.DefineModule(name) + d.keep(err) + return m +} + +func (d *definer) class(parent *rb.Module, name string) *rb.Module { + if parent == nil { + return nil + } + c, err := parent.DefineClass(name, nil) + d.keep(err) + return c +} + +func (d *definer) keep(err error) { + if d.err == nil { + d.err = err + } +} + +// fn defines a module function; it does nothing once an error is kept. +func (d *definer) fn(m *rb.Module, name string, f rb.Func) { + if m != nil { + m.DefineSingletonMethod(name, f) + } +} + +// method defines an instance method; it does nothing once an error is kept. +func (d *definer) method(m *rb.Module, name string, f rb.Func) { + if m != nil { + m.DefineMethod(name, f) + } +} + +// structClass defines the class name inside parent for the Go struct type of +// sample (a value, not a pointer): values of the type and pointers to it +// become its instances, each exported field a reader, each convertible method +// a method, plus to_h and inspect. +func (d *definer) structClass(parent *rb.Module, name string, sample any) *rb.Module { + c := d.class(parent, name) + if c == nil { + return nil + } + t := reflect.TypeOf(sample) + c.Bind(sample) + c.Bind(reflect.New(t).Interface()) + fields := readableFields(t) + for _, f := range fields { + f := f + c.DefineMethod(snakeName(f.Name), func(call *rb.Call) (any, error) { + if err := noArgs(call); err != nil { + return nil, err + } + return out(structOf(call.Self).FieldByIndex(f.Index)), nil + }) + } + c.DefineMethod("to_h", func(call *rb.Call) (any, error) { + v := structOf(call.Self) + pairs := make([]any, len(fields)) + for i, f := range fields { + pairs[i] = []any{rb.Symbol(snakeName(f.Name)), out(v.FieldByIndex(f.Index))} + } + list, _ := call.Interp.ValueOf(pairs) + return list.Call("to_h") + }) + c.DefineMethod("inspect", func(call *rb.Call) (any, error) { + v := structOf(call.Self) + parts := make([]string, len(fields)) + for i, f := range fields { + fv, _ := call.Interp.ValueOf(out(v.FieldByIndex(f.Index))) + parts[i] = snakeName(f.Name) + "=" + fv.Inspect() + } + return "#<" + c.Name() + " " + strings.Join(parts, ", ") + ">", nil + }) + bindMethods(c, reflect.PointerTo(t), nil) + return c +} + +// structOf is the struct value behind self, a T or a *T. +func structOf(self rb.Value) reflect.Value { + return reflect.Indirect(reflect.ValueOf(self.Go())) +} + +// readableFields are the exported fields that convert to Ruby. +func readableFields(t reflect.Type) []reflect.StructField { + var out []reflect.StructField + for _, f := range reflect.VisibleFields(t) { + if f.IsExported() && !f.Anonymous && f.Type.Kind() != reflect.Func { + out = append(out, f) + } + } + return out +} + +func noArgs(c *rb.Call) error { + if len(c.Args) > 0 || len(c.Kwargs) > 0 { + return argError("wrong number of arguments (given %d, expected 0)", len(c.Args)+len(c.Kwargs)) + } + return nil +} + +// bindMethods binds every method of t (a pointer type, so the value methods +// are included) whose parameters and results convert, except those named in +// skip, under its snake_case name. It returns the Ruby names bound. +func bindMethods(c *rb.Module, t reflect.Type, skip map[string]bool) []string { + var names []string + for i := range t.NumMethod() { + m := t.Method(i) + if skip[m.Name] || !convertible(m.Type, 1) { + continue + } + name := snakeName(m.Name) + names = append(names, name) + c.DefineMethod(name, func(call *rb.Call) (any, error) { + recv := reflect.ValueOf(call.Self.Go()) + if recv.Kind() != reflect.Pointer { + p := reflect.New(recv.Type()) + p.Elem().Set(recv) + recv = p + } + return invoke(recv.MethodByName(m.Name), call, call.Args) + }) + } + return names +} + +// convertible reports whether a func type's parameters (from the first'th +// on) and results all convert: no channels or interfaces other than error, +// and no func other than a func(float64) float64 (which the block supplies). +func convertible(ft reflect.Type, first int) bool { + for i := first; i < ft.NumIn(); i++ { + if in := ft.In(i); !plain(in) && in != scalarFuncType { + return false + } + } + for i := range ft.NumOut() { + if o := ft.Out(i); !plain(o) && o != errorType { + return false + } + } + return true +} + +var ( + errorType = reflect.TypeFor[error]() + scalarFuncType = reflect.TypeFor[func(float64) float64]() + generatorType = reflect.TypeFor[*random.Generator]() +) + +// plain reports whether values of t cross between Ruby and Go: numbers, +// strings, booleans, slices, arrays and maps of those, NDArrays, and structs +// (as opaque or bound objects). +func plain(t reflect.Type) bool { + switch t.Kind() { + case reflect.Func, reflect.Chan, reflect.Interface, reflect.UnsafePointer, reflect.Uintptr: + return false + case reflect.Slice, reflect.Array, reflect.Pointer: + return plain(t.Elem()) + case reflect.Map: + return plain(t.Key()) && plain(t.Elem()) + } + return true +} + +// isOptions reports whether a parameter is an option struct, *XxxOptions, +// which keyword arguments fill. +func isOptions(t reflect.Type) bool { + return t.Kind() == reflect.Pointer && t.Elem().Kind() == reflect.Struct && strings.HasSuffix(t.Elem().Name(), "Options") +} + +// filled reports whether a parameter is filled otherwise than by a positional +// argument: an option struct, the block, or the random generator. +func filled(t reflect.Type) bool { return isOptions(t) || t == scalarFuncType || t == generatorType } + +// invoke calls a Go func for a Ruby call, its parameters filled in order: +// +// - an option struct (*XxxOptions) from the keyword arguments, nil when +// there are none; +// - a func(float64) float64 by the block; +// - a *random.Generator from the keyword random_state: an Integer seed +// (numpy.random.default_rng(seed)) or a Stats::RNG; nil without one; +// - every other parameter by the next positional argument, converted to its +// type, and a variadic one by all the remaining arguments (a single Array +// standing for the list too, so rvs(2, 3) and rvs([2, 3]) are one). +// +// It returns the results converted (see results). +func invoke(fn reflect.Value, c *rb.Call, args []rb.Value) (any, error) { + ft := fn.Type() + kw, rest := split(c.Kwargs, "random_state") + in := make([]reflect.Value, 0, ft.NumIn()) + pos, kwUsed := 0, len(rest) == 0 + var blockErr error + for i := range ft.NumIn() { + t := ft.In(i) + switch { + case isOptions(t): + p := reflect.Zero(t) + if len(rest) > 0 { + p = reflect.New(t.Elem()) + if err := options(p.Interface(), rest); err != nil { + return nil, err + } + kwUsed = true + } + in = append(in, p) + case t == scalarFuncType: + b, err := needBlock(c) + if err != nil { + return nil, err + } + in = append(in, reflect.ValueOf(scalarBlock(b, &blockErr))) + case t == generatorType: + g, err := generator(kw["random_state"]) + if err != nil { + return nil, err + } + delete(kw, "random_state") + in = append(in, reflect.ValueOf(g)) + case ft.IsVariadic() && i == ft.NumIn()-1: + tail := make([]any, len(args)-min(pos, len(args))) + for j := range tail { + tail[j] = args[pos+j].Go() + } + if len(tail) == 1 && t.Elem().Kind() == reflect.Int { + if list, ok := tail[0].([]any); ok { + tail = list + } + } + for _, a := range tail { + v, err := convert(a, t.Elem()) + if err != nil { + return nil, err + } + in = append(in, v) + } + pos = len(args) + default: + if pos >= len(args) { + return nil, wrongArgs(len(args), ft) + } + v, err := convert(args[pos].Go(), t) + if err != nil { + return nil, err + } + in = append(in, v) + pos++ + } + } + if pos != len(args) { + return nil, wrongArgs(len(args), ft) + } + if !kwUsed { + return nil, argError("unknown keyword: :%s", sortedKeys(rest)[0]) + } + if len(kw) > 0 { + return nil, argError("unknown keyword: :random_state") + } + res := fn.Call(in) + if blockErr != nil { + return nil, blockErr + } + return results(res) +} + +// wrongArgs is MRI's ArgumentError for a positional count that does not fit. +func wrongArgs(given int, ft reflect.Type) error { + n := 0 + for i := range ft.NumIn() { + if !filled(ft.In(i)) { + n++ + } + } + if ft.IsVariadic() { + return argError("wrong number of arguments (given %d, expected %d+)", given, n-1) + } + return argError("wrong number of arguments (given %d, expected %d)", given, n) +} + +// generator reads random_state: nil (a generator seeded by the system), an +// Integer seed, or a Stats::RNG. +func generator(v rb.Value) (*random.Generator, error) { + switch x := v.Go().(type) { + case nil: + return nil, nil + case int64: + if x < 0 { + return nil, argError("random_state must be a non-negative Integer") + } + return random.DefaultRNG(uint64(x)), nil + case *random.Generator: + return x, nil + } + return nil, typeError(v.Go(), "a seed or a Stats::RNG") +} + +// scalarBlock is a block as a func(float64) float64. A Ruby exception in the +// block, or a value that is not a number, is kept in *errp (the first one) and +// the function answers NaN from then on, so the Go algorithm finishes +// quickly; the binding raises *errp once it returns. +func scalarBlock(b *rb.Block, errp *error) func(float64) float64 { + return func(x float64) float64 { + if *errp != nil { + return math.NaN() + } + r, err := b.Call(x) + if err == nil { + var f float64 + if f, err = toFloat(r.Go()); err == nil { + return f + } + } + *errp = err + return math.NaN() + } +} + +// results converts what a Go func returned: a final error, when not nil, +// raises; one other result is that value, several an Array. +func results(vals []reflect.Value) (any, error) { + if n := len(vals); n > 0 && vals[n-1].Type() == errorType { + if err, _ := vals[n-1].Interface().(error); err != nil { + return nil, goError(err) + } + vals = vals[:n-1] + } + switch len(vals) { + case 0: + return nil, nil + case 1: + return out(vals[0]), nil + } + list := make([]any, len(vals)) + for i, v := range vals { + list[i] = out(v) + } + return list, nil +} + +// out converts one Go result for Ruby: a []float64 is an NDArray, a value of +// an enumerated type its symbol, a nil pointer or slice nil; the rest +// converts as rbgo converts it. +func out(v reflect.Value) any { + if vals, ok := enumTable[v.Type()]; ok { + for _, e := range vals { + if reflect.ValueOf(e.value).Equal(v) { + return rb.Symbol(e.name) + } + } + return v.Interface() + } + switch v.Kind() { + case reflect.Slice: + if v.IsNil() { + return nil + } + if f, ok := v.Interface().([]float64); ok { + return vec(f) + } + list := make([]any, v.Len()) + for i := range list { + list[i] = out(v.Index(i)) + } + return list + case reflect.Pointer: + if v.IsNil() { + return nil + } + } + return v.Interface() +} + +// ufunc applies f elementwise, NumPy-style: to a Float (or an Integer) it +// returns a Float, to an NDArray an NDArray of the same shape, to an Array an +// Array. +func ufunc(x rb.Value, f func(float64) float64) (any, error) { + switch v := x.Go().(type) { + case *nd.Array: + return v.Map(f), nil + case []any: + res := make([]any, len(v)) + for i, e := range v { + fl, err := toFloat(e) + if err != nil { + return nil, err + } + res[i] = f(fl) + } + return res, nil + } + fl, err := toFloat(x.Go()) + if err != nil { + return nil, typeError(x.Go(), "Float or NDArray") + } + return f(fl), nil +} + +// arity checks the number of positional arguments. +func arity(c *rb.Call, min, max int) error { + if n := len(c.Args); n < min || n > max { + want := fmt.Sprint(min) + if max > min { + want = fmt.Sprintf("%d..%d", min, max) + } + return argError("wrong number of arguments (given %d, expected %s)", n, want) + } + return nil +} + +// errNoBlock is the error of a method that needs a block. +var errNoBlock = errors.New("no block given") + +// needBlock returns the call's block, or the error for a method that needs one. +func needBlock(c *rb.Call) (*rb.Block, error) { + if c.Block == nil { + return nil, rb.NewError("ArgumentError", errNoBlock.Error()+" (yield)") + } + return c.Block, nil +} diff --git a/smoke_test.go b/smoke_test.go new file mode 100644 index 0000000..0c9d940 --- /dev/null +++ b/smoke_test.go @@ -0,0 +1,46 @@ +package ruby + +import ( + "bytes" + "testing" + + rb "github.com/go-embedded-ruby/ruby" +) + +func TestSmoke(t *testing.T) { + var out bytes.Buffer + in := rb.New(rb.WithStdout(&out), rb.WithStderr(&out)) + if err := Register(in); err != nil { + t.Fatal(err) + } + for _, src := range []string{ + `Stats::Normal.new(loc: 0, scale: 1).pdf(0.5)`, + `Stats::Normal.new.pdf(NDArray.linspace(-1, 1, 3))`, + `Stats::Normal.new(1, 2)`, + `Stats::Normal.new(1, 2).interval(0.95)`, + `Stats.mean([1, 2, 3, 4])`, + `Stats.mean([1, 2, 3, 4], weights: [1, 1, 1, 5])`, + `Stats.ttest_ind([1, 2, 3, 4], [2, 3, 4, 5.5], welch: true)`, + `Stats.ttest_ind([1, 2, 3, 4], [2, 3, 4, 5.5], alternative: :less).pvalue`, + `Stats.f_oneway([1, 2, 3], [2, 3, 4], [5, 6, 7])`, + `Stats::Normal.fit([1, 2, 3, 4.5])`, + `Stats::Gamma.fit([1, 2, 3, 4.5], fixed: {loc: 0})`, + `Stats::Normal.new.rvs(3, random_state: 42)`, + `Stats::Binomial.new(n: 10, p: 0.3).pmf([0, 1, 2])`, + `Stats.ks_1samp([0.1, -0.3, 0.5]) { |x| Stats::Normal.new.cdf(x) }`, + `Stats.describe(NDArray.arange(10))`, + `Stats::RNG.new(42).standard_normal(2, 2)`, + `Stats::GaussianKDE.new([1, 2, 3, 4, 7]).evaluate([2, 3])`, + `Stats.bootstrap([[1, 2, 3, 4, 5, 6]], random_state: 1, n_resamples: 99) { |s| s.mean }`, + `Stats::MultivariateNormal.new([0, 0], NDArray.eye(2)).pdf([0, 0])`, + `Stats.pearsonr([1, 2, 3, 4], [1, 3, 2, 4]).confidence_interval(0.9)`, + `Stats::Normal.new.to_h`, + } { + v, err := in.Eval(src, "smoke.rb", 1) + if err != nil { + t.Errorf("%s: %v", src, err) + continue + } + t.Logf("%s => %s", src, v.Inspect()) + } +} diff --git a/special.go b/special.go new file mode 100644 index 0000000..44ec167 --- /dev/null +++ b/special.go @@ -0,0 +1,5 @@ +package ruby + +import rb "github.com/go-embedded-ruby/ruby" + +func registerSpecial(in *rb.Interp) error { return nil } diff --git a/stats.go b/stats.go new file mode 100644 index 0000000..d2a007d --- /dev/null +++ b/stats.go @@ -0,0 +1,516 @@ +package ruby + +import ( + "fmt" + "reflect" + + rb "github.com/go-embedded-ruby/ruby" + nd "github.com/go-ndarray/ndarray" + "github.com/go-ndarray/ndarray/random" + "github.com/go-statistics/stats" +) + +// statsFuncs are the functions of Stats over data, each the two +// instantiations of the go-statistics function it is named after. +var statsFuncs = []struct { + name string + overS, overA any + goName string +}{ + // Descriptive statistics. + {"mean", stats.Mean[[]float64], stats.Mean[*nd.Array], "Mean"}, + {"average", stats.Average[[]float64], stats.Average[*nd.Array], "Average"}, + {"var", stats.Var[[]float64], stats.Var[*nd.Array], "Var"}, + {"std", stats.Std[[]float64], stats.Std[*nd.Array], "Std"}, + {"median", stats.Median[[]float64], stats.Median[*nd.Array], "Median"}, + {"gmean", stats.GMean[[]float64], stats.GMean[*nd.Array], "GMean"}, + {"hmean", stats.HMean[[]float64], stats.HMean[*nd.Array], "HMean"}, + {"pmean", stats.PMean[[]float64], stats.PMean[*nd.Array], "PMean"}, + {"skew", stats.Skew[[]float64], stats.Skew[*nd.Array], "Skew"}, + {"kurtosis", stats.Kurtosis[[]float64], stats.Kurtosis[*nd.Array], "Kurtosis"}, + {"moment", stats.Moment[[]float64], stats.Moment[*nd.Array], "Moment"}, + {"sem", stats.SEM[[]float64], stats.SEM[*nd.Array], "SEM"}, + {"variation", stats.Variation[[]float64], stats.Variation[*nd.Array], "Variation"}, + {"iqr", stats.IQR[[]float64], stats.IQR[*nd.Array], "IQR"}, + {"median_abs_deviation", stats.MedianAbsDeviation[[]float64], stats.MedianAbsDeviation[*nd.Array], "MedianAbsDeviation"}, + {"quantile", stats.Quantile[[]float64], stats.Quantile[*nd.Array], "Quantile"}, + {"quantiles", stats.Quantiles[[]float64], stats.Quantiles[*nd.Array], "Quantiles"}, + {"percentile", stats.Percentile[[]float64], stats.Percentile[*nd.Array], "Percentile"}, + {"percentiles", stats.Percentiles[[]float64], stats.Percentiles[*nd.Array], "Percentiles"}, + {"tmean", stats.TMean[[]float64], stats.TMean[*nd.Array], "TMean"}, + {"tvar", stats.TVar[[]float64], stats.TVar[*nd.Array], "TVar"}, + {"tstd", stats.TStd[[]float64], stats.TStd[*nd.Array], "TStd"}, + {"tsem", stats.TSem[[]float64], stats.TSem[*nd.Array], "TSem"}, + {"tmin", stats.TMin[[]float64], stats.TMin[*nd.Array], "TMin"}, + {"tmax", stats.TMax[[]float64], stats.TMax[*nd.Array], "TMax"}, + {"trim_mean", stats.TrimMean[[]float64], stats.TrimMean[*nd.Array], "TrimMean"}, + {"trimboth", stats.TrimBoth[[]float64], stats.TrimBoth[*nd.Array], "TrimBoth"}, + {"trim1", stats.Trim1[[]float64], stats.Trim1[*nd.Array], "Trim1"}, + {"zscore", stats.ZScore[[]float64], stats.ZScore[*nd.Array], "ZScore"}, + {"rankdata", stats.RankData[[]float64], stats.RankData[*nd.Array], "RankData"}, + {"describe", stats.Describe[[]float64], stats.Describe[*nd.Array], "Describe"}, + {"mode", stats.Mode[[]float64], stats.Mode[*nd.Array], "Mode"}, + {"entropy", stats.Entropy[[]float64], stats.Entropy[*nd.Array], "Entropy"}, + {"differential_entropy", stats.DifferentialEntropy[[]float64], stats.DifferentialEntropy[*nd.Array], "DifferentialEntropy"}, + {"histogram", stats.Histogram[[]float64], stats.Histogram[*nd.Array], "Histogram"}, + {"histogram_bin_edges", stats.HistogramBinEdges[[]float64], stats.HistogramBinEdges[*nd.Array], "HistogramBinEdges"}, + {"cumfreq", stats.CumFreq[[]float64], stats.CumFreq[*nd.Array], "CumFreq"}, + {"relfreq", stats.RelFreq[[]float64], stats.RelFreq[*nd.Array], "RelFreq"}, + {"zmap", stats.ZMap[[]float64, []float64], stats.ZMap[*nd.Array, *nd.Array], "ZMap"}, + {"binned_statistic", stats.BinnedStatistic[[]float64, []float64], stats.BinnedStatistic[*nd.Array, *nd.Array], "BinnedStatistic"}, + // Tests. + {"ttest_1samp", stats.TTest1Samp[[]float64], stats.TTest1Samp[*nd.Array], "TTest1Samp"}, + {"ttest_ind", stats.TTestInd[[]float64], stats.TTestInd[*nd.Array], "TTestInd"}, + {"ttest_rel", stats.TTestRel[[]float64], stats.TTestRel[*nd.Array], "TTestRel"}, + {"mannwhitneyu", stats.MannWhitneyU[[]float64], stats.MannWhitneyU[*nd.Array], "MannWhitneyU"}, + {"wilcoxon", stats.Wilcoxon[[]float64], stats.Wilcoxon[*nd.Array], "Wilcoxon"}, + {"ks_1samp", stats.KSTest1Samp[[]float64], stats.KSTest1Samp[*nd.Array], "KSTest1Samp"}, + {"ks_2samp", stats.KSTest2Samp[[]float64], stats.KSTest2Samp[*nd.Array], "KSTest2Samp"}, + {"shapiro", stats.ShapiroWilk[[]float64], stats.ShapiroWilk[*nd.Array], "ShapiroWilk"}, + {"normaltest", stats.Normaltest[[]float64], stats.Normaltest[*nd.Array], "Normaltest"}, + {"skewtest", stats.SkewTest[[]float64], stats.SkewTest[*nd.Array], "SkewTest"}, + {"kurtosistest", stats.KurtosisTest[[]float64], stats.KurtosisTest[*nd.Array], "KurtosisTest"}, + {"jarque_bera", stats.JarqueBera[[]float64], stats.JarqueBera[*nd.Array], "JarqueBera"}, + {"anderson", stats.AndersonDarling[[]float64], stats.AndersonDarling[*nd.Array], "AndersonDarling"}, + {"pearsonr", stats.Pearsonr[[]float64], stats.Pearsonr[*nd.Array], "Pearsonr"}, + {"spearmanr", stats.Spearmanr[[]float64], stats.Spearmanr[*nd.Array], "Spearmanr"}, + {"kendalltau", stats.Kendalltau[[]float64], stats.Kendalltau[*nd.Array], "Kendalltau"}, + {"linregress", stats.Linregress[[]float64], stats.Linregress[*nd.Array], "Linregress"}, + {"chisquare", stats.ChiSquare[[]float64], stats.ChiSquare[*nd.Array], "ChiSquare"}, + {"power_divergence", stats.PowerDivergence[[]float64], stats.PowerDivergence[*nd.Array], "PowerDivergence"}, + {"f_oneway", stats.FOneway[[]float64], stats.FOneway[*nd.Array], "FOneway"}, + {"kruskal", stats.KruskalWallis[[]float64], stats.KruskalWallis[*nd.Array], "KruskalWallis"}, + {"levene", stats.Levene[[]float64], stats.Levene[*nd.Array], "Levene"}, + {"bartlett", stats.Bartlett[[]float64], stats.Bartlett[*nd.Array], "Bartlett"}, +} + +// statsPlain are the functions of Stats that are not generic over the data. +var statsPlain = []struct { + name, goName string + fn any +}{ + {"chi2_contingency", "Chi2Contingency", stats.Chi2Contingency}, + {"fisher_exact", "FisherExact", stats.FisherExact}, + {"fit_multivariate_normal", "FitMultivariateNormal", stats.FitMultivariateNormal}, +} + +// statsResults are the result types, each a class of Stats with a reader per +// field. +var statsResults = []struct { + name string + sample any +}{ + {"TestResult", stats.TestResult{}}, + {"TTestResult", stats.TTestResult{}}, + {"RankTestResult", stats.RankTestResult{}}, + {"KSResult", stats.KSResult{}}, + {"PearsonResult", stats.PearsonResult{}}, + {"LinregressResult", stats.LinregressResult{}}, + {"AndersonResult", stats.AndersonResult{}}, + {"Chi2ContingencyResult", stats.Chi2ContingencyResult{}}, + {"DescribeResult", stats.DescribeResult{}}, + {"ModeResult", stats.ModeResult{}}, + {"HistogramResult", stats.HistogramResult{}}, + {"FreqResult", stats.FreqResult{}}, + {"BinnedResult", stats.BinnedResult{}}, + {"ConfidenceInterval", stats.ConfidenceInterval{}}, + {"BootstrapResult", stats.BootstrapResult{}}, + {"CDFEstimate", stats.CDFEstimate{}}, +} + +// dist is a distribution of Stats: its Ruby class name, its zero value, its +// fit (the two instantiations of FitXxx; nil when go-statistics has none) +// and whether it is discrete. +type dist struct { + name string + zero any + fitS any + fitA any + discrete bool +} + +var distributions = []dist{ + {"Beta", stats.Beta{}, stats.FitBeta[[]float64], stats.FitBeta[*nd.Array], false}, + {"Cauchy", stats.Cauchy{}, stats.FitCauchy[[]float64], stats.FitCauchy[*nd.Array], false}, + {"Chi", stats.Chi{}, stats.FitChi[[]float64], stats.FitChi[*nd.Array], false}, + {"ChiSquared", stats.ChiSquared{}, stats.FitChiSquared[[]float64], stats.FitChiSquared[*nd.Array], false}, + {"Exponential", stats.Exponential{}, stats.FitExponential[[]float64], stats.FitExponential[*nd.Array], false}, + {"F", stats.F{}, stats.FitF[[]float64], stats.FitF[*nd.Array], false}, + {"Gamma", stats.Gamma{}, stats.FitGamma[[]float64], stats.FitGamma[*nd.Array], false}, + {"Gumbel", stats.Gumbel{}, stats.FitGumbel[[]float64], stats.FitGumbel[*nd.Array], false}, + {"HalfNormal", stats.HalfNormal{}, stats.FitHalfNormal[[]float64], stats.FitHalfNormal[*nd.Array], false}, + {"InverseGamma", stats.InverseGamma{}, stats.FitInverseGamma[[]float64], stats.FitInverseGamma[*nd.Array], false}, + {"Laplace", stats.Laplace{}, stats.FitLaplace[[]float64], stats.FitLaplace[*nd.Array], false}, + {"LogNormal", stats.LogNormal{}, stats.FitLogNormal[[]float64], stats.FitLogNormal[*nd.Array], false}, + {"Logistic", stats.Logistic{}, stats.FitLogistic[[]float64], stats.FitLogistic[*nd.Array], false}, + {"Lomax", stats.Lomax{}, stats.FitLomax[[]float64], stats.FitLomax[*nd.Array], false}, + {"NoncentralChiSquared", stats.NoncentralChiSquared{}, nil, nil, false}, + {"Normal", stats.Normal{}, stats.FitNormal[[]float64], stats.FitNormal[*nd.Array], false}, + {"Pareto", stats.Pareto{}, stats.FitPareto[[]float64], stats.FitPareto[*nd.Array], false}, + {"Rayleigh", stats.Rayleigh{}, stats.FitRayleigh[[]float64], stats.FitRayleigh[*nd.Array], false}, + {"Reciprocal", stats.Reciprocal{}, stats.FitReciprocal[[]float64], stats.FitReciprocal[*nd.Array], false}, + {"StudentT", stats.StudentT{}, stats.FitStudentT[[]float64], stats.FitStudentT[*nd.Array], false}, + {"Triangular", stats.Triangular{}, stats.FitTriangular[[]float64], stats.FitTriangular[*nd.Array], false}, + {"Uniform", stats.Uniform{}, stats.FitUniform[[]float64], stats.FitUniform[*nd.Array], false}, + {"VonMises", stats.VonMises{}, stats.FitVonMises[[]float64], stats.FitVonMises[*nd.Array], false}, + {"Weibull", stats.Weibull{}, stats.FitWeibull[[]float64], stats.FitWeibull[*nd.Array], false}, + {"Bernoulli", stats.Bernoulli{}, nil, nil, true}, + {"Binomial", stats.Binomial{}, nil, nil, true}, + {"DiscreteUniform", stats.DiscreteUniform{}, nil, nil, true}, + {"Geometric", stats.Geometric{}, nil, nil, true}, + {"Hypergeometric", stats.Hypergeometric{}, nil, nil, true}, + {"NegativeBinomial", stats.NegativeBinomial{}, nil, nil, true}, + {"Poisson", stats.Poisson{}, nil, nil, true}, + {"Zipf", stats.Zipf{}, nil, nil, true}, +} + +// elementwise are the methods of a distribution that apply to each element +// of an NDArray or an Array, as SciPy's do. +var elementwise = map[string]bool{ + "PDF": true, "LogPDF": true, "PMF": true, "LogPMF": true, "CDF": true, "LogCDF": true, + "SF": true, "LogSF": true, "PPF": true, "ISF": true, +} + +func registerStats(in *rb.Interp) error { + d := &definer{in: in} + m := d.module("Stats") + if d.err != nil { + return d.err + } + for _, r := range statsResults { + d.structClass(m, r.name, r.sample) + } + for _, f := range statsFuncs { + d.fn(m, f.name, dataFunc(f.overS, f.overA)) + } + for _, f := range statsPlain { + fn := reflect.ValueOf(f.fn) + d.fn(m, f.name, func(c *rb.Call) (any, error) { return invoke(fn, c, c.Args) }) + } + for _, ds := range distributions { + defineDist(d, m, ds) + } + defineRNG(d, m) + defineKDE(d, m) + defineMultivariate(d, m) + d.fn(m, "bootstrap", bootstrap) + return d.err +} + +// defineDist defines a distribution class: new from its parameters +// (positional in the Go struct's field order, or by keyword; loc defaults to +// 0 and scale to 1), fit, the elementwise methods, rvs, and every other method +// of the Go type. +func defineDist(d *definer, m *rb.Module, ds dist) { + c := d.structClass(m, ds.name, ds.zero) + t := reflect.TypeOf(ds.zero) + fields := readableFields(t) + c.DefineSingletonMethod("new", func(call *rb.Call) (any, error) { + v, err := newDist(t, fields, call.Args, call.Kwargs) + if err != nil { + return nil, err + } + return v.Interface(), nil + }) + for name := range elementwise { + if _, ok := reflect.PointerTo(t).MethodByName(name); !ok { + continue + } + c.DefineMethod(snakeName(name), func(call *rb.Call) (any, error) { + if err := arity(call, 1, 1); err != nil { + return nil, err + } + f := reflect.ValueOf(call.Self.Go()).MethodByName(name).Interface().(func(float64) float64) + return ufunc(call.Args[0], f) + }) + } + if ds.fitS != nil { + fitS, fitA := reflect.ValueOf(ds.fitS), reflect.ValueOf(ds.fitA) + c.DefineSingletonMethod("fit", func(call *rb.Call) (any, error) { + if err := arity(call, 1, 1); err != nil { + return nil, err + } + o, err := fitOptions(fields, call.Kwargs) + if err != nil { + return nil, err + } + x, err := data(call.Args[0].Go()) + if err != nil { + return nil, err + } + fit := fitS + if _, ok := x.(*nd.Array); ok { + fit = fitA + } + return results(fit.Call([]reflect.Value{reflect.ValueOf(x), reflect.ValueOf(o)})) + }) + } + skip := map[string]bool{"Validate": true} + for name := range elementwise { + skip[name] = true + } + _ = bindMethods(c, reflect.PointerTo(t), skip) +} + +// newDist builds a distribution value from Ruby arguments and validates it, +// which is what go-statistics' NewXxx does. +func newDist(t reflect.Type, fields []reflect.StructField, args []rb.Value, kw map[string]rb.Value) (reflect.Value, error) { + if len(args) > len(fields) { + return reflect.Value{}, argError("wrong number of arguments (given %d, expected 0..%d)", len(args), len(fields)) + } + v := reflect.New(t).Elem() + set := map[string]bool{} + assign := func(f reflect.StructField, x rb.Value) error { + if set[f.Name] { + return argError("%s given twice", snakeName(f.Name)) + } + fv, err := convert(x.Go(), f.Type) + if err != nil { + return err + } + v.FieldByIndex(f.Index).Set(fv) + set[f.Name] = true + return nil + } + for i, a := range args { + if err := assign(fields[i], a); err != nil { + return reflect.Value{}, err + } + } + byName := fieldsByName(t) + for _, k := range sortedKeys(kw) { + f, ok := byName[normalize(k)] + if !ok { + return reflect.Value{}, argError("unknown keyword: :%s (known: %s)", k, joinNames(fields)) + } + if err := assign(f, kw[k]); err != nil { + return reflect.Value{}, err + } + } + for _, f := range fields { + if set[f.Name] { + continue + } + switch f.Name { + case "Loc": + case "Scale": + v.FieldByIndex(f.Index).SetFloat(1) + default: + return reflect.Value{}, argError("missing keyword: :%s", snakeName(f.Name)) + } + } + if err := v.Interface().(interface{ Validate() error }).Validate(); err != nil { + return reflect.Value{}, goError(err) + } + return v, nil +} + +func joinNames(fields []reflect.StructField) string { + s := "" + for i, f := range fields { + if i > 0 { + s += ", " + } + s += snakeName(f.Name) + } + return s +} + +// fitOptions reads fit's keyword: fixed: {loc: 0} holds the named +// parameters at the given values (SciPy's floc=0), FitOptions.Fixed by Go +// field name. +func fitOptions(fields []reflect.StructField, kw map[string]rb.Value) (*stats.FitOptions, error) { + for _, k := range sortedKeys(kw) { + if k != "fixed" { + return nil, argError("unknown keyword: :%s (known: fixed)", k) + } + } + fixed, ok := kw["fixed"] + if !ok { + return nil, nil + } + h, isHash := fixed.Go().(map[any]any) + if !isHash { + return nil, typeError(fixed.Go(), "Hash") + } + byName := map[string]string{} + for _, f := range fields { + byName[normalize(f.Name)] = f.Name + } + o := &stats.FitOptions{Fixed: map[string]float64{}} + for k, v := range h { + name, err := toString(k) + if err != nil { + return nil, err + } + goName, ok := byName[normalize(name)] + if !ok { + return nil, argError("unknown parameter %s (known: %s)", name, joinNames(fields)) + } + if o.Fixed[goName], err = toFloat(v); err != nil { + return nil, err + } + } + return o, nil +} + +// defineRNG defines Stats::RNG, numpy.random.default_rng: the generator +// random_state takes, so a sequence of draws can be reproduced. +func defineRNG(d *definer, m *rb.Module) { + c := d.class(m, "RNG") + if c == nil { + return + } + c.Bind(&random.Generator{}) + c.DefineSingletonMethod("new", func(call *rb.Call) (any, error) { + if err := arity(call, 0, 1); err != nil { + return nil, err + } + if len(call.Args) == 0 { + return random.DefaultRNG(), nil + } + seed, err := toInt(call.Args[0].Go()) + if err != nil || seed < 0 { + return nil, argError("seed must be a non-negative Integer") + } + return random.DefaultRNG(uint64(seed)), nil + }) + for _, name := range []string{"Random", "StandardNormal", "Normal", "Uniform", "Integers", "Exponential", "StandardExponential", "Poisson", "Binomial", "Gamma", "Beta"} { + mt, ok := reflect.TypeFor[*random.Generator]().MethodByName(name) + if !ok { + panic(fmt.Sprintf("random.Generator has no %s", name)) + } + c.DefineMethod(snakeName(name), func(call *rb.Call) (any, error) { + return invoke(reflect.ValueOf(call.Self.Go()).Method(mt.Index), call, call.Args) + }) + } + c.DefineMethod("inspect", func(*rb.Call) (any, error) { return "#", nil }) +} + +// defineKDE defines Stats::GaussianKDE. +func defineKDE(d *definer, m *rb.Module) { + c := d.class(m, "GaussianKDE") + if c == nil { + return + } + c.Bind(&stats.GaussianKDE{}) + newKDE := dataFunc(stats.NewGaussianKDE[[]float64], stats.NewGaussianKDE[*nd.Array]) + c.DefineSingletonMethod("new", newKDE) + bindMethods(c, reflect.TypeFor[*stats.GaussianKDE](), map[string]bool{"Evaluate": true, "EvaluateSlice": true}) + // evaluate (and call) take points as an Array or an NDArray; SciPy's + // kde(points). + eval := func(call *rb.Call) (any, error) { + k := call.Self.Go().(*stats.GaussianKDE) + if err := arity(call, 1, 1); err != nil { + return nil, err + } + x, err := ndArray(call.Args[0].Go()) + if err != nil { + return nil, err + } + if k.D() == 1 { + r, err := k.EvaluateSlice(arrayFloats(x)) + return resultOrError(vec(r), err) + } + r, err := k.Evaluate(x) + return resultOrError(vec(r), err) + } + c.DefineMethod("evaluate", eval) + c.DefineMethod("call", eval) + c.DefineMethod("inspect", func(call *rb.Call) (any, error) { + k := call.Self.Go().(*stats.GaussianKDE) + return fmt.Sprintf("#", k.D(), k.Dataset().Shape()[len(k.Dataset().Shape())-1]), nil + }) +} + +func resultOrError(v any, err error) (any, error) { + if err != nil { + return nil, goError(err) + } + return v, nil +} + +// defineMultivariate defines Stats::MultivariateNormal, Stats::Dirichlet and +// Stats::Multinomial: new is the Go constructor, and the methods are the Go +// methods. +func defineMultivariate(d *definer, m *rb.Module) { + for _, mv := range []struct { + name string + ctor any + zero any + }{ + {"MultivariateNormal", stats.NewMultivariateNormal, &stats.MultivariateNormal{}}, + {"Dirichlet", stats.NewDirichlet, &stats.Dirichlet{}}, + {"Multinomial", stats.NewMultinomial, &stats.Multinomial{}}, + } { + c := d.class(m, mv.name) + if c == nil { + return + } + c.Bind(mv.zero) + ctor := reflect.ValueOf(mv.ctor) + c.DefineSingletonMethod("new", func(call *rb.Call) (any, error) { return invoke(ctor, call, call.Args) }) + bindMethods(c, reflect.TypeOf(mv.zero), nil) + name := mv.name + c.DefineMethod("inspect", func(*rb.Call) (any, error) { return "#", nil }) + } +} + +// bootstrap is Stats.bootstrap(samples, **options) { |*samples| statistic }: +// scipy.stats.bootstrap. The block is the statistic; it is called with one +// NDArray per sample and returns a Float. +func bootstrap(c *rb.Call) (any, error) { + if err := arity(c, 1, 1); err != nil { + return nil, err + } + b, err := needBlock(c) + if err != nil { + return nil, err + } + list, ok := c.Args[0].Go().([]any) + if !ok { + return nil, typeError(c.Args[0].Go(), "an Array of samples") + } + samples := make([][]float64, len(list)) + for i, s := range list { + if samples[i], err = toFloats(s); err != nil { + return nil, err + } + } + kw, rest := split(c.Kwargs, "random_state") + g, err := generator(kw["random_state"]) + if err != nil { + return nil, err + } + var opts *stats.BootstrapOptions + if len(rest) > 0 { + opts = &stats.BootstrapOptions{} + if err := options(opts, rest); err != nil { + return nil, err + } + } + var blockErr error + statistic := func(s ...[]float64) float64 { + if blockErr != nil { + return 0 + } + args := make([]any, len(s)) + for i, x := range s { + args[i] = vec(x) + } + r, err := b.Call(args...) + if err == nil { + var f float64 + if f, err = toFloat(r.Go()); err == nil { + return f + } + } + blockErr = err + return 0 + } + res, err := stats.Bootstrap(samples, statistic, g, opts) + if blockErr != nil { + return nil, blockErr + } + return resultOrError(res, err) +} From 4ffdbd4819e9e263983c0123775b2e8acbf3ed4d Mon Sep 17 00:00:00 2001 From: tannevaled Date: Sat, 17 Oct 2026 22:21:07 +0200 Subject: [PATCH 02/20] WIP: Special, Optimize, Integrate; generator also lists the special functions --- callbacks.go | 151 ++++++++ enums.go | 8 +- enums_gen.go | 2 +- integrate.go | 357 ++++++++++++++++++- internal/{genenums => gen}/main.go | 115 ++++-- optimize.go | 545 ++++++++++++++++++++++++++++- smoke_test.go | 28 ++ special.go | 159 ++++++++- special_gen.go | 76 ++++ 9 files changed, 1411 insertions(+), 30 deletions(-) create mode 100644 callbacks.go rename internal/{genenums => gen}/main.go (63%) create mode 100644 special_gen.go diff --git a/callbacks.go b/callbacks.go new file mode 100644 index 0000000..71bf7c1 --- /dev/null +++ b/callbacks.go @@ -0,0 +1,151 @@ +package ruby + +import ( + "math" + "reflect" + + rb "github.com/go-embedded-ruby/ruby" +) + +// This file turns Ruby blocks and Procs into the Go callbacks the numerical +// libraries take: objectives, residual vectors, models, right-hand sides. + +// caller calls a block or a Proc with Go arguments. +type caller func(args ...any) (rb.Value, error) + +// blockCaller calls the block. +func blockCaller(b *rb.Block) caller { return b.Call } + +// procCaller calls a Proc, a Method or anything else answering #call: a jac: +// or fprime: keyword, say. +func procCaller(v rb.Value) (caller, error) { + ok, err := v.Call("respond_to?", rb.Symbol("call")) + if err != nil || ok.Go() != true { + return nil, typeError(v.Go(), "a Proc") + } + return func(args ...any) (rb.Value, error) { return v.Call("call", args...) }, nil +} + +// guard keeps the first error a callback met. A Go algorithm cannot be +// stopped from inside its callback, so after an error the callback answers +// NaN (or leaves its output alone) without calling Ruby again, the algorithm +// winds down, and the binding returns the error instead of its result. +type guard struct{ err error } + +func (g *guard) fail(err error) { + if g.err == nil { + g.err = err + } +} + +// makeFunc builds a Go func of type t that calls Ruby. Its float64 +// parameters reach Ruby as Floats and its []float64 parameters as NDArrays +// (copies: the Go side reuses its buffers). A func returning a float64 returns +// what Ruby returns; a func returning nothing writes what Ruby returns — an +// Array or an NDArray — into its last []float64 parameter. With splat, a +// trailing []float64 input is passed as separate Floats (curve_fit's +// |x, a, b|). +func (g *guard) makeFunc(t reflect.Type, call caller, splat bool) reflect.Value { + outBuf := t.NumOut() == 0 + return reflect.MakeFunc(t, func(in []reflect.Value) []reflect.Value { + inputs := in + if outBuf { + inputs = in[:len(in)-1] + } + var args []any + for i, v := range inputs { + switch x := v.Interface().(type) { + case float64: + args = append(args, x) + case []float64: + if splat && i == len(inputs)-1 { + for _, e := range x { + args = append(args, e) + } + } else { + args = append(args, vec(x)) + } + } + } + if outBuf { + out := in[len(in)-1].Interface().([]float64) + if g.err == nil { + g.fill(out, call, args) + } + return nil + } + f := math.NaN() + if g.err == nil { + f = g.scalar(call, args) + } + return []reflect.Value{reflect.ValueOf(f)} + }) +} + +func (g *guard) scalar(call caller, args []any) float64 { + r, err := call(args...) + if err != nil { + g.fail(err) + return math.NaN() + } + f, err := toFloat(r.Go()) + if err != nil { + g.fail(err) + return math.NaN() + } + return f +} + +func (g *guard) fill(out []float64, call caller, args []any) { + r, err := call(args...) + if err != nil { + g.fail(err) + return + } + v, err := toFloats(r.Go()) + if err != nil { + g.fail(err) + return + } + if len(v) != len(out) { + g.fail(argError("the block returned %d values, expected %d", len(v), len(out))) + return + } + copy(out, v) +} + +// fn is makeFunc for a func type known at compile time. +func fn[F any](g *guard, call caller) F { + return g.makeFunc(reflect.TypeFor[F](), call, false).Interface().(F) +} + +// optionalProc reads a keyword that may hold a Proc, building the callback of +// type F from it; ok is false when the keyword is absent. +func optionalProc[F any](g *guard, kw map[string]rb.Value, name string) (f F, ok bool, err error) { + v, present := kw[name] + if !present { + return f, false, nil + } + call, err := procCaller(v) + if err != nil { + return f, false, err + } + return fn[F](g, call), true, nil +} + +// numberOrProc reads a limit of integration that may be a number or a Proc +// of the outer variables: dblquad's gfun and hfun, tplquad's qfun and rfun. +func numberOrProc[F any](g *guard, v rb.Value) (F, error) { + if c, err := toFloat(v.Go()); err == nil { + t := reflect.TypeFor[F]() + return reflect.MakeFunc(t, func([]reflect.Value) []reflect.Value { + return []reflect.Value{reflect.ValueOf(c)} + }).Interface().(F), nil + } + call, err := procCaller(v) + if err != nil { + var zero F + return zero, typeError(v.Go(), "a Float or a Proc") + } + return fn[F](g, call), nil +} diff --git a/enums.go b/enums.go index d69787a..49aa39c 100644 --- a/enums.go +++ b/enums.go @@ -1,9 +1,15 @@ package ruby -//go:generate go run ./internal/genenums +//go:generate go run ./internal/gen // enumValue is one value of an enumerated Go type and its Ruby symbol. type enumValue struct { name string value any } + +// namedFunc is a Go function and its Go name. +type namedFunc struct { + name string + fn any +} diff --git a/enums_gen.go b/enums_gen.go index 68e66aa..4f4ed9b 100644 --- a/enums_gen.go +++ b/enums_gen.go @@ -1,4 +1,4 @@ -// Code generated by internal/genenums; DO NOT EDIT. +// Code generated by internal/gen; DO NOT EDIT. package ruby diff --git a/integrate.go b/integrate.go index 887e61f..13b3721 100644 --- a/integrate.go +++ b/integrate.go @@ -1,5 +1,358 @@ package ruby -import rb "github.com/go-embedded-ruby/ruby" +import ( + "reflect" -func registerIntegrate(in *rb.Interp) error { return nil } + rb "github.com/go-embedded-ruby/ruby" + nd "github.com/go-ndarray/ndarray" + "github.com/go-numerics/integrate" +) + +// registerIntegrate defines Integrate, go-numerics/integrate with the +// integrand or the right-hand side as the block: +// +// Integrate.quad(a, b, **options) { |x| f(x) } # => QuadResult (value, abs_err, …) +// Integrate.dblquad(a, b, g, h) { |y, x| f(y, x) } # g, h: Floats or Procs of x +// Integrate.tplquad(a, b, g, h, q, r) { |z, y, x| ... } # q, r: Floats or Procs of (x, y) +// Integrate.nquad([[a, b], [c, d]]) { |x| f(x) } # x an NDArray +// Integrate.fixed_quad(a, b, n = 5) { |x| f(x) } +// Integrate.romberg(a, b, **options) { |x| f(x) } +// Integrate.solve_ivp([t0, t1], y0, **options) { |t, y| dydt } # => Solution +// Integrate.odeint(y0, t, **options) { |t, y| dydt } # => [NDArray, OdeintInfo] +// Integrate.trapezoid(y, x: nil, dx: 1.0, axis: -1) # also simpson +// Integrate.cumulative_trapezoid(y, x: nil, dx: 1.0, initial: false, axis: -1) +// Integrate.romb(y, dx: 1.0) +// Integrate.gauss_legendre(n) # => [nodes, weights] +// +// The right-hand side gets t as a Float and y as an NDArray, as in the Go +// API (and SciPy's solve_ivp; odeint takes the same order, SciPy's +// tfirst=True). The keywords are the fields of the Go option struct. +func registerIntegrate(in *rb.Interp) error { + d := &definer{in: in} + m := d.module("Integrate") + d.structClass(m, "QuadResult", integrate.QuadResult{}) + d.structClass(m, "Solution", integrate.Solution{}) + d.structClass(m, "OdeintInfo", integrate.OdeintInfo{}) + odeSol := d.class(m, "ODESolution") + if odeSol != nil { + odeSol.Bind(&integrate.ODESolution{}) + bindMethods(odeSol, reflect.TypeFor[*integrate.ODESolution](), map[string]bool{"EvalInto": true}) + } + d.fn(m, "quad", quad) + d.fn(m, "dblquad", dblquad) + d.fn(m, "tplquad", tplquad) + d.fn(m, "nquad", nquad) + d.fn(m, "fixed_quad", fixedQuad) + d.fn(m, "romberg", romberg) + d.fn(m, "solve_ivp", solveIVP) + d.fn(m, "odeint", odeint) + d.fn(m, "trapezoid", sampled(integrate.Trapezoid, integrate.TrapezoidND)) + d.fn(m, "simpson", sampled(integrate.Simpson, integrate.SimpsonND)) + d.fn(m, "cumulative_trapezoid", cumulativeTrapezoid) + d.fn(m, "romb", romb) + d.fn(m, "gauss_legendre", func(c *rb.Call) (any, error) { + if err := arity(c, 1, 1); err != nil { + return nil, err + } + n, err := toInt(c.Args[0].Go()) + if err != nil { + return nil, err + } + x, w := integrate.GaussLegendre(int(n)) + return []any{vec(x), vec(w)}, nil + }) + return d.err +} + +func init() { + converters[reflect.TypeFor[integrate.Range]()] = func(v any) (reflect.Value, error) { + b, err := toFloats(v) + if err != nil || len(b) != 2 { + return reflect.Value{}, typeError(v, "a range [lo, hi]") + } + return reflect.ValueOf(integrate.ConstRange(b[0], b[1])), nil + } +} + +func quad(c *rb.Call) (any, error) { + call, err := blockAndArgs(c, 2) + if err != nil { + return nil, err + } + ab, err := floatArgs(c) + if err != nil { + return nil, err + } + o, err := optsFrom[integrate.QuadOptions](c.Kwargs) + if err != nil { + return nil, err + } + g := &guard{} + res, err := integrate.Quad(fn[func(float64) float64](g, call), ab[0], ab[1], o) + return finish(g, res, err) +} + +func dblquad(c *rb.Call) (any, error) { + call, err := blockAndArgs(c, 4) + if err != nil { + return nil, err + } + ab, err := floatArgs(&rb.Call{Args: c.Args[:2]}) + if err != nil { + return nil, err + } + g := &guard{} + lo, err := numberOrProc[func(float64) float64](g, c.Args[2]) + if err != nil { + return nil, err + } + hi, err := numberOrProc[func(float64) float64](g, c.Args[3]) + if err != nil { + return nil, err + } + o, err := optsFrom[integrate.QuadOptions](c.Kwargs) + if err != nil { + return nil, err + } + res, err := integrate.DblQuad(fn[func(y, x float64) float64](g, call), ab[0], ab[1], lo, hi, o) + return finish(g, res, err) +} + +func tplquad(c *rb.Call) (any, error) { + call, err := blockAndArgs(c, 6) + if err != nil { + return nil, err + } + ab, err := floatArgs(&rb.Call{Args: c.Args[:2]}) + if err != nil { + return nil, err + } + g := &guard{} + var lims [2]func(float64) float64 + for i := range lims { + if lims[i], err = numberOrProc[func(float64) float64](g, c.Args[2+i]); err != nil { + return nil, err + } + } + var inner [2]func(x, y float64) float64 + for i := range inner { + if inner[i], err = numberOrProc[func(x, y float64) float64](g, c.Args[4+i]); err != nil { + return nil, err + } + } + o, err := optsFrom[integrate.QuadOptions](c.Kwargs) + if err != nil { + return nil, err + } + res, err := integrate.TplQuad(fn[func(z, y, x float64) float64](g, call), ab[0], ab[1], lims[0], lims[1], inner[0], inner[1], o) + return finish(g, res, err) +} + +func nquad(c *rb.Call) (any, error) { + call, err := blockAndArgs(c, 1) + if err != nil { + return nil, err + } + rv, err := convert(c.Args[0].Go(), reflect.TypeFor[[]integrate.Range]()) + if err != nil { + return nil, err + } + var o *integrate.NQuadOptions + if len(c.Kwargs) > 0 { + q, err := optsFrom[integrate.QuadOptions](c.Kwargs) + if err != nil { + return nil, err + } + o = &integrate.NQuadOptions{Opts: []*integrate.QuadOptions{q}} + } + g := &guard{} + res, err := integrate.NQuad(fn[func([]float64) float64](g, call), rv.Interface().([]integrate.Range), o) + return finish(g, res, err) +} + +func fixedQuad(c *rb.Call) (any, error) { + if err := arity(c, 2, 3); err != nil { + return nil, err + } + b, err := needBlock(c) + if err != nil { + return nil, err + } + ab, err := floatArgs(&rb.Call{Args: c.Args[:2]}) + if err != nil { + return nil, err + } + n := int64(5) + if len(c.Args) == 3 { + if n, err = toInt(c.Args[2].Go()); err != nil { + return nil, err + } + } + g := &guard{} + v, err := integrate.FixedQuad(fn[func(float64) float64](g, blockCaller(b)), ab[0], ab[1], int(n)) + return finish(g, v, err) +} + +func romberg(c *rb.Call) (any, error) { + call, err := blockAndArgs(c, 2) + if err != nil { + return nil, err + } + ab, err := floatArgs(c) + if err != nil { + return nil, err + } + o, err := optsFrom[integrate.RombergOptions](c.Kwargs) + if err != nil { + return nil, err + } + g := &guard{} + v, err := integrate.Romberg(fn[func(float64) float64](g, call), ab[0], ab[1], o) + return finish(g, v, err) +} + +// odeFunc is the block as a right-hand side: |t, y| -> dy/dt. +func odeFunc(g *guard, call caller) integrate.ODEFunc { + f := fn[func(t float64, y, dydt []float64)](g, call) + return integrate.ODEFunc(f) +} + +func solveIVP(c *rb.Call) (any, error) { + call, err := blockAndArgs(c, 2) + if err != nil { + return nil, err + } + span, err := convert(c.Args[0].Go(), reflect.TypeFor[[2]float64]()) + if err != nil { + return nil, err + } + y0, err := toFloats(c.Args[1].Go()) + if err != nil { + return nil, err + } + o, err := optsFrom[integrate.IVPOptions](c.Kwargs) + if err != nil { + return nil, err + } + g := &guard{} + res, err := integrate.SolveIVP(odeFunc(g, call), span.Interface().([2]float64), y0, o) + return finish(g, res, err) +} + +func odeint(c *rb.Call) (any, error) { + call, err := blockAndArgs(c, 2) + if err != nil { + return nil, err + } + y0, err := toFloats(c.Args[0].Go()) + if err != nil { + return nil, err + } + t, err := toFloats(c.Args[1].Go()) + if err != nil { + return nil, err + } + o, err := optsFrom[integrate.OdeintOptions](c.Kwargs) + if err != nil { + return nil, err + } + g := &guard{} + y, info, err := integrate.Odeint(odeFunc(g, call), y0, t, o) + return finish(g, []any{y, info}, err) +} + +// sampledKw reads the keywords of the sample-based rules: x:, dx: (1.0) and +// axis: (-1, for an NDArray of more than one dimension). +func sampledKw(c *rb.Call, extra ...string) (x []float64, dx float64, axis int, mine map[string]rb.Value, err error) { + mine, rest := split(c.Kwargs, append([]string{"x", "dx", "axis"}, extra...)...) + if len(rest) > 0 { + return nil, 0, 0, nil, argError("unknown keyword: :%s (known: x, dx, axis)", sortedKeys(rest)[0]) + } + dx, axis = 1, -1 + if v, ok := mine["x"]; ok { + if x, err = toFloats(v.Go()); err != nil { + return + } + } + if v, ok := mine["dx"]; ok { + if dx, err = toFloat(v.Go()); err != nil { + return + } + } + if v, ok := mine["axis"]; ok { + var a int64 + if a, err = toInt(v.Go()); err != nil { + return + } + axis = int(a) + } + return +} + +// sampled binds a rule over samples, its 1-d and its n-d forms: an NDArray of +// more than one dimension integrates along axis and returns an NDArray. +func sampled(oneD func(y, x []float64, dx float64) (float64, error), nD func(y *nd.Array, x []float64, dx float64, axis int) (*nd.Array, error)) rb.Func { + return func(c *rb.Call) (any, error) { + if err := arity(c, 1, 1); err != nil { + return nil, err + } + x, dx, axis, _, err := sampledKw(c) + if err != nil { + return nil, err + } + if a, ok := c.Args[0].Go().(*nd.Array); ok && a.Ndim() > 1 { + r, err := nD(a, x, dx, axis) + return resultOrError(r, err) + } + y, err := toFloats(c.Args[0].Go()) + if err != nil { + return nil, err + } + r, err := oneD(y, x, dx) + return resultOrError(r, err) + } +} + +func cumulativeTrapezoid(c *rb.Call) (any, error) { + if err := arity(c, 1, 1); err != nil { + return nil, err + } + x, dx, axis, mine, err := sampledKw(c, "initial") + if err != nil { + return nil, err + } + initial := false + if v, ok := mine["initial"]; ok { + b, isBool := v.Go().(bool) + if !isBool { + return nil, typeError(v.Go(), "true or false") + } + initial = b + } + if a, ok := c.Args[0].Go().(*nd.Array); ok && a.Ndim() > 1 { + r, err := integrate.CumulativeTrapezoidND(a, x, dx, axis, initial) + return resultOrError(r, err) + } + y, err := toFloats(c.Args[0].Go()) + if err != nil { + return nil, err + } + r, err := integrate.CumulativeTrapezoid(y, x, dx, initial) + return resultOrError(vec(r), err) +} + +func romb(c *rb.Call) (any, error) { + if err := arity(c, 1, 1); err != nil { + return nil, err + } + _, dx, _, _, err := sampledKw(c) + if err != nil { + return nil, err + } + y, err := toFloats(c.Args[0].Go()) + if err != nil { + return nil, err + } + r, err := integrate.Romb(y, dx) + return resultOrError(r, err) +} diff --git a/internal/genenums/main.go b/internal/gen/main.go similarity index 63% rename from internal/genenums/main.go rename to internal/gen/main.go index 3da4653..f7c07ae 100644 --- a/internal/genenums/main.go +++ b/internal/gen/main.go @@ -1,11 +1,14 @@ -// Command genenums writes enums_gen.go: for every enumerated type of the bound -// Go packages — a named type with a block of constants, such as -// stats.Alternative or optimize.Method — the Ruby symbols that name its values. +// Command gen writes the two generated files of the bindings, the parts that +// reflection cannot find at run time because Go keeps no list of them: // -// Constants carry no reflection information, so this is the one part of the -// bindings that has to be generated: everything else is read from the Go types -// at run time. Run it from the module root with `go run ./internal/genenums`; -// TestEnumsAreGenerated fails when the output is out of date. +// - enums_gen.go: for every enumerated type of the bound Go packages — a +// named type with a block of constants, such as stats.Alternative or +// optimize.Method — the Ruby symbols that name its values; +// - special_gen.go: every exported function of go-numerics/special, by +// name, which the Special module then binds by its signature. +// +// Run it from the module root with `go generate` (or `go run ./internal/gen`); +// TestGeneratedFilesAreCurrent fails when either is out of date. package main import ( @@ -34,18 +37,90 @@ var packages = []struct{ path, name string }{ type constant struct{ name string } func main() { - out := "enums_gen.go" + dir := "." if len(os.Args) > 1 { - out = os.Args[1] + dir = os.Args[1] } - src, err := generate(moduleDir) - if err == nil { - err = os.WriteFile(out, src, 0o644) + if err := writeAll(dir, moduleDir); err != nil { + fmt.Fprintln(os.Stderr, "gen:", err) + os.Exit(1) } +} + +// writeAll writes both files into dir. +func writeAll(dir string, mod func(string) (string, error)) error { + enums, err := generate(mod) if err != nil { - fmt.Fprintln(os.Stderr, "genenums:", err) - os.Exit(1) + return err + } + funcs, err := generateSpecial(mod) + if err != nil { + return err + } + if err := os.WriteFile(filepath.Join(dir, "enums_gen.go"), enums, 0o644); err != nil { + return err } + return os.WriteFile(filepath.Join(dir, "special_gen.go"), funcs, 0o644) +} + +// generateSpecial returns the source of special_gen.go. +func generateSpecial(mod func(string) (string, error)) ([]byte, error) { + d, err := mod("github.com/go-numerics/special") + if err != nil { + return nil, err + } + names, err := funcs(d) + if err != nil { + return nil, err + } + var b bytes.Buffer + b.WriteString("// Code generated by internal/gen; DO NOT EDIT.\n\npackage ruby\n\nimport \"github.com/go-numerics/special\"\n\n") + b.WriteString("// specialFuncs are the exported functions of go-numerics/special.\nvar specialFuncs = []namedFunc{\n") + for _, n := range names { + fmt.Fprintf(&b, "\t{%q, special.%s},\n", n, n) + } + b.WriteString("}\n") + return format.Source(b.Bytes()) +} + +// funcs lists the exported, non-generic top-level functions of the package in +// dir, sorted. +func funcs(dir string) ([]string, error) { + files, err := goFiles(dir) + if err != nil { + return nil, err + } + fset := token.NewFileSet() + var out []string + for _, f := range files { + file, err := parser.ParseFile(fset, f, nil, 0) + if err != nil { + return nil, err + } + for _, d := range file.Decls { + if fd, ok := d.(*ast.FuncDecl); ok && fd.Recv == nil && fd.Name.IsExported() && fd.Type.TypeParams == nil { + out = append(out, fd.Name.Name) + } + } + } + sort.Strings(out) + return out, nil +} + +// goFiles are the non-test Go files of dir, sorted. +func goFiles(dir string) ([]string, error) { + all, _ := filepath.Glob(filepath.Join(dir, "*.go")) + var files []string + for _, f := range all { + if !strings.HasSuffix(f, "_test.go") { + files = append(files, f) + } + } + if len(files) == 0 { + return nil, fmt.Errorf("no Go files in %s", dir) + } + sort.Strings(files) + return files, nil } // moduleDir finds a module's source in the module cache; a seam for tests. @@ -62,7 +137,7 @@ var moduleDir = func(path string) (string, error) { // generate returns the formatted source of enums_gen.go. func generate(dir func(string) (string, error)) ([]byte, error) { var b bytes.Buffer - b.WriteString("// Code generated by internal/genenums; DO NOT EDIT.\n\npackage ruby\n\nimport (\n\t\"reflect\"\n\n") + b.WriteString("// Code generated by internal/gen; DO NOT EDIT.\n\npackage ruby\n\nimport (\n\t\"reflect\"\n\n") for _, p := range packages { fmt.Fprintf(&b, "\t%q\n", p.path) } @@ -104,17 +179,13 @@ func generate(dir func(string) (string, error)) ([]byte, error) { // own inherits the previous one's in its block, as iota constants do. func enums(dir string) (map[string][]constant, error) { fset := token.NewFileSet() - files, err := filepath.Glob(filepath.Join(dir, "*.go")) - if err != nil || len(files) == 0 { - return nil, fmt.Errorf("no Go files in %s", dir) + files, err := goFiles(dir) + if err != nil { + return nil, err } - sort.Strings(files) named := map[string]bool{} var decls []*ast.GenDecl for _, f := range files { - if strings.HasSuffix(f, "_test.go") { - continue - } file, err := parser.ParseFile(fset, f, nil, 0) if err != nil { return nil, err diff --git a/optimize.go b/optimize.go index bdf4649..acd112b 100644 --- a/optimize.go +++ b/optimize.go @@ -1,5 +1,546 @@ package ruby -import rb "github.com/go-embedded-ruby/ruby" +import ( + "math" + "reflect" -func registerOptimize(in *rb.Interp) error { return nil } + rb "github.com/go-embedded-ruby/ruby" + "github.com/go-numerics/optimize" +) + +// registerOptimize defines Optimize, go-numerics/optimize with the objective +// as the block: +// +// Optimize.minimize(x0, method: :bfgs, jac: ->(x) { ... }, **options) { |x| f(x) } +// Optimize.minimize_scalar(method: :brent, bracket: [a, b]) { |x| f(x) } +// Optimize.brentq(a, b) { |x| f(x) } # also bisect, brenth, ridder +// Optimize.newton(x0, fprime: ->(x) { ... }) { |x| f(x) } +// Optimize.root_scalar(method: "brentq", bracket: [a, b]) { |x| f(x) } +// Optimize.root(x0) { |x| [f1(x), f2(x)] } # also fsolve +// Optimize.least_squares(x0, m: 3) { |x| residuals } # also leastsq +// Optimize.curve_fit(xdata, ydata, p0: [1, 1]) { |x, a, b| model } +// Optimize.linprog(c, a_ub: ..., b_ub: ..., bounds: [[0, nil], ...]) +// Optimize.brute([[lo, hi], ...], ns: 20) { |x| f(x) } +// Optimize.fminbound(x1, x2) { |x| f(x) } +// Optimize.bracket(xa, xb) { |x| f(x) } +// Optimize.approx_fprime(x, epsilon) { |x| f(x) } +// +// Vectors reach the block as NDArrays; it returns a Float, or an Array or an +// NDArray for a vector. The keywords are the fields of the Go option struct +// (max_iter:, gtol:, bounds:, …). A bound is [lo, hi], nil for a missing side. +func registerOptimize(in *rb.Interp) error { + d := &definer{in: in} + m := d.module("Optimize") + for _, r := range []struct { + name string + sample any + }{ + {"Result", optimize.Result{}}, + {"ScalarResult", optimize.ScalarResult{}}, + {"RootScalarResult", optimize.RootScalarResult{}}, + {"RootResult", optimize.RootResult{}}, + {"CurveFitResult", optimize.CurveFitResult{}}, + {"LSResult", optimize.LSResult{}}, + {"LeastsqResult", optimize.LeastsqResult{}}, + {"LPResult", optimize.LPResult{}}, + {"LPDual", optimize.LPDual{}}, + {"BruteResult", optimize.BruteResult{}}, + } { + d.structClass(m, r.name, r.sample) + } + d.fn(m, "minimize", minimize) + d.fn(m, "minimize_scalar", minimizeScalar) + for name, f := range map[string]func(func(float64) float64, float64, float64, *optimize.BracketOptions) (*optimize.RootScalarResult, error){ + "brentq": optimize.Brentq, "brenth": optimize.Brenth, "bisect": optimize.Bisect, "ridder": optimize.Ridder, + } { + d.fn(m, name, bracketRoot(f)) + } + d.fn(m, "newton", newton) + d.fn(m, "root_scalar", rootScalar) + d.fn(m, "root", systemRoot(optimize.Root)) + d.fn(m, "fsolve", systemRoot(optimize.Fsolve)) + d.fn(m, "least_squares", leastSquares) + d.fn(m, "leastsq", leastsq) + d.fn(m, "curve_fit", curveFit) + d.fn(m, "linprog", linprog) + d.fn(m, "brute", brute) + d.fn(m, "fminbound", fminbound) + d.fn(m, "bracket", bracketFn) + d.fn(m, "approx_fprime", approxFprime) + return d.err +} + +func init() { + converters[reflect.TypeFor[optimize.Bound]()] = func(v any) (reflect.Value, error) { + b, err := toBound(v) + return reflect.ValueOf(b), err + } + converters[reflect.TypeFor[*optimize.Bound]()] = func(v any) (reflect.Value, error) { + b, err := toBound(v) + return reflect.ValueOf(&b), err + } + converters[reflect.TypeFor[optimize.Range]()] = func(v any) (reflect.Value, error) { + r, err := toRange(v) + return reflect.ValueOf(r), err + } +} + +// toBound reads [lo, hi], nil for a missing side (±Infinity). +func toBound(v any) (optimize.Bound, error) { + pair, ok := v.([]any) + if !ok || len(pair) != 2 { + return optimize.Bound{}, typeError(v, "a bound [lo, hi]") + } + side := func(x any, inf float64) (float64, error) { + if x == nil { + return inf, nil + } + return toFloat(x) + } + lo, err := side(pair[0], math.Inf(-1)) + if err != nil { + return optimize.Bound{}, err + } + hi, err := side(pair[1], math.Inf(1)) + return optimize.Bound{Lo: lo, Hi: hi}, err +} + +// toRange reads a brute-force axis: [lo, hi] (ns points) or [lo, hi, step]. +func toRange(v any) (optimize.Range, error) { + list, ok := v.([]any) + if !ok || len(list) < 2 || len(list) > 3 { + return optimize.Range{}, typeError(v, "a range [lo, hi] or [lo, hi, step]") + } + f, err := toFloats(list) + if err != nil { + return optimize.Range{}, err + } + r := optimize.Range{Lo: f[0], Hi: f[1]} + if len(f) == 3 { + r.Step = f[2] + } + return r, nil +} + +// optsFrom fills an option struct from keywords, nil when there are none. +func optsFrom[O any](kw map[string]rb.Value) (*O, error) { + if len(kw) == 0 { + return nil, nil + } + o := new(O) + if err := options(o, kw); err != nil { + return nil, err + } + return o, nil +} + +// finish returns a result, or the error of the callback first, then the Go +// error. +func finish(g *guard, res any, err error) (any, error) { + if g.err != nil { + return nil, g.err + } + if err != nil { + return nil, goError(err) + } + return res, nil +} + +// blockAndArgs checks the positional count and returns the block's caller. +func blockAndArgs(c *rb.Call, n int) (caller, error) { + if err := arity(c, n, n); err != nil { + return nil, err + } + b, err := needBlock(c) + if err != nil { + return nil, err + } + return blockCaller(b), nil +} + +func floatArgs(c *rb.Call) ([]float64, error) { + out := make([]float64, len(c.Args)) + for i, a := range c.Args { + f, err := toFloat(a.Go()) + if err != nil { + return nil, err + } + out[i] = f + } + return out, nil +} + +// enumKw reads an enumerated keyword, def when it is absent. +func enumKw[E any](kw map[string]rb.Value, name string, def E) (E, error) { + v, ok := kw[name] + if !ok { + return def, nil + } + ev, err := convert(v.Go(), reflect.TypeFor[E]()) + if err != nil { + return def, err + } + return ev.Interface().(E), nil +} + +func minimize(c *rb.Call) (any, error) { + call, err := blockAndArgs(c, 1) + if err != nil { + return nil, err + } + x0, err := toFloats(c.Args[0].Go()) + if err != nil { + return nil, err + } + mine, rest := split(c.Kwargs, "method", "jac") + method, err := enumKw(mine, "method", optimize.BFGS) + if err != nil { + return nil, err + } + g := &guard{} + p := optimize.Problem{Func: fn[func([]float64) float64](g, call)} + if p.Grad, _, err = optionalProc[func(x, grad []float64)](g, mine, "jac"); err != nil { + return nil, err + } + o, err := optsFrom[optimize.Options](rest) + if err != nil { + return nil, err + } + res, err := optimize.Minimize(p, x0, method, o) + return finish(g, res, err) +} + +func minimizeScalar(c *rb.Call) (any, error) { + call, err := blockAndArgs(c, 0) + if err != nil { + return nil, err + } + mine, rest := split(c.Kwargs, "method") + method, err := enumKw(mine, "method", optimize.Brent) + if err != nil { + return nil, err + } + o, err := optsFrom[optimize.ScalarOptions](rest) + if err != nil { + return nil, err + } + g := &guard{} + res, err := optimize.MinimizeScalar(fn[func(float64) float64](g, call), method, o) + return finish(g, res, err) +} + +func bracketRoot(f func(func(float64) float64, float64, float64, *optimize.BracketOptions) (*optimize.RootScalarResult, error)) rb.Func { + return func(c *rb.Call) (any, error) { + call, err := blockAndArgs(c, 2) + if err != nil { + return nil, err + } + ab, err := floatArgs(c) + if err != nil { + return nil, err + } + o, err := optsFrom[optimize.BracketOptions](c.Kwargs) + if err != nil { + return nil, err + } + g := &guard{} + res, err := f(fn[func(float64) float64](g, call), ab[0], ab[1], o) + return finish(g, res, err) + } +} + +// derivatives reads fprime: and fprime2: Procs. +func derivatives(g *guard, kw map[string]rb.Value) (f1, f2 func(float64) float64, rest map[string]rb.Value, err error) { + mine, rest := split(kw, "fprime", "fprime2") + if f1, _, err = optionalProc[func(float64) float64](g, mine, "fprime"); err != nil { + return nil, nil, nil, err + } + if f2, _, err = optionalProc[func(float64) float64](g, mine, "fprime2"); err != nil { + return nil, nil, nil, err + } + return f1, f2, rest, nil +} + +func newton(c *rb.Call) (any, error) { + call, err := blockAndArgs(c, 1) + if err != nil { + return nil, err + } + x0, err := toFloat(c.Args[0].Go()) + if err != nil { + return nil, err + } + g := &guard{} + f1, f2, rest, err := derivatives(g, c.Kwargs) + if err != nil { + return nil, err + } + o := &optimize.NewtonOptions{} + if err := options(o, rest); err != nil { + return nil, err + } + _, o.X1Set = rest["x1"] + o.Fprime, o.Fprime2 = f1, f2 + res, err := optimize.Newton(fn[func(float64) float64](g, call), x0, o) + return finish(g, res, err) +} + +func rootScalar(c *rb.Call) (any, error) { + call, err := blockAndArgs(c, 0) + if err != nil { + return nil, err + } + g := &guard{} + f1, f2, rest, err := derivatives(g, c.Kwargs) + if err != nil { + return nil, err + } + mine, rest := split(rest, "method") + method := "" + if v, ok := mine["method"]; ok { + if method, err = toString(v.Go()); err != nil { + return nil, err + } + } + o := &optimize.RootScalarOptions{} + if err := options(o, rest); err != nil { + return nil, err + } + _, o.X0Set = rest["x0"] + _, o.X1Set = rest["x1"] + o.Fprime, o.Fprime2 = f1, f2 + res, err := optimize.RootScalar(fn[func(float64) float64](g, call), method, o) + return finish(g, res, err) +} + +func systemRoot(f func(optimize.SystemProblem, []float64, *optimize.RootOptions) (*optimize.RootResult, error)) rb.Func { + return func(c *rb.Call) (any, error) { + call, err := blockAndArgs(c, 1) + if err != nil { + return nil, err + } + x0, err := toFloats(c.Args[0].Go()) + if err != nil { + return nil, err + } + g := &guard{} + mine, rest := split(c.Kwargs, "jac") + jac, _, err := optionalProc[func(x, jac []float64)](g, mine, "jac") + if err != nil { + return nil, err + } + o, err := optsFrom[optimize.RootOptions](rest) + if err != nil { + return nil, err + } + p := optimize.SystemProblem{Func: fn[func(x, f []float64)](g, call), Jac: jac} + res, err := f(p, x0, o) + return finish(g, res, err) + } +} + +// lsProblem builds the residual problem of least_squares and leastsq: m: is +// the number of residuals, and when it is not given the block is called once +// at x0 to count them. +func lsProblem(c *rb.Call, g *guard, call caller, x0 []float64) (optimize.LSProblem, map[string]rb.Value, error) { + mine, rest := split(c.Kwargs, "m", "jac") + p := optimize.LSProblem{Func: fn[func(x, f []float64)](g, call)} + if v, ok := mine["m"]; ok { + m, err := toInt(v.Go()) + if err != nil { + return p, nil, err + } + p.M = int(m) + } else { + r, err := call(vec(x0)) + if err != nil { + return p, nil, err + } + f, err := toFloats(r.Go()) + if err != nil { + return p, nil, err + } + p.M = len(f) + } + jac, _, err := optionalProc[func(x, jac []float64)](g, mine, "jac") + p.Jac = jac + return p, rest, err +} + +func leastSquares(c *rb.Call) (any, error) { + call, err := blockAndArgs(c, 1) + if err != nil { + return nil, err + } + x0, err := toFloats(c.Args[0].Go()) + if err != nil { + return nil, err + } + g := &guard{} + p, rest, err := lsProblem(c, g, call, x0) + if err != nil { + return nil, err + } + mine, rest := split(rest, "method") + method, err := enumKw(mine, "method", optimize.TRF) + if err != nil { + return nil, err + } + o, err := optsFrom[optimize.LSOptions](rest) + if err != nil { + return nil, err + } + res, err := optimize.LeastSquares(p, x0, method, o) + return finish(g, res, err) +} + +func leastsq(c *rb.Call) (any, error) { + call, err := blockAndArgs(c, 1) + if err != nil { + return nil, err + } + x0, err := toFloats(c.Args[0].Go()) + if err != nil { + return nil, err + } + g := &guard{} + p, rest, err := lsProblem(c, g, call, x0) + if err != nil { + return nil, err + } + o, err := optsFrom[optimize.LeastsqOptions](rest) + if err != nil { + return nil, err + } + res, err := optimize.Leastsq(p, x0, o) + return finish(g, res, err) +} + +func curveFit(c *rb.Call) (any, error) { + call, err := blockAndArgs(c, 2) + if err != nil { + return nil, err + } + x, err := ndArray(c.Args[0].Go()) + if err != nil { + return nil, err + } + y, err := ndArray(c.Args[1].Go()) + if err != nil { + return nil, err + } + mine, rest := split(c.Kwargs, "p0") + var p0 []float64 + if v, ok := mine["p0"]; ok { + if p0, err = toFloats(v.Go()); err != nil { + return nil, err + } + } + o, err := optsFrom[optimize.CurveFitOptions](rest) + if err != nil { + return nil, err + } + g := &guard{} + model := g.makeFunc(reflect.TypeFor[func(x float64, p []float64) float64](), call, true).Interface().(func(float64, []float64) float64) + res, err := optimize.CurveFit(model, x, y, p0, o) + return finish(g, res, err) +} + +func linprog(c *rb.Call) (any, error) { + if err := arity(c, 1, 1); err != nil { + return nil, err + } + cost, err := toFloats(c.Args[0].Go()) + if err != nil { + return nil, err + } + mine, rest := split(c.Kwargs, "a_ub", "b_ub", "a_eq", "b_eq", "bounds") + var cons optimize.LPConstraints + if err := options(&cons, mine); err != nil { + return nil, err + } + o, err := optsFrom[optimize.LPOptions](rest) + if err != nil { + return nil, err + } + res, err := optimize.Linprog(cost, cons, o) + return finish(&guard{}, res, err) +} + +func brute(c *rb.Call) (any, error) { + call, err := blockAndArgs(c, 1) + if err != nil { + return nil, err + } + rv, err := convert(c.Args[0].Go(), reflect.TypeFor[[]optimize.Range]()) + if err != nil { + return nil, err + } + o, err := optsFrom[optimize.BruteOptions](c.Kwargs) + if err != nil { + return nil, err + } + g := &guard{} + res, err := optimize.Brute(fn[func([]float64) float64](g, call), rv.Interface().([]optimize.Range), o) + return finish(g, res, err) +} + +func fminbound(c *rb.Call) (any, error) { + call, err := blockAndArgs(c, 2) + if err != nil { + return nil, err + } + x, err := floatArgs(c) + if err != nil { + return nil, err + } + mine, rest := split(c.Kwargs, "xtol", "maxfun") + if len(rest) > 0 { + return nil, argError("unknown keyword: :%s (known: xtol, maxfun)", sortedKeys(rest)[0]) + } + xtol, maxfun := 1e-5, int64(500) + if v, ok := mine["xtol"]; ok { + if xtol, err = toFloat(v.Go()); err != nil { + return nil, err + } + } + if v, ok := mine["maxfun"]; ok { + if maxfun, err = toInt(v.Go()); err != nil { + return nil, err + } + } + g := &guard{} + res, err := optimize.Fminbound(fn[func(float64) float64](g, call), x[0], x[1], xtol, int(maxfun)) + return finish(g, res, err) +} + +func bracketFn(c *rb.Call) (any, error) { + call, err := blockAndArgs(c, 2) + if err != nil { + return nil, err + } + x, err := floatArgs(c) + if err != nil { + return nil, err + } + g := &guard{} + xs, fs, nfev, err := optimize.Bracket(fn[func(float64) float64](g, call), x[0], x[1]) + return finish(g, []any{xs[:], fs[:], nfev}, err) +} + +func approxFprime(c *rb.Call) (any, error) { + call, err := blockAndArgs(c, 2) + if err != nil { + return nil, err + } + x, err := toFloats(c.Args[0].Go()) + if err != nil { + return nil, err + } + eps, err := toFloat(c.Args[1].Go()) + if err != nil { + return nil, err + } + g := &guard{} + return finish(g, vec(optimize.ApproxFprime(fn[func([]float64) float64](g, call), x, eps)), nil) +} diff --git a/smoke_test.go b/smoke_test.go index 0c9d940..e7d8b39 100644 --- a/smoke_test.go +++ b/smoke_test.go @@ -35,6 +35,34 @@ func TestSmoke(t *testing.T) { `Stats::MultivariateNormal.new([0, 0], NDArray.eye(2)).pdf([0, 0])`, `Stats.pearsonr([1, 2, 3, 4], [1, 3, 2, 4]).confidence_interval(0.9)`, `Stats::Normal.new.to_h`, + `Special.gamma(5)`, + `Special.gamma(NDArray.arange(1, 5))`, + `Special.beta_inc(2, 3, [0.1, 0.5])`, + `Special.jv(NDArray.arange(3).reshape(3, 1), NDArray.linspace(0, 1, 2))`, + `Special.jn_zeros(0, 3)`, + `Special.log_gamma_sign(-0.5)`, + `Special.softmax([1, 2, 3])`, + `Special.comb(5, 2)`, + `Special.in(1, 0.5)`, + `Optimize.minimize([1.0, 1.0]) { |x| (x[0] - 3)**2 + (x[1] + 1)**2 }`, + `Optimize.minimize([1.0, 1.0], method: :nelder_mead) { |x| (x[0] - 3)**2 + (x[1] + 1)**2 }.x`, + `Optimize.minimize([1.0], method: :lbfgsb, bounds: [[2, nil]]) { |x| x[0]**2 }.x`, + `Optimize.brentq(0, 2) { |x| x * x - 2 }.root`, + `Optimize.newton(1.0, fprime: ->(x) { 2 * x }) { |x| x * x - 2 }.root`, + `Optimize.root_scalar(method: "brentq", bracket: [0, 2]) { |x| x * x - 2 }.root`, + `Optimize.root([1.0, 1.0]) { |x| [x[0] + x[1] - 3, x[0] - x[1] - 1] }.x`, + `Optimize.curve_fit([0, 1, 2, 3], [1, 2.7, 7.4, 20.1], p0: [1, 1]) { |x, a, b| a * Math.exp(b * x) }.popt`, + `Optimize.linprog([-1, -2], a_ub: NDArray.from([1, 1], [1, 2]), b_ub: [4], bounds: [[0, nil], [0, 3]]).x`, + `Optimize.minimize_scalar { |x| (x - 2)**2 }.x`, + `Optimize.least_squares([1.0, 1.0]) { |x| [x[0] - 1, x[1] - 2, x[0] + x[1] - 3] }.x`, + `Integrate.quad(0, 1) { |x| x * x }`, + `Integrate.quad(0, Float::INFINITY) { |x| Math.exp(-x) }.value`, + `Integrate.dblquad(0, 1, 0, ->(x) { x }) { |y, x| x * y }.value`, + `Integrate.solve_ivp([0, 1], [1.0], t_eval: [0, 0.5, 1]) { |t, y| [-y[0]] }.y`, + `Integrate.odeint([1.0], [0, 0.5, 1]) { |t, y| [-y[0]] }`, + `Integrate.trapezoid([1, 2, 3])`, + `Integrate.simpson(NDArray.arange(6).reshape(2, 3), axis: 1)`, + `Integrate.gauss_legendre(3)`, } { v, err := in.Eval(src, "smoke.rb", 1) if err != nil { diff --git a/special.go b/special.go index 44ec167..49b51f8 100644 --- a/special.go +++ b/special.go @@ -1,5 +1,160 @@ package ruby -import rb "github.com/go-embedded-ruby/ruby" +import ( + "reflect" -func registerSpecial(in *rb.Interp) error { return nil } + rb "github.com/go-embedded-ruby/ruby" + nd "github.com/go-ndarray/ndarray" + "github.com/go-numerics/special" +) + +// registerSpecial defines Special, every function of go-numerics/special +// under its snake_case name (Special.gamma, Special.beta_inc, Special.jv, …). +// +// A function of real arguments returning a real is a ufunc, as in SciPy: any +// real argument may be an NDArray (or an Array), the arrays broadcast against +// each other and the scalars, and the result has their shape — an NDArray, or +// an Array when every array argument was an Array. Integer parameters (an +// order n) take an Integer. The few functions of another shape are bound as +// they are: log_gamma_sign returns [value, sign], jn_zeros an NDArray, and +// softmax, log_softmax and log_sum_exp take a vector. +func registerSpecial(in *rb.Interp) error { + d := &definer{in: in} + m := d.module("Special") + for _, f := range specialFuncs { + d.fn(m, snakeName(f.name), specialFunc(f.fn)) + } + return d.err +} + +// specialFunc binds one function of package special. +func specialFunc(f any) rb.Func { + fn := reflect.ValueOf(f) + ft := fn.Type() + switch { + case isUfunc(ft): + return func(c *rb.Call) (any, error) { return applyUfunc(fn, c) } + case ft == reflect.TypeOf(special.Softmax): + return func(c *rb.Call) (any, error) { + if err := arity(c, 1, 1); err != nil { + return nil, err + } + x, err := toFloats(c.Args[0].Go()) + if err != nil { + return nil, err + } + return vec(f.(func(dst, x []float64) []float64)(nil, x)), nil + } + } + return func(c *rb.Call) (any, error) { return invoke(fn, c, c.Args) } +} + +// isUfunc reports whether a function maps reals (and integers) to one real, +// with at least one real parameter to broadcast over. +func isUfunc(ft reflect.Type) bool { + if ft.NumOut() != 1 || ft.Out(0).Kind() != reflect.Float64 { + return false + } + reals := 0 + for i := range ft.NumIn() { + switch ft.In(i).Kind() { + case reflect.Float64: + reals++ + case reflect.Int, reflect.Int64: + default: + return false + } + } + return reals > 0 +} + +// applyUfunc calls a ufunc elementwise over its array arguments. +func applyUfunc(fn reflect.Value, c *rb.Call) (any, error) { + ft := fn.Type() + if err := arity(c, ft.NumIn(), ft.NumIn()); err != nil { + return nil, err + } + if len(c.Kwargs) > 0 { + return nil, argError("unknown keyword: :%s", sortedKeys(c.Kwargs)[0]) + } + scalars := make([]reflect.Value, ft.NumIn()) + var arrays []*nd.Array + at := make([]int, ft.NumIn()) // index into arrays, or -1 for a scalar + allLists := true + for i, a := range c.Args { + at[i] = -1 + x := a.Go() + if ft.In(i).Kind() == reflect.Float64 { + switch v := x.(type) { + case *nd.Array: + at[i] = len(arrays) + arrays = append(arrays, v) + allLists = false + continue + case []any: + a, err := ndArray(v) + if err != nil { + return nil, err + } + at[i] = len(arrays) + arrays = append(arrays, a) + continue + } + } + v, err := convert(x, ft.In(i)) + if err != nil { + return nil, err + } + scalars[i] = v + } + if len(arrays) == 0 { + return fn.Call(scalars)[0].Float(), nil + } + flat, shape, err := broadcast(arrays) + if err != nil { + return nil, err + } + n := len(flat[0]) + res := make([]float64, n) + args := append([]reflect.Value(nil), scalars...) + for k := range n { + for i, j := range at { + if j >= 0 { + args[i] = reflect.ValueOf(flat[j][k]) + } + } + res[k] = fn.Call(args)[0].Float() + } + if allLists { + list := make([]any, n) + for k, r := range res { + list[k] = r + } + return list, nil + } + a, _ := nd.FromData(res, shape...) + return a, nil +} + +// broadcast brings the arrays to their common shape, NumPy's rules, and +// returns their elements row-major. Multiplying by ones broadcasts exactly: +// x·1 is x for every float64, NaN and the infinities included. +func broadcast(arrays []*nd.Array) ([][]float64, []int, error) { + common := arrays[0] + for _, a := range arrays[1:] { + ones, _ := nd.Ones(a.Shape()...) + b, err := common.Mul(ones) + if err != nil { + return nil, nil, goError(err) + } + common = b + } + shape := common.Shape() + ones, _ := nd.Ones(shape...) + out := make([][]float64, len(arrays)) + for i, a := range arrays { + b, _ := a.Mul(ones) // a broadcasts to the common shape: it was part of it + out[i] = arrayFloats(b) + } + return out, shape, nil +} diff --git a/special_gen.go b/special_gen.go new file mode 100644 index 0000000..611128a --- /dev/null +++ b/special_gen.go @@ -0,0 +1,76 @@ +// Code generated by internal/gen; DO NOT EDIT. + +package ruby + +import "github.com/go-numerics/special" + +// specialFuncs are the exported functions of go-numerics/special. +var specialFuncs = []namedFunc{ + {"Beta", special.Beta}, + {"BetaInc", special.BetaInc}, + {"BetaIncC", special.BetaIncC}, + {"BetaIncCInv", special.BetaIncCInv}, + {"BetaIncInv", special.BetaIncInv}, + {"Binom", special.Binom}, + {"Comb", special.Comb}, + {"Digamma", special.Digamma}, + {"EllipE", special.EllipE}, + {"EllipK", special.EllipK}, + {"EllipKm1", special.EllipKm1}, + {"Erf", special.Erf}, + {"ErfInv", special.ErfInv}, + {"Erfc", special.Erfc}, + {"ErfcInv", special.ErfcInv}, + {"Erfcx", special.Erfcx}, + {"Expit", special.Expit}, + {"Factorial", special.Factorial}, + {"Factorial2", special.Factorial2}, + {"Gamma", special.Gamma}, + {"GammaP", special.GammaP}, + {"GammaPInv", special.GammaPInv}, + {"GammaQ", special.GammaQ}, + {"GammaQInv", special.GammaQInv}, + {"GammaSign", special.GammaSign}, + {"HurwitzZeta", special.HurwitzZeta}, + {"I0", special.I0}, + {"I0e", special.I0e}, + {"I1", special.I1}, + {"I1e", special.I1e}, + {"In", special.In}, + {"Iv", special.Iv}, + {"Ive", special.Ive}, + {"J0", special.J0}, + {"J1", special.J1}, + {"Jn", special.Jn}, + {"JnZeros", special.JnZeros}, + {"Jv", special.Jv}, + {"K0", special.K0}, + {"K0e", special.K0e}, + {"K1", special.K1}, + {"K1e", special.K1e}, + {"Kn", special.Kn}, + {"Kv", special.Kv}, + {"Kve", special.Kve}, + {"Log1pExp", special.Log1pExp}, + {"LogBeta", special.LogBeta}, + {"LogExpit", special.LogExpit}, + {"LogFactorial", special.LogFactorial}, + {"LogGamma", special.LogGamma}, + {"LogGammaSign", special.LogGammaSign}, + {"LogNdtr", special.LogNdtr}, + {"LogSoftmax", special.LogSoftmax}, + {"LogSumExp", special.LogSumExp}, + {"Logit", special.Logit}, + {"Ndtr", special.Ndtr}, + {"Ndtri", special.Ndtri}, + {"Perm", special.Perm}, + {"Polygamma", special.Polygamma}, + {"Softmax", special.Softmax}, + {"Xlog1py", special.Xlog1py}, + {"Xlogy", special.Xlogy}, + {"Y0", special.Y0}, + {"Y1", special.Y1}, + {"Yn", special.Yn}, + {"Yv", special.Yv}, + {"Zeta", special.Zeta}, +} From 1419f6c03037ccee33f7b95a4f615e831d517cab Mon Sep 17 00:00:00 2001 From: tannevaled Date: Sat, 17 Oct 2026 22:28:23 +0200 Subject: [PATCH 03/20] Tests: every Stats, Special, Optimize and Integrate binding compared with the Go call --- conv.go | 40 ++++++ helpers_test.go | 272 +++++++++++++++++++++++++++++++++++++ integrate_test.go | 68 ++++++++++ optimize_test.go | 119 ++++++++++++++++ reflect.go | 8 +- special_test.go | 117 ++++++++++++++++ stats.go | 9 +- stats_test.go | 336 ++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 964 insertions(+), 5 deletions(-) create mode 100644 helpers_test.go create mode 100644 integrate_test.go create mode 100644 optimize_test.go create mode 100644 special_test.go create mode 100644 stats_test.go diff --git a/conv.go b/conv.go index 4ed4635..0c23f99 100644 --- a/conv.go +++ b/conv.go @@ -194,6 +194,23 @@ func convert(v any, t reflect.Type) (reflect.Value, error) { a, err := ndArray(v) return reflect.ValueOf(a), err } + if v == nil { + return reflect.Zero(t), nil + } + if reflect.TypeOf(v) != t { + // An optional value, such as Ddof *float64 or Range *[2]float64. + ev, err := convert(v, t.Elem()) + if err != nil { + return reflect.Value{}, err + } + p := reflect.New(t.Elem()) + p.Elem().Set(ev) + return p, nil + } + case reflect.Struct: + if h, ok := v.(map[any]any); ok { + return convertStruct(h, t) + } } if v != nil && reflect.TypeOf(v) == t { return reflect.ValueOf(v), nil // a Go value that went through Ruby @@ -203,6 +220,29 @@ func convert(v any, t reflect.Type) (reflect.Value, error) { var ndArrayType = reflect.TypeFor[*nd.Array]() +// convertStruct fills a struct from a Hash, as options fills an option +// struct from keywords: bandwidth: {rule: :silverman}. +func convertStruct(h map[any]any, t reflect.Type) (reflect.Value, error) { + p := reflect.New(t) + fields := fieldsByName(t) + for k, e := range h { + name, err := toString(k) + if err != nil { + return reflect.Value{}, err + } + f, ok := fields[normalize(name)] + if !ok { + return reflect.Value{}, argError("unknown key %s for %s (known: %s)", name, t.Name(), strings.Join(keywordNames(t), ", ")) + } + ev, err := convert(e, f.Type) + if err != nil { + return reflect.Value{}, err + } + p.Elem().FieldByIndex(f.Index).Set(ev) + } + return p.Elem(), nil +} + // toString reads a String or a Symbol. func toString(v any) (string, error) { switch s := v.(type) { diff --git a/helpers_test.go b/helpers_test.go new file mode 100644 index 0000000..74df56b --- /dev/null +++ b/helpers_test.go @@ -0,0 +1,272 @@ +package ruby + +import ( + "bytes" + "errors" + "fmt" + "math" + "math/big" + "reflect" + "strings" + "testing" + + rb "github.com/go-embedded-ruby/ruby" + nd "github.com/go-ndarray/ndarray" +) + +// newInterp is an interpreter with every module registered. +func newInterp(t *testing.T) *rb.Interp { + t.Helper() + var out bytes.Buffer + in := rb.New(rb.WithStdout(&out), rb.WithStderr(&out)) + if err := Register(in); err != nil { + t.Fatal(err) + } + return in +} + +// eval runs src and returns its value converted to Go. +func eval(t *testing.T, in *rb.Interp, src string) any { + t.Helper() + v, err := in.Eval(src, "test.rb", 1) + if err != nil { + t.Fatalf("%s: %v", src, err) + } + return v.Go() +} + +// evalErr runs src, which must raise an exception of class whose message +// contains msg. +func evalErr(t *testing.T, in *rb.Interp, src, class, msg string) { + t.Helper() + _, err := in.Eval(src, "test.rb", 1) + var e *rb.Error + if !errors.As(err, &e) { + t.Fatalf("%s: no exception (err = %v)", src, err) + } + if e.Class != class || !strings.Contains(e.Message, msg) { + t.Fatalf("%s: raised %s: %s; want %s containing %q", src, e.Class, e.Message, class, msg) + } +} + +// same reports whether a value Ruby returned (converted to Go) is the Go +// result, bit for bit: floats compare by their bits (NaN equal to NaN), an +// NDArray equals a []float64 or an NDArray with the same elements and shape, +// an Array a slice element by element, and structs field by field. +func same(got, want any) bool { + return sameValue(reflect.ValueOf(got), reflect.ValueOf(want)) +} + +func sameValue(g, w reflect.Value) bool { + if !g.IsValid() || !w.IsValid() { + return g.IsValid() == w.IsValid() || isNilish(g) && isNilish(w) + } + if g.Kind() == reflect.Interface { + return sameValue(g.Elem(), w) + } + if w.Kind() == reflect.Interface { + return sameValue(g, w.Elem()) + } + if ga, ok := g.Interface().(*nd.Array); ok && ga != nil { + return sameFloats(arrayFloats(ga), floatsOf(w), shapeOf(ga), shapeOfValue(w)) + } + if wa, ok := w.Interface().(*nd.Array); ok && wa != nil { + return sameFloats(floatsOf(g), arrayFloats(wa), shapeOfValue(g), shapeOf(wa)) + } + switch w.Kind() { + case reflect.Float64, reflect.Float32: + gf, ok := asFloat(g) + wf := w.Float() + return ok && (math.Float64bits(gf) == math.Float64bits(wf) || math.IsNaN(gf) && math.IsNaN(wf)) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return g.CanInt() && g.Int() == w.Int() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return g.CanInt() && uint64(g.Int()) == w.Uint() + case reflect.Slice, reflect.Array: + if w.Kind() == reflect.Slice && w.IsNil() { + return isNilish(g) + } + if g.Kind() != reflect.Slice && g.Kind() != reflect.Array || g.Len() != w.Len() { + return false + } + for i := range w.Len() { + if !sameValue(g.Index(i), w.Index(i)) { + return false + } + } + return true + case reflect.Pointer: + if w.IsNil() { + return isNilish(g) + } + if g.Kind() == reflect.Pointer { + if b, ok := w.Interface().(*big.Int); ok { + gb, ok := g.Interface().(*big.Int) + return ok && gb.Cmp(b) == 0 + } + return sameValue(g.Elem(), w.Elem()) + } + return sameValue(g, w.Elem()) + case reflect.Struct: + if g.Kind() == reflect.Pointer { + g = g.Elem() + } + if g.Type() != w.Type() { + return false + } + for i := range w.NumField() { + if !w.Type().Field(i).IsExported() { + continue + } + if !sameValue(g.Field(i), w.Field(i)) { + return false + } + } + return true + case reflect.Map: + if g.Kind() != reflect.Map || g.Len() != w.Len() { + return false + } + for _, k := range w.MapKeys() { + gv := g.MapIndex(k) + if !gv.IsValid() { + // Ruby keys convert: a Go string key may be a Symbol there. + gv = g.MapIndex(reflect.ValueOf(rb.Symbol(fmt.Sprint(k.Interface())))) + } + if !gv.IsValid() || !sameValue(gv, w.MapIndex(k)) { + return false + } + } + return true + } + return reflect.DeepEqual(g.Interface(), w.Interface()) +} + +func isNilish(v reflect.Value) bool { + if !v.IsValid() { + return true + } + switch v.Kind() { + case reflect.Pointer, reflect.Slice, reflect.Map, reflect.Interface: + return v.IsNil() + } + return false +} + +func asFloat(v reflect.Value) (float64, bool) { + switch v.Kind() { + case reflect.Float64, reflect.Float32: + return v.Float(), true + } + return 0, false +} + +func floatsOf(v reflect.Value) []float64 { + if v.Kind() == reflect.Interface { + v = v.Elem() + } + if a, ok := v.Interface().(*nd.Array); ok { + return arrayFloats(a) + } + if v.Kind() == reflect.Float64 { + return []float64{v.Float()} + } + if v.Kind() != reflect.Slice && v.Kind() != reflect.Array { + return nil + } + out := make([]float64, v.Len()) + for i := range out { + e := v.Index(i) + if e.Kind() == reflect.Interface { + e = e.Elem() + } + out[i], _ = asFloat(e) + } + return out +} + +func shapeOf(a *nd.Array) []int { return a.Shape() } + +func shapeOfValue(v reflect.Value) []int { + if v.Kind() == reflect.Interface { + v = v.Elem() + } + if a, ok := v.Interface().(*nd.Array); ok { + return a.Shape() + } + if v.Kind() == reflect.Float64 { + return []int{} + } + return []int{v.Len()} +} + +func sameFloats(a, b []float64, sa, sb []int) bool { + if len(a) != len(b) { + return false + } + // A 1-d slice matches an array of any shape with its elements (the Go + // side often returns []float64 where Ruby gets an NDArray). + if len(sa) > 1 && len(sb) > 1 && !reflect.DeepEqual(sa, sb) { + return false + } + for i := range a { + if math.Float64bits(a[i]) != math.Float64bits(b[i]) && !(math.IsNaN(a[i]) && math.IsNaN(b[i])) { + return false + } + } + return true +} + +// check fails unless the Ruby source evaluates to the Go value. +func check(t *testing.T, in *rb.Interp, src string, want any) { + t.Helper() + got := eval(t, in, src) + if !same(got, want) { + t.Errorf("%s\n ruby: %#v\n go: %#v", src, got, want) + } +} + +// must is a Go call's value, failing the test on its error. +func must[T any](t *testing.T) func(v T, err error) T { + return func(v T, err error) T { + t.Helper() + if err != nil { + t.Fatal(err) + } + return v + } +} + +// same is the instrument of every comparison with Go, so it must see a +// difference of one ulp, of shape, of length and of a field. +func TestSameIsStrict(t *testing.T) { + type pair struct{ A, B float64 } + a2, _ := nd.FromData([]float64{1, 2, 3, 4}, 2, 2) + a4, _ := nd.FromData([]float64{1, 2, 3, 4}, 4, 1) + for i, c := range []struct { + got, want any + same bool + }{ + {1.0, 1.0, true}, + {1.0, math.Nextafter(1, 2), false}, + {math.NaN(), math.NaN(), true}, + {int64(3), 3, true}, + {int64(3), 4, false}, + {[]any{1.0, 2.0}, []float64{1, 2}, true}, + {[]any{1.0, 2.0}, []float64{1, 2, 3}, false}, + {a2, []float64{1, 2, 3, 4}, true}, + {a2, a4, false}, + {pair{1, 2}, pair{1, 2}, true}, + {pair{1, 2}, pair{1, 3}, false}, + {&pair{1, 2}, pair{1, 2}, true}, + {nil, (*pair)(nil), true}, + {"a", "a", true}, + {"a", "b", false}, + {map[any]any{rb.Symbol("k"): 1.0}, map[string]float64{"k": 1}, true}, + {map[any]any{rb.Symbol("k"): 2.0}, map[string]float64{"k": 1}, false}, + } { + if got := same(c.got, c.want); got != c.same { + t.Errorf("case %d: same(%#v, %#v) = %v", i, c.got, c.want, got) + } + } +} diff --git a/integrate_test.go b/integrate_test.go new file mode 100644 index 0000000..6dd8793 --- /dev/null +++ b/integrate_test.go @@ -0,0 +1,68 @@ +package ruby + +import ( + "math" + "testing" + + nd "github.com/go-ndarray/ndarray" + "github.com/go-numerics/integrate" +) + +func TestIntegrateMatchesGo(t *testing.T) { + in := newInterp(t) + q := must[integrate.QuadResult](t) + sq := func(x float64) float64 { return x * x } + check(t, in, "Integrate.quad(0, 2) { |x| x * x }", q(integrate.Quad(sq, 0, 2, nil))) + check(t, in, "Integrate.quad(0, Float::INFINITY, eps_rel: 1e-10) { |x| Math.exp(-x) }", q(integrate.Quad(func(x float64) float64 { return math.Exp(-x) }, 0, math.Inf(1), &integrate.QuadOptions{EpsRel: 1e-10}))) + check(t, in, "Integrate.quad(0, 1, weight: :cos, omega: 3) { |x| x }", q(integrate.Quad(func(x float64) float64 { return x }, 0, 1, &integrate.QuadOptions{Weight: integrate.WeightCos, Omega: 3}))) + + xy := func(y, x float64) float64 { return x * y } + check(t, in, "Integrate.dblquad(0, 1, 0, 2) { |y, x| x * y }", q(integrate.DblQuad(xy, 0, 1, integrate.Const(0), integrate.Const(2), nil))) + check(t, in, "Integrate.dblquad(0, 1, ->(x) { x * x }, ->(x) { x }, eps_abs: 1e-12) { |y, x| x * y }", + q(integrate.DblQuad(xy, 0, 1, func(x float64) float64 { return x * x }, func(x float64) float64 { return x }, &integrate.QuadOptions{EpsAbs: 1e-12}))) + xyz := func(z, y, x float64) float64 { return x + y + z } + check(t, in, "Integrate.tplquad(0, 1, 0, 1, 0, ->(x, y) { x + y }) { |z, y, x| x + y + z }", + q(integrate.TplQuad(xyz, 0, 1, integrate.Const(0), integrate.Const(1), integrate.Const2(0), func(x, y float64) float64 { return x + y }, nil))) + check(t, in, "Integrate.nquad([[0, 1], [0, 2]]) { |x| x[0] * x[1] }", + q(integrate.NQuad(func(x []float64) float64 { return x[0] * x[1] }, []integrate.Range{integrate.ConstRange(0, 1), integrate.ConstRange(0, 2)}, nil))) + check(t, in, "Integrate.nquad([[0, 1]], limit: 10) { |x| x[0] }", + q(integrate.NQuad(func(x []float64) float64 { return x[0] }, []integrate.Range{integrate.ConstRange(0, 1)}, &integrate.NQuadOptions{Opts: []*integrate.QuadOptions{{Limit: 10}}}))) + check(t, in, "Integrate.fixed_quad(0, 1) { |x| Math.sin(x) }", must[float64](t)(integrate.FixedQuad(math.Sin, 0, 1, 5))) + check(t, in, "Integrate.fixed_quad(0, 1, 9) { |x| Math.sin(x) }", must[float64](t)(integrate.FixedQuad(math.Sin, 0, 1, 9))) + check(t, in, "Integrate.romberg(0, 1) { |x| Math.sin(x) }", must[float64](t)(integrate.Romberg(math.Sin, 0, 1, nil))) + check(t, in, "Integrate.romberg(0, 1, tol: 1e-10, div_max: 12) { |x| Math.sin(x) }", must[float64](t)(integrate.Romberg(math.Sin, 0, 1, &integrate.RombergOptions{Tol: 1e-10, DivMax: 12}))) + + decay := func(t float64, y, dydt []float64) { dydt[0] = -0.5 * y[0]; dydt[1] = y[0] - y[1] } + decayRuby := "{ |t, y| [-0.5 * y[0], y[0] - y[1]] }" + check(t, in, "Integrate.solve_ivp([0, 4], [1.0, 0.0]) "+decayRuby, must[*integrate.Solution](t)(integrate.SolveIVP(decay, [2]float64{0, 4}, []float64{1, 0}, nil))) + check(t, in, "Integrate.solve_ivp([0, 4], [1.0, 0.0], method: :radau, t_eval: [0, 1, 2, 4], r_tol: 1e-8) "+decayRuby, + must[*integrate.Solution](t)(integrate.SolveIVP(decay, [2]float64{0, 4}, []float64{1, 0}, &integrate.IVPOptions{Method: integrate.Radau, TEval: []float64{0, 1, 2, 4}, RTol: 1e-8}))) + sol := must[*integrate.Solution](t)(integrate.SolveIVP(decay, [2]float64{0, 4}, []float64{1, 0}, &integrate.IVPOptions{DenseOutput: true})) + check(t, in, "Integrate.solve_ivp([0, 4], [1.0, 0.0], dense_output: true) { |t, y| [-0.5 * y[0], y[0] - y[1]] }.sol.eval(1.5)", sol.Sol.Eval(1.5)) + y, info, err := integrate.Odeint(decay, []float64{1, 0}, []float64{0, 1, 2}, nil) + if err != nil { + t.Fatal(err) + } + check(t, in, "Integrate.odeint([1.0, 0.0], [0, 1, 2]) "+decayRuby, []any{y, info}) + y, info, err = integrate.Odeint(decay, []float64{1, 0}, []float64{0, 1, 2}, &integrate.OdeintOptions{RTol: 1e-10}) + if err != nil { + t.Fatal(err) + } + check(t, in, "Integrate.odeint([1.0, 0.0], [0, 1, 2], r_tol: 1e-10) "+decayRuby, []any{y, info}) + + ys := []float64{1, 4, 9, 16, 25} + xs := []float64{0, 1, 2.5, 3, 4} + f := must[float64](t) + check(t, in, "Integrate.trapezoid([1, 4, 9, 16, 25])", f(integrate.Trapezoid(ys, nil, 1))) + check(t, in, "Integrate.trapezoid([1, 4, 9, 16, 25], x: [0, 1, 2.5, 3, 4])", f(integrate.Trapezoid(ys, xs, 1))) + check(t, in, "Integrate.simpson(NDArray.from([1, 4, 9, 16, 25], [5]), dx: 0.5)", f(integrate.Simpson(ys, nil, 0.5))) + grid, _ := nd.FromData([]float64{1, 2, 3, 4, 5, 6}, 2, 3) + check(t, in, "Integrate.trapezoid(NDArray.from([1, 2, 3, 4, 5, 6], [2, 3]), axis: 0)", must[*nd.Array](t)(integrate.TrapezoidND(grid, nil, 1, 0))) + check(t, in, "Integrate.simpson(NDArray.from([1, 2, 3, 4, 5, 6], [2, 3]))", must[*nd.Array](t)(integrate.SimpsonND(grid, nil, 1, -1))) + check(t, in, "Integrate.cumulative_trapezoid([1, 4, 9, 16, 25], initial: true)", must[[]float64](t)(integrate.CumulativeTrapezoid(ys, nil, 1, true))) + check(t, in, "Integrate.cumulative_trapezoid([1, 4, 9, 16, 25], x: [0, 1, 2.5, 3, 4])", must[[]float64](t)(integrate.CumulativeTrapezoid(ys, xs, 1, false))) + check(t, in, "Integrate.cumulative_trapezoid(NDArray.from([1, 2, 3, 4, 5, 6], [2, 3]), axis: 1, initial: true)", must[*nd.Array](t)(integrate.CumulativeTrapezoidND(grid, nil, 1, 1, true))) + check(t, in, "Integrate.romb([1, 4, 9, 16, 25], dx: 0.25)", f(integrate.Romb(ys, 0.25))) + gx, gw := integrate.GaussLegendre(4) + check(t, in, "Integrate.gauss_legendre(4)", []any{gx, gw}) +} diff --git a/optimize_test.go b/optimize_test.go new file mode 100644 index 0000000..23c3ea9 --- /dev/null +++ b/optimize_test.go @@ -0,0 +1,119 @@ +package ruby + +import ( + "math" + "testing" + + nd "github.com/go-ndarray/ndarray" + "github.com/go-numerics/optimize" +) + +func rosen(x []float64) float64 { + // float64(...) rounds each product, as Ruby does: without it the Go + // compiler may fuse a multiply and an add (FMA on arm64) and the two + // sides would differ by the fused rounding, not by the binding. + a, b := 1-x[0], x[1]-float64(x[0]*x[0]) + return float64(a*a) + float64(100*b*b) +} + +const rosenRuby = "{ |x| a = 1 - x[0]; b = x[1] - x[0] * x[0]; a * a + 100 * b * b }" + +func rosenGrad(x, g []float64) { + g[0] = float64(-2*(1-x[0])) - float64(400*x[0]*(x[1]-float64(x[0]*x[0]))) + g[1] = 200 * (x[1] - float64(x[0]*x[0])) +} + +const rosenGradRuby = "->(x) { [-2 * (1 - x[0]) - 400 * x[0] * (x[1] - x[0] * x[0]), 200 * (x[1] - x[0] * x[0])] }" + +func cube(x float64) float64 { return float64(x*x*x) - float64(2*x) - 5 } + +const cubeRuby = "{ |x| x * x * x - 2 * x - 5 }" + +func TestOptimizeMatchesGo(t *testing.T) { + in := newInterp(t) + x0 := []float64{-1.2, 1} + m := must[*optimize.Result](t) + check(t, in, "Optimize.minimize([-1.2, 1]) "+rosenRuby, m(optimize.Minimize(optimize.Problem{Func: rosen}, x0, optimize.BFGS, nil))) + for _, meth := range []struct { + sym string + m optimize.Method + }{{":nelder_mead", optimize.NelderMead}, {":powell", optimize.Powell}, {":cg", optimize.CG}, {"'L-BFGS-B'", optimize.LBFGSB}} { + check(t, in, "Optimize.minimize([-1.2, 1], method: "+meth.sym+", max_iter: 400) "+rosenRuby, + m(optimize.Minimize(optimize.Problem{Func: rosen}, x0, meth.m, &optimize.Options{MaxIter: 400}))) + } + check(t, in, "Optimize.minimize([-1.2, 1], jac: "+rosenGradRuby+") "+rosenRuby, + m(optimize.Minimize(optimize.Problem{Func: rosen, Grad: rosenGrad}, x0, optimize.BFGS, nil))) + check(t, in, "Optimize.minimize([3.0, 3.0], method: :lbfgsb, bounds: [[2, nil], [nil, 2.5]]) "+rosenRuby, + m(optimize.Minimize(optimize.Problem{Func: rosen}, []float64{3, 3}, optimize.LBFGSB, &optimize.Options{Bounds: []optimize.Bound{{Lo: 2, Hi: math.Inf(1)}, {Lo: math.Inf(-1), Hi: 2.5}}}))) + + sq := func(x float64) float64 { return (x - 2) * (x - 2) } + check(t, in, "Optimize.minimize_scalar { |x| (x - 2) * (x - 2) }", must[*optimize.ScalarResult](t)(optimize.MinimizeScalar(sq, optimize.Brent, nil))) + check(t, in, "Optimize.minimize_scalar(method: :bounded, bounds: [0, 1]) { |x| (x - 2) * (x - 2) }", + must[*optimize.ScalarResult](t)(optimize.MinimizeScalar(sq, optimize.Bounded, &optimize.ScalarOptions{Bounds: &optimize.Bound{Lo: 0, Hi: 1}}))) + + r := must[*optimize.RootScalarResult](t) + for name, f := range map[string]func(func(float64) float64, float64, float64, *optimize.BracketOptions) (*optimize.RootScalarResult, error){ + "brentq": optimize.Brentq, "brenth": optimize.Brenth, "bisect": optimize.Bisect, "ridder": optimize.Ridder, + } { + check(t, in, "Optimize."+name+"(2, 3) "+cubeRuby, r(f(cube, 2, 3, nil))) + check(t, in, "Optimize."+name+"(2, 3, xtol: 1e-6) "+cubeRuby, r(f(cube, 2, 3, &optimize.BracketOptions{Xtol: 1e-6}))) + } + dcube := func(x float64) float64 { return float64(3*x*x) - 2 } + check(t, in, "Optimize.newton(2.0, fprime: ->(x) { 3 * x * x - 2 }, fprime2: ->(x) { 6 * x }) "+cubeRuby, + r(optimize.Newton(cube, 2, &optimize.NewtonOptions{Fprime: dcube, Fprime2: func(x float64) float64 { return 6 * x }}))) + check(t, in, "Optimize.newton(2.0, x1: 2.5) "+cubeRuby, r(optimize.Newton(cube, 2, &optimize.NewtonOptions{X1: 2.5, X1Set: true}))) + check(t, in, "Optimize.root_scalar(method: 'brentq', bracket: [2, 3]) "+cubeRuby, r(optimize.RootScalar(cube, "brentq", &optimize.RootScalarOptions{Bracket: []float64{2, 3}}))) + check(t, in, "Optimize.root_scalar(x0: 2.0, fprime: ->(x) { 3 * x * x - 2 }) "+cubeRuby, r(optimize.RootScalar(cube, "", &optimize.RootScalarOptions{X0: 2, X0Set: true, Fprime: dcube}))) + check(t, in, "Optimize.root_scalar(method: :secant, x0: 2.0, x1: 3.0) "+cubeRuby, r(optimize.RootScalar(cube, "secant", &optimize.RootScalarOptions{X0: 2, X0Set: true, X1: 3, X1Set: true}))) + + sys := func(x, f []float64) { + d := x[0] - x[1] + f[0] = x[0] + float64(0.5*d*d*d) - 1 + e := x[1] - x[0] + f[1] = float64(0.5*e*e*e) + x[1] + } + sysRuby := "{ |x| d = x[0] - x[1]; [x[0] + 0.5 * d * d * d - 1, 0.5 * (x[1] - x[0]) * (x[1] - x[0]) * (x[1] - x[0]) + x[1]] }" + sysJac := func(x, j []float64) { + d := x[0] - x[1] + j[0], j[1] = 1+float64(1.5*d*d), -1.5*d*d + j[2], j[3] = -1.5*d*d, 1+float64(1.5*d*d) + } + sysJacRuby := "->(x) { d = x[0] - x[1]; [1 + 1.5 * d * d, -1.5 * d * d, -1.5 * d * d, 1 + 1.5 * d * d] }" + rr := must[*optimize.RootResult](t) + check(t, in, "Optimize.root([0, 0]) "+sysRuby, rr(optimize.Root(optimize.SystemProblem{Func: sys}, []float64{0, 0}, nil))) + check(t, in, "Optimize.fsolve([0, 0], jac: "+sysJacRuby+", xtol: 1e-10) "+sysRuby, rr(optimize.Fsolve(optimize.SystemProblem{Func: sys, Jac: sysJac}, []float64{0, 0}, &optimize.RootOptions{Xtol: 1e-10}))) + + resid := func(x, f []float64) { f[0], f[1], f[2] = x[0]-1, x[1]-2, float64(x[0]*x[1])-2.5 } + residRuby := "{ |x| [x[0] - 1, x[1] - 2, x[0] * x[1] - 2.5] }" + check(t, in, "Optimize.least_squares([0.5, 0.5]) "+residRuby, must[*optimize.LSResult](t)(optimize.LeastSquares(optimize.LSProblem{Func: resid, M: 3}, []float64{0.5, 0.5}, optimize.TRF, nil))) + check(t, in, "Optimize.least_squares([0.5, 0.5], m: 3, method: :lm, xtol: 1e-12) "+residRuby, must[*optimize.LSResult](t)(optimize.LeastSquares(optimize.LSProblem{Func: resid, M: 3}, []float64{0.5, 0.5}, optimize.LM, &optimize.LSOptions{Xtol: 1e-12}))) + residJac := func(x, j []float64) { j[0], j[1], j[2], j[3], j[4], j[5] = 1, 0, 0, 1, x[1], x[0] } + check(t, in, "Optimize.least_squares([0.5, 0.5], jac: ->(x) { [1, 0, 0, 1, x[1], x[0]] }) "+residRuby, must[*optimize.LSResult](t)(optimize.LeastSquares(optimize.LSProblem{Func: resid, Jac: residJac, M: 3}, []float64{0.5, 0.5}, optimize.TRF, nil))) + check(t, in, "Optimize.leastsq([0.5, 0.5], ftol: 1e-10) "+residRuby, must[*optimize.LeastsqResult](t)(optimize.Leastsq(optimize.LSProblem{Func: resid, M: 3}, []float64{0.5, 0.5}, &optimize.LeastsqOptions{Ftol: 1e-10}))) + + xd, _ := nd.FromData([]float64{0, 1, 2, 3, 4}, 5) + yd, _ := nd.FromData([]float64{1.1, 2.6, 7.6, 20.0, 54.9}, 5) + model := func(x float64, p []float64) float64 { return p[0] * math.Exp(p[1]*x) } + check(t, in, "Optimize.curve_fit([0, 1, 2, 3, 4], NDArray.from([1.1, 2.6, 7.6, 20.0, 54.9], [5]), p0: [1, 1]) { |x, a, b| a * Math.exp(b * x) }", + must[*optimize.CurveFitResult](t)(optimize.CurveFit(model, xd, yd, []float64{1, 1}, nil))) + check(t, in, "Optimize.curve_fit([0, 1, 2, 3, 4], [1.1, 2.6, 7.6, 20.0, 54.9], p0: [1, 1], bounds: [[0, 10], [0, 2]], method: :trf) { |x, a, b| a * Math.exp(b * x) }", + must[*optimize.CurveFitResult](t)(optimize.CurveFit(model, xd, yd, []float64{1, 1}, &optimize.CurveFitOptions{Bounds: []optimize.Bound{{Lo: 0, Hi: 10}, {Lo: 0, Hi: 2}}, Method: optimize.TRF}))) + + aub, _ := nd.FromData([]float64{1, 1, 1, 3}, 2, 2) + bub, _ := nd.FromData([]float64{4, 6}, 2) + check(t, in, "Optimize.linprog([-1, -2], a_ub: NDArray.from([1, 1, 1, 3], [2, 2]), b_ub: [4, 6], bounds: [[0, nil], [0, nil]], method: :interior_point)", + must[*optimize.LPResult](t)(optimize.Linprog([]float64{-1, -2}, optimize.LPConstraints{AUb: aub, BUb: bub, Bounds: []optimize.Bound{{Lo: 0, Hi: math.Inf(1)}, {Lo: 0, Hi: math.Inf(1)}}}, &optimize.LPOptions{Method: optimize.LPInteriorPoint}))) + check(t, in, "Optimize.linprog([-1, -2], a_ub: NDArray.from([1, 1, 1, 3], [2, 2]), b_ub: [4, 6])", + must[*optimize.LPResult](t)(optimize.Linprog([]float64{-1, -2}, optimize.LPConstraints{AUb: aub, BUb: bub}, nil))) + + check(t, in, "Optimize.brute([[-2, 2], [-1, 3, 0.5]], ns: 9) "+rosenRuby, + must[*optimize.BruteResult](t)(optimize.Brute(rosen, []optimize.Range{{Lo: -2, Hi: 2}, {Lo: -1, Hi: 3, Step: 0.5}}, &optimize.BruteOptions{Ns: 9}))) + check(t, in, "Optimize.fminbound(0, 1) { |x| (x - 2) * (x - 2) }", must[*optimize.ScalarResult](t)(optimize.Fminbound(sq, 0, 1, 1e-5, 500))) + check(t, in, "Optimize.fminbound(0, 3, xtol: 1e-8, maxfun: 50) { |x| (x - 2) * (x - 2) }", must[*optimize.ScalarResult](t)(optimize.Fminbound(sq, 0, 3, 1e-8, 50))) + xs, fs, nfev, err := optimize.Bracket(sq, 0, 1) + if err != nil { + t.Fatal(err) + } + check(t, in, "Optimize.bracket(0, 1) { |x| (x - 2) * (x - 2) }", []any{xs[:], fs[:], nfev}) + check(t, in, "Optimize.approx_fprime([-1.2, 1], 1e-7) "+rosenRuby, optimize.ApproxFprime(rosen, []float64{-1.2, 1}, 1e-7)) +} diff --git a/reflect.go b/reflect.go index 6d8ba2c..15540ce 100644 --- a/reflect.go +++ b/reflect.go @@ -236,7 +236,7 @@ func invoke(fn reflect.Value, c *rb.Call, args []rb.Value) (any, error) { } in = append(in, reflect.ValueOf(scalarBlock(b, &blockErr))) case t == generatorType: - g, err := generator(kw["random_state"]) + g, err := generator(kw["random_state"].Go()) if err != nil { return nil, err } @@ -304,8 +304,8 @@ func wrongArgs(given int, ft reflect.Type) error { // generator reads random_state: nil (a generator seeded by the system), an // Integer seed, or a Stats::RNG. -func generator(v rb.Value) (*random.Generator, error) { - switch x := v.Go().(type) { +func generator(v any) (*random.Generator, error) { + switch x := v.(type) { case nil: return nil, nil case int64: @@ -316,7 +316,7 @@ func generator(v rb.Value) (*random.Generator, error) { case *random.Generator: return x, nil } - return nil, typeError(v.Go(), "a seed or a Stats::RNG") + return nil, typeError(v, "a seed or a Stats::RNG") } // scalarBlock is a block as a func(float64) float64. A Ruby exception in the diff --git a/special_test.go b/special_test.go new file mode 100644 index 0000000..978936d --- /dev/null +++ b/special_test.go @@ -0,0 +1,117 @@ +package ruby + +import ( + "fmt" + "reflect" + "strings" + "testing" + + "github.com/go-numerics/special" +) + +// specialArgs are arguments in each function's domain: reals and orders. +func specialArgs(ft reflect.Type, real float64) ([]reflect.Value, []string) { + vals := make([]reflect.Value, ft.NumIn()) + src := make([]string, ft.NumIn()) + for i := range vals { + switch ft.In(i).Kind() { + case reflect.Float64: + x := real + 0.25*float64(i) + vals[i], src[i] = reflect.ValueOf(x), fmt.Sprint(x) + case reflect.Int: + vals[i], src[i] = reflect.ValueOf(2+i), fmt.Sprint(2+i) + case reflect.Int64: + vals[i], src[i] = reflect.ValueOf(int64(5-i)), fmt.Sprint(5-i) + case reflect.Slice: + vals[i], src[i] = reflect.ValueOf([]float64{0.5, 1.5, -1}), "[0.5, 1.5, -1]" + } + } + return vals, src +} + +// Every function of Special is go-numerics/special's on the same inputs; a +// ufunc also elementwise over an Array and an NDArray, with broadcasting. +func TestSpecialMatchesGo(t *testing.T) { + in := newInterp(t) + for _, f := range specialFuncs { + fn := reflect.ValueOf(f.fn) + ft := fn.Type() + name := "Special." + snakeName(f.name) + switch { + case ft == reflect.TypeOf(special.Softmax): + check(t, in, name+"([0.5, 1.5, -1])", fn.Call([]reflect.Value{reflect.Zero(ft.In(0)), reflect.ValueOf([]float64{0.5, 1.5, -1})})[0].Interface()) + continue + case !isUfunc(ft): + vals, src := specialArgs(ft, 0.7) + out := fn.Call(vals) + var want any = out[0].Interface() + if len(out) == 2 { + want = []any{out[0].Interface(), out[1].Interface()} + } + check(t, in, name+"("+strings.Join(src, ", ")+")", want) + continue + } + for _, x := range []float64{0.3, 0.7, 1.9, 3.5} { + vals, src := specialArgs(ft, x) + check(t, in, name+"("+strings.Join(src, ", ")+")", fn.Call(vals)[0].Float()) + } + // Elementwise: the first real parameter an Array, then an NDArray of + // shape (3, 1) against a second real of shape (2,) when there is one. + first := -1 + reals := 0 + for i := range ft.NumIn() { + if ft.In(i).Kind() == reflect.Float64 { + if first < 0 { + first = i + } + reals++ + } + } + xs := []float64{0.3, 0.7, 1.9} + want := make([]float64, len(xs)) + srcList := make([]string, ft.NumIn()) + for k, x := range xs { + vals, src := specialArgs(ft, 0.6) + vals[first] = reflect.ValueOf(x) + want[k] = fn.Call(vals)[0].Float() + copy(srcList, src) + } + srcList[first] = "[0.3, 0.7, 1.9]" + check(t, in, name+"("+strings.Join(srcList, ", ")+")", want) + srcList[first] = "NDArray.from([0.3, 0.7, 1.9], [3, 1])" + if reals == 1 { + check(t, in, name+"("+strings.Join(srcList, ", ")+")", vecShape(want, 3, 1)) + continue + } + second := -1 + for i := first + 1; i < ft.NumIn(); i++ { + if ft.In(i).Kind() == reflect.Float64 { + second = i + break + } + } + ys := []float64{0.45, 1.2} + grid := make([]float64, 0, 6) + for _, x := range xs { + for _, y := range ys { + vals, _ := specialArgs(ft, 0.6) + vals[first], vals[second] = reflect.ValueOf(x), reflect.ValueOf(y) + grid = append(grid, fn.Call(vals)[0].Float()) + } + } + srcList[second] = "NDArray.from([0.45, 1.2], [2])" + check(t, in, name+"("+strings.Join(srcList, ", ")+")", vecShape(grid, 3, 2)) + } +} + +func TestSpecialErrors(t *testing.T) { + in := newInterp(t) + evalErr(t, in, "Special.gamma", "ArgumentError", "wrong number of arguments (given 0, expected 1)") + evalErr(t, in, "Special.gamma(1, x: 2)", "ArgumentError", "unknown keyword: :x") + evalErr(t, in, "Special.gamma('a')", "TypeError", "no implicit conversion of String into Float") + evalErr(t, in, "Special.gamma(['a'])", "TypeError", "no implicit conversion of String into Float") + evalErr(t, in, "Special.beta(NDArray.zeros(2), NDArray.zeros(3))", "ArgumentError", "broadcast") + evalErr(t, in, "Special.softmax", "ArgumentError", "wrong number of arguments") + evalErr(t, in, "Special.softmax('a')", "TypeError", "Array of Floats") + evalErr(t, in, "Special.jn(1.5, 1.0)", "TypeError", "into Integer") +} diff --git a/stats.go b/stats.go index d2a007d..f46d1db 100644 --- a/stats.go +++ b/stats.go @@ -171,6 +171,13 @@ var elementwise = map[string]bool{ "SF": true, "LogSF": true, "PPF": true, "ISF": true, } +func init() { + converters[generatorType] = func(v any) (reflect.Value, error) { + g, err := generator(v) + return reflect.ValueOf(g), err + } +} + func registerStats(in *rb.Interp) error { d := &definer{in: in} m := d.module("Stats") @@ -478,7 +485,7 @@ func bootstrap(c *rb.Call) (any, error) { } } kw, rest := split(c.Kwargs, "random_state") - g, err := generator(kw["random_state"]) + g, err := generator(kw["random_state"].Go()) if err != nil { return nil, err } diff --git a/stats_test.go b/stats_test.go new file mode 100644 index 0000000..ee12939 --- /dev/null +++ b/stats_test.go @@ -0,0 +1,336 @@ +package ruby + +import ( + "fmt" + "reflect" + "sort" + "strings" + "testing" + + rb "github.com/go-embedded-ruby/ruby" + nd "github.com/go-ndarray/ndarray" + "github.com/go-ndarray/ndarray/random" + "github.com/go-statistics/stats" +) + +var ( + sampleX = []float64{2.1, 3.4, 1.9, 5.6, 4.4, 3.3, 2.8, 4.9, 3.7, 2.2} + sampleY = []float64{1.2, 2.9, 2.2, 4.8, 4.1, 2.5, 3.1, 3.9, 3.0, 1.8} + sampleZ = []float64{3.1, 4.2, 3.9, 5.0, 4.8, 4.4, 3.6, 5.2, 4.1, 3.3} +) + +// Markers for the data arguments of a statsFuncs case. +type dataX struct{} +type dataY struct{} +type dataZ struct{} + +// statsCases call each function of statsFuncs: rubyArgs is what follows the +// data in Ruby, goArgs the Go arguments with the data as markers, opts the Go +// option struct the Ruby keywords stand for (nil: none). +var statsCases = []struct { + name, ruby string + goArgs []any +}{ + {"mean", "X", []any{dataX{}, (*stats.MeanOptions)(nil)}}, + {"mean", "X, weights: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], ddof: 1, omit_nan: true", []any{dataX{}, &stats.MeanOptions{Weights: []float64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, Ddof: 1, OmitNaN: true}}}, + {"average", "X, weights: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]", []any{dataX{}, &stats.MeanOptions{Weights: []float64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}}}}, + {"var", "X, ddof: 1", []any{dataX{}, &stats.MeanOptions{Ddof: 1}}}, + {"std", "X", []any{dataX{}, (*stats.MeanOptions)(nil)}}, + {"median", "X", []any{dataX{}, (*stats.MedianOptions)(nil)}}, + {"gmean", "X", []any{dataX{}, (*stats.PowerMeanOptions)(nil)}}, + {"hmean", "X, weights: [1, 1, 1, 1, 1, 2, 2, 2, 2, 2]", []any{dataX{}, &stats.PowerMeanOptions{Weights: []float64{1, 1, 1, 1, 1, 2, 2, 2, 2, 2}}}}, + {"pmean", "X, 3", []any{dataX{}, 3.0, (*stats.PowerMeanOptions)(nil)}}, + {"skew", "X, unbiased: true", []any{dataX{}, &stats.ShapeOptions{Unbiased: true}}}, + {"kurtosis", "X, pearson: true", []any{dataX{}, &stats.ShapeOptions{Pearson: true}}}, + {"moment", "X, 3, center: 0.5", []any{dataX{}, 3, &stats.MomentOptions{Center: ptr(0.5)}}}, + {"sem", "X, ddof: 0.0", []any{dataX{}, &stats.DdofOptions{Ddof: ptr(0.0)}}}, + {"variation", "X", []any{dataX{}, (*stats.DdofOptions)(nil)}}, + {"iqr", "X, range: [10, 90], method: :nearest", []any{dataX{}, &stats.IQROptions{Range: [2]float64{10, 90}, Method: stats.QuantileNearest}}}, + {"median_abs_deviation", "X, scale: 1.4826", []any{dataX{}, &stats.MADOptions{Scale: 1.4826}}}, + {"quantile", "X, 0.3", []any{dataX{}, 0.3, (*stats.QuantileOptions)(nil)}}, + {"quantiles", "X, [0.1, 0.5, 0.9], method: :weibull", []any{dataX{}, []float64{0.1, 0.5, 0.9}, &stats.QuantileOptions{Method: stats.QuantileWeibull}}}, + {"percentile", "X, 75", []any{dataX{}, 75.0, (*stats.QuantileOptions)(nil)}}, + {"percentiles", "X, [25, 75]", []any{dataX{}, []float64{25, 75}, (*stats.QuantileOptions)(nil)}}, + {"tmean", "X, limits: {lower: 2, upper: 5}", []any{dataX{}, &stats.TrimmedOptions{Limits: &stats.Limits{Lower: 2, Upper: 5}}}}, + {"tvar", "X", []any{dataX{}, (*stats.TrimmedOptions)(nil)}}, + {"tstd", "X", []any{dataX{}, (*stats.TrimmedOptions)(nil)}}, + {"tsem", "X", []any{dataX{}, (*stats.TrimmedOptions)(nil)}}, + {"tmin", "X, 2.5, exclusive: true", []any{dataX{}, 2.5, &stats.BoundOptions{Exclusive: true}}}, + {"tmax", "X, 4.5", []any{dataX{}, 4.5, (*stats.BoundOptions)(nil)}}, + {"trim_mean", "X, 0.1", []any{dataX{}, 0.1}}, + {"trimboth", "X, 0.1", []any{dataX{}, 0.1}}, + {"trim1", "X, 0.2, :left", []any{dataX{}, 0.2, stats.TailLeft}}, + {"zscore", "X", []any{dataX{}, (*stats.DdofOptions)(nil)}}, + {"rankdata", "X, :min", []any{dataX{}, stats.RankMin}}, + {"describe", "X", []any{dataX{}, (*stats.ShapeOptions)(nil)}}, + {"mode", "[1, 2, 2, 3]", []any{[]float64{1, 2, 2, 3}, (*stats.NaNOptions)(nil)}}, + {"entropy", "[0.2, 0.3, 0.5], base: 2", []any{[]float64{0.2, 0.3, 0.5}, &stats.EntropyOptions{Base: 2}}}, + {"differential_entropy", "X", []any{dataX{}, (*stats.DiffEntropyOptions)(nil)}}, + {"histogram", "X, bins: 4, range: [1, 6], density: true", []any{dataX{}, &stats.HistogramOptions{Bins: 4, Range: &[2]float64{1, 6}, Density: true}}}, + {"histogram_bin_edges", "X, rule: :sturges", []any{dataX{}, &stats.HistogramOptions{Rule: stats.BinsSturges}}}, + {"cumfreq", "X, num_bins: 4", []any{dataX{}, &stats.FreqOptions{NumBins: 4}}}, + {"relfreq", "X", []any{dataX{}, (*stats.FreqOptions)(nil)}}, + {"zmap", "X, Y", []any{dataX{}, dataY{}, (*stats.DdofOptions)(nil)}}, + {"binned_statistic", "X, Y, statistic: :median, bins: 3", []any{dataX{}, dataY{}, &stats.BinnedOptions{Statistic: stats.BinMedian, Bins: 3}}}, + {"ttest_1samp", "X, 3.0, alternative: :greater", []any{dataX{}, 3.0, &stats.TTestOptions{Alternative: stats.Greater}}}, + {"ttest_ind", "X, Y, welch: true, trim: 0.1", []any{dataX{}, dataY{}, &stats.TTestIndOptions{Welch: true, Trim: 0.1}}}, + {"ttest_rel", "X, Y", []any{dataX{}, dataY{}, (*stats.TTestOptions)(nil)}}, + {"mannwhitneyu", "X, Y, method: :asymptotic", []any{dataX{}, dataY{}, &stats.MannWhitneyUOptions{Method: stats.MethodAsymptotic}}}, + {"wilcoxon", "X, Y, alternative: :less", []any{dataX{}, dataY{}, &stats.WilcoxonOptions{Alternative: stats.Less}}}, + {"ks_2samp", "X, Y", []any{dataX{}, dataY{}, (*stats.KSOptions)(nil)}}, + {"shapiro", "X", []any{dataX{}}}, + {"normaltest", "X + Y", []any{append(append([]float64{}, sampleX...), sampleY...)}}, + {"skewtest", "X + Y", []any{append(append([]float64{}, sampleX...), sampleY...), (*stats.SkewTestOptions)(nil)}}, + {"kurtosistest", "X + Y + Z", []any{append(append(append([]float64{}, sampleX...), sampleY...), sampleZ...), (*stats.SkewTestOptions)(nil)}}, + {"jarque_bera", "X", []any{dataX{}}}, + {"anderson", "X, :expon", []any{dataX{}, stats.ADExpon}}, + {"pearsonr", "X, Y", []any{dataX{}, dataY{}, (*stats.CorrelationOptions)(nil)}}, + {"spearmanr", "X, Y, alternative: :greater", []any{dataX{}, dataY{}, &stats.CorrelationOptions{Alternative: stats.Greater}}}, + {"kendalltau", "X, Y, variant: :c", []any{dataX{}, dataY{}, &stats.KendallOptions{Variant: stats.TauC}}}, + {"linregress", "X, Y", []any{dataX{}, dataY{}, (*stats.CorrelationOptions)(nil)}}, + {"chisquare", "[10, 20, 30], expected: [20, 20, 20]", []any{[]float64{10, 20, 30}, &stats.ChiSquareOptions{Expected: []float64{20, 20, 20}}}}, + {"power_divergence", "[10, 20, 30], 0.5", []any{[]float64{10, 20, 30}, 0.5, (*stats.ChiSquareOptions)(nil)}}, + {"f_oneway", "X, Y, Z, welch: true", []any{&stats.FOnewayOptions{Welch: true}, dataX{}, dataY{}, dataZ{}}}, + {"kruskal", "X, Y, Z", []any{dataX{}, dataY{}, dataZ{}}}, + {"levene", "X, Y, Z, center: :mean", []any{&stats.LeveneOptions{Center: stats.CenterMean}, dataX{}, dataY{}, dataZ{}}}, + {"bartlett", "X, Y", []any{dataX{}, dataY{}}}, +} + +func ptr[T any](v T) *T { return &v } + +// goData replaces the data markers with the samples, as slices or arrays. +func goData(args []any, arrays bool) []reflect.Value { + out := make([]reflect.Value, len(args)) + for i, a := range args { + var s []float64 + switch a.(type) { + case dataX: + s = sampleX + case dataY: + s = sampleY + case dataZ: + s = sampleZ + default: + if f, ok := a.([]float64); ok && arrays && i == 0 { + s = f + } else { + out[i] = reflect.ValueOf(a) + continue + } + } + if arrays { + out[i] = reflect.ValueOf(vec(s)) + } else { + out[i] = reflect.ValueOf(append([]float64(nil), s...)) + } + } + return out +} + +// Every data function of Stats, on Arrays and on NDArrays, gives the go-statistics +// function's result for the same inputs, bit for bit. +func TestStatsFunctionsMatchGo(t *testing.T) { + in := newInterp(t) + byName := map[string]int{} + for i, f := range statsFuncs { + byName[f.name] = i + } + covered := map[string]bool{} + for _, arrays := range []bool{false, true} { + for _, c := range statsCases { + f := statsFuncs[byName[c.name]] + fn := reflect.ValueOf(f.overS) + if arrays { + fn = reflect.ValueOf(f.overA) + } + args := goData(c.goArgs, arrays) + for i := range args { + if !args[i].IsValid() || (args[i].Kind() == reflect.Pointer && args[i].IsNil()) { + args[i] = reflect.Zero(fn.Type().In(min(i, fn.Type().NumIn()-1))) + } + } + res := fn.Call(args) + if err, _ := res[len(res)-1].Interface().(error); err != nil { + t.Fatalf("%s (Go): %v", c.name, err) + } + want := res[0].Interface() + if len(res) == 3 { + want = []any{res[0].Interface(), res[1].Interface()} + } + src := "Stats." + c.name + "(" + c.ruby + ")" + if arrays { + src = strings.NewReplacer("X", "XA", "Y", "YA", "Z", "ZA").Replace(src) + src = strings.ReplaceAll(src, "XA + YA + ZA", "NDArray.concatenate([XA, YA, ZA])") + src = strings.ReplaceAll(src, "XA + YA", "NDArray.concatenate([XA, YA])") + } + setSamples(t, in) + check(t, in, src, want) + covered[c.name] = true + } + } + for _, f := range statsFuncs { + if !covered[f.name] && f.name != "ks_1samp" { + t.Errorf("Stats.%s has no comparison with Go", f.name) + } + } +} + +// setSamples defines the samples as Ruby constants: X, Y, Z as Arrays and +// XA, YA, ZA as NDArrays. +func setSamples(t *testing.T, in *rb.Interp) { + t.Helper() + for name, s := range map[string][]float64{"X": sampleX, "Y": sampleY, "Z": sampleZ} { + lit := fmt.Sprint(s) + lit = strings.ReplaceAll(lit[1:len(lit)-1], " ", ", ") + src := fmt.Sprintf("%s = [%s].freeze unless defined?(%s)\n%sA = NDArray.from(%s, [%d]) unless defined?(%sA)", name, lit, name, name, name, len(s), name) + if _, err := in.Eval(src, "samples.rb", 1); err != nil { + t.Fatal(err) + } + } +} + +// distParams are valid parameters for each distribution: the Ruby keywords +// and the Go field values (loc and scale shifted off their defaults, so the +// defaults are tested by the "default" case below and not hidden). +var distParams = map[string]map[string]any{ + "Beta": {"A": 2.5, "B": 3.5, "Loc": 0.1, "Scale": 2.0}, + "Cauchy": {"Loc": 0.5, "Scale": 1.5}, + "Chi": {"Df": 3.0, "Loc": 0.1, "Scale": 1.2}, + "ChiSquared": {"Df": 4.0, "Loc": 0.1, "Scale": 1.2}, + "Exponential": {"Loc": 0.2, "Scale": 1.7}, + "F": {"Dfn": 5.0, "Dfd": 7.0, "Loc": 0.0, "Scale": 1.0}, + "Gamma": {"A": 2.2, "Loc": 0.1, "Scale": 1.3}, + "Gumbel": {"Loc": 0.4, "Scale": 1.1}, + "HalfNormal": {"Loc": 0.1, "Scale": 1.4}, + "InverseGamma": {"A": 3.1, "Loc": 0.0, "Scale": 1.2}, + "Laplace": {"Loc": 0.3, "Scale": 0.8}, + "LogNormal": {"S": 0.6, "Loc": 0.0, "Scale": 1.5}, + "Logistic": {"Loc": 0.2, "Scale": 0.9}, + "Lomax": {"C": 2.5, "Loc": 0.0, "Scale": 1.1}, + "NoncentralChiSquared": {"Df": 3.0, "Nc": 1.5, "Loc": 0.0, "Scale": 1.0}, + "Normal": {"Loc": 0.5, "Scale": 2.0}, + "Pareto": {"B": 2.7, "Loc": 0.0, "Scale": 1.0}, + "Rayleigh": {"Loc": 0.1, "Scale": 1.3}, + "Reciprocal": {"A": 0.5, "B": 4.0, "Loc": 0.0, "Scale": 1.0}, + "StudentT": {"Df": 6.0, "Loc": 0.2, "Scale": 1.1}, + "Triangular": {"C": 0.3, "Loc": 0.5, "Scale": 3.0}, + "Uniform": {"Loc": 1.0, "Scale": 4.0}, + "VonMises": {"Kappa": 2.0, "Loc": 0.1, "Scale": 1.0}, + "Weibull": {"C": 1.8, "Loc": 0.0, "Scale": 1.4}, + "Bernoulli": {"P": 0.3, "Loc": 0.0}, + "Binomial": {"N": int64(12), "P": 0.4, "Loc": 1.0}, + "DiscreteUniform": {"Low": int64(2), "High": int64(9), "Loc": 0.0}, + "Geometric": {"P": 0.35, "Loc": 0.0}, + "Hypergeometric": {"Total": int64(20), "Good": int64(7), "Draws": int64(12), "Loc": 0.0}, + "NegativeBinomial": {"N": 3.0, "P": 0.4, "Loc": 0.0}, + "Poisson": {"Mu": 3.3, "Loc": 1.0}, + "Zipf": {"A": 2.5, "Loc": 0.0}, +} + +// rubyKw spells Go field values as Ruby keywords. +func rubyKw(params map[string]any) string { + var parts []string + for _, k := range sortedFields(params) { + parts = append(parts, fmt.Sprintf("%s: %v", snakeName(k), params[k])) + } + return strings.Join(parts, ", ") +} + +func sortedFields(params map[string]any) []string { + keys := make([]string, 0, len(params)) + for k := range params { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + +// goDist is the Go distribution value with the parameters set directly. +func goDist(zero any, params map[string]any) reflect.Value { + v := reflect.New(reflect.TypeOf(zero)).Elem() + for k, p := range params { + v.FieldByName(k).Set(reflect.ValueOf(p)) + } + return v +} + +// Every distribution method called from Ruby is the Go method on the same +// distribution, bit for bit, for a Float, an Array and an NDArray argument. +func TestDistributionsMatchGo(t *testing.T) { + in := newInterp(t) + points := []float64{-0.5, 0, 0.3, 1, 2.5, 4} + probs := []float64{0.01, 0.25, 0.5, 0.9} + for _, ds := range distributions { + params, ok := distParams[ds.name] + if !ok { + t.Fatalf("no parameters for %s", ds.name) + } + g := goDist(ds.zero, params) + ctor := fmt.Sprintf("Stats::%s.new(%s)", ds.name, rubyKw(params)) + eval(t, in, "d = "+ctor) + if got := eval(t, in, "d"); !same(got, g.Interface()) { + t.Fatalf("%s: built %#v, Go %#v", ctor, got, g.Interface()) + } + call := func(name string, args ...any) any { + out := g.MethodByName(name).Call(valuesOf(args)) + if len(out) == 2 { + return []any{out[0].Interface(), out[1].Interface()} + } + return out[0].Interface() + } + for name := range elementwise { + if !g.MethodByName(name).IsValid() { + continue + } + xs := points + if name == "PPF" || name == "ISF" { + xs = probs + } + f := g.MethodByName(name).Interface().(func(float64) float64) + want := make([]float64, len(xs)) + for i, x := range xs { + want[i] = f(x) + check(t, in, fmt.Sprintf("d.%s(%v)", snakeName(name), x), want[i]) + } + arr := fmt.Sprintf("%v", xs) + arr = strings.ReplaceAll(arr[1:len(arr)-1], " ", ", ") + check(t, in, fmt.Sprintf("d.%s([%s])", snakeName(name), arr), want) + check(t, in, fmt.Sprintf("d.%s(NDArray.from([%s], [2, %d]))", snakeName(name), arr, len(xs)/2), vecShape(want, 2, len(xs)/2)) + } + for _, name := range []string{"Mean", "Var", "Std", "Skew", "Kurtosis", "Median", "Entropy", "Support"} { + check(t, in, "d."+snakeName(name), call(name)) + } + check(t, in, "d.moment(2)", call("Moment", 2)) + check(t, in, "d.interval(0.9)", call("Interval", 0.9)) + rvs := g.MethodByName("Rvs").Call([]reflect.Value{reflect.ValueOf(random.DefaultRNG(7)), reflect.ValueOf(2), reflect.ValueOf(3)}) + check(t, in, "d.rvs(2, 3, random_state: 7)", rvs[0].Interface()) + check(t, in, "d.rvs([2, 3], random_state: Stats::RNG.new(7))", rvs[0].Interface()) + if ds.fitS != nil { + sample := "[0.4, 1.3, 2.2, 0.9, 1.7, 3.1, 0.6, 1.1, 2.5, 1.9]" + x := []float64{0.4, 1.3, 2.2, 0.9, 1.7, 3.1, 0.6, 1.1, 2.5, 1.9} + fit := reflect.ValueOf(ds.fitS).Call([]reflect.Value{reflect.ValueOf(x), reflect.ValueOf((*stats.FitOptions)(nil))}) + if err, _ := fit[1].Interface().(error); err == nil { + check(t, in, fmt.Sprintf("Stats::%s.fit(%s)", ds.name, sample), fit[0].Interface()) + fitA := reflect.ValueOf(ds.fitA).Call([]reflect.Value{reflect.ValueOf(vec(x)), reflect.ValueOf((*stats.FitOptions)(nil))}) + check(t, in, fmt.Sprintf("Stats::%s.fit(NDArray.from(%s, [10]))", ds.name, sample), fitA[0].Interface()) + } + fixed := reflect.ValueOf(ds.fitS).Call([]reflect.Value{reflect.ValueOf(x), reflect.ValueOf(&stats.FitOptions{Fixed: map[string]float64{"Loc": 0}})}) + if err, _ := fixed[1].Interface().(error); err == nil { + check(t, in, fmt.Sprintf("Stats::%s.fit(%s, fixed: {loc: 0})", ds.name, sample), fixed[0].Interface()) + } + } + } +} + +func valuesOf(args []any) []reflect.Value { + out := make([]reflect.Value, len(args)) + for i, a := range args { + out[i] = reflect.ValueOf(a) + } + return out +} + +func vecShape(x []float64, shape ...int) *nd.Array { + a, _ := nd.FromData(append([]float64(nil), x...), shape...) + return a +} From e807935b9f6d6be4970c9b29517a67bd7933e72c Mon Sep 17 00:00:00 2001 From: tannevaled Date: Sat, 17 Oct 2026 22:43:07 +0200 Subject: [PATCH 04/20] Callbacks abort the Go algorithm on a Ruby error; error, edge and object tests; 100% of the bindings covered --- callbacks.go | 80 ++++++------- conv.go | 2 +- conv_test.go | 200 +++++++++++++++++++++++++++++++ errors_test.go | 320 +++++++++++++++++++++++++++++++++++++++++++++++++ integrate.go | 3 + optimize.go | 37 +++++- reflect.go | 55 ++------- special.go | 3 + stats.go | 30 ++--- stats_test.go | 59 +++++++++ 10 files changed, 676 insertions(+), 113 deletions(-) create mode 100644 conv_test.go create mode 100644 errors_test.go diff --git a/callbacks.go b/callbacks.go index 71bf7c1..dea3c51 100644 --- a/callbacks.go +++ b/callbacks.go @@ -1,7 +1,6 @@ package ruby import ( - "math" "reflect" rb "github.com/go-embedded-ruby/ruby" @@ -26,17 +25,8 @@ func procCaller(v rb.Value) (caller, error) { return func(args ...any) (rb.Value, error) { return v.Call("call", args...) }, nil } -// guard keeps the first error a callback met. A Go algorithm cannot be -// stopped from inside its callback, so after an error the callback answers -// NaN (or leaves its output alone) without calling Ruby again, the algorithm -// winds down, and the binding returns the error instead of its result. -type guard struct{ err error } - -func (g *guard) fail(err error) { - if g.err == nil { - g.err = err - } -} +// guard builds the callbacks of one call of a binding. +type guard struct{} // makeFunc builds a Go func of type t that calls Ruby. Its float64 // parameters reach Ruby as Floats and its []float64 parameters as NDArrays @@ -67,51 +57,55 @@ func (g *guard) makeFunc(t reflect.Type, call caller, splat bool) reflect.Value } } } + r, err := call(args...) + abortOn(err) if outBuf { out := in[len(in)-1].Interface().([]float64) - if g.err == nil { - g.fill(out, call, args) + v, err := toFloats(r.Go()) + abortOn(err) + if len(v) != len(out) { + abortOn(argError("the block returned %d values, expected %d", len(v), len(out))) } + copy(out, v) return nil } - f := math.NaN() - if g.err == nil { - f = g.scalar(call, args) - } + f, err := toFloat(r.Go()) + abortOn(err) return []reflect.Value{reflect.ValueOf(f)} }) } -func (g *guard) scalar(call caller, args []any) float64 { - r, err := call(args...) - if err != nil { - g.fail(err) - return math.NaN() - } - f, err := toFloat(r.Go()) +// callbackAbort carries an error out of a callback, through the Go algorithm +// that called it, to the binding (see guarded). +type callbackAbort struct{ err error } + +// abortOn stops the running Go algorithm when err is not nil: its callback +// cannot return an error, and answering NaN instead would let an adaptive +// algorithm run on for as long as it takes to give up — a triple quadrature +// whose block raises would call it a billion times. +func abortOn(err error) { if err != nil { - g.fail(err) - return math.NaN() + panic(callbackAbort{err}) } - return f } -func (g *guard) fill(out []float64, call caller, args []any) { - r, err := call(args...) - if err != nil { - g.fail(err) - return - } - v, err := toFloats(r.Go()) - if err != nil { - g.fail(err) - return - } - if len(v) != len(out) { - g.fail(argError("the block returned %d values, expected %d", len(v), len(out))) - return +// guarded wraps a binding whose Go algorithm calls back into Ruby: an error +// in a callback ends the algorithm and becomes the binding's error. Any other +// panic — a break or a throw in the block, unwinding to its Ruby frame — goes +// on. +func guarded(f rb.Func) rb.Func { + return func(c *rb.Call) (res any, err error) { + defer func() { + if r := recover(); r != nil { + a, ok := r.(callbackAbort) + if !ok { + panic(r) + } + res, err = nil, a.err + } + }() + return f(c) } - copy(out, v) } // fn is makeFunc for a func type known at compile time. diff --git a/conv.go b/conv.go index 0c23f99..0104713 100644 --- a/conv.go +++ b/conv.go @@ -412,7 +412,7 @@ func contains(list []string, s string) bool { var snakeOverrides = map[string]string{ "PValue": "pvalue", "ZStatistic": "zstatistic", "NObs": "nobs", "OmitNaN": "omit_nan", "LogPDF": "logpdf", "LogCDF": "logcdf", "LogSF": "logsf", "LogPMF": "logpmf", - "DF": "df", "NaN": "nan", + "DF": "df", "NaN": "nan", "IntegrateBox1D": "integrate_box_1d", } // snakeName is the Ruby name of a Go identifier: PValue is pvalue, NumIter diff --git a/conv_test.go b/conv_test.go new file mode 100644 index 0000000..640e736 --- /dev/null +++ b/conv_test.go @@ -0,0 +1,200 @@ +package ruby + +import ( + "math" + "math/big" + "reflect" + "strings" + "testing" + + rb "github.com/go-embedded-ruby/ruby" + nd "github.com/go-ndarray/ndarray" + "github.com/go-statistics/stats" +) + +func TestDescribe(t *testing.T) { + in := rb.New() + obj, _ := in.Eval("Object.new", "x.rb", 1) + a, _ := nd.Zeros(1) + for x, want := range map[any]string{ + nil: "nil", true: "true", int64(1): "Integer", 1.5: "Float", complex(1, 2): "Complex", + "s": "String", rb.Symbol("s"): "Symbol", a: "NDArray", obj: "Object", struct{}{}: "struct {}", + } { + if got := describe(x); got != want { + t.Errorf("describe(%#v) = %q, want %q", x, got, want) + } + } + for x, want := range map[string]any{"Integer": big.NewInt(1), "Rational": big.NewRat(1, 2), "Array": []any{}, "Hash": map[any]any{}} { + if got := describe(want); got != x { + t.Errorf("describe(%#v) = %q", want, got) + } + } +} + +func TestToFloatAndFriends(t *testing.T) { + if f, err := toFloat(new(big.Int).Lsh(big.NewInt(1), 70)); err != nil || f != math.Ldexp(1, 70) { + t.Fatal(f, err) + } + if f, err := toFloat(big.NewRat(1, 4)); err != nil || f != 0.25 { + t.Fatal(f, err) + } + if _, err := toFloat("x"); err == nil { + t.Fatal("toFloat accepted a String") + } + if _, err := toInt(1.5); err == nil { + t.Fatal("toInt accepted a Float") + } + if _, err := toFloats([]any{1.0, "x"}); err == nil { + t.Fatal("toFloats accepted a String element") + } + if _, err := toFloats(3.0); err == nil { + t.Fatal("toFloats accepted a Float") + } + if _, err := ndArray("x"); err == nil { + t.Fatal("ndArray accepted a String") + } + if vec(nil) != nil { + t.Fatal("vec(nil) is not nil") + } + if _, err := toString(1.0); err == nil { + t.Fatal("toString accepted a Float") + } + if s, _ := toString(rb.Symbol("a")); s != "a" { + t.Fatal(s) + } +} + +type convTarget struct { + F32 float32 + I8 int8 + B bool + S string + Names []string + Pair [2]float64 + M map[string]float64 + Opt *float64 + Lim *stats.Limits + A *nd.Array + Alt stats.Alternative + Inner stats.Limits + Fn func() +} + +func TestConvert(t *testing.T) { + typ := reflect.TypeFor[convTarget]() + field := func(name string) reflect.Type { f, _ := typ.FieldByName(name); return f.Type } + ok := []struct { + field string + in any + want any + }{ + {"F32", 1.5, float32(1.5)}, + {"I8", int64(-3), int8(-3)}, + {"B", true, true}, + {"S", rb.Symbol("x"), "x"}, + {"Names", []any{"a", rb.Symbol("b")}, []string{"a", "b"}}, + {"Pair", []any{1.0, int64(2)}, [2]float64{1, 2}}, + {"M", map[any]any{rb.Symbol("a"): 1.0}, map[string]float64{"a": 1}}, + {"Opt", 0.5, ptr(0.5)}, + {"Opt", nil, (*float64)(nil)}, + {"Lim", map[any]any{"lower": int64(1)}, &stats.Limits{Lower: 1}}, + {"Alt", "two-sided", stats.TwoSided}, + {"Alt", rb.Symbol("greater"), stats.Greater}, + {"Inner", stats.Limits{Upper: 2}, stats.Limits{Upper: 2}}, + } + for _, c := range ok { + v, err := convert(c.in, field(c.field)) + if err != nil { + t.Fatalf("%s: %v", c.field, err) + } + if !reflect.DeepEqual(v.Interface(), c.want) { + t.Errorf("%s: %#v, want %#v", c.field, v.Interface(), c.want) + } + } + bad := []struct { + field string + in any + msg string + }{ + {"B", int64(1), "true or false"}, + {"Names", "a", "into Array"}, + {"Names", []any{1.0}, "into String"}, + {"Pair", []any{1.0}, "expected an Array of 2 elements, got 1"}, + {"M", []any{}, "into Hash"}, + {"M", map[any]any{1.0: 1.0}, "into String"}, + {"M", map[any]any{"a": "b"}, "into Float"}, + {"Opt", "x", "into Float"}, + {"Lim", map[any]any{1.0: 1.0}, "into String"}, + {"Lim", map[any]any{"nope": 1.0}, "unknown key nope for Limits (known: lower, upper"}, + {"Lim", map[any]any{"lower": "x"}, "into Float"}, + {"Alt", 1.0, "into Symbol"}, + {"Alt", rb.Symbol("sideways"), `unknown alternative "sideways" (expected one of :two_sided, :less, :greater)`}, + {"Inner", 1.0, "into stats.Limits"}, + {"Fn", nil, "into func()"}, + } + for _, c := range bad { + _, err := convert(c.in, field(c.field)) + if err == nil || !strings.Contains(err.Error(), c.msg) { + t.Errorf("%s from %#v: error %v, want one containing %q", c.field, c.in, err, c.msg) + } + } + // convertMap needs String keys. + if _, err := convertMap(map[any]any{}, reflect.TypeFor[map[int]float64]()); err == nil { + t.Fatal("convertMap accepted an int key type") + } +} + +func TestOptions(t *testing.T) { + in := rb.New() + v := func(src string) rb.Value { x, _ := in.Eval(src, "x.rb", 1); return x } + var o stats.MeanOptions + if err := options(&o, map[string]rb.Value{"ddof": v("1"), "omit_nan": v("true")}); err != nil || o.Ddof != 1 || !o.OmitNaN { + t.Fatal(o, err) + } + err := options(&o, map[string]rb.Value{"nope": v("1")}) + if err == nil || !strings.Contains(err.Error(), "unknown keyword: :nope (known: weights, ddof, omit_nan)") { + t.Fatal(err) + } + if err := options(&o, map[string]rb.Value{"ddof": v("'x'")}); err == nil { + t.Fatal("options accepted a String ddof") + } +} + +func TestSnakeName(t *testing.T) { + for in, want := range map[string]string{ + "PValue": "pvalue", "TwoSided": "two_sided", "XArray": "x_array", "BetaIncCInv": "beta_inc_c_inv", + "I0e": "i0e", "Log1pExp": "log1p_exp", "LogPDF": "logpdf", "HessInvArray": "hess_inv_array", "Xlog1py": "xlog1py", + } { + if got := snakeName(in); got != want { + t.Errorf("snakeName(%q) = %q, want %q", in, got, want) + } + } +} + +func TestPlainAndConvertible(t *testing.T) { + for typ, want := range map[reflect.Type]bool{ + reflect.TypeFor[float64](): true, + reflect.TypeFor[[]float64](): true, + reflect.TypeFor[map[string][]int](): true, + reflect.TypeFor[map[string]func()](): false, + reflect.TypeFor[func()](): false, + reflect.TypeFor[chan int](): false, + reflect.TypeFor[error](): false, + reflect.TypeFor[*stats.Normal](): true, + reflect.TypeFor[[]func(float64) int](): false, + } { + if got := plain(typ); got != want { + t.Errorf("plain(%v) = %v", typ, got) + } + } + for typ, want := range map[reflect.Type]bool{ + reflect.TypeFor[func(float64) (float64, error)](): true, + reflect.TypeFor[func(func(float64) float64) float64](): true, + reflect.TypeFor[func(func() int) float64](): false, + reflect.TypeFor[func() func()](): false, + } { + if got := convertible(typ, 0); got != want { + t.Errorf("convertible(%v) = %v", typ, got) + } + } +} diff --git a/errors_test.go b/errors_test.go new file mode 100644 index 0000000..ad30d75 --- /dev/null +++ b/errors_test.go @@ -0,0 +1,320 @@ +package ruby + +import ( + "reflect" + "strings" + "testing" + + rb "github.com/go-embedded-ruby/ruby" + nd "github.com/go-ndarray/ndarray" + "github.com/go-statistics/stats" +) + +// errorCases are calls that must raise: wrong counts, wrong types, unknown +// keywords, errors of the Go library, and errors in the block. +var errorCases = []struct{ src, class, msg string }{ + // Arity and blocks. + {"Optimize.minimize", "ArgumentError", "wrong number of arguments (given 0, expected 1)"}, + {"Optimize.minimize([1])", "ArgumentError", "no block given"}, + {"Optimize.brentq(1, 2, 3) { }", "ArgumentError", "given 3, expected 2"}, + {"Integrate.fixed_quad(0) { }", "ArgumentError", "given 1, expected 2..3"}, + {"Integrate.fixed_quad(0, 1)", "ArgumentError", "no block given"}, + // Argument types. + {"Optimize.minimize('x') { }", "TypeError", "an Array of Floats"}, + {"Optimize.brentq('a', 2) { }", "TypeError", "into Float"}, + {"Optimize.newton('a') { }", "TypeError", "into Float"}, + {"Optimize.root('x') { }", "TypeError", "an Array of Floats"}, + {"Optimize.fsolve('x') { }", "TypeError", "an Array of Floats"}, + {"Optimize.least_squares('x') { }", "TypeError", "an Array of Floats"}, + {"Optimize.leastsq('x') { }", "TypeError", "an Array of Floats"}, + {"Optimize.curve_fit('x', [1]) { }", "TypeError", "an Array of Floats"}, + {"Optimize.curve_fit([1], 'y') { }", "TypeError", "an Array of Floats"}, + {"Optimize.curve_fit([1], [1], p0: 'p') { }", "TypeError", "an Array of Floats"}, + {"Optimize.linprog('c')", "TypeError", "an Array of Floats"}, + {"Optimize.linprog([1], a_ub: 'a')", "TypeError", "an Array of Floats"}, + {"Optimize.linprog([1], bounds: [[1]])", "TypeError", "a bound [lo, hi]"}, + {"Optimize.linprog([1], bounds: [['a', 1]])", "TypeError", "into Float"}, + {"Optimize.linprog([1], bounds: [[1, 'b']])", "TypeError", "into Float"}, + {"Optimize.brute('r') { }", "TypeError", "into Array"}, + {"Optimize.brute([[1]]) { }", "TypeError", "a range [lo, hi] or [lo, hi, step]"}, + {"Optimize.brute([[1, 'b']]) { }", "TypeError", "into Float"}, + {"Optimize.fminbound(0, 'a') { }", "TypeError", "into Float"}, + {"Optimize.fminbound(0, 1, xtol: 'a') { }", "TypeError", "into Float"}, + {"Optimize.fminbound(0, 1, maxfun: 1.5) { }", "TypeError", "into Integer"}, + {"Optimize.bracket(0, 'a') { }", "TypeError", "into Float"}, + {"Optimize.approx_fprime('x', 1) { }", "TypeError", "an Array of Floats"}, + {"Optimize.approx_fprime([1], 'e') { }", "TypeError", "into Float"}, + {"Optimize.root_scalar(method: 1) { }", "TypeError", "into String"}, + {"Optimize.least_squares([1], m: 'm') { }", "TypeError", "into Integer"}, + {"Integrate.quad('a', 1) { }", "TypeError", "into Float"}, + {"Integrate.dblquad('a', 1, 0, 1) { }", "TypeError", "into Float"}, + {"Integrate.dblquad(0, 1, 'g', 1) { }", "TypeError", "a Float or a Proc"}, + {"Integrate.dblquad(0, 1, 0, 'h') { }", "TypeError", "a Float or a Proc"}, + {"Integrate.tplquad('a', 1, 0, 1, 0, 1) { }", "TypeError", "into Float"}, + {"Integrate.tplquad(0, 1, 'g', 1, 0, 1) { }", "TypeError", "a Float or a Proc"}, + {"Integrate.tplquad(0, 1, 0, 1, 'q', 1) { }", "TypeError", "a Float or a Proc"}, + {"Integrate.nquad('r') { }", "TypeError", "into Array"}, + {"Integrate.nquad([[1]]) { }", "TypeError", "a range [lo, hi]"}, + {"Integrate.fixed_quad('a', 1) { }", "TypeError", "into Float"}, + {"Integrate.fixed_quad(0, 1, 1.5) { }", "TypeError", "into Integer"}, + {"Integrate.romberg('a', 1) { }", "TypeError", "into Float"}, + {"Integrate.solve_ivp('s', [1]) { }", "TypeError", "into Array"}, + {"Integrate.solve_ivp([0, 1], 'y') { }", "TypeError", "an Array of Floats"}, + {"Integrate.odeint('y', [1]) { }", "TypeError", "an Array of Floats"}, + {"Integrate.odeint([1], 't') { }", "TypeError", "an Array of Floats"}, + {"Integrate.trapezoid", "ArgumentError", "wrong number of arguments"}, + {"Integrate.trapezoid('y')", "TypeError", "an Array of Floats"}, + {"Integrate.trapezoid([1], x: 'x')", "TypeError", "an Array of Floats"}, + {"Integrate.trapezoid([1], dx: 'x')", "TypeError", "into Float"}, + {"Integrate.trapezoid([1], axis: 'x')", "TypeError", "into Integer"}, + {"Integrate.trapezoid([1], nope: 1)", "ArgumentError", "unknown keyword: :nope (known: x, dx, axis)"}, + {"Integrate.cumulative_trapezoid", "ArgumentError", "wrong number of arguments"}, + {"Integrate.cumulative_trapezoid([1], nope: 1)", "ArgumentError", "unknown keyword: :nope"}, + {"Integrate.cumulative_trapezoid([1], initial: 1)", "TypeError", "true or false"}, + {"Integrate.cumulative_trapezoid('y')", "TypeError", "an Array of Floats"}, + {"Integrate.romb", "ArgumentError", "wrong number of arguments"}, + {"Integrate.romb([1], nope: 1)", "ArgumentError", "unknown keyword"}, + {"Integrate.romb('y')", "TypeError", "an Array of Floats"}, + {"Integrate.gauss_legendre", "ArgumentError", "wrong number of arguments"}, + {"Integrate.gauss_legendre(1.5)", "TypeError", "into Integer"}, + // Keywords. + {"Optimize.minimize([1], nope: 1) { |x| 0 }", "ArgumentError", "unknown keyword: :nope"}, + {"Optimize.minimize([1], method: :nope) { |x| 0 }", "ArgumentError", "unknown method"}, + {"Optimize.minimize([1], jac: 1) { |x| 0 }", "TypeError", "a Proc"}, + {"Optimize.minimize([1], jac: BasicObject.new) { |x| 0 }", "TypeError", "a Proc"}, + {"Optimize.minimize_scalar(method: :nope) { |x| 0 }", "ArgumentError", "unknown scalar_method"}, + {"Optimize.minimize_scalar(nope: 1) { |x| 0 }", "ArgumentError", "unknown keyword"}, + {"Optimize.minimize_scalar(1) { |x| 0 }", "ArgumentError", "given 1, expected 0"}, + {"Optimize.brentq(0, 1, nope: 1) { |x| 0 }", "ArgumentError", "unknown keyword"}, + {"Optimize.newton(1.0, fprime: 1) { |x| 0 }", "TypeError", "a Proc"}, + {"Optimize.newton(1.0, fprime2: 1) { |x| 0 }", "TypeError", "a Proc"}, + {"Optimize.newton(1.0, nope: 1) { |x| 0 }", "ArgumentError", "unknown keyword"}, + {"Optimize.root_scalar(fprime: 1) { |x| 0 }", "TypeError", "a Proc"}, + {"Optimize.root_scalar(nope: 1) { |x| 0 }", "ArgumentError", "unknown keyword"}, + {"Optimize.root([1], jac: 1) { |x| [0] }", "TypeError", "a Proc"}, + {"Optimize.root([1], nope: 1) { |x| [0] }", "ArgumentError", "unknown keyword"}, + {"Optimize.least_squares([1], jac: 1) { |x| [0] }", "TypeError", "a Proc"}, + {"Optimize.least_squares([1], method: :nope) { |x| [0] }", "ArgumentError", "unknown ls_method"}, + {"Optimize.least_squares([1], nope: 1) { |x| [0] }", "ArgumentError", "unknown keyword"}, + {"Optimize.leastsq([1], jac: 1) { |x| [0] }", "TypeError", "a Proc"}, + {"Optimize.leastsq([1], nope: 1) { |x| [0] }", "ArgumentError", "unknown keyword"}, + {"Optimize.curve_fit([1], [1], nope: 1) { |x, a| a }", "ArgumentError", "unknown keyword"}, + {"Optimize.linprog([1], nope: 1)", "ArgumentError", "unknown keyword"}, + {"Optimize.brute([[0, 1]], nope: 1) { |x| 0 }", "ArgumentError", "unknown keyword"}, + {"Optimize.fminbound(0, 1, nope: 1) { |x| 0 }", "ArgumentError", "unknown keyword: :nope (known: xtol, maxfun)"}, + {"Integrate.quad(0, 1, nope: 1) { |x| x }", "ArgumentError", "unknown keyword"}, + {"Integrate.dblquad(0, 1, 0, 1, nope: 1) { |y, x| x }", "ArgumentError", "unknown keyword"}, + {"Integrate.tplquad(0, 1, 0, 1, 0, 1, nope: 1) { |z, y, x| x }", "ArgumentError", "unknown keyword"}, + {"Integrate.nquad([[0, 1]], nope: 1) { |x| 0 }", "ArgumentError", "unknown keyword"}, + {"Integrate.romberg(0, 1, nope: 1) { |x| x }", "ArgumentError", "unknown keyword"}, + {"Integrate.solve_ivp([0, 1], [1], nope: 1) { |t, y| y }", "ArgumentError", "unknown keyword"}, + {"Integrate.odeint([1], [0, 1], nope: 1) { |t, y| y }", "ArgumentError", "unknown keyword"}, + // Errors of the Go library. + {"Optimize.minimize([]) { |x| 0 }", "ArgumentError", "optimize"}, + {"Optimize.minimize_scalar(method: :bounded) { |x| 0 }", "ArgumentError", "optimize"}, + {"Optimize.brentq(0, 1) { |x| 1 }", "ArgumentError", "optimize"}, + {"Optimize.newton(1.0, max_iter: -1) { |x| x }", "ArgumentError", "optimize"}, + {"Optimize.root_scalar(method: 'nope') { |x| x }", "ArgumentError", "optimize"}, + {"Optimize.root([]) { |x| [] }", "ArgumentError", "optimize"}, + {"Optimize.least_squares([], m: 0) { |x| [] }", "ArgumentError", "optimize"}, + {"Optimize.leastsq([], m: 0) { |x| [] }", "ArgumentError", "optimize"}, + {"Optimize.curve_fit([1, 2], [1]) { |x, a| a }", "ArgumentError", "optimize"}, + {"Optimize.linprog([])", "ArgumentError", "optimize"}, + {"Optimize.brute([]) { |x| 0 }", "ArgumentError", "optimize"}, + {"Optimize.fminbound(1, 0) { |x| x }", "ArgumentError", "optimize"}, + {"Optimize.bracket(0, 1) { |x| -x }", "ArgumentError", "optimize"}, + {"Integrate.quad(0, 1, limit: -1) { |x| x }", "ArgumentError", "integrate"}, + {"Integrate.romberg(0.01, 1, div_max: 1) { |x| Math.sin(1 / x) }", "ArgumentError", "DivMax exceeded"}, + {"Integrate.solve_ivp([0, 1], []) { |t, y| y }", "TypeError", "nil"}, + {"Integrate.odeint([1], []) { |t, y| y }", "ArgumentError", "integrate"}, + {"Integrate.trapezoid([1, 2], x: [1])", "ArgumentError", "integrate"}, + {"Integrate.simpson(NDArray.zeros(2, 3), axis: 5)", "ArgumentError", "integrate"}, + {"Integrate.cumulative_trapezoid([1, 2], x: [1])", "ArgumentError", "integrate"}, + {"Integrate.cumulative_trapezoid(NDArray.zeros(2, 3), axis: 5)", "ArgumentError", "integrate"}, + {"Integrate.romb([1, 2, 3, 4])", "ArgumentError", "integrate"}, + {"Integrate.fixed_quad(0, 1, 0) { |x| x }", "ArgumentError", "integrate"}, + // Errors in the block, and what it returns. + {"Optimize.minimize([1]) { |x| raise IOError, 'in block' }", "IOError", "in block"}, + {"Optimize.minimize([1]) { |x| 'not a number' }", "TypeError", "into Float"}, + {"Optimize.root([1]) { |x| raise IOError, 'in block' }", "IOError", "in block"}, + {"Optimize.root([1]) { |x| 'v' }", "TypeError", "an Array of Floats"}, + {"Optimize.root([1, 2]) { |x| [1] }", "ArgumentError", "the block returned 1 values, expected 2"}, + {"Optimize.least_squares([1]) { |x| raise IOError, 'first call' }", "IOError", "first call"}, + {"Optimize.least_squares([1]) { |x| 'v' }", "TypeError", "an Array of Floats"}, + {"Integrate.quad(0, 1) { |x| raise IOError, 'q' }", "IOError", "q"}, + {"Integrate.dblquad(0, 1, ->(x) { raise IOError, 'lim' }, 1) { |y, x| x }", "IOError", "lim"}, + {"Integrate.fixed_quad(0, 1) { |x| raise IOError, 'fq' }", "IOError", "fq"}, + {"Integrate.romberg(0, 1) { |x| raise IOError, 'r' }", "IOError", "r"}, + {"Integrate.solve_ivp([0, 1], [1]) { |t, y| raise IOError, 'ivp' }", "IOError", "ivp"}, + {"Integrate.odeint([1], [0, 1]) { |t, y| raise IOError, 'ode' }", "IOError", "ode"}, + {"Integrate.nquad([[0, 1]]) { |x| raise IOError, 'n' }", "IOError", "n"}, + {"Integrate.tplquad(0, 1, 0, 1, 0, 1) { |z, y, x| raise IOError, 't' }", "IOError", "t"}, + {"Optimize.minimize_scalar { |x| raise IOError, 'ms' }", "IOError", "ms"}, + {"Optimize.brentq(0, 1) { |x| raise IOError, 'bq' }", "IOError", "bq"}, + {"Optimize.newton(1.0) { |x| raise IOError, 'nw' }", "IOError", "nw"}, + {"Optimize.root_scalar(bracket: [0, 1]) { |x| raise IOError, 'rs' }", "IOError", "rs"}, + {"Optimize.leastsq([1], m: 1) { |x| raise IOError, 'lq' }", "IOError", "lq"}, + {"Optimize.curve_fit([1, 2], [1, 2], p0: [1]) { |x, a| raise IOError, 'cf' }", "IOError", "cf"}, + {"Optimize.brute([[0, 1]]) { |x| raise IOError, 'br' }", "IOError", "br"}, + {"Optimize.fminbound(0, 1) { |x| raise IOError, 'fb' }", "IOError", "fb"}, + {"Optimize.bracket(0, 1) { |x| raise IOError, 'bk' }", "IOError", "bk"}, + {"Optimize.approx_fprime([1], 1e-6) { |x| raise IOError, 'af' }", "IOError", "af"}, + {"Optimize.fsolve([1]) { |x| raise IOError, 'fs' }", "IOError", "fs"}, + {"Optimize.brute([[0, 1]], workers: 2) { |x| 0 }", "ArgumentError", "workers: must be 1"}, + {"Optimize.curve_fit([1], [1], p0: [1], workers: 2) { |x, a| a }", "ArgumentError", "workers: must be 1"}, +} + +func TestErrors(t *testing.T) { + in := newInterp(t) + for _, c := range errorCases { + evalErr(t, in, c.src, c.class, c.msg) + } +} + +// A break in the block leaves the Go algorithm and is the call's value, as in +// Ruby. +func TestBreakOutOfTheBlock(t *testing.T) { + in := newInterp(t) + for _, src := range []string{ + "Optimize.minimize([1.0]) { |x| break :early }", + "Optimize.brute([[0, 1]]) { |x| break :early }", // from Brute's own goroutine + "catch(:early) { Optimize.brute([[0, 1]]) { |x| throw :early, :early } }", + } { + if got := eval(t, in, src); got != rb.Symbol("early") { + t.Fatalf("%s = %#v", src, got) + } + } +} + +// Register fails, and says why, when a name it defines is taken. +func TestRegisterConflicts(t *testing.T) { + for _, pre := range []string{"Stats = 1", "Special = 1", "Optimize = 1", "Integrate = 1", + "module Stats; Normal = 1; end", "module Stats; RNG = 1; end", "module Stats; GaussianKDE = 1; end", + "module Stats; MultivariateNormal = 1; end", "module Integrate; ODESolution = 1; end", "module Stats; TestResult = 1; end"} { + in := rb.New() + if _, err := in.Eval(pre, "pre.rb", 1); err != nil { + t.Fatal(err) + } + err := Register(in) + if err == nil || !strings.Contains(err.Error(), "already defined") { + t.Errorf("%s: Register = %v", pre, err) + } + } +} + +// statsErrorCases are the Stats calls that must raise. +var statsErrorCases = []struct{ src, class, msg string }{ + {"Stats.mean", "ArgumentError", "wrong number of arguments (given 0, expected 1)"}, + {"Stats.mean([1], [2])", "ArgumentError", "wrong number of arguments (given 2, expected 1)"}, + {"Stats.mean('x')", "TypeError", "an Array of Floats"}, + {"Stats.mean([1], nope: 1)", "ArgumentError", "unknown keyword: :nope"}, + {"Stats.mean([1], ddof: 'x')", "TypeError", "into Float"}, + {"Stats.trim_mean([1, 2], 0.1, extra: 1)", "ArgumentError", "unknown keyword: :extra"}, + {"Stats.mean([1], random_state: 1)", "ArgumentError", "unknown keyword: :random_state"}, + {"Stats.mean([])", "ArgumentError", "stats"}, + {"Stats::RNG.new(1).normal", "ArgumentError", "wrong number of arguments (given 0, expected 2+)"}, + {"Stats.f_oneway([1, 2], 'x')", "TypeError", "an Array of Floats"}, + {"Stats.ks_1samp([1, 2])", "ArgumentError", "no block given"}, + {"Stats.ks_1samp([1, 2]) { |x| raise IOError, 'cdf' }", "IOError", "cdf"}, + {"Stats.ks_1samp([1, 2]) { |x| 'p' }", "TypeError", "into Float"}, + {"Stats.fisher_exact([[1, 2], [3]])", "ArgumentError", "expected an Array of 2 elements"}, + {"Stats::Normal.new(1, 2, 3)", "ArgumentError", "wrong number of arguments (given 3, expected 0..2)"}, + {"Stats::Normal.new(1, loc: 2)", "ArgumentError", "loc given twice"}, + {"Stats::Normal.new('a')", "TypeError", "into Float"}, + {"Stats::Normal.new(nope: 1)", "ArgumentError", "unknown keyword: :nope (known: loc, scale)"}, + {"Stats::Normal.new(scale: 'a')", "TypeError", "into Float"}, + {"Stats::Gamma.new", "ArgumentError", "missing keyword: :a"}, + {"Stats::Normal.new(scale: -1)", "ArgumentError", "stats"}, + {"Stats::Normal.new.pdf", "ArgumentError", "wrong number of arguments (given 0, expected 1)"}, + {"Stats::Normal.new.pdf('x')", "TypeError", "into Float or NDArray"}, + {"Stats::Normal.new.pdf(['x'])", "TypeError", "into Float"}, + {"Stats::Normal.new.mean(1)", "ArgumentError", "wrong number of arguments (given 1, expected 0)"}, + {"Stats::Normal.new.loc(1)", "ArgumentError", "wrong number of arguments (given 1, expected 0)"}, + {"Stats::Normal.new.moment", "ArgumentError", "wrong number of arguments (given 0, expected 1)"}, + {"Stats::Normal.new.moment(1.5)", "TypeError", "into Integer"}, + {"Stats::Normal.new.rvs(2, random_state: -1)", "ArgumentError", "non-negative"}, + {"Stats::Normal.new.rvs(2, random_state: 'x')", "TypeError", "a seed or a Stats::RNG"}, + {"Stats::Normal.new.rvs('x')", "TypeError", "into Integer"}, + {"Stats::Normal.new.rvs(-1, random_state: 1)", "ArgumentError", ""}, + {"Stats::Normal.fit", "ArgumentError", "wrong number of arguments"}, + {"Stats::Normal.fit([1, 2], nope: 1)", "ArgumentError", "unknown keyword: :nope (known: fixed)"}, + {"Stats::Normal.fit([1, 2], fixed: 1)", "TypeError", "into Hash"}, + {"Stats::Normal.fit([1, 2], fixed: {1 => 2})", "TypeError", "into String"}, + {"Stats::Normal.fit([1, 2], fixed: {nope: 2})", "ArgumentError", "unknown parameter nope (known: loc, scale)"}, + {"Stats::Normal.fit([1, 2], fixed: {loc: 'x'})", "TypeError", "into Float"}, + {"Stats::Normal.fit('x')", "TypeError", "an Array of Floats"}, + {"Stats::Normal.fit([])", "ArgumentError", "stats"}, + {"Stats::RNG.new(1, 2)", "ArgumentError", "wrong number of arguments"}, + {"Stats::RNG.new(-1)", "ArgumentError", "non-negative"}, + {"Stats::RNG.new(1.5)", "ArgumentError", "non-negative"}, + {"Stats::GaussianKDE.new([1, 2, 3]).evaluate", "ArgumentError", "wrong number of arguments"}, + {"Stats::GaussianKDE.new([1, 2, 3]).evaluate('x')", "TypeError", "an Array of Floats"}, + {"Stats::GaussianKDE.new(NDArray.from([1, 2, 3, 4, 5, 6.5], [2, 3])).evaluate([1, 2, 3])", "ArgumentError", "stats"}, + {"Stats.bootstrap", "ArgumentError", "wrong number of arguments"}, + {"Stats.bootstrap([[1, 2]])", "ArgumentError", "no block given"}, + {"Stats.bootstrap('x') { }", "TypeError", "an Array of samples"}, + {"Stats.bootstrap(['x']) { }", "TypeError", "an Array of Floats"}, + {"Stats.bootstrap([[1, 2]], random_state: 'x') { }", "TypeError", "a seed"}, + {"Stats.bootstrap([[1, 2]], nope: 1) { }", "ArgumentError", "unknown keyword"}, + {"Stats.bootstrap([[1, 2, 3]], n_resamples: 9) { |s| raise IOError, 'stat' }", "IOError", "stat"}, + {"Stats.bootstrap([[1, 2, 3]], n_resamples: 9) { |s| 'x' }", "TypeError", "into Float"}, + {"Stats.bootstrap([[1]]) { |s| s.mean }", "ArgumentError", "stats"}, + {"Stats::MultivariateNormal.new('m', NDArray.eye(2))", "TypeError", "an Array of Floats"}, + {"Stats::MultivariateNormal.new([0], NDArray.eye(2))", "ArgumentError", "stats"}, + {"Stats::MultivariateNormal.new([0, 0], NDArray.eye(2), nope: 1)", "ArgumentError", "unknown keyword"}, + {"Stats::MultivariateNormal.new([0, 0])", "ArgumentError", "wrong number of arguments (given 1, expected 2)"}, + {"Stats::MultivariateNormal.new([0, 0], NDArray.eye(2), 3)", "ArgumentError", "wrong number of arguments (given 3, expected 2)"}, + {"Stats::Normal.new.pdf(1, nope: 2)", "ArgumentError", "unknown keyword: :nope"}, +} + +func TestStatsErrors(t *testing.T) { + in := newInterp(t) + for _, c := range statsErrorCases { + evalErr(t, in, c.src, c.class, c.msg) + } +} + +func TestBlockRequired(t *testing.T) { + in := newInterp(t) + for _, src := range []string{ + "Integrate.quad(0, 1)", "Integrate.dblquad(0, 1, 0, 1)", "Integrate.tplquad(0, 1, 0, 1, 0, 1)", "Integrate.nquad([[0, 1]])", + "Integrate.romberg(0, 1)", "Integrate.solve_ivp([0, 1], [1])", "Integrate.odeint([1], [0, 1])", + "Optimize.newton(1.0)", "Optimize.minimize_scalar", "Optimize.root([1])", "Optimize.fsolve([1])", "Optimize.least_squares([1])", + "Optimize.leastsq([1])", "Optimize.curve_fit([1], [1])", "Optimize.brute([[0, 1]])", "Optimize.fminbound(0, 1)", + "Optimize.bracket(0, 1)", "Optimize.approx_fprime([1], 1e-6)", "Optimize.root_scalar", "Optimize.brentq(0, 1)", + } { + evalErr(t, in, src, "ArgumentError", "no block given") + } + evalErr(t, in, "Optimize.linprog", "ArgumentError", "wrong number of arguments") +} + +func TestReflectEdges(t *testing.T) { + if r, err := results(nil); r != nil || err != nil { + t.Fatal(r, err) + } + if got := out(reflect.ValueOf(stats.Less)); got != rb.Symbol("less") { + t.Fatalf("enum out = %#v", got) + } + if got := out(reflect.ValueOf(stats.Alternative(99))); got != stats.Alternative(99) { + t.Fatalf("unknown enum out = %#v", got) + } + if got := out(reflect.ValueOf((*stats.Normal)(nil))); got != nil { + t.Fatalf("nil pointer out = %#v", got) + } +} + +func TestStatsOddments(t *testing.T) { + in := newInterp(t) + for src, want := range map[string]string{ + "Stats::RNG.new.inspect": "#", + "Stats::MultivariateNormal.new([0, 0], NDArray.eye(2)).inspect": "#", + "Stats::GaussianKDE.new([1, 2, 3.5]).inspect": "#", + } { + if got := eval(t, in, src); got != want { + t.Errorf("%s = %#v", src, got) + } + } + if _, ok := eval(t, in, "Stats::RNG.new.random(2)").(*nd.Array); !ok { + t.Fatal("RNG.new.random is not an NDArray") + } +} diff --git a/integrate.go b/integrate.go index 13b3721..aff75a7 100644 --- a/integrate.go +++ b/integrate.go @@ -30,6 +30,9 @@ import ( func registerIntegrate(in *rb.Interp) error { d := &definer{in: in} m := d.module("Integrate") + if d.err != nil { + return d.err + } d.structClass(m, "QuadResult", integrate.QuadResult{}) d.structClass(m, "Solution", integrate.Solution{}) d.structClass(m, "OdeintInfo", integrate.OdeintInfo{}) diff --git a/optimize.go b/optimize.go index acd112b..ff71689 100644 --- a/optimize.go +++ b/optimize.go @@ -31,6 +31,9 @@ import ( func registerOptimize(in *rb.Interp) error { d := &definer{in: in} m := d.module("Optimize") + if d.err != nil { + return d.err + } for _, r := range []struct { name string sample any @@ -122,6 +125,10 @@ func toRange(v any) (optimize.Range, error) { return r, nil } +// errWorkers refuses workers: above 1: a Ruby block runs on one goroutine +// at a time, and the Go side would call it from several at once. +var errWorkers = rb.NewError("ArgumentError", "workers: must be 1: a Ruby block cannot be called from several goroutines at once") + // optsFrom fills an option struct from keywords, nil when there are none. func optsFrom[O any](kw map[string]rb.Value) (*O, error) { if len(kw) == 0 { @@ -137,9 +144,6 @@ func optsFrom[O any](kw map[string]rb.Value) (*O, error) { // finish returns a result, or the error of the callback first, then the Go // error. func finish(g *guard, res any, err error) (any, error) { - if g.err != nil { - return nil, g.err - } if err != nil { return nil, goError(err) } @@ -440,6 +444,9 @@ func curveFit(c *rb.Call) (any, error) { if err != nil { return nil, err } + if o != nil && o.Workers > 1 { + return nil, errWorkers + } g := &guard{} model := g.makeFunc(reflect.TypeFor[func(x float64, p []float64) float64](), call, true).Interface().(func(float64, []float64) float64) res, err := optimize.CurveFit(model, x, y, p0, o) @@ -480,8 +487,30 @@ func brute(c *rb.Call) (any, error) { if err != nil { return nil, err } + if o != nil && o.Workers > 1 { + return nil, errWorkers + } + // Brute evaluates the grid on a goroutine of its own, where neither an + // error nor a break may unwind: what the block raised, or how it left, is + // kept and replayed here, on the caller's goroutine, once Brute is done. g := &guard{} - res, err := optimize.Brute(fn[func([]float64) float64](g, call), rv.Interface().([]optimize.Range), o) + objective := fn[func([]float64) float64](g, call) + var pending any + safe := func(x []float64) (f float64) { + if pending != nil { + return math.NaN() + } + defer func() { + if r := recover(); r != nil { + pending, f = r, math.NaN() + } + }() + return objective(x) + } + res, err := optimize.Brute(safe, rv.Interface().([]optimize.Range), o) + if pending != nil { + panic(pending) + } return finish(g, res, err) } diff --git a/reflect.go b/reflect.go index 15540ce..f5a03bf 100644 --- a/reflect.go +++ b/reflect.go @@ -3,7 +3,6 @@ package ruby import ( "errors" "fmt" - "math" "reflect" "strings" @@ -29,10 +28,9 @@ func (d *definer) module(name string) *rb.Module { return m } +// class defines a class inside parent; nil (and the error kept) when the +// name is taken by something else. func (d *definer) class(parent *rb.Module, name string) *rb.Module { - if parent == nil { - return nil - } c, err := parent.DefineClass(name, nil) d.keep(err) return c @@ -44,19 +42,9 @@ func (d *definer) keep(err error) { } } -// fn defines a module function; it does nothing once an error is kept. -func (d *definer) fn(m *rb.Module, name string, f rb.Func) { - if m != nil { - m.DefineSingletonMethod(name, f) - } -} - -// method defines an instance method; it does nothing once an error is kept. -func (d *definer) method(m *rb.Module, name string, f rb.Func) { - if m != nil { - m.DefineMethod(name, f) - } -} +// fn defines a module function. +// Every binding is guarded: its Go algorithm may call back into Ruby. +func (d *definer) fn(m *rb.Module, name string, f rb.Func) { m.DefineSingletonMethod(name, guarded(f)) } // structClass defines the class name inside parent for the Go struct type of // sample (a value, not a pointer): values of the type and pointers to it @@ -137,7 +125,7 @@ func bindMethods(c *rb.Module, t reflect.Type, skip map[string]bool) []string { } name := snakeName(m.Name) names = append(names, name) - c.DefineMethod(name, func(call *rb.Call) (any, error) { + c.DefineMethod(name, guarded(func(call *rb.Call) (any, error) { recv := reflect.ValueOf(call.Self.Go()) if recv.Kind() != reflect.Pointer { p := reflect.New(recv.Type()) @@ -145,7 +133,7 @@ func bindMethods(c *rb.Module, t reflect.Type, skip map[string]bool) []string { recv = p } return invoke(recv.MethodByName(m.Name), call, call.Args) - }) + })) } return names } @@ -215,7 +203,6 @@ func invoke(fn reflect.Value, c *rb.Call, args []rb.Value) (any, error) { kw, rest := split(c.Kwargs, "random_state") in := make([]reflect.Value, 0, ft.NumIn()) pos, kwUsed := 0, len(rest) == 0 - var blockErr error for i := range ft.NumIn() { t := ft.In(i) switch { @@ -234,7 +221,7 @@ func invoke(fn reflect.Value, c *rb.Call, args []rb.Value) (any, error) { if err != nil { return nil, err } - in = append(in, reflect.ValueOf(scalarBlock(b, &blockErr))) + in = append(in, (&guard{}).makeFunc(scalarFuncType, blockCaller(b), false)) case t == generatorType: g, err := generator(kw["random_state"].Go()) if err != nil { @@ -281,11 +268,7 @@ func invoke(fn reflect.Value, c *rb.Call, args []rb.Value) (any, error) { if len(kw) > 0 { return nil, argError("unknown keyword: :random_state") } - res := fn.Call(in) - if blockErr != nil { - return nil, blockErr - } - return results(res) + return results(fn.Call(in)) } // wrongArgs is MRI's ArgumentError for a positional count that does not fit. @@ -319,26 +302,6 @@ func generator(v any) (*random.Generator, error) { return nil, typeError(v, "a seed or a Stats::RNG") } -// scalarBlock is a block as a func(float64) float64. A Ruby exception in the -// block, or a value that is not a number, is kept in *errp (the first one) and -// the function answers NaN from then on, so the Go algorithm finishes -// quickly; the binding raises *errp once it returns. -func scalarBlock(b *rb.Block, errp *error) func(float64) float64 { - return func(x float64) float64 { - if *errp != nil { - return math.NaN() - } - r, err := b.Call(x) - if err == nil { - var f float64 - if f, err = toFloat(r.Go()); err == nil { - return f - } - } - *errp = err - return math.NaN() - } -} // results converts what a Go func returned: a final error, when not nil, // raises; one other result is that value, several an Array. diff --git a/special.go b/special.go index 49b51f8..e111edb 100644 --- a/special.go +++ b/special.go @@ -21,6 +21,9 @@ import ( func registerSpecial(in *rb.Interp) error { d := &definer{in: in} m := d.module("Special") + if d.err != nil { + return d.err + } for _, f := range specialFuncs { d.fn(m, snakeName(f.name), specialFunc(f.fn)) } diff --git a/stats.go b/stats.go index f46d1db..5986947 100644 --- a/stats.go +++ b/stats.go @@ -210,6 +210,9 @@ func registerStats(in *rb.Interp) error { // of the Go type. func defineDist(d *definer, m *rb.Module, ds dist) { c := d.structClass(m, ds.name, ds.zero) + if c == nil { + return + } t := reflect.TypeOf(ds.zero) fields := readableFields(t) c.DefineSingletonMethod("new", func(call *rb.Call) (any, error) { @@ -227,6 +230,9 @@ func defineDist(d *definer, m *rb.Module, ds dist) { if err := arity(call, 1, 1); err != nil { return nil, err } + if len(call.Kwargs) > 0 { + return nil, argError("unknown keyword: :%s", sortedKeys(call.Kwargs)[0]) + } f := reflect.ValueOf(call.Self.Go()).MethodByName(name).Interface().(func(float64) float64) return ufunc(call.Args[0], f) }) @@ -383,10 +389,7 @@ func defineRNG(d *definer, m *rb.Module) { return random.DefaultRNG(uint64(seed)), nil }) for _, name := range []string{"Random", "StandardNormal", "Normal", "Uniform", "Integers", "Exponential", "StandardExponential", "Poisson", "Binomial", "Gamma", "Beta"} { - mt, ok := reflect.TypeFor[*random.Generator]().MethodByName(name) - if !ok { - panic(fmt.Sprintf("random.Generator has no %s", name)) - } + mt, _ := reflect.TypeFor[*random.Generator]().MethodByName(name) c.DefineMethod(snakeName(name), func(call *rb.Call) (any, error) { return invoke(reflect.ValueOf(call.Self.Go()).Method(mt.Index), call, call.Args) }) @@ -496,28 +499,17 @@ func bootstrap(c *rb.Call) (any, error) { return nil, err } } - var blockErr error statistic := func(s ...[]float64) float64 { - if blockErr != nil { - return 0 - } args := make([]any, len(s)) for i, x := range s { args[i] = vec(x) } r, err := b.Call(args...) - if err == nil { - var f float64 - if f, err = toFloat(r.Go()); err == nil { - return f - } - } - blockErr = err - return 0 + abortOn(err) + f, err := toFloat(r.Go()) + abortOn(err) + return f } res, err := stats.Bootstrap(samples, statistic, g, opts) - if blockErr != nil { - return nil, blockErr - } return resultOrError(res, err) } diff --git a/stats_test.go b/stats_test.go index ee12939..acebcc4 100644 --- a/stats_test.go +++ b/stats_test.go @@ -334,3 +334,62 @@ func vecShape(x []float64, shape ...int) *nd.Array { a, _ := nd.FromData(append([]float64(nil), x...), shape...) return a } + +// The Stats functions and classes not generic over the data, each against the +// Go call. +func TestStatsObjectsMatchGo(t *testing.T) { + in := newInterp(t) + setSamples(t, in) + m := func(a *nd.Array, err error) *nd.Array { + if err != nil { + t.Fatal(err) + } + return a + } + k1 := must[*stats.GaussianKDE](t)(stats.NewGaussianKDE(sampleX, nil)) + check(t, in, "Stats::GaussianKDE.new(X).evaluate([2, 3.5])", must[[]float64](t)(k1.EvaluateSlice([]float64{2, 3.5}))) + check(t, in, "Stats::GaussianKDE.new(X).call(NDArray.from([2, 3.5], [2]))", must[[]float64](t)(k1.EvaluateSlice([]float64{2, 3.5}))) + check(t, in, "Stats::GaussianKDE.new(X).factor", k1.Factor()) + check(t, in, "Stats::GaussianKDE.new(X).integrate_box_1d(1, 3)", must[float64](t)(k1.IntegrateBox1D(1, 3))) + k1s := must[*stats.GaussianKDE](t)(stats.NewGaussianKDE(sampleX, &stats.GaussianKDEOptions{Bandwidth: stats.KDEBandwidth{Rule: stats.BandwidthSilverman}})) + check(t, in, "Stats::GaussianKDE.new(X, bandwidth: {rule: :silverman}).factor", k1s.Factor()) + pts := m(nd.FromData(append(append([]float64{}, sampleX...), sampleY...), 2, 10)) + k2 := must[*stats.GaussianKDE](t)(stats.NewGaussianKDE(pts, nil)) + q := m(nd.FromData([]float64{2, 3, 2.5, 1.5}, 2, 2)) + check(t, in, "Stats::GaussianKDE.new(NDArray.concatenate([XA, YA]).reshape(2, 10)).evaluate(NDArray.from([2, 3, 2.5, 1.5], [2, 2]))", must[[]float64](t)(k2.Evaluate(q))) + + cov := m(nd.FromData([]float64{2, 0.5, 0.5, 1}, 2, 2)) + mvn := must[*stats.MultivariateNormal](t)(stats.NewMultivariateNormal([]float64{0, 1}, cov, nil)) + mvnRuby := "Stats::MultivariateNormal.new([0, 1], NDArray.from([2, 0.5, 0.5, 1], [2, 2]))" + check(t, in, mvnRuby+".logpdf([0.3, 0.2])", must[float64](t)(mvn.LogPDF([]float64{0.3, 0.2}))) + check(t, in, mvnRuby+".cdf([0.3, 0.2], rng: 5, max_pts: 20000)", must[float64](t)(mvn.CDF([]float64{0.3, 0.2}, &stats.MultivariateNormalCDFOptions{RNG: random.DefaultRNG(5), MaxPts: 20000}))) + check(t, in, mvnRuby+".rvs(3, random_state: 9)", must[*nd.Array](t)(mvn.Rvs(random.DefaultRNG(9), 3))) + dir := must[*stats.Dirichlet](t)(stats.NewDirichlet([]float64{1, 2, 3})) + check(t, in, "Stats::Dirichlet.new([1, 2, 3]).mean", dir.Mean()) + check(t, in, "Stats::Dirichlet.new([1, 2, 3]).pdf([0.2, 0.3, 0.5])", must[float64](t)(dir.PDF([]float64{0.2, 0.3, 0.5}))) + mn := must[*stats.Multinomial](t)(stats.NewMultinomial(5, []float64{0.2, 0.8})) + check(t, in, "Stats::Multinomial.new(5, [0.2, 0.8]).pmf([2, 3])", must[float64](t)(mn.PMF([]float64{2, 3}))) + + check(t, in, "Stats.chi2_contingency(NDArray.from([10, 20, 30, 40], [2, 2]))", must[stats.Chi2ContingencyResult](t)(stats.Chi2Contingency(m(nd.FromData([]float64{10, 20, 30, 40}, 2, 2)), nil))) + check(t, in, "Stats.fisher_exact([[1, 9], [11, 3]], alternative: :less)", must[stats.TestResult](t)(stats.FisherExact([2][2]int64{{1, 9}, {11, 3}}, &stats.FisherExactOptions{Alternative: stats.Less}))) + fm, fc, err := stats.FitMultivariateNormal(pts, nil) + if err != nil { + t.Fatal(err) + } + check(t, in, "Stats.fit_multivariate_normal(NDArray.concatenate([XA, YA]).reshape(2, 10))", []any{fm, fc}) + + norm := stats.Normal{Loc: 3, Scale: 1} + check(t, in, "d = Stats::Normal.new(3, 1); Stats.ks_1samp(X) { |x| d.cdf(x) }", must[stats.KSResult](t)(stats.KSTest1Samp(sampleX, norm.CDF, nil))) + check(t, in, "Stats.ks_1samp(XA, alternative: :less) { |x| d.cdf(x) }", must[stats.KSResult](t)(stats.KSTest1Samp(vec(sampleX), norm.CDF, &stats.KSOptions{Alternative: stats.Less}))) + + mean := func(s ...[]float64) float64 { return must[float64](t)(stats.Mean(s[0], nil)) } + check(t, in, "Stats.bootstrap([X], random_state: 3, n_resamples: 199, method: :percentile) { |s| Stats.mean(s) }", + must[stats.BootstrapResult](t)(stats.Bootstrap([][]float64{sampleX}, mean, random.DefaultRNG(3), &stats.BootstrapOptions{NResamples: 199, Method: stats.BootstrapPercentile}))) + check(t, in, "Stats.bootstrap([X], random_state: Stats::RNG.new(3), n_resamples: 99) { |s| Stats.mean(s) }", + must[stats.BootstrapResult](t)(stats.Bootstrap([][]float64{sampleX}, mean, random.DefaultRNG(3), &stats.BootstrapOptions{NResamples: 99}))) + + rng := random.DefaultRNG(11) + check(t, in, "r = Stats::RNG.new(11); r.normal(1, 2, 3)", must[*nd.Array](t)(rng.Normal(1, 2, 3))) + check(t, in, "r.integers(0, 10, [2, 2])", must[*nd.Array](t)(rng.Integers(0, 10, 2, 2))) + check(t, in, "r.uniform(0, 1)", must[*nd.Array](t)(rng.Uniform(0, 1))) +} From dc3c6663214ca5975e7e202197044932da2b866e Mon Sep 17 00:00:00 2001 From: tannevaled Date: Sat, 17 Oct 2026 22:44:20 +0200 Subject: [PATCH 05/20] internal/gen: tests, the generated files checked current --- internal/gen/main.go | 5 +- internal/gen/main_test.go | 127 ++++++++++++++++++++++++++++++++++++++ reflect.go | 1 - 3 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 internal/gen/main_test.go diff --git a/internal/gen/main.go b/internal/gen/main.go index f7c07ae..14ed34c 100644 --- a/internal/gen/main.go +++ b/internal/gen/main.go @@ -36,6 +36,9 @@ var packages = []struct{ path, name string }{ type constant struct{ name string } +// exit is os.Exit, a seam for tests. +var exit = os.Exit + func main() { dir := "." if len(os.Args) > 1 { @@ -43,7 +46,7 @@ func main() { } if err := writeAll(dir, moduleDir); err != nil { fmt.Fprintln(os.Stderr, "gen:", err) - os.Exit(1) + exit(1) } } diff --git a/internal/gen/main_test.go b/internal/gen/main_test.go new file mode 100644 index 0000000..41dbfb8 --- /dev/null +++ b/internal/gen/main_test.go @@ -0,0 +1,127 @@ +package main + +import ( + "errors" + "os" + "path/filepath" + "testing" +) + +// The generated files in the repository are what the generator writes now. +func TestGeneratedFilesAreCurrent(t *testing.T) { + dir := t.TempDir() + if err := writeAll(dir, moduleDir); err != nil { + t.Fatal(err) + } + for _, f := range []string{"enums_gen.go", "special_gen.go"} { + want, err := os.ReadFile(filepath.Join(dir, f)) + if err != nil { + t.Fatal(err) + } + got, err := os.ReadFile(filepath.Join("..", "..", f)) + if err != nil { + t.Fatal(err) + } + if string(got) != string(want) { + t.Errorf("%s is out of date: run go generate", f) + } + } +} + +func TestMain_(t *testing.T) { + dir := t.TempDir() + saved := os.Args + defer func() { os.Args = saved }() + os.Args = []string{"gen", dir} + main() + if _, err := os.Stat(filepath.Join(dir, "enums_gen.go")); err != nil { + t.Fatal(err) + } + // A failure exits 1. + code := 0 + exit = func(c int) { code = c } + defer func() { exit = os.Exit }() + os.Args = []string{"gen", filepath.Join(dir, "missing", "dir")} + main() + if code != 1 { + t.Fatalf("exit code %d", code) + } + // With no argument it writes into the current directory: the module root + // here, whose files TestGeneratedFilesAreCurrent shows are what it writes. + t.Chdir(filepath.Join("..", "..")) + os.Args = []string{"gen"} + code = 0 + main() + if code != 0 { + t.Fatalf("exit code %d", code) + } +} + +func TestFailures(t *testing.T) { + boom := errors.New("boom") + failing := func(string) (string, error) { return "", boom } + if err := writeAll(t.TempDir(), failing); !errors.Is(err, boom) { + t.Fatalf("writeAll = %v", err) + } + // enums succeed, special fails. + onlyEnums := func(p string) (string, error) { + if p == "github.com/go-numerics/special" { + return "", boom + } + return moduleDir(p) + } + if err := writeAll(t.TempDir(), onlyEnums); !errors.Is(err, boom) { + t.Fatalf("writeAll = %v", err) + } + empty := t.TempDir() + if _, err := generate(func(string) (string, error) { return empty, nil }); err == nil { + t.Fatal("generate accepted a directory with no Go file") + } + if _, err := generateSpecial(func(string) (string, error) { return empty, nil }); err == nil { + t.Fatal("generateSpecial accepted a directory with no Go file") + } + bad := t.TempDir() + os.WriteFile(filepath.Join(bad, "x.go"), []byte("package x\nfunc ("), 0o644) + os.WriteFile(filepath.Join(bad, "x_test.go"), []byte("package x"), 0o644) + if _, err := enums(bad); err == nil { + t.Fatal("enums accepted a file that does not parse") + } + if _, err := funcs(bad); err == nil { + t.Fatal("funcs accepted a file that does not parse") + } + if _, err := moduleDir("example.com/not/a/module"); err == nil { + t.Fatal("moduleDir found a module that is not required") + } + // The special_gen write fails when enums_gen is a directory... and + // enums_gen fails when the directory does not exist. + dir := t.TempDir() + os.Mkdir(filepath.Join(dir, "special_gen.go"), 0o755) + if err := writeAll(dir, moduleDir); err == nil { + t.Fatal("writeAll wrote over a directory") + } +} + +func TestWords(t *testing.T) { + for in, want := range map[string]string{ + "TwoSided": "two_sided", "LBFGSB": "lbfgsb", "RK45": "rk45", "GumbelL": "gumbel_l", "NewtonCG": "newton_cg", "DOP853": "dop853", + } { + if got := snake(in); got != want { + t.Errorf("snake(%q) = %q, want %q", in, got, want) + } + } + for _, c := range []struct { + names []string + want string + }{ + {[]string{"CenterMedian", "CenterMean"}, "Center"}, + {[]string{"TwoSided", "Less"}, ""}, + {[]string{"Only"}, ""}, + {[]string{"BinsCount", "BinsFD"}, "Bins"}, + {[]string{"Tau", "TauB"}, ""}, + {[]string{"Ab", "Abc"}, ""}, + } { + if got := commonPrefix(c.names); got != c.want { + t.Errorf("commonPrefix(%v) = %q, want %q", c.names, got, c.want) + } + } +} diff --git a/reflect.go b/reflect.go index f5a03bf..2bfa625 100644 --- a/reflect.go +++ b/reflect.go @@ -302,7 +302,6 @@ func generator(v any) (*random.Generator, error) { return nil, typeError(v, "a seed or a Stats::RNG") } - // results converts what a Go func returned: a final error, when not nil, // raises; one other result is that value, several an Array. func results(vals []reflect.Value) (any, error) { From b8038d80a0d5a84a794f35f50e964acf982bfe45 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Sat, 17 Oct 2026 22:23:39 +0200 Subject: [PATCH 06/20] Plot: bind go-plot figures, axes and options Co-Authored-By: Claude Opus 5.5 --- plot.go | 979 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 977 insertions(+), 2 deletions(-) diff --git a/plot.go b/plot.go index 7198767..a3a3d62 100644 --- a/plot.go +++ b/plot.go @@ -1,5 +1,980 @@ package ruby -import rb "github.com/go-embedded-ruby/ruby" +// The module Plot binds go-plot/plot, matplotlib's object model in pure Go: +// +// fig, ax = Plot.subplots(figsize: [6, 4]) +// ax.plot([1, 2, 3], [1, 4, 9], color: "C1", label: "squares", marker: "o") +// ax.legend +// ax.title = "growth" +// File.write("fig.svg", fig.to_svg) +// +// Bound, by Ruby name (each calls the Go function or method in parentheses): +// +// Plot.figure(figsize: [w, h], dpi: d) (NewFigure, FigSize, DPI) +// Plot.subplots(rows = 1, cols = 1, figsize:, dpi:, squeeze: true) +// (Subplots) -> [fig, axes] +// Plot.values(data) (Values) -> NDArray +// Plot::Figure.new(figsize:, dpi:) (NewFigure) +// Figure#width #height #dpi (the fields) +// Figure#pixel_size (PixelSize) -> [w, h] +// Figure#axes (Axes) +// Figure#add_axes(l, b, w, h) / ([l, b, w, h]) (AddAxes) +// Figure#subplots(rows = 1, cols = 1, squeeze: true) (Figure.Subplots) +// Figure#tight_layout (TightLayout) -> true/false +// Figure#colorbar(mappable, ax) / (m, ax: ax) (Colorbar) -> Axes +// Figure#to_svg #to_png (SaveSVG, SavePNG) -> String +// Figure#save_svg(path) #save_png(path) (SaveSVG, SavePNG) +// Figure#savefig(path) (by extension: .svg or .png) +// Figure#display (Display) -> {mime => bytes} +// Figure#inspect #to_s "#" +// Axes#plot(y) / (x, y, **opts) (Plot) +// Axes#scatter(x, y, **opts) (Scatter) -> Mappable or nil +// Axes#hist(x, **opts) (Hist) +// Axes#bar(x, height, **opts) (Bar) +// Axes#barh(y, width, **opts) (Barh) +// Axes#fill_between(x, y1, y2 = nil, **opts) (FillBetween) +// Axes#errorbar(x, y, **opts) (ErrorBar) +// Axes#axhline(y = 0, **opts) #axvline(x = 0) (Axhline, Axvline) +// Axes#text(x, y, s, **opts) (Text) +// Axes#annotate(s, xy, xytext = xy, **opts) (Annotate; xy are [x, y]) +// Axes#imshow(data, **opts) (Imshow) -> Mappable +// Axes#contour(z) / (x, y, z, **opts) (Contour) -> Mappable +// Axes#contourf(z) / (x, y, z, **opts) (Contourf) -> Mappable +// Axes#legend(**opts) (Legend) +// Axes#grid(on = true) (Grid) +// Axes#set_title(s) #title= (SetTitle) +// Axes#set_xlabel(s) #xlabel= #set_ylabel #ylabel= (SetXLabel, SetYLabel) +// Axes#set_xlim(lo, hi) / ([lo, hi]) #xlim= #set_ylim #ylim= (SetXLim, SetYLim) +// Axes#xlim #ylim -> [lo, hi]; xlim(lo, hi) sets (XLim, SetXLim, ...) +// Axes#set_xscale(s) #xscale= #set_yscale #yscale= (SetXScale, SetYScale) +// Axes#set_xticks(locs, labels = nil) #set_yticks (SetXTicks, SetYTicks) +// Axes#set_aspect(r) #aspect= (a number, :equal for 1, :auto for 0) +// Axes#twinx (TwinX) +// Axes#figure (Figure) +// Plot::Mappable#inspect +// +// Keyword options map to the plot.Option constructors by their snake_case +// name, read from the functions themselves; case and underscores aside, so +// line_width: and linewidth: are one: align alpha arrow aspect bin_edges bins +// bottom c_values cap_size clip cmap color colors density edge_color +// error_color extent face_color font_size ha label levels line_style +// line_width loc log_norm marker marker_edge_width marker_size n_levels origin +// rgba rotation sizes va vmax vmin where width x_err y_err z_order; and +// matplotlib's spellings lw ls ms mew fc ec edgecolors ecolor c (c_values) s +// (sizes) clip_on height (width) left (bottom) arrowprops (arrow, when +// truthy). An option without an argument (density, arrow, log_norm) takes +// true (false leaves it out); one with several takes an Array (extent: [l, r, +// b, t], rgba: [r, g, b, a]); bins: and levels: take an Integer (Bins, +// NLevels) or an Array (BinEdges, Levels); color: takes a String or an Array +// [r, g, b] / [r, g, b, a] (RGBA). Options are applied in the alphabetical +// order of their keywords. An unknown keyword is an ArgumentError naming the +// known ones, and what go-plot panics on (an unknown colour, marker or line +// style, data of mismatched lengths) is an ArgumentError too. +// +// Data is an Array of numbers, an NDArray (passed as it is) or a +// DataFrame::Series (its Floats); 2-D data (imshow, contour) an NDArray or an +// Array of Arrays, and 3-D data (an RGB image) an NDArray or nested Arrays. +// +// Plot.subplots squeezes as matplotlib does: a 1x1 grid gives the Axes +// itself, one row or one column a flat Array, any other grid an Array of +// rows; squeeze: false always gives the Array of rows. +// +// Not bound: Figure.Draw and Figure.Image (a renderer and a raster, types Ruby +// has no use for; to_png gives the pixels), the fields Facecolor, Text and +// SubplotParams, and Mappable's Cmap and Norm. -func registerPlot(in *rb.Interp) error { return nil } +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "reflect" + "runtime" + "sort" + "strings" + + "github.com/go-dataframe/dataframe" + rb "github.com/go-embedded-ruby/ruby" + nd "github.com/go-ndarray/ndarray" + "github.com/go-plot/plot" +) + +func registerPlot(in *rb.Interp) error { + d := &definer{in: in} + m := d.module("Plot") + fig := d.class(m, "Figure") + ax := d.class(m, "Axes") + mp := d.class(m, "Mappable") + if d.err != nil { + return d.err + } + fig.Bind(&plot.Figure{}) + ax.Bind(&plot.Axes{}) + mp.Bind(&plot.Mappable{}) + mp.DefineMethod("inspect", func(c *rb.Call) (any, error) { + return fmt.Sprintf("#", c.Self.Go().(*plot.Mappable).Cmap.Name), nil + }) + m.DefineSingletonMethod("figure", plNewFigure) + fig.DefineSingletonMethod("new", plNewFigure) + m.DefineSingletonMethod("subplots", plSubplots) + m.DefineSingletonMethod("values", plValues) + for name, f := range plFigureMethods { + fig.DefineMethod(name, f) + } + for name, f := range plAxesMethods { + ax.DefineMethod(name, f) + } + return nil +} + +// plProtect runs f, turning a panic of go-plot (an invalid colour, data of +// mismatched lengths, ...) into an ArgumentError. +func plProtect(f func()) (err error) { + defer func() { + if r := recover(); r != nil { + err = argError("%v", r) + } + }() + f() + return nil +} + +// plNoKw rejects keyword arguments. +func plNoKw(c *rb.Call) error { + if len(c.Kwargs) > 0 { + return argError("unknown keyword: :%s", sortedKeys(c.Kwargs)[0]) + } + return nil +} + +// plExact checks a call has n positional arguments and no keywords. +func plExact(c *rb.Call, n int) error { + if err := plNoKw(c); err != nil { + return err + } + return arity(c, n, n) +} + +func plValues(c *rb.Call) (any, error) { + if err := plExact(c, 1); err != nil { + return nil, err + } + v, err := plData(c.Args[0].Go()) + if err != nil { + return nil, err + } + var f []float64 + if err := plProtect(func() { f = plot.Values(v) }); err != nil { + return nil, err + } + return vec(f), nil +} + +// plFigOptions reads figsize: and dpi:. +func plFigOptions(kw map[string]rb.Value) ([]plot.FigOption, error) { + var opts []plot.FigOption + for _, k := range sortedKeys(kw) { + switch k { + case "figsize": + wh, err := toFloats(kw[k].Go()) + if err != nil { + return nil, err + } + if len(wh) != 2 { + return nil, argError("figsize must be [width, height], got %d values", len(wh)) + } + opts = append(opts, plot.FigSize(wh[0], wh[1])) + case "dpi": + f, err := toFloat(kw[k].Go()) + if err != nil { + return nil, err + } + opts = append(opts, plot.DPI(f)) + default: + return nil, argError("unknown keyword: :%s (known: dpi, figsize)", k) + } + } + return opts, nil +} + +func plNewFigure(c *rb.Call) (any, error) { + if err := arity(c, 0, 0); err != nil { + return nil, err + } + opts, err := plFigOptions(c.Kwargs) + if err != nil { + return nil, err + } + return plot.NewFigure(opts...), nil +} + +// plGrid reads (rows = 1, cols = 1) and squeeze:, leaving the other keywords. +func plGrid(c *rb.Call) (rows, cols int, squeeze bool, rest map[string]rb.Value, err error) { + if err = arity(c, 0, 2); err != nil { + return + } + dims := []int{1, 1} + for i, a := range c.Args { + n, e := toInt(a.Go()) + if e != nil { + return 0, 0, false, nil, e + } + dims[i] = int(n) + } + if dims[0] < 1 || dims[1] < 1 { + return 0, 0, false, nil, argError("rows and cols must be positive, got %d and %d", dims[0], dims[1]) + } + mine, rest := split(c.Kwargs, "squeeze") + squeeze = true + if s, ok := mine["squeeze"]; ok { + b, ok := s.Go().(bool) + if !ok { + return 0, 0, false, nil, typeError(s.Go(), "true or false") + } + squeeze = b + } + return dims[0], dims[1], squeeze, rest, nil +} + +// plSqueeze shapes a grid of Axes as matplotlib's squeeze=True does. +func plSqueeze(axs [][]*plot.Axes, squeeze bool) any { + if !squeeze { + return axs + } + switch { + case len(axs) == 1 && len(axs[0]) == 1: + return axs[0][0] + case len(axs) == 1: + return axs[0] + case len(axs[0]) == 1: + col := make([]*plot.Axes, len(axs)) + for i, r := range axs { + col[i] = r[0] + } + return col + } + return axs +} + +func plSubplots(c *rb.Call) (any, error) { + rows, cols, squeeze, rest, err := plGrid(c) + if err != nil { + return nil, err + } + opts, err := plFigOptions(rest) + if err != nil { + return nil, err + } + f, axs := plot.Subplots(rows, cols, opts...) + return []any{f, plSqueeze(axs, squeeze)}, nil +} + +// plData converts Ruby plot data to what go-plot's data parameters take: nil, +// a float64, a []float64, an *ndarray.Array (as it is), a [][]float64 (an +// Array of Arrays of numbers) or, deeper, an *ndarray.Array of that shape. A +// DataFrame::Series gives its Floats. +func plData(v any) (any, error) { + switch x := v.(type) { + case nil, *nd.Array: + return x, nil + case int64, float64: + return toFloat(x) + case *dataframe.Series: + f, err := x.Floats() + if err != nil { + return nil, goError(err) + } + return f, nil + case []any: + if len(x) > 0 { + if _, nested := x[0].([]any); nested { + return plNested(x) + } + } + return toFloats(x) + } + return nil, typeError(v, "plot data (an Array of numbers or an NDArray)") +} + +// plNested converts nested Arrays: two levels to a [][]float64, more to an +// NDArray of their shape. Every level must be rectangular. +func plNested(x []any) (any, error) { + var shape []int + for e := any(x); ; { + l, ok := e.([]any) + if !ok || len(l) == 0 { + break + } + shape = append(shape, len(l)) + e = l[0] + } + var flat []float64 + var walk func(e any, depth int) error + walk = func(e any, depth int) error { + if depth == len(shape) { + f, err := toFloat(e) + flat = append(flat, f) + return err + } + l, ok := e.([]any) + if !ok || len(l) != shape[depth] { + return argError("plot data must be rectangular nested Arrays of numbers") + } + for _, s := range l { + if err := walk(s, depth+1); err != nil { + return err + } + } + return nil + } + if err := walk(x, 0); err != nil { + return nil, err + } + if len(shape) == 2 { + rows := make([][]float64, shape[0]) + for i := range rows { + rows[i] = flat[i*shape[1] : (i+1)*shape[1]] + } + return rows, nil + } + a, _ := nd.FromData(flat, shape...) // the shape is the data's own + return a, nil +} + +// plOptionFuncs are the plot.Option constructors; each one's keyword is its +// Go name in snake_case, read from the function itself. +var plOptionFuncs = []any{ + plot.Align, plot.Alpha, plot.Arrow, plot.Aspect, plot.BinEdges, plot.Bins, + plot.Bottom, plot.CValues, plot.CapSize, plot.Clip, plot.Cmap, plot.Color, + plot.Colors, plot.Density, plot.EdgeColor, plot.ErrorColor, plot.Extent, + plot.FaceColor, plot.FontSize, plot.HA, plot.Label, plot.Levels, + plot.LineStyle, plot.LineWidth, plot.Loc, plot.LogNorm, plot.Marker, + plot.MarkerEdgeWidth, plot.MarkerSize, plot.NLevels, plot.Origin, plot.RGBA, + plot.Rotation, plot.Sizes, plot.VA, plot.Vmax, plot.Vmin, plot.Where, + plot.Width, plot.XErr, plot.YErr, plot.ZOrder, +} + +// plAliases are matplotlib's spellings of options, and +// the Go constructor each stands for. +var plAliases = map[string]string{ + "lw": "LineWidth", "ls": "LineStyle", "ms": "MarkerSize", "mew": "MarkerEdgeWidth", + "fc": "FaceColor", "ec": "EdgeColor", "edgecolors": "EdgeColor", "ecolor": "ErrorColor", + "c": "CValues", "s": "Sizes", "clip_on": "Clip", "height": "Width", "left": "Bottom", + "arrowprops": "Arrow", +} + +// plOptions indexes the constructors by normalized keyword; plKnown lists the +// keywords for error messages. +var plOptions, plKnown = plOptionIndex() + +func plOptionIndex() (map[string]reflect.Value, string) { + byGo := map[string]reflect.Value{} + idx := map[string]reflect.Value{} + var names []string + for _, f := range plOptionFuncs { + fv := reflect.ValueOf(f) + full := runtime.FuncForPC(fv.Pointer()).Name() + goName := full[strings.LastIndex(full, ".")+1:] + byGo[goName] = fv + idx[normalize(goName)] = fv + names = append(names, snakeName(goName)) + } + for alias, goName := range plAliases { + idx[normalize(alias)] = byGo[goName] + names = append(names, alias) + } + sort.Strings(names) + return idx, strings.Join(names, ", ") +} + +// plOpts converts keyword arguments to plot options, in keyword order. +func plOpts(kw map[string]rb.Value) ([]plot.Option, error) { + var opts []plot.Option + for _, k := range sortedKeys(kw) { + x := kw[k].Go() + key := normalize(k) + switch key { + case "bins": + if _, ok := x.(int64); !ok { + key = "binedges" + } + case "levels": + if _, ok := x.(int64); ok { + key = "nlevels" + } + case "color": + if l, ok := x.([]any); ok { + key = "rgba" + if len(l) == 3 { + x = append(l, 1.0) + } + } + case "arrowprops": + x = x != nil && x != false + } + fn, ok := plOptions[key] + if !ok { + return nil, argError("unknown keyword: :%s (known: %s)", k, plKnown) + } + o, err := plOption(k, fn, x) + if err != nil { + return nil, err + } + if o != nil { + opts = append(opts, o) + } + } + return opts, nil +} + +// plOption calls one option constructor with a keyword's value: true for one +// without parameters (false gives no option), an Array (or one value) for a +// variadic one, an Array of as many values for one of several parameters, +// plot data for an any parameter, and the value converted otherwise. +func plOption(k string, fn reflect.Value, x any) (plot.Option, error) { + ft := fn.Type() + var args []reflect.Value + switch { + case ft.NumIn() == 0: + b, ok := x.(bool) + if !ok { + return nil, typeError(x, "true or false") + } + if !b { + return nil, nil + } + case ft.IsVariadic(): + list, ok := x.([]any) + if !ok { + list = []any{x} + } + for _, e := range list { + v, err := convert(e, ft.In(0).Elem()) + if err != nil { + return nil, err + } + args = append(args, v) + } + case ft.NumIn() > 1: + list, ok := x.([]any) + if !ok || len(list) != ft.NumIn() { + return nil, argError("%s: expected an Array of %d values", k, ft.NumIn()) + } + for i, e := range list { + v, err := convert(e, ft.In(i)) + if err != nil { + return nil, err + } + args = append(args, v) + } + case ft.In(0).Kind() == reflect.Interface: + d, err := plData(x) + if err != nil { + return nil, err + } + args = append(args, reflect.ValueOf(&d).Elem()) + default: + v, err := convert(x, ft.In(0)) + if err != nil { + return nil, err + } + args = append(args, v) + } + var o plot.Option + err := plProtect(func() { o = fn.Call(args)[0].Interface().(plot.Option) }) + return o, err +} + +func plAxes(c *rb.Call) *plot.Axes { return c.Self.Go().(*plot.Axes) } +func plFig(c *rb.Call) *plot.Figure { return c.Self.Go().(*plot.Figure) } + +// plCall reads min..max positional arguments as plot data and the keywords +// as options. +func plCall(c *rb.Call, min, max int) ([]any, []plot.Option, error) { + if err := arity(c, min, max); err != nil { + return nil, nil, err + } + data := make([]any, len(c.Args)) + for i, a := range c.Args { + d, err := plData(a.Go()) + if err != nil { + return nil, nil, err + } + data[i] = d + } + opts, err := plOpts(c.Kwargs) + return data, opts, err +} + +// plMappable is a Mappable for Ruby: nil when go-plot gave none. +func plMappable(m *plot.Mappable) any { + if m == nil { + return nil + } + return m +} + +// plDraw binds a plotting method of fixed data arity n. +func plDraw(n int, draw func(a *plot.Axes, d []any, opts []plot.Option)) rb.Func { + return func(c *rb.Call) (any, error) { + d, opts, err := plCall(c, n, n) + if err != nil { + return nil, err + } + return nil, plProtect(func() { draw(plAxes(c), d, opts) }) + } +} + +// plMap binds a method that returns a Mappable, taking a single z or x, y, z +// (contour) or just data (imshow, scatter). +func plMap(counts []int, draw func(a *plot.Axes, d []any, opts []plot.Option) *plot.Mappable) rb.Func { + return func(c *rb.Call) (any, error) { + d, opts, err := plCall(c, counts[0], counts[len(counts)-1]) + if err != nil { + return nil, err + } + if len(counts) == 2 && len(d) != counts[0] && len(d) != counts[1] { + return nil, argError("wrong number of arguments (given %d, expected %d or %d)", len(d), counts[0], counts[1]) + } + if len(d) == 1 && len(counts) == 2 { + d = []any{nil, nil, d[0]} + } + var m *plot.Mappable + if err := plProtect(func() { m = draw(plAxes(c), d, opts) }); err != nil { + return nil, err + } + return plMappable(m), nil + } +} + +// plFloats reads n numbers from the positional arguments, or from a single +// Array of n. +func plFloats(c *rb.Call, n int) ([]float64, error) { + if err := plNoKw(c); err != nil { + return nil, err + } + args := make([]any, len(c.Args)) + for i, a := range c.Args { + args[i] = a.Go() + } + if len(args) == 1 { + if l, ok := args[0].([]any); ok { + args = l + } + } + if len(args) != n { + return nil, argError("wrong number of arguments (given %d, expected %d)", len(args), n) + } + out := make([]float64, n) + for i, a := range args { + f, err := toFloat(a) + if err != nil { + return nil, err + } + out[i] = f + } + return out, nil +} + +// plString binds a setter of one String. +func plString(set func(a *plot.Axes, s string)) rb.Func { + return func(c *rb.Call) (any, error) { + if err := plExact(c, 1); err != nil { + return nil, err + } + s, err := toString(c.Args[0].Go()) + if err != nil { + return nil, err + } + set(plAxes(c), s) + return nil, nil + } +} + +// plSetLim binds set_xlim and its kind: (lo, hi) or ([lo, hi]). +func plSetLim(set func(a *plot.Axes, lo, hi float64)) rb.Func { + return func(c *rb.Call) (any, error) { + f, err := plFloats(c, 2) + if err != nil { + return nil, err + } + set(plAxes(c), f[0], f[1]) + return nil, nil + } +} + +// plLim binds xlim: with no argument the limits, with two (or an Array) a +// setter. +func plLim(get func(a *plot.Axes) (float64, float64), set func(a *plot.Axes, lo, hi float64)) rb.Func { + setter := plSetLim(set) + return func(c *rb.Call) (any, error) { + if len(c.Args) == 0 && len(c.Kwargs) == 0 { + lo, hi := get(plAxes(c)) + return []any{lo, hi}, nil + } + return setter(c) + } +} + +// plTicks binds set_xticks(locs, labels = nil). +func plTicks(set func(a *plot.Axes, locs []float64, labels ...string)) rb.Func { + return func(c *rb.Call) (any, error) { + if err := plNoKw(c); err != nil { + return nil, err + } + if err := arity(c, 1, 2); err != nil { + return nil, err + } + locs, err := toFloats(c.Args[0].Go()) + if err != nil { + return nil, err + } + var labels []string + if len(c.Args) == 2 { + v, err := convert(c.Args[1].Go(), reflect.TypeFor[[]string]()) + if err != nil { + return nil, err + } + labels = v.Interface().([]string) + } + set(plAxes(c), locs, labels...) + return nil, nil + } +} + +func plSetAspect(c *rb.Call) (any, error) { + if err := plExact(c, 1); err != nil { + return nil, err + } + var r float64 + switch x := c.Args[0].Go().(type) { + case rb.Symbol, string: + s, _ := toString(x) + switch s { + case "equal": + r = 1 + case "auto": + r = 0 + default: + return nil, argError("unknown aspect %q (expected a number, :equal or :auto)", s) + } + default: + f, err := toFloat(x) + if err != nil { + return nil, err + } + r = f + } + plAxes(c).SetAspect(r) + return nil, nil +} + +func plGridOn(c *rb.Call) (any, error) { + if err := plNoKw(c); err != nil { + return nil, err + } + if err := arity(c, 0, 1); err != nil { + return nil, err + } + on := true + if len(c.Args) == 1 { + b, ok := c.Args[0].Go().(bool) + if !ok { + return nil, typeError(c.Args[0].Go(), "true or false") + } + on = b + } + plAxes(c).Grid(on) + return nil, nil +} + +// plLine binds axhline and axvline: (at = 0, **opts). +func plLine(draw func(a *plot.Axes, at float64, opts ...plot.Option)) rb.Func { + return func(c *rb.Call) (any, error) { + if err := arity(c, 0, 1); err != nil { + return nil, err + } + at := 0.0 + if len(c.Args) == 1 { + f, err := toFloat(c.Args[0].Go()) + if err != nil { + return nil, err + } + at = f + } + opts, err := plOpts(c.Kwargs) + if err != nil { + return nil, err + } + return nil, plProtect(func() { draw(plAxes(c), at, opts...) }) + } +} + +func plText(c *rb.Call) (any, error) { + if err := arity(c, 3, 3); err != nil { + return nil, err + } + x, err := toFloat(c.Args[0].Go()) + if err != nil { + return nil, err + } + y, err := toFloat(c.Args[1].Go()) + if err != nil { + return nil, err + } + s, err := toString(c.Args[2].Go()) + if err != nil { + return nil, err + } + opts, err := plOpts(c.Kwargs) + if err != nil { + return nil, err + } + return nil, plProtect(func() { plAxes(c).Text(x, y, s, opts...) }) +} + +// plPoint reads an [x, y] pair. +func plPoint(v any) ([]float64, error) { + p, err := toFloats(v) + if err == nil && len(p) != 2 { + err = argError("a point is [x, y], got %d values", len(p)) + } + return p, err +} + +func plAnnotate(c *rb.Call) (any, error) { + if err := arity(c, 2, 3); err != nil { + return nil, err + } + s, err := toString(c.Args[0].Go()) + if err != nil { + return nil, err + } + xy, err := plPoint(c.Args[1].Go()) + if err != nil { + return nil, err + } + txy := xy + if len(c.Args) == 3 { + if txy, err = plPoint(c.Args[2].Go()); err != nil { + return nil, err + } + } + opts, err := plOpts(c.Kwargs) + if err != nil { + return nil, err + } + return nil, plProtect(func() { plAxes(c).Annotate(s, xy[0], xy[1], txy[0], txy[1], opts...) }) +} + +func plPlot(c *rb.Call) (any, error) { + d, opts, err := plCall(c, 1, 2) + if err != nil { + return nil, err + } + if len(d) == 1 { + d = []any{nil, d[0]} + } + return nil, plProtect(func() { plAxes(c).Plot(d[0], d[1], opts...) }) +} + +func plFillBetween(c *rb.Call) (any, error) { + d, opts, err := plCall(c, 2, 3) + if err != nil { + return nil, err + } + if len(d) == 2 { + d = append(d, nil) + } + return nil, plProtect(func() { plAxes(c).FillBetween(d[0], d[1], d[2], opts...) }) +} + +var plAxesMethods = map[string]rb.Func{ + "plot": plPlot, + "scatter": plMap([]int{2}, func(a *plot.Axes, d []any, o []plot.Option) *plot.Mappable { + return a.Scatter(d[0], d[1], o...) + }), + "hist": plDraw(1, func(a *plot.Axes, d []any, o []plot.Option) { a.Hist(d[0], o...) }), + "bar": plDraw(2, func(a *plot.Axes, d []any, o []plot.Option) { a.Bar(d[0], d[1], o...) }), + "barh": plDraw(2, func(a *plot.Axes, d []any, o []plot.Option) { a.Barh(d[0], d[1], o...) }), + "errorbar": plDraw(2, func(a *plot.Axes, d []any, o []plot.Option) { + a.ErrorBar(d[0], d[1], o...) + }), + "fill_between": plFillBetween, + "axhline": plLine((*plot.Axes).Axhline), + "axvline": plLine((*plot.Axes).Axvline), + "text": plText, + "annotate": plAnnotate, + "imshow": plMap([]int{1}, func(a *plot.Axes, d []any, o []plot.Option) *plot.Mappable { + return a.Imshow(d[0], o...) + }), + "contour": plMap([]int{1, 3}, func(a *plot.Axes, d []any, o []plot.Option) *plot.Mappable { + return a.Contour(d[0], d[1], d[2], o...) + }), + "contourf": plMap([]int{1, 3}, func(a *plot.Axes, d []any, o []plot.Option) *plot.Mappable { + return a.Contourf(d[0], d[1], d[2], o...) + }), + "legend": plDraw(0, func(a *plot.Axes, _ []any, o []plot.Option) { a.Legend(o...) }), + "grid": plGridOn, + "set_title": plString((*plot.Axes).SetTitle), + "title=": plString((*plot.Axes).SetTitle), + "set_xlabel": plString((*plot.Axes).SetXLabel), + "xlabel=": plString((*plot.Axes).SetXLabel), + "set_ylabel": plString((*plot.Axes).SetYLabel), + "ylabel=": plString((*plot.Axes).SetYLabel), + "set_xscale": plString((*plot.Axes).SetXScale), + "xscale=": plString((*plot.Axes).SetXScale), + "set_yscale": plString((*plot.Axes).SetYScale), + "yscale=": plString((*plot.Axes).SetYScale), + "set_xlim": plSetLim((*plot.Axes).SetXLim), + "xlim=": plSetLim((*plot.Axes).SetXLim), + "set_ylim": plSetLim((*plot.Axes).SetYLim), + "ylim=": plSetLim((*plot.Axes).SetYLim), + "xlim": plLim((*plot.Axes).XLim, (*plot.Axes).SetXLim), + "ylim": plLim((*plot.Axes).YLim, (*plot.Axes).SetYLim), + "set_xticks": plTicks((*plot.Axes).SetXTicks), + "set_yticks": plTicks((*plot.Axes).SetYTicks), + "set_aspect": plSetAspect, + "aspect=": plSetAspect, + "twinx": func(c *rb.Call) (any, error) { + if err := plExact(c, 0); err != nil { + return nil, err + } + return plAxes(c).TwinX(), nil + }, + "figure": func(c *rb.Call) (any, error) { + if err := plExact(c, 0); err != nil { + return nil, err + } + return plAxes(c).Figure(), nil + }, +} + +// plSVG and plPNG render a figure; writing to memory cannot fail. +func plSVG(f *plot.Figure) []byte { + var b bytes.Buffer + _ = f.SaveSVG(&b) + return b.Bytes() +} + +func plPNG(f *plot.Figure) []byte { + var b bytes.Buffer + _ = f.SavePNG(&b) + return b.Bytes() +} + +// plSave binds a method writing the figure to the file at a path. +func plSave(render func(f *plot.Figure, path string) ([]byte, error)) rb.Func { + return func(c *rb.Call) (any, error) { + if err := plExact(c, 1); err != nil { + return nil, err + } + path, err := toString(c.Args[0].Go()) + if err != nil { + return nil, err + } + b, err := render(plFig(c), path) + if err != nil { + return nil, err + } + if err := os.WriteFile(path, b, 0o644); err != nil { + return nil, rb.NewError("IOError", err.Error()) + } + return nil, nil + } +} + +// plFigNoArgs binds a figure method without arguments. +func plFigNoArgs(f func(*plot.Figure) any) rb.Func { + return func(c *rb.Call) (any, error) { + if err := plExact(c, 0); err != nil { + return nil, err + } + return f(plFig(c)), nil + } +} + +func plInspect(f *plot.Figure) any { + w, h := f.PixelSize() + return fmt.Sprintf("#", w, h) +} + +func plColorbar(c *rb.Call) (any, error) { + mine, rest := split(c.Kwargs, "ax") + if len(rest) > 0 { + return nil, argError("unknown keyword: :%s (known: ax)", sortedKeys(rest)[0]) + } + if err := arity(c, 1, 2); err != nil { + return nil, err + } + m, ok := c.Args[0].Go().(*plot.Mappable) + if !ok { + return nil, typeError(c.Args[0].Go(), "Plot::Mappable") + } + axv, given := mine["ax"] + if len(c.Args) == 2 { + axv, given = c.Args[1], true + } + if !given { + return nil, argError("colorbar needs the Axes: colorbar(mappable, ax)") + } + ax, ok := axv.Go().(*plot.Axes) + if !ok { + return nil, typeError(axv.Go(), "Plot::Axes") + } + return plFig(c).Colorbar(m, ax), nil +} + +func plAddAxes(c *rb.Call) (any, error) { + f, err := plFloats(c, 4) + if err != nil { + return nil, err + } + return plFig(c).AddAxes(f[0], f[1], f[2], f[3]), nil +} + +func plFigSubplots(c *rb.Call) (any, error) { + rows, cols, squeeze, rest, err := plGrid(c) + if err != nil { + return nil, err + } + if len(rest) > 0 { + return nil, argError("unknown keyword: :%s (known: squeeze)", sortedKeys(rest)[0]) + } + return plSqueeze(plFig(c).Subplots(rows, cols), squeeze), nil +} + +var plFigureMethods = map[string]rb.Func{ + "width": plFigNoArgs(func(f *plot.Figure) any { return f.Width }), + "height": plFigNoArgs(func(f *plot.Figure) any { return f.Height }), + "dpi": plFigNoArgs(func(f *plot.Figure) any { return f.DPI }), + "pixel_size": plFigNoArgs(func(f *plot.Figure) any { + w, h := f.PixelSize() + return []any{w, h} + }), + "axes": plFigNoArgs(func(f *plot.Figure) any { return f.Axes() }), + "tight_layout": plFigNoArgs(func(f *plot.Figure) any { return f.TightLayout() }), + "to_svg": plFigNoArgs(func(f *plot.Figure) any { return string(plSVG(f)) }), + "to_png": plFigNoArgs(func(f *plot.Figure) any { return plPNG(f) }), + "display": plFigNoArgs(func(f *plot.Figure) any { return f.Display() }), + "inspect": plFigNoArgs(plInspect), + "to_s": plFigNoArgs(plInspect), + "add_axes": plAddAxes, + "subplots": plFigSubplots, + "colorbar": plColorbar, + "save_svg": plSave(func(f *plot.Figure, _ string) ([]byte, error) { + return plSVG(f), nil + }), + "save_png": plSave(func(f *plot.Figure, _ string) ([]byte, error) { + return plPNG(f), nil + }), + "savefig": plSave(func(f *plot.Figure, path string) ([]byte, error) { + switch strings.ToLower(filepath.Ext(path)) { + case ".svg": + return plSVG(f), nil + case ".png": + return plPNG(f), nil + } + return nil, argError("savefig: unknown format of %q (expected .svg or .png)", path) + }), +} From 7504a1ba1a185eebfb78a31998b6afe894b1b9bd Mon Sep 17 00:00:00 2001 From: tannevaled Date: Sat, 17 Oct 2026 22:30:24 +0200 Subject: [PATCH 07/20] DataFrame: bind go-dataframe frames, series, group-bys and indexes Co-Authored-By: Claude Opus 5.5 --- dataframe.go | 1797 +++++++++++++++++++++++++++++++++++++++++++++++++- plot.go | 3 + 2 files changed, 1798 insertions(+), 2 deletions(-) diff --git a/dataframe.go b/dataframe.go index 3be470c..c8e8054 100644 --- a/dataframe.go +++ b/dataframe.go @@ -1,5 +1,1798 @@ package ruby -import rb "github.com/go-embedded-ruby/ruby" +// The class DataFrame binds go-dataframe/dataframe, pandas' DataFrame and +// Series in pure Go: +// +// df = DataFrame.new(city: ["Lyon", "Paris", "Lyon"], pop: [522, 2103, 530]) +// df.group_by("city").mean +// df[df["pop"] > 525] +// DataFrame.read_csv(File.read("data.csv"), parse_dates: ["day"]) +// +// Bound, by Ruby name (each calls the Go function or method in parentheses; +// "=" marks a default, "kw" an argument that may also be given by keyword): +// +// DataFrame, class methods: +// +// new(hash) / new(name: data, ...) (New, Col; columns in the order given) +// read_csv(text, **opts) (ReadCSV) +// read_csv_file(path, **opts) (ReadCSVFile) +// from_array(ndarray, names = []) (FromArray) +// from_series(*series) (FromSeries) +// concat(*frames, axis: 0, ignore_index: false, inner: false) (ConcatWith) +// +// DataFrame, instance methods: +// +// columns length size shape dtypes index (Columns Len Len Shape DTypes Index) +// [](name) [](names) [](mask) (Column, Select, Filter) +// column(name) column_at(i) series (Column ColumnAt Series) +// head(n = 5) tail(n = 5) (Head Tail) +// describe copy transpose t (Describe Copy T T) +// count mean sum std var min max median (the column reductions) +// isna notna ffill bfill (IsNA NotNA FFill BFill) +// shift(n = 1) round(decimals = 0) (Shift Round) +// drop(*names) select(*names) (Drop Select) +// filter(mask) (Filter: a bool Series or Array) +// sort_values(by, ascending: true) (SortValues; by and ascending +// a value or an Array) +// sort_index(ascending: true) (SortIndex) +// set_index(*names) reset_index(drop: false) with_index(index) +// reindex(labels) reindex_columns(*names) set_columns(*names) +// rename(hash) (Rename) +// with_column(name, data) (WithColumn) +// astype(dtype) / astype(hash) (AsTypeAll / AsType) +// dropna(*subset, how: :any, thresh: nil, axis: 0) +// (DropNA DropNAAll DropNAThresh DropNAColumns) +// fillna(value) / fillna(hash) (FillNA / FillNAColumns) +// group_by(*keys) groupby(*keys) (GroupBy) +// merge(right, how:, on:, left_on:, right_on:, suffixes:, indicator:) (Merge) +// join(other, how: "left", lsuffix: "", rsuffix: "") (Join) +// pivot(index, columns, values) (Pivot; kw) +// pivot_table(index, columns, values, aggfunc = "mean") (PivotTable; kw) +// melt(id_vars: [], value_vars: []) (Melt) +// stack unstack to_period(freq) (Stack Unstack ToPeriod) +// query(expr, **vars) eval(expr, **vars) (Query Eval) +// iloc(rows, *cols) loc(rows, *cols) (Iloc Loc; see Selectors) +// at(label, column) iat(i, j) (At IAt) +// apply(name = "") { |row| ... } (Apply; row is a Hash) +// resample(rule, on: nil) (Resample / ResampleOn) +// + - * / add sub mul div (Add Sub Mul Div) +// values to_ndarray (Values) -> NDArray +// to_h {name => Array}, in column order +// to_csv(**opts) write_csv(path, **opts) (WriteCSV WriteCSVFile) +// to_s inspect to_html display (String String HTML Display) +// +// DataFrame::Series: +// +// Series.new(data, name: "", index: nil) (NewSeries / NewSeriesIndexed) +// name length size count dtype index (Name Len Len Count DType Index) +// [](i) [](mask) [](range) (At, Filter, Iloc) +// get(label) (Get) +// values to_ndarray floats to_a valid (Values Values Floats Items Valid) +// mean sum std var(ddof = 1) min max median prod nunique +// (Mean SumValue Std VarDDOF Min Max ...) +// quantile(q = 0.5) agg(name) (Quantile Agg) +// head(n = 5) tail(n = 5) unique value_counts copy to_frame reset_index +// isna notna dropna fillna(v) ffill bfill (IsNA NotNA DropNA FillNA FFill BFill) +// shift(n = 1) diff(n = 1) pct_change(n = 1) +// cumsum cumprod cummax cummin abs neg round(decimals = 0) +// rename(name) astype(dtype) filter(mask) reindex(labels) with_index(index) +// sort_values(ascending: true) sort_index(ascending: true) +// between(lo, hi) isin(*values) (Between IsIn) +// + - * / % ** add sub mul div mod pow floordiv (Add Sub ... FloorDiv) +// > < >= <= gt lt ge le eq ne (Gt Lt Ge Le Eq Ne) -> a bool Series +// & | ^ ~ invert (And Or Xor Not Not) +// map { |v| ... } (Map) +// rolling(window, min_periods: nil) (Rolling / RollingMin) +// iloc(sel) loc(sel) (Iloc Loc) +// dt (Dt) -> DataFrame::DatetimeFields +// to_datetime(layout = nil) to_period(freq) to_timestamp unstack +// to_s inspect to_html display (String String HTML Display) +// +// DataFrame::GroupBy: agg(col: funcs, ...) (Agg, columns in order), mean sum +// count min max median std var first last nunique size ngroups n_groups +// select(*cols) [](*cols) transform(col, fn) groups ([[key, frame], ...]), +// to_s inspect. +// +// DataFrame::Resampler: agg mean sum count min max median std first last size. +// +// DataFrame::Rolling: mean sum min max std median. +// +// DataFrame::Index: Index.new(data, name: ""), Index.range(n); to_a labels +// length size name names nlevels range? label(i) [](i) get(label) values +// dtype level(k) rename(*names) to_s inspect display. +// +// DataFrame::DatetimeFields: year month day hour minute second day_of_week +// dayofweek day_of_year dayofyear normalize strftime(layout). +// +// read_csv keywords (the CSVOption of each): sep (CSVSeparator), header +// (CSVHeader), names (CSVNames), usecols (CSVUseCols), nrows (CSVNRows), +// skiprows (CSVSkipRows), na_values (CSVNAValues), keep_default_na +// (CSVKeepDefaultNA), parse_dates (CSVParseDates), date_format +// (CSVDateFormat), index_col (CSVIndexCol for a name, CSVIndexColumn for an +// Integer), dtype (CSVDTypes, a Hash), true_values (CSVTrueValues), +// false_values (CSVFalseValues), skip_blank_lines (CSVSkipBlankLines), +// comment (CSVComment), quotechar (CSVQuote; "" for none). to_csv keywords: +// sep, header, index, na_rep, line_terminator. +// +// Values: a column from a Ruby Array is NewSeries over its elements as +// []any, so its dtype is inferred as pandas infers it (Integers int64, Floats +// float64, Strings string, true/false bool, Times datetime, nil missing); an +// NDArray is passed as it is (the same array) and a Series as it is. A Time +// crosses as a time.Time and comes back as a UTC Time; a dtype is a String +// ("int64") or a Symbol (:float64); a Symbol is a String. +// +// Selectors (iloc, loc, Series#[]): an Integer (a position, or for loc a +// label), an Array (positions, labels, or bools as a mask), a bool Series (a +// mask), a Range (positions start...stop or start..stop, for loc an +// inclusive label range a..b, open with nil), or nil / :all (everything). +// +// The keyword columns of DataFrame.new and GroupBy#agg keep their order: +// both are defined in Ruby, taking **kw and passing kw.to_a on, because a +// Go method receives keywords as a map. +// +// rbgo's operator syntax does not reach methods of host-bound objects for +// the arithmetic operators, == and unary minus (rbgo dispatches them to Ruby +// objects only): `s + 1` raises a TypeError there, while `s.+(1)`, `s.add(1)` +// and the comparisons `s > 1` work. Use the named methods (add, sub, mul, +// div, mod, pow, neg, eq, ne) until rbgo dispatches them. +// +// Not bound: the Arrow and Parquet subpackages, dfplot, NewCategorical and +// Categorical, NewSeriesBitmap, NewSeriesMasked and ValidityBitmap (a nil in +// an Array marks a missing value instead), MapFloat (map covers it), +// DataFrame.Apply over Go closures other than the block, GroupBy.Apply and +// ApplyColumns (they take Go functions returning frames), Copy-free storage +// access, Index.Display's siblings HTML, Timestamp, Must, the display limits +// MaxRows/MinRows/MaxColumns and the error values. -func registerDataFrame(in *rb.Interp) error { return nil } +import ( + "bytes" + "reflect" + "strings" + "time" + + "github.com/go-dataframe/dataframe" + rb "github.com/go-embedded-ruby/ruby" + nd "github.com/go-ndarray/ndarray" +) + +var dtypeType = reflect.TypeFor[dataframe.DType]() + +func init() { + // A dtype is named by a String or a Symbol: :int64, "float", "str". + converters[dtypeType] = func(v any) (reflect.Value, error) { + s, err := toString(v) + if err != nil { + return reflect.Value{}, typeError(v, "a dtype") + } + dt, err := dataframe.ParseDType(s) + if err != nil { + return reflect.Value{}, goError(err) + } + return reflect.ValueOf(dt), nil + } +} + +// dfPrelude defines in Ruby what must see the keywords in order. +const dfPrelude = ` +class DataFrame + def self.new(data = nil, **columns) + data = data.to_a if data.is_a?(Hash) + _from_pairs(data, columns.to_a) + end + + class GroupBy + def agg(spec = nil, **columns) + spec = spec.to_a if spec.is_a?(Hash) + _agg(spec, columns.to_a) + end + end + + class Resampler + def agg(spec = nil, **columns) + spec = spec.to_a if spec.is_a?(Hash) + _agg(spec, columns.to_a) + end + end +end +` + +// dfEnv is what the bindings of one interpreter share: the class Time. +type dfEnv struct { + time rb.Value +} + +// dfM is a Go method bound under a Ruby name: kw names its fixed parameters +// for keyword use, def gives defaults for the last ones. +type dfM struct { + ruby, goName string + kw []string + def []any +} + +func registerDataFrame(in *rb.Interp) error { + d := &definer{in: in} + frame, err := in.DefineClass("DataFrame", nil) + d.keep(err) + if d.err != nil { + return d.err + } + series := d.class(frame, "Series") + group := d.class(frame, "GroupBy") + resampler := d.class(frame, "Resampler") + rolling := d.class(frame, "Rolling") + index := d.class(frame, "Index") + dt := d.class(frame, "DatetimeFields") + if d.err != nil { + return d.err + } + tc, _ := in.Eval("Time", "(dataframe)", 1) + e := &dfEnv{time: tc} + frame.Bind(&dataframe.DataFrame{}) + series.Bind(&dataframe.Series{}) + group.Bind(&dataframe.GroupBy{}) + resampler.Bind(&dataframe.Resampler{}) + rolling.Bind(&dataframe.Rolling{}) + index.Bind(&dataframe.Index{}) + dt.Bind(dataframe.DatetimeFields{}) + + e.bind(frame, dfFrameMethods) + e.bind(series, dfSeriesMethods) + e.bind(group, dfGroupMethods) + e.bind(resampler, dfResamplerMethods) + e.bind(rolling, dfRollingMethods) + e.bind(index, dfIndexMethods) + e.bind(dt, dfDatetimeMethods) + + frame.DefineSingletonMethod("_from_pairs", e.fromPairs) + frame.DefineSingletonMethod("read_csv", e.readCSV) + frame.DefineSingletonMethod("read_csv_file", e.readCSVFile) + frame.DefineSingletonMethod("from_array", e.fromArray) + frame.DefineSingletonMethod("from_series", e.fromSeries) + frame.DefineSingletonMethod("concat", e.concat) + for name, f := range map[string]func(*rb.Call) (any, error){ + "[]": e.frameIndex, "filter": e.frameFilter, "dtypes": e.dtypes, "to_h": e.toH, + "to_csv": e.toCSV, "write_csv": e.writeCSV, "sort_values": e.sortValues, + "group_by": e.groupBy, "groupby": e.groupBy, "dropna": e.dropNA, "fillna": e.fillNA, + "astype": e.asType, "rename": e.rename, "merge": e.merge, "join": e.join, + "melt": e.melt, "query": e.query, "eval": e.eval, "iloc": e.frameIloc, + "loc": e.frameLoc, "apply": e.apply, "resample": e.resample, + } { + frame.DefineMethod(name, f) + } + series.DefineSingletonMethod("new", e.newSeries) + for name, f := range map[string]func(*rb.Call) (any, error){ + "[]": e.seriesIndex, "filter": e.seriesFilter, "map": e.seriesMap, + "rolling": e.rolling, "iloc": e.seriesIloc, "loc": e.seriesLoc, + "to_datetime": e.toDatetime, + } { + series.DefineMethod(name, f) + } + group.DefineMethod("_agg", e.agg) + group.DefineMethod("groups", e.groups) + resampler.DefineMethod("_agg", e.agg) + index.DefineSingletonMethod("new", e.newIndex) + index.DefineSingletonMethod("range", e.rangeIndex) + _, err = in.Eval(dfPrelude, "(dataframe)", 1) + return err +} + +// bind defines each method of table on cls, calling the Go method of the +// receiver. +func (e *dfEnv) bind(cls *rb.Module, table []dfM) { + for _, m := range table { + cls.DefineMethod(m.ruby, func(c *rb.Call) (any, error) { + fn := reflect.ValueOf(c.Self.Go()).MethodByName(m.goName) + args := make([]any, len(c.Args)) + for i, a := range c.Args { + args[i] = a.Go() + } + return e.call(fn, m, args, c.Kwargs) + }) + } +} + +// call calls a Go function: positional arguments in order, keywords at the +// positions m.kw names, defaults for the last fixed parameters left out, and +// the remaining arguments (or a single Array) for a variadic parameter. +func (e *dfEnv) call(fn reflect.Value, m dfM, args []any, kw map[string]rb.Value) (any, error) { + ft := fn.Type() + fixed := ft.NumIn() + if ft.IsVariadic() { + fixed-- + } + vals := make([]any, max(len(args), fixed)) + set := make([]bool, len(vals)) + copy(vals, args) + for i := range args { + set[i] = true + } + for _, k := range sortedKeys(kw) { + i := indexOf(m.kw, k) + if i < 0 || i >= fixed { + known := "none" + if len(m.kw) > 0 { + known = strings.Join(m.kw, ", ") + } + return nil, argError("unknown keyword: :%s (known: %s)", k, known) + } + if set[i] { + return nil, argError("%s given both by position and by keyword", k) + } + vals[i], set[i] = kw[k].Go(), true + } + for i := range fixed { + if !set[i] { + if j := i - (fixed - len(m.def)); j >= 0 { + vals[i] = m.def[j] + continue + } + return nil, wrongArgs(len(args), ft) + } + } + if !ft.IsVariadic() && len(vals) > fixed { + return nil, wrongArgs(len(args), ft) + } + in := make([]reflect.Value, 0, len(vals)) + for i := range fixed { + v, err := dfArg(vals[i], ft.In(i)) + if err != nil { + return nil, err + } + in = append(in, v) + } + if ft.IsVariadic() { + tail := vals[fixed:] + if len(tail) == 1 { + if l, ok := tail[0].([]any); ok { + tail = l + } + } + for _, x := range tail { + v, err := dfArg(x, ft.In(fixed).Elem()) + if err != nil { + return nil, err + } + in = append(in, v) + } + } + return e.results(fn.Call(in)) +} + +func indexOf(list []string, s string) int { + for i, e := range list { + if e == s { + return i + } + } + return -1 +} + +// dfArg converts a Ruby value to a parameter: an any parameter takes the +// value as dfValue makes it, the others convert as for every binding. +func dfArg(x any, t reflect.Type) (reflect.Value, error) { + if t.Kind() == reflect.Interface { + v := dfValue(x) + return reflect.ValueOf(&v).Elem(), nil + } + return convert(x, t) +} + +// dfValue is a Ruby value as go-dataframe takes data and scalars: a Symbol +// is a string, a Time a time.Time, an Array a []any of such values; the rest +// (numbers, strings, booleans, nil, NDArrays, Series) is the value itself. +func dfValue(x any) any { + switch v := x.(type) { + case rb.Symbol: + return string(v) + case []any: + out := make([]any, len(v)) + for i, e := range v { + out[i] = dfValue(e) + } + return out + case rb.Value: + if v.ClassName() == "Time" { + s, _ := v.Call("to_i") + ns, _ := v.Call("nsec") + return time.Unix(s.Go().(int64), ns.Go().(int64)).UTC() + } + } + return x +} + +// results converts what a Go method returned (see out). +func (e *dfEnv) results(vals []reflect.Value) (any, error) { + if n := len(vals); n > 0 && vals[n-1].Type() == errorType { + if err, _ := vals[n-1].Interface().(error); err != nil { + return nil, goError(err) + } + vals = vals[:n-1] + } + if len(vals) == 1 { + return e.out(vals[0]), nil + } + list := make([]any, len(vals)) + for i, v := range vals { + list[i] = e.out(v) + } + return list, nil +} + +// out converts a Go result for Ruby: a time.Time is a UTC Time, a DType its +// name, slices element by element; the rest as every binding converts it. +func (e *dfEnv) out(v reflect.Value) any { + if v.Kind() == reflect.Interface { + if v.IsNil() { + return nil + } + v = v.Elem() + } + switch x := v.Interface().(type) { + case time.Time: + t, _ := e.time.Call("at", x.Unix(), int64(x.Nanosecond()), rb.Symbol("nsec")) + u, _ := t.Call("utc") + return u + case dataframe.DType: + return x.String() + case []float64, []byte: + return out(v) + } + if v.Kind() == reflect.Slice { + if v.IsNil() { + return nil + } + list := make([]any, v.Len()) + for i := range list { + list[i] = e.out(v.Index(i)) + } + return list + } + return out(v) +} + +// outAny converts a Go value of unknown type. +func (e *dfEnv) outAny(x any) any { return e.out(reflect.ValueOf(&x).Elem()) } + +// noKw rejects keyword arguments. +func noKw(c *rb.Call) error { + if len(c.Kwargs) > 0 { + return argError("unknown keyword: :%s", sortedKeys(c.Kwargs)[0]) + } + return nil +} + +// kwOnly checks the keywords are among names. +func kwOnly(c *rb.Call, names ...string) error { + for _, k := range sortedKeys(c.Kwargs) { + if !contains(names, k) { + return argError("unknown keyword: :%s (known: %s)", k, strings.Join(names, ", ")) + } + } + return nil +} + +func frameOf(c *rb.Call) *dataframe.DataFrame { return c.Self.Go().(*dataframe.DataFrame) } +func seriesOf(c *rb.Call) *dataframe.Series { return c.Self.Go().(*dataframe.Series) } + +// strs reads a String, a Symbol or an Array of them. +func strs(x any) ([]string, error) { + if l, ok := x.([]any); ok { + out := make([]string, len(l)) + for i, e := range l { + s, err := toString(e) + if err != nil { + return nil, err + } + out[i] = s + } + return out, nil + } + s, err := toString(x) + if err != nil { + return nil, typeError(x, "String or an Array of Strings") + } + return []string{s}, nil +} + +// argStrs reads every positional argument (or a single Array) as names. +func argStrs(c *rb.Call) ([]string, error) { + var list []any + for _, a := range c.Args { + list = append(list, a.Go()) + } + if len(list) == 1 { + if l, ok := list[0].([]any); ok { + list = l + } + } + return strs(list) +} + +// pairs reads the [name, value] pairs DataFrame.new and agg receive. +func pairs(xs ...any) ([][2]any, error) { + var out [][2]any + for _, x := range xs { + if x == nil { + continue + } + l, ok := x.([]any) + if !ok { + return nil, typeError(x, "Hash") + } + for _, p := range l { + kv, ok := p.([]any) + if !ok || len(kv) != 2 { + return nil, typeError(p, "a [name, value] pair") + } + out = append(out, [2]any{kv[0], kv[1]}) + } + } + return out, nil +} + +func (e *dfEnv) fromPairs(c *rb.Call) (any, error) { + ps, err := pairs(c.Args[0].Go(), c.Args[1].Go()) + if err != nil { + return nil, err + } + cols := make([]dataframe.Column, len(ps)) + for i, p := range ps { + name, err := toString(p[0]) + if err != nil { + return nil, err + } + cols[i] = dataframe.Col(name, dfValue(p[1])) + } + return e.results(reflect.ValueOf(dataframe.New).Call(colValues(cols))) +} + +func colValues(cols []dataframe.Column) []reflect.Value { + out := make([]reflect.Value, len(cols)) + for i, c := range cols { + out[i] = reflect.ValueOf(c) + } + return out +} + +// byte1 reads a one-character String as a byte; "" (or nil) is 0. +func byte1(x any) (byte, error) { + if x == nil { + return 0, nil + } + s, err := toString(x) + if err != nil { + return 0, err + } + if len(s) > 1 { + return 0, argError("expected a single character, got %q", s) + } + if s == "" { + return 0, nil + } + return s[0], nil +} + +// csvOptions converts read_csv's keywords to CSV options, in keyword order. +func csvOptions(kw map[string]rb.Value) ([]dataframe.CSVOption, error) { + var opts []dataframe.CSVOption + for _, k := range sortedKeys(kw) { + x := kw[k].Go() + var o dataframe.CSVOption + var err error + switch k { + case "sep", "comment", "quotechar": + var b byte + b, err = byte1(x) + o = map[string]func(byte) dataframe.CSVOption{ + "sep": dataframe.CSVSeparator, "comment": dataframe.CSVComment, "quotechar": dataframe.CSVQuote, + }[k](b) + case "header", "keep_default_na", "skip_blank_lines": + b, ok := x.(bool) + if !ok { + return nil, typeError(x, "true or false") + } + o = map[string]func(bool) dataframe.CSVOption{ + "header": dataframe.CSVHeader, "keep_default_na": dataframe.CSVKeepDefaultNA, + "skip_blank_lines": dataframe.CSVSkipBlankLines, + }[k](b) + case "names", "usecols", "na_values", "parse_dates", "true_values", "false_values": + var s []string + s, err = strs(x) + o = map[string]func(...string) dataframe.CSVOption{ + "names": dataframe.CSVNames, "usecols": dataframe.CSVUseCols, "na_values": dataframe.CSVNAValues, + "parse_dates": dataframe.CSVParseDates, "true_values": dataframe.CSVTrueValues, + "false_values": dataframe.CSVFalseValues, + }[k](s...) + case "nrows", "skiprows": + var n int64 + n, err = toInt(x) + o = map[string]func(int) dataframe.CSVOption{ + "nrows": dataframe.CSVNRows, "skiprows": dataframe.CSVSkipRows, + }[k](int(n)) + case "date_format": + var s string + s, err = toString(x) + o = dataframe.CSVDateFormat(s) + case "index_col": + if i, ok := x.(int64); ok { + o = dataframe.CSVIndexColumn(int(i)) + break + } + var s string + s, err = toString(x) + o = dataframe.CSVIndexCol(s) + case "dtype": + var v reflect.Value + v, err = convert(x, reflect.TypeFor[map[string]dataframe.DType]()) + if err == nil { + o = dataframe.CSVDTypes(v.Interface().(map[string]dataframe.DType)) + } + default: + return nil, argError("unknown keyword: :%s (known: comment, date_format, dtype, false_values, header, index_col, keep_default_na, na_values, names, nrows, parse_dates, quotechar, sep, skip_blank_lines, skiprows, true_values, usecols)", k) + } + if err != nil { + return nil, err + } + opts = append(opts, o) + } + return opts, nil +} + +// readArgs reads (text_or_path, **opts). +func readArgs(c *rb.Call) (string, []dataframe.CSVOption, error) { + if err := arity(c, 1, 1); err != nil { + return "", nil, err + } + s, err := toString(c.Args[0].Go()) + if err != nil { + return "", nil, err + } + opts, err := csvOptions(c.Kwargs) + return s, opts, err +} + +func (e *dfEnv) readCSV(c *rb.Call) (any, error) { + text, opts, err := readArgs(c) + if err != nil { + return nil, err + } + return e.result(dataframe.ReadCSV(strings.NewReader(text), opts...)) +} + +func (e *dfEnv) readCSVFile(c *rb.Call) (any, error) { + path, opts, err := readArgs(c) + if err != nil { + return nil, err + } + return e.result(dataframe.ReadCSVFile(path, opts...)) +} + +// result converts a Go (value, error) pair. +func (e *dfEnv) result(v any, err error) (any, error) { + if err != nil { + return nil, goError(err) + } + return e.outAny(v), nil +} + +func (e *dfEnv) fromArray(c *rb.Call) (any, error) { + if err := noKw(c); err != nil { + return nil, err + } + if err := arity(c, 1, 2); err != nil { + return nil, err + } + a, ok := c.Args[0].Go().(*nd.Array) + if !ok { + return nil, typeError(c.Args[0].Go(), "NDArray") + } + var names []string + if len(c.Args) == 2 { + n, err := strs(c.Args[1].Go()) + if err != nil { + return nil, err + } + names = n + } + return e.result(dataframe.FromArray(a, names...)) +} + +// seriesArgs reads every positional argument as a Series. +func seriesArgs(c *rb.Call) ([]*dataframe.Series, error) { + out := make([]*dataframe.Series, len(c.Args)) + for i, a := range c.Args { + s, ok := a.Go().(*dataframe.Series) + if !ok { + return nil, typeError(a.Go(), "DataFrame::Series") + } + out[i] = s + } + return out, nil +} + +func (e *dfEnv) fromSeries(c *rb.Call) (any, error) { + if err := noKw(c); err != nil { + return nil, err + } + ss, err := seriesArgs(c) + if err != nil { + return nil, err + } + return e.result(dataframe.FromSeries(ss...)) +} + +func (e *dfEnv) concat(c *rb.Call) (any, error) { + var opts dataframe.ConcatOptions + if err := options(&opts, c.Kwargs); err != nil { + return nil, err + } + frames := make([]*dataframe.DataFrame, len(c.Args)) + for i, a := range c.Args { + f, ok := a.Go().(*dataframe.DataFrame) + if !ok { + return nil, typeError(a.Go(), "DataFrame") + } + frames[i] = f + } + return e.result(dataframe.ConcatWith(opts, frames...)) +} + +// mask reads a filter mask: a bool Series as it is, an Array as a []bool. +func mask(x any) (any, error) { + if s, ok := x.(*dataframe.Series); ok { + return s, nil + } + v, err := convert(x, reflect.TypeFor[[]bool]()) + if err != nil { + return nil, err + } + return v.Interface(), nil +} + +// isMask reports whether a [] argument is a mask: a Series, or an Array +// whose first element is a boolean. +func isMask(x any) bool { + switch v := x.(type) { + case *dataframe.Series: + return true + case []any: + if len(v) > 0 { + _, ok := v[0].(bool) + return ok + } + } + return false +} + +func (e *dfEnv) frameIndex(c *rb.Call) (any, error) { + if err := plExact(c, 1); err != nil { + return nil, err + } + df := frameOf(c) + x := c.Args[0].Go() + if isMask(x) { + return e.frameFilter(c) + } + if l, ok := x.([]any); ok { + names, err := strs(l) + if err != nil { + return nil, err + } + return e.result(df.Select(names...)) + } + name, err := toString(x) + if err != nil { + return nil, typeError(x, "a column name, an Array of names or a mask") + } + return e.result(df.Column(name)) +} + +func (e *dfEnv) frameFilter(c *rb.Call) (any, error) { + if err := plExact(c, 1); err != nil { + return nil, err + } + m, err := mask(c.Args[0].Go()) + if err != nil { + return nil, err + } + return e.result(frameOf(c).Filter(m)) +} + +// orderedHash builds a Ruby Hash from pairs, in their order. +func orderedHash(c *rb.Call, ps []any) (any, error) { + list, _ := c.Interp.ValueOf(ps) + return list.Call("to_h") +} + +func (e *dfEnv) dtypes(c *rb.Call) (any, error) { + if err := plExact(c, 0); err != nil { + return nil, err + } + df := frameOf(c) + dts := df.DTypes() + ps := make([]any, len(dts)) + for i, name := range df.Columns() { + ps[i] = []any{name, dts[i].String()} + } + return orderedHash(c, ps) +} + +func (e *dfEnv) toH(c *rb.Call) (any, error) { + if err := plExact(c, 0); err != nil { + return nil, err + } + var ps []any + for _, s := range frameOf(c).Series() { + ps = append(ps, []any{s.Name(), e.outAny(s.Items())}) + } + return orderedHash(c, ps) +} + +// csvWriteOptions converts to_csv's keywords. +func csvWriteOptions(kw map[string]rb.Value) ([]dataframe.CSVWriteOption, error) { + var opts []dataframe.CSVWriteOption + for _, k := range sortedKeys(kw) { + x := kw[k].Go() + switch k { + case "sep": + b, err := byte1(x) + if err != nil { + return nil, err + } + opts = append(opts, dataframe.CSVWriteSeparator(b)) + case "header", "index": + b, ok := x.(bool) + if !ok { + return nil, typeError(x, "true or false") + } + f := dataframe.CSVWriteHeader + if k == "index" { + f = dataframe.CSVWriteIndex + } + opts = append(opts, f(b)) + case "na_rep", "line_terminator": + s, err := toString(x) + if err != nil { + return nil, err + } + f := dataframe.CSVWriteNARep + if k == "line_terminator" { + f = dataframe.CSVWriteLineTerminator + } + opts = append(opts, f(s)) + default: + return nil, argError("unknown keyword: :%s (known: header, index, line_terminator, na_rep, sep)", k) + } + } + return opts, nil +} + +func (e *dfEnv) toCSV(c *rb.Call) (any, error) { + if err := arity(c, 0, 0); err != nil { + return nil, err + } + opts, err := csvWriteOptions(c.Kwargs) + if err != nil { + return nil, err + } + var b bytes.Buffer + _ = frameOf(c).WriteCSV(&b, opts...) // writing to memory cannot fail + return b.String(), nil +} + +func (e *dfEnv) writeCSV(c *rb.Call) (any, error) { + if err := arity(c, 1, 1); err != nil { + return nil, err + } + path, err := toString(c.Args[0].Go()) + if err != nil { + return nil, err + } + opts, err := csvWriteOptions(c.Kwargs) + if err != nil { + return nil, err + } + if err := frameOf(c).WriteCSVFile(path, opts...); err != nil { + return nil, rb.NewError("IOError", err.Error()) + } + return nil, nil +} + +func (e *dfEnv) sortValues(c *rb.Call) (any, error) { + if err := kwOnly(c, "ascending"); err != nil { + return nil, err + } + if err := arity(c, 1, 1); err != nil { + return nil, err + } + by, err := strs(c.Args[0].Go()) + if err != nil { + return nil, err + } + var asc []bool + if a, ok := c.Kwargs["ascending"]; ok { + x := a.Go() + if _, isList := x.([]any); !isList { + x = []any{x} + } + v, err := convert(x, reflect.TypeFor[[]bool]()) + if err != nil { + return nil, err + } + asc = v.Interface().([]bool) + } + return e.result(frameOf(c).SortValues(by, asc...)) +} + +func (e *dfEnv) groupBy(c *rb.Call) (any, error) { + if err := noKw(c); err != nil { + return nil, err + } + keys, err := argStrs(c) + if err != nil { + return nil, err + } + return e.result(frameOf(c).GroupBy(keys...)) +} + +func (e *dfEnv) dropNA(c *rb.Call) (any, error) { + if err := kwOnly(c, "axis", "how", "thresh"); err != nil { + return nil, err + } + df := frameOf(c) + if a, ok := c.Kwargs["axis"]; ok { + n, err := toInt(a.Go()) + if err != nil { + return nil, err + } + if n == 1 { + return df.DropNAColumns(), nil + } + if n != 0 { + return nil, argError("axis must be 0 or 1, got %d", n) + } + } + subset, err := argStrs(c) + if err != nil { + return nil, err + } + if t, ok := c.Kwargs["thresh"]; ok { + n, err := toInt(t.Go()) + if err != nil { + return nil, err + } + return e.result(df.DropNAThresh(int(n), subset...)) + } + if h, ok := c.Kwargs["how"]; ok { + how, err := toString(h.Go()) + if err != nil { + return nil, err + } + switch how { + case "all": + return e.result(df.DropNAAll(subset...)) + case "any": + default: + return nil, argError("how must be :any or :all, got %q", how) + } + } + return e.result(df.DropNA(subset...)) +} + +// valueMap reads a Hash of column name to value. +func valueMap(x map[any]any) (map[string]any, error) { + out := make(map[string]any, len(x)) + for k, v := range x { + s, err := toString(k) + if err != nil { + return nil, err + } + out[s] = dfValue(v) + } + return out, nil +} + +// hashArg reads a call's single Hash argument, given as a Hash or as +// keywords. +func hashArg(c *rb.Call) (map[any]any, bool) { + if len(c.Args) == 0 && len(c.Kwargs) > 0 { + h := map[any]any{} + for k, v := range c.Kwargs { + h[k] = v.Go() + } + return h, true + } + if len(c.Args) == 1 && len(c.Kwargs) == 0 { + h, ok := c.Args[0].Go().(map[any]any) + return h, ok + } + return nil, false +} + +func (e *dfEnv) fillNA(c *rb.Call) (any, error) { + if h, ok := hashArg(c); ok { + m, err := valueMap(h) + if err != nil { + return nil, err + } + return e.result(frameOf(c).FillNAColumns(m)) + } + if err := plExact(c, 1); err != nil { + return nil, err + } + return frameOf(c).FillNA(dfValue(c.Args[0].Go())), nil +} + +func (e *dfEnv) asType(c *rb.Call) (any, error) { + if h, ok := hashArg(c); ok { + v, err := convert(h, reflect.TypeFor[map[string]dataframe.DType]()) + if err != nil { + return nil, err + } + return e.result(frameOf(c).AsType(v.Interface().(map[string]dataframe.DType))) + } + if err := plExact(c, 1); err != nil { + return nil, err + } + v, err := convert(c.Args[0].Go(), dtypeType) + if err != nil { + return nil, err + } + return e.result(frameOf(c).AsTypeAll(v.Interface().(dataframe.DType))) +} + +func (e *dfEnv) rename(c *rb.Call) (any, error) { + h, ok := hashArg(c) + if !ok { + return nil, argError("rename takes a Hash of old name to new name") + } + v, err := convert(h, reflect.TypeFor[map[string]string]()) + if err != nil { + return nil, err + } + return frameOf(c).Rename(v.Interface().(map[string]string)), nil +} + +// otherFrame reads the single positional DataFrame of merge and join. +func otherFrame(c *rb.Call) (*dataframe.DataFrame, error) { + if err := arity(c, 1, 1); err != nil { + return nil, err + } + o, ok := c.Args[0].Go().(*dataframe.DataFrame) + if !ok { + return nil, typeError(c.Args[0].Go(), "DataFrame") + } + return o, nil +} + +func (e *dfEnv) merge(c *rb.Call) (any, error) { + right, err := otherFrame(c) + if err != nil { + return nil, err + } + var opts dataframe.MergeOptions + if err := options(&opts, c.Kwargs); err != nil { + return nil, err + } + return e.result(frameOf(c).Merge(right, opts)) +} + +// kwStrings reads string keywords with defaults. +func kwStrings(c *rb.Call, defaults map[string]string, names ...string) ([]string, error) { + if err := kwOnly(c, names...); err != nil { + return nil, err + } + out := make([]string, len(names)) + for i, n := range names { + out[i] = defaults[n] + if v, ok := c.Kwargs[n]; ok { + s, err := toString(v.Go()) + if err != nil { + return nil, err + } + out[i] = s + } + } + return out, nil +} + +func (e *dfEnv) join(c *rb.Call) (any, error) { + s, err := kwStrings(c, map[string]string{"how": "left"}, "how", "lsuffix", "rsuffix") + if err != nil { + return nil, err + } + other, err := otherFrame(c) + if err != nil { + return nil, err + } + return e.result(frameOf(c).Join(other, s[0], s[1], s[2])) +} + +func (e *dfEnv) melt(c *rb.Call) (any, error) { + if err := kwOnly(c, "id_vars", "value_vars"); err != nil { + return nil, err + } + if err := arity(c, 0, 0); err != nil { + return nil, err + } + vars := [2][]string{} + for i, k := range []string{"id_vars", "value_vars"} { + if v, ok := c.Kwargs[k]; ok { + s, err := strs(v.Go()) + if err != nil { + return nil, err + } + vars[i] = s + } + } + return e.result(frameOf(c).Melt(vars[0], vars[1])) +} + +// exprArgs reads (expr, **vars). +func exprArgs(c *rb.Call) (string, []map[string]any, error) { + if err := arity(c, 1, 1); err != nil { + return "", nil, err + } + expr, err := toString(c.Args[0].Go()) + if err != nil { + return "", nil, err + } + var vars []map[string]any + if len(c.Kwargs) > 0 { + m := map[string]any{} + for k, v := range c.Kwargs { + m[k] = dfValue(v.Go()) + } + vars = append(vars, m) + } + return expr, vars, nil +} + +func (e *dfEnv) query(c *rb.Call) (any, error) { + expr, vars, err := exprArgs(c) + if err != nil { + return nil, err + } + return e.result(frameOf(c).Query(expr, vars...)) +} + +func (e *dfEnv) eval(c *rb.Call) (any, error) { + expr, vars, err := exprArgs(c) + if err != nil { + return nil, err + } + return e.result(frameOf(c).Eval(expr, vars...)) +} + +// selector reads a row (or column) selector; positional says whether +// numbers and ranges are positions (iloc) or labels (loc). +func selector(x any, positional bool) (dataframe.Selector, error) { + switch v := x.(type) { + case nil: + return dataframe.All(), nil + case rb.Symbol: + if v == "all" { + return dataframe.All(), nil + } + case *dataframe.Series: + return dataframe.Mask(v), nil + case []any: + if isMask(v) { + m, err := mask(v) + if err != nil { + return dataframe.Selector{}, err + } + return dataframe.Mask(m), nil + } + if positional { + p, err := convert(v, reflect.TypeFor[[]int]()) + if err != nil { + return dataframe.Selector{}, err + } + return dataframe.Pos(p.Interface().([]int)...), nil + } + return dataframe.Labels(dfValue(v).([]any)...), nil + case int64: + if positional { + return dataframe.Pos(int(v)), nil + } + case rb.Value: + if v.ClassName() == "Range" { + return rangeSelector(v, positional) + } + } + if positional { + return dataframe.Selector{}, typeError(x, "a position, an Array, a Range or a mask") + } + return dataframe.Labels(dfValue(x)), nil +} + +// rangeSelector reads a Range: positions (an exclusive or inclusive end, nil +// for open) or an inclusive range of labels. +func rangeSelector(r rb.Value, positional bool) (dataframe.Selector, error) { + b, _ := r.Call("begin") + en, _ := r.Call("end") + ex, _ := r.Call("exclude_end?") + lo, hi := b.Go(), en.Go() + if !positional { + if ex.Go() == true { + return dataframe.Selector{}, argError("a label range includes its end: use a..b, not a...b") + } + return dataframe.LabelRange(dfValue(lo), dfValue(hi)), nil + } + start := int64(0) + if lo != nil { + s, err := toInt(lo) + if err != nil { + return dataframe.Selector{}, err + } + start = s + } + if hi == nil { + return dataframe.SliceFrom(int(start)), nil + } + stop, err := toInt(hi) + if err != nil { + return dataframe.Selector{}, err + } + if ex.Go() != true { + stop++ + } + return dataframe.Slice(int(start), int(stop)), nil +} + +func (e *dfEnv) frameIloc(c *rb.Call) (any, error) { + if err := noKw(c); err != nil { + return nil, err + } + if err := arity(c, 1, 2); err != nil { + return nil, err + } + sels := make([]dataframe.Selector, len(c.Args)) + for i, a := range c.Args { + s, err := selector(a.Go(), true) + if err != nil { + return nil, err + } + sels[i] = s + } + return e.result(frameOf(c).Iloc(sels[0], sels[1:]...)) +} + +func (e *dfEnv) frameLoc(c *rb.Call) (any, error) { + if err := noKw(c); err != nil { + return nil, err + } + if err := arity(c, 1, 2); err != nil { + return nil, err + } + rows, err := selector(c.Args[0].Go(), false) + if err != nil { + return nil, err + } + var cols []string + if len(c.Args) == 2 { + if cols, err = strs(c.Args[1].Go()); err != nil { + return nil, err + } + } + return e.result(frameOf(c).Loc(rows, cols...)) +} + +func (e *dfEnv) apply(c *rb.Call) (any, error) { + if err := noKw(c); err != nil { + return nil, err + } + if err := arity(c, 0, 1); err != nil { + return nil, err + } + b, err := needBlock(c) + if err != nil { + return nil, err + } + name := "" + if len(c.Args) == 1 { + if name, err = toString(c.Args[0].Go()); err != nil { + return nil, err + } + } + df := frameOf(c) + cols := df.Columns() + var blockErr error + s, err := df.Apply(name, func(r dataframe.Row) any { + if blockErr != nil { + return nil + } + ps := make([]any, len(cols)) + for i, col := range cols { + ps[i] = []any{col, e.outAny(r.Get(col))} + } + h, _ := orderedHash(c, ps) + v, err := b.Call(h) + if err != nil { + blockErr = err + return nil + } + return dfValue(v.Go()) + }) + if blockErr != nil { + return nil, blockErr + } + return e.result(s, err) +} + +func (e *dfEnv) resample(c *rb.Call) (any, error) { + if err := kwOnly(c, "on"); err != nil { + return nil, err + } + if err := arity(c, 1, 1); err != nil { + return nil, err + } + rule, err := toString(c.Args[0].Go()) + if err != nil { + return nil, err + } + if on, ok := c.Kwargs["on"]; ok { + col, err := toString(on.Go()) + if err != nil { + return nil, err + } + return e.result(frameOf(c).ResampleOn(col, rule)) + } + return e.result(frameOf(c).Resample(rule)) +} + +// newIndexArg reads an index: an Index as it is, else labels for NewIndex. +func newIndexArg(x any) (*dataframe.Index, error) { + if ix, ok := x.(*dataframe.Index); ok { + return ix, nil + } + ix, err := dataframe.NewIndex("", dfValue(x)) + if err != nil { + return nil, goError(err) + } + return ix, nil +} + +func (e *dfEnv) newSeries(c *rb.Call) (any, error) { + if err := kwOnly(c, "index", "name"); err != nil { + return nil, err + } + if err := arity(c, 1, 1); err != nil { + return nil, err + } + name := "" + if n, ok := c.Kwargs["name"]; ok { + s, err := toString(n.Go()) + if err != nil { + return nil, err + } + name = s + } + data := dfValue(c.Args[0].Go()) + if ixv, ok := c.Kwargs["index"]; ok { + ix, err := newIndexArg(ixv.Go()) + if err != nil { + return nil, err + } + return e.result(dataframe.NewSeriesIndexed(name, data, ix)) + } + return e.result(dataframe.NewSeries(name, data)) +} + +func (e *dfEnv) seriesIndex(c *rb.Call) (any, error) { + if err := plExact(c, 1); err != nil { + return nil, err + } + x := c.Args[0].Go() + s := seriesOf(c) + if isMask(x) { + return e.seriesFilter(c) + } + if i, ok := x.(int64); ok { + return e.result(s.At(int(i))) + } + sel, err := selector(x, true) + if err != nil { + return nil, err + } + return e.result(s.Iloc(sel)) +} + +func (e *dfEnv) seriesFilter(c *rb.Call) (any, error) { + if err := plExact(c, 1); err != nil { + return nil, err + } + m, err := mask(c.Args[0].Go()) + if err != nil { + return nil, err + } + return e.result(seriesOf(c).Filter(m)) +} + +func (e *dfEnv) seriesMap(c *rb.Call) (any, error) { + if err := plExact(c, 0); err != nil { + return nil, err + } + b, err := needBlock(c) + if err != nil { + return nil, err + } + var blockErr error + s, err := seriesOf(c).Map(func(x any) any { + if blockErr != nil { + return nil + } + v, err := b.Call(e.outAny(x)) + if err != nil { + blockErr = err + return nil + } + return dfValue(v.Go()) + }) + if blockErr != nil { + return nil, blockErr + } + return e.result(s, err) +} + +func (e *dfEnv) rolling(c *rb.Call) (any, error) { + if err := kwOnly(c, "min_periods"); err != nil { + return nil, err + } + if err := arity(c, 1, 1); err != nil { + return nil, err + } + w, err := toInt(c.Args[0].Go()) + if err != nil { + return nil, err + } + if mp, ok := c.Kwargs["min_periods"]; ok { + n, err := toInt(mp.Go()) + if err != nil { + return nil, err + } + return e.result(seriesOf(c).RollingMin(int(w), int(n))) + } + return e.result(seriesOf(c).Rolling(int(w))) +} + +func (e *dfEnv) seriesSel(c *rb.Call, positional bool) (any, error) { + if err := plExact(c, 1); err != nil { + return nil, err + } + sel, err := selector(c.Args[0].Go(), positional) + if err != nil { + return nil, err + } + if positional { + return e.result(seriesOf(c).Iloc(sel)) + } + return e.result(seriesOf(c).Loc(sel)) +} + +func (e *dfEnv) seriesIloc(c *rb.Call) (any, error) { return e.seriesSel(c, true) } +func (e *dfEnv) seriesLoc(c *rb.Call) (any, error) { return e.seriesSel(c, false) } + +func (e *dfEnv) toDatetime(c *rb.Call) (any, error) { + if err := noKw(c); err != nil { + return nil, err + } + if err := arity(c, 0, 1); err != nil { + return nil, err + } + var layout []string + if len(c.Args) == 1 { + s, err := toString(c.Args[0].Go()) + if err != nil { + return nil, err + } + layout = append(layout, s) + } + return e.result(dataframe.ToDatetime(seriesOf(c), layout...)) +} + +// agg is GroupBy#agg and Resampler#agg after the Ruby prelude: [col, +// funcs] pairs, funcs a name or an Array of names. +func (e *dfEnv) agg(c *rb.Call) (any, error) { + ps, err := pairs(c.Args[0].Go(), c.Args[1].Go()) + if err != nil { + return nil, err + } + specs := make([]dataframe.AggSpec, len(ps)) + for i, p := range ps { + col, err := toString(p[0]) + if err != nil { + return nil, err + } + fns, err := strs(p[1]) + if err != nil { + return nil, err + } + specs[i] = dataframe.Agg(col, fns...) + } + fn := reflect.ValueOf(c.Self.Go()).MethodByName("Agg") + in := make([]reflect.Value, len(specs)) + for i, s := range specs { + in[i] = reflect.ValueOf(s) + } + return e.results(fn.Call(in)) +} + +func (e *dfEnv) groups(c *rb.Call) (any, error) { + if err := plExact(c, 0); err != nil { + return nil, err + } + keys, frames := c.Self.Go().(*dataframe.GroupBy).Groups() + out := make([]any, len(keys)) + for i := range keys { + out[i] = []any{e.outAny(keys[i]), frames[i]} + } + return out, nil +} + +func (e *dfEnv) newIndex(c *rb.Call) (any, error) { + if err := kwOnly(c, "name"); err != nil { + return nil, err + } + if err := arity(c, 1, 1); err != nil { + return nil, err + } + name := "" + if n, ok := c.Kwargs["name"]; ok { + s, err := toString(n.Go()) + if err != nil { + return nil, err + } + name = s + } + return e.result(dataframe.NewIndex(name, dfValue(c.Args[0].Go()))) +} + +func (e *dfEnv) rangeIndex(c *rb.Call) (any, error) { + if err := plExact(c, 1); err != nil { + return nil, err + } + n, err := toInt(c.Args[0].Go()) + if err != nil { + return nil, err + } + return dataframe.NewRangeIndex(int(n)), nil +} + +var dfFrameMethods = []dfM{ + {ruby: "columns", goName: "Columns"}, + {ruby: "length", goName: "Len"}, + {ruby: "size", goName: "Len"}, + {ruby: "shape", goName: "Shape"}, + {ruby: "describe", goName: "Describe"}, + {ruby: "copy", goName: "Copy"}, + {ruby: "count", goName: "Count"}, + {ruby: "mean", goName: "Mean"}, + {ruby: "sum", goName: "Sum"}, + {ruby: "std", goName: "Std"}, + {ruby: "var", goName: "Var"}, + {ruby: "min", goName: "Min"}, + {ruby: "max", goName: "Max"}, + {ruby: "median", goName: "Median"}, + {ruby: "isna", goName: "IsNA"}, + {ruby: "notna", goName: "NotNA"}, + {ruby: "ffill", goName: "FFill"}, + {ruby: "bfill", goName: "BFill"}, + {ruby: "transpose", goName: "T"}, + {ruby: "t", goName: "T"}, + {ruby: "drop", goName: "Drop"}, + {ruby: "select", goName: "Select"}, + {ruby: "set_index", goName: "SetIndex"}, + {ruby: "reset_index", goName: "ResetIndex", kw: []string{"drop"}, def: []any{false}}, + {ruby: "sort_index", goName: "SortIndex", kw: []string{"ascending"}, def: []any{true}}, + {ruby: "reindex", goName: "Reindex"}, + {ruby: "reindex_columns", goName: "ReindexColumns"}, + {ruby: "set_columns", goName: "SetColumns"}, + {ruby: "column", goName: "Column"}, + {ruby: "column_at", goName: "ColumnAt"}, + {ruby: "series", goName: "Series"}, + {ruby: "iat", goName: "IAt"}, + {ruby: "at", goName: "At"}, + {ruby: "values", goName: "Values"}, + {ruby: "to_ndarray", goName: "Values"}, + {ruby: "to_s", goName: "String"}, + {ruby: "inspect", goName: "String"}, + {ruby: "to_html", goName: "HTML"}, + {ruby: "display", goName: "Display"}, + {ruby: "index", goName: "Index"}, + {ruby: "with_column", goName: "WithColumn"}, + {ruby: "with_index", goName: "WithIndex"}, + {ruby: "pivot", goName: "Pivot", kw: []string{"index", "columns", "values"}}, + {ruby: "pivot_table", goName: "PivotTable", kw: []string{"index", "columns", "values", "aggfunc"}, def: []any{"mean"}}, + {ruby: "stack", goName: "Stack"}, + {ruby: "unstack", goName: "Unstack"}, + {ruby: "to_period", goName: "ToPeriod"}, + {ruby: "head", goName: "Head", kw: []string{"n"}, def: []any{int64(5)}}, + {ruby: "tail", goName: "Tail", kw: []string{"n"}, def: []any{int64(5)}}, + {ruby: "round", goName: "Round", kw: []string{"decimals"}, def: []any{int64(0)}}, + {ruby: "shift", goName: "Shift", kw: []string{"periods"}, def: []any{int64(1)}}, + {ruby: "add", goName: "Add"}, + {ruby: "+", goName: "Add"}, + {ruby: "sub", goName: "Sub"}, + {ruby: "-", goName: "Sub"}, + {ruby: "mul", goName: "Mul"}, + {ruby: "*", goName: "Mul"}, + {ruby: "div", goName: "Div"}, + {ruby: "/", goName: "Div"}, +} + +var dfSeriesMethods = []dfM{ + {ruby: "name", goName: "Name"}, + {ruby: "length", goName: "Len"}, + {ruby: "size", goName: "Len"}, + {ruby: "count", goName: "Count"}, + {ruby: "dtype", goName: "DType"}, + {ruby: "index", goName: "Index"}, + {ruby: "values", goName: "Values"}, + {ruby: "to_ndarray", goName: "Values"}, + {ruby: "floats", goName: "Floats"}, + {ruby: "to_a", goName: "Items"}, + {ruby: "valid", goName: "Valid"}, + {ruby: "get", goName: "Get"}, + {ruby: "mean", goName: "Mean"}, + {ruby: "sum", goName: "SumValue"}, + {ruby: "std", goName: "Std"}, + {ruby: "var", goName: "VarDDOF", kw: []string{"ddof"}, def: []any{int64(1)}}, + {ruby: "min", goName: "Min"}, + {ruby: "max", goName: "Max"}, + {ruby: "median", goName: "Median"}, + {ruby: "prod", goName: "Prod"}, + {ruby: "nunique", goName: "NUnique"}, + {ruby: "quantile", goName: "Quantile", kw: []string{"q"}, def: []any{0.5}}, + {ruby: "agg", goName: "Agg"}, + {ruby: "head", goName: "Head", kw: []string{"n"}, def: []any{int64(5)}}, + {ruby: "tail", goName: "Tail", kw: []string{"n"}, def: []any{int64(5)}}, + {ruby: "unique", goName: "Unique"}, + {ruby: "value_counts", goName: "ValueCounts"}, + {ruby: "copy", goName: "Copy"}, + {ruby: "to_frame", goName: "ToFrame"}, + {ruby: "reset_index", goName: "ResetIndex"}, + {ruby: "isna", goName: "IsNA"}, + {ruby: "notna", goName: "NotNA"}, + {ruby: "dropna", goName: "DropNA"}, + {ruby: "fillna", goName: "FillNA"}, + {ruby: "ffill", goName: "FFill"}, + {ruby: "bfill", goName: "BFill"}, + {ruby: "shift", goName: "Shift", kw: []string{"periods"}, def: []any{int64(1)}}, + {ruby: "diff", goName: "Diff", kw: []string{"periods"}, def: []any{int64(1)}}, + {ruby: "pct_change", goName: "PctChange", kw: []string{"periods"}, def: []any{int64(1)}}, + {ruby: "cumsum", goName: "CumSum"}, + {ruby: "cumprod", goName: "CumProd"}, + {ruby: "cummax", goName: "CumMax"}, + {ruby: "cummin", goName: "CumMin"}, + {ruby: "abs", goName: "Abs"}, + {ruby: "neg", goName: "Neg"}, + {ruby: "-@", goName: "Neg"}, + {ruby: "round", goName: "Round", kw: []string{"decimals"}, def: []any{int64(0)}}, + {ruby: "rename", goName: "Rename"}, + {ruby: "astype", goName: "AsType"}, + {ruby: "reindex", goName: "Reindex"}, + {ruby: "with_index", goName: "SetIndex"}, + {ruby: "sort_values", goName: "SortValues", kw: []string{"ascending"}, def: []any{true}}, + {ruby: "sort_index", goName: "SortIndex", kw: []string{"ascending"}, def: []any{true}}, + {ruby: "between", goName: "Between"}, + {ruby: "isin", goName: "IsIn"}, + {ruby: "dt", goName: "Dt"}, + {ruby: "to_period", goName: "ToPeriod"}, + {ruby: "to_timestamp", goName: "ToTimestamp"}, + {ruby: "unstack", goName: "Unstack"}, + {ruby: "to_s", goName: "String"}, + {ruby: "inspect", goName: "String"}, + {ruby: "to_html", goName: "HTML"}, + {ruby: "display", goName: "Display"}, + {ruby: "+", goName: "Add"}, + {ruby: "add", goName: "Add"}, + {ruby: "-", goName: "Sub"}, + {ruby: "sub", goName: "Sub"}, + {ruby: "*", goName: "Mul"}, + {ruby: "mul", goName: "Mul"}, + {ruby: "/", goName: "Div"}, + {ruby: "div", goName: "Div"}, + {ruby: "%", goName: "Mod"}, + {ruby: "mod", goName: "Mod"}, + {ruby: "**", goName: "Pow"}, + {ruby: "pow", goName: "Pow"}, + {ruby: "floordiv", goName: "FloorDiv"}, + {ruby: ">", goName: "Gt"}, + {ruby: "gt", goName: "Gt"}, + {ruby: "<", goName: "Lt"}, + {ruby: "lt", goName: "Lt"}, + {ruby: ">=", goName: "Ge"}, + {ruby: "ge", goName: "Ge"}, + {ruby: "<=", goName: "Le"}, + {ruby: "le", goName: "Le"}, + {ruby: "eq", goName: "Eq"}, + {ruby: "ne", goName: "Ne"}, + {ruby: "&", goName: "And"}, + {ruby: "|", goName: "Or"}, + {ruby: "^", goName: "Xor"}, + {ruby: "~", goName: "Not"}, + {ruby: "invert", goName: "Not"}, +} + +var dfGroupMethods = []dfM{ + {ruby: "mean", goName: "Mean"}, + {ruby: "sum", goName: "Sum"}, + {ruby: "count", goName: "Count"}, + {ruby: "min", goName: "Min"}, + {ruby: "max", goName: "Max"}, + {ruby: "median", goName: "Median"}, + {ruby: "std", goName: "Std"}, + {ruby: "var", goName: "Var"}, + {ruby: "first", goName: "First"}, + {ruby: "last", goName: "Last"}, + {ruby: "nunique", goName: "NUnique"}, + {ruby: "size", goName: "Size"}, + {ruby: "ngroups", goName: "NGroups"}, + {ruby: "n_groups", goName: "NGroups"}, + {ruby: "select", goName: "Select"}, + {ruby: "[]", goName: "Select"}, + {ruby: "transform", goName: "Transform"}, + {ruby: "to_s", goName: "String"}, + {ruby: "inspect", goName: "String"}, +} + +var dfResamplerMethods = []dfM{ + {ruby: "mean", goName: "Mean"}, + {ruby: "sum", goName: "Sum"}, + {ruby: "count", goName: "Count"}, + {ruby: "min", goName: "Min"}, + {ruby: "max", goName: "Max"}, + {ruby: "median", goName: "Median"}, + {ruby: "std", goName: "Std"}, + {ruby: "first", goName: "First"}, + {ruby: "last", goName: "Last"}, + {ruby: "size", goName: "Size"}, +} + +var dfRollingMethods = []dfM{ + {ruby: "mean", goName: "Mean"}, + {ruby: "sum", goName: "Sum"}, + {ruby: "min", goName: "Min"}, + {ruby: "max", goName: "Max"}, + {ruby: "std", goName: "Std"}, + {ruby: "median", goName: "Median"}, +} + +var dfIndexMethods = []dfM{ + {ruby: "to_a", goName: "Labels"}, + {ruby: "labels", goName: "Labels"}, + {ruby: "length", goName: "Len"}, + {ruby: "size", goName: "Len"}, + {ruby: "name", goName: "Name"}, + {ruby: "names", goName: "Names"}, + {ruby: "nlevels", goName: "NLevels"}, + {ruby: "range?", goName: "IsRange"}, + {ruby: "label", goName: "Label"}, + {ruby: "[]", goName: "Label"}, + {ruby: "get", goName: "Get"}, + {ruby: "values", goName: "Values"}, + {ruby: "dtype", goName: "DType"}, + {ruby: "level", goName: "Level"}, + {ruby: "rename", goName: "Rename"}, + {ruby: "to_s", goName: "String"}, + {ruby: "inspect", goName: "String"}, + {ruby: "display", goName: "Display"}, +} + +var dfDatetimeMethods = []dfM{ + {ruby: "year", goName: "Year"}, + {ruby: "month", goName: "Month"}, + {ruby: "day", goName: "Day"}, + {ruby: "hour", goName: "Hour"}, + {ruby: "minute", goName: "Minute"}, + {ruby: "second", goName: "Second"}, + {ruby: "day_of_week", goName: "DayOfWeek"}, + {ruby: "dayofweek", goName: "DayOfWeek"}, + {ruby: "day_of_year", goName: "DayOfYear"}, + {ruby: "dayofyear", goName: "DayOfYear"}, + {ruby: "normalize", goName: "Normalize"}, + {ruby: "strftime", goName: "Strftime"}, + {ruby: "inspect", goName: "Year"}, +} diff --git a/plot.go b/plot.go index a3a3d62..a800998 100644 --- a/plot.go +++ b/plot.go @@ -102,6 +102,9 @@ import ( func registerPlot(in *rb.Interp) error { d := &definer{in: in} m := d.module("Plot") + if d.err != nil { + return d.err + } fig := d.class(m, "Figure") ax := d.class(m, "Axes") mp := d.class(m, "Mappable") From 81e17dffbd65de34aef32720269cb049cb692281 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Sat, 17 Oct 2026 22:30:59 +0200 Subject: [PATCH 08/20] DataFrame: DatetimeFields keeps the default inspect Co-Authored-By: Claude Opus 5.5 --- dataframe.go | 1 - 1 file changed, 1 deletion(-) diff --git a/dataframe.go b/dataframe.go index c8e8054..2b63f41 100644 --- a/dataframe.go +++ b/dataframe.go @@ -1794,5 +1794,4 @@ var dfDatetimeMethods = []dfM{ {ruby: "dayofyear", goName: "DayOfYear"}, {ruby: "normalize", goName: "Normalize"}, {ruby: "strftime", goName: "Strftime"}, - {ruby: "inspect", goName: "Year"}, } From 990e08e39f77282903907bc9a9ae3b0b2c7c9ba9 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Sat, 17 Oct 2026 22:33:46 +0200 Subject: [PATCH 09/20] Plot: every binding compared with the Go call, SVG byte for byte Co-Authored-By: Claude Opus 5.5 --- plot_test.go | 387 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 387 insertions(+) create mode 100644 plot_test.go diff --git a/plot_test.go b/plot_test.go new file mode 100644 index 0000000..9d500b2 --- /dev/null +++ b/plot_test.go @@ -0,0 +1,387 @@ +package ruby + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" + + rb "github.com/go-embedded-ruby/ruby" + "github.com/go-dataframe/dataframe" + nd "github.com/go-ndarray/ndarray" + "github.com/go-plot/plot" +) + +// plotIs checks a Ruby program ending with a Plot::Figure draws the very +// figure build draws in Go: the same SVG bytes. +func plotIs(t *testing.T, in *rb.Interp, src string, build func() *plot.Figure) { + t.Helper() + v, err := in.Eval(src, "test.rb", 1) + if err != nil { + t.Fatalf("%s: %v", src, err) + } + f, ok := v.Go().(*plot.Figure) + if !ok { + t.Fatalf("%s: %s is not a Plot::Figure", src, v.Inspect()) + } + svg, err := v.Call("to_svg") + if err != nil { + t.Fatal(err) + } + want := plSVG(build()) + if svg.Go().(string) != string(want) || !bytes.Equal(plSVG(f), want) { + t.Errorf("%s: the SVG differs from Go's", src) + } +} + +// one is a 1x1 figure and its Axes, as Plot.subplots gives them. +func one(opts ...plot.FigOption) (*plot.Figure, *plot.Axes) { + f, axs := plot.Subplots(1, 1, opts...) + return f, axs[0][0] +} + +func TestPlotAxes(t *testing.T) { + in := newInterp(t) + x := []float64{1, 2, 3, 4} + y := []float64{1, 4, 9, 16} + eval(t, in, `X = [1, 2, 3, 4]; Y = [1, 4, 9, 16]`) + plotIs(t, in, `fig, ax = Plot.subplots(figsize: [5, 3], dpi: 80) + ax.plot(Y) + ax.plot(X, Y, alpha: 0.5, clip: false, color: "C2", ec: "k", fc: "w", label: "sq", + linestyle: "--", linewidth: 2, marker: "o", markersize: 4, mew: 1, z_order: 3) + ax.legend(loc: "upper left", fontsize: 9) + ax.grid + ax.grid(false) + ax.grid(true) + ax.title = "T" + ax.set_title("Title $x^2$") + ax.set_xlabel("x"); ax.xlabel = "xx" + ax.set_ylabel("y"); ax.ylabel = "yy" + fig`, func() *plot.Figure { + f, ax := one(plot.DPI(80), plot.FigSize(5, 3)) + ax.Plot(nil, y) + ax.Plot(x, y, plot.Alpha(0.5), plot.Clip(false), plot.Color("C2"), plot.EdgeColor("k"), plot.FaceColor("w"), + plot.Label("sq"), plot.LineStyle("--"), plot.LineWidth(2), plot.Marker("o"), plot.MarkerSize(4), + plot.MarkerEdgeWidth(1), plot.ZOrder(3)) + ax.Legend(plot.FontSize(9), plot.Loc("upper left")) + ax.Grid(true) + ax.Grid(false) + ax.Grid(true) + ax.SetTitle("T") + ax.SetTitle("Title $x^2$") + ax.SetXLabel("x") + ax.SetXLabel("xx") + ax.SetYLabel("y") + ax.SetYLabel("yy") + return f + }) + plotIs(t, in, `fig, ax = Plot.subplots + m = ax.scatter(X, Y, c: [1, 2, 3, 4], cmap: "plasma", s: [10, 20, 30, 40], vmax: 4, vmin: 0) + fig.colorbar(m, ax) + m2 = ax.scatter(X, Y, colors: ["red", "blue", "green", "k"], edgecolors: "k", log_norm: false) + fig.colorbar(ax.scatter(X, Y, c: NDArray.linspace(1, 4, 4), log_norm: true), ax: ax) + raise "scatter without c gives a Mappable" unless m2.nil? + fig`, func() *plot.Figure { + f, ax := one() + m := ax.Scatter(x, y, plot.CValues([]float64{1, 2, 3, 4}), plot.Cmap("plasma"), plot.Sizes([]float64{10, 20, 30, 40}), + plot.Vmax(4), plot.Vmin(0)) + f.Colorbar(m, ax) + ax.Scatter(x, y, plot.Colors("red", "blue", "green", "k"), plot.EdgeColor("k")) + lin, _ := nd.FromData([]float64{1, 2, 3, 4}, 4) + f.Colorbar(ax.Scatter(x, y, plot.CValues(lin), plot.LogNorm()), ax) + return f + }) + plotIs(t, in, `fig, ax = Plot.subplots + ax.hist([1, 2, 2, 3, 3, 3, 4], bins: 3, density: true, edgecolor: "k") + ax.hist([1, 2, 2, 3], bins: [0, 2, 4], color: "red", alpha: 0.5) + ax.bar(X, Y, align: "edge", bottom: 1, colors: ["red", "green", "blue", "k"], width: 0.5) + ax.barh(X, Y, height: 0.4, left: 2, color: [0.2, 0.4, 0.6]) + ax.bar(X, Y, colors: "red", color: [1, 0, 0, 0.5]) + fig`, func() *plot.Figure { + f, ax := one() + ax.Hist([]float64{1, 2, 2, 3, 3, 3, 4}, plot.Bins(3), plot.Density(), plot.EdgeColor("k")) + ax.Hist([]float64{1, 2, 2, 3}, plot.BinEdges(0, 2, 4), plot.Color("red"), plot.Alpha(0.5)) + ax.Bar(x, y, plot.Align("edge"), plot.Bottom(1), plot.Colors("red", "green", "blue", "k"), plot.Width(0.5)) + ax.Barh(x, y, plot.Width(0.4), plot.Bottom(2), plot.RGBA(0.2, 0.4, 0.6, 1)) + ax.Bar(x, y, plot.RGBA(1, 0, 0, 0.5), plot.Colors("red")) + return f + }) + plotIs(t, in, `fig, ax = Plot.subplots + ax.fill_between(X, Y) + ax.fill_between(X, Y, [0, 1, 2, 3], where: [true, false, true, true], alpha: 0.3, label: "band") + ax.errorbar(X, Y, capsize: 3, ecolor: "red", xerr: [0.1, 0.2, 0.1, 0.2], yerr: 0.5) + ax.axhline + ax.axhline(2, color: "k", ls: "--", lw: 1) + ax.axvline(1.5) + ax.text(1, 2, "hi", fontsize: 12, ha: "center", rotation: 45, va: "top") + ax.annotate("pt", [2, 4], [3, 8], arrowprops: {}) + ax.annotate("p", [1, 1]) + ax.annotate("q", [1, 1], [2, 2], arrow: true, arrowprops: nil) + fig`, func() *plot.Figure { + f, ax := one() + ax.FillBetween(x, y, nil) + ax.FillBetween(x, y, []float64{0, 1, 2, 3}, plot.Alpha(0.3), plot.Label("band"), plot.Where([]bool{true, false, true, true})) + ax.ErrorBar(x, y, plot.CapSize(3), plot.ErrorColor("red"), plot.XErr([]float64{0.1, 0.2, 0.1, 0.2}), plot.YErr(0.5)) + ax.Axhline(0) + ax.Axhline(2, plot.Color("k"), plot.LineStyle("--"), plot.LineWidth(1)) + ax.Axvline(1.5) + ax.Text(1, 2, "hi", plot.FontSize(12), plot.HA("center"), plot.Rotation(45), plot.VA("top")) + ax.Annotate("pt", 2, 4, 3, 8, plot.Arrow()) + ax.Annotate("p", 1, 1, 1, 1) + ax.Annotate("q", 1, 1, 2, 2, plot.Arrow()) + return f + }) + z := [][]float64{{1, 2, 3}, {4, 5, 6}, {7, 8, 10}} + rgb, _ := nd.FromData([]float64{1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1}, 2, 2, 3) + plotIs(t, in, `fig, (a, b, c) = Plot.subplots(1, 3) + Z = [[1, 2, 3], [4, 5, 6], [7, 8, 10]] + m = a.imshow(Z, aspect: "auto", cmap: "gray", extent: [0, 1, 0, 1], origin: "lower") + fig.colorbar(m, a) + b.imshow([[[1, 0, 0], [0, 1, 0]], [[0, 0, 1], [1, 1, 1]]]) + c.contour(Z, levels: 4) + cs = c.contourf([0, 1, 2], [0, 1, 2], Z, levels: [0, 5, 10], alpha: 0.5) + fig.colorbar(cs, c) + fig`, func() *plot.Figure { + f, axs := plot.Subplots(1, 3) + a, b, c := axs[0][0], axs[0][1], axs[0][2] + m := a.Imshow(z, plot.Aspect("auto"), plot.Cmap("gray"), plot.Extent(0, 1, 0, 1), plot.Origin("lower")) + f.Colorbar(m, a) + b.Imshow(rgb) + c.Contour(nil, nil, z, plot.NLevels(4)) + cs := c.Contourf([]float64{0, 1, 2}, []float64{0, 1, 2}, z, plot.Alpha(0.5), plot.Levels(0, 5, 10)) + f.Colorbar(cs, c) + return f + }) + plotIs(t, in, `fig, ax = Plot.subplots + ax.plot(X, Y) + ax.set_xlim(0, 10); ax.xlim = [0, 5]; ax.xlim(1, 6) + ax.set_ylim([1, 20]); ax.ylim = [0, 30]; ax.ylim(1, 40) + ax.set_xscale("log"); ax.xscale = "linear" + ax.set_yscale("linear"); ax.yscale = "log" + ax.set_xticks([1, 2], ["a", "b"]) + ax.set_yticks([1, 10]) + ax.set_aspect(:equal); ax.aspect = 2; ax.set_aspect("auto"); ax.set_aspect(1) + t = ax.twinx + t.plot(X, [4, 3, 2, 1], color: "red") + raise "xlim" unless ax.xlim == [1.0, 6.0] && ax.ylim == [1.0, 40.0] + fig`, func() *plot.Figure { + f, ax := one() + ax.Plot(x, y) + ax.SetXLim(0, 10) + ax.SetXLim(0, 5) + ax.SetXLim(1, 6) + ax.SetYLim(1, 20) + ax.SetYLim(0, 30) + ax.SetYLim(1, 40) + ax.SetXScale("log") + ax.SetXScale("linear") + ax.SetYScale("linear") + ax.SetYScale("log") + ax.SetXTicks([]float64{1, 2}, "a", "b") + ax.SetYTicks([]float64{1, 10}) + ax.SetAspect(1) + ax.SetAspect(2) + ax.SetAspect(0) + ax.SetAspect(1) + tw := ax.TwinX() + tw.Plot(x, []float64{4, 3, 2, 1}, plot.Color("red")) + return f + }) + // Data as an NDArray and as a DataFrame::Series. + plotIs(t, in, `fig, ax = Plot.subplots + ax.plot(NDArray.linspace(0, 1, 5), DataFrame::Series.new([1, 3, 2, 5, 4])) + ax.plot(DataFrame::Series.new([1.5, 2.5])) + fig`, func() *plot.Figure { + f, ax := one() + lin, _ := nd.FromData([]float64{0, 0.25, 0.5, 0.75, 1}, 5) + s, _ := dataframe.NewSeries("", []any{int64(1), int64(3), int64(2), int64(5), int64(4)}) + sf, _ := s.Floats() + ax.Plot(lin, sf) + ax.Plot(nil, []float64{1.5, 2.5}) + return f + }) +} + +func TestPlotFigure(t *testing.T) { + in := newInterp(t) + plotIs(t, in, `fig = Plot.figure(figsize: [4, 3], dpi: 50) + a = fig.add_axes(0.1, 0.1, 0.8, 0.8) + b = fig.add_axes([0.5, 0.5, 0.3, 0.3]) + a.plot([1, 2]) + b.plot([2, 1]) + g = fig.subplots(2, 1) + g[0].plot([1, 2, 3]) + raise "axes" unless fig.axes.size == 4 + raise "tight" unless fig.tight_layout == true + fig`, func() *plot.Figure { + f := plot.NewFigure(plot.DPI(50), plot.FigSize(4, 3)) + a := f.AddAxes(0.1, 0.1, 0.8, 0.8) + b := f.AddAxes(0.5, 0.5, 0.3, 0.3) + a.Plot(nil, []float64{1, 2}) + b.Plot(nil, []float64{2, 1}) + g := f.Subplots(2, 1) + g[0][0].Plot(nil, []float64{1, 2, 3}) + f.TightLayout() + return f + }) + plotIs(t, in, `Plot::Figure.new`, func() *plot.Figure { return plot.NewFigure() }) + plotIs(t, in, `f = Plot::Figure.new; f.subplots(squeeze: false)[0][0].plot([1]); f`, func() *plot.Figure { + f := plot.NewFigure() + f.Subplots(1, 1)[0][0].Plot(nil, []float64{1}) + return f + }) + + f, ax := one() + ax.Plot(nil, []float64{3, 1, 2}) + eval(t, in, `fig, ax = Plot.subplots; ax.plot([3, 1, 2])`) + check(t, in, `fig.width`, f.Width) + check(t, in, `fig.height`, f.Height) + check(t, in, `fig.dpi`, f.DPI) + w, h := f.PixelSize() + check(t, in, `fig.pixel_size`, []int{w, h}) + check(t, in, `fig.inspect`, "#") + check(t, in, `fig.to_s`, "#") + check(t, in, `fig.to_png.bytes`, plPNG(f)) + disp := f.Display() + check(t, in, `fig.display["image/svg+xml"]`, string(disp["image/svg+xml"])) + check(t, in, `fig.display["image/png"].bytes`, disp["image/png"]) + check(t, in, `Plot.subplots(1, 1, squeeze: false)[1].size`, 1) + check(t, in, `Plot.subplots(1, 3)[1].map(&:class).uniq.map(&:to_s)`, []string{"Plot::Axes"}) + check(t, in, `Plot.subplots(3, 1)[1].size`, 3) + check(t, in, `Plot.subplots(2, 2)[1].map(&:size)`, []int{2, 2}) + check(t, in, `Plot.subplots(2)[1].size`, 2) + check(t, in, `Plot.subplots[1].imshow([[1, 2], [3, 4]]).inspect`, "#") + if eval(t, in, `ax.figure`) != eval(t, in, `fig`) { + t.Error("Axes#figure is not the figure") + } + check(t, in, `Plot.values([1, 2])`, plot.Values([]float64{1, 2})) + check(t, in, `Plot.values(NDArray.linspace(0, 1, 3))`, []float64{0, 0.5, 1}) + check(t, in, `Plot.values(3)`, plot.Values(3.0)) + + // Saving writes the bytes to_svg and to_png give. + dir := t.TempDir() + if err := in.Set("dir", dir); err != nil { + t.Fatal(err) + } + eval(t, in, `fig.save_svg(File.join(dir, "a.svg")); fig.save_png(File.join(dir, "a.png")) + fig.savefig(File.join(dir, "b.SVG")); fig.savefig(File.join(dir, "b.png"))`) + for name, want := range map[string][]byte{"a.svg": plSVG(f), "b.SVG": plSVG(f), "a.png": plPNG(f), "b.png": plPNG(f)} { + got, err := os.ReadFile(filepath.Join(dir, name)) + if err != nil || !bytes.Equal(got, want) { + t.Errorf("%s: not the figure's bytes (%v)", name, err) + } + } + evalErr(t, in, `fig.savefig(File.join(dir, "a.pdf"))`, "ArgumentError", "unknown format") + evalErr(t, in, `fig.save_svg(File.join(dir, "no", "such", "a.svg"))`, "IOError", "no such file") + evalErr(t, in, `fig.save_svg(1)`, "TypeError", "String") + evalErr(t, in, `fig.save_svg`, "ArgumentError", "given 0, expected 1") +} + +func TestPlotErrors(t *testing.T) { + in := newInterp(t) + eval(t, in, `fig, ax = Plot.subplots; s = DataFrame::Series.new(["a"])`) + for _, c := range []struct{ src, class, msg string }{ + {`ax.plot`, "ArgumentError", "given 0, expected 1..2"}, + {`ax.plot([1], bogus: 1)`, "ArgumentError", "unknown keyword: :bogus (known: align, alpha,"}, + {`ax.plot([1], color: "nonsense")`, "ArgumentError", "not a colour"}, + {`ax.plot([1], marker: "?")`, "ArgumentError", "unknown marker"}, + {`ax.plot([1, 2], [1])`, "ArgumentError", "same length"}, + {`ax.plot(["a"])`, "TypeError", "String into Float"}, + {`ax.plot(:a)`, "TypeError", "Symbol into plot data"}, + {`ax.plot(s)`, "ArgumentError", "dtype"}, + {`ax.imshow([[1, 2], [3]])`, "ArgumentError", "rectangular"}, + {`ax.imshow([[1, 2], 3])`, "ArgumentError", "rectangular"}, + {`ax.imshow([[[1], ["a"]]])`, "TypeError", "String into Float"}, + {`ax.imshow([[]])`, "TypeError", "Array into Float"}, + {`ax.hist([1], bins: "x")`, "TypeError", "String into Float"}, + {`ax.hist([1], density: 1)`, "TypeError", "true or false"}, + {`ax.imshow([[1]], extent: [1])`, "ArgumentError", "extent: expected an Array of 4 values"}, + {`ax.imshow([[1]], extent: [1, 2, 3, "a"])`, "TypeError", "String into Float"}, + {`ax.scatter([1], [1], c: :x)`, "TypeError", "Symbol into plot data"}, + {`ax.plot([1], alpha: "a")`, "TypeError", "String into Float"}, + {`ax.hist`, "ArgumentError", "given 0, expected 1"}, + {`ax.hist([1], bogus: 1)`, "ArgumentError", "unknown keyword: :bogus"}, + {`ax.scatter([1])`, "ArgumentError", "given 1, expected 2"}, + {`ax.scatter([1], [1, 2])`, "ArgumentError", "same length"}, + {`ax.contour([[1]], [[1]])`, "ArgumentError", "given 2, expected 1 or 3"}, + {`ax.contour([1, 2], [1], [[1, 2], [3, 4]])`, "ArgumentError", "must match"}, + {`ax.fill_between([1])`, "ArgumentError", "given 1, expected 2..3"}, + {`ax.fill_between([1, 2], [1])`, "ArgumentError", "same length"}, + {`ax.axhline(1, 2)`, "ArgumentError", "given 2, expected 0..1"}, + {`ax.axhline("a")`, "TypeError", "String into Float"}, + {`ax.axhline(1, bogus: 1)`, "ArgumentError", "unknown keyword"}, + {`ax.axhline(1, ls: "~")`, "ArgumentError", "unknown line style"}, + {`ax.text(1, 2)`, "ArgumentError", "given 2, expected 3"}, + {`ax.text("a", 2, "s")`, "TypeError", "String into Float"}, + {`ax.text(1, "a", "s")`, "TypeError", "String into Float"}, + {`ax.text(1, 2, 3)`, "TypeError", "Integer into String"}, + {`ax.text(1, 2, "s", bogus: 1)`, "ArgumentError", "unknown keyword"}, + {`ax.text(1, 2, "s", color: "nonsense")`, "ArgumentError", "not a colour"}, + {`ax.annotate("s")`, "ArgumentError", "given 1, expected 2..3"}, + {`ax.annotate(1, [1, 2])`, "TypeError", "Integer into String"}, + {`ax.annotate("s", [1])`, "ArgumentError", "a point is [x, y]"}, + {`ax.annotate("s", "a")`, "TypeError", "String into an Array"}, + {`ax.annotate("s", [1, 2], [1])`, "ArgumentError", "a point is [x, y]"}, + {`ax.annotate("s", [1, 2], bogus: 1)`, "ArgumentError", "unknown keyword"}, + {`ax.annotate("s", [1, 2], color: "nonsense")`, "ArgumentError", "not a colour"}, + {`ax.legend(loc: "nowhere")`, "ArgumentError", "unknown legend location"}, + {`ax.legend(1)`, "ArgumentError", "given 1, expected 0"}, + {`ax.grid(1)`, "TypeError", "true or false"}, + {`ax.grid(true, false)`, "ArgumentError", "given 2, expected 0..1"}, + {`ax.grid(on: true)`, "ArgumentError", "unknown keyword: :on"}, + {`ax.set_title(1)`, "TypeError", "Integer into String"}, + {`ax.set_title("a", "b")`, "ArgumentError", "given 2, expected 1"}, + {`ax.set_title("a", x: 1)`, "ArgumentError", "unknown keyword: :x"}, + {`ax.set_xlim(1)`, "ArgumentError", "given 1, expected 2"}, + {`ax.set_xlim(1, "a")`, "TypeError", "String into Float"}, + {`ax.set_xlim(1, 2, x: 1)`, "ArgumentError", "unknown keyword"}, + {`ax.xlim(1)`, "ArgumentError", "given 1, expected 2"}, + {`ax.set_xticks`, "ArgumentError", "given 0, expected 1..2"}, + {`ax.set_xticks(["a"])`, "TypeError", "String into Float"}, + {`ax.set_xticks([1], [1])`, "TypeError", "Integer into String"}, + {`ax.set_xticks([1], x: 1)`, "ArgumentError", "unknown keyword"}, + {`ax.set_aspect(:square)`, "ArgumentError", "unknown aspect"}, + {`ax.set_aspect([1])`, "TypeError", "Array into Float"}, + {`ax.set_aspect`, "ArgumentError", "given 0, expected 1"}, + {`ax.twinx(1)`, "ArgumentError", "given 1, expected 0"}, + {`ax.figure(1)`, "ArgumentError", "given 1, expected 0"}, + {`fig.colorbar(1, ax)`, "TypeError", "Integer into Plot::Mappable"}, + {`fig.colorbar`, "ArgumentError", "given 0, expected 1..2"}, + {`fig.colorbar(ax.imshow([[1]]))`, "ArgumentError", "colorbar needs the Axes"}, + {`fig.colorbar(ax.imshow([[1]]), 1)`, "TypeError", "Integer into Plot::Axes"}, + {`fig.colorbar(ax.imshow([[1]]), ax, x: 1)`, "ArgumentError", "unknown keyword: :x (known: ax)"}, + {`fig.add_axes(1, 2)`, "ArgumentError", "given 2, expected 4"}, + {`fig.add_axes(1, 2, 3, "a")`, "TypeError", "String into Float"}, + {`fig.subplots(1, 1, x: 1)`, "ArgumentError", "unknown keyword: :x (known: squeeze)"}, + {`fig.subplots(0)`, "ArgumentError", "rows and cols must be positive"}, + {`fig.width(1)`, "ArgumentError", "given 1, expected 0"}, + {`fig.to_svg(x: 1)`, "ArgumentError", "unknown keyword: :x"}, + {`Plot.subplots(1, 2, 3)`, "ArgumentError", "given 3, expected 0..2"}, + {`Plot.subplots("a")`, "TypeError", "String into Integer"}, + {`Plot.subplots(squeeze: 1)`, "TypeError", "true or false"}, + {`Plot.subplots(bogus: 1)`, "ArgumentError", "unknown keyword: :bogus (known: dpi, figsize)"}, + {`Plot.figure(1)`, "ArgumentError", "given 1, expected 0"}, + {`Plot.figure(figsize: [1])`, "ArgumentError", "figsize must be [width, height]"}, + {`Plot.figure(figsize: "a")`, "TypeError", "String into an Array"}, + {`Plot.figure(dpi: "a")`, "TypeError", "String into Float"}, + {`Plot.values`, "ArgumentError", "given 0, expected 1"}, + {`Plot.values(:a)`, "TypeError", "Symbol into plot data"}, + {`Plot.values([[1, 2], [3, 4]])`, "ArgumentError", "cannot plot data"}, + } { + evalErr(t, in, c.src, c.class, c.msg) + } +} + +// A name already taken by something else is an error of Register. +func TestPlotRegisterConflicts(t *testing.T) { + for _, src := range []string{`class Plot; end`, `module Plot; module Figure; end; end`} { + in := rb.New() + if _, err := in.Eval(src, "x.rb", 1); err != nil { + t.Fatal(err) + } + if err := registerPlot(in); err == nil || !strings.Contains(err.Error(), "already defined") { + t.Errorf("%s: registerPlot = %v", src, err) + } + } +} From fe5087fc16d0ddf372fcb626eb0589d743f3a498 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Sat, 17 Oct 2026 22:36:59 +0200 Subject: [PATCH 10/20] DataFrame: construction and frame methods compared with the Go calls Co-Authored-By: Claude Opus 5.5 --- dataframe_test.go | 420 ++++++++++++++++++++++++++++++++++++++++++++++ plot_test.go | 2 +- 2 files changed, 421 insertions(+), 1 deletion(-) create mode 100644 dataframe_test.go diff --git a/dataframe_test.go b/dataframe_test.go new file mode 100644 index 0000000..38af9b5 --- /dev/null +++ b/dataframe_test.go @@ -0,0 +1,420 @@ +package ruby + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + "time" + + rb "github.com/go-embedded-ruby/ruby" + "github.com/go-dataframe/dataframe" + nd "github.com/go-ndarray/ndarray" +) + +// dfSame compares a Ruby result (converted to Go) with the Go one: frames, +// series, indexes and group-bys by their full printed form and type, dtypes +// by name, instants by their nanoseconds, slices element by element, and +// everything else as same does. +func dfSame(got, want any) bool { + switch w := want.(type) { + case dataframe.DType: + return got == w.String() + case time.Time: + v, ok := got.(rb.Value) + if !ok || v.ClassName() != "Time" { + return false + } + s, _ := v.Call("to_i") + ns, _ := v.Call("nsec") + off, _ := v.Call("utc_offset") + return s.Go() == w.Unix() && ns.Go() == int64(w.Nanosecond()) && off.Go() == int64(0) + case fmt.Stringer: + g, ok := got.(fmt.Stringer) + return ok && reflect.TypeOf(g) == reflect.TypeOf(w) && g.String() == w.String() + case map[string][]byte: + g, ok := got.(map[any]any) + if !ok || len(g) != len(w) { + return false + } + for k, v := range w { + if g[k] != string(v) { + return false + } + } + return true + } + wv := reflect.ValueOf(want) + if want != nil && wv.Kind() == reflect.Slice && wv.Type().Elem().Kind() != reflect.Float64 { + g, ok := got.([]any) + if !ok || len(g) != wv.Len() { + return false + } + for i := range g { + if !dfSame(g[i], wv.Index(i).Interface()) { + return false + } + } + return true + } + return same(got, want) +} + +func dfIs(t *testing.T, in *rb.Interp, src string, want any) { + t.Helper() + got := eval(t, in, src) + if !dfSame(got, want) { + t.Errorf("%s\n ruby: %#v\n go: %#v", src, got, want) + } +} + +// dfFixture holds the frames every test shares, built the same way in Go and +// in Ruby. +type dfFixture struct { + df, num, long, ts, multi *dataframe.DataFrame + s, f, b, str, day *dataframe.Series +} + +const dfSetup = ` +df = DataFrame.new(city: ["Lyon", "Paris", "Lyon"], pop: [522, 2103, 530], x: [1.5, nil, 2.0]) +num = df.select("pop", "x") +long = DataFrame.new("k" => ["a", "a", "b", "b"], "c" => ["x", "y", "x", "y"], "v" => [1, 2, 3, 4]) +ts = DataFrame.read_csv("day,v\n2024-01-01,1\n2024-01-02,2\n2024-01-09,5\n", parse_dates: ["day"], index_col: "day") +multi = long.set_index("k", "c") +s = df["pop"] +f = df["x"] +b = s > 525 +str = df["city"] +day = ts.reset_index["day"] +nil +` + +func newDFFixture(t *testing.T) (*rb.Interp, dfFixture) { + t.Helper() + in := newInterp(t) + eval(t, in, dfSetup) + m := must[*dataframe.DataFrame](t) + ms := must[*dataframe.Series](t) + var x dfFixture + x.df = m(dataframe.New( + dataframe.Col("city", []any{"Lyon", "Paris", "Lyon"}), + dataframe.Col("pop", []any{int64(522), int64(2103), int64(530)}), + dataframe.Col("x", []any{1.5, nil, 2.0}))) + x.num = m(x.df.Select("pop", "x")) + x.long = m(dataframe.New( + dataframe.Col("k", []any{"a", "a", "b", "b"}), + dataframe.Col("c", []any{"x", "y", "x", "y"}), + dataframe.Col("v", []any{int64(1), int64(2), int64(3), int64(4)}))) + x.ts = m(dataframe.ReadCSV(strings.NewReader("day,v\n2024-01-01,1\n2024-01-02,2\n2024-01-09,5\n"), + dataframe.CSVIndexCol("day"), dataframe.CSVParseDates("day"))) + x.multi = m(x.long.SetIndex("k", "c")) + x.s = ms(x.df.Column("pop")) + x.f = ms(x.df.Column("x")) + x.b = ms(x.s.Gt(int64(525))) + x.str = ms(x.df.Column("city")) + x.day = ms(x.ts.ResetIndex(false).Column("day")) + return in, x +} + +func TestDataFrameConstruction(t *testing.T) { + in, x := newDFFixture(t) + m := must[*dataframe.DataFrame](t) + dfIs(t, in, `df`, x.df) + dfIs(t, in, `long`, x.long) + dfIs(t, in, `DataFrame.new({"b" => [1], "a" => ["z"]}, c: [true])`, m(dataframe.New( + dataframe.Col("b", []any{int64(1)}), dataframe.Col("a", []any{"z"}), dataframe.Col("c", []any{true})))) + dfIs(t, in, `DataFrame.new([[:a, NDArray.linspace(0, 1, 3)], ["s", df["pop"]]])`, m(dataframe.New( + dataframe.Col("a", must[*nd.Array](t)(nd.FromData([]float64{0, 0.5, 1}, 3))), dataframe.Col("s", x.s)))) + dfIs(t, in, `DataFrame.new`, m(dataframe.New())) + evalErr(t, in, `DataFrame.new(t: [Time.utc(2020, 1, 2, 3, 4, 5), :sym])`, "ArgumentError", "") + evalErr(t, in, `DataFrame.new(a: [1, 2], b: [1])`, "ArgumentError", "length") + evalErr(t, in, `DataFrame.new(1)`, "TypeError", "Integer into Hash") + evalErr(t, in, `DataFrame.new([1])`, "TypeError", "Integer into a [name, value] pair") + evalErr(t, in, `DataFrame.new([[1, [2]]])`, "TypeError", "Integer into String") + + tm := time.Date(2020, 1, 2, 3, 4, 5, 6000, time.UTC) + dfIs(t, in, `DataFrame.new(t: [Time.utc(2020, 1, 2, 3, 4, 5, 6)])`, m(dataframe.New(dataframe.Col("t", []any{tm})))) + dfIs(t, in, `DataFrame.new(t: [Time.utc(2020, 1, 2, 3, 4, 5, 6)])["t"][0]`, tm) + + csv := "a;b;c\n# note\n1;x;2020-01-02\nNA;y;2020-02-03\n3;;2020-03-04\n" + dfIs(t, in, `DataFrame.read_csv("a,b\n1,2\n")`, m(dataframe.ReadCSV(strings.NewReader("a,b\n1,2\n")))) + if err := in.Set("csv", csv); err != nil { + t.Fatal(err) + } + dfIs(t, in, `DataFrame.read_csv(csv, sep: ";", comment: "#", parse_dates: "c", date_format: "%Y-%m-%d", + na_values: ["x"], keep_default_na: true, usecols: [:a, :b, :c], nrows: 3, quotechar: "'", skip_blank_lines: false)`, + m(dataframe.ReadCSV(strings.NewReader(csv), dataframe.CSVComment('#'), dataframe.CSVDateFormat("%Y-%m-%d"), + dataframe.CSVKeepDefaultNA(true), dataframe.CSVNAValues("x"), dataframe.CSVNRows(3), dataframe.CSVParseDates("c"), + dataframe.CSVQuote('\''), dataframe.CSVSeparator(';'), dataframe.CSVSkipBlankLines(false), dataframe.CSVUseCols("a", "b", "c")))) + tf := "h\nskip\nq,r\nyes,1\nno,2\n" + if err := in.Set("tf", tf); err != nil { + t.Fatal(err) + } + dfIs(t, in, `DataFrame.read_csv(tf, skiprows: 2, true_values: "yes", false_values: ["no"], dtype: {"r" => :float64}, index_col: 1)`, + m(dataframe.ReadCSV(strings.NewReader(tf), dataframe.CSVDTypes(map[string]dataframe.DType{"r": dataframe.Float64}), + dataframe.CSVFalseValues("no"), dataframe.CSVIndexColumn(1), dataframe.CSVSkipRows(2), dataframe.CSVTrueValues("yes")))) + dfIs(t, in, `DataFrame.read_csv("1,2\n3,4\n", header: false, names: ["p", "q"], quotechar: "")`, + m(dataframe.ReadCSV(strings.NewReader("1,2\n3,4\n"), dataframe.CSVHeader(false), dataframe.CSVNames("p", "q"), dataframe.CSVQuote(0)))) + dir := t.TempDir() + path := filepath.Join(dir, "a.csv") + if err := os.WriteFile(path, []byte("a,b\n1,2\n"), 0o644); err != nil { + t.Fatal(err) + } + if err := in.Set("path", path); err != nil { + t.Fatal(err) + } + dfIs(t, in, `DataFrame.read_csv_file(path, sep: nil)`, m(dataframe.ReadCSVFile(path, dataframe.CSVSeparator(0)))) + for _, c := range []struct{ src, class, msg string }{ + {`DataFrame.read_csv`, "ArgumentError", "given 0, expected 1"}, + {`DataFrame.read_csv(1)`, "TypeError", "Integer into String"}, + {`DataFrame.read_csv("a", bogus: 1)`, "ArgumentError", "unknown keyword: :bogus (known: comment,"}, + {`DataFrame.read_csv("a", sep: ";;")`, "ArgumentError", "single character"}, + {`DataFrame.read_csv("a", sep: 1)`, "TypeError", "Integer into String"}, + {`DataFrame.read_csv("a", header: 1)`, "TypeError", "true or false"}, + {`DataFrame.read_csv("a", names: 1)`, "TypeError", "String or an Array"}, + {`DataFrame.read_csv("a", names: [1])`, "TypeError", "Integer into String"}, + {`DataFrame.read_csv("a", nrows: "x")`, "TypeError", "String into Integer"}, + {`DataFrame.read_csv("a", date_format: 1)`, "TypeError", "Integer into String"}, + {`DataFrame.read_csv("a", index_col: 1.5)`, "TypeError", "Float into String"}, + {`DataFrame.read_csv("a", dtype: {"a" => :nope})`, "ArgumentError", "nope"}, + {`DataFrame.read_csv("a", dtype: {"a" => 1})`, "TypeError", "Integer into a dtype"}, + {`DataFrame.read_csv("a,b\n1,2\n3,4,5\n")`, "ArgumentError", "line"}, + {`DataFrame.read_csv_file(path + ".none")`, "ArgumentError", "no such file"}, + {`DataFrame.read_csv_file`, "ArgumentError", "given 0, expected 1"}, + } { + evalErr(t, in, c.src, c.class, c.msg) + } + + a := must[*nd.Array](t)(nd.FromData([]float64{1, 2, 3, 4}, 2, 2)) + if err := in.Set("arr", a); err != nil { + t.Fatal(err) + } + dfIs(t, in, `DataFrame.from_array(arr, ["p", "q"])`, m(dataframe.FromArray(a, "p", "q"))) + dfIs(t, in, `DataFrame.from_array(arr)`, m(dataframe.FromArray(a))) + evalErr(t, in, `DataFrame.from_array(arr, "p")`, "ArgumentError", "") + evalErr(t, in, `DataFrame.from_array([1])`, "TypeError", "Array into NDArray") + evalErr(t, in, `DataFrame.from_array(arr, [1])`, "TypeError", "Integer into String") + evalErr(t, in, `DataFrame.from_array(arr, x: 1)`, "ArgumentError", "unknown keyword: :x") + evalErr(t, in, `DataFrame.from_array`, "ArgumentError", "given 0, expected 1..2") + + dfIs(t, in, `DataFrame.from_series(s, f)`, m(dataframe.FromSeries(x.s, x.f))) + evalErr(t, in, `DataFrame.from_series(1)`, "TypeError", "Integer into DataFrame::Series") + evalErr(t, in, `DataFrame.from_series(s, x: 1)`, "ArgumentError", "unknown keyword: :x") + + dfIs(t, in, `DataFrame.concat(df, num)`, m(dataframe.Concat(x.df, x.num))) + dfIs(t, in, `DataFrame.concat(df, num, ignore_index: true, inner: true)`, + m(dataframe.ConcatWith(dataframe.ConcatOptions{IgnoreIndex: true, Inner: true}, x.df, x.num))) + dfIs(t, in, `DataFrame.concat(df, long, axis: 1)`, m(dataframe.ConcatWith(dataframe.ConcatOptions{Axis: 1}, x.df, x.long))) + evalErr(t, in, `DataFrame.concat`, "ArgumentError", "nothing to concatenate") + evalErr(t, in, `DataFrame.concat(df, 1)`, "TypeError", "Integer into DataFrame") + evalErr(t, in, `DataFrame.concat(df, bogus: 1)`, "ArgumentError", "unknown keyword: :bogus") +} + +func TestDataFrameMethods(t *testing.T) { + in, x := newDFFixture(t) + m := must[*dataframe.DataFrame](t) + ms := must[*dataframe.Series](t) + df, num, long, ts := x.df, x.num, x.long, x.ts + r, c := df.Shape() + disp := map[string][]byte{} + for k, v := range df.Display() { + disp[k] = v + } + for _, cs := range []struct { + src string + want any + }{ + {`df.columns`, df.Columns()}, + {`df.length`, df.Len()}, + {`df.size`, df.Len()}, + {`df.shape`, []int{r, c}}, + {`df.describe`, df.Describe()}, + {`df.copy`, df.Copy()}, + {`df.count`, df.Count()}, + {`df.mean`, df.Mean()}, + {`df.sum`, df.Sum()}, + {`df.std`, df.Std()}, + {`df.var`, df.Var()}, + {`df.min`, df.Min()}, + {`df.max`, df.Max()}, + {`df.median`, df.Median()}, + {`df.isna`, df.IsNA()}, + {`df.notna`, df.NotNA()}, + {`df.ffill`, df.FFill()}, + {`df.bfill`, df.BFill()}, + {`df.transpose`, df.T()}, + {`df.t`, df.T()}, + {`df.drop("x")`, m(df.Drop("x"))}, + {`df.drop(["x", "pop"])`, m(df.Drop("x", "pop"))}, + {`df.select(:city, :x)`, m(df.Select("city", "x"))}, + {`df.set_index("city")`, m(df.SetIndex("city"))}, + {`multi`, x.multi}, + {`multi.reset_index`, x.multi.ResetIndex(false)}, + {`multi.reset_index(drop: true)`, x.multi.ResetIndex(true)}, + {`multi.reset_index(true)`, x.multi.ResetIndex(true)}, + {`df.sort_index(ascending: false)`, df.SortIndex(false)}, + {`df.sort_index`, df.SortIndex(true)}, + {`df.reindex([2, 0, 7])`, m(df.Reindex([]any{int64(2), int64(0), int64(7)}))}, + {`df.reindex_columns("x", "q")`, df.ReindexColumns("x", "q")}, + {`df.set_columns("a", "b", "c")`, m(df.SetColumns("a", "b", "c"))}, + {`df.column("pop")`, m2(ms)(df.Column("pop"))}, + {`df.column_at(2)`, ms(df.ColumnAt(2))}, + {`df.series`, df.Series()}, + {`df.iat(1, 1)`, int64(2103)}, + {`df.iat(-1, 0)`, "Lyon"}, + {`df.at(2, "city")`, "Lyon"}, + {`df.at(1, "x")`, nil}, + {`num.values`, must[*nd.Array](t)(num.Values())}, + {`num.to_ndarray`, must[*nd.Array](t)(num.Values())}, + {`df.to_s`, df.String()}, + {`df.inspect`, df.String()}, + {`df.to_html`, df.HTML()}, + {`df.display`, disp}, + {`df.index`, df.Index()}, + {`multi.index`, x.multi.Index()}, + {`df.with_column("y", [1, 2, 3])`, m(df.WithColumn("y", []any{int64(1), int64(2), int64(3)}))}, + {`df.with_column("y", 7)`, m(df.WithColumn("y", int64(7)))}, + {`df.with_column("pop", df["x"])`, m(df.WithColumn("pop", x.f))}, + {`df.with_index(DataFrame::Index.new(["a", "b", "c"], name: "n"))`, + m(df.WithIndex(must[*dataframe.Index](t)(dataframe.NewIndex("n", []any{"a", "b", "c"}))))}, + {`long.pivot("k", "c", "v")`, m(long.Pivot("k", "c", "v"))}, + {`long.pivot(index: "k", columns: "c", values: "v")`, m(long.Pivot("k", "c", "v"))}, + {`long.pivot_table("c", "k", "v")`, m(long.PivotTable("c", "k", "v", "mean"))}, + {`long.pivot_table("c", "k", "v", aggfunc: "sum")`, m(long.PivotTable("c", "k", "v", "sum"))}, + {`num.stack`, ms(num.Stack())}, + {`multi.unstack`, m(x.multi.Unstack())}, + {`ts.to_period("M")`, m(ts.ToPeriod("M"))}, + {`df.head`, df.Head(5)}, + {`df.head(1)`, df.Head(1)}, + {`df.head(n: 2)`, df.Head(2)}, + {`df.tail(1)`, df.Tail(1)}, + {`df.tail`, df.Tail(5)}, + {`num.round`, num.Round(0)}, + {`num.div(7).round(decimals: 2)`, m(num.Div(int64(7))).Round(2)}, + {`df.shift`, df.Shift(1)}, + {`df.shift(periods: -1)`, df.Shift(-1)}, + {`num.add(1)`, m(num.Add(int64(1)))}, + {`num.+(num)`, m(num.Add(num))}, + {`num.sub(0.5)`, m(num.Sub(0.5))}, + {`num.-(1)`, m(num.Sub(int64(1)))}, + {`num.mul(2)`, m(num.Mul(int64(2)))}, + {`num.*(num)`, m(num.Mul(num))}, + {`num.div(4)`, m(num.Div(int64(4)))}, + {`num./(num)`, m(num.Div(num))}, + // The methods written by hand. + {`df["pop"]`, ms(df.Column("pop"))}, + {`df[:city]`, ms(df.Column("city"))}, + {`df[["x", :city]]`, m(df.Select("x", "city"))}, + {`df[b]`, m(df.Filter(x.b))}, + {`df[[true, false, true]]`, m(df.Filter([]bool{true, false, true}))}, + {`df.filter(b)`, m(df.Filter(x.b))}, + {`df.filter([false, false, true])`, m(df.Filter([]bool{false, false, true}))}, + {`df.dtypes`, map[string]string{"city": "string", "pop": "int64", "x": "float64"}}, + {`df.dtypes.keys`, []string{"city", "pop", "x"}}, + {`df.to_h.keys`, df.Columns()}, + {`df.to_h["x"]`, x.f.Items()}, + {`ts.to_h["v"]`, []any{int64(1), int64(2), int64(5)}}, + {`ts.reset_index.to_h["day"]`, x.day.Items()}, + {`df.sort_values("pop")`, m(df.SortValues([]string{"pop"}))}, + {`df.sort_values(["city", "pop"], ascending: [true, false])`, m(df.SortValues([]string{"city", "pop"}, true, false))}, + {`df.sort_values(:x, ascending: false)`, m(df.SortValues([]string{"x"}, false))}, + {`df.group_by("city").mean`, m(m3(df.GroupBy("city")).Mean())}, + {`df.groupby(["city"]).sum`, m(m3(df.GroupBy("city")).Sum())}, + {`df.dropna`, m(df.DropNA())}, + {`df.dropna("pop")`, m(df.DropNA("pop"))}, + {`df.dropna(how: :all)`, m(df.DropNAAll())}, + {`df.dropna(how: "any")`, m(df.DropNA())}, + {`df.dropna(:x, thresh: 1)`, m(df.DropNAThresh(1, "x"))}, + {`df.dropna(axis: 1)`, df.DropNAColumns()}, + {`df.dropna(axis: 0)`, m(df.DropNA())}, + {`df.fillna(0)`, df.FillNA(int64(0))}, + {`df.fillna("x" => 9.5)`, m(df.FillNAColumns(map[string]any{"x": 9.5}))}, + {`df.fillna(x: 9.5)`, m(df.FillNAColumns(map[string]any{"x": 9.5}))}, + {`df.astype(:string)`, m(df.AsTypeAll(dataframe.String))}, + {`df.astype("pop" => "float64")`, m(df.AsType(map[string]dataframe.DType{"pop": dataframe.Float64}))}, + {`df.astype(pop: :float32)`, m(df.AsType(map[string]dataframe.DType{"pop": dataframe.Float32}))}, + {`df.rename("pop" => "p")`, df.Rename(map[string]string{"pop": "p"})}, + {`df.rename(pop: "p", x: :y)`, df.Rename(map[string]string{"pop": "p", "x": "y"})}, + {`df.merge(long.rename(k: "city"))`, m(df.Merge(long.Rename(map[string]string{"k": "city"}), dataframe.MergeOptions{}))}, + {`df.merge(DataFrame.new(c: ["Lyon"], q: [1]), how: "left", left_on: ["city"], right_on: ["c"], indicator: true, suffixes: ["_l", "_r"])`, + m(df.Merge(m(dataframe.New(dataframe.Col("c", []any{"Lyon"}), dataframe.Col("q", []any{int64(1)}))), + dataframe.MergeOptions{How: "left", LeftOn: []string{"city"}, RightOn: []string{"c"}, Indicator: true, Suffixes: [2]string{"_l", "_r"}}))}, + {`df.join(num, lsuffix: "_a", rsuffix: "_b")`, m(df.Join(num, "left", "_a", "_b"))}, + {`df.join(num.rename(pop: "p", x: "q"), how: "inner")`, m(df.Join(num.Rename(map[string]string{"pop": "p", "x": "q"}), "inner", "", ""))}, + {`long.melt(id_vars: ["k"], value_vars: "v")`, m(long.Melt([]string{"k"}, []string{"v"}))}, + {`long.melt(id_vars: :k)`, m(long.Melt([]string{"k"}, nil))}, + {`long.melt`, m(long.Melt(nil, nil))}, + {`df.query("pop > 525")`, m(df.Query("pop > 525"))}, + {`df.query("pop > @lo and city == @c", lo: 500, c: :Lyon)`, m(df.Query("pop > @lo and city == @c", map[string]any{"lo": int64(500), "c": "Lyon"}))}, + {`df.eval("pop * 2")`, ms(df.Eval("pop * 2"))}, + {`df.eval("pop * @k", k: 1.5)`, ms(df.Eval("pop * @k", map[string]any{"k": 1.5}))}, + {`df.iloc(0)`, m(df.Iloc(dataframe.Pos(0)))}, + {`df.iloc([2, 0], [1])`, m(df.Iloc(dataframe.Pos(2, 0), dataframe.Pos(1)))}, + {`df.iloc(0...2)`, m(df.Iloc(dataframe.Slice(0, 2)))}, + {`df.iloc(1..2)`, m(df.Iloc(dataframe.Slice(1, 3)))}, + {`df.iloc(1..)`, m(df.Iloc(dataframe.SliceFrom(1)))}, + {`df.iloc(..1)`, m(df.Iloc(dataframe.Slice(0, 2)))}, + {`df.iloc(nil, 0..1)`, m(df.Iloc(dataframe.All(), dataframe.Slice(0, 2)))}, + {`df.iloc(:all, [true, false, true])`, m(df.Iloc(dataframe.All(), dataframe.Mask([]bool{true, false, true})))}, + {`df.iloc(b)`, m(df.Iloc(dataframe.Mask(x.b)))}, + {`df.loc(1)`, m(df.Loc(dataframe.Labels(int64(1))))}, + {`df.loc([2, 0], ["x"])`, m(df.Loc(dataframe.Labels(int64(2), int64(0)), "x"))}, + {`df.loc(0..1, :city)`, m(df.Loc(dataframe.LabelRange(int64(0), int64(1)), "city"))}, + {`df.loc(1.., :city)`, m(df.Loc(dataframe.LabelRange(int64(1), nil), "city"))}, + {`df.set_index("city").loc("Paris")`, m(m(df.SetIndex("city")).Loc(dataframe.Labels("Paris")))}, + {`ts.loc(Time.utc(2024, 1, 2)..)`, m(ts.Loc(dataframe.LabelRange(time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC), nil)))}, + {`df.loc(b, "pop")`, m(df.Loc(dataframe.Mask(x.b), "pop"))}, + {`df.loc([false, true, true])`, m(df.Loc(dataframe.Mask([]bool{false, true, true})))}, + {`df.apply { |r| r["pop"] * 2 }`, ms(df.Apply("", func(r dataframe.Row) any { return r.Get("pop").(int64) * 2 }))}, + {`df.apply("q") { |r| r.keys.join }`, ms(df.Apply("q", func(r dataframe.Row) any { return "citypopx" }))}, + {`df.apply("q") { |r| r["x"] }`, ms(df.Apply("q", func(r dataframe.Row) any { return r.Get("x") }))}, + {`ts.resample("W").sum`, m(m4(ts.Resample("W")).Sum())}, + {`ts.reset_index.resample("2D", on: "day").mean`, m(m4(ts.ResetIndex(false).ResampleOn("day", "2D")).Mean())}, + } { + dfIs(t, in, cs.src, cs.want) + } + + var buf bytes.Buffer + if err := df.WriteCSV(&buf); err != nil { + t.Fatal(err) + } + dfIs(t, in, `df.to_csv`, buf.String()) + buf.Reset() + if err := df.WriteCSV(&buf, dataframe.CSVWriteHeader(false), dataframe.CSVWriteIndex(false), dataframe.CSVWriteLineTerminator("\r\n"), + dataframe.CSVWriteNARep("NA"), dataframe.CSVWriteSeparator(';')); err != nil { + t.Fatal(err) + } + dfIs(t, in, `df.to_csv(header: false, index: false, line_terminator: "\r\n", na_rep: "NA", sep: ";")`, buf.String()) + dir := t.TempDir() + if err := in.Set("out", filepath.Join(dir, "o.csv")); err != nil { + t.Fatal(err) + } + eval(t, in, `df.write_csv(out, index: false)`) + buf.Reset() + _ = df.WriteCSV(&buf, dataframe.CSVWriteIndex(false)) + if got, _ := os.ReadFile(filepath.Join(dir, "o.csv")); string(got) != buf.String() { + t.Errorf("write_csv wrote %q, want %q", got, buf.String()) + } +} + +// m2, m3 and m4 are must for the other result types. +func m2(ms func(*dataframe.Series, error) *dataframe.Series) func(*dataframe.Series, error) *dataframe.Series { + return ms +} + +func m3(g *dataframe.GroupBy, err error) *dataframe.GroupBy { + if err != nil { + panic(err) + } + return g +} + +func m4(r *dataframe.Resampler, err error) *dataframe.Resampler { + if err != nil { + panic(err) + } + return r +} diff --git a/plot_test.go b/plot_test.go index 9d500b2..0bdc96d 100644 --- a/plot_test.go +++ b/plot_test.go @@ -7,8 +7,8 @@ import ( "strings" "testing" - rb "github.com/go-embedded-ruby/ruby" "github.com/go-dataframe/dataframe" + rb "github.com/go-embedded-ruby/ruby" nd "github.com/go-ndarray/ndarray" "github.com/go-plot/plot" ) From e6b2968d12a9a68eab2b9dbe22d2d653c0bb3f3f Mon Sep 17 00:00:00 2001 From: tannevaled Date: Sat, 17 Oct 2026 22:40:51 +0200 Subject: [PATCH 11/20] DataFrame: series, group-bys, indexes and every error branch tested against Go Co-Authored-By: Claude Opus 5.5 --- dataframe.go | 7 +- dataframe_test.go | 471 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 472 insertions(+), 6 deletions(-) diff --git a/dataframe.go b/dataframe.go index 2b63f41..f2bf411 100644 --- a/dataframe.go +++ b/dataframe.go @@ -419,7 +419,7 @@ func (e *dfEnv) results(vals []reflect.Value) (any, error) { } // out converts a Go result for Ruby: a time.Time is a UTC Time, a DType its -// name, slices element by element; the rest as every binding converts it. +// name, slices element by element (nil gives []); the rest as every binding converts it. func (e *dfEnv) out(v reflect.Value) any { if v.Kind() == reflect.Interface { if v.IsNil() { @@ -437,10 +437,7 @@ func (e *dfEnv) out(v reflect.Value) any { case []float64, []byte: return out(v) } - if v.Kind() == reflect.Slice { - if v.IsNil() { - return nil - } + if v.Kind() == reflect.Slice { // a nil slice is an empty Array list := make([]any, v.Len()) for i := range list { list[i] = e.out(v.Index(i)) diff --git a/dataframe_test.go b/dataframe_test.go index 38af9b5..8fdb4d4 100644 --- a/dataframe_test.go +++ b/dataframe_test.go @@ -10,8 +10,8 @@ import ( "testing" "time" - rb "github.com/go-embedded-ruby/ruby" "github.com/go-dataframe/dataframe" + rb "github.com/go-embedded-ruby/ruby" nd "github.com/go-ndarray/ndarray" ) @@ -122,6 +122,7 @@ func newDFFixture(t *testing.T) (*rb.Interp, dfFixture) { func TestDataFrameConstruction(t *testing.T) { in, x := newDFFixture(t) m := must[*dataframe.DataFrame](t) + ms := must[*dataframe.Series](t) dfIs(t, in, `df`, x.df) dfIs(t, in, `long`, x.long) dfIs(t, in, `DataFrame.new({"b" => [1], "a" => ["z"]}, c: [true])`, m(dataframe.New( @@ -129,6 +130,9 @@ func TestDataFrameConstruction(t *testing.T) { dfIs(t, in, `DataFrame.new([[:a, NDArray.linspace(0, 1, 3)], ["s", df["pop"]]])`, m(dataframe.New( dataframe.Col("a", must[*nd.Array](t)(nd.FromData([]float64{0, 0.5, 1}, 3))), dataframe.Col("s", x.s)))) dfIs(t, in, `DataFrame.new`, m(dataframe.New())) + dfIs(t, in, `DataFrame.new.columns`, m(dataframe.New()).Columns()) + dfIs(t, in, `DataFrame.new.series`, m(dataframe.New()).Series()) + dfIs(t, in, `DataFrame::Series.new([1]).valid`, ms(dataframe.NewSeries("", []any{int64(1)})).Valid()) evalErr(t, in, `DataFrame.new(t: [Time.utc(2020, 1, 2, 3, 4, 5), :sym])`, "ArgumentError", "") evalErr(t, in, `DataFrame.new(a: [1, 2], b: [1])`, "ArgumentError", "length") evalErr(t, in, `DataFrame.new(1)`, "TypeError", "Integer into Hash") @@ -418,3 +422,468 @@ func m4(r *dataframe.Resampler, err error) *dataframe.Resampler { } return r } + +func TestDataFrameSeries(t *testing.T) { + in, x := newDFFixture(t) + ms := must[*dataframe.Series](t) + s, f, b, str, day := x.s, x.f, x.b, x.str, x.day + mf := func(v *nd.Array, err error) *nd.Array { return must[*nd.Array](t)(v, err) } + agg := func(v any, err error) any { return must[any](t)(v, err) } + fl := func(v []float64, err error) []float64 { return must[[]float64](t)(v, err) } + dt, err := day.Dt() + if err != nil { + t.Fatal(err) + } + rollMean := func(r *dataframe.Rolling, err error) *dataframe.Series { + return must[*dataframe.Rolling](t)(r, err).Mean() + } + disp := map[string][]byte{} + for k, v := range s.Display() { + disp[k] = v + } + for _, c := range []struct { + src string + want any + }{ + {`DataFrame::Series.new([1, 2, 3])`, ms(dataframe.NewSeries("", []any{int64(1), int64(2), int64(3)}))}, + {`DataFrame::Series.new([1.5, nil], name: :a)`, ms(dataframe.NewSeries("a", []any{1.5, nil}))}, + {`DataFrame::Series.new(NDArray.linspace(0, 1, 3), name: "a")`, ms(dataframe.NewSeries("a", mf(nd.FromData([]float64{0, 0.5, 1}, 3))))}, + {`DataFrame::Series.new([1, 2], index: ["a", "b"])`, ms(dataframe.NewSeriesIndexed("", []any{int64(1), int64(2)}, + must[*dataframe.Index](t)(dataframe.NewIndex("", []any{"a", "b"}))))}, + {`DataFrame::Series.new([1, 2], index: DataFrame::Index.range(2))`, ms(dataframe.NewSeriesIndexed("", []any{int64(1), int64(2)}, dataframe.NewRangeIndex(2)))}, + {`s.name`, s.Name()}, + {`s.length`, s.Len()}, + {`s.size`, s.Len()}, + {`f.count`, f.Count()}, + {`s.dtype`, s.DType()}, + {`s.index`, s.Index()}, + {`s.values`, mf(s.Values())}, + {`s.to_ndarray`, mf(s.Values())}, + {`s.floats`, fl(s.Floats())}, + {`s.to_a`, s.Items()}, + {`day.to_a`, day.Items()}, + {`f.valid`, f.Valid()}, + {`s.get(1)`, agg(s.Get(int64(1)))}, + {`s.mean`, s.Mean()}, + {`s.sum`, s.SumValue()}, + {`f.sum`, f.SumValue()}, + {`s.std`, s.Std()}, + {`s.var`, s.VarDDOF(1)}, + {`s.var(ddof: 0)`, s.VarDDOF(0)}, + {`s.var(0)`, s.VarDDOF(0)}, + {`s.min`, s.Min()}, + {`s.max`, s.Max()}, + {`day.max`, day.Max()}, + {`s.median`, s.Median()}, + {`s.prod`, s.Prod()}, + {`str.nunique`, str.NUnique()}, + {`s.quantile`, s.Quantile(0.5)}, + {`s.quantile(0.25)`, s.Quantile(0.25)}, + {`s.quantile(q: 0.75)`, s.Quantile(0.75)}, + {`s.agg("sum")`, agg(s.Agg("sum"))}, + {`s.agg(:mean)`, agg(s.Agg("mean"))}, + {`s.head`, s.Head(5)}, + {`s.head(1)`, s.Head(1)}, + {`s.tail(2)`, s.Tail(2)}, + {`str.unique`, str.Unique()}, + {`str.value_counts`, str.ValueCounts()}, + {`s.copy`, s.Copy()}, + {`s.to_frame`, s.ToFrame()}, + {`s.reset_index`, s.ResetIndex()}, + {`f.isna`, f.IsNA()}, + {`f.notna`, f.NotNA()}, + {`f.dropna`, f.DropNA()}, + {`f.fillna(0)`, ms(f.FillNA(int64(0)))}, + {`f.ffill`, f.FFill()}, + {`f.bfill`, f.BFill()}, + {`s.shift`, s.Shift(1)}, + {`s.shift(periods: 2)`, s.Shift(2)}, + {`s.diff`, ms(s.Diff(1))}, + {`s.pct_change(2)`, ms(s.PctChange(2))}, + {`s.cumsum`, ms(s.CumSum())}, + {`s.cumprod`, ms(s.CumProd())}, + {`s.cummax`, ms(s.CumMax())}, + {`s.cummin`, ms(s.CumMin())}, + {`s.neg.abs`, ms(ms(s.Neg()).Abs())}, + {`s.send(:-@)`, ms(s.Neg())}, + {`f.round`, f.Round(0)}, + {`s.div(7).round(decimals: 3)`, ms(s.Div(int64(7))).Round(3)}, + {`s.rename("p")`, s.Rename("p")}, + {`s.astype(:float32)`, ms(s.AsType(dataframe.Float32))}, + {`s.reindex([2, 0])`, ms(s.Reindex([]any{int64(2), int64(0)}))}, + {`s.with_index(DataFrame::Index.new(["a", "b", "c"]))`, ms(s.SetIndex(must[*dataframe.Index](t)(dataframe.NewIndex("", []any{"a", "b", "c"}))))}, + {`s.sort_values`, s.SortValues(true)}, + {`s.sort_values(ascending: false)`, s.SortValues(false)}, + {`s.sort_values.sort_index(ascending: false)`, s.SortValues(true).SortIndex(false)}, + {`s.sort_index`, s.SortIndex(true)}, + {`s.between(500, 600)`, ms(s.Between(int64(500), int64(600)))}, + {`str.isin("Lyon", "Rome")`, ms(str.IsIn("Lyon", "Rome"))}, + {`str.isin(["Paris"])`, ms(str.IsIn("Paris"))}, + {`s.isin(522, 1)`, ms(s.IsIn(int64(522), int64(1)))}, + {`day.dt.year`, dt.Year()}, + {`day.dt.month`, dt.Month()}, + {`day.dt.day`, dt.Day()}, + {`day.dt.hour`, dt.Hour()}, + {`day.dt.minute`, dt.Minute()}, + {`day.dt.second`, dt.Second()}, + {`day.dt.day_of_week`, dt.DayOfWeek()}, + {`day.dt.dayofweek`, dt.DayOfWeek()}, + {`day.dt.day_of_year`, dt.DayOfYear()}, + {`day.dt.dayofyear`, dt.DayOfYear()}, + {`day.dt.normalize`, dt.Normalize()}, + {`day.dt.strftime("%d/%m")`, dt.Strftime("%d/%m")}, + {`day.to_period("M")`, ms(day.ToPeriod("M"))}, + {`day.to_period("M").to_timestamp`, ms(ms(day.ToPeriod("M")).ToTimestamp())}, + {`multi["v"].unstack`, must[*dataframe.DataFrame](t)(ms(x.multi.Column("v")).Unstack())}, + {`s.to_s`, s.String()}, + {`s.inspect`, s.String()}, + {`s.to_html`, s.HTML()}, + {`s.display`, disp}, + {`s.+(1)`, ms(s.Add(int64(1)))}, + {`s.add(s)`, ms(s.Add(s))}, + {`str.add("!")`, ms(str.Add("!"))}, + {`s.-(1.5)`, ms(s.Sub(1.5))}, + {`s.sub(s)`, ms(s.Sub(s))}, + {`s.*(2)`, ms(s.Mul(int64(2)))}, + {`s.mul(f)`, ms(s.Mul(f))}, + {`s./(2)`, ms(s.Div(int64(2)))}, + {`s.div(f)`, ms(s.Div(f))}, + {`s.%(7)`, ms(s.Mod(int64(7)))}, + {`s.mod(7)`, ms(s.Mod(int64(7)))}, + {`s.**(2)`, ms(s.Pow(int64(2)))}, + {`s.pow(0.5)`, ms(s.Pow(0.5))}, + {`s.floordiv(7)`, ms(s.FloorDiv(int64(7)))}, + {`s > 525`, ms(s.Gt(int64(525)))}, + {`s.gt(525)`, ms(s.Gt(int64(525)))}, + {`s < 525`, ms(s.Lt(int64(525)))}, + {`s.lt(s)`, ms(s.Lt(s))}, + {`s >= 530`, ms(s.Ge(int64(530)))}, + {`s.ge(530)`, ms(s.Ge(int64(530)))}, + {`s <= 530`, ms(s.Le(int64(530)))}, + {`s.le(530)`, ms(s.Le(int64(530)))}, + {`str.eq("Lyon")`, ms(str.Eq("Lyon"))}, + {`str.ne(:Lyon)`, ms(str.Ne("Lyon"))}, + {`day > Time.utc(2024, 1, 1)`, ms(day.Gt(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)))}, + {`b & s.lt(600)`, ms(b.And(ms(s.Lt(int64(600)))))}, + {`b | s.lt(600)`, ms(b.Or(ms(s.Lt(int64(600)))))}, + {`b ^ s.lt(600)`, ms(b.Xor(ms(s.Lt(int64(600)))))}, + {`~b`, ms(b.Not())}, + {`b.invert`, ms(b.Not())}, + // The methods written by hand. + {`s[1]`, agg(s.At(1))}, + {`s[-1]`, agg(s.At(-1))}, + {`day[0]`, agg(day.At(0))}, + {`f[1]`, nil}, + {`s[b]`, ms(s.Filter(b))}, + {`s[[true, false, false]]`, ms(s.Filter([]bool{true, false, false}))}, + {`s[0..1]`, ms(s.Iloc(dataframe.Slice(0, 2)))}, + {`s[[2, 0]]`, ms(s.Iloc(dataframe.Pos(2, 0)))}, + {`s.filter(b)`, ms(s.Filter(b))}, + {`s.filter([false, true, true])`, ms(s.Filter([]bool{false, true, true}))}, + {`s.map { |v| v * 2 }`, ms(s.Map(func(v any) any { return v.(int64) * 2 }))}, + {`f.map { |v| v.nil? ? "none" : "v" }`, ms(f.Map(func(v any) any { + if v == nil { + return "none" + } + return "v" + }))}, + {`day.map { |v| v.year }`, ms(day.Map(func(v any) any { return int64(v.(time.Time).Year()) }))}, + {`s.rolling(2).mean`, rollMean(s.Rolling(2))}, + {`s.rolling(2, min_periods: 1).mean`, rollMean(s.RollingMin(2, 1))}, + {`s.rolling(2).sum`, must[*dataframe.Rolling](t)(s.Rolling(2)).Sum()}, + {`s.rolling(2).min`, must[*dataframe.Rolling](t)(s.Rolling(2)).Min()}, + {`s.rolling(2).max`, must[*dataframe.Rolling](t)(s.Rolling(2)).Max()}, + {`s.rolling(2).std`, must[*dataframe.Rolling](t)(s.Rolling(2)).Std()}, + {`s.rolling(2).median`, must[*dataframe.Rolling](t)(s.Rolling(2)).Median()}, + {`s.iloc([0, 2])`, ms(s.Iloc(dataframe.Pos(0, 2)))}, + {`s.loc(1..2)`, ms(s.Loc(dataframe.LabelRange(int64(1), int64(2))))}, + {`s.loc(b)`, ms(s.Loc(dataframe.Mask(b)))}, + {`DataFrame::Series.new(["2024-03-04"]).to_datetime`, ms(dataframe.ToDatetime(ms(dataframe.NewSeries("", []any{"2024-03-04"}))))}, + {`DataFrame::Series.new(["04/03/2024"]).to_datetime("%d/%m/%Y")`, + ms(dataframe.ToDatetime(ms(dataframe.NewSeries("", []any{"04/03/2024"})), "%d/%m/%Y"))}, + } { + dfIs(t, in, c.src, c.want) + } +} + +func TestDataFrameGroupsAndIndexes(t *testing.T) { + in, x := newDFFixture(t) + m := must[*dataframe.DataFrame](t) + ms := must[*dataframe.Series](t) + g := m3(x.long.GroupBy("k")) + g2 := m3(x.df.GroupBy("city", "pop")) + r := m4(x.ts.Resample("W")) + eval(t, in, `g = long.group_by("k"); r = ts.resample("W")`) + ix := x.multi.Index() + sx := must[*dataframe.Index](t)(dataframe.NewIndex("n", []any{"a", "b", "a"})) + eval(t, in, `ix = multi.index; sx = DataFrame::Index.new(["a", "b", "a"], name: "n")`) + keys, frames := g.Groups() + keys2, frames2 := g2.Groups() + var groups, groups2 []any + for i := range keys { + groups = append(groups, []any{keys[i], frames[i]}) + } + for i := range keys2 { + groups2 = append(groups2, []any{keys2[i], frames2[i]}) + } + ixDisp := map[string][]byte{} + for k, v := range sx.Display() { + ixDisp[k] = v + } + for _, c := range []struct { + src string + want any + }{ + {`g.mean`, m(g.Mean())}, + {`g.sum`, m(g.Sum())}, + {`g.count`, m(g.Count())}, + {`g.min`, m(g.Min())}, + {`g.max`, m(g.Max())}, + {`g.median`, m(g.Median())}, + {`g.std`, m(g.Std())}, + {`g.var`, m(g.Var())}, + {`g.first`, m(g.First())}, + {`g.last`, m(g.Last())}, + {`g.nunique`, m(g.NUnique())}, + {`g.size`, g.Size()}, + {`g.ngroups`, g.NGroups()}, + {`g.n_groups`, g.NGroups()}, + {`g.select("v").sum`, m(m3(g.Select("v")).Sum())}, + {`g["v"].sum`, m(m3(g.Select("v")).Sum())}, + {`g.transform("v", "sum")`, ms(g.Transform("v", "sum"))}, + {`g.to_s`, g.String()}, + {`g.inspect`, g.String()}, + {`g.agg(v: [:sum, :max])`, m(g.Agg(dataframe.Agg("v", "sum", "max")))}, + {`g.agg({"v" => "mean"}, c: :first)`, m(g.Agg(dataframe.Agg("v", "mean"), dataframe.Agg("c", "first")))}, + {`g.groups`, groups}, + {`df.group_by("city", "pop").groups`, groups2}, + {`r.mean`, m(r.Mean())}, + {`r.sum`, m(r.Sum())}, + {`r.count`, m(r.Count())}, + {`r.min`, m(r.Min())}, + {`r.max`, m(r.Max())}, + {`r.median`, m(r.Median())}, + {`r.std`, m(r.Std())}, + {`r.first`, m(r.First())}, + {`r.last`, m(r.Last())}, + {`r.size`, r.Size()}, + {`r.agg(v: [:sum, :mean])`, m(r.Agg(dataframe.Agg("v", "sum", "mean")))}, + {`ix.to_a`, ix.Labels()}, + {`ix.labels`, ix.Labels()}, + {`ix.length`, ix.Len()}, + {`ix.size`, ix.Len()}, + {`sx.name`, sx.Name()}, + {`ix.names`, ix.Names()}, + {`ix.nlevels`, ix.NLevels()}, + {`ix.range?`, ix.IsRange()}, + {`df.index.range?`, true}, + {`ix.label(1)`, ix.Label(1)}, + {`sx[2]`, sx.Label(2)}, + {`sx.get("a")`, must[[]int](t)(sx.Get("a"))}, + {`ix.get(["a", "y"])`, must[[]int](t)(ix.Get([]any{"a", "y"}))}, + {`df.index.values`, must[*nd.Array](t)(x.df.Index().Values())}, + {`sx.dtype`, sx.DType()}, + {`ix.level(1)`, ms(ix.Level(1))}, + {`ix.rename("p", "q")`, must[*dataframe.Index](t)(ix.Rename("p", "q"))}, + {`ix.to_s`, ix.String()}, + {`ix.inspect`, ix.String()}, + {`sx.display`, ixDisp}, + {`ts.index[0]`, x.ts.Index().Label(0)}, + {`DataFrame::Index.range(3)`, dataframe.NewRangeIndex(3)}, + {`DataFrame::Index.new([1, 2])`, must[*dataframe.Index](t)(dataframe.NewIndex("", []any{int64(1), int64(2)}))}, + } { + dfIs(t, in, c.src, c.want) + } +} + +func TestDataFrameErrors(t *testing.T) { + in, _ := newDFFixture(t) + eval(t, in, `g = long.group_by("k")`) + for _, c := range []struct{ src, class, msg string }{ + // The table-driven methods. + {`df.head(1, 2)`, "ArgumentError", "given 2, expected 1"}, + {`df.head(bogus: 1)`, "ArgumentError", "unknown keyword: :bogus (known: n)"}, + {`df.columns(x: 1)`, "ArgumentError", "unknown keyword: :x (known: none)"}, + {`df.head(1, n: 2)`, "ArgumentError", "n given both by position and by keyword"}, + {`df.column`, "ArgumentError", "given 0, expected 1"}, + {`df.column("nope")`, "ArgumentError", "no such column"}, + {`df.column(1)`, "TypeError", "Integer into String"}, + {`df.drop(1)`, "TypeError", "Integer into String"}, + {`s.astype(:nope)`, "ArgumentError", "nope"}, + {`df.with_column("y", [1])`, "ArgumentError", "length"}, + // [] and filter. + {`df[1]`, "TypeError", "a column name, an Array of names or a mask"}, + {`df[[1]]`, "TypeError", "Integer into String"}, + {`df["a", "b"]`, "ArgumentError", "given 2, expected 1"}, + {`df[[true]]`, "ArgumentError", ""}, + {`df.filter(1)`, "TypeError", "Integer into Array"}, + {`df.filter`, "ArgumentError", "given 0, expected 1"}, + {`df.dtypes(1)`, "ArgumentError", "given 1, expected 0"}, + {`df.to_h(1)`, "ArgumentError", "given 1, expected 0"}, + {`df.to_csv(1)`, "ArgumentError", "given 1, expected 0"}, + {`df.to_csv(bogus: 1)`, "ArgumentError", "unknown keyword: :bogus (known: header,"}, + {`df.to_csv(sep: 1)`, "TypeError", "Integer into String"}, + {`df.to_csv(header: 1)`, "TypeError", "true or false"}, + {`df.to_csv(na_rep: 1)`, "TypeError", "Integer into String"}, + {`df.write_csv`, "ArgumentError", "given 0, expected 1"}, + {`df.write_csv(1)`, "TypeError", "Integer into String"}, + {`df.write_csv("/no/such/dir/x.csv")`, "IOError", "no such file"}, + {`df.write_csv("x.csv", bogus: 1)`, "ArgumentError", "unknown keyword"}, + {`df.sort_values("pop", bogus: 1)`, "ArgumentError", "unknown keyword: :bogus (known: ascending)"}, + {`df.sort_values`, "ArgumentError", "given 0, expected 1"}, + {`df.sort_values(1)`, "TypeError", "String or an Array"}, + {`df.sort_values("pop", ascending: 1)`, "TypeError", "true or false"}, + {`df.sort_values("nope")`, "ArgumentError", "no such column"}, + {`df.group_by("city", x: 1)`, "ArgumentError", "unknown keyword"}, + {`df.group_by(1)`, "TypeError", "Integer into String"}, + {`df.group_by("nope")`, "ArgumentError", "no such column"}, + {`df.dropna(bogus: 1)`, "ArgumentError", "unknown keyword: :bogus (known: axis, how, thresh)"}, + {`df.dropna(axis: "a")`, "TypeError", "String into Integer"}, + {`df.dropna(axis: 2)`, "ArgumentError", "axis must be 0 or 1"}, + {`df.dropna(1)`, "TypeError", "Integer into String"}, + {`df.dropna(thresh: "a")`, "TypeError", "String into Integer"}, + {`df.dropna(how: 1)`, "TypeError", "Integer into String"}, + {`df.dropna(how: :some)`, "ArgumentError", "how must be :any or :all"}, + {`df.dropna("nope")`, "ArgumentError", "no such column"}, + {`df.fillna({1 => 2})`, "TypeError", "Integer into String"}, + {`df.fillna("nope" => 2)`, "ArgumentError", "no such column"}, + {`df.fillna`, "ArgumentError", "given 0, expected 1"}, + {`df.astype({1 => :int64})`, "TypeError", "Integer into String"}, + {`df.astype("city" => :int64)`, "ArgumentError", ""}, + {`df.astype(1)`, "TypeError", "Integer into a dtype"}, + {`df.astype(:int64)`, "ArgumentError", ""}, + {`df.astype`, "ArgumentError", "given 0, expected 1"}, + {`df.rename(1)`, "ArgumentError", "rename takes a Hash"}, + {`df.rename({"a" => 1})`, "TypeError", "Integer into String"}, + {`df.merge(1)`, "TypeError", "Integer into DataFrame"}, + {`df.merge`, "ArgumentError", "given 0, expected 1"}, + {`df.merge(df, bogus: 1)`, "ArgumentError", "unknown keyword: :bogus"}, + {`df.merge(df, how: "sideways")`, "ArgumentError", ""}, + {`df.join(df, bogus: 1)`, "ArgumentError", "unknown keyword: :bogus (known: how, lsuffix, rsuffix)"}, + {`df.join(df, how: 1)`, "TypeError", "Integer into String"}, + {`df.join(1)`, "TypeError", "Integer into DataFrame"}, + {`df.join(df)`, "ArgumentError", ""}, + {`df.melt(bogus: 1)`, "ArgumentError", "unknown keyword: :bogus (known: id_vars, value_vars)"}, + {`df.melt(1)`, "ArgumentError", "given 1, expected 0"}, + {`df.melt(id_vars: 1)`, "TypeError", "String or an Array"}, + {`df.melt(id_vars: "nope")`, "ArgumentError", "no such column"}, + {`df.query`, "ArgumentError", "given 0, expected 1"}, + {`df.query(1)`, "TypeError", "Integer into String"}, + {`df.query("pop >")`, "ArgumentError", "bad query"}, + {`df.eval(1)`, "TypeError", "Integer into String"}, + {`df.eval("pop >")`, "ArgumentError", "bad query"}, + {`df.iloc`, "ArgumentError", "given 0, expected 1..2"}, + {`df.iloc(0, x: 1)`, "ArgumentError", "unknown keyword"}, + {`df.iloc("a")`, "TypeError", "a position, an Array, a Range or a mask"}, + {`df.iloc(:some)`, "TypeError", "a position, an Array, a Range or a mask"}, + {`df.iloc(["a"])`, "TypeError", "String into Integer"}, + {`df.iloc([true, 1])`, "TypeError", "Integer into true or false"}, + {`df.iloc("a".."b")`, "TypeError", "String into Integer"}, + {`df.iloc(0.."b")`, "TypeError", "String into Integer"}, + {`df.iloc(9)`, "ArgumentError", "out of range"}, + {`df.loc`, "ArgumentError", "given 0, expected 1..2"}, + {`df.loc(0, x: 1)`, "ArgumentError", "unknown keyword"}, + {`df.loc(0...1)`, "ArgumentError", "a label range includes its end"}, + {`df.loc(0, 1)`, "TypeError", "String or an Array"}, + {`df.loc(9)`, "ArgumentError", "not in index"}, + {`df.apply`, "ArgumentError", "no block given"}, + {`df.apply(x: 1) { }`, "ArgumentError", "unknown keyword"}, + {`df.apply(1, 2) { }`, "ArgumentError", "given 2, expected 0..1"}, + {`df.apply(1) { }`, "TypeError", "Integer into String"}, + {`df.apply { |r| raise "boom" }`, "RuntimeError", "boom"}, + {`df.apply { |r| r["pop"] == 522 ? "a" : 1 }`, "ArgumentError", ""}, + {`df.resample("D", bogus: 1)`, "ArgumentError", "unknown keyword: :bogus (known: on)"}, + {`df.resample`, "ArgumentError", "given 0, expected 1"}, + {`df.resample(1)`, "TypeError", "Integer into String"}, + {`df.resample("D", on: 1)`, "TypeError", "Integer into String"}, + {`df.resample("D")`, "ArgumentError", ""}, + {`df.resample("D", on: "city")`, "ArgumentError", ""}, + // Series. + {`DataFrame::Series.new([1], bogus: 1)`, "ArgumentError", "unknown keyword: :bogus (known: index, name)"}, + {`DataFrame::Series.new`, "ArgumentError", "given 0, expected 1"}, + {`DataFrame::Series.new([1], name: 1)`, "TypeError", "Integer into String"}, + {`DataFrame::Series.new([1], index: [[1]])`, "ArgumentError", ""}, + {`DataFrame::Series.new([1], index: [1, 2])`, "ArgumentError", "length"}, + {`DataFrame::Series.new(1)`, "ArgumentError", "cannot make a column"}, + {`s[]`, "ArgumentError", "given 0, expected 1"}, + {`s["a"]`, "TypeError", "a position, an Array, a Range or a mask"}, + {`s[9]`, "ArgumentError", "out of range"}, + {`s[[9]]`, "ArgumentError", "out of range"}, + {`s.filter(1)`, "TypeError", "Integer into Array"}, + {`s.filter`, "ArgumentError", "given 0, expected 1"}, + {`s.filter([true])`, "ArgumentError", ""}, + {`s.map`, "ArgumentError", "no block given"}, + {`s.map(1) { }`, "ArgumentError", "given 1, expected 0"}, + {`s.map { |v| raise "boom" }`, "RuntimeError", "boom"}, + {`s.map { |v| v == 522 ? "a" : 1 }`, "ArgumentError", ""}, + {`s.rolling(2, bogus: 1)`, "ArgumentError", "unknown keyword: :bogus (known: min_periods)"}, + {`s.rolling`, "ArgumentError", "given 0, expected 1"}, + {`s.rolling("a")`, "TypeError", "String into Integer"}, + {`s.rolling(2, min_periods: "a")`, "TypeError", "String into Integer"}, + {`s.rolling(0)`, "ArgumentError", ""}, + {`s.iloc`, "ArgumentError", "given 0, expected 1"}, + {`s.iloc("a")`, "TypeError", "a position"}, + {`s.loc(9)`, "ArgumentError", "not in index"}, + {`s.iloc([9])`, "ArgumentError", "out of range"}, + {`s.to_datetime(x: 1)`, "ArgumentError", "unknown keyword"}, + {`s.to_datetime(1, 2)`, "ArgumentError", "given 2, expected 0..1"}, + {`s.to_datetime(1)`, "TypeError", "Integer into String"}, + {`str.to_datetime`, "ArgumentError", ""}, + // GroupBy, Resampler, Index. + {`g.agg(1)`, "TypeError", "Integer into Hash"}, + {`g.agg([[1, :sum]])`, "TypeError", "Integer into String"}, + {`g.agg(v: 1)`, "TypeError", "String or an Array"}, + {`g.agg(nope: :sum)`, "ArgumentError", ""}, + {`g.groups(1)`, "ArgumentError", "given 1, expected 0"}, + {`DataFrame::Index.new([1], bogus: 1)`, "ArgumentError", "unknown keyword: :bogus (known: name)"}, + {`DataFrame::Index.new`, "ArgumentError", "given 0, expected 1"}, + {`DataFrame::Index.new([1], name: 1)`, "TypeError", "Integer into String"}, + {`DataFrame::Index.new(1)`, "ArgumentError", "cannot make a column"}, + {`DataFrame::Index.range`, "ArgumentError", "given 0, expected 1"}, + {`DataFrame::Index.range("a")`, "TypeError", "String into Integer"}, + } { + evalErr(t, in, c.src, c.class, c.msg) + } +} + +// dfSame must tell apart frames that differ in one value or in their type. +func TestDFSameIsStrict(t *testing.T) { + a, _ := dataframe.New(dataframe.Col("a", []int64{1, 2})) + b, _ := dataframe.New(dataframe.Col("a", []int64{1, 3})) + s, _ := a.Column("a") + for i, c := range []struct { + got, want any + same bool + }{ + {a, a, true}, + {a, b, false}, + {s, a, false}, + {"int64", dataframe.Int64, true}, + {"float64", dataframe.Int64, false}, + {[]any{a}, []*dataframe.DataFrame{a}, true}, + {[]any{a}, []*dataframe.DataFrame{b}, false}, + {[]any{a, a}, []*dataframe.DataFrame{a}, false}, + {1, []*dataframe.DataFrame{a}, false}, + {map[any]any{"k": "v"}, map[string][]byte{"k": []byte("v")}, true}, + {map[any]any{"k": "w"}, map[string][]byte{"k": []byte("v")}, false}, + {1, map[string][]byte{"k": []byte("v")}, false}, + {1, time.Unix(0, 0), false}, + } { + if got := dfSame(c.got, c.want); got != c.same { + t.Errorf("case %d: dfSame = %v", i, got) + } + } +} + +// A name already taken by something else is an error of Register. +func TestDataFrameRegisterConflicts(t *testing.T) { + for _, src := range []string{`module DataFrame; end`, `class DataFrame; module Series; end; end`} { + in := rb.New() + if _, err := in.Eval(src, "x.rb", 1); err != nil { + t.Fatal(err) + } + if err := registerDataFrame(in); err == nil || !strings.Contains(err.Error(), "already defined") { + t.Errorf("%s: registerDataFrame = %v", src, err) + } + } +} From 3db9f6a3c68254559a747607b30f9dd29b3a11fe Mon Sep 17 00:00:00 2001 From: tannevaled Date: Sat, 17 Oct 2026 22:44:30 +0200 Subject: [PATCH 12/20] Examples of DataFrame and Plot, run by a test Co-Authored-By: Claude Opus 5.5 --- examples/dataframe.rb | 39 +++++++++++++++++++++++++++++++++++++++ examples/plot.rb | 31 +++++++++++++++++++++++++++++++ examples_df_plot_test.go | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 examples/dataframe.rb create mode 100644 examples/plot.rb create mode 100644 examples_df_plot_test.go diff --git a/examples/dataframe.rb b/examples/dataframe.rb new file mode 100644 index 0000000..52cdb44 --- /dev/null +++ b/examples/dataframe.rb @@ -0,0 +1,39 @@ +# DataFrame: pandas' DataFrame and Series, from go-dataframe. +# Run with the go-scikit bindings registered (ruby.Register). + +df = DataFrame.new( + city: ["Lyon", "Paris", "Lyon", "Nice", "Paris"], + year: [2020, 2020, 2021, 2021, 2021], + pop: [516.1, 2148.3, 522.2, 342.7, 2133.1] +) +puts df +puts df.shape.inspect # [5, 3] +puts df.dtypes.inspect # {"city" => "string", "year" => "int64", "pop" => "float64"} + +# Columns are Series; comparisons give masks, and a mask selects rows. +big = df[df["pop"] > 500] +puts big +puts df["pop"].mean + +# Group, aggregate, sort. +puts df.group_by("city").agg(pop: [:mean, :max], year: :count) +puts df.sort_values(["year", "pop"], ascending: [true, false]).head(3) + +# Arithmetic on a Series uses the named methods (add, mul, ...). +puts df["pop"].mul(1000).round.to_a.inspect + +# Reading CSV text, with pandas' options. +csv = <<~CSV + day,visits + 2024-01-01,10 + 2024-01-02, + 2024-01-09,7 +CSV +visits = DataFrame.read_csv(csv, parse_dates: ["day"], index_col: "day") +puts visits.fillna(0).resample("W").sum +puts visits.index[0] # 2024-01-01 00:00:00 UTC + +# A Series built row by row, and the data back as an NDArray. +ratio = df.apply("per_year") { |row| row["pop"] / row["year"] } +puts ratio.values.inspect +puts df.to_csv(index: false) diff --git a/examples/plot.rb b/examples/plot.rb new file mode 100644 index 0000000..bcec795 --- /dev/null +++ b/examples/plot.rb @@ -0,0 +1,31 @@ +# Plot: matplotlib's figures and axes, from go-plot. +# Run with the go-scikit bindings registered (ruby.Register). + +x = NDArray.linspace(0, 2 * Math::PI, 50).to_a # an NDArray works as data too +fig, (left, right) = Plot.subplots(1, 2, figsize: [9, 3.5]) + +left.plot(x, x.map { |v| Math.sin(v) }, label: "sin", color: "C0", linewidth: 2) +left.plot(x, x.map { |v| Math.cos(v) }, label: "cos", linestyle: "--") +left.axhline(0, color: "k", lw: 0.5) +left.legend(loc: "upper right") +left.title = "Trigonometry" +left.xlabel = "$x$ (rad)" +left.grid + +z = (0...20).map { |i| (0...20).map { |j| Math.sin(i / 3.0) * Math.cos(j / 4.0) } } +m = right.imshow(z, cmap: "viridis", origin: "lower") +fig.colorbar(m, right) +right.set_title("A field") + +fig.tight_layout +p fig # # +svg = fig.to_svg # File.write("fig.svg", svg) saves it +puts "#{svg.bytesize} bytes of SVG, #{fig.to_png.bytesize} bytes of PNG" +puts fig.display.keys.inspect # what a notebook shows + +# Bars and a histogram from a DataFrame column. +df = DataFrame.new(group: %w[a b c], value: [3, 7, 5]) +fig2, ax = Plot.subplots +ax.bar([0, 1, 2], df["value"], colors: ["C0", "C1", "C2"]) +ax.set_xticks([0, 1, 2], df["group"].to_a) +puts fig2.inspect diff --git a/examples_df_plot_test.go b/examples_df_plot_test.go new file mode 100644 index 0000000..09a84b6 --- /dev/null +++ b/examples_df_plot_test.go @@ -0,0 +1,34 @@ +package ruby + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + rb "github.com/go-embedded-ruby/ruby" +) + +// The examples of DataFrame and Plot run without raising. +func TestExamplesDataFramePlot(t *testing.T) { + for _, name := range []string{"dataframe.rb", "plot.rb"} { + path := filepath.Join("examples", name) + src, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + var out bytes.Buffer + in := rb.New(rb.WithStdout(&out), rb.WithStderr(&out)) + if err := Register(in); err != nil { + t.Fatal(err) + } + if _, err := in.Eval(string(src), path, 1); err != nil { + t.Errorf("%s: %v\n%s", path, err, out.String()) + continue + } + if out.Len() == 0 { + t.Errorf("%s printed nothing", path) + } + t.Logf("%s:\n%s", path, out.String()) + } +} From a899494a68312b6bf9e45cf782b91dd7c635fc20 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Sat, 17 Oct 2026 22:55:07 +0200 Subject: [PATCH 13/20] Examples per module, README listing every binding (checked by a test), CI and Renovate config, fuzz of binding arguments --- .github/workflows/ci.yml | 122 ++++++ README.md | 436 ++++++++++++++++++- examples/integrate.rb | 10 + examples/optimize.rb | 16 + examples/special.rb | 8 + examples/stats.rb | 19 + examples_df_plot_test.go => examples_test.go | 11 +- fuzz_test.go | 47 ++ readme_test.go | 101 +++++ renovate.json | 4 + smoke_test.go | 74 ---- 11 files changed, 768 insertions(+), 80 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 examples/integrate.rb create mode 100644 examples/optimize.rb create mode 100644 examples/special.rb create mode 100644 examples/stats.rb rename examples_df_plot_test.go => examples_test.go (67%) create mode 100644 fuzz_test.go create mode 100644 readme_test.go create mode 100644 renovate.json delete mode 100644 smoke_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8b34e6b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,122 @@ +name: ci + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +# Copied from go-notebook/notebook's ci.yml and adapted in two ways: +# +# - every lane runs twice where it matters, on rbgo's default build and on +# its rbgo_core build (-tags rbgo_core: the language and the numeric stack, +# no gem), which is what a notebook worker and a browser page link; +# - the cross-compile lane covers the targets rbgo itself builds for (its +# release matrix plus js/wasm), not every GOOS/GOARCH: rbgo's internal/vm +# does not compile for plan9, aix, solaris, illumos, the BSDs or 32-bit +# targets, so neither can anything that embeds it. +jobs: + test: + name: test (${{ matrix.os }}, ${{ matrix.tags || 'default' }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + tags: ['', rbgo_core] + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 + with: + go-version: '1.27.2' + - run: go version + # Not on Windows: its checkout converts line endings to CRLF. + - name: gofmt + if: runner.os != 'Windows' + shell: bash + run: | + unformatted=$(gofmt -l .) + [ -z "$unformatted" ] || { echo "::error::not gofmt-formatted:"; echo "$unformatted"; exit 1; } + - run: go vet -tags "${{ matrix.tags }}" ./... + - run: go build -tags "${{ matrix.tags }}" ./... + - name: Test + 100% coverage gate + shell: bash + run: | + COVERPKG=$(go list ./... | paste -sd, -) + go test -tags "${{ matrix.tags }}" -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./... + go tool cover -func=cover.out | tail -n 25 + cov=$(go tool cover -func=cover.out | tail -1 | awk '{print $3}' | tr -d '%') + echo "coverage: ${cov}%" + awk -v t="$cov" 'BEGIN { exit !(t+0 >= 100.0) }' || { + echo "::error::coverage ${cov}% < 100%"; exit 1; } + + security: + name: govulncheck + fuzz + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 + with: + go-version: '1.27.2' + - name: govulncheck + run: go run golang.org/x/vuln/cmd/govulncheck@v1.8.0 ./... + - name: Fuzz the binding arguments for 60 s + run: go test -tags rbgo_core -run '^$' -fuzz FuzzBindingArguments -fuzztime 60s . + + arch-native: + name: arch arm64 + runs-on: ubuntu-26.04-arm + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 + with: + go-version: '1.27.2' + - run: go vet ./... && go test ./... && go test -tags rbgo_core ./... + + arch-qemu: + name: arch ${{ matrix.goarch }} (qemu) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - { goarch: riscv64, qemu: qemu-riscv64-static } + - { goarch: loong64, qemu: qemu-loongarch64-static } + - { goarch: ppc64le, qemu: qemu-ppc64le-static } + - { goarch: s390x, qemu: qemu-s390x-static } + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 + with: + go-version: '1.27.2' + - run: sudo apt-get update && sudo apt-get install -y qemu-user-static + - name: Vet + test under qemu (${{ matrix.goarch }}) + run: | + GOOS=linux GOARCH=${{ matrix.goarch }} go vet -tags rbgo_core ./... + GOARCH=${{ matrix.goarch }} go test -tags rbgo_core -short -exec=${{ matrix.qemu }} ./... + + cross: + name: compile on every target rbgo builds for + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 + with: + go-version: '1.27.2' + - name: go test -c for each target, both builds + shell: bash + run: | + fail=0 + for t in linux/amd64 linux/arm64 linux/riscv64 linux/loong64 linux/ppc64le linux/s390x \ + darwin/amd64 darwin/arm64 windows/amd64 windows/arm64 wasip1/wasm js/wasm; do + os=${t%/*}; arch=${t#*/} + for tags in "" rbgo_core; do + for p in $(go list ./...); do + GOOS=$os GOARCH=$arch go test -tags "$tags" -c -o /dev/null "$p" >/dev/null 2>err.txt || { echo "::error::$t [$tags] $p"; cat err.txt; fail=1; } + done + done + done + exit $fail diff --git a/README.md b/README.md index 28829e7..cc2fca8 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,434 @@ -# ruby -Ruby bindings to the go-scikit stack, for the pure-Go Ruby +# ruby — go-scikit + +**Ruby bindings to the go-scikit stack, for the pure-Go Ruby.** + +The Ruby is [go-embedded-ruby/ruby](https://github.com/go-embedded-ruby/ruby) +("rbgo", a bytecode VM in pure Go, CGO=0). This module defines, in an rbgo +interpreter, Ruby modules over the Go libraries of the stack: + +| Ruby | Go library | SciPy / pandas / matplotlib counterpart | +| --- | --- | --- | +| `Stats` | [go-statistics/stats](https://github.com/go-statistics/stats) v0.1.0 | `scipy.stats` | +| `Special` | [go-numerics/special](https://github.com/go-numerics/special) v0.1.0 | `scipy.special` | +| `Optimize` | [go-numerics/optimize](https://github.com/go-numerics/optimize) v0.1.0 | `scipy.optimize` | +| `Integrate` | [go-numerics/integrate](https://github.com/go-numerics/integrate) v0.1.0 | `scipy.integrate` | +| `DataFrame` | [go-dataframe/dataframe](https://github.com/go-dataframe/dataframe) v0.1.0 | `pandas` | +| `Plot` | [go-plot/plot](https://github.com/go-plot/plot) v0.1.1 | `matplotlib.pyplot` | + +Arrays are rbgo's own `NDArray` ([go-ndarray](https://github.com/go-ndarray/ndarray)): +an NDArray passed to a binding, or returned by one, is the `*ndarray.Array` +itself, never a copy. + +```go +import ( + rb "github.com/go-embedded-ruby/ruby" + scikit "github.com/go-scikit/ruby" +) + +in := rb.New() +scikit.Register(in) +v, _ := in.Eval(`Stats::Normal.new(loc: 0, scale: 1).pdf(NDArray.linspace(-1, 1, 3))`, "x.rb", 1) +fmt.Println(v.Inspect()) // array([0.24197072, 0.39894228, 0.24197072]) +``` + +```ruby +d = Stats::Normal.fit(x) # a Stats::Normal +t = Stats.ttest_ind(a, b, welch: true) # t.statistic, t.pvalue, t.df +res = Optimize.minimize([-1.2, 1], method: :bfgs) { |x| (1 - x[0])**2 + 100 * (x[1] - x[0]**2)**2 } +q = Integrate.quad(0, Float::INFINITY) { |x| Math.exp(-x * x) } +sol = Integrate.solve_ivp([0, 10], [1.0, 0.0]) { |t, y| [y[1], -y[0]] } +fig, ax = Plot.subplots +ax.plot(sol.t, sol.y_array[0], color: "steelblue", label: "y") +svg = fig.to_svg +``` + +One short program per module is in [examples/](examples/); a test runs them all. + +## The rules + +- **Same code underneath.** Every Ruby method calls the Go function it is named + after, with the same inputs; the tests compare each binding with the direct + Go call and require the results to be identical, bit for bit (a figure: + the same SVG bytes). No numerical code is written here. +- **Names** are snake_case, SciPy's where SciPy has one: `ttest_ind`, + `mannwhitneyu`, `pvalue`, `logpdf`. Special's functions are the Go names in + snake_case (`Special.beta_inc`, `Special.gamma_p_inv`). +- **Keyword arguments are the fields of the Go option struct**: + `Stats.mean(x, weights: w, omit_nan: true)` fills `stats.MeanOptions`, + `Optimize.minimize(x0, max_iter: 400, gtol: 1e-8)` `optimize.Options`. An + enumerated field takes a symbol (`alternative: :less`, `method: :nelder_mead`, + or the Go string value, `method: "L-BFGS-B"`), a bound `[lo, hi]` with `nil` + for a missing side, a struct a Hash. An unknown keyword is an ArgumentError + that names the known ones. +- **Data** is an Array of numbers or an NDArray (an NDArray goes to the Go + function as it is: `stats.Data` is `[]float64 | *ndarray.Array`). Elementwise + methods (`pdf`, `cdf`, `ppf`, … and every real function of Special) take a + Float, an Array or an NDArray, and broadcast as NumPy does. +- **Results** are Ruby objects of a class named after the Go type + (`Stats::TTestResult`, `Optimize::Result`, `Integrate::Solution`), with a + reader per field, `to_h`, `inspect`, and the Go type's own methods + (`res.confidence_interval(0.95)`). A `[]float64` comes back as an NDArray. +- **Callbacks are blocks** (or Procs, for `jac:`, `fprime:`, and integration + limits): a vector reaches the block as an NDArray, and the block returns a + Float, or an Array or NDArray for a vector. An exception in the block stops + the Go algorithm at once and is raised again, unchanged, by the binding; a + `break` or a `throw` leaves it as in Ruby. A binding that would call the block + from several goroutines refuses `workers:` above 1. +- **Random draws** take `random_state:` — an Integer seed (NumPy's + `default_rng(seed)`, the same numbers) or a `Stats::RNG`. + +## What is bound + +Every module function (`Module.name`), class method and instance method, +grouped by module and class; `TestREADMEListsEveryBinding` fails when a +binding is missing here. The argument lists, and what each name calls in Go, +are in the doc comments of [stats.go](stats.go), [special.go](special.go), +[optimize.go](optimize.go), [integrate.go](integrate.go), +[dataframe.go](dataframe.go) and [plot.go](plot.go). Every distribution +class of `Stats` (`Beta` … `Zipf`) has the same methods as `Stats::Normal` +(`pmf`/`logpmf` in place of `pdf`/`logpdf` for the discrete ones), plus its +parameters as readers. + +#### DataFrame + +`*` `+` `-` `/` `[]` `add` `apply` `astype` `at` `bfill` `column` `column_at` `columns` `copy` `count` `describe` `display` `div` `drop` `dropna` `dtypes` `eval` `ffill` `fillna` `filter` `group_by` `groupby` `head` `iat` `iloc` `index` `inspect` `isna` `join` `length` `loc` `max` `mean` `median` `melt` `merge` `min` `mul` `notna` `pivot` `pivot_table` `query` `reindex` `reindex_columns` `rename` `resample` `reset_index` `round` `select` `series` `set_columns` `set_index` `shape` `shift` `size` `sort_index` `sort_values` `stack` `std` `sub` `sum` `t` `tail` `to_csv` `to_h` `to_html` `to_ndarray` `to_period` `to_s` `transpose` `unstack` `values` `var` `with_column` `with_index` `write_csv` `_from_pairs` `concat` `from_array` `from_series` `new` `read_csv` `read_csv_file` + +#### DataFrame::DatetimeFields + +`day` `day_of_week` `day_of_year` `dayofweek` `dayofyear` `hour` `minute` `month` `normalize` `second` `strftime` `year` + +#### DataFrame::GroupBy + +`[]` `_agg` `agg` `count` `first` `groups` `inspect` `last` `max` `mean` `median` `min` `n_groups` `ngroups` `nunique` `select` `size` `std` `sum` `to_s` `transform` `var` + +#### DataFrame::Index + +`[]` `display` `dtype` `get` `inspect` `label` `labels` `length` `level` `name` `names` `nlevels` `range?` `rename` `size` `to_a` `to_s` `values` `new` `range` + +#### DataFrame::Resampler + +`_agg` `agg` `count` `first` `last` `max` `mean` `median` `min` `size` `std` `sum` + +#### DataFrame::Rolling + +`max` `mean` `median` `min` `std` `sum` + +#### DataFrame::Series + +`%` `&` `*` `**` `+` `-` `-@` `/` `<` `<=` `>` `>=` `[]` `^` `abs` `add` `agg` `astype` `between` `bfill` `copy` `count` `cummax` `cummin` `cumprod` `cumsum` `diff` `display` `div` `dropna` `dt` `dtype` `eq` `ffill` `fillna` `filter` `floats` `floordiv` `ge` `get` `gt` `head` `iloc` `index` `inspect` `invert` `isin` `isna` `le` `length` `loc` `lt` `map` `max` `mean` `median` `min` `mod` `mul` `name` `ne` `neg` `notna` `nunique` `pct_change` `pow` `prod` `quantile` `reindex` `rename` `reset_index` `rolling` `round` `shift` `size` `sort_index` `sort_values` `std` `sub` `sum` `tail` `to_a` `to_datetime` `to_frame` `to_html` `to_ndarray` `to_period` `to_s` `to_timestamp` `unique` `unstack` `valid` `value_counts` `values` `var` `with_index` `|` `~` `new` + +#### Integrate + +`cumulative_trapezoid` `dblquad` `fixed_quad` `gauss_legendre` `nquad` `odeint` `quad` `romb` `romberg` `simpson` `solve_ivp` `tplquad` `trapezoid` + +#### Integrate::ODESolution + +`eval` `eval_array` `t_max` `t_min` + +#### Integrate::OdeintInfo + +`hu` `inspect` `m_used` `message` `nfe` `nje` `nst` `t_cur` `to_h` + +#### Integrate::QuadResult + +`abs_err` `cycle_ier` `inspect` `n_eval` `n_intervals` `to_h` `value` + +#### Integrate::Solution + +`component` `inspect` `message` `n_fev` `n_fev_jac` `n_jev` `n_steps` `nlu` `sol` `status` `success` `t` `t_array` `t_events` `to_h` `y` `y_array` `y_events` + +#### Optimize + +`approx_fprime` `bisect` `bracket` `brenth` `brentq` `brute` `curve_fit` `fminbound` `fsolve` `least_squares` `leastsq` `linprog` `minimize` `minimize_scalar` `newton` `ridder` `root` `root_scalar` + +#### Optimize::BruteResult + +`axes` `fun` `inspect` `jout` `success` `to_h` `x` + +#### Optimize::CurveFitResult + +`cov_ok` `fvec` `inspect` `message` `nfev` `pcov` `pcov_array` `perr` `popt` `popt_array` `status` `to_h` + +#### Optimize::LPDual + +`inspect` `marginals` `residual` `to_h` + +#### Optimize::LPResult + +`con` `eq_lin` `fun` `ineq_lin` `inspect` `lower` `message` `nit` `slack` `status` `success` `to_h` `upper` `x` `x_array` + +#### Optimize::LSResult + +`active_mask` `cost` `fun` `grad` `inspect` `jac` `jac_array` `message` `nfev` `njev` `optimality` `status` `success` `to_h` `x` `x_array` + +#### Optimize::LeastsqResult + +`cov_x` `cov_x_array` `fjac` `fjac_array` `fvec` `ier` `inspect` `ipvt` `mesg` `nfev` `njev` `qtf` `success` `to_h` `x` `x_array` + +#### Optimize::Result + +`fun` `hess_inv` `hess_inv_array` `inspect` `jac` `message` `nfev` `nhev` `nit` `njev` `status` `success` `to_h` `x` `x_array` + +#### Optimize::RootResult + +`fjac` `fjac_array` `fun` `inspect` `message` `nfev` `njev` `qtf` `r` `status` `success` `to_h` `x` `x_array` + +#### Optimize::RootScalarResult + +`converged` `flag` `function_calls` `inspect` `iterations` `method` `root` `to_h` + +#### Optimize::ScalarResult + +`fun` `inspect` `message` `nfev` `nit` `status` `success` `to_h` `x` + +#### Plot + +`figure` `subplots` `values` + +#### Plot::Axes + +`annotate` `aspect=` `axhline` `axvline` `bar` `barh` `contour` `contourf` `errorbar` `figure` `fill_between` `grid` `hist` `imshow` `legend` `plot` `scatter` `set_aspect` `set_title` `set_xlabel` `set_xlim` `set_xscale` `set_xticks` `set_ylabel` `set_ylim` `set_yscale` `set_yticks` `text` `title=` `twinx` `xlabel=` `xlim` `xlim=` `xscale=` `ylabel=` `ylim` `ylim=` `yscale=` + +#### Plot::Figure + +`add_axes` `axes` `colorbar` `display` `dpi` `height` `inspect` `pixel_size` `save_png` `save_svg` `savefig` `subplots` `tight_layout` `to_png` `to_s` `to_svg` `width` `new` + +#### Plot::Mappable + +`inspect` + +#### Special + +`beta` `beta_inc` `beta_inc_c` `beta_inc_c_inv` `beta_inc_inv` `binom` `comb` `digamma` `ellip_e` `ellip_k` `ellip_km1` `erf` `erf_inv` `erfc` `erfc_inv` `erfcx` `expit` `factorial` `factorial2` `gamma` `gamma_p` `gamma_p_inv` `gamma_q` `gamma_q_inv` `gamma_sign` `hurwitz_zeta` `i0` `i0e` `i1` `i1e` `in` `iv` `ive` `j0` `j1` `jn` `jn_zeros` `jv` `k0` `k0e` `k1` `k1e` `kn` `kv` `kve` `log1p_exp` `log_beta` `log_expit` `log_factorial` `log_gamma` `log_gamma_sign` `log_ndtr` `log_softmax` `log_sum_exp` `logit` `ndtr` `ndtri` `perm` `polygamma` `softmax` `xlog1py` `xlogy` `y0` `y1` `yn` `yv` `zeta` + +#### Stats + +`anderson` `average` `bartlett` `binned_statistic` `bootstrap` `chi2_contingency` `chisquare` `cumfreq` `describe` `differential_entropy` `entropy` `f_oneway` `fisher_exact` `fit_multivariate_normal` `gmean` `histogram` `histogram_bin_edges` `hmean` `iqr` `jarque_bera` `kendalltau` `kruskal` `ks_1samp` `ks_2samp` `kurtosis` `kurtosistest` `levene` `linregress` `mannwhitneyu` `mean` `median` `median_abs_deviation` `mode` `moment` `normaltest` `pearsonr` `percentile` `percentiles` `pmean` `power_divergence` `quantile` `quantiles` `rankdata` `relfreq` `sem` `shapiro` `skew` `skewtest` `spearmanr` `std` `tmax` `tmean` `tmin` `trim1` `trim_mean` `trimboth` `tsem` `tstd` `ttest_1samp` `ttest_ind` `ttest_rel` `tvar` `var` `variation` `wilcoxon` `zmap` `zscore` + +#### Stats::AndersonResult + +`critical_values` `inspect` `loc` `pvalue` `scale` `significance_levels` `statistic` `to_h` + +#### Stats::Bernoulli + +`cdf` `entropy` `inspect` `interval` `isf` `kurtosis` `loc` `logcdf` `logpmf` `logsf` `mean` `median` `moment` `p` `pmf` `ppf` `rvs` `sf` `skew` `std` `support` `to_h` `validate` `var` `new` + +#### Stats::Beta + +`a` `b` `cdf` `entropy` `inspect` `interval` `isf` `kurtosis` `loc` `logcdf` `logpdf` `logsf` `mean` `median` `moment` `pdf` `ppf` `rvs` `scale` `sf` `skew` `std` `support` `to_h` `validate` `var` `fit` `new` + +#### Stats::BinnedResult + +`bin_number` `edges` `inspect` `statistic` `to_h` + +#### Stats::Binomial + +`cdf` `entropy` `inspect` `interval` `isf` `kurtosis` `loc` `logcdf` `logpmf` `logsf` `mean` `median` `moment` `n` `p` `pmf` `ppf` `rvs` `sf` `skew` `std` `support` `to_h` `validate` `var` `new` + +#### Stats::BootstrapResult + +`bootstrap_distribution` `confidence_interval` `inspect` `standard_error` `to_h` + +#### Stats::CDFEstimate + +`error` `inspect` `n_points` `to_h` `value` + +#### Stats::Cauchy + +`cdf` `entropy` `inspect` `interval` `isf` `kurtosis` `loc` `logcdf` `logpdf` `logsf` `mean` `median` `moment` `pdf` `ppf` `rvs` `scale` `sf` `skew` `std` `support` `to_h` `validate` `var` `fit` `new` + +#### Stats::Chi + +`cdf` `df` `entropy` `inspect` `interval` `isf` `kurtosis` `loc` `logcdf` `logpdf` `logsf` `mean` `median` `moment` `pdf` `ppf` `rvs` `scale` `sf` `skew` `std` `support` `to_h` `validate` `var` `fit` `new` + +#### Stats::Chi2ContingencyResult + +`dof` `expected` `inspect` `pvalue` `statistic` `to_h` + +#### Stats::ChiSquared + +`cdf` `df` `entropy` `inspect` `interval` `isf` `kurtosis` `loc` `logcdf` `logpdf` `logsf` `mean` `median` `moment` `pdf` `ppf` `rvs` `scale` `sf` `skew` `std` `support` `to_h` `validate` `var` `fit` `new` + +#### Stats::ConfidenceInterval + +`high` `inspect` `low` `to_h` + +#### Stats::DescribeResult + +`inspect` `kurtosis` `max` `mean` `min` `nobs` `skewness` `to_h` `variance` + +#### Stats::Dirichlet + +`alpha` `cov` `entropy` `inspect` `log_pdf_points` `logpdf` `mean` `pdf` `pdf_points` `rvs` `var` `new` + +#### Stats::DiscreteUniform + +`cdf` `entropy` `high` `inspect` `interval` `isf` `kurtosis` `loc` `logcdf` `logpmf` `logsf` `low` `mean` `median` `moment` `pmf` `ppf` `rvs` `sf` `skew` `std` `support` `to_h` `validate` `var` `new` + +#### Stats::Exponential + +`cdf` `entropy` `inspect` `interval` `isf` `kurtosis` `loc` `logcdf` `logpdf` `logsf` `mean` `median` `moment` `pdf` `ppf` `rvs` `scale` `sf` `skew` `std` `support` `to_h` `validate` `var` `fit` `new` + +#### Stats::F + +`cdf` `dfd` `dfn` `entropy` `inspect` `interval` `isf` `kurtosis` `loc` `logcdf` `logpdf` `logsf` `mean` `median` `moment` `pdf` `ppf` `rvs` `scale` `sf` `skew` `std` `support` `to_h` `validate` `var` `fit` `new` + +#### Stats::FreqResult + +`bin_size` `extra_points` `freq` `inspect` `lower_limit` `to_h` + +#### Stats::Gamma + +`a` `cdf` `entropy` `inspect` `interval` `isf` `kurtosis` `loc` `logcdf` `logpdf` `logsf` `mean` `median` `moment` `pdf` `ppf` `rvs` `scale` `sf` `skew` `std` `support` `to_h` `validate` `var` `fit` `new` + +#### Stats::GaussianKDE + +`call` `cho_cov` `covariance` `d` `dataset` `evaluate` `factor` `inspect` `integrate_box` `integrate_box_1d` `integrate_gaussian` `integrate_kde` `inv_cov` `log_det` `log_pdf_slice` `logpdf` `marginal` `n` `neff` `resample` `scotts_factor` `set_bandwidth` `silverman_factor` `weights` `new` + +#### Stats::Geometric + +`cdf` `entropy` `inspect` `interval` `isf` `kurtosis` `loc` `logcdf` `logpmf` `logsf` `mean` `median` `moment` `p` `pmf` `ppf` `rvs` `sf` `skew` `std` `support` `to_h` `validate` `var` `new` + +#### Stats::Gumbel + +`cdf` `entropy` `inspect` `interval` `isf` `kurtosis` `loc` `logcdf` `logpdf` `logsf` `mean` `median` `moment` `pdf` `ppf` `rvs` `scale` `sf` `skew` `std` `support` `to_h` `validate` `var` `fit` `new` + +#### Stats::HalfNormal + +`cdf` `entropy` `inspect` `interval` `isf` `kurtosis` `loc` `logcdf` `logpdf` `logsf` `mean` `median` `moment` `pdf` `ppf` `rvs` `scale` `sf` `skew` `std` `support` `to_h` `validate` `var` `fit` `new` + +#### Stats::HistogramResult + +`counts` `edges` `inspect` `to_h` + +#### Stats::Hypergeometric + +`cdf` `draws` `entropy` `good` `inspect` `interval` `isf` `kurtosis` `loc` `logcdf` `logpmf` `logsf` `mean` `median` `moment` `pmf` `ppf` `rvs` `sf` `skew` `std` `support` `to_h` `total` `validate` `var` `new` + +#### Stats::InverseGamma + +`a` `cdf` `entropy` `inspect` `interval` `isf` `kurtosis` `loc` `logcdf` `logpdf` `logsf` `mean` `median` `moment` `pdf` `ppf` `rvs` `scale` `sf` `skew` `std` `support` `to_h` `validate` `var` `fit` `new` + +#### Stats::KSResult + +`inspect` `pvalue` `statistic` `statistic_location` `statistic_sign` `to_h` + +#### Stats::Laplace + +`cdf` `entropy` `inspect` `interval` `isf` `kurtosis` `loc` `logcdf` `logpdf` `logsf` `mean` `median` `moment` `pdf` `ppf` `rvs` `scale` `sf` `skew` `std` `support` `to_h` `validate` `var` `fit` `new` + +#### Stats::LinregressResult + +`inspect` `intercept` `intercept_std_err` `pvalue` `r_value` `slope` `std_err` `to_h` + +#### Stats::LogNormal + +`cdf` `entropy` `inspect` `interval` `isf` `kurtosis` `loc` `logcdf` `logpdf` `logsf` `mean` `median` `moment` `pdf` `ppf` `rvs` `s` `scale` `sf` `skew` `std` `support` `to_h` `validate` `var` `fit` `new` + +#### Stats::Logistic + +`cdf` `entropy` `inspect` `interval` `isf` `kurtosis` `loc` `logcdf` `logpdf` `logsf` `mean` `median` `moment` `pdf` `ppf` `rvs` `scale` `sf` `skew` `std` `support` `to_h` `validate` `var` `fit` `new` + +#### Stats::Lomax + +`c` `cdf` `entropy` `inspect` `interval` `isf` `kurtosis` `loc` `logcdf` `logpdf` `logsf` `mean` `median` `moment` `pdf` `ppf` `rvs` `scale` `sf` `skew` `std` `support` `to_h` `validate` `var` `fit` `new` + +#### Stats::ModeResult + +`count` `inspect` `mode` `to_h` + +#### Stats::Multinomial + +`cov` `entropy` `inspect` `log_pmf_points` `logpmf` `mean` `n` `p` `pmf` `pmf_points` `rvs` `new` + +#### Stats::MultivariateNormal + +`cdf` `cdf_estimate` `cdf_points` `cov` `dim` `entropy` `inspect` `log_p_det` `log_pdf_points` `logcdf` `logpdf` `mean` `pdf` `pdf_points` `rank` `rvs` `new` + +#### Stats::NegativeBinomial + +`cdf` `entropy` `inspect` `interval` `isf` `kurtosis` `loc` `logcdf` `logpmf` `logsf` `mean` `median` `moment` `n` `p` `pmf` `ppf` `rvs` `sf` `skew` `std` `support` `to_h` `validate` `var` `new` + +#### Stats::NoncentralChiSquared + +`cdf` `df` `entropy` `inspect` `interval` `isf` `kurtosis` `loc` `logcdf` `logpdf` `logsf` `mean` `median` `moment` `nc` `pdf` `ppf` `rvs` `scale` `sf` `skew` `std` `support` `to_h` `validate` `var` `new` + +#### Stats::Normal + +`cdf` `entropy` `inspect` `interval` `isf` `kurtosis` `loc` `logcdf` `logpdf` `logsf` `mean` `median` `moment` `pdf` `ppf` `rvs` `scale` `sf` `skew` `std` `support` `to_h` `validate` `var` `fit` `new` + +#### Stats::Pareto + +`b` `cdf` `entropy` `inspect` `interval` `isf` `kurtosis` `loc` `logcdf` `logpdf` `logsf` `mean` `median` `moment` `pdf` `ppf` `rvs` `scale` `sf` `skew` `std` `support` `to_h` `validate` `var` `fit` `new` + +#### Stats::PearsonResult + +`confidence_interval` `inspect` `pvalue` `statistic` `to_h` + +#### Stats::Poisson + +`cdf` `entropy` `inspect` `interval` `isf` `kurtosis` `loc` `logcdf` `logpmf` `logsf` `mean` `median` `moment` `mu` `pmf` `ppf` `rvs` `sf` `skew` `std` `support` `to_h` `validate` `var` `new` + +#### Stats::RNG + +`beta` `binomial` `exponential` `gamma` `inspect` `integers` `normal` `poisson` `random` `standard_exponential` `standard_normal` `uniform` `new` + +#### Stats::RankTestResult + +`inspect` `pvalue` `statistic` `to_h` `zstatistic` + +#### Stats::Rayleigh + +`cdf` `entropy` `inspect` `interval` `isf` `kurtosis` `loc` `logcdf` `logpdf` `logsf` `mean` `median` `moment` `pdf` `ppf` `rvs` `scale` `sf` `skew` `std` `support` `to_h` `validate` `var` `fit` `new` + +#### Stats::Reciprocal + +`a` `b` `cdf` `entropy` `inspect` `interval` `isf` `kurtosis` `loc` `logcdf` `logpdf` `logsf` `mean` `median` `moment` `pdf` `ppf` `rvs` `scale` `sf` `skew` `std` `support` `to_h` `validate` `var` `fit` `new` + +#### Stats::StudentT + +`cdf` `df` `entropy` `inspect` `interval` `isf` `kurtosis` `loc` `logcdf` `logpdf` `logsf` `mean` `median` `moment` `pdf` `ppf` `rvs` `scale` `sf` `skew` `std` `support` `to_h` `validate` `var` `fit` `new` + +#### Stats::TTestResult + +`confidence_interval` `df` `inspect` `pvalue` `statistic` `to_h` + +#### Stats::TestResult + +`inspect` `pvalue` `statistic` `to_h` + +#### Stats::Triangular + +`c` `cdf` `entropy` `inspect` `interval` `isf` `kurtosis` `loc` `logcdf` `logpdf` `logsf` `mean` `median` `moment` `pdf` `ppf` `rvs` `scale` `sf` `skew` `std` `support` `to_h` `validate` `var` `fit` `new` + +#### Stats::Uniform + +`cdf` `entropy` `inspect` `interval` `isf` `kurtosis` `loc` `logcdf` `logpdf` `logsf` `mean` `median` `moment` `pdf` `ppf` `rvs` `scale` `sf` `skew` `std` `support` `to_h` `validate` `var` `fit` `new` + +#### Stats::VonMises + +`cdf` `entropy` `inspect` `interval` `isf` `kappa` `kurtosis` `loc` `logcdf` `logpdf` `logsf` `mean` `median` `moment` `pdf` `ppf` `rvs` `scale` `sf` `skew` `std` `support` `to_h` `validate` `var` `fit` `new` + +#### Stats::Weibull + +`c` `cdf` `entropy` `inspect` `interval` `isf` `kurtosis` `loc` `logcdf` `logpdf` `logsf` `mean` `median` `moment` `pdf` `ppf` `rvs` `scale` `sf` `skew` `std` `support` `to_h` `validate` `var` `fit` `new` + +#### Stats::Zipf + +`a` `cdf` `entropy` `inspect` `interval` `isf` `kurtosis` `loc` `logcdf` `logpmf` `logsf` `mean` `median` `moment` `pmf` `ppf` `rvs` `sf` `skew` `std` `support` `to_h` `validate` `var` `new` + + +--- PASS: TestPrintBindings (0.02s) +PASS +ok github.com/go-scikit/ruby 1.227s +## Build tags + +rbgo builds with or without its gem bindings; `-tags rbgo_core` leaves the +gems out (the language and the numeric stack only), which is what a notebook +worker or a browser page wants. These bindings work, and are tested, on both. +rbgo compiles for linux, darwin and windows on 64-bit targets and for wasm +(js and wasip1), and so does this module. + +## License + +BSD-3-Clause. See [LICENSE](LICENSE). diff --git a/examples/integrate.rb b/examples/integrate.rb new file mode 100644 index 0000000..01eab2e --- /dev/null +++ b/examples/integrate.rb @@ -0,0 +1,10 @@ +# Integrate: the integrand, or the right-hand side, is the block. +q = Integrate.quad(0, Float::INFINITY) { |x| Math.exp(-x * x) } +puts "∫ exp(-x²) over [0, ∞) = #{q.value} (√π/2 = #{Math.sqrt(Math::PI) / 2})" + +puts Integrate.dblquad(0, 1, 0, ->(x) { x }) { |y, x| x * y }.value + +# A damped oscillator: y'' + 0.2 y' + y = 0. +sol = Integrate.solve_ivp([0, 10], [1.0, 0.0], t_eval: NDArray.linspace(0, 10, 6).to_a) { |t, y| [y[1], -0.2 * y[1] - y[0]] } +p sol.y_array +p Integrate.trapezoid(NDArray.linspace(0, 1, 11) ** 2, dx: 0.1) diff --git a/examples/optimize.rb b/examples/optimize.rb new file mode 100644 index 0000000..944874d --- /dev/null +++ b/examples/optimize.rb @@ -0,0 +1,16 @@ +# Optimize: the objective is the block. +rosen = ->(x) { (1 - x[0])**2 + 100 * (x[1] - x[0]**2)**2 } + +res = Optimize.minimize([-1.2, 1.0], method: :bfgs) { |x| rosen.(x) } +puts "BFGS: x = #{res.x.inspect}, f = #{res.fun}, #{res.nit} iterations" + +res = Optimize.minimize([3.0, 3.0], method: :lbfgsb, bounds: [[2, nil], [nil, 2.5]]) { |x| rosen.(x) } +puts "bounded: #{res.x.inspect}" + +puts "root of x^3 - 2x - 5: #{Optimize.brentq(2, 3) { |x| x**3 - 2 * x - 5 }.root}" + +fit = Optimize.curve_fit([0, 1, 2, 3, 4], [1.1, 2.6, 7.6, 20.0, 54.9], p0: [1, 1]) { |x, a, b| a * Math.exp(b * x) } +puts "curve_fit: a, b = #{fit.popt.inspect}" + +lp = Optimize.linprog([-1, -2], a_ub: NDArray.from([1, 1, 1, 3], [2, 2]), b_ub: [4, 6], bounds: [[0, nil], [0, nil]]) +puts "linprog: #{lp.x.inspect}, #{lp.message}" diff --git a/examples/special.rb b/examples/special.rb new file mode 100644 index 0000000..58efa89 --- /dev/null +++ b/examples/special.rb @@ -0,0 +1,8 @@ +# Special: every function of go-numerics/special, elementwise like a ufunc. +p Special.gamma(5) # 24.0 +p Special.gamma(NDArray.arange(1, 6)) # 1, 1, 2, 6, 24 +p Special.erf([0.0, 0.5, 1.0]) +p Special.beta_inc(2, 3, NDArray.linspace(0, 1, 5)) +# Broadcasting: orders down the rows, points along the columns. +p Special.jv(NDArray.arange(3).reshape(3, 1), NDArray.linspace(0, 2, 3)) +p Special.jn_zeros(0, 3) diff --git a/examples/stats.rb b/examples/stats.rb new file mode 100644 index 0000000..8292aa4 --- /dev/null +++ b/examples/stats.rb @@ -0,0 +1,19 @@ +# Stats: distributions as classes, descriptive statistics, tests, fits. +rng = Stats::RNG.new(42) +x = Stats::Normal.new(loc: 10, scale: 2).rvs(200, random_state: rng) +y = Stats::Normal.new(loc: 10.6, scale: 2).rvs(200, random_state: rng) + +puts "mean #{Stats.mean(x).round(3)}, std #{Stats.std(x, ddof: 1).round(3)}" +p Stats.describe(x) + +t = Stats.ttest_ind(x, y, welch: true) +puts "Welch t = #{t.statistic.round(3)}, p = #{t.pvalue.round(4)}" +p t.confidence_interval(0.95) + +fit = Stats::Normal.fit(x) +puts "fitted loc #{fit.loc.round(3)}, scale #{fit.scale.round(3)}" + +# Elementwise over an NDArray, as SciPy. +grid = NDArray.linspace(4, 16, 5) +p Stats::Normal.new(loc: 10, scale: 2).pdf(grid) +p Stats.ks_1samp(x) { |v| fit.cdf(v) }.pvalue > 0.05 diff --git a/examples_df_plot_test.go b/examples_test.go similarity index 67% rename from examples_df_plot_test.go rename to examples_test.go index 09a84b6..3062cb0 100644 --- a/examples_df_plot_test.go +++ b/examples_test.go @@ -9,10 +9,13 @@ import ( rb "github.com/go-embedded-ruby/ruby" ) -// The examples of DataFrame and Plot run without raising. -func TestExamplesDataFramePlot(t *testing.T) { - for _, name := range []string{"dataframe.rb", "plot.rb"} { - path := filepath.Join("examples", name) +// Every example runs without raising, and prints something. +func TestExamples(t *testing.T) { + paths, _ := filepath.Glob(filepath.Join("examples", "*.rb")) + if len(paths) < 6 { + t.Fatalf("%d examples, want one per module", len(paths)) + } + for _, path := range paths { src, err := os.ReadFile(path) if err != nil { t.Fatal(err) diff --git a/fuzz_test.go b/fuzz_test.go new file mode 100644 index 0000000..03aeade --- /dev/null +++ b/fuzz_test.go @@ -0,0 +1,47 @@ +package ruby + +import ( + "bytes" + "testing" + + rb "github.com/go-embedded-ruby/ruby" +) + +// Whatever values a binding is handed, it answers with a result or a Ruby +// exception: the host process never panics. The values are built in Go, not +// parsed from the fuzzed bytes, so no input can make the interpreter loop. +func FuzzBindingArguments(f *testing.F) { + f.Add(1.5, int64(3), "loc", true, uint8(0)) + f.Add(-0.0, int64(-1), "two_sided", false, uint8(7)) + f.Add(1e308, int64(1)<<62, "", true, uint8(200)) + var out bytes.Buffer + in := rb.New(rb.WithStdout(&out), rb.WithStderr(&out)) + if err := Register(in); err != nil { + f.Fatal(err) + } + stats, _ := in.Eval("Stats", "f.rb", 1) + special, _ := in.Eval("Special", "f.rb", 1) + normal, _ := in.Eval("Stats::Normal", "f.rb", 1) + f.Fuzz(func(t *testing.T, x float64, n int64, s string, b bool, pick uint8) { + vals := []any{x, n, s, b, nil, []any{x, n}, []any{x, x, x}, map[string]any{s: x}, rb.Symbol(s)} + a := vals[int(pick)%len(vals)] + c := vals[int(pick/9)%len(vals)] + for _, call := range []struct { + recv rb.Value + name string + args []any + }{ + {stats, "mean", []any{a}}, + {stats, "quantile", []any{a, c}}, + {stats, "ttest_1samp", []any{a, c}}, + {stats, "describe", []any{a}}, + {special, "gamma", []any{a}}, + {special, "jv", []any{a, c}}, + {special, "comb", []any{a, c}}, + {normal, "new", []any{a, c}}, + {normal, "fit", []any{a}}, + } { + _, _ = call.recv.Call(call.name, call.args...) + } + }) +} diff --git a/readme_test.go b/readme_test.go new file mode 100644 index 0000000..863a1bc --- /dev/null +++ b/readme_test.go @@ -0,0 +1,101 @@ +package ruby + +import ( + "os" + "sort" + "strings" + "testing" +) + +// bindings lists every Ruby name the bindings define: "Module.function", +// "Class.class_method" and "Class#method", for the modules and classes +// Register makes. +func bindings(t *testing.T) []string { + in := newInterp(t) + src := ` +out = [] +walk = lambda do |mod| + mod.singleton_methods(false).each { |m| out << "#{mod}.#{m}" } + mod.instance_methods(false).each { |m| out << "#{mod}##{m}" } if mod.is_a?(Class) + mod.constants(false).each do |c| + v = mod.const_get(c) + walk.(v) if v.is_a?(Module) && v.name.start_with?("#{mod}::") + end +end +[Stats, Special, Optimize, Integrate, DataFrame, Plot].each { |m| walk.(m) } +out` + v, err := in.Eval(src, "names.rb", 1) + if err != nil { + t.Fatal(err) + } + var names []string + for _, n := range v.Go().([]any) { + names = append(names, n.(string)) + } + sort.Strings(names) + return names +} + +// The README lists what is bound, every name of it: a binding added without +// its line in the README fails here. +func TestREADMEListsEveryBinding(t *testing.T) { + readme, err := os.ReadFile("README.md") + if err != nil { + t.Fatal(err) + } + text := string(readme) + missing := 0 + for _, n := range bindings(t) { + // The README groups names by class: "Stats::Normal#pdf" is listed as + // `pdf` in the Stats::Normal entry, so look for the bare name in it. + owner, name := n, n + if i := strings.LastIndexAny(n, ".#"); i >= 0 { + owner, name = n[:i], n[i+1:] + } + section := sectionOf(text, owner) + if !strings.Contains(section, "`"+name+"`") { + if missing < 40 { + t.Errorf("README does not list %s", n) + } + missing++ + } + } + if missing > 0 { + t.Errorf("%d names missing in all", missing) + } +} + +// sectionOf is the README's entry for a module or class: the lines from its +// "#### Owner" heading to the next heading. +func sectionOf(text, owner string) string { + h := "#### " + owner + "\n" + i := strings.Index(text, h) + if i < 0 { + return "" + } + rest := text[i+len(h):] + if j := strings.Index(rest, "\n#"); j >= 0 { + rest = rest[:j] + } + return rest +} + +// TestPrintBindings prints the names grouped as the README lists them; run +// it with -v -run TestPrintBindings when the bindings change. +func TestPrintBindings(t *testing.T) { + groups := map[string][]string{} + var owners []string + for _, n := range bindings(t) { + i := strings.LastIndexAny(n, ".#") + owner := n[:i] + if _, ok := groups[owner]; !ok { + owners = append(owners, owner) + } + groups[owner] = append(groups[owner], "`"+n[i+1:]+"`") + } + var b strings.Builder + for _, o := range owners { + b.WriteString("#### " + o + "\n\n" + strings.Join(groups[o], " ") + "\n\n") + } + t.Log("\n" + b.String()) +} diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..123150f --- /dev/null +++ b/renovate.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["github>go-scikit/.github"] +} diff --git a/smoke_test.go b/smoke_test.go deleted file mode 100644 index e7d8b39..0000000 --- a/smoke_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package ruby - -import ( - "bytes" - "testing" - - rb "github.com/go-embedded-ruby/ruby" -) - -func TestSmoke(t *testing.T) { - var out bytes.Buffer - in := rb.New(rb.WithStdout(&out), rb.WithStderr(&out)) - if err := Register(in); err != nil { - t.Fatal(err) - } - for _, src := range []string{ - `Stats::Normal.new(loc: 0, scale: 1).pdf(0.5)`, - `Stats::Normal.new.pdf(NDArray.linspace(-1, 1, 3))`, - `Stats::Normal.new(1, 2)`, - `Stats::Normal.new(1, 2).interval(0.95)`, - `Stats.mean([1, 2, 3, 4])`, - `Stats.mean([1, 2, 3, 4], weights: [1, 1, 1, 5])`, - `Stats.ttest_ind([1, 2, 3, 4], [2, 3, 4, 5.5], welch: true)`, - `Stats.ttest_ind([1, 2, 3, 4], [2, 3, 4, 5.5], alternative: :less).pvalue`, - `Stats.f_oneway([1, 2, 3], [2, 3, 4], [5, 6, 7])`, - `Stats::Normal.fit([1, 2, 3, 4.5])`, - `Stats::Gamma.fit([1, 2, 3, 4.5], fixed: {loc: 0})`, - `Stats::Normal.new.rvs(3, random_state: 42)`, - `Stats::Binomial.new(n: 10, p: 0.3).pmf([0, 1, 2])`, - `Stats.ks_1samp([0.1, -0.3, 0.5]) { |x| Stats::Normal.new.cdf(x) }`, - `Stats.describe(NDArray.arange(10))`, - `Stats::RNG.new(42).standard_normal(2, 2)`, - `Stats::GaussianKDE.new([1, 2, 3, 4, 7]).evaluate([2, 3])`, - `Stats.bootstrap([[1, 2, 3, 4, 5, 6]], random_state: 1, n_resamples: 99) { |s| s.mean }`, - `Stats::MultivariateNormal.new([0, 0], NDArray.eye(2)).pdf([0, 0])`, - `Stats.pearsonr([1, 2, 3, 4], [1, 3, 2, 4]).confidence_interval(0.9)`, - `Stats::Normal.new.to_h`, - `Special.gamma(5)`, - `Special.gamma(NDArray.arange(1, 5))`, - `Special.beta_inc(2, 3, [0.1, 0.5])`, - `Special.jv(NDArray.arange(3).reshape(3, 1), NDArray.linspace(0, 1, 2))`, - `Special.jn_zeros(0, 3)`, - `Special.log_gamma_sign(-0.5)`, - `Special.softmax([1, 2, 3])`, - `Special.comb(5, 2)`, - `Special.in(1, 0.5)`, - `Optimize.minimize([1.0, 1.0]) { |x| (x[0] - 3)**2 + (x[1] + 1)**2 }`, - `Optimize.minimize([1.0, 1.0], method: :nelder_mead) { |x| (x[0] - 3)**2 + (x[1] + 1)**2 }.x`, - `Optimize.minimize([1.0], method: :lbfgsb, bounds: [[2, nil]]) { |x| x[0]**2 }.x`, - `Optimize.brentq(0, 2) { |x| x * x - 2 }.root`, - `Optimize.newton(1.0, fprime: ->(x) { 2 * x }) { |x| x * x - 2 }.root`, - `Optimize.root_scalar(method: "brentq", bracket: [0, 2]) { |x| x * x - 2 }.root`, - `Optimize.root([1.0, 1.0]) { |x| [x[0] + x[1] - 3, x[0] - x[1] - 1] }.x`, - `Optimize.curve_fit([0, 1, 2, 3], [1, 2.7, 7.4, 20.1], p0: [1, 1]) { |x, a, b| a * Math.exp(b * x) }.popt`, - `Optimize.linprog([-1, -2], a_ub: NDArray.from([1, 1], [1, 2]), b_ub: [4], bounds: [[0, nil], [0, 3]]).x`, - `Optimize.minimize_scalar { |x| (x - 2)**2 }.x`, - `Optimize.least_squares([1.0, 1.0]) { |x| [x[0] - 1, x[1] - 2, x[0] + x[1] - 3] }.x`, - `Integrate.quad(0, 1) { |x| x * x }`, - `Integrate.quad(0, Float::INFINITY) { |x| Math.exp(-x) }.value`, - `Integrate.dblquad(0, 1, 0, ->(x) { x }) { |y, x| x * y }.value`, - `Integrate.solve_ivp([0, 1], [1.0], t_eval: [0, 0.5, 1]) { |t, y| [-y[0]] }.y`, - `Integrate.odeint([1.0], [0, 0.5, 1]) { |t, y| [-y[0]] }`, - `Integrate.trapezoid([1, 2, 3])`, - `Integrate.simpson(NDArray.arange(6).reshape(2, 3), axis: 1)`, - `Integrate.gauss_legendre(3)`, - } { - v, err := in.Eval(src, "smoke.rb", 1) - if err != nil { - t.Errorf("%s: %v", src, err) - continue - } - t.Logf("%s => %s", src, v.Inspect()) - } -} From 7ec66fa0582a14bc483c343336f4e93672ff8259 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Sat, 17 Oct 2026 23:11:38 +0200 Subject: [PATCH 14/20] cells: run a notebook's Ruby cells, and exchange variables with its Go cells --- README.md | 8 ++ cells/cells.go | 326 ++++++++++++++++++++++++++++++++++++++++++++ cells/cells_test.go | 234 +++++++++++++++++++++++++++++++ 3 files changed, 568 insertions(+) create mode 100644 cells/cells.go create mode 100644 cells/cells_test.go diff --git a/README.md b/README.md index cc2fca8..2213a63 100644 --- a/README.md +++ b/README.md @@ -421,6 +421,14 @@ parameters as readers. --- PASS: TestPrintBindings (0.02s) PASS ok github.com/go-scikit/ruby 1.227s +## Notebooks + +Package [cells](cells/) runs the Ruby cells of a +[go-notebook](https://github.com/go-notebook/notebook) notebook: one +interpreter per worker, whose top-level locals persist from cell to cell, and +the exchange of variables with the Go cells. Its documentation states the +conversion rules both ways; a notebook links it, you do not call it. + ## Build tags rbgo builds with or without its gem bindings; `-tags rbgo_core` leaves the diff --git a/cells/cells.go b/cells/cells.go new file mode 100644 index 0000000..e83970c --- /dev/null +++ b/cells/cells.go @@ -0,0 +1,326 @@ +// Package cells runs the Ruby cells of a notebook: one Ruby interpreter per +// session, with the go-scikit modules registered, whose top-level locals +// persist from cell to cell, and the exchange of variables with the Go cells +// of the same notebook. +// +// It is what a go-notebook worker links when a notebook has Ruby cells; the +// generated program calls Run once per Ruby cell. It knows nothing of the +// notebook runtime: the cell's display and notes reach it through funcs. +// +// # Variables across languages +// +// A Go variable a Ruby cell reads is set as a Ruby local of the same name +// before the cell runs, converted as rbgo converts Go values (see rb.Value): a +// []float64 becomes an Array of Floats, a map a Hash, an *ndarray.Array the +// NDArray itself, any other value an opaque object that comes back as itself. +// A Go variable whose name starts with an upper-case letter becomes a Ruby +// constant. +// +// A Ruby local a cell assigns is handed back to Go after the cell runs, into +// a Go variable of the type its value has (GoValue): nil, true/false, Integer +// (int64, or *big.Int when large), Float (float64), String (string), Symbol +// (ruby.Symbol), Complex (complex128), Rational (*big.Rat), NDArray +// (*ndarray.Array, the same array), an Array whose elements are all Floats, +// all Integers, all Strings or all booleans ([]float64, []int64, []string, +// []bool; otherwise []any), a Hash with String keys (map[string]any; with +// Symbol keys map[ruby.Symbol]any; otherwise map[any]any), an object of a +// go-scikit class (the Go value it wraps: *plot.Figure, stats.TTestResult, …), +// and any other Ruby object as a ruby.Value. +package cells + +import ( + "fmt" + "math/big" + "os" + "reflect" + "strings" + "unicode" + + rb "github.com/go-embedded-ruby/ruby" + nd "github.com/go-ndarray/ndarray" + + scikit "github.com/go-scikit/ruby" +) + +// Session is the Ruby side of one notebook worker. +type Session struct { + in *rb.Interp + obj rb.Value // Object, for constants +} + +// stdout and stderr write to the process's streams as they are when written, +// so a worker that redirects them per cell captures what Ruby prints. +type stdout struct{} + +func (stdout) Write(p []byte) (int, error) { return os.Stdout.Write(p) } + +type stderr struct{} + +func (stderr) Write(p []byte) (int, error) { return os.Stderr.Write(p) } + +// New starts an interpreter with the go-scikit modules. +func New() *Session { + in := rb.New(rb.WithStdout(stdout{}), rb.WithStderr(stderr{})) + _ = scikit.Register(in) // a fresh interpreter has none of their names + obj, _ := in.Eval("Object", "(cells)", 1) + return &Session{in: in, obj: obj} +} + +// Var is a notebook variable at the boundary: its name and a pointer to the +// Go variable that holds it. +type Var struct { + Name string + Ptr any + // IfAbsent sets the Ruby local only when the interpreter does not hold + // it already: a Ruby cell's variable, which the interpreter keeps between + // cells but which a new worker gets back from its Go holder. + IfAbsent bool +} + +// Cell is one Ruby cell to run. +type Cell struct { + Source string + File string // the notebook + Line int // where Source starts in it + // Display shows the value of the cell's final expression. + Display bool + // In are the variables the cell reads, Out those it assigns. + In, Out []Var +} + +// Error is a Ruby cell's failure: the exception, at its line in the +// notebook, with the backtrace frames in the notebook. +type Error struct{ msg string } + +func (e *Error) Error() string { return e.msg } + +// CellError is the failure as a notebook shows it, without a Go stack. +func (e *Error) CellError() string { return e.msg } + +// Run runs a Ruby cell. show displays the value of its final expression; +// note(name, type) records the Go type each variable it assigns now has, in +// GoType's syntax, so the notebook can declare it. It panics with an *Error +// when the cell raises, which is how a worker fails a cell. +func (s *Session) Run(c Cell, show func(...any), note func(name, typ string)) { + for _, v := range c.In { + if err := s.set(v); err != nil { + panic(&Error{fmt.Sprintf("%s:%d: %v", c.File, c.Line, err)}) + } + } + val, err := s.in.Eval(c.Source, c.File, c.Line) + if err != nil { + panic(&Error{describe(err, c.File)}) + } + for _, v := range c.Out { + got, _ := s.in.Lookup(v.Name) // nil when the assignment did not run + x := GoValue(got) + note(v.Name, GoType(x)) + if err := assign(v.Ptr, x); err != nil { + panic(&Error{fmt.Sprintf("%s:%d: %s: %v", c.File, c.Line, v.Name, err)}) + } + } + if c.Display && !val.IsNil() { + if d, ok := val.Go().(interface{ Display() map[string][]byte }); ok { + if _, isArray := d.(*nd.Array); !isArray { + show(d) + return + } + } + show(Inspect(val.Inspect())) + } +} + +// describe renders a Ruby exception: file:line: message (Class), then the +// backtrace frames that are in the notebook. +func describe(err error, file string) string { + e, ok := err.(*rb.Error) + if !ok { + return err.Error() + } + var b strings.Builder + b.WriteString(e.Error()) + for _, f := range e.Backtrace { + if strings.HasPrefix(f, file+":") { + b.WriteString("\n\tfrom " + f) + } + } + return b.String() +} + +// set hands a Go variable to Ruby. +func (s *Session) set(v Var) error { + x := reflect.ValueOf(v.Ptr).Elem().Interface() + if r := []rune(v.Name); len(r) > 0 && unicode.IsUpper(r[0]) { + if def, _ := s.obj.Call("const_defined?", rb.Symbol(v.Name), false); def.Go() == true { + if _, err := s.obj.Call("send", rb.Symbol("remove_const"), rb.Symbol(v.Name)); err != nil { + return err + } + } + _, err := s.obj.Call("const_set", rb.Symbol(v.Name), x) + return err + } + if v.IfAbsent { + if _, err := s.in.Lookup(v.Name); err == nil { + return nil + } + } + return s.in.Set(v.Name, x) +} + +// Inspect is a Ruby value's inspect as a notebook shows it: plain text. +type Inspect string + +// Display shows the text. +func (i Inspect) Display() map[string][]byte { + return map[string][]byte{"text/plain": []byte(i)} +} + +// GoValue is a Ruby value as a Go cell gets it; see the package +// documentation for the table. +func GoValue(v rb.Value) any { + return narrow(v.Go()) +} + +func narrow(x any) any { + switch v := x.(type) { + case []any: + return narrowSlice(v) + case map[any]any: + return narrowMap(v) + } + return x +} + +func narrowSlice(v []any) any { + if len(v) == 0 { + return v + } + switch v[0].(type) { + case float64: + return collect[float64](v) + case int64: + return collect[int64](v) + case string: + return collect[string](v) + case bool: + return collect[bool](v) + } + return v +} + +// collect is v as a []T when every element is a T, else v. +func collect[T any](v []any) any { + out := make([]T, len(v)) + for i, e := range v { + t, ok := e.(T) + if !ok { + return v + } + out[i] = t + } + return out +} + +func narrowMap(m map[any]any) any { + strs := make(map[string]any, len(m)) + syms := make(map[rb.Symbol]any, len(m)) + for k, e := range m { + switch k := k.(type) { + case string: + strs[k] = e + case rb.Symbol: + syms[k] = e + default: + return m + } + } + switch { + case len(strs) == len(m): + return strs + case len(syms) == len(m): + return syms + } + return m +} + +// GoType names the Go type of a converted value for a generated program: +// built-in types as Go writes them, and a named type of another package as +// {import/path}.Name, which the generator turns into an import and a +// qualified name. A type it cannot name — nil, an unexported or generic type, +// a func — is any. +func GoType(x any) string { + if x == nil { + return "any" + } + if s := typeName(reflect.TypeOf(x)); s != "" { + return s + } + return "any" +} + +func typeName(t reflect.Type) string { + if t == bigIntType || t == bigRatType { + return "*{math/big}." + t.Elem().Name() + } + if t.Name() != "" { + if t.PkgPath() == "" { + return t.Name() // a predeclared type + } + if !token(t.Name()) { + return "" // unexported, or a generic instance + } + return "{" + t.PkgPath() + "}." + t.Name() + } + switch t.Kind() { + case reflect.Pointer: + if e := typeName(t.Elem()); e != "" { + return "*" + e + } + case reflect.Slice: + if e := typeName(t.Elem()); e != "" { + return "[]" + e + } + case reflect.Map: + k, e := typeName(t.Key()), typeName(t.Elem()) + if k != "" && e != "" { + return "map[" + k + "]" + e + } + case reflect.Interface: + if t.NumMethod() == 0 { + return "any" + } + } + return "" +} + +var ( + bigIntType = reflect.TypeFor[*big.Int]() + bigRatType = reflect.TypeFor[*big.Rat]() +) + +// token reports whether a type name is an exported identifier (a generic +// instance's name holds brackets). +func token(name string) bool { + for i, r := range name { + if i == 0 && !unicode.IsUpper(r) || !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '_' { + return false + } + } + return true +} + +// assign stores a converted value in the Go variable ptr points to: as it is +// into an any, else when its type is the variable's. +func assign(ptr any, x any) error { + dst := reflect.ValueOf(ptr).Elem() + if x == nil { + dst.SetZero() + return nil + } + v := reflect.ValueOf(x) + if !v.Type().AssignableTo(dst.Type()) { + return fmt.Errorf("is now a %s in Go, and Go cells read it as a %s", GoType(x), dst.Type()) + } + dst.Set(v) + return nil +} diff --git a/cells/cells_test.go b/cells/cells_test.go new file mode 100644 index 0000000..6de6cf2 --- /dev/null +++ b/cells/cells_test.go @@ -0,0 +1,234 @@ +package cells + +import ( + "errors" + "math/big" + "os" + "reflect" + "strings" + "testing" + + rb "github.com/go-embedded-ruby/ruby" + nd "github.com/go-ndarray/ndarray" + "github.com/go-plot/plot" + "github.com/go-statistics/stats" +) + +type shown struct{ vals []any } + +func (s *shown) show(vs ...any) { s.vals = append(s.vals, vs...) } + +func run(t *testing.T, s *Session, c Cell) (*shown, map[string]string) { + t.Helper() + sh := &shown{} + notes := map[string]string{} + if c.File == "" { + c.File, c.Line = "nb.md", 3 + } + s.Run(c, sh.show, func(n, typ string) { notes[n] = typ }) + return sh, notes +} + +func TestRunExchangesVariables(t *testing.T) { + s := New() + xs := []float64{1, 2, 3} + n := 10 + arr, _ := nd.FromData([]float64{1, 2}, 2) + var m float64 + var names []string + var a *nd.Array + var anyv any + sh, notes := run(t, s, Cell{ + Source: "m = xs.sum / xs.size\nnames = %w[a b]\na = arr * K\nh = {'k' => 1}\nm + 1", + Display: true, + In: []Var{{Name: "xs", Ptr: &xs}, {Name: "arr", Ptr: &arr}, {Name: "K", Ptr: &n}}, + Out: []Var{{Name: "m", Ptr: &m}, {Name: "names", Ptr: &names}, {Name: "a", Ptr: &a}, {Name: "h", Ptr: &anyv}}, + }) + if m != 2 || !reflect.DeepEqual(names, []string{"a", "b"}) || a.Repr() != "array([10., 20.])" { + t.Fatalf("m %v names %v a %v", m, names, a.Repr()) + } + if !reflect.DeepEqual(anyv, map[string]any{"k": int64(1)}) { + t.Fatalf("h = %#v", anyv) + } + want := map[string]string{"m": "float64", "names": "[]string", "a": "*{github.com/go-ndarray/ndarray}.Array", "h": "map[string]any"} + if !reflect.DeepEqual(notes, want) { + t.Fatalf("notes = %v", notes) + } + if len(sh.vals) != 1 || sh.vals[0] != Inspect("3.0") { + t.Fatalf("shown %#v", sh.vals) + } + // A constant set again replaces it, without a warning. + n = 3 + sh, _ = run(t, s, Cell{Source: "K * 2", Display: true, In: []Var{{Name: "K", Ptr: &n}}}) + if sh.vals[0] != Inspect("6") { + t.Fatalf("K = %v", sh.vals) + } + // IfAbsent keeps the interpreter's own value; a fresh session takes it. + stale := 99.0 + sh, _ = run(t, s, Cell{Source: "m", Display: true, In: []Var{{Name: "m", Ptr: &stale, IfAbsent: true}}}) + if sh.vals[0] != Inspect("2.0") { + t.Fatalf("m = %v", sh.vals) + } + sh, _ = run(t, New(), Cell{Source: "m", Display: true, In: []Var{{Name: "m", Ptr: &stale, IfAbsent: true}}}) + if sh.vals[0] != Inspect("99.0") { + t.Fatalf("m = %v", sh.vals) + } + // What a cell prints goes to the process's streams. + run(t, s, Cell{Source: "print \"\\n\"; $stderr.print \"\\n\""}) + // Nothing shown for nil, or without Display. + if sh, _ := run(t, s, Cell{Source: "nil", Display: true}); len(sh.vals) != 0 { + t.Fatal("nil was shown") + } + if sh, _ := run(t, s, Cell{Source: "1"}); len(sh.vals) != 0 { + t.Fatal("shown without Display") + } + // A figure shows itself; an NDArray by its inspect. + sh, _ = run(t, s, Cell{Source: "fig, ax = Plot.subplots\nax.plot([1, 2], [3, 4])\nfig", Display: true}) + if _, ok := sh.vals[0].(*plot.Figure); !ok { + t.Fatalf("figure shown as %T", sh.vals[0]) + } + sh, _ = run(t, s, Cell{Source: "NDArray.arange(2)", Display: true}) + if sh.vals[0] != Inspect("array([0., 1.])") { + t.Fatalf("NDArray shown as %#v", sh.vals[0]) + } + if got := Inspect("x").Display()["text/plain"]; string(got) != "x" { + t.Fatal(string(got)) + } + // A variable the cell did not assign (a branch not taken) is nil. + f := 1.5 + run(t, s, Cell{Source: "if false\n q = 1\nend", Out: []Var{{Name: "q", Ptr: &f}}}) + if f != 0 { + t.Fatalf("q = %v", f) + } +} + +func TestRunErrors(t *testing.T) { + s := New() + catch := func(c Cell) string { + t.Helper() + var msg string + func() { + defer func() { + r := recover() + e, ok := r.(*Error) + if !ok { + t.Fatalf("panic %#v", r) + } + if e.Error() != e.CellError() { + t.Fatal("Error and CellError differ") + } + msg = e.CellError() + }() + run(t, s, c) + }() + return msg + } + if msg := catch(Cell{Source: "def f = raise(ArgumentError, 'bad')\n\nf", File: "nb.md", Line: 10}); !strings.HasPrefix(msg, "nb.md:10: bad (ArgumentError)") || !strings.Contains(msg, "from nb.md:12:in '
'") { + t.Fatalf("msg = %q", msg) + } + var f float64 + if msg := catch(Cell{Source: "y = 'text'", Out: []Var{{Name: "y", Ptr: &f}}}); !strings.Contains(msg, "y: is now a string in Go, and Go cells read it as a float64") { + t.Fatalf("msg = %q", msg) + } + bad := "not a local" + if msg := catch(Cell{Source: "1", In: []Var{{Name: "a-b", Ptr: &bad}}}); !strings.Contains(msg, "not a local variable name") { + t.Fatalf("msg = %q", msg) + } + if got := describe(errors.New("plain"), "nb.md"); got != "plain" { + t.Fatal(got) + } +} + +func TestSetConstantErrors(t *testing.T) { + s := New() + // A constant that cannot be removed: a frozen Object refuses. + if _, err := s.in.Eval("module Kernel; end; class Object; def self.remove_const(*) = raise('no'); private_class_method :remove_const; end", "x.rb", 1); err != nil { + t.Fatal(err) + } + n := 1 + if err := s.set(Var{Name: "Zq", Ptr: &n}); err != nil { + t.Fatal(err) + } + if err := s.set(Var{Name: "Zq", Ptr: &n}); err == nil { + t.Fatal("remove_const raised, set did not fail") + } +} + +func TestGoValueAndType(t *testing.T) { + in := rb.New() + val := func(src string) any { + v, err := in.Eval(src, "x.rb", 1) + if err != nil { + t.Fatal(err) + } + return GoValue(v) + } + big2 := new(big.Int).Lsh(big.NewInt(1), 80) + obj, _ := in.Eval("Object.new", "x.rb", 1) + for src, want := range map[string]struct { + v any + typ string + }{ + "nil": {nil, "any"}, + "true": {true, "bool"}, + "3": {int64(3), "int64"}, + "2**80": {big2, "*{math/big}.Int"}, + "1.5": {1.5, "float64"}, + "'s'": {"s", "string"}, + ":s": {rb.Symbol("s"), "{github.com/go-embedded-ruby/ruby}.Symbol"}, + "Complex(1, 2)": {complex(1, 2), "complex128"}, + "Rational(1, 2)": {big.NewRat(1, 2), "*{math/big}.Rat"}, + "[1.5, 2.5]": {[]float64{1.5, 2.5}, "[]float64"}, + "[1, 2]": {[]int64{1, 2}, "[]int64"}, + "%w[a b]": {[]string{"a", "b"}, "[]string"}, + "[true, false]": {[]bool{true, false}, "[]bool"}, + "[1, 'a']": {[]any{int64(1), "a"}, "[]any"}, + "[1.5, 'a']": {[]any{1.5, "a"}, "[]any"}, + "[]": {[]any{}, "[]any"}, + "[nil]": {[]any{nil}, "[]any"}, + "{'a' => 1}": {map[string]any{"a": int64(1)}, "map[string]any"}, + "{a: 1}": {map[rb.Symbol]any{"a": int64(1)}, "map[{github.com/go-embedded-ruby/ruby}.Symbol]any"}, + "{1 => 2}": {map[any]any{int64(1): int64(2)}, "map[any]any"}, + "{'a' => 1, b: 2}": {map[any]any{"a": int64(1), rb.Symbol("b"): int64(2)}, "map[any]any"}, + } { + got := val(src) + if !reflect.DeepEqual(got, want.v) && !(want.v != nil && reflect.TypeOf(got) == reflect.TypeOf(want.v) && strings.Contains(src, "**")) { + t.Errorf("%s: GoValue = %#v, want %#v", src, got, want.v) + } + if typ := GoType(got); typ != want.typ { + t.Errorf("%s: GoType = %s, want %s", src, typ, want.typ) + } + } + if typ := GoType(GoValue(obj)); typ != "{github.com/go-embedded-ruby/ruby}.Value" { + t.Errorf("an Object: %s", typ) + } + type local struct{} + for _, c := range []struct { + x any + want string + }{ + {stats.TTestResult{}, "{github.com/go-statistics/stats}.TTestResult"}, + {&plot.Figure{}, "*{github.com/go-plot/plot}.Figure"}, + {local{}, "any"}, + {func() {}, "any"}, + {map[string]func(){}, "any"}, + {map[local]int{}, "any"}, + {[]func(){}, "any"}, + {(*func())(nil), "any"}, + {[]error{}, "[]error"}, + {[]interface{ M() }{}, "any"}, + {os.Stdout, "*{os}.File"}, + {reflect.TypeFor[int](), "any"}, // its dynamic type is unexported + } { + if typ := GoType(c.x); typ != c.want { + t.Errorf("GoType(%T) = %s, want %s", c.x, typ, c.want) + } + } + if token("Gen[int]") || token("lower") || !token("Upper_1") { + t.Fatal("token") + } + var anyv any = 1 + if err := assign(&anyv, nil); err != nil || anyv != nil { + t.Fatal(anyv, err) + } +} From 75495ad6c3012fc8f814435d1ffaf957cdb8920a Mon Sep 17 00:00:00 2001 From: tannevaled Date: Sun, 18 Oct 2026 09:15:07 +0200 Subject: [PATCH 15/20] cells: NewWriting, for a host that captures output itself --- cells/cells.go | 12 +++++++++--- cells/cells_test.go | 9 +++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/cells/cells.go b/cells/cells.go index e83970c..c8aa206 100644 --- a/cells/cells.go +++ b/cells/cells.go @@ -30,6 +30,7 @@ package cells import ( "fmt" + "io" "math/big" "os" "reflect" @@ -58,9 +59,14 @@ type stderr struct{} func (stderr) Write(p []byte) (int, error) { return os.Stderr.Write(p) } -// New starts an interpreter with the go-scikit modules. -func New() *Session { - in := rb.New(rb.WithStdout(stdout{}), rb.WithStderr(stderr{})) +// New starts an interpreter with the go-scikit modules, writing to the +// process's standard output and error. +func New() *Session { return NewWriting(stdout{}, stderr{}) } + +// NewWriting is New writing what Ruby prints to out, and its $stderr and +// warnings to errOut: a host that captures a cell's output itself. +func NewWriting(out, errOut io.Writer) *Session { + in := rb.New(rb.WithStdout(out), rb.WithStderr(errOut)) _ = scikit.Register(in) // a fresh interpreter has none of their names obj, _ := in.Eval("Object", "(cells)", 1) return &Session{in: in, obj: obj} diff --git a/cells/cells_test.go b/cells/cells_test.go index 6de6cf2..a1cfce3 100644 --- a/cells/cells_test.go +++ b/cells/cells_test.go @@ -232,3 +232,12 @@ func TestGoValueAndType(t *testing.T) { t.Fatal(anyv, err) } } + +func TestNewWriting(t *testing.T) { + var out, errOut strings.Builder + s := NewWriting(&out, &errOut) + run(t, s, Cell{Source: "puts 1\nwarn 'w'"}) + if out.String() != "1\n" || errOut.String() != "w\n" { + t.Fatalf("out %q err %q", out.String(), errOut.String()) + } +} From d4ec4fc614840b20db5840562d69e4396cf60477 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Sun, 18 Oct 2026 09:56:13 +0200 Subject: [PATCH 16/20] Test a result's to_h and a nil slice field --- stats_test.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/stats_test.go b/stats_test.go index acebcc4..15329f4 100644 --- a/stats_test.go +++ b/stats_test.go @@ -393,3 +393,23 @@ func TestStatsObjectsMatchGo(t *testing.T) { check(t, in, "r.integers(0, 10, [2, 2])", must[*nd.Array](t)(rng.Integers(0, 10, 2, 2))) check(t, in, "r.uniform(0, 1)", must[*nd.Array](t)(rng.Uniform(0, 1))) } + +// A result's to_h holds every field, in order, converted as its readers +// convert them: a []int an Array, a nil slice nil. +func TestResultToH(t *testing.T) { + in := newInterp(t) + setSamples(t, in) + got := eval(t, in, "Stats.binned_statistic(X, Y, bins: 3).to_h.to_a") + res := must[stats.BinnedResult](t)(stats.BinnedStatistic(sampleX, sampleY, &stats.BinnedOptions{Bins: 3})) + want := []any{ + []any{rb.Symbol("statistic"), res.Statistic}, + []any{rb.Symbol("edges"), res.Edges}, + []any{rb.Symbol("bin_number"), res.BinNumber}, + } + if !same(got, want) { + t.Fatalf("to_h = %#v", got) + } + if v := eval(t, in, "Integrate.quad(0, 1) { |x| x }.cycle_ier"); v != nil { + t.Fatalf("a nil slice reads %#v", v) + } +} From 85379b51ce069f361c12d36edaf1e53876805b83 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Sun, 18 Oct 2026 10:00:40 +0200 Subject: [PATCH 17/20] Special: call one- and two-real ufuncs directly, not through reflect (gamma over 1e5 points 35x faster, beta 9x) --- special.go | 44 ++++++++++++++++++++++++++++++++++--------- special_bench_test.go | 17 +++++++++++++++++ 2 files changed, 52 insertions(+), 9 deletions(-) create mode 100644 special_bench_test.go diff --git a/special.go b/special.go index e111edb..6176db0 100644 --- a/special.go +++ b/special.go @@ -119,15 +119,7 @@ func applyUfunc(fn reflect.Value, c *rb.Call) (any, error) { } n := len(flat[0]) res := make([]float64, n) - args := append([]reflect.Value(nil), scalars...) - for k := range n { - for i, j := range at { - if j >= 0 { - args[i] = reflect.ValueOf(flat[j][k]) - } - } - res[k] = fn.Call(args)[0].Float() - } + ufuncLoop(fn, at, flat, scalars, res) if allLists { list := make([]any, n) for k, r := range res { @@ -161,3 +153,37 @@ func broadcast(arrays []*nd.Array) ([][]float64, []int, error) { } return out, shape, nil } + +// ufuncLoop fills res with fn over the elements: flat holds the array +// arguments, at says which argument each is (-1: the scalar in scalars). +// The functions of one or two reals — most of Special — are called +// directly; reflect.Value.Call allocates its arguments on every call and +// costs ten times the function itself, so it is kept for the others. +func ufuncLoop(fn reflect.Value, at []int, flat [][]float64, scalars []reflect.Value, res []float64) { + arg := func(i, k int) float64 { + if at[i] >= 0 { + return flat[at[i]][k] + } + return scalars[i].Float() + } + switch f := fn.Interface().(type) { + case func(float64) float64: + for k, x := range flat[0] { + res[k] = f(x) + } + case func(float64, float64) float64: + for k := range res { + res[k] = f(arg(0, k), arg(1, k)) + } + default: + args := append([]reflect.Value(nil), scalars...) + for k := range res { + for i, j := range at { + if j >= 0 { + args[i] = reflect.ValueOf(flat[j][k]) + } + } + res[k] = fn.Call(args)[0].Float() + } + } +} diff --git a/special_bench_test.go b/special_bench_test.go new file mode 100644 index 0000000..3a8ba34 --- /dev/null +++ b/special_bench_test.go @@ -0,0 +1,17 @@ +package ruby + +import "testing" + +func BenchmarkSpecialUfunc(b *testing.B) { + in := newInterp(&testing.T{}) + in.Eval("x = NDArray.linspace(0.1, 5, 100_000); y = NDArray.linspace(0.5, 2, 100_000)", "b.rb", 1) + for _, src := range []string{"Special.gamma(x)", "Special.beta(x, y)", "Special.jv(2.5, x)"} { + b.Run(src, func(b *testing.B) { + for b.Loop() { + if _, err := in.Eval(src, "b.rb", 1); err != nil { + b.Fatal(err) + } + } + }) + } +} From a881de224ebdb0cbbeb3b1f664989dc2e95ff667 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Sun, 18 Oct 2026 15:20:40 +0200 Subject: [PATCH 18/20] cells: a runaway recursion's error shows twelve frames, not thousands --- cells/cells.go | 13 ++++++++++++- cells/cells_test.go | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/cells/cells.go b/cells/cells.go index c8aa206..2286e65 100644 --- a/cells/cells.go +++ b/cells/cells.go @@ -136,6 +136,10 @@ func (s *Session) Run(c Cell, show func(...any), note func(name, typ string)) { } } +// maxFrames bounds the backtrace an error shows: a runaway recursion has +// thousands of frames, all alike. +const maxFrames = 12 + // describe renders a Ruby exception: file:line: message (Class), then the // backtrace frames that are in the notebook. func describe(err error, file string) string { @@ -145,11 +149,18 @@ func describe(err error, file string) string { } var b strings.Builder b.WriteString(e.Error()) + n := 0 for _, f := range e.Backtrace { - if strings.HasPrefix(f, file+":") { + if !strings.HasPrefix(f, file+":") { + continue + } + if n++; n <= maxFrames { b.WriteString("\n\tfrom " + f) } } + if n > maxFrames { + fmt.Fprintf(&b, "\n\t... and %d more frames", n-maxFrames) + } return b.String() } diff --git a/cells/cells_test.go b/cells/cells_test.go index a1cfce3..760742e 100644 --- a/cells/cells_test.go +++ b/cells/cells_test.go @@ -134,6 +134,9 @@ func TestRunErrors(t *testing.T) { if msg := catch(Cell{Source: "1", In: []Var{{Name: "a-b", Ptr: &bad}}}); !strings.Contains(msg, "not a local variable name") { t.Fatalf("msg = %q", msg) } + if got := describe(&rb.Error{Class: "E", Message: "m", Backtrace: []string{"lib.rb:1:in 'g'", "nb.md:2:in '
'"}}, "nb.md"); got != "E: m\n\tfrom nb.md:2:in '
'" { + t.Fatalf("describe = %q", got) + } if got := describe(errors.New("plain"), "nb.md"); got != "plain" { t.Fatal(got) } @@ -241,3 +244,15 @@ func TestNewWriting(t *testing.T) { t.Fatalf("out %q err %q", out.String(), errOut.String()) } } + +func TestDeepRecursionIsBounded(t *testing.T) { + s := New() + var msg string + func() { + defer func() { msg = recover().(*Error).CellError() }() + run(t, s, Cell{Source: "def f(n) = f(n + 1)\nf(0)"}) + }() + if !strings.Contains(msg, "stack level too deep (SystemStackError)") || !strings.Contains(msg, "more frames") || strings.Count(msg, "\n") > maxFrames+1 { + t.Fatalf("msg = %q", msg) + } +} From 2956206bf3a0277c3a5c23f3a796893341e85008 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Sun, 18 Oct 2026 22:36:09 +0200 Subject: [PATCH 19/20] go-embedded-ruby/ruby v0.8.0, the released embedding API, instead of a local checkout; the README test counts only this package's names --- README.md | 2 ++ go.mod | 20 ++++++++++---------- go.sum | 10 ++++++---- readme_test.go | 22 ++++++++++++++++++++-- 4 files changed, 38 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 2213a63..42eb084 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ svg = fig.to_svg One short program per module is in [examples/](examples/); a test runs them all. +> **Overlap with rbgo.** Since v0.7.0, rbgo defines modules `Stats`, `Special`, `Optimize` and `Integrate` of its own, over the same four libraries, with SciPy's names (`Stats.norm`, `Special.gammaln`). `Register` adds these bindings to those modules. Where both define the same name (`Special.gamma`, `Optimize.minimize`, …) this package's method replaces rbgo's, and the other names of both remain. Which of the two should own these modules is an open question, tracked with the release notes. + ## The rules - **Same code underneath.** Every Ruby method calls the Go function it is named diff --git a/go.mod b/go.mod index c576863..653d918 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,16 @@ module github.com/go-scikit/ruby go 1.27.2 -require github.com/go-embedded-ruby/ruby v0.6.0 +require ( + github.com/go-dataframe/dataframe v0.1.0 + github.com/go-embedded-ruby/ruby v0.8.0 + github.com/go-ndarray/ndarray v0.12.0 + github.com/go-numerics/integrate v0.2.0 + github.com/go-numerics/optimize v0.2.0 + github.com/go-numerics/special v0.1.0 + github.com/go-plot/plot v0.1.1 + github.com/go-statistics/stats v0.1.0 +) require ( filippo.io/age v1.3.2 // indirect @@ -79,7 +88,6 @@ require ( github.com/go-composites/time v0.0.0-20260929013028-aa3c57ae1371 // indirect github.com/go-crdt/collab v0.74.0 // indirect github.com/go-crdt/crdt v0.55.0 // indirect - github.com/go-dataframe/dataframe v0.1.0 // indirect github.com/go-datetime/dates v0.1.0 // indirect github.com/go-encryptions/unixcrypt v0.1.0 // indirect github.com/go-facter/facter v0.0.0-20260830120958-454b72e642ab // indirect @@ -100,16 +108,11 @@ require ( github.com/go-logr/logr v1.4.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-mustache/mustache v0.1.0 // indirect - github.com/go-ndarray/ndarray v0.12.0 // indirect github.com/go-nokogiri/nokogiri v0.1.0 // indirect - github.com/go-numerics/integrate v0.1.0 // indirect - github.com/go-numerics/optimize v0.1.0 // indirect - github.com/go-numerics/special v0.1.0 // indirect github.com/go-opentype/fonts v0.12.0 // indirect github.com/go-opentype/opentype v0.15.0 // indirect github.com/go-opentype/shape v0.5.0 // indirect github.com/go-pcore/pcore v0.0.0-20260831114716-f9c3e7f59eaa // indirect - github.com/go-plot/plot v0.1.1 // indirect github.com/go-puppet/puppet v0.0.0-20260918012035-fc6b0424cdbd // indirect github.com/go-regexp/engine v0.3.0 // indirect github.com/go-richdoc/richdoc v0.4.0 // indirect @@ -298,7 +301,6 @@ require ( github.com/go-simd/crc32 v0.0.0-20260903220012-5f164e0e0487 // indirect github.com/go-simd/hex v0.0.0-20260903220024-a8d22a843218 // indirect github.com/go-sql-driver/mysql v1.10.1 // indirect - github.com/go-statistics/stats v0.1.0 // indirect github.com/go-tex/math v0.50.0 // indirect github.com/go-typeset/bidi v0.3.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect @@ -410,5 +412,3 @@ require ( modernc.org/memory v1.12.1 // indirect modernc.org/sqlite v1.60.1 // indirect ) - -replace github.com/go-embedded-ruby/ruby => ../../go-embedded-ruby/ruby.wt-ruby diff --git a/go.sum b/go.sum index e9e8cbe..3ceae99 100644 --- a/go.sum +++ b/go.sum @@ -204,6 +204,8 @@ github.com/go-dataframe/dataframe v0.1.0 h1:l92liqTQ/StH5rurH2ipgvXunMbtCJAkDNWE github.com/go-dataframe/dataframe v0.1.0/go.mod h1:rAJdylzFafSOhTAV6z+gSs4i0D9M9XKMKqhWUjIEolk= github.com/go-datetime/dates v0.1.0 h1:Uj8TaTZ09DXdq75EGQ/teFdrXpfVAC3ZDHAPQvPTnaA= github.com/go-datetime/dates v0.1.0/go.mod h1:KeZ1wUeMScJD6HOvj3hINwbsB+GCSFjJLWcJhrBlwIo= +github.com/go-embedded-ruby/ruby v0.8.0 h1:09E9fzAQutBwsfBR7Sm3aAEmeYiG+I6JQBzVTdiSxE8= +github.com/go-embedded-ruby/ruby v0.8.0/go.mod h1:g7gQohmhj96s4qiaoTtFTa2Hbfbnh0QTQej1w81LxOE= github.com/go-encryptions/unixcrypt v0.1.0 h1:XWk4h9uCDd+T2hovOzgO0B2vOdJvAwi9nbOBVb3o62c= github.com/go-encryptions/unixcrypt v0.1.0/go.mod h1:B1dJSjz3kdxvP7cpi5xSzyxezgBijmtNCUeCKqqDd3o= github.com/go-facter/facter v0.0.0-20260830120958-454b72e642ab h1:bXPxQDhADpiF/S/8jlRFhVisFPRaxPtPgjibLnOzvsE= @@ -253,10 +255,10 @@ github.com/go-ndarray/ndarray v0.12.0 h1:4xfl9ZQo1m0UXopo06KypELg1wUYgQb5kdKaJPZ github.com/go-ndarray/ndarray v0.12.0/go.mod h1:ZcgNCBVLXt1FxeZjQWI5Z+k0SSWAT0/wR8slfat/gRA= github.com/go-nokogiri/nokogiri v0.1.0 h1:3Q3BewGHK16Bt34ggJeyf9xeIMJyA3dmfbIxARPgR9U= github.com/go-nokogiri/nokogiri v0.1.0/go.mod h1:zjghooYIK8Xfg+fL4/OUhROslx51xGsM3JKtB2TI0tg= -github.com/go-numerics/integrate v0.1.0 h1:m1HxKk9CjH8tPjd6rPNq6tdHEl95BHokGbdniG4qbBY= -github.com/go-numerics/integrate v0.1.0/go.mod h1:EGBYtIsMIeVcuVcf6LnLx5/p2lCwbAybdGMiHhwoh/c= -github.com/go-numerics/optimize v0.1.0 h1:SOA+QxTuIjeZSAsd5Dyj433lItnOKWyprCbHQoLxg/c= -github.com/go-numerics/optimize v0.1.0/go.mod h1:ieZ+/EsbBw/EfHl4W8JryPyCBB8n5a8xKk6lu/xh7zk= +github.com/go-numerics/integrate v0.2.0 h1:IkJhDimTc7nQETCixEvbbHhtOF36GbANBKJI3mJB3Kw= +github.com/go-numerics/integrate v0.2.0/go.mod h1:ABjpcWy8ZSOg8j6S+7YRsvMJ2LpVkH4E64/CGet2gCw= +github.com/go-numerics/optimize v0.2.0 h1:7OO7HlQBCzT4hIMsgmI7T1PQveUwoQJ70xBP84aN7dc= +github.com/go-numerics/optimize v0.2.0/go.mod h1:EJZo6XNRjuAQcV22re4/kJYr+bYkpKx4IVT2aiG97Pk= github.com/go-numerics/special v0.1.0 h1:HcsebTwcNuAhFBP8RuOirO9XVmWJSrxARiOVkyeCfUk= github.com/go-numerics/special v0.1.0/go.mod h1:T0QKLvh32lH5Bq9wiTEvtlaFd1vaOKRGrOPgvTeYfdM= github.com/go-opentype/fonts v0.12.0 h1:gjYYkbDZBK3JHpraakgGNnXdkGq8oADerhDYjC22ILQ= diff --git a/readme_test.go b/readme_test.go index 863a1bc..2e777c6 100644 --- a/readme_test.go +++ b/readme_test.go @@ -5,13 +5,31 @@ import ( "sort" "strings" "testing" + + rb "github.com/go-embedded-ruby/ruby" ) // bindings lists every Ruby name the bindings define: "Module.function", // "Class.class_method" and "Class#method", for the modules and classes // Register makes. func bindings(t *testing.T) []string { - in := newInterp(t) + // The names rbgo defines itself in the same modules (its own Stats, + // Special, Optimize and Integrate) are not this package's. + own := map[string]bool{} + for _, n := range walkNames(t, rb.New()) { + own[n] = true + } + var names []string + for _, n := range walkNames(t, newInterp(t)) { + if !own[n] { + names = append(names, n) + } + } + return names +} + +// walkNames lists the names of the six modules in an interpreter. +func walkNames(t *testing.T, in *rb.Interp) []string { src := ` out = [] walk = lambda do |mod| @@ -22,7 +40,7 @@ walk = lambda do |mod| walk.(v) if v.is_a?(Module) && v.name.start_with?("#{mod}::") end end -[Stats, Special, Optimize, Integrate, DataFrame, Plot].each { |m| walk.(m) } +[:Stats, :Special, :Optimize, :Integrate, :DataFrame, :Plot].each { |m| walk.(Object.const_get(m)) if Object.const_defined?(m) } out` v, err := in.Eval(src, "names.rb", 1) if err != nil { From f63439f8be845b8dd6cdf84f05b9a7177fc425c7 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Sun, 18 Oct 2026 22:56:46 +0200 Subject: [PATCH 20/20] Tests on Windows: CRLF checkouts, and the system's own words for a missing file --- dataframe_test.go | 4 ++-- internal/gen/main_test.go | 4 +++- plot_test.go | 2 +- readme_test.go | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/dataframe_test.go b/dataframe_test.go index 8fdb4d4..e018efa 100644 --- a/dataframe_test.go +++ b/dataframe_test.go @@ -186,7 +186,7 @@ func TestDataFrameConstruction(t *testing.T) { {`DataFrame.read_csv("a", dtype: {"a" => :nope})`, "ArgumentError", "nope"}, {`DataFrame.read_csv("a", dtype: {"a" => 1})`, "TypeError", "Integer into a dtype"}, {`DataFrame.read_csv("a,b\n1,2\n3,4,5\n")`, "ArgumentError", "line"}, - {`DataFrame.read_csv_file(path + ".none")`, "ArgumentError", "no such file"}, + {`DataFrame.read_csv_file(path + ".none")`, "ArgumentError", "a.csv.none"}, // the system's own words differ {`DataFrame.read_csv_file`, "ArgumentError", "given 0, expected 1"}, } { evalErr(t, in, c.src, c.class, c.msg) @@ -727,7 +727,7 @@ func TestDataFrameErrors(t *testing.T) { {`df.to_csv(na_rep: 1)`, "TypeError", "Integer into String"}, {`df.write_csv`, "ArgumentError", "given 0, expected 1"}, {`df.write_csv(1)`, "TypeError", "Integer into String"}, - {`df.write_csv("/no/such/dir/x.csv")`, "IOError", "no such file"}, + {`df.write_csv("/no/such/dir/x.csv")`, "IOError", "/no/such/dir/x.csv"}, {`df.write_csv("x.csv", bogus: 1)`, "ArgumentError", "unknown keyword"}, {`df.sort_values("pop", bogus: 1)`, "ArgumentError", "unknown keyword: :bogus (known: ascending)"}, {`df.sort_values`, "ArgumentError", "given 0, expected 1"}, diff --git a/internal/gen/main_test.go b/internal/gen/main_test.go index 41dbfb8..e47def8 100644 --- a/internal/gen/main_test.go +++ b/internal/gen/main_test.go @@ -4,6 +4,7 @@ import ( "errors" "os" "path/filepath" + "strings" "testing" ) @@ -22,7 +23,8 @@ func TestGeneratedFilesAreCurrent(t *testing.T) { if err != nil { t.Fatal(err) } - if string(got) != string(want) { + // A Windows checkout has CRLF line endings; the generator writes LF. + if strings.ReplaceAll(string(got), "\r\n", "\n") != string(want) { t.Errorf("%s is out of date: run go generate", f) } } diff --git a/plot_test.go b/plot_test.go index 0bdc96d..af70226 100644 --- a/plot_test.go +++ b/plot_test.go @@ -273,7 +273,7 @@ func TestPlotFigure(t *testing.T) { } } evalErr(t, in, `fig.savefig(File.join(dir, "a.pdf"))`, "ArgumentError", "unknown format") - evalErr(t, in, `fig.save_svg(File.join(dir, "no", "such", "a.svg"))`, "IOError", "no such file") + evalErr(t, in, `fig.save_svg(File.join(dir, "no", "such", "a.svg"))`, "IOError", "a.svg") evalErr(t, in, `fig.save_svg(1)`, "TypeError", "String") evalErr(t, in, `fig.save_svg`, "ArgumentError", "given 0, expected 1") } diff --git a/readme_test.go b/readme_test.go index 2e777c6..4c0d0c6 100644 --- a/readme_test.go +++ b/readme_test.go @@ -61,7 +61,7 @@ func TestREADMEListsEveryBinding(t *testing.T) { if err != nil { t.Fatal(err) } - text := string(readme) + text := strings.ReplaceAll(string(readme), "\r\n", "\n") // a Windows checkout missing := 0 for _, n := range bindings(t) { // The README groups names by class: "Stats::Normal#pdf" is listed as