Skip to content

Commit

Permalink
Merge pull request #1 from redskyops/add/categorical
Browse files Browse the repository at this point in the history
API support for categorical parameters
  • Loading branch information
jgustie authored Sep 22, 2020
2 parents 49eff73 + 13406b1 commit 0d76b7c
Show file tree
Hide file tree
Showing 4 changed files with 227 additions and 5 deletions.
9 changes: 6 additions & 3 deletions pkg/redskyapi/experiments/v1alpha1/experiment.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,9 @@ type Constraint struct {
type ParameterType string

const (
ParameterTypeInteger ParameterType = "int"
ParameterTypeDouble ParameterType = "double"
ParameterTypeInteger ParameterType = "int"
ParameterTypeDouble ParameterType = "double"
ParameterTypeCategorical ParameterType = "categorical"
)

type Bounds struct {
Expand All @@ -114,7 +115,9 @@ type Parameter struct {
// The type of the parameter.
Type ParameterType `json:"type"`
// The domain of the parameter.
Bounds Bounds `json:"bounds"`
Bounds *Bounds `json:"bounds,omitempty"`
// The discrete values for a categorical parameter.
Values []string `json:"values,omitempty"`
}

type ExperimentMeta struct {
Expand Down
94 changes: 94 additions & 0 deletions pkg/redskyapi/experiments/v1alpha1/numstr/numorstr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
Copyright 2020 GramLabs, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package numstr

import (
"encoding/json"
"strconv"
)

// NumberOrString is value that can a JSON number or string.
type NumberOrString struct {
IsString bool
NumVal json.Number
StrVal string
}

// FromInt64 returns the supplied value as a NumberOrString
func FromInt64(val int64) NumberOrString {
return NumberOrString{NumVal: json.Number(strconv.FormatInt(val, 10))}
}

// FromFloat64 returns the supplied value as a NumberOrString
func FromFloat64(val float64) NumberOrString {
return NumberOrString{NumVal: json.Number(strconv.FormatFloat(val, 'f', -1, 64))}
}

// FromNumber returns the supplied value as a NumberOrString
func FromNumber(val json.Number) NumberOrString {
return NumberOrString{NumVal: val}
}

// FromString returns the supplied value as a NumberOrString
func FromString(val string) NumberOrString {
return NumberOrString{StrVal: val, IsString: true}
}

// String coerces the value to a string.
func (s *NumberOrString) String() string {
if s.IsString {
return s.StrVal
}
return s.NumVal.String()
}

// Int64Value coerces the value to an int64.
func (s *NumberOrString) Int64Value() int64 {
if s.IsString {
v, _ := strconv.ParseInt(s.StrVal, 10, 64)
return v
}
v, _ := s.NumVal.Int64()
return v
}

// Float64Value coerces the value to a float64.
func (s *NumberOrString) Float64Value() float64 {
if s.IsString {
v, _ := strconv.ParseFloat(s.StrVal, 64)
return v
}
v, _ := s.NumVal.Float64()
return v
}

// MarshalJSON writes the value with the appropriate type.
func (s NumberOrString) MarshalJSON() ([]byte, error) {
if s.IsString {
return json.Marshal(s.StrVal)
}
return json.Marshal(s.NumVal)
}

// UnmarshalJSON reads the value from either a string or number.
func (s *NumberOrString) UnmarshalJSON(b []byte) error {
if b[0] == '"' {
s.IsString = true
return json.Unmarshal(b, &s.StrVal)
}
return json.Unmarshal(b, &s.NumVal)
}
124 changes: 124 additions & 0 deletions pkg/redskyapi/experiments/v1alpha1/parameter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
Copyright 2020 GramLabs, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1alpha1

import (
"encoding/json"
"fmt"
"strconv"
"strings"

"github.com/redskyops/redskyops-go/pkg/redskyapi/experiments/v1alpha1/numstr"
)

// LowerBound attempts to return the lower bound for this parameter.
func (p *Parameter) LowerBound() (*numstr.NumberOrString, error) {
if p.Type == ParameterTypeCategorical {
if len(p.Values) == 0 {
return nil, fmt.Errorf("unable to determine categorical minimum bound")
}
return &numstr.NumberOrString{StrVal: p.Values[0], IsString: true}, nil
}

if p.Bounds == nil {
return nil, fmt.Errorf("unable to determine numeric minimum bound")
}

return &numstr.NumberOrString{NumVal: p.Bounds.Min}, nil
}

// UpperBound attempts to return the upper bound for this parameter.
func (p *Parameter) UpperBound() (*numstr.NumberOrString, error) {
if p.Type == ParameterTypeCategorical {
if len(p.Values) == 0 {
return nil, fmt.Errorf("unable to determine categorical maximum bound")
}
return &numstr.NumberOrString{StrVal: p.Values[len(p.Values)-1], IsString: true}, nil
}

if p.Bounds == nil {
return nil, fmt.Errorf("unable to determine numeric maximum bound")
}

return &numstr.NumberOrString{NumVal: p.Bounds.Max}, nil
}

// ParseValue attempts to parse the supplied value into a NumberOrString based on the type of this parameter.
func (p *Parameter) ParseValue(s string) (*numstr.NumberOrString, error) {
var v numstr.NumberOrString
switch p.Type {
case ParameterTypeInteger:
if _, err := strconv.ParseInt(s, 10, 64); err != nil {
return nil, err
}
v = numstr.FromNumber(json.Number(s))
case ParameterTypeDouble:
if _, err := strconv.ParseFloat(s, 64); err != nil {
return nil, err
}
v = numstr.FromNumber(json.Number(s))
case ParameterTypeCategorical:
v = numstr.FromString(s)
}
return &v, nil
}

// CheckParameterValue validates that the supplied value can be used for a parameter.
func CheckParameterValue(p *Parameter, v *numstr.NumberOrString) error {
if p.Type == ParameterTypeCategorical {
if !v.IsString {
return fmt.Errorf("categorical value must be a string: %s", v.String())
}
for _, allowed := range p.Values {
if v.StrVal == allowed {
return nil
}
}
return fmt.Errorf("categorical value is out of range: %s [%s]", v.String(), strings.Join(p.Values, ", "))
}

if v.IsString {
return fmt.Errorf("numeric value must not be a string: %s", v.String())
}

lower, err := p.LowerBound()
if err != nil {
return err
}
upper, err := p.UpperBound()
if err != nil {
return err
}

switch p.Type {
case ParameterTypeInteger:
val := v.Int64Value()
min, max := lower.Int64Value(), upper.Int64Value()
if val < min || val > max {
return fmt.Errorf("integer value is out of range [%d-%d]: %d", min, max, val)
}
case ParameterTypeDouble:
val := v.Float64Value()
min, max := lower.Float64Value(), upper.Float64Value()
if val < min || val > max {
return fmt.Errorf("double value is out of range [%f-%f]: %f", min, max, val)
}
default:
return fmt.Errorf("unknown parameter type: %s", p.Type)
}
return nil
}
5 changes: 3 additions & 2 deletions pkg/redskyapi/experiments/v1alpha1/trial.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ limitations under the License.
package v1alpha1

import (
"encoding/json"
"fmt"
"net/url"
"strings"
"time"

"github.com/redskyops/redskyops-go/pkg/redskyapi/experiments/v1alpha1/numstr"
)

type TrialMeta struct {
Expand All @@ -47,7 +48,7 @@ type Assignment struct {
// The name of the parameter in the experiment the assignment corresponds to.
ParameterName string `json:"parameterName"`
// The assigned value of the parameter.
Value json.Number `json:"value"`
Value numstr.NumberOrString `json:"value"`
}

type TrialAssignments struct {
Expand Down

0 comments on commit 0d76b7c

Please sign in to comment.