From e8408055550401683e727a439fb15bdc422d2ff1 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 00:47:00 +0000 Subject: [PATCH 01/20] feat(runtime): hold unordered unique collections as sets and index tensors of any rank A Collection whose elements the library declares unique and not ordered (Set, UniqueCollection, Map) holds them as a set: equal regardless of order, deduplicated, enumerating in a canonical order whenever an ordered operation consumes it, and converted to that sequence when read into an ordered or nonunique feature. Collections the library declares ordered (OrderedSet, OrderedMap, List, Array) or nonunique (Bag) keep their sequence. CollectionFunctions read a Collection object through its elements. Tensor quantities of rank above two construct, index with one index per dimension, and keep their shape through the existing arithmetic; a wrong number of indexes or an index outside a dimension is a typed error. Co-Authored-By: jason.han --- internal/core/runtime/builtins_named.go | 2 +- internal/core/runtime/collections.go | 76 ++- internal/core/runtime/collections_test.go | 8 +- internal/core/runtime/conformance_test.go | 6 + internal/core/runtime/eval.go | 46 +- internal/core/runtime/instance.go | 8 +- internal/core/runtime/robustness_test.go | 14 + internal/core/runtime/set_feature.go | 96 ++++ internal/core/runtime/set_feature_test.go | 256 +++++++++ internal/core/runtime/set_order.go | 104 ++++ internal/core/runtime/shape.go | 2 + internal/core/runtime/subsetting.go | 2 +- internal/core/runtime/tensor_test.go | 57 ++ .../runtime/testdata/conformance/README.md | 4 + ...nsumed_by_ordered_operations.expected.json | 17 + ...c_set_consumed_by_ordered_operations.sysml | 24 + ...onsumed_by_ordered_operations.trace.golden | 80 +++ .../instance_tensor_rank_three.expected.json | 524 ++++++++++++++++++ .../instance_tensor_rank_three.sysml | 55 ++ ...e_tensor_rank_three_failures.expected.json | 102 ++++ .../instance_tensor_rank_three_failures.sysml | 50 ++ .../library_bag_elements.expected.json | 13 + .../conformance/library_bag_elements.sysml | 8 + .../library_map_elements.expected.json | 11 + .../conformance/library_map_elements.sysml | 9 + ...library_ordered_set_elements.expected.json | 12 + .../library_ordered_set_elements.sysml | 8 + .../library_set_elements.expected.json | 12 + .../conformance/library_set_elements.sysml | 9 + ...et_elements_already_distinct.expected.json | 12 + ...ibrary_set_elements_already_distinct.sysml | 7 + .../library_set_elements_empty.expected.json | 8 + .../library_set_elements_empty.sysml | 7 + .../library_set_operations.expected.json | 45 ++ .../conformance/library_set_operations.sysml | 38 ++ ...y_unique_collection_elements.expected.json | 12 + .../library_unique_collection_elements.sysml | 8 + internal/core/runtime/trace.go | 1 - internal/core/runtime/value.go | 50 +- internal/core/runtime/value_test.go | 4 +- internal/repl/runtime_commands_test.go | 2 +- 41 files changed, 1749 insertions(+), 60 deletions(-) create mode 100644 internal/core/runtime/set_feature.go create mode 100644 internal/core/runtime/set_feature_test.go create mode 100644 internal/core/runtime/set_order.go create mode 100644 internal/core/runtime/testdata/conformance/calc_set_consumed_by_ordered_operations.expected.json create mode 100644 internal/core/runtime/testdata/conformance/calc_set_consumed_by_ordered_operations.sysml create mode 100644 internal/core/runtime/testdata/conformance/calc_set_consumed_by_ordered_operations.trace.golden create mode 100644 internal/core/runtime/testdata/conformance/instance_tensor_rank_three.expected.json create mode 100644 internal/core/runtime/testdata/conformance/instance_tensor_rank_three.sysml create mode 100644 internal/core/runtime/testdata/conformance/instance_tensor_rank_three_failures.expected.json create mode 100644 internal/core/runtime/testdata/conformance/instance_tensor_rank_three_failures.sysml create mode 100644 internal/core/runtime/testdata/conformance/library_bag_elements.expected.json create mode 100644 internal/core/runtime/testdata/conformance/library_bag_elements.sysml create mode 100644 internal/core/runtime/testdata/conformance/library_map_elements.expected.json create mode 100644 internal/core/runtime/testdata/conformance/library_map_elements.sysml create mode 100644 internal/core/runtime/testdata/conformance/library_ordered_set_elements.expected.json create mode 100644 internal/core/runtime/testdata/conformance/library_ordered_set_elements.sysml create mode 100644 internal/core/runtime/testdata/conformance/library_set_elements.expected.json create mode 100644 internal/core/runtime/testdata/conformance/library_set_elements.sysml create mode 100644 internal/core/runtime/testdata/conformance/library_set_elements_already_distinct.expected.json create mode 100644 internal/core/runtime/testdata/conformance/library_set_elements_already_distinct.sysml create mode 100644 internal/core/runtime/testdata/conformance/library_set_elements_empty.expected.json create mode 100644 internal/core/runtime/testdata/conformance/library_set_elements_empty.sysml create mode 100644 internal/core/runtime/testdata/conformance/library_set_operations.expected.json create mode 100644 internal/core/runtime/testdata/conformance/library_set_operations.sysml create mode 100644 internal/core/runtime/testdata/conformance/library_unique_collection_elements.expected.json create mode 100644 internal/core/runtime/testdata/conformance/library_unique_collection_elements.sysml diff --git a/internal/core/runtime/builtins_named.go b/internal/core/runtime/builtins_named.go index 15fbdf159..8f0b19c81 100644 --- a/internal/core/runtime/builtins_named.go +++ b/internal/core/runtime/builtins_named.go @@ -15,7 +15,7 @@ func registerNamedOperatorBuiltins() { builtins["BaseFunctions::#"] = builtinBaseIndex builtins["BaseFunctions::,"] = builtinSequenceConcat // CollectionFunctions::'==' is `col1.elements->equals(col2.elements)`. - builtins["CollectionFunctions::=="] = builtinSequenceEquals + builtins["CollectionFunctions::=="] = builtinCollectionEquals // The range, declared abstractly over DataValue and ScalarValue and // concretely over Integer; every level yields the integer sequence. diff --git a/internal/core/runtime/collections.go b/internal/core/runtime/collections.go index 0e8cb5677..04ad78784 100644 --- a/internal/core/runtime/collections.go +++ b/internal/core/runtime/collections.go @@ -66,23 +66,57 @@ func collectionElements(val Value) []Value { // overCollectionElements adapts a sequence operation to the CollectionFunctions // form that the library defines over `col.elements`: an array or vector is -// passed as the charged sequence of its elements, any other value as itself. +// passed as the charged sequence of its elements, a Collection object as what +// its `elements` hold (a set for a Set), any other value as itself. func overCollectionElements(apply builtinFunc) builtinFunc { return func(ec *EvalContext, args []Value) (Value, error) { if len(args) > 0 { - switch args[0].Kind { - case ValArray, ValVector, ValVectorQuantity, ValTensorQuantity: - elements, err := ec.newSequence(collectionElements(args[0])) - if err != nil { - return Value{}, err - } - args = append([]Value{elements}, args[1:]...) + col, err := ec.collectionElementsValue(args[0]) + if err != nil { + return Value{}, err } + args = append([]Value{col}, args[1:]...) } return apply(ec, args) } } +// collectionElementsValue is `col.elements` for a value passed as a Collection: +// the charged sequence of an array's or vector's elements, the collection a +// Collection object's `elements` feature holds, and any other value itself. +func (ec *EvalContext) collectionElementsValue(col Value) (Value, error) { + switch col.Kind { + case ValArray, ValVector, ValVectorQuantity, ValTensorQuantity: + return ec.newSequence(collectionElements(col)) + case ValInstance: + if elements, ok, err := ec.ctx.collectionObjectElements(col); ok || err != nil { + return elements, err + } + } + return col, nil +} + +// collectionObjectElements is what a Collection object's `elements` feature +// reads as; ok is false for a value that is no Collection object. +func (ctx *Context) collectionObjectElements(val Value) (Value, bool, error) { + if val.Kind != ValInstance { + return Value{}, false, nil + } + inst, ok := ctx.Instance(val.Instance) + if !ok || !ctx.specializes(inst.Type, ctx.librarySymbol(collectionType)) { + return Value{}, false, nil + } + if _, ok := inst.FeatureValues[collectionElementsName]; !ok { + return Value{}, false, nil + } + fv, err := inst.GetFeatureValue(ctx, collectionElementsName) + if err != nil { + return Value{}, true, err + } + elements, err := ctx.readFeatureValue(fv, collectionElementsName) + return elements, true, err +} + // elementCount is len(elementsOf(val)) without materializing a scalar's // one-element sequence. func elementCount(val *Value) int64 { @@ -542,6 +576,9 @@ func builtinSequenceEquals(ec *EvalContext, args []Value) (Value, error) { if err := checkArity("SequenceFunctions::equals", args, 2); err != nil { return Value{}, err } + if args[0].Kind == ValSet && args[1].Kind == ValSet { + return boolValue(args[0].Set().Equal(args[1].Set())), nil + } x, y := elementsOf(args[0]), elementsOf(args[1]) if len(x) != len(y) { return boolValue(false), nil @@ -554,6 +591,23 @@ func builtinSequenceEquals(ec *EvalContext, args []Value) (Value, error) { return boolValue(true), nil } +// builtinCollectionEquals is CollectionFunctions::'==', equals over the two +// collections' elements (`col1.elements->equals(col2.elements)`). +func builtinCollectionEquals(ec *EvalContext, args []Value) (Value, error) { + if err := checkArity("CollectionFunctions::'=='", args, 2); err != nil { + return Value{}, err + } + col1, err := ec.collectionElementsValue(args[0]) + if err != nil { + return Value{}, err + } + col2, err := ec.collectionElementsValue(args[1]) + if err != nil { + return Value{}, err + } + return builtinSequenceEquals(ec, []Value{col1, col2}) +} + // builtinSequenceSame is SequenceFunctions::same: the sequences have the same // size and identical (`===`, not `==`) elements at every position, so a // sequence of Integers is not the same as a sequence of equal Reals. @@ -783,7 +837,11 @@ func builtinCollectionContainsAll(ec *EvalContext, args []Value) (Value, error) if err := checkArity("CollectionFunctions::containsAll", args, 2); err != nil { return Value{}, err } - return boolValue(includesAll(elementsOf(args[0]), collectionElements(args[1]))), nil + col2, err := ec.collectionElementsValue(args[1]) + if err != nil { + return Value{}, err + } + return boolValue(includesAll(elementsOf(args[0]), collectionElements(col2))), nil } // builtinControlSelect is ControlFunctions::select, the elements the selector diff --git a/internal/core/runtime/collections_test.go b/internal/core/runtime/collections_test.go index 8d68429df..b5fadedbd 100644 --- a/internal/core/runtime/collections_test.go +++ b/internal/core/runtime/collections_test.go @@ -439,8 +439,8 @@ package test { } // TestCollectionOperationsOverSets pins that the operations read a set as the -// sequence of its elements, in the order the set was built in, so a model -// iterating or filtering a set gets a stable answer rather than a hash order. +// sequence of its elements in canonical order, whatever order the set was built +// in, so a model iterating or filtering a set gets the same answer for equal sets. func TestCollectionOperationsOverSets(t *testing.T) { set := NewSet() for _, n := range []int64{3, 1, 2, 3} { @@ -448,8 +448,8 @@ func TestCollectionOperationsOverSets(t *testing.T) { } setVal := NewSetValue(set) - if got := intsOf(t, sequenceOf(elementsOf(setVal))); !equalInts(got, []int64{3, 1, 2}) { - t.Fatalf("set elements = %v, want the distinct elements in insertion order", got) + if got := intsOf(t, sequenceOf(elementsOf(setVal))); !equalInts(got, []int64{1, 2, 3}) { + t.Fatalf("set elements = %v, want the distinct elements in canonical order", got) } size, err := builtinSequenceSize(nil, []Value{setVal}) diff --git a/internal/core/runtime/conformance_test.go b/internal/core/runtime/conformance_test.go index a37413ba9..1017e9cb2 100644 --- a/internal/core/runtime/conformance_test.go +++ b/internal/core/runtime/conformance_test.go @@ -1665,6 +1665,12 @@ func validateValue(t reporter, ctx *Context, name string, expected ExpectedValue for i, want := range expected.Elements { validateValue(t, ctx, fmt.Sprintf("%s#(%d)", name, i+1), want, elements[i]) } + case "Set": + if actual.Kind != ValSet || actual.Set() == nil { + t.Errorf("%s: type = %v, want Set", name, actual.Kind) + return + } + validateElements(t, ctx, name, expected.Elements, actual.Set().Elements()) case "Vector": if actual.Kind != ValVector || actual.Vector() == nil { t.Errorf("%s: type = %v, want Vector", name, actual.Kind) diff --git a/internal/core/runtime/eval.go b/internal/core/runtime/eval.go index f0f36f869..7889e6f67 100644 --- a/internal/core/runtime/eval.go +++ b/internal/core/runtime/eval.go @@ -892,7 +892,7 @@ func (ec *EvalContext) declaredValue(sym *symbols.Symbol, value ast.Node) (Value if err := ec.ctx.classifyHeld(sym, val); err != nil { return Value{}, fmt.Errorf("%s: %w", what, err) } - return ec.bindVariationOf(sym, ec.ctx.classifiedFrame(sym, val)) + return ec.bindVariationOf(sym, ec.ctx.classifiedFrame(sym, ec.ctx.declaredCollection(sym, val))) } // occurrenceReference evaluates a name denoting one object — an occurrence or a @@ -1947,6 +1947,17 @@ func (ctx *Context) equalityValues(op ast.OperatorKind, left, right Value) (Valu return equalQuantities(op, lq, rq) } + // Two Collection objects compare by their elements (CollectionFunctions::'=='). + if lc, ok, err := ctx.collectionObjectElements(left); err != nil { + return Value{}, err + } else if ok { + if rc, ok, err := ctx.collectionObjectElements(right); err != nil { + return Value{}, err + } else if ok { + left, right = lc, rc + } + } + equal := valueEqual(left, right) if op == ast.OpNeq { equal = !equal @@ -2619,6 +2630,11 @@ func valueEqual(a, b Value) bool { if a.Kind == ValComplex || b.Kind == ValComplex { return complexEqual(a, b) } + // A set compared with a sequence flows into the ordered context: its + // canonical sequence is compared. + if a.Kind == ValSet && b.Kind == ValSequence || a.Kind == ValSequence && b.Kind == ValSet { + return sequenceEqual(sequenceOf(elementsOf(a)).Sequence(), sequenceOf(elementsOf(b)).Sequence()) + } if a.Kind != b.Kind { return false } @@ -2636,7 +2652,7 @@ func valueEqual(a, b Value) bool { case ValSequence: return sequenceEqual(a.Sequence(), b.Sequence()) case ValSet: - return setEqual(a.Set(), b.Set()) + return a.Set().Equal(b.Set()) case ValVariant: // A variation compares equal to the variant it selected. return a.Variant() == b.Variant() @@ -2755,29 +2771,3 @@ func sequenceEqual(a, b *Sequence) bool { } return true } - -// setEqual checks set equality as an unordered multiset of exact values. -func setEqual(a, b *Set) bool { - if a == nil || b == nil { - return a == b - } - if a.Size() != b.Size() { - return false - } - used := make([]bool, b.Size()) - rights := b.Elements() - for _, left := range a.Elements() { - found := false - for i, right := range rights { - if !used[i] && valueEqual(left, right) { - used[i] = true - found = true - break - } - } - if !found { - return false - } - } - return true -} diff --git a/internal/core/runtime/instance.go b/internal/core/runtime/instance.go index 6b7741efc..24db51541 100644 --- a/internal/core/runtime/instance.go +++ b/internal/core/runtime/instance.go @@ -111,7 +111,7 @@ func (s *FeatureValue) ReadValue(name string) (Value, error) { return value, nil } if lower := s.Feature.Multiplicity.Lower; lower.Known && !lower.Infinite && lower.Value == 0 { - return sequenceOf(nil), nil + return collectionOf(s.Feature, nil), nil } return Value{}, fmt.Errorf("%w: %s", ErrUninitializedFeatureValue, name) } @@ -464,14 +464,14 @@ func (feat *EffectiveFeature) heldBy() *symbols.Symbol { // admitted is the value an admitted val is stored as: a collection for a multi-valued // feature, its elements charged, the objects a declared value holds classified by the feature. func (ctx *Context) admitted(feat *EffectiveFeature, val Value, how admission) (Value, error) { - if !feat.Scalar() && val.Kind != ValSequence && val.Kind != ValSet { + if !feat.Scalar() && (val.Kind != ValSequence && val.Kind != ValSet || feat.HoldsSet != (val.Kind == ValSet)) { // A multi-valued feature holds a collection however it was written, so a // single value written to one is that collection's one element. elements := elementsOf(val) if err := ctx.chargeElements(int64(len(elements))); err != nil { return Value{}, err } - val = sequenceOf(elements) + val = collectionOf(feat, elements) } else if feat.Scalar() && (val.Kind == ValSequence || val.Kind == ValSet) { // A scalar feature holds the one element of a one-element collection. if elements := elementsOf(val); len(elements) == 1 { @@ -759,7 +759,7 @@ func (inst *Instance) materializeIntrinsic(ctx *Context, fv *FeatureValue, name seq.Append(Value{Kind: ValInstance, Instance: childInst.ID}) children = append(children, childInst) } - fv.Values = NewSequenceValue(seq) + fv.Values = collectionOf(fv.Feature, seq.Elements()) fv.Materialized = true if err := ctx.startClassifierBehaviorsOf(children, mark); err != nil { return fail(err) diff --git a/internal/core/runtime/robustness_test.go b/internal/core/runtime/robustness_test.go index 51e6318ab..8f887653b 100644 --- a/internal/core/runtime/robustness_test.go +++ b/internal/core/runtime/robustness_test.go @@ -1418,6 +1418,17 @@ func testTensorQuantityFailureModes(t *testing.T) { {"tensor times tensor by operator", "stress * stress", ErrUnevaluableLibraryFunction, "TensorCalculations::tensorTensorMult"}, {"outer product", "VectorCalculations::outer(VectorFunctions::VectorOf((1.0, 2.0)) [Pa], VectorFunctions::VectorOf((1.0, 2.0)) [Pa])", ErrUnevaluableLibraryFunction, "VectorCalculations::outer"}, {"transform", "TensorCalculations::transform(stressRef, stress)", ErrUnevaluableLibraryFunction, "TensorCalculations::transform"}, + {"rank three, too few indexes", "cube#(1, 2)", ErrMultiplicityViolation, "2 indexes address an array of rank 3"}, + {"rank three, too many indexes", "cube#(1, 1, 1, 1)", ErrMultiplicityViolation, "4 indexes address an array of rank 3"}, + {"rank three, first index low", "cube#(0, 1, 1)", ErrIndexOutOfRange, "index 1 is 0, dimension 1 has 1..2"}, + {"rank three, middle index high", "cube#(1, 3, 1)", ErrIndexOutOfRange, "index 2 is 3, dimension 2 has 1..2"}, + {"rank three, last index high", "cube#(1, 1, 3)", ErrIndexOutOfRange, "index 3 is 3, dimension 3 has 1..2"}, + {"rank three, non-integer index", "cube#(1, 1.5, 1)", ErrTypeMismatch, "requires an Integer index"}, + {"rank three, too few components", "TensorCalculations::'['((1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0), cubeRef)", ErrMultiplicityViolation, "7 elements for a reference of dimensions [2, 2, 2]"}, + {"rank three, too many components", "TensorCalculations::'['((1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0), cubeRef)", ErrMultiplicityViolation, "9 elements for a reference of dimensions [2, 2, 2]"}, + {"rank three against rank two", "cube + stress", ErrMultiplicityViolation, "dimensions [2, 2, 2] and [2, 2] differ"}, + {"rank three shapes differ", "cube - TensorCalculations::'['((1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0), slabRef)", ErrMultiplicityViolation, "dimensions [2, 2, 2] and [2, 3, 2] differ"}, + {"rank three unit predicate", "TensorCalculations::isUnitTensorQuantity(cube)", ErrUnevaluableLibraryFunction, "only a square tensor of order two has an identity"}, } { t.Run(tc.name, func(t *testing.T) { src := fmt.Sprintf(` @@ -1427,6 +1438,9 @@ func testTensorQuantityFailureModes(t *testing.T) { private import MeasurementReferences::*; private import Quantities::*; attribute stressRef : TensorMeasurementReference { :>> dimensions = (2, 2); :>> mRefs = (Pa, Pa, Pa, Pa); } + attribute cubeRef : TensorMeasurementReference { :>> dimensions = (2, 2, 2); :>> mRefs = (Pa, Pa, Pa, Pa, Pa, Pa, Pa, Pa); } + attribute slabRef : TensorMeasurementReference { :>> dimensions = (2, 3, 2); :>> mRefs = (Pa, Pa, Pa, Pa, Pa, Pa, Pa, Pa, Pa, Pa, Pa, Pa); } + attribute cube = TensorCalculations::'['((1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0), cubeRef); attribute lengthRef : TensorMeasurementReference { :>> dimensions = (2, 2); :>> mRefs = (Pa, m, Pa, Pa); } attribute rowRef : TensorMeasurementReference { :>> dimensions = (3); :>> mRefs = (Pa, Pa, Pa); } attribute oneRef : TensorMeasurementReference { :>> dimensions = (2, 2); :>> mRefs = Pa; } diff --git a/internal/core/runtime/set_feature.go b/internal/core/runtime/set_feature.go new file mode 100644 index 000000000..d2e28f165 --- /dev/null +++ b/internal/core/runtime/set_feature.go @@ -0,0 +1,96 @@ +package runtime + +import ( + "github.com/Open-MBEE/OpenSysML/internal/core/ast" + "github.com/Open-MBEE/OpenSysML/internal/core/semantics" + "github.com/Open-MBEE/OpenSysML/internal/core/symbols" +) + +// The library Collections declare `elements` once as nonunique, at the root, +// and redefine it unique (UniqueCollection, Map) or ordered (OrderedCollection). +const ( + collectionType = "Collections::Collection" + collectionElementsName = "elements" + collectionElementsFeature = collectionType + "::" + collectionElementsName + orderedCollectionType = "Collections::OrderedCollection" +) + +// holdsSet reports whether a multi-valued feature of typeSym holds a set rather +// than a sequence. That is `elements` of a library Collection whose own +// redefinition of it is unique and not ordered — Set's and Map's, not Bag's, +// which inherits the nonunique root, and not OrderedSet's or OrderedMap's — +// unless the feature itself is declared ordered or nonunique. +func (ctx *Context) holdsSet(feat, typeSym *symbols.Symbol, mult semantics.Range) bool { + if feat == nil || ctx.model == nil || !mult.Upper.Infinite && mult.Upper.Value <= 1 { + return false + } + if declaredOrderedOrNonunique(feat) { + return false + } + root := ctx.librarySymbol(collectionElementsFeature) + if root == nil || !ctx.specializes(feat, root) { + return false + } + if ctx.specializes(typeSym, ctx.librarySymbol(orderedCollectionType)) { + return false + } + redefined := ctx.libraryDeclared(feat) && feat != root + for _, sup := range ctx.model.AllSupertypes(feat) { + if sup == root || !ctx.libraryDeclared(sup) || !ctx.specializes(sup, root) { + continue + } + if declaredOrderedOrNonunique(sup) { + return false + } + redefined = true + } + return redefined +} + +// declaredOrderedOrNonunique reports whether a feature's own declaration says +// ordered or nonunique. +func declaredOrderedOrNonunique(feat *symbols.Symbol) bool { + usage, ok := feat.Decl.(*ast.Usage) + return ok && (usage.IsOrdered || usage.IsNonunique) +} + +// specializes reports whether sym is general, or has it among its supertypes. +func (ctx *Context) specializes(sym, general *symbols.Symbol) bool { + if sym == nil || general == nil { + return false + } + if sym == general { + return true + } + for _, sup := range ctx.model.AllSupertypes(sym) { + if sup == general { + return true + } + } + return false +} + +// collectionOf is the collection a multi-valued feature holds the elements as. +func collectionOf(feat *EffectiveFeature, elements []Value) Value { + if feat.HoldsSet { + return setOf(elements) + } + return sequenceOf(elements) +} + +// declaredCollection is a collection read through a multi-valued declaration, as +// the kind that declaration holds: a set flowing into a sequence is its canonical +// sequence, a sequence flowing into a set its distinct elements. +func (ctx *Context) declaredCollection(sym *symbols.Symbol, val Value) Value { + if val.Kind != ValSet && val.Kind != ValSequence { + return val + } + mult := ctx.featureMultiplicity(sym, nil) + if !mult.Upper.Infinite && mult.Upper.Value <= 1 { + return val + } + if holds := ctx.holdsSet(sym, ctx.findOwnerType(sym), mult); holds != (val.Kind == ValSet) { + return collectionOf(&EffectiveFeature{HoldsSet: holds}, elementsOf(val)) + } + return val +} diff --git a/internal/core/runtime/set_feature_test.go b/internal/core/runtime/set_feature_test.go new file mode 100644 index 000000000..cf232b7ff --- /dev/null +++ b/internal/core/runtime/set_feature_test.go @@ -0,0 +1,256 @@ +package runtime + +import ( + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/core/semantics" + "github.com/Open-MBEE/OpenSysML/internal/core/symbols" +) + +// setModel declares one of each library Collection, valued from the same +// repeating numbers, and features a Set's elements flow into. +const setModel = `package test { + private import ScalarValues::*; + private import Collections::*; + private import CollectionFunctions::*; + private import ControlFunctions::*; + private import SequenceFunctions::*; + + attribute s : Set { :>> elements = (3, 1, 2, 2, 3); } + attribute t : Set { :>> elements = (2, 3, 1); } + attribute e : Set { :>> elements = (); } + attribute u : UniqueCollection { :>> elements = (3, 1, 2, 2); } + attribute kv1 : KeyValuePair { :>> key = 1; :>> val = "a"; } + attribute kv2 : KeyValuePair { :>> key = 2; :>> val = "b"; } + attribute m : Map { :>> elements = (kv1, kv2, kv1); } + attribute om : OrderedMap { :>> elements = (kv2, kv1); } + attribute b : Bag { :>> elements = (3, 1, 2, 2); } + attribute os : OrderedSet { :>> elements = (3, 1, 2); } + attribute l : List { :>> elements = (3, 1, 2, 2); } + attribute arr : Array { :>> elements = (3, 1, 2, 2); :>> dimensions = (4); } + attribute nested : Set { :>> elements = (s, e, s); } + attribute mixed : Set { :>> elements = (2, "a", true, 1.5, 1, "a"); } + + attribute plain : Integer[*] = s.elements; + attribute ordered : Integer[*] ordered = s.elements; + attribute repeatable : Integer[*] nonunique = s.elements; + attribute fromSet : List { :>> elements = s.elements; } + attribute fromList : Set { :>> elements = l.elements; } + attribute fromBag : Set { :>> elements = b.elements; } + + part def P { attribute ints : Integer[*]; attribute ord : Integer[*] ordered; attribute chosen : Set; } + part p : P { :>> ints = s.elements; :>> ord = s.elements; :>> chosen { :>> elements = l.elements; } } +}` + +func setModelContext(t *testing.T) (*Context, *symbols.Scope) { + t.Helper() + ctx, idx := libraryModelContext(t, setModel) + pkg, ok := idx.DocumentRoot("").LookupLocal("test") + if !ok { + t.Fatal("package test not found") + } + return ctx, pkg.Scope +} + +func mustEvalIn(t *testing.T, ctx *Context, scope *symbols.Scope, src string) Value { + t.Helper() + val, err := evalIn(t, ctx, scope, src) + if err != nil { + t.Fatalf("%s: %v", src, err) + } + return val +} + +// TestCollectionElementsHoldTheLibraryKind pins which library Collections hold +// their elements as a set: those the library declares unique and not ordered. +// Everything ordered or nonunique stays the sequence it was written as. +func TestCollectionElementsHoldTheLibraryKind(t *testing.T) { + ctx, scope := setModelContext(t) + sets := map[string][]int64{ + "s.elements": {1, 2, 3}, + "t.elements": {1, 2, 3}, + "e.elements": {}, + "u.elements": {1, 2, 3}, + } + for src, want := range sets { + val := mustEvalIn(t, ctx, scope, src) + if val.Kind != ValSet { + t.Errorf("%s: kind = %v, want a set", src, val.Kind) + continue + } + if got := intsOf(t, sequenceOf(elementsOf(val))); !equalInts(got, want) { + t.Errorf("%s = %v, want %v", src, got, want) + } + } + sequences := map[string][]int64{ + "b.elements": {3, 1, 2, 2}, + "os.elements": {3, 1, 2}, + "l.elements": {3, 1, 2, 2}, + "arr.elements": {3, 1, 2, 2}, + } + for src, want := range sequences { + val := mustEvalIn(t, ctx, scope, src) + if val.Kind != ValSequence { + t.Errorf("%s: kind = %v, want a sequence", src, val.Kind) + continue + } + if got := intsOf(t, val); !equalInts(got, want) { + t.Errorf("%s = %v, want %v", src, got, want) + } + } + for src, kind := range map[string]ValueKind{"m.elements": ValSet, "om.elements": ValSequence} { + val := mustEvalIn(t, ctx, scope, src) + if val.Kind != kind || len(elementsOf(val)) != 2 { + t.Errorf("%s = %s, want two pairs as a %v", src, FormatValue(val), kind) + } + } + if val := mustEvalIn(t, ctx, scope, "nested.elements"); val.Kind != ValSet || val.Set().Size() != 2 { + t.Errorf("nested.elements = %s, want a set of the two distinct sets", FormatValue(val)) + } +} + +// TestSetFlowsIntoDeclaredCollections pins the boundary rule: a set read into a +// feature the runtime holds as a sequence — ordered, nonunique, a plain +// `Integer[*]`, a List's elements, an inherited or redefined feature — becomes +// its canonical sequence, and a sequence read into a Set's elements becomes the +// set of its distinct elements. +func TestSetFlowsIntoDeclaredCollections(t *testing.T) { + ctx, scope := setModelContext(t) + for _, src := range []string{"plain", "ordered", "repeatable", "fromSet.elements", "p.ints", "p.ord"} { + val := mustEvalIn(t, ctx, scope, src) + if val.Kind != ValSequence { + t.Errorf("%s: kind = %v, want a sequence", src, val.Kind) + continue + } + if got := intsOf(t, val); !equalInts(got, []int64{1, 2, 3}) { + t.Errorf("%s = %v, want the set's canonical sequence (1, 2, 3)", src, got) + } + } + for _, src := range []string{"fromList.elements", "fromBag.elements", "p.chosen.elements"} { + val := mustEvalIn(t, ctx, scope, src) + if val.Kind != ValSet { + t.Errorf("%s: kind = %v, want a set", src, val.Kind) + continue + } + if got := intsOf(t, sequenceOf(elementsOf(val))); !equalInts(got, []int64{1, 2, 3}) { + t.Errorf("%s = %v, want the distinct elements {1, 2, 3}", src, got) + } + } +} + +// TestCollectionFunctionsOverCollectionObjects pins that CollectionFunctions +// read a Collection object through its elements, whichever kind it holds. +func TestCollectionFunctionsOverCollectionObjects(t *testing.T) { + ctx, scope := setModelContext(t) + ints := map[string]int64{ + "size(s)": 3, "size(e)": 0, "size(b)": 4, "size(m)": 2, + "CollectionFunctions::head(s)": 1, "CollectionFunctions::last(s)": 3, + } + for src, want := range ints { + if val := mustEvalIn(t, ctx, scope, src); val.Kind != ValConst || val.Const.Int != want { + t.Errorf("%s = %s, want %d", src, FormatValue(val), want) + } + } + bools := map[string]bool{ + "isEmpty(e)": true, "isEmpty(s)": false, "notEmpty(s)": true, "notEmpty(e)": false, + "contains(s, 2)": true, "contains(s, 5)": false, "contains(e, 2)": false, + "containsAll(s, t)": true, "containsAll(t, s)": true, "containsAll(s, e)": true, "containsAll(s, b)": true, + "containsAll(e, s)": false, "containsAll(s, os)": true, + "s == t": true, "t == s": true, "s == e": false, "s != e": true, "e == e": true, + "s == os": false, "s == b": false, "os == os": true, + "s.elements == t.elements": true, "s.elements != e.elements": true, + } + for src, want := range bools { + if val := mustEvalIn(t, ctx, scope, src); val.Kind != ValConst || val.Const.Kind != semantics.ValBool || val.Const.Bool != want { + t.Errorf("%s = %s, want %v", src, FormatValue(val), want) + } + } + if got := intsOf(t, mustEvalIn(t, ctx, scope, "CollectionFunctions::tail(s)")); !equalInts(got, []int64{2, 3}) { + t.Errorf("tail(s) = %v, want (2, 3)", got) + } + if val := mustEvalIn(t, ctx, scope, "CollectionFunctions::head(e)"); val.Kind != ValNull { + t.Errorf("head(e) = %s, want null", FormatValue(val)) + } +} + +// TestSetAgainstSequenceComparesCanonically pins that a set meeting a sequence +// in `==` is its canonical sequence: equal to the elements in canonical order, +// not to the order the set happened to be written in. +func TestSetAgainstSequenceComparesCanonically(t *testing.T) { + ctx, scope := setModelContext(t) + for src, want := range map[string]bool{ + "s.elements == (1, 2, 3)": true, + "(1, 2, 3) == s.elements": true, + "s.elements == (3, 1, 2)": false, + "s.elements == (1, 2, 2, 3)": false, + "e.elements == ()": true, + "s.elements->head() == 1": true, + "s.elements#(3) == 3": true, + } { + if val := mustEvalIn(t, ctx, scope, src); val.Kind != ValConst || val.Const.Bool != want { + t.Errorf("%s = %s, want %v", src, FormatValue(val), want) + } + } + if got := intsOf(t, mustEvalIn(t, ctx, scope, "s.elements->select{in x; x > 1}")); !equalInts(got, []int64{2, 3}) { + t.Errorf("select over a set = %v, want (2, 3)", got) + } +} + +// TestSetRendersCanonically pins the value and trace forms of a set over +// several value classes: Booleans, numbers, then strings, never map order. +func TestSetRendersCanonically(t *testing.T) { + ctx, scope := setModelContext(t) + val := mustEvalIn(t, ctx, scope, "mixed.elements") + if got, want := FormatTraceValue(val), `{true, 1, 1.5, 2, "a"}`; got != want { + t.Errorf("trace = %s, want %s", got, want) + } + if got, want := FormatValue(val), `Set{true, 1, 1.5, 2, "a"}`; got != want { + t.Errorf("value = %s, want %s", got, want) + } + if got, want := FormatTraceValue(mustEvalIn(t, ctx, scope, "e.elements")), "{}"; got != want { + t.Errorf("empty trace = %s, want %s", got, want) + } +} + +// TestCanonicalOrderIsTotal pins the order over one class of values that has +// no order of its own: quantities of different dimensions group by dimension, +// then by magnitude; integers beyond a double's precision stay distinct and +// ordered; and the same members added in any order enumerate alike. +func TestCanonicalOrderIsTotal(t *testing.T) { + metre, second := &symbols.Symbol{Name: "metre"}, &symbols.Symbol{Name: "second"} + unit := func(text string, scale float64, base *symbols.Symbol) Unit { + return Unit{Text: text, Term: semantics.UnitTerm{Scale: semantics.UnitScale(scale), Factors: []semantics.UnitFactor{{Unit: base, Exponent: 1}}}} + } + quantity := func(n int64, u Unit) Value { + return NewQuantityValue(&Quantity{Num: semantics.Value{Kind: semantics.ValInt, Int: n}, Unit: u}) + } + km, m, s := unit("km", 1000, metre), unit("m", 1, metre), unit("s", 1, second) + members := []Value{quantity(2, km), quantity(500, m), quantity(1, s), quantity(3, m)} + want := []string{"3 [m]", "500 [m]", "2 [km]", "1 [s]"} + for _, order := range [][]int{{0, 1, 2, 3}, {3, 2, 1, 0}, {2, 0, 3, 1}} { + set := NewSet() + for _, i := range order { + set.Add(members[i]) + } + var got []string + for _, elem := range set.Elements() { + got = append(got, FormatTraceValue(elem)) + } + if len(got) != len(want) { + t.Fatalf("added in %v: %v, want %v", order, got, want) + } + for i := range want { + if got[i] != want[i] { + t.Errorf("added in %v: element %d = %s, want %s", order, i, got[i], want[i]) + } + } + } + + big := NewSet() + for _, n := range []int64{1 << 53, 1<<53 + 1, 1<<53 - 1} { + big.Add(Value{Kind: ValConst, Const: semantics.Value{Kind: semantics.ValInt, Int: n}}) + } + if got := intsOf(t, sequenceOf(big.Elements())); !equalInts(got, []int64{1<<53 - 1, 1 << 53, 1<<53 + 1}) { + t.Errorf("large integers = %v, want them ascending", got) + } +} diff --git a/internal/core/runtime/set_order.go b/internal/core/runtime/set_order.go new file mode 100644 index 000000000..3f3e4840e --- /dev/null +++ b/internal/core/runtime/set_order.go @@ -0,0 +1,104 @@ +package runtime + +import ( + "math" + + "github.com/Open-MBEE/OpenSysML/internal/core/semantics" +) + +// canonicalLess is the total order a set enumerates its elements in, so that +// every operation consuming a set — a trace rendering it, `collect` over it, a +// sequence it flows into — sees equal sets alike. Values order by class first +// (null, Booleans, numbers, complex numbers, strings, quantities, enumeration +// literals, objects, then every other kind), then within a class by their own +// order where they have one (numeric, lexicographic, dimension then magnitude, +// object identity) and by their trace rendering otherwise. +func canonicalLess(a, b Value) bool { + ca, cb := canonicalClass(a), canonicalClass(b) + if ca != cb { + return ca < cb + } + switch ca { + case classBool: + return !a.Const.Bool && b.Const.Bool + case classNumber: + return numberLess(a.Const, b.Const) + case classComplex: + x, y := a.Complex(), b.Complex() + if real(x) != real(y) { + return real(x) < real(y) + } + if imag(x) != imag(y) { + return imag(x) < imag(y) + } + case classString: + return a.Str() < b.Str() + case classQuantity: + qa, qb := a.Quantity(), b.Quantity() + if da, db := qa.Unit.Term.DimensionKey(), qb.Unit.Term.DimensionKey(); da != db { + return da < db + } + if c, err := semantics.CompareMagnitudes(*qa, *qb); err == nil && c != 0 { + return c < 0 + } + case classObject: + if a.Instance != b.Instance { + return a.Instance < b.Instance + } + } + return FormatTraceValue(a) < FormatTraceValue(b) +} + +const ( + classNull = iota + classBool + classNumber + classComplex + classString + classQuantity + classEnumLiteral + classObject + classOther +) + +// canonicalClass groups values so that only values with an order of their own +// are compared by it. +func canonicalClass(v Value) int { + switch v.Kind { + case ValNull, ValInvalid: + return classNull + case ValConst: + if v.Const.Kind == semantics.ValBool { + return classBool + } + return classNumber + case ValComplex: + return classComplex + case ValString: + return classString + case ValQuantity: + if v.Quantity() != nil { + return classQuantity + } + case ValEnumLiteral: + return classEnumLiteral + case ValInstance, ValVariant: + return classObject + } + return classOther +} + +// numberLess orders the numeric constants, infinity above every finite number. +func numberLess(a, b semantics.Value) bool { + if a.Kind == semantics.ValInt && b.Kind == semantics.ValInt { + return a.Int < b.Int + } + return numberOf(a) < numberOf(b) +} + +func numberOf(v semantics.Value) float64 { + if v.Kind == semantics.ValInfinity { + return math.Inf(1) + } + return v.AsReal() +} diff --git a/internal/core/runtime/shape.go b/internal/core/runtime/shape.go index 4f43b0f1d..873b05338 100644 --- a/internal/core/runtime/shape.go +++ b/internal/core/runtime/shape.go @@ -16,6 +16,7 @@ type EffectiveFeature struct { Multiplicity semantics.Range // declared or inherited (default 1..1) DefaultValue ast.Node // value-binding expression (nil if none) DefaultDecl *symbols.Symbol // feature the DefaultValue was written on (nil if none) + HoldsSet bool // values form a set: a Collection's unordered unique elements } // Scalar reports whether the feature holds at most one value. An unbounded @@ -111,6 +112,7 @@ func (ctx *Context) buildFeatures(typeSym *symbols.Symbol) []EffectiveFeature { Multiplicity: mult, DefaultValue: defaultVal, DefaultDecl: defaultDecl, + HoldsSet: ctx.holdsSet(memberSym, typeSym, mult), }) } return append(result, ctx.connectorEndFeatures(typeSym, seenNames)...) diff --git a/internal/core/runtime/subsetting.go b/internal/core/runtime/subsetting.go index b80f3a120..a802629aa 100644 --- a/internal/core/runtime/subsetting.go +++ b/internal/core/runtime/subsetting.go @@ -474,7 +474,7 @@ func (ctx *Context) fillOptionalSubsetters(inst *Instance, name string, n int) ( if fill.fv.Feature.Scalar() { fill.fv.Value = fill.held[0] } else { - fill.fv.Values = sequenceOf(fill.held) + fill.fv.Values = collectionOf(fill.fv.Feature, fill.held) } fill.fv.Materialized = true ctx.invalidateDependents(fill.fv) diff --git a/internal/core/runtime/tensor_test.go b/internal/core/runtime/tensor_test.go index c0484a66e..f8287eafb 100644 --- a/internal/core/runtime/tensor_test.go +++ b/internal/core/runtime/tensor_test.go @@ -297,3 +297,60 @@ func TestWriteOfEqualTensorComponentsOverAnotherReferenceRecomputesDerivedValues t.Fatal("writing the same tensor over the same reference again unmaterialized its readers") } } + +// TestTensorQuantityRankThree: a tensor of rank three constructs, indexes with +// one index per dimension in row-major order, keeps its shape through the +// arithmetic, renders canonically, and is equal to a same-shape tensor of the +// same components only. +func TestTensorQuantityRankThree(t *testing.T) { + ctx, idx := libraryModelContext(t, `package test { + private import ISQ::*; + private import SI::*; + private import MeasurementReferences::*; + private import Quantities::*; + attribute cubeRef : TensorMeasurementReference { + :>> dimensions = (2, 2, 2); + :>> mRefs = (Pa, Pa, Pa, Pa, Pa, Pa, Pa, Pa); + } + attribute cube = TensorCalculations::'['((1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0), cubeRef); + }`) + pkg, _ := idx.DocumentRoot("").LookupLocal("test") + scope := pkg.Scope + cube := tensorEval(t, ctx, scope, "cube") + if cube.Kind != ValTensorQuantity || cube.TensorQuantity().Rank() != 3 || cube.TensorQuantity().FlattenedSize() != 8 { + t.Fatalf("cube = %s, want a rank-3 tensor of 8 components", FormatValue(cube)) + } + if got, want := FormatTraceValue(cube), "Tensor(2, 2, 2)[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0] [Pa]"; got != want { + t.Errorf("trace = %q, want %q", got, want) + } + for src, want := range map[string]float64{ + "cube#(1, 1, 1)": 1, "cube#(1, 1, 2)": 2, "cube#(1, 2, 1)": 3, "cube#(2, 1, 1)": 5, "cube#(2, 2, 2)": 8, + "(cube + cube)#(2, 1, 2)": 12, "(2 * cube)#(1, 2, 2)": 8, "(cube - cube)#(2, 2, 1)": 0, + } { + val := tensorEval(t, ctx, scope, src) + if val.Kind != ValQuantity || val.Quantity().Unit.Text != "Pa" || numberOf(val.Quantity().Num) != want { + t.Errorf("%s = %s, want %v [Pa]", src, FormatValue(val), want) + } + } + for _, src := range []string{"cube + cube", "2 * cube", "cube - cube"} { + val := tensorEval(t, ctx, scope, src) + if val.Kind != ValTensorQuantity || !strings.HasPrefix(FormatValue(val), "Tensor(2, 2, 2)[") { + t.Errorf("%s = %s, want the shape kept", src, FormatValue(val)) + } + } + tq := cube.TensorQuantity() + if !valueEqual(cube, NewTensorQuantityValue([]int64{2, 2, 2}, tq.Num, tq.Units)) { + t.Error("a same-shape tensor of the same components is not equal") + } + if valueEqual(cube, NewTensorQuantityValue([]int64{2, 4}, tq.Num, tq.Units)) { + t.Error("a reshaped tensor of the same components is equal") + } + for src, want := range map[string]error{ + "cube#(1, 2)": ErrMultiplicityViolation, "cube#(1, 1, 1, 1)": ErrMultiplicityViolation, + "cube#(0, 1, 1)": ErrIndexOutOfRange, "cube#(1, 1, 3)": ErrIndexOutOfRange, + } { + if _, err := evalIn(t, ctx, scope, src); !errors.Is(err, want) { + t.Errorf("%s: err = %v, want %v", src, err, want) + } + } +} diff --git a/internal/core/runtime/testdata/conformance/README.md b/internal/core/runtime/testdata/conformance/README.md index 837e3df97..18657ecd6 100644 --- a/internal/core/runtime/testdata/conformance/README.md +++ b/internal/core/runtime/testdata/conformance/README.md @@ -368,6 +368,10 @@ Supported types: (`{"type": "Complex", "value": 0.0, "im": 1.0}`) - `Sequence`: the `elements` it holds, in order, instead of `value` — for a multi-valued feature, whose order is part of its contract +- `Set`: the distinct `elements` it holds, in the canonical order a set + enumerates in (booleans, numbers, strings, quantities, enumeration literals, + objects; each class in its own order) — for a `Collections::Set`'s elements, + or any other feature the library declares unique and unordered - `Instance`: an object, whose identity a case does not pin (no `value`) - `Unset`: a valueless feature of a value type, holding no value (no `value`) - `Variant`: the name of the variant a variation feature is bound to, as a JSON diff --git a/internal/core/runtime/testdata/conformance/calc_set_consumed_by_ordered_operations.expected.json b/internal/core/runtime/testdata/conformance/calc_set_consumed_by_ordered_operations.expected.json new file mode 100644 index 000000000..904e0b170 --- /dev/null +++ b/internal/core/runtime/testdata/conformance/calc_set_consumed_by_ordered_operations.expected.json @@ -0,0 +1,17 @@ +{ + "type": "calc", + "libraries": true, + "trace": true, + "inputs": [], + "result": {"type": "Sequence", "elements": [ + {"type": "Integer", "value": 5}, + {"type": "Integer", "value": 15}, + {"type": "Integer", "value": 25}, + {"type": "Integer", "value": 10}, + {"type": "Integer", "value": 20}, + {"type": "Integer", "value": 30}, + {"type": "Integer", "value": 20}, + {"type": "Integer", "value": 0}, + {"type": "Integer", "value": 1} + ]} +} diff --git a/internal/core/runtime/testdata/conformance/calc_set_consumed_by_ordered_operations.sysml b/internal/core/runtime/testdata/conformance/calc_set_consumed_by_ordered_operations.sysml new file mode 100644 index 000000000..f1f6b6bf0 --- /dev/null +++ b/internal/core/runtime/testdata/conformance/calc_set_consumed_by_ordered_operations.sysml @@ -0,0 +1,24 @@ +// A set has no order of its own, so an operation that walks its elements in +// order — collect, head, tail, `#`, `==` against a sequence — sees them in the +// canonical order every set enumerates in: Booleans, then numbers ascending, +// then strings, quantities, enumeration literals and objects. The same set +// written in another order answers the same, so the trace of this calc is the +// golden for that order. +package test { + private import ScalarValues::*; + private import Collections::*; + private import ControlFunctions::*; + private import SequenceFunctions::*; + + attribute s : Set { :>> elements = (30, 10, 20, 20); } + + calc walk { + attribute scaled : Integer[*] ordered = s.elements->collect{in x; x - 5}; + attribute least : Integer = s.elements->head(); + attribute rest : Integer[*] ordered = s.elements->tail(); + attribute second : Integer = s.elements#(2); + attribute asWritten : Boolean = s.elements == (30, 10, 20); + attribute asEnumerated : Boolean = s.elements == (10, 20, 30); + return : Integer[*] ordered = (scaled#(1), scaled#(2), scaled#(3), least, rest#(1), rest#(2), second, if asWritten ? 1 else 0, if asEnumerated ? 1 else 0); + } +} diff --git a/internal/core/runtime/testdata/conformance/calc_set_consumed_by_ordered_operations.trace.golden b/internal/core/runtime/testdata/conformance/calc_set_consumed_by_ordered_operations.trace.golden new file mode 100644 index 000000000..155b82f3e --- /dev/null +++ b/internal/core/runtime/testdata/conformance/calc_set_consumed_by_ordered_operations.trace.golden @@ -0,0 +1,80 @@ +enter calc test::walk + stmt declare scaled + enter calc ControlFunctions::collect +materialize: s #1 + eval literal 30 -> 30 + eval literal 10 -> 10 + eval literal 20 -> 20 + eval literal 20 -> 20 + eval sequence of 4 -> (30, 10, 20, 20) + eval chain elements -> {10, 20, 30} + eval body -> expr(body) + bind collection = {10, 20, 30} [argument] + bind mapper = expr(body) [argument] + eval feature x -> 10 + eval literal 5 -> 5 + eval operator - -> 5 + eval feature x -> 20 + eval literal 5 -> 5 + eval operator - -> 15 + eval feature x -> 30 + eval literal 5 -> 5 + eval operator - -> 25 + exit calc ControlFunctions::collect -> (5, 15, 25) + eval invoke collect -> (5, 15, 25) + stmt declare least + enter calc SequenceFunctions::head + eval chain elements -> {10, 20, 30} + bind seq = {10, 20, 30} [argument] + exit calc SequenceFunctions::head -> 10 + eval invoke head -> 10 + stmt declare rest + enter calc SequenceFunctions::tail + eval chain elements -> {10, 20, 30} + bind seq = {10, 20, 30} [argument] + exit calc SequenceFunctions::tail -> (20, 30) + eval invoke tail -> (20, 30) + stmt declare second + eval chain elements -> {10, 20, 30} + eval literal 2 -> 2 + eval index -> 20 + stmt declare asWritten + eval chain elements -> {10, 20, 30} + eval literal 30 -> 30 + eval literal 10 -> 10 + eval literal 20 -> 20 + eval sequence of 3 -> (30, 10, 20) + eval operator == -> false + stmt declare asEnumerated + eval chain elements -> {10, 20, 30} + eval literal 10 -> 10 + eval literal 20 -> 20 + eval literal 30 -> 30 + eval sequence of 3 -> (10, 20, 30) + eval operator == -> true + stmt return + eval feature scaled -> (5, 15, 25) + eval literal 1 -> 1 + eval index -> 5 + eval feature scaled -> (5, 15, 25) + eval literal 2 -> 2 + eval index -> 15 + eval feature scaled -> (5, 15, 25) + eval literal 3 -> 3 + eval index -> 25 + eval feature least -> 10 + eval feature rest -> (20, 30) + eval literal 1 -> 1 + eval index -> 20 + eval feature rest -> (20, 30) + eval literal 2 -> 2 + eval index -> 30 + eval feature second -> 20 + eval feature asWritten -> false + eval literal 0 -> 0 + eval operator if -> 0 + eval feature asEnumerated -> true + eval literal 1 -> 1 + eval operator if -> 1 + eval sequence of 9 -> (5, 15, 25, 10, 20, 30, 20, 0, 1) +exit calc test::walk -> (5, 15, 25, 10, 20, 30, 20, 0, 1) diff --git a/internal/core/runtime/testdata/conformance/instance_tensor_rank_three.expected.json b/internal/core/runtime/testdata/conformance/instance_tensor_rank_three.expected.json new file mode 100644 index 000000000..16384c5c8 --- /dev/null +++ b/internal/core/runtime/testdata/conformance/instance_tensor_rank_three.expected.json @@ -0,0 +1,524 @@ +{ + "type": "instance", + "libraries": true, + "instantiate": "test::Block", + "slots": { + "cube": { + "type": "TensorQuantity", + "dimensions": [ + 2, + 2, + 2 + ], + "elements": [ + { + "type": "Quantity", + "value": 1.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 2.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 3.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 4.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 5.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 6.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 7.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 8.0, + "unit": "Pa" + } + ] + }, + "slab": { + "type": "TensorQuantity", + "dimensions": [ + 2, + 3, + 2 + ], + "elements": [ + { + "type": "Quantity", + "value": 1, + "unit": "m" + }, + { + "type": "Quantity", + "value": 2, + "unit": "m" + }, + { + "type": "Quantity", + "value": 3, + "unit": "m" + }, + { + "type": "Quantity", + "value": 4, + "unit": "m" + }, + { + "type": "Quantity", + "value": 5, + "unit": "m" + }, + { + "type": "Quantity", + "value": 6, + "unit": "m" + }, + { + "type": "Quantity", + "value": 7, + "unit": "m" + }, + { + "type": "Quantity", + "value": 8, + "unit": "m" + }, + { + "type": "Quantity", + "value": 9, + "unit": "m" + }, + { + "type": "Quantity", + "value": 10, + "unit": "m" + }, + { + "type": "Quantity", + "value": 11, + "unit": "m" + }, + { + "type": "Quantity", + "value": 12, + "unit": "m" + } + ] + }, + "hyper": { + "type": "TensorQuantity", + "dimensions": [ + 2, + 1, + 2, + 2 + ], + "elements": [ + { + "type": "Quantity", + "value": 1, + "unit": "s" + }, + { + "type": "Quantity", + "value": 2, + "unit": "s" + }, + { + "type": "Quantity", + "value": 3, + "unit": "s" + }, + { + "type": "Quantity", + "value": 4, + "unit": "s" + }, + { + "type": "Quantity", + "value": 5, + "unit": "s" + }, + { + "type": "Quantity", + "value": 6, + "unit": "s" + }, + { + "type": "Quantity", + "value": 7, + "unit": "s" + }, + { + "type": "Quantity", + "value": 8, + "unit": "s" + } + ] + }, + "dims": { + "type": "Sequence", + "elements": [ + { + "type": "Integer", + "value": 2 + }, + { + "type": "Integer", + "value": 2 + }, + { + "type": "Integer", + "value": 2 + } + ] + }, + "order": { + "type": "Integer", + "value": 3 + }, + "flattenedSize": { + "type": "Integer", + "value": 8 + }, + "slabDims": { + "type": "Sequence", + "elements": [ + { + "type": "Integer", + "value": 2 + }, + { + "type": "Integer", + "value": 3 + }, + { + "type": "Integer", + "value": 2 + } + ] + }, + "hyperOrder": { + "type": "Integer", + "value": 4 + }, + "num": { + "type": "Sequence", + "elements": [ + { + "type": "Real", + "value": 1.0 + }, + { + "type": "Real", + "value": 2.0 + }, + { + "type": "Real", + "value": 3.0 + }, + { + "type": "Real", + "value": 4.0 + }, + { + "type": "Real", + "value": 5.0 + }, + { + "type": "Real", + "value": 6.0 + }, + { + "type": "Real", + "value": 7.0 + }, + { + "type": "Real", + "value": 8.0 + } + ] + }, + "origin": { + "type": "Quantity", + "value": 1.0, + "unit": "Pa" + }, + "lastVariesFastest": { + "type": "Quantity", + "value": 2.0, + "unit": "Pa" + }, + "middleIndex": { + "type": "Quantity", + "value": 3.0, + "unit": "Pa" + }, + "firstIndex": { + "type": "Quantity", + "value": 5.0, + "unit": "Pa" + }, + "last": { + "type": "Quantity", + "value": 8.0, + "unit": "Pa" + }, + "slabCell": { + "type": "Quantity", + "value": 11, + "unit": "m" + }, + "hyperCell": { + "type": "Quantity", + "value": 6, + "unit": "s" + }, + "sum": { + "type": "TensorQuantity", + "dimensions": [ + 2, + 2, + 2 + ], + "elements": [ + { + "type": "Quantity", + "value": 2.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 4.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 6.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 8.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 10.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 12.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 14.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 16.0, + "unit": "Pa" + } + ] + }, + "difference": { + "type": "TensorQuantity", + "dimensions": [ + 2, + 2, + 2 + ], + "elements": [ + { + "type": "Quantity", + "value": 0.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 0.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 0.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 0.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 0.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 0.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 0.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 0.0, + "unit": "Pa" + } + ] + }, + "doubled": { + "type": "TensorQuantity", + "dimensions": [ + 2, + 2, + 2 + ], + "elements": [ + { + "type": "Quantity", + "value": 2.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 4.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 6.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 8.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 10.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 12.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 14.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 16.0, + "unit": "Pa" + } + ] + }, + "stretched": { + "type": "TensorQuantity", + "dimensions": [ + 2, + 3, + 2 + ], + "elements": [ + { + "type": "Quantity", + "value": 3.0, + "unit": "m**2" + }, + { + "type": "Quantity", + "value": 6.0, + "unit": "m**2" + }, + { + "type": "Quantity", + "value": 9.0, + "unit": "m**2" + }, + { + "type": "Quantity", + "value": 12.0, + "unit": "m**2" + }, + { + "type": "Quantity", + "value": 15.0, + "unit": "m**2" + }, + { + "type": "Quantity", + "value": 18.0, + "unit": "m**2" + }, + { + "type": "Quantity", + "value": 21.0, + "unit": "m**2" + }, + { + "type": "Quantity", + "value": 24.0, + "unit": "m**2" + }, + { + "type": "Quantity", + "value": 27.0, + "unit": "m**2" + }, + { + "type": "Quantity", + "value": 30.0, + "unit": "m**2" + }, + { + "type": "Quantity", + "value": 33.0, + "unit": "m**2" + }, + { + "type": "Quantity", + "value": 36.0, + "unit": "m**2" + } + ] + }, + "zero": { + "type": "Boolean", + "value": false + }, + "isZero": { + "type": "Boolean", + "value": true + } + } +} diff --git a/internal/core/runtime/testdata/conformance/instance_tensor_rank_three.sysml b/internal/core/runtime/testdata/conformance/instance_tensor_rank_three.sysml new file mode 100644 index 000000000..a9b272bf4 --- /dev/null +++ b/internal/core/runtime/testdata/conformance/instance_tensor_rank_three.sysml @@ -0,0 +1,55 @@ +// A TensorQuantityValue is of any order: `'['` pairs the numbers with a +// reference of three (or four) dimensions in row-major order, the last index +// varying fastest, and the tensor answers its dimensions, order, flattenedSize +// and num from that shape. `#` takes one Positive index per dimension; `+`, `-` +// and the scalar multiplications keep the shape, component by component. +package test { + public import ScalarValues::*; + public import ISQ::*; + public import SI::*; + public import MeasurementReferences::*; + public import Quantities::*; + public import TensorCalculations::*; + + attribute cubeRef : TensorMeasurementReference { + :>> dimensions = (2, 2, 2); + :>> mRefs = (Pa, Pa, Pa, Pa, Pa, Pa, Pa, Pa); + } + attribute slabRef : TensorMeasurementReference { + :>> dimensions = (2, 3, 2); + :>> mRefs = (m, m, m, m, m, m, m, m, m, m, m, m); + } + attribute hyperRef : TensorMeasurementReference { + :>> dimensions = (2, 1, 2, 2); + :>> mRefs = (s, s, s, s, s, s, s, s); + } + + part def Block { + attribute cube : TensorQuantityValue = TensorCalculations::'['((1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0), cubeRef); + attribute slab : TensorQuantityValue = TensorCalculations::'['((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12), slabRef); + attribute hyper : TensorQuantityValue = TensorCalculations::'['((1, 2, 3, 4, 5, 6, 7, 8), hyperRef); + + attribute dims : Positive[*] ordered = cube.dimensions; + attribute order = cube.order; + attribute flattenedSize = cube.flattenedSize; + attribute slabDims : Positive[*] ordered = slab.dimensions; + attribute hyperOrder = hyper.order; + attribute num : Number[*] ordered = cube.num; + + attribute origin = cube#(1, 1, 1); + attribute lastVariesFastest = cube#(1, 1, 2); + attribute middleIndex = cube#(1, 2, 1); + attribute firstIndex = cube#(2, 1, 1); + attribute last = cube#(2, 2, 2); + attribute slabCell = slab#(2, 3, 1); + attribute hyperCell = hyper#(2, 1, 1, 2); + + attribute sum = cube + cube; + attribute difference = cube - cube; + attribute doubled = 2 * cube; + attribute stretched = 3 [m] * slab; + + attribute zero = isZeroTensorQuantity(cube); + attribute isZero = isZeroTensorQuantity(cube - cube); + } +} diff --git a/internal/core/runtime/testdata/conformance/instance_tensor_rank_three_failures.expected.json b/internal/core/runtime/testdata/conformance/instance_tensor_rank_three_failures.expected.json new file mode 100644 index 000000000..078808ec2 --- /dev/null +++ b/internal/core/runtime/testdata/conformance/instance_tensor_rank_three_failures.expected.json @@ -0,0 +1,102 @@ +{ + "type": "instance", + "libraries": true, + "instantiate": "test::Block", + "slots": { + "cube": { + "type": "TensorQuantity", + "dimensions": [ + 2, + 2, + 2 + ], + "elements": [ + { + "type": "Quantity", + "value": 1.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 2.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 3.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 4.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 5.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 6.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 7.0, + "unit": "Pa" + }, + { + "type": "Quantity", + "value": 8.0, + "unit": "Pa" + } + ] + }, + "tooFewIndexes": { + "error": "multiplicity violation: BaseFunctions::'#': 2 indexes address an array of rank 3 (indexes: Positive[n], n = arr.rank)" + }, + "oneIndex": { + "error": "multiplicity violation: BaseFunctions::'#': 1 indexes address an array of rank 3 (indexes: Positive[n], n = arr.rank)" + }, + "tooManyIndexes": { + "error": "multiplicity violation: BaseFunctions::'#': 4 indexes address an array of rank 3 (indexes: Positive[n], n = arr.rank)" + }, + "firstIndexLow": { + "error": "index out of range: BaseFunctions::'#': index 1 is 0, dimension 1 has 1..2" + }, + "firstIndexHigh": { + "error": "index out of range: BaseFunctions::'#': index 1 is 3, dimension 1 has 1..2" + }, + "middleIndexHigh": { + "error": "index out of range: BaseFunctions::'#': index 2 is 3, dimension 2 has 1..2" + }, + "lastIndexLow": { + "error": "index out of range: BaseFunctions::'#': index 3 is 0, dimension 3 has 1..2" + }, + "lastIndexHigh": { + "error": "index out of range: BaseFunctions::'#': index 3 is 3, dimension 3 has 1..2" + }, + "slabMiddleHigh": { + "error": "index out of range: BaseFunctions::'#': index 2 is 4, dimension 2 has 1..3" + }, + "nonIntegerIndex": { + "error": "type mismatch: BaseFunctions::'#' requires an Integer index, got a Real" + }, + "short": { + "error": "multiplicity violation: function TensorCalculations::'[': 7 elements for a reference of dimensions [2, 2, 2]: the declaration `in elements: Number[1..n]` has `n = mRef.flattenedSize` = 8, and Array binds mRefs to one reference per component" + }, + "long": { + "error": "multiplicity violation: function TensorCalculations::'[': 9 elements for a reference of dimensions [2, 2, 2]: the declaration `in elements: Number[1..n]` has `n = mRef.flattenedSize` = 8, and Array binds mRefs to one reference per component" + }, + "shapes": { + "error": "multiplicity violation: function TensorCalculations::'+': dimensions [2, 2, 2] and [2, 3, 2] differ; both parameters are TensorQuantityValue[1] of one shape" + }, + "orders": { + "error": "multiplicity violation: function TensorCalculations::'+': dimensions [2, 2, 2] and [2, 2] differ; both parameters are TensorQuantityValue[1] of one shape" + }, + "scaledShapes": { + "error": "multiplicity violation: function TensorCalculations::'-': dimensions [2, 2, 2] and [2, 3, 2] differ; both parameters are TensorQuantityValue[1] of one shape" + } + } +} diff --git a/internal/core/runtime/testdata/conformance/instance_tensor_rank_three_failures.sysml b/internal/core/runtime/testdata/conformance/instance_tensor_rank_three_failures.sysml new file mode 100644 index 000000000..861338ca9 --- /dev/null +++ b/internal/core/runtime/testdata/conformance/instance_tensor_rank_three_failures.sysml @@ -0,0 +1,50 @@ +// Indexing a tensor of order three is shape-checked: `#` takes exactly one +// index per dimension, so two or four indexes are a multiplicity violation, and +// each index must lie in 1..dimension, so 0 and dimension + 1 are out of range +// in whichever position they stand. `'['` refuses numbers that do not fill the +// three dimensions, and `+` refuses two tensors of order three whose shapes +// differ, as it does a tensor of order three against one of order two. +package test { + public import ScalarValues::*; + public import ISQ::*; + public import SI::*; + public import MeasurementReferences::*; + public import Quantities::*; + public import TensorCalculations::*; + + attribute cubeRef : TensorMeasurementReference { + :>> dimensions = (2, 2, 2); + :>> mRefs = (Pa, Pa, Pa, Pa, Pa, Pa, Pa, Pa); + } + attribute slabRef : TensorMeasurementReference { + :>> dimensions = (2, 3, 2); + :>> mRefs = (Pa, Pa, Pa, Pa, Pa, Pa, Pa, Pa, Pa, Pa, Pa, Pa); + } + attribute planeRef : TensorMeasurementReference { + :>> dimensions = (2, 2); + :>> mRefs = (Pa, Pa, Pa, Pa); + } + + part def Block { + attribute cube : TensorQuantityValue = TensorCalculations::'['((1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0), cubeRef); + attribute slab : TensorQuantityValue = TensorCalculations::'['((1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0), slabRef); + attribute plane : TensorQuantityValue = TensorCalculations::'['((1.0, 2.0, 3.0, 4.0), planeRef); + + attribute tooFewIndexes = cube#(1, 2); + attribute oneIndex = cube#(1); + attribute tooManyIndexes = cube#(1, 1, 1, 1); + attribute firstIndexLow = cube#(0, 1, 1); + attribute firstIndexHigh = cube#(3, 1, 1); + attribute middleIndexHigh = cube#(1, 3, 1); + attribute lastIndexLow = cube#(1, 1, 0); + attribute lastIndexHigh = cube#(1, 1, 3); + attribute slabMiddleHigh = slab#(1, 4, 1); + attribute nonIntegerIndex = cube#(1, 1.5, 1); + + attribute short = TensorCalculations::'['((1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0), cubeRef); + attribute long = TensorCalculations::'['((1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0), cubeRef); + attribute shapes = cube + slab; + attribute orders = cube + plane; + attribute scaledShapes = 2 * cube - slab; + } +} diff --git a/internal/core/runtime/testdata/conformance/library_bag_elements.expected.json b/internal/core/runtime/testdata/conformance/library_bag_elements.expected.json new file mode 100644 index 000000000..9f828ec92 --- /dev/null +++ b/internal/core/runtime/testdata/conformance/library_bag_elements.expected.json @@ -0,0 +1,13 @@ +{ + "type": "instance", + "libraries": true, + "instantiate": "test::b", + "slots": { + "elements": {"type": "Sequence", "elements": [ + {"type": "Integer", "value": 3}, + {"type": "Integer", "value": 1}, + {"type": "Integer", "value": 2}, + {"type": "Integer", "value": 2} + ]} + } +} diff --git a/internal/core/runtime/testdata/conformance/library_bag_elements.sysml b/internal/core/runtime/testdata/conformance/library_bag_elements.sysml new file mode 100644 index 000000000..104c69cd2 --- /dev/null +++ b/internal/core/runtime/testdata/conformance/library_bag_elements.sysml @@ -0,0 +1,8 @@ +// A Bag's elements are "unordered and nonunique" (Collections::Bag inherits +// the nonunique root Collection::elements), so a Bag is not a set: every +// element it was given is held, repeats included, as a sequence. +package test { + private import Collections::*; + + attribute b : Bag { :>> elements = (3, 1, 2, 2); } +} diff --git a/internal/core/runtime/testdata/conformance/library_map_elements.expected.json b/internal/core/runtime/testdata/conformance/library_map_elements.expected.json new file mode 100644 index 000000000..e315a6b4f --- /dev/null +++ b/internal/core/runtime/testdata/conformance/library_map_elements.expected.json @@ -0,0 +1,11 @@ +{ + "type": "instance", + "libraries": true, + "instantiate": "test::m", + "slots": { + "elements": {"type": "Set", "elements": [ + {"type": "Instance"}, + {"type": "Instance"} + ]} + } +} diff --git a/internal/core/runtime/testdata/conformance/library_map_elements.sysml b/internal/core/runtime/testdata/conformance/library_map_elements.sysml new file mode 100644 index 000000000..63270f96e --- /dev/null +++ b/internal/core/runtime/testdata/conformance/library_map_elements.sysml @@ -0,0 +1,9 @@ +// Map redefines Collection::elements as unique KeyValuePairs with no order, so +// a Map's elements are a set of pairs: the same pair given twice is held once. +package test { + private import Collections::*; + + attribute kv1 : KeyValuePair { :>> key = 1; :>> val = "a"; } + attribute kv2 : KeyValuePair { :>> key = 2; :>> val = "b"; } + attribute m : Map { :>> elements = (kv1, kv2, kv1); } +} diff --git a/internal/core/runtime/testdata/conformance/library_ordered_set_elements.expected.json b/internal/core/runtime/testdata/conformance/library_ordered_set_elements.expected.json new file mode 100644 index 000000000..badfbbdef --- /dev/null +++ b/internal/core/runtime/testdata/conformance/library_ordered_set_elements.expected.json @@ -0,0 +1,12 @@ +{ + "type": "instance", + "libraries": true, + "instantiate": "test::os", + "slots": { + "elements": {"type": "Sequence", "elements": [ + {"type": "Integer", "value": 3}, + {"type": "Integer", "value": 1}, + {"type": "Integer", "value": 2} + ]} + } +} diff --git a/internal/core/runtime/testdata/conformance/library_ordered_set_elements.sysml b/internal/core/runtime/testdata/conformance/library_ordered_set_elements.sysml new file mode 100644 index 000000000..5ee93e713 --- /dev/null +++ b/internal/core/runtime/testdata/conformance/library_ordered_set_elements.sysml @@ -0,0 +1,8 @@ +// An OrderedSet's elements are "unique and ordered" (Collections::OrderedSet +// redefines them `ordered`), so their order is part of the value: they are +// held as a sequence in the order given, not as a set. +package test { + private import Collections::*; + + attribute os : OrderedSet { :>> elements = (3, 1, 2); } +} diff --git a/internal/core/runtime/testdata/conformance/library_set_elements.expected.json b/internal/core/runtime/testdata/conformance/library_set_elements.expected.json new file mode 100644 index 000000000..7978558b9 --- /dev/null +++ b/internal/core/runtime/testdata/conformance/library_set_elements.expected.json @@ -0,0 +1,12 @@ +{ + "type": "instance", + "libraries": true, + "instantiate": "test::s", + "slots": { + "elements": {"type": "Set", "elements": [ + {"type": "Integer", "value": 1}, + {"type": "Integer", "value": 2}, + {"type": "Integer", "value": 3} + ]} + } +} diff --git a/internal/core/runtime/testdata/conformance/library_set_elements.sysml b/internal/core/runtime/testdata/conformance/library_set_elements.sysml new file mode 100644 index 000000000..d5d4fa788 --- /dev/null +++ b/internal/core/runtime/testdata/conformance/library_set_elements.sysml @@ -0,0 +1,9 @@ +// The library declares a Set's elements "unique and unordered" +// (Collections::Set, redefining UniqueCollection::elements), so a Set holds a +// set: the elements it was given with every repeat dropped, and no order of +// its own — they enumerate in the runtime's canonical order, not as written. +package test { + private import Collections::*; + + attribute s : Set { :>> elements = (3, 1, 2, 2, 3); } +} diff --git a/internal/core/runtime/testdata/conformance/library_set_elements_already_distinct.expected.json b/internal/core/runtime/testdata/conformance/library_set_elements_already_distinct.expected.json new file mode 100644 index 000000000..7978558b9 --- /dev/null +++ b/internal/core/runtime/testdata/conformance/library_set_elements_already_distinct.expected.json @@ -0,0 +1,12 @@ +{ + "type": "instance", + "libraries": true, + "instantiate": "test::s", + "slots": { + "elements": {"type": "Set", "elements": [ + {"type": "Integer", "value": 1}, + {"type": "Integer", "value": 2}, + {"type": "Integer", "value": 3} + ]} + } +} diff --git a/internal/core/runtime/testdata/conformance/library_set_elements_already_distinct.sysml b/internal/core/runtime/testdata/conformance/library_set_elements_already_distinct.sysml new file mode 100644 index 000000000..8b9e21710 --- /dev/null +++ b/internal/core/runtime/testdata/conformance/library_set_elements_already_distinct.sysml @@ -0,0 +1,7 @@ +// A Set given elements that are already distinct holds exactly those: nothing +// is dropped, and the set is the same set whatever order they were written in. +package test { + private import Collections::*; + + attribute s : Set { :>> elements = (2, 3, 1); } +} diff --git a/internal/core/runtime/testdata/conformance/library_set_elements_empty.expected.json b/internal/core/runtime/testdata/conformance/library_set_elements_empty.expected.json new file mode 100644 index 000000000..84a7da6dd --- /dev/null +++ b/internal/core/runtime/testdata/conformance/library_set_elements_empty.expected.json @@ -0,0 +1,8 @@ +{ + "type": "instance", + "libraries": true, + "instantiate": "test::e", + "slots": { + "elements": {"type": "Set", "elements": []} + } +} diff --git a/internal/core/runtime/testdata/conformance/library_set_elements_empty.sysml b/internal/core/runtime/testdata/conformance/library_set_elements_empty.sysml new file mode 100644 index 000000000..41d5fe282 --- /dev/null +++ b/internal/core/runtime/testdata/conformance/library_set_elements_empty.sysml @@ -0,0 +1,7 @@ +// A Set given no elements is the empty set, not an empty sequence: elements is +// declared [0..*], so a Set that values it with nothing holds a set of nothing. +package test { + private import Collections::*; + + attribute e : Set { :>> elements = (); } +} diff --git a/internal/core/runtime/testdata/conformance/library_set_operations.expected.json b/internal/core/runtime/testdata/conformance/library_set_operations.expected.json new file mode 100644 index 000000000..5fdebc817 --- /dev/null +++ b/internal/core/runtime/testdata/conformance/library_set_operations.expected.json @@ -0,0 +1,45 @@ +{ + "type": "instance", + "libraries": true, + "instantiate": "test::h", + "slots": { + "s": {"type": "Instance"}, + "t": {"type": "Instance"}, + "e": {"type": "Instance"}, + "b": {"type": "Instance"}, + "os": {"type": "Instance"}, + "count": {"type": "Integer", "value": 3}, + "emptyCount": {"type": "Integer", "value": 0}, + "bagCount": {"type": "Integer", "value": 4}, + "ordered": {"type": "Sequence", "elements": [ + {"type": "Integer", "value": 1}, + {"type": "Integer", "value": 2}, + {"type": "Integer", "value": 3} + ]}, + "repeatable": {"type": "Sequence", "elements": [ + {"type": "Integer", "value": 1}, + {"type": "Integer", "value": 2}, + {"type": "Integer", "value": 3} + ]}, + "plain": {"type": "Sequence", "elements": [ + {"type": "Integer", "value": 1}, + {"type": "Integer", "value": 2}, + {"type": "Integer", "value": 3} + ]}, + "inOrderedSet": {"type": "Sequence", "elements": [ + {"type": "Integer", "value": 3}, + {"type": "Integer", "value": 1}, + {"type": "Integer", "value": 2} + ]} + }, + "constraints": { + "equalRegardlessOfOrder": true, + "elementsEqualRegardlessOfOrder": true, + "distinctFromTheEmptySet": true, + "emptySetsAreEqual": true, + "membership": true, + "allMembers": true, + "emptiness": true, + "notASequence": true + } +} diff --git a/internal/core/runtime/testdata/conformance/library_set_operations.sysml b/internal/core/runtime/testdata/conformance/library_set_operations.sysml new file mode 100644 index 000000000..641ad0eb5 --- /dev/null +++ b/internal/core/runtime/testdata/conformance/library_set_operations.sysml @@ -0,0 +1,38 @@ +// What a set answers: two Sets are equal whatever order their elements were +// given in and however often, and so are their elements; size counts distinct +// elements; contains and containsAll are membership; the empty set is empty. +// A set flowing into any other multi-valued feature — ordered, nonunique or a +// plain `Integer[*]`, all of which the runtime holds as sequences — becomes the +// sequence of its elements in canonical order; a Bag or OrderedSet is never a set. +package test { + private import ScalarValues::*; + private import Collections::*; + private import CollectionFunctions::*; + + part def Holder { + attribute s : Set { :>> elements = (3, 1, 2, 2, 3); } + attribute t : Set { :>> elements = (2, 3, 1); } + attribute e : Set { :>> elements = (); } + attribute b : Bag { :>> elements = (3, 1, 2, 2); } + attribute os : OrderedSet { :>> elements = (3, 1, 2); } + + attribute count : Natural = size(s); + attribute emptyCount : Natural = size(e); + attribute bagCount : Natural = size(b); + attribute ordered : Integer[*] ordered = s.elements; + attribute repeatable : Integer[*] nonunique = s.elements; + attribute plain : Integer[*] = t.elements; + attribute inOrderedSet : Integer[*] ordered = os.elements; + + assert constraint equalRegardlessOfOrder { s == t } + assert constraint elementsEqualRegardlessOfOrder { s.elements == t.elements } + assert constraint distinctFromTheEmptySet { s != e } + assert constraint emptySetsAreEqual { e == e } + assert constraint membership { contains(s, 2) and not contains(s, 5) and not contains(e, 2) } + assert constraint allMembers { containsAll(s, t) and containsAll(t, s) and containsAll(s, e) } + assert constraint emptiness { isEmpty(e) and notEmpty(s) and not isEmpty(s) } + assert constraint notASequence { s != os and s.elements != b.elements } + } + + part h : Holder; +} diff --git a/internal/core/runtime/testdata/conformance/library_unique_collection_elements.expected.json b/internal/core/runtime/testdata/conformance/library_unique_collection_elements.expected.json new file mode 100644 index 000000000..47c35868f --- /dev/null +++ b/internal/core/runtime/testdata/conformance/library_unique_collection_elements.expected.json @@ -0,0 +1,12 @@ +{ + "type": "instance", + "libraries": true, + "instantiate": "test::u", + "slots": { + "elements": {"type": "Set", "elements": [ + {"type": "Integer", "value": 1}, + {"type": "Integer", "value": 2}, + {"type": "Integer", "value": 3} + ]} + } +} diff --git a/internal/core/runtime/testdata/conformance/library_unique_collection_elements.sysml b/internal/core/runtime/testdata/conformance/library_unique_collection_elements.sysml new file mode 100644 index 000000000..9a44429f0 --- /dev/null +++ b/internal/core/runtime/testdata/conformance/library_unique_collection_elements.sysml @@ -0,0 +1,8 @@ +// UniqueCollection redefines Collection::elements without `nonunique`, which +// the library notes makes it unique, and says nothing of order: its elements +// are a set, as a Set's are. +package test { + private import Collections::*; + + attribute u : UniqueCollection { :>> elements = (3, 1, 2, 2); } +} diff --git a/internal/core/runtime/trace.go b/internal/core/runtime/trace.go index fbb8f1065..2fc603cb1 100644 --- a/internal/core/runtime/trace.go +++ b/internal/core/runtime/trace.go @@ -350,7 +350,6 @@ func FormatTraceValue(v Value) string { for _, elem := range v.Set().Elements() { parts = append(parts, FormatTraceValue(elem)) } - sort.Strings(parts) return "{" + strings.Join(parts, ", ") + "}" case ValQuantity: if v.Quantity() == nil { diff --git a/internal/core/runtime/value.go b/internal/core/runtime/value.go index 5ab8093a9..3016b7c2e 100644 --- a/internal/core/runtime/value.go +++ b/internal/core/runtime/value.go @@ -3,6 +3,7 @@ package runtime import ( "fmt" "math" + "sort" "strconv" "strings" @@ -481,13 +482,12 @@ func (s *Sequence) Elements() []Value { // Set is a unique collection backed by hash buckets and exact comparisons. A set // has no inherent order, but enumerating one has to answer in some order, and -// insertion order is the one order a set does carry: it makes a sequence -// derived from a set — what `select` and `collect` over a set return — -// reproducible instead of dependent on map iteration. +// the one it answers in is the canonical order (see canonicalLess): equal sets +// enumerate alike, whatever order their elements were added in. type Set struct { elements map[valueKey][]Value - order []Value - size int + order []Value // insertion order, the tie-break canonical order falls back to + sorted []Value // canonical order, built on the first read after an Add } // NewSet creates an empty Set. @@ -506,7 +506,7 @@ func (s *Set) Add(val Value) { } s.elements[key] = append(bucket, val) s.order = append(s.order, val) - s.size++ + s.sorted = nil } // Contains checks if the value is in the set. @@ -521,10 +521,42 @@ func (s *Set) Contains(val Value) bool { // Size returns the number of unique elements. func (s *Set) Size() int { - return s.size + if s == nil { + return 0 + } + return len(s.order) } -// Elements returns all elements, in the order they were added. +// Elements returns all elements in canonical order. func (s *Set) Elements() []Value { - return append([]Value(nil), s.order...) + if s.sorted == nil && len(s.order) > 0 { + s.sorted = append([]Value(nil), s.order...) + sort.SliceStable(s.sorted, func(i, j int) bool { return canonicalLess(s.sorted[i], s.sorted[j]) }) + } + return append([]Value(nil), s.sorted...) +} + +// Equal holds when the sets have the same members, in whatever order. +func (s *Set) Equal(other *Set) bool { + if s == nil || other == nil { + return s.Size() == 0 && other.Size() == 0 + } + if s.Size() != other.Size() { + return false + } + for _, elem := range s.order { + if !other.Contains(elem) { + return false + } + } + return true +} + +// setOf builds a set value holding the distinct elements. +func setOf(elements []Value) Value { + set := NewSet() + for _, elem := range elements { + set.Add(elem) + } + return NewSetValue(set) } diff --git a/internal/core/runtime/value_test.go b/internal/core/runtime/value_test.go index 2d6831081..84e5bd5e2 100644 --- a/internal/core/runtime/value_test.go +++ b/internal/core/runtime/value_test.go @@ -147,7 +147,7 @@ func TestSetConstructionUsesBucketedLinearWork(t *testing.T) { } } -func TestSetElementsPreserveInsertionOrder(t *testing.T) { +func TestSetElementsEnumerateInCanonicalOrder(t *testing.T) { set := NewSet() for _, value := range []int64{2, 1, 2, 3} { set.Add(Value{Kind: ValConst, Const: semantics.Value{Kind: semantics.ValInt, Int: value}}) @@ -156,7 +156,7 @@ func TestSetElementsPreserveInsertionOrder(t *testing.T) { if len(elements) != 3 { t.Fatalf("set has %d elements, want 3", len(elements)) } - for i, want := range []int64{2, 1, 3} { + for i, want := range []int64{1, 2, 3} { if got := elements[i].Const.Int; got != want { t.Errorf("element %d = %d, want %d", i, got, want) } diff --git a/internal/repl/runtime_commands_test.go b/internal/repl/runtime_commands_test.go index bfef4f846..5212c1045 100644 --- a/internal/repl/runtime_commands_test.go +++ b/internal/repl/runtime_commands_test.go @@ -372,7 +372,7 @@ func TestFormatValue(t *testing.T) { {"string", runtime.NewStringValue("hi"), `"hi"`}, {"instance", runtime.Value{Kind: runtime.ValInstance, Instance: 3}, "Instance(ID: 3)"}, {"sequence", runtime.NewSequenceValue(sequence), `[1, "hi"]`}, - {"set", runtime.NewSetValue(set), `Set{"z", "a"}`}, + {"set", runtime.NewSetValue(set), `Set{"a", "z"}`}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { From 7274ea770a68ddb65444749cb8227c969de51813 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 02:15:31 +0000 Subject: [PATCH 02/20] feat(grpc): carry sets and tensor quantities of any rank across the wire and every client Value gains dedicated set and tensor_quantity arms, advertised as the set_values and tensor_values capabilities; the Go, Python, Node, Rust and Java clients decode, send and refuse them by capability. Sets have no RDF literal form and neither value compiles natively; both are documented, and the spec-compliance rows cite the library declarations. Co-Authored-By: jason.han --- api/proto/sysml.pb.go | 714 ++++++----- api/proto/sysml.proto | 38 + .../unreleased/set-and-tensor-values.added.md | 3 + client/opensysml/README.md | 14 +- client/opensysml/client.go | 42 +- client/opensysml/convert.go | 41 + client/opensysml/set_tensor_test.go | 221 ++++ client/opensysml/structured_internal_test.go | 62 + client/opensysml/types.go | 2 + client/opensysml/value.go | 59 +- .../org/openmbee/opensysml/Capabilities.java | 6 + .../java/org/openmbee/opensysml/Value.java | 156 +++ .../openmbee/opensysml/internal/Protos.java | 29 + .../opensysml/proto/ServerInfoResponse.java | 156 +++ .../proto/ServerInfoResponseOrBuilder.java | 48 + .../org/openmbee/opensysml/proto/Sysml.java | 449 +++---- .../opensysml/proto/TensorQuantity.java | 1051 +++++++++++++++++ .../proto/TensorQuantityOrBuilder.java | 88 ++ .../org/openmbee/opensysml/proto/Value.java | 512 ++++++++ .../opensysml/proto/ValueOrBuilder.java | 54 + .../openmbee/opensysml/proto/ValueSet.java | 742 ++++++++++++ .../opensysml/proto/ValueSetOrBuilder.java | 36 + .../opensysml/ApiIntegrationTest.java | 52 + .../openmbee/opensysml/PublicTypesTest.java | 92 ++ .../opensysml/internal/ProtosTest.java | 136 +++ .../opensysml/conformance/Rendering.java | 11 + clients/node/README.md | 15 +- clients/node/src/core/capabilities.ts | 4 + clients/node/src/core/index.ts | 3 + clients/node/src/core/values.ts | 84 +- clients/node/src/generated/sysml_pb.ts | 156 ++- clients/node/test/client.test.ts | 43 + clients/node/test/values.test.ts | 116 ++ clients/python/opensysml/__init__.py | 8 +- clients/python/opensysml/capabilities.py | 13 + clients/python/opensysml/connection.py | 55 +- clients/python/opensysml/proto/sysml_pb2.py | 172 +-- clients/python/opensysml/proto/sysml_pb2.pyi | 22 +- clients/python/opensysml/values.py | 163 ++- clients/python/tests/test_set_tensor.py | 422 +++++++ clients/rust/README.md | 22 +- .../rust/conformance/sysml.descriptor.binpb | Bin 78424 -> 81117 bytes clients/rust/opensysml/src/domain.rs | 378 +++++- clients/rust/opensysml/src/lib.rs | 3 +- .../rust/opensysml/src/proto/sysml/sysml.rs | 47 +- clients/rust/opensysml/tests/client.rs | 69 ++ cmd/conformance/pkgclient.go | 28 + conformance/fixtures/set_tensor.sysml | 14 + conformance/scenarios/01-server-info.json | 2 + conformance/scenarios/04-evaluate.json | 176 +++ conformance/scenarios/10-evaluate-calc.json | 213 ++++ docs/project/native-compilation.md | 4 +- docs/project/spec-compliance.md | 43 +- docs/reference/rdf-mapping.md | 22 + docs/reference/wire-contract.md | 84 +- internal/core/export/precedence.go | 2 + internal/core/export/rdf_expr.go | 16 +- internal/core/export/set_tensor_rdf_test.go | 80 ++ ...brary_set_sequence_functions.expected.json | 35 + .../library_set_sequence_functions.sysml | 17 + internal/grpc/capability_response.go | 29 + internal/grpc/convert.go | 140 ++- internal/grpc/convert_set_tensor_test.go | 510 ++++++++ internal/grpc/convert_structured_test.go | 22 - internal/grpc/service.go | 25 +- internal/repl/compile_test.go | 5 + internal/repl/testdata/compile_calcs.sysml | 12 + 67 files changed, 7373 insertions(+), 715 deletions(-) create mode 100644 changes/unreleased/set-and-tensor-values.added.md create mode 100644 client/opensysml/set_tensor_test.go create mode 100644 clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/TensorQuantity.java create mode 100644 clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/TensorQuantityOrBuilder.java create mode 100644 clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ValueSet.java create mode 100644 clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ValueSetOrBuilder.java create mode 100644 clients/python/tests/test_set_tensor.py create mode 100644 conformance/fixtures/set_tensor.sysml create mode 100644 internal/core/export/set_tensor_rdf_test.go create mode 100644 internal/core/runtime/testdata/conformance/library_set_sequence_functions.expected.json create mode 100644 internal/core/runtime/testdata/conformance/library_set_sequence_functions.sysml create mode 100644 internal/grpc/convert_set_tensor_test.go diff --git a/api/proto/sysml.pb.go b/api/proto/sysml.pb.go index 8f091cb09..aafea5468 100644 --- a/api/proto/sysml.pb.go +++ b/api/proto/sysml.pb.go @@ -3923,6 +3923,8 @@ type Value struct { // *Value_MeasurementRef // *Value_Infinity // *Value_Function + // *Value_Set + // *Value_TensorQuantity Kind isValue_Kind `protobuf_oneof:"kind"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -4118,6 +4120,24 @@ func (x *Value) GetFunction() *Function { return nil } +func (x *Value) GetSet() *ValueSet { + if x != nil { + if x, ok := x.Kind.(*Value_Set); ok { + return x.Set + } + } + return nil +} + +func (x *Value) GetTensorQuantity() *TensorQuantity { + if x != nil { + if x, ok := x.Kind.(*Value_TensorQuantity); ok { + return x.TensorQuantity + } + } + return nil +} + type isValue_Kind interface { isValue_Kind() } @@ -4195,6 +4215,14 @@ type Value_Function struct { Function *Function `protobuf:"bytes,17,opt,name=function,proto3,oneof"` // a calc as a value, named by its declaration } +type Value_Set struct { + Set *ValueSet `protobuf:"bytes,18,opt,name=set,proto3,oneof"` // distinct elements with no order of their own +} + +type Value_TensorQuantity struct { + TensorQuantity *TensorQuantity `protobuf:"bytes,19,opt,name=tensor_quantity,json=tensorQuantity,proto3,oneof"` // shape and one Quantity per component +} + func (*Value_IntValue) isValue_Kind() {} func (*Value_RealValue) isValue_Kind() {} @@ -4229,6 +4257,10 @@ func (*Value_Infinity) isValue_Kind() {} func (*Value_Function) isValue_Kind() {} +func (*Value_Set) isValue_Kind() {} + +func (*Value_TensorQuantity) isValue_Kind() {} + // Function is a calc held as a value: a calc definition, or a calc usage with // an input no read could supply, as `Sq` in `Fn(Sq, 3.0)` or the `f` of // `in calc f {...}`. It crosses as the declaration it is a value of, which is @@ -4293,6 +4325,117 @@ func (x *Function) GetSelfId() int64 { return 0 } +// ValueSet is a unique, unordered collection — a Collections::Set's elements — +// as distinct from a ValueSequence, whose order is part of its value. Two sets +// are equal when they hold the same elements in any order. The service sends +// the elements in the runtime's canonical order (Booleans, numbers, strings, +// quantities, enumeration literals, objects, each class in its own order), so +// equal sets cross alike; a client may send them in any order, but sending an +// element twice is rejected rather than read as one, since a repeated element +// is what a sequence carries. +type ValueSet struct { + state protoimpl.MessageState `protogen:"open.v1"` + Elements []*Value `protobuf:"bytes,1,rep,name=elements,proto3" json:"elements,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ValueSet) Reset() { + *x = ValueSet{} + mi := &file_sysml_proto_msgTypes[49] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ValueSet) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ValueSet) ProtoMessage() {} + +func (x *ValueSet) ProtoReflect() protoreflect.Message { + mi := &file_sysml_proto_msgTypes[49] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ValueSet.ProtoReflect.Descriptor instead. +func (*ValueSet) Descriptor() ([]byte, []int) { + return file_sysml_proto_rawDescGZIP(), []int{49} +} + +func (x *ValueSet) GetElements() []*Value { + if x != nil { + return x.Elements + } + return nil +} + +// TensorQuantity is a Quantities::TensorQuantityValue of any rank: its +// dimensions and, flattened in row-major order under them, one Quantity per +// component, each with its unit and reduction as a scalar Quantity carries them. +// A tensor of rank one is not a VectorQuantity, on the wire as in the runtime. +type TensorQuantity struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Positive extents, one per rank; their product (one for rank 0) is how many + // components there are, and a tensor not filling them is rejected. + Dimensions []int64 `protobuf:"varint,1,rep,packed,name=dimensions,proto3" json:"dimensions,omitempty"` + // A named unit sent without its unit_term is rejected as a Quantity's is. + Components []*Quantity `protobuf:"bytes,2,rep,name=components,proto3" json:"components,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TensorQuantity) Reset() { + *x = TensorQuantity{} + mi := &file_sysml_proto_msgTypes[50] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TensorQuantity) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TensorQuantity) ProtoMessage() {} + +func (x *TensorQuantity) ProtoReflect() protoreflect.Message { + mi := &file_sysml_proto_msgTypes[50] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TensorQuantity.ProtoReflect.Descriptor instead. +func (*TensorQuantity) Descriptor() ([]byte, []int) { + return file_sysml_proto_rawDescGZIP(), []int{50} +} + +func (x *TensorQuantity) GetDimensions() []int64 { + if x != nil { + return x.Dimensions + } + return nil +} + +func (x *TensorQuantity) GetComponents() []*Quantity { + if x != nil { + return x.Components + } + return nil +} + // Array is a Collections::Array: its elements flattened in row-major order // under its dimensions, compared by content rather than by the object read. type Array struct { @@ -4308,7 +4451,7 @@ type Array struct { func (x *Array) Reset() { *x = Array{} - mi := &file_sysml_proto_msgTypes[49] + mi := &file_sysml_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4320,7 +4463,7 @@ func (x *Array) String() string { func (*Array) ProtoMessage() {} func (x *Array) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[49] + mi := &file_sysml_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4333,7 +4476,7 @@ func (x *Array) ProtoReflect() protoreflect.Message { // Deprecated: Use Array.ProtoReflect.Descriptor instead. func (*Array) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{49} + return file_sysml_proto_rawDescGZIP(), []int{51} } func (x *Array) GetDimensions() []int64 { @@ -4363,7 +4506,7 @@ type Vector struct { func (x *Vector) Reset() { *x = Vector{} - mi := &file_sysml_proto_msgTypes[50] + mi := &file_sysml_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4375,7 +4518,7 @@ func (x *Vector) String() string { func (*Vector) ProtoMessage() {} func (x *Vector) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[50] + mi := &file_sysml_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4388,7 +4531,7 @@ func (x *Vector) ProtoReflect() protoreflect.Message { // Deprecated: Use Vector.ProtoReflect.Descriptor instead. func (*Vector) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{50} + return file_sysml_proto_rawDescGZIP(), []int{52} } func (x *Vector) GetComponents() []*Value { @@ -4411,7 +4554,7 @@ type VectorQuantity struct { func (x *VectorQuantity) Reset() { *x = VectorQuantity{} - mi := &file_sysml_proto_msgTypes[51] + mi := &file_sysml_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4423,7 +4566,7 @@ func (x *VectorQuantity) String() string { func (*VectorQuantity) ProtoMessage() {} func (x *VectorQuantity) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[51] + mi := &file_sysml_proto_msgTypes[53] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4436,7 +4579,7 @@ func (x *VectorQuantity) ProtoReflect() protoreflect.Message { // Deprecated: Use VectorQuantity.ProtoReflect.Descriptor instead. func (*VectorQuantity) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{51} + return file_sysml_proto_rawDescGZIP(), []int{53} } func (x *VectorQuantity) GetComponents() []*Quantity { @@ -4458,7 +4601,7 @@ type Complex struct { func (x *Complex) Reset() { *x = Complex{} - mi := &file_sysml_proto_msgTypes[52] + mi := &file_sysml_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4470,7 +4613,7 @@ func (x *Complex) String() string { func (*Complex) ProtoMessage() {} func (x *Complex) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[52] + mi := &file_sysml_proto_msgTypes[54] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4483,7 +4626,7 @@ func (x *Complex) ProtoReflect() protoreflect.Message { // Deprecated: Use Complex.ProtoReflect.Descriptor instead. func (*Complex) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{52} + return file_sysml_proto_rawDescGZIP(), []int{54} } func (x *Complex) GetReal() float64 { @@ -4517,7 +4660,7 @@ type EnumLiteral struct { func (x *EnumLiteral) Reset() { *x = EnumLiteral{} - mi := &file_sysml_proto_msgTypes[53] + mi := &file_sysml_proto_msgTypes[55] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4529,7 +4672,7 @@ func (x *EnumLiteral) String() string { func (*EnumLiteral) ProtoMessage() {} func (x *EnumLiteral) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[53] + mi := &file_sysml_proto_msgTypes[55] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4542,7 +4685,7 @@ func (x *EnumLiteral) ProtoReflect() protoreflect.Message { // Deprecated: Use EnumLiteral.ProtoReflect.Descriptor instead. func (*EnumLiteral) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{53} + return file_sysml_proto_rawDescGZIP(), []int{55} } func (x *EnumLiteral) GetLiteralId() string { @@ -4575,7 +4718,7 @@ type ValueSequence struct { func (x *ValueSequence) Reset() { *x = ValueSequence{} - mi := &file_sysml_proto_msgTypes[54] + mi := &file_sysml_proto_msgTypes[56] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4587,7 +4730,7 @@ func (x *ValueSequence) String() string { func (*ValueSequence) ProtoMessage() {} func (x *ValueSequence) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[54] + mi := &file_sysml_proto_msgTypes[56] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4600,7 +4743,7 @@ func (x *ValueSequence) ProtoReflect() protoreflect.Message { // Deprecated: Use ValueSequence.ProtoReflect.Descriptor instead. func (*ValueSequence) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{54} + return file_sysml_proto_rawDescGZIP(), []int{56} } func (x *ValueSequence) GetElements() []*Value { @@ -4634,7 +4777,7 @@ type Quantity struct { func (x *Quantity) Reset() { *x = Quantity{} - mi := &file_sysml_proto_msgTypes[55] + mi := &file_sysml_proto_msgTypes[57] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4646,7 +4789,7 @@ func (x *Quantity) String() string { func (*Quantity) ProtoMessage() {} func (x *Quantity) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[55] + mi := &file_sysml_proto_msgTypes[57] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4659,7 +4802,7 @@ func (x *Quantity) ProtoReflect() protoreflect.Message { // Deprecated: Use Quantity.ProtoReflect.Descriptor instead. func (*Quantity) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{55} + return file_sysml_proto_rawDescGZIP(), []int{57} } func (x *Quantity) GetMagnitude() isQuantity_Magnitude { @@ -4745,7 +4888,7 @@ type MeasurementRef struct { func (x *MeasurementRef) Reset() { *x = MeasurementRef{} - mi := &file_sysml_proto_msgTypes[56] + mi := &file_sysml_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4757,7 +4900,7 @@ func (x *MeasurementRef) String() string { func (*MeasurementRef) ProtoMessage() {} func (x *MeasurementRef) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[56] + mi := &file_sysml_proto_msgTypes[58] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4770,7 +4913,7 @@ func (x *MeasurementRef) ProtoReflect() protoreflect.Message { // Deprecated: Use MeasurementRef.ProtoReflect.Descriptor instead. func (*MeasurementRef) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{56} + return file_sysml_proto_rawDescGZIP(), []int{58} } func (x *MeasurementRef) GetUnit() string { @@ -4809,7 +4952,7 @@ type UnitTerm struct { func (x *UnitTerm) Reset() { *x = UnitTerm{} - mi := &file_sysml_proto_msgTypes[57] + mi := &file_sysml_proto_msgTypes[59] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4821,7 +4964,7 @@ func (x *UnitTerm) String() string { func (*UnitTerm) ProtoMessage() {} func (x *UnitTerm) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[57] + mi := &file_sysml_proto_msgTypes[59] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4834,7 +4977,7 @@ func (x *UnitTerm) ProtoReflect() protoreflect.Message { // Deprecated: Use UnitTerm.ProtoReflect.Descriptor instead. func (*UnitTerm) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{57} + return file_sysml_proto_rawDescGZIP(), []int{59} } func (x *UnitTerm) GetScaleNum() float64 { @@ -4870,7 +5013,7 @@ type UnitFactor struct { func (x *UnitFactor) Reset() { *x = UnitFactor{} - mi := &file_sysml_proto_msgTypes[58] + mi := &file_sysml_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4882,7 +5025,7 @@ func (x *UnitFactor) String() string { func (*UnitFactor) ProtoMessage() {} func (x *UnitFactor) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[58] + mi := &file_sysml_proto_msgTypes[60] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4895,7 +5038,7 @@ func (x *UnitFactor) ProtoReflect() protoreflect.Message { // Deprecated: Use UnitFactor.ProtoReflect.Descriptor instead. func (*UnitFactor) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{58} + return file_sysml_proto_rawDescGZIP(), []int{60} } func (x *UnitFactor) GetUnitId() string { @@ -4927,7 +5070,7 @@ type Diagnostic struct { func (x *Diagnostic) Reset() { *x = Diagnostic{} - mi := &file_sysml_proto_msgTypes[59] + mi := &file_sysml_proto_msgTypes[61] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4939,7 +5082,7 @@ func (x *Diagnostic) String() string { func (*Diagnostic) ProtoMessage() {} func (x *Diagnostic) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[59] + mi := &file_sysml_proto_msgTypes[61] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4952,7 +5095,7 @@ func (x *Diagnostic) ProtoReflect() protoreflect.Message { // Deprecated: Use Diagnostic.ProtoReflect.Descriptor instead. func (*Diagnostic) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{59} + return file_sysml_proto_rawDescGZIP(), []int{61} } func (x *Diagnostic) GetSeverity() string { @@ -4997,7 +5140,7 @@ type Span struct { func (x *Span) Reset() { *x = Span{} - mi := &file_sysml_proto_msgTypes[60] + mi := &file_sysml_proto_msgTypes[62] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5009,7 +5152,7 @@ func (x *Span) String() string { func (*Span) ProtoMessage() {} func (x *Span) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[60] + mi := &file_sysml_proto_msgTypes[62] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5022,7 +5165,7 @@ func (x *Span) ProtoReflect() protoreflect.Message { // Deprecated: Use Span.ProtoReflect.Descriptor instead. func (*Span) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{60} + return file_sysml_proto_rawDescGZIP(), []int{62} } func (x *Span) GetFile() string { @@ -5070,7 +5213,7 @@ type ServerInfoRequest struct { func (x *ServerInfoRequest) Reset() { *x = ServerInfoRequest{} - mi := &file_sysml_proto_msgTypes[61] + mi := &file_sysml_proto_msgTypes[63] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5082,7 +5225,7 @@ func (x *ServerInfoRequest) String() string { func (*ServerInfoRequest) ProtoMessage() {} func (x *ServerInfoRequest) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[61] + mi := &file_sysml_proto_msgTypes[63] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5095,7 +5238,7 @@ func (x *ServerInfoRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ServerInfoRequest.ProtoReflect.Descriptor instead. func (*ServerInfoRequest) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{61} + return file_sysml_proto_rawDescGZIP(), []int{63} } // ServerInfoResponse describes the running service. @@ -5154,6 +5297,18 @@ type ServerInfoResponse struct { // unsupported null, and one is accepted as an action input // or calc argument; without it, one is refused with // UNIMPLEMENTED rather than read as another value. + // "set_values" - a Value carries a unique, unordered collection (a + // Collections::Set's elements) as set, each element once in + // canonical order, rather than reporting it as an + // unsupported null, and one is accepted as an action input + // or calc argument in any order; without it, one is refused + // with UNIMPLEMENTED rather than read as a sequence. + // "tensor_values" - a Value carries a tensor quantity of any rank as + // tensor_quantity, its dimensions and one Quantity per + // row-major component, rather than reporting it as an + // unsupported null, and one is accepted as an action input + // or calc argument; without it, one is refused with + // UNIMPLEMENTED rather than read as another value. // "apply_edits" - the ApplyEdits RPC edits a parsed model's own source, // preserving everything the edit did not touch. // "document_query" - the RunDocumentQuery RPC runs a named document query @@ -5174,7 +5329,7 @@ type ServerInfoResponse struct { func (x *ServerInfoResponse) Reset() { *x = ServerInfoResponse{} - mi := &file_sysml_proto_msgTypes[62] + mi := &file_sysml_proto_msgTypes[64] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5186,7 +5341,7 @@ func (x *ServerInfoResponse) String() string { func (*ServerInfoResponse) ProtoMessage() {} func (x *ServerInfoResponse) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[62] + mi := &file_sysml_proto_msgTypes[64] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5199,7 +5354,7 @@ func (x *ServerInfoResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ServerInfoResponse.ProtoReflect.Descriptor instead. func (*ServerInfoResponse) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{62} + return file_sysml_proto_rawDescGZIP(), []int{64} } func (x *ServerInfoResponse) GetVersion() string { @@ -5229,7 +5384,7 @@ type QueryRequest struct { func (x *QueryRequest) Reset() { *x = QueryRequest{} - mi := &file_sysml_proto_msgTypes[63] + mi := &file_sysml_proto_msgTypes[65] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5241,7 +5396,7 @@ func (x *QueryRequest) String() string { func (*QueryRequest) ProtoMessage() {} func (x *QueryRequest) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[63] + mi := &file_sysml_proto_msgTypes[65] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5254,7 +5409,7 @@ func (x *QueryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use QueryRequest.ProtoReflect.Descriptor instead. func (*QueryRequest) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{63} + return file_sysml_proto_rawDescGZIP(), []int{65} } func (x *QueryRequest) GetModelHash() string { @@ -5291,7 +5446,7 @@ type QueryResponse struct { func (x *QueryResponse) Reset() { *x = QueryResponse{} - mi := &file_sysml_proto_msgTypes[64] + mi := &file_sysml_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5303,7 +5458,7 @@ func (x *QueryResponse) String() string { func (*QueryResponse) ProtoMessage() {} func (x *QueryResponse) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[64] + mi := &file_sysml_proto_msgTypes[66] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5316,7 +5471,7 @@ func (x *QueryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use QueryResponse.ProtoReflect.Descriptor instead. func (*QueryResponse) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{64} + return file_sysml_proto_rawDescGZIP(), []int{66} } func (x *QueryResponse) GetElements() []*QueryResultElement { @@ -5346,7 +5501,7 @@ type Query struct { func (x *Query) Reset() { *x = Query{} - mi := &file_sysml_proto_msgTypes[65] + mi := &file_sysml_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5358,7 +5513,7 @@ func (x *Query) String() string { func (*Query) ProtoMessage() {} func (x *Query) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[65] + mi := &file_sysml_proto_msgTypes[67] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5371,7 +5526,7 @@ func (x *Query) ProtoReflect() protoreflect.Message { // Deprecated: Use Query.ProtoReflect.Descriptor instead. func (*Query) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{65} + return file_sysml_proto_rawDescGZIP(), []int{67} } func (x *Query) GetScope() []string { @@ -5410,7 +5565,7 @@ type Constraint struct { func (x *Constraint) Reset() { *x = Constraint{} - mi := &file_sysml_proto_msgTypes[66] + mi := &file_sysml_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5422,7 +5577,7 @@ func (x *Constraint) String() string { func (*Constraint) ProtoMessage() {} func (x *Constraint) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[66] + mi := &file_sysml_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5435,7 +5590,7 @@ func (x *Constraint) ProtoReflect() protoreflect.Message { // Deprecated: Use Constraint.ProtoReflect.Descriptor instead. func (*Constraint) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{66} + return file_sysml_proto_rawDescGZIP(), []int{68} } func (x *Constraint) GetConstraint() isConstraint_Constraint { @@ -5498,7 +5653,7 @@ type PrimitiveConstraint struct { func (x *PrimitiveConstraint) Reset() { *x = PrimitiveConstraint{} - mi := &file_sysml_proto_msgTypes[67] + mi := &file_sysml_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5510,7 +5665,7 @@ func (x *PrimitiveConstraint) String() string { func (*PrimitiveConstraint) ProtoMessage() {} func (x *PrimitiveConstraint) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[67] + mi := &file_sysml_proto_msgTypes[69] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5523,7 +5678,7 @@ func (x *PrimitiveConstraint) ProtoReflect() protoreflect.Message { // Deprecated: Use PrimitiveConstraint.ProtoReflect.Descriptor instead. func (*PrimitiveConstraint) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{67} + return file_sysml_proto_rawDescGZIP(), []int{69} } func (x *PrimitiveConstraint) GetInverse() bool { @@ -5566,7 +5721,7 @@ type CompositeConstraint struct { func (x *CompositeConstraint) Reset() { *x = CompositeConstraint{} - mi := &file_sysml_proto_msgTypes[68] + mi := &file_sysml_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5578,7 +5733,7 @@ func (x *CompositeConstraint) String() string { func (*CompositeConstraint) ProtoMessage() {} func (x *CompositeConstraint) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[68] + mi := &file_sysml_proto_msgTypes[70] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5591,7 +5746,7 @@ func (x *CompositeConstraint) ProtoReflect() protoreflect.Message { // Deprecated: Use CompositeConstraint.ProtoReflect.Descriptor instead. func (*CompositeConstraint) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{68} + return file_sysml_proto_rawDescGZIP(), []int{70} } func (x *CompositeConstraint) GetOperator() CompositeOperator { @@ -5623,7 +5778,7 @@ type QueryResultElement struct { func (x *QueryResultElement) Reset() { *x = QueryResultElement{} - mi := &file_sysml_proto_msgTypes[69] + mi := &file_sysml_proto_msgTypes[71] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5635,7 +5790,7 @@ func (x *QueryResultElement) String() string { func (*QueryResultElement) ProtoMessage() {} func (x *QueryResultElement) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[69] + mi := &file_sysml_proto_msgTypes[71] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5648,7 +5803,7 @@ func (x *QueryResultElement) ProtoReflect() protoreflect.Message { // Deprecated: Use QueryResultElement.ProtoReflect.Descriptor instead. func (*QueryResultElement) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{69} + return file_sysml_proto_rawDescGZIP(), []int{71} } func (x *QueryResultElement) GetId() string { @@ -5692,7 +5847,7 @@ type SweepRange struct { func (x *SweepRange) Reset() { *x = SweepRange{} - mi := &file_sysml_proto_msgTypes[70] + mi := &file_sysml_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5704,7 +5859,7 @@ func (x *SweepRange) String() string { func (*SweepRange) ProtoMessage() {} func (x *SweepRange) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[70] + mi := &file_sysml_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5717,7 +5872,7 @@ func (x *SweepRange) ProtoReflect() protoreflect.Message { // Deprecated: Use SweepRange.ProtoReflect.Descriptor instead. func (*SweepRange) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{70} + return file_sysml_proto_rawDescGZIP(), []int{72} } func (x *SweepRange) GetParameter() string { @@ -5779,7 +5934,7 @@ type RunSweepRequest struct { func (x *RunSweepRequest) Reset() { *x = RunSweepRequest{} - mi := &file_sysml_proto_msgTypes[71] + mi := &file_sysml_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5791,7 +5946,7 @@ func (x *RunSweepRequest) String() string { func (*RunSweepRequest) ProtoMessage() {} func (x *RunSweepRequest) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[71] + mi := &file_sysml_proto_msgTypes[73] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5804,7 +5959,7 @@ func (x *RunSweepRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RunSweepRequest.ProtoReflect.Descriptor instead. func (*RunSweepRequest) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{71} + return file_sysml_proto_rawDescGZIP(), []int{73} } func (x *RunSweepRequest) GetModelHash() string { @@ -5887,7 +6042,7 @@ type SweepRow struct { func (x *SweepRow) Reset() { *x = SweepRow{} - mi := &file_sysml_proto_msgTypes[72] + mi := &file_sysml_proto_msgTypes[74] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5899,7 +6054,7 @@ func (x *SweepRow) String() string { func (*SweepRow) ProtoMessage() {} func (x *SweepRow) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[72] + mi := &file_sysml_proto_msgTypes[74] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5912,7 +6067,7 @@ func (x *SweepRow) ProtoReflect() protoreflect.Message { // Deprecated: Use SweepRow.ProtoReflect.Descriptor instead. func (*SweepRow) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{72} + return file_sysml_proto_rawDescGZIP(), []int{74} } func (x *SweepRow) GetInputs() []*CalcOutput { @@ -5985,7 +6140,7 @@ type RunSweepResponse struct { func (x *RunSweepResponse) Reset() { *x = RunSweepResponse{} - mi := &file_sysml_proto_msgTypes[73] + mi := &file_sysml_proto_msgTypes[75] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5997,7 +6152,7 @@ func (x *RunSweepResponse) String() string { func (*RunSweepResponse) ProtoMessage() {} func (x *RunSweepResponse) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[73] + mi := &file_sysml_proto_msgTypes[75] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6010,7 +6165,7 @@ func (x *RunSweepResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RunSweepResponse.ProtoReflect.Descriptor instead. func (*RunSweepResponse) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{73} + return file_sysml_proto_rawDescGZIP(), []int{75} } func (x *RunSweepResponse) GetRows() []*SweepRow { @@ -6088,7 +6243,7 @@ type RunDocumentQueryRequest struct { func (x *RunDocumentQueryRequest) Reset() { *x = RunDocumentQueryRequest{} - mi := &file_sysml_proto_msgTypes[74] + mi := &file_sysml_proto_msgTypes[76] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6100,7 +6255,7 @@ func (x *RunDocumentQueryRequest) String() string { func (*RunDocumentQueryRequest) ProtoMessage() {} func (x *RunDocumentQueryRequest) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[74] + mi := &file_sysml_proto_msgTypes[76] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6113,7 +6268,7 @@ func (x *RunDocumentQueryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RunDocumentQueryRequest.ProtoReflect.Descriptor instead. func (*RunDocumentQueryRequest) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{74} + return file_sysml_proto_rawDescGZIP(), []int{76} } func (x *RunDocumentQueryRequest) GetModelHash() string { @@ -6148,7 +6303,7 @@ type DocumentQueryBinding struct { func (x *DocumentQueryBinding) Reset() { *x = DocumentQueryBinding{} - mi := &file_sysml_proto_msgTypes[75] + mi := &file_sysml_proto_msgTypes[77] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6160,7 +6315,7 @@ func (x *DocumentQueryBinding) String() string { func (*DocumentQueryBinding) ProtoMessage() {} func (x *DocumentQueryBinding) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[75] + mi := &file_sysml_proto_msgTypes[77] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6173,7 +6328,7 @@ func (x *DocumentQueryBinding) ProtoReflect() protoreflect.Message { // Deprecated: Use DocumentQueryBinding.ProtoReflect.Descriptor instead. func (*DocumentQueryBinding) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{75} + return file_sysml_proto_rawDescGZIP(), []int{77} } func (x *DocumentQueryBinding) GetParameter() string { @@ -6214,7 +6369,7 @@ type DocumentValue struct { func (x *DocumentValue) Reset() { *x = DocumentValue{} - mi := &file_sysml_proto_msgTypes[76] + mi := &file_sysml_proto_msgTypes[78] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6226,7 +6381,7 @@ func (x *DocumentValue) String() string { func (*DocumentValue) ProtoMessage() {} func (x *DocumentValue) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[76] + mi := &file_sysml_proto_msgTypes[78] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6239,7 +6394,7 @@ func (x *DocumentValue) ProtoReflect() protoreflect.Message { // Deprecated: Use DocumentValue.ProtoReflect.Descriptor instead. func (*DocumentValue) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{76} + return file_sysml_proto_rawDescGZIP(), []int{78} } func (x *DocumentValue) GetKind() isDocumentValue_Kind { @@ -6375,7 +6530,7 @@ type DocumentQueryColumn struct { func (x *DocumentQueryColumn) Reset() { *x = DocumentQueryColumn{} - mi := &file_sysml_proto_msgTypes[77] + mi := &file_sysml_proto_msgTypes[79] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6387,7 +6542,7 @@ func (x *DocumentQueryColumn) String() string { func (*DocumentQueryColumn) ProtoMessage() {} func (x *DocumentQueryColumn) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[77] + mi := &file_sysml_proto_msgTypes[79] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6400,7 +6555,7 @@ func (x *DocumentQueryColumn) ProtoReflect() protoreflect.Message { // Deprecated: Use DocumentQueryColumn.ProtoReflect.Descriptor instead. func (*DocumentQueryColumn) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{77} + return file_sysml_proto_rawDescGZIP(), []int{79} } func (x *DocumentQueryColumn) GetName() string { @@ -6420,7 +6575,7 @@ type DocumentQueryCell struct { func (x *DocumentQueryCell) Reset() { *x = DocumentQueryCell{} - mi := &file_sysml_proto_msgTypes[78] + mi := &file_sysml_proto_msgTypes[80] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6432,7 +6587,7 @@ func (x *DocumentQueryCell) String() string { func (*DocumentQueryCell) ProtoMessage() {} func (x *DocumentQueryCell) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[78] + mi := &file_sysml_proto_msgTypes[80] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6445,7 +6600,7 @@ func (x *DocumentQueryCell) ProtoReflect() protoreflect.Message { // Deprecated: Use DocumentQueryCell.ProtoReflect.Descriptor instead. func (*DocumentQueryCell) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{78} + return file_sysml_proto_rawDescGZIP(), []int{80} } func (x *DocumentQueryCell) GetValues() []*DocumentValue { @@ -6468,7 +6623,7 @@ type DocumentQueryRow struct { func (x *DocumentQueryRow) Reset() { *x = DocumentQueryRow{} - mi := &file_sysml_proto_msgTypes[79] + mi := &file_sysml_proto_msgTypes[81] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6480,7 +6635,7 @@ func (x *DocumentQueryRow) String() string { func (*DocumentQueryRow) ProtoMessage() {} func (x *DocumentQueryRow) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[79] + mi := &file_sysml_proto_msgTypes[81] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6493,7 +6648,7 @@ func (x *DocumentQueryRow) ProtoReflect() protoreflect.Message { // Deprecated: Use DocumentQueryRow.ProtoReflect.Descriptor instead. func (*DocumentQueryRow) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{79} + return file_sysml_proto_rawDescGZIP(), []int{81} } func (x *DocumentQueryRow) GetElement() *DocumentValue { @@ -6524,7 +6679,7 @@ type RunDocumentQueryResponse struct { func (x *RunDocumentQueryResponse) Reset() { *x = RunDocumentQueryResponse{} - mi := &file_sysml_proto_msgTypes[80] + mi := &file_sysml_proto_msgTypes[82] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6536,7 +6691,7 @@ func (x *RunDocumentQueryResponse) String() string { func (*RunDocumentQueryResponse) ProtoMessage() {} func (x *RunDocumentQueryResponse) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[80] + mi := &file_sysml_proto_msgTypes[82] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6549,7 +6704,7 @@ func (x *RunDocumentQueryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RunDocumentQueryResponse.ProtoReflect.Descriptor instead. func (*RunDocumentQueryResponse) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{80} + return file_sysml_proto_rawDescGZIP(), []int{82} } func (x *RunDocumentQueryResponse) GetColumns() []*DocumentQueryColumn { @@ -6581,7 +6736,7 @@ type RenderDocumentRequest struct { func (x *RenderDocumentRequest) Reset() { *x = RenderDocumentRequest{} - mi := &file_sysml_proto_msgTypes[81] + mi := &file_sysml_proto_msgTypes[83] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6593,7 +6748,7 @@ func (x *RenderDocumentRequest) String() string { func (*RenderDocumentRequest) ProtoMessage() {} func (x *RenderDocumentRequest) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[81] + mi := &file_sysml_proto_msgTypes[83] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6606,7 +6761,7 @@ func (x *RenderDocumentRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RenderDocumentRequest.ProtoReflect.Descriptor instead. func (*RenderDocumentRequest) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{81} + return file_sysml_proto_rawDescGZIP(), []int{83} } func (x *RenderDocumentRequest) GetModelHash() string { @@ -6634,7 +6789,7 @@ type RenderDocumentResponse struct { func (x *RenderDocumentResponse) Reset() { *x = RenderDocumentResponse{} - mi := &file_sysml_proto_msgTypes[82] + mi := &file_sysml_proto_msgTypes[84] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6646,7 +6801,7 @@ func (x *RenderDocumentResponse) String() string { func (*RenderDocumentResponse) ProtoMessage() {} func (x *RenderDocumentResponse) ProtoReflect() protoreflect.Message { - mi := &file_sysml_proto_msgTypes[82] + mi := &file_sysml_proto_msgTypes[84] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6659,7 +6814,7 @@ func (x *RenderDocumentResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RenderDocumentResponse.ProtoReflect.Descriptor instead. func (*RenderDocumentResponse) Descriptor() ([]byte, []int) { - return file_sysml_proto_rawDescGZIP(), []int{82} + return file_sysml_proto_rawDescGZIP(), []int{84} } func (x *RenderDocumentResponse) GetMarkdown() string { @@ -6970,7 +7125,7 @@ const file_sysml_proto_rawDesc = "" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" + "\x04type\x18\x02 \x01(\tR\x04type\x12\"\n" + "\x05value\x18\x03 \x01(\v2\f.sysml.ValueR\x05value\x12\x12\n" + - "\x04unit\x18\x04 \x01(\tR\x04unit\"\xce\x05\n" + + "\x04unit\x18\x04 \x01(\tR\x04unit\"\xb5\x06\n" + "\x05Value\x12\x1d\n" + "\tint_value\x18\x01 \x01(\x03H\x00R\bintValue\x12\x1f\n" + "\n" + @@ -6992,11 +7147,22 @@ const file_sysml_proto_rawDesc = "" + "\x0fvector_quantity\x18\x0e \x01(\v2\x15.sysml.VectorQuantityH\x00R\x0evectorQuantity\x12@\n" + "\x0fmeasurement_ref\x18\x0f \x01(\v2\x15.sysml.MeasurementRefH\x00R\x0emeasurementRef\x12\x1c\n" + "\binfinity\x18\x10 \x01(\bH\x00R\binfinity\x12-\n" + - "\bfunction\x18\x11 \x01(\v2\x0f.sysml.FunctionH\x00R\bfunctionB\x06\n" + + "\bfunction\x18\x11 \x01(\v2\x0f.sysml.FunctionH\x00R\bfunction\x12#\n" + + "\x03set\x18\x12 \x01(\v2\x0f.sysml.ValueSetH\x00R\x03set\x12@\n" + + "\x0ftensor_quantity\x18\x13 \x01(\v2\x15.sysml.TensorQuantityH\x00R\x0etensorQuantityB\x06\n" + "\x04kind\"<\n" + "\bFunction\x12\x17\n" + "\acalc_id\x18\x01 \x01(\tR\x06calcId\x12\x17\n" + - "\aself_id\x18\x02 \x01(\x03R\x06selfId\"Q\n" + + "\aself_id\x18\x02 \x01(\x03R\x06selfId\"4\n" + + "\bValueSet\x12(\n" + + "\belements\x18\x01 \x03(\v2\f.sysml.ValueR\belements\"a\n" + + "\x0eTensorQuantity\x12\x1e\n" + + "\n" + + "dimensions\x18\x01 \x03(\x03R\n" + + "dimensions\x12/\n" + + "\n" + + "components\x18\x02 \x03(\v2\x0f.sysml.QuantityR\n" + + "components\"Q\n" + "\x05Array\x12\x1e\n" + "\n" + "dimensions\x18\x01 \x03(\x03R\n" + @@ -7236,7 +7402,7 @@ func file_sysml_proto_rawDescGZIP() []byte { } var file_sysml_proto_enumTypes = make([]protoimpl.EnumInfo, 4) -var file_sysml_proto_msgTypes = make([]protoimpl.MessageInfo, 91) +var file_sysml_proto_msgTypes = make([]protoimpl.MessageInfo, 93) var file_sysml_proto_goTypes = []any{ (FailureReason)(0), // 0: sysml.FailureReason (EditFailure)(0), // 1: sysml.EditFailure @@ -7291,98 +7457,100 @@ var file_sysml_proto_goTypes = []any{ (*AttributeInfo)(nil), // 50: sysml.AttributeInfo (*Value)(nil), // 51: sysml.Value (*Function)(nil), // 52: sysml.Function - (*Array)(nil), // 53: sysml.Array - (*Vector)(nil), // 54: sysml.Vector - (*VectorQuantity)(nil), // 55: sysml.VectorQuantity - (*Complex)(nil), // 56: sysml.Complex - (*EnumLiteral)(nil), // 57: sysml.EnumLiteral - (*ValueSequence)(nil), // 58: sysml.ValueSequence - (*Quantity)(nil), // 59: sysml.Quantity - (*MeasurementRef)(nil), // 60: sysml.MeasurementRef - (*UnitTerm)(nil), // 61: sysml.UnitTerm - (*UnitFactor)(nil), // 62: sysml.UnitFactor - (*Diagnostic)(nil), // 63: sysml.Diagnostic - (*Span)(nil), // 64: sysml.Span - (*ServerInfoRequest)(nil), // 65: sysml.ServerInfoRequest - (*ServerInfoResponse)(nil), // 66: sysml.ServerInfoResponse - (*QueryRequest)(nil), // 67: sysml.QueryRequest - (*QueryResponse)(nil), // 68: sysml.QueryResponse - (*Query)(nil), // 69: sysml.Query - (*Constraint)(nil), // 70: sysml.Constraint - (*PrimitiveConstraint)(nil), // 71: sysml.PrimitiveConstraint - (*CompositeConstraint)(nil), // 72: sysml.CompositeConstraint - (*QueryResultElement)(nil), // 73: sysml.QueryResultElement - (*SweepRange)(nil), // 74: sysml.SweepRange - (*RunSweepRequest)(nil), // 75: sysml.RunSweepRequest - (*SweepRow)(nil), // 76: sysml.SweepRow - (*RunSweepResponse)(nil), // 77: sysml.RunSweepResponse - (*RunDocumentQueryRequest)(nil), // 78: sysml.RunDocumentQueryRequest - (*DocumentQueryBinding)(nil), // 79: sysml.DocumentQueryBinding - (*DocumentValue)(nil), // 80: sysml.DocumentValue - (*DocumentQueryColumn)(nil), // 81: sysml.DocumentQueryColumn - (*DocumentQueryCell)(nil), // 82: sysml.DocumentQueryCell - (*DocumentQueryRow)(nil), // 83: sysml.DocumentQueryRow - (*RunDocumentQueryResponse)(nil), // 84: sysml.RunDocumentQueryResponse - (*RenderDocumentRequest)(nil), // 85: sysml.RenderDocumentRequest - (*RenderDocumentResponse)(nil), // 86: sysml.RenderDocumentResponse - nil, // 87: sysml.RunAnalysisRequest.NamedArgumentsEntry - nil, // 88: sysml.Instance.FeatureValuesEntry - nil, // 89: sysml.ExecuteActionRequest.InputsEntry - nil, // 90: sysml.ExecuteActionResponse.OutputsEntry - nil, // 91: sysml.ExecuteStateResponse.FinalContextEntry - nil, // 92: sysml.SymbolInfo.MetadataEntry - nil, // 93: sysml.QueryResultElement.PropertiesEntry - nil, // 94: sysml.RunSweepRequest.NamedArgumentsEntry + (*ValueSet)(nil), // 53: sysml.ValueSet + (*TensorQuantity)(nil), // 54: sysml.TensorQuantity + (*Array)(nil), // 55: sysml.Array + (*Vector)(nil), // 56: sysml.Vector + (*VectorQuantity)(nil), // 57: sysml.VectorQuantity + (*Complex)(nil), // 58: sysml.Complex + (*EnumLiteral)(nil), // 59: sysml.EnumLiteral + (*ValueSequence)(nil), // 60: sysml.ValueSequence + (*Quantity)(nil), // 61: sysml.Quantity + (*MeasurementRef)(nil), // 62: sysml.MeasurementRef + (*UnitTerm)(nil), // 63: sysml.UnitTerm + (*UnitFactor)(nil), // 64: sysml.UnitFactor + (*Diagnostic)(nil), // 65: sysml.Diagnostic + (*Span)(nil), // 66: sysml.Span + (*ServerInfoRequest)(nil), // 67: sysml.ServerInfoRequest + (*ServerInfoResponse)(nil), // 68: sysml.ServerInfoResponse + (*QueryRequest)(nil), // 69: sysml.QueryRequest + (*QueryResponse)(nil), // 70: sysml.QueryResponse + (*Query)(nil), // 71: sysml.Query + (*Constraint)(nil), // 72: sysml.Constraint + (*PrimitiveConstraint)(nil), // 73: sysml.PrimitiveConstraint + (*CompositeConstraint)(nil), // 74: sysml.CompositeConstraint + (*QueryResultElement)(nil), // 75: sysml.QueryResultElement + (*SweepRange)(nil), // 76: sysml.SweepRange + (*RunSweepRequest)(nil), // 77: sysml.RunSweepRequest + (*SweepRow)(nil), // 78: sysml.SweepRow + (*RunSweepResponse)(nil), // 79: sysml.RunSweepResponse + (*RunDocumentQueryRequest)(nil), // 80: sysml.RunDocumentQueryRequest + (*DocumentQueryBinding)(nil), // 81: sysml.DocumentQueryBinding + (*DocumentValue)(nil), // 82: sysml.DocumentValue + (*DocumentQueryColumn)(nil), // 83: sysml.DocumentQueryColumn + (*DocumentQueryCell)(nil), // 84: sysml.DocumentQueryCell + (*DocumentQueryRow)(nil), // 85: sysml.DocumentQueryRow + (*RunDocumentQueryResponse)(nil), // 86: sysml.RunDocumentQueryResponse + (*RenderDocumentRequest)(nil), // 87: sysml.RenderDocumentRequest + (*RenderDocumentResponse)(nil), // 88: sysml.RenderDocumentResponse + nil, // 89: sysml.RunAnalysisRequest.NamedArgumentsEntry + nil, // 90: sysml.Instance.FeatureValuesEntry + nil, // 91: sysml.ExecuteActionRequest.InputsEntry + nil, // 92: sysml.ExecuteActionResponse.OutputsEntry + nil, // 93: sysml.ExecuteStateResponse.FinalContextEntry + nil, // 94: sysml.SymbolInfo.MetadataEntry + nil, // 95: sysml.QueryResultElement.PropertiesEntry + nil, // 96: sysml.RunSweepRequest.NamedArgumentsEntry } var file_sysml_proto_depIdxs = []int32{ 0, // 0: sysml.Verdict.failure_reason:type_name -> sysml.FailureReason 4, // 1: sysml.VerifyConstraintResponse.verdict:type_name -> sysml.Verdict 28, // 2: sysml.VerifyConstraintResponse.instances:type_name -> sysml.Instance - 63, // 3: sysml.VerifyConstraintResponse.diagnostics:type_name -> sysml.Diagnostic + 65, // 3: sysml.VerifyConstraintResponse.diagnostics:type_name -> sysml.Diagnostic 4, // 4: sysml.VerifyRequirementResponse.verdict:type_name -> sysml.Verdict 28, // 5: sysml.VerifyRequirementResponse.instances:type_name -> sysml.Instance - 63, // 6: sysml.VerifyRequirementResponse.diagnostics:type_name -> sysml.Diagnostic + 65, // 6: sysml.VerifyRequirementResponse.diagnostics:type_name -> sysml.Diagnostic 8, // 7: sysml.VerifyRequirementResponse.verification_verdicts:type_name -> sysml.VerificationVerdict 4, // 8: sysml.VerifySatisfactionResponse.verdicts:type_name -> sysml.Verdict 28, // 9: sysml.VerifySatisfactionResponse.instances:type_name -> sysml.Instance - 63, // 10: sysml.VerifySatisfactionResponse.diagnostics:type_name -> sysml.Diagnostic + 65, // 10: sysml.VerifySatisfactionResponse.diagnostics:type_name -> sysml.Diagnostic 0, // 11: sysml.VerifySatisfactionResponse.failure_reason:type_name -> sysml.FailureReason 8, // 12: sysml.VerifySatisfactionResponse.verification_verdicts:type_name -> sysml.VerificationVerdict 51, // 13: sysml.EvaluateCalcRequest.arguments:type_name -> sysml.Value 51, // 14: sysml.EvaluateCalcResponse.result:type_name -> sysml.Value 14, // 15: sysml.EvaluateCalcResponse.outputs:type_name -> sysml.CalcOutput - 63, // 16: sysml.EvaluateCalcResponse.diagnostics:type_name -> sysml.Diagnostic + 65, // 16: sysml.EvaluateCalcResponse.diagnostics:type_name -> sysml.Diagnostic 0, // 17: sysml.EvaluateCalcResponse.failure_reason:type_name -> sysml.FailureReason 51, // 18: sysml.CalcOutput.value:type_name -> sysml.Value 51, // 19: sysml.RunAnalysisRequest.arguments:type_name -> sysml.Value - 87, // 20: sysml.RunAnalysisRequest.named_arguments:type_name -> sysml.RunAnalysisRequest.NamedArgumentsEntry + 89, // 20: sysml.RunAnalysisRequest.named_arguments:type_name -> sysml.RunAnalysisRequest.NamedArgumentsEntry 14, // 21: sysml.RunAnalysisResponse.outputs:type_name -> sysml.CalcOutput 4, // 22: sysml.RunAnalysisResponse.verdicts:type_name -> sysml.Verdict 28, // 23: sysml.RunAnalysisResponse.instances:type_name -> sysml.Instance - 63, // 24: sysml.RunAnalysisResponse.diagnostics:type_name -> sysml.Diagnostic + 65, // 24: sysml.RunAnalysisResponse.diagnostics:type_name -> sysml.Diagnostic 0, // 25: sysml.RunAnalysisResponse.failure_reason:type_name -> sysml.FailureReason 8, // 26: sysml.RunAnalysisResponse.verification_verdicts:type_name -> sysml.VerificationVerdict 18, // 27: sysml.ParseSourcesRequest.documents:type_name -> sysml.SourceDocument 46, // 28: sysml.ParseSourcesResponse.roots:type_name -> sysml.SymbolInfo - 63, // 29: sysml.ParseSourcesResponse.diagnostics:type_name -> sysml.Diagnostic + 65, // 29: sysml.ParseSourcesResponse.diagnostics:type_name -> sysml.Diagnostic 46, // 30: sysml.ParseFileResponse.root:type_name -> sysml.SymbolInfo - 63, // 31: sysml.ParseFileResponse.diagnostics:type_name -> sysml.Diagnostic + 65, // 31: sysml.ParseFileResponse.diagnostics:type_name -> sysml.Diagnostic 46, // 32: sysml.SymbolResponse.symbol:type_name -> sysml.SymbolInfo - 63, // 33: sysml.DiagnosticsResponse.diagnostics:type_name -> sysml.Diagnostic + 65, // 33: sysml.DiagnosticsResponse.diagnostics:type_name -> sysml.Diagnostic 51, // 34: sysml.EvaluateResponse.result:type_name -> sysml.Value - 63, // 35: sysml.EvaluateResponse.diagnostics:type_name -> sysml.Diagnostic - 88, // 36: sysml.Instance.feature_values:type_name -> sysml.Instance.FeatureValuesEntry + 65, // 35: sysml.EvaluateResponse.diagnostics:type_name -> sysml.Diagnostic + 90, // 36: sysml.Instance.feature_values:type_name -> sysml.Instance.FeatureValuesEntry 51, // 37: sysml.FeatureValue.value:type_name -> sysml.Value 51, // 38: sysml.FeatureValue.values:type_name -> sysml.Value 28, // 39: sysml.InstantiateResponse.instance:type_name -> sysml.Instance - 63, // 40: sysml.InstantiateResponse.diagnostics:type_name -> sysml.Diagnostic + 65, // 40: sysml.InstantiateResponse.diagnostics:type_name -> sysml.Diagnostic 28, // 41: sysml.InstantiateResponse.instances:type_name -> sysml.Instance - 89, // 42: sysml.ExecuteActionRequest.inputs:type_name -> sysml.ExecuteActionRequest.InputsEntry - 90, // 43: sysml.ExecuteActionResponse.outputs:type_name -> sysml.ExecuteActionResponse.OutputsEntry - 63, // 44: sysml.ExecuteActionResponse.diagnostics:type_name -> sysml.Diagnostic - 91, // 45: sysml.ExecuteStateResponse.final_context:type_name -> sysml.ExecuteStateResponse.FinalContextEntry - 63, // 46: sysml.ExecuteStateResponse.diagnostics:type_name -> sysml.Diagnostic - 63, // 47: sysml.ConvertResponse.diagnostics:type_name -> sysml.Diagnostic + 91, // 42: sysml.ExecuteActionRequest.inputs:type_name -> sysml.ExecuteActionRequest.InputsEntry + 92, // 43: sysml.ExecuteActionResponse.outputs:type_name -> sysml.ExecuteActionResponse.OutputsEntry + 65, // 44: sysml.ExecuteActionResponse.diagnostics:type_name -> sysml.Diagnostic + 93, // 45: sysml.ExecuteStateResponse.final_context:type_name -> sysml.ExecuteStateResponse.FinalContextEntry + 65, // 46: sysml.ExecuteStateResponse.diagnostics:type_name -> sysml.Diagnostic + 65, // 47: sysml.ConvertResponse.diagnostics:type_name -> sysml.Diagnostic 39, // 48: sysml.ApplyEditsRequest.operations:type_name -> sysml.EditOperation 42, // 49: sysml.EditOperation.set_value:type_name -> sysml.SetValueEdit 43, // 50: sysml.EditOperation.rename:type_name -> sysml.RenameEdit @@ -7390,112 +7558,116 @@ var file_sysml_proto_depIdxs = []int32{ 41, // 52: sysml.EditOperation.delete:type_name -> sysml.DeleteEdit 45, // 53: sysml.ApplyEditsResponse.applied:type_name -> sysml.AppliedEdit 1, // 54: sysml.ApplyEditsResponse.failure:type_name -> sysml.EditFailure - 63, // 55: sysml.ApplyEditsResponse.diagnostics:type_name -> sysml.Diagnostic - 92, // 56: sysml.SymbolInfo.metadata:type_name -> sysml.SymbolInfo.MetadataEntry + 65, // 55: sysml.ApplyEditsResponse.diagnostics:type_name -> sysml.Diagnostic + 94, // 56: sysml.SymbolInfo.metadata:type_name -> sysml.SymbolInfo.MetadataEntry 50, // 57: sysml.SymbolInfo.attributes:type_name -> sysml.AttributeInfo 48, // 58: sysml.SymbolInfo.type_info:type_name -> sysml.TypeInfo 49, // 59: sysml.SymbolInfo.multiplicity:type_name -> sysml.MultiplicityInfo 47, // 60: sysml.SymbolInfo.specializations:type_name -> sysml.Specialization 51, // 61: sysml.AttributeInfo.value:type_name -> sysml.Value - 58, // 62: sysml.Value.sequence:type_name -> sysml.ValueSequence - 59, // 63: sysml.Value.quantity:type_name -> sysml.Quantity - 57, // 64: sysml.Value.enum_literal:type_name -> sysml.EnumLiteral - 56, // 65: sysml.Value.complex:type_name -> sysml.Complex - 53, // 66: sysml.Value.array:type_name -> sysml.Array - 54, // 67: sysml.Value.vector:type_name -> sysml.Vector - 55, // 68: sysml.Value.vector_quantity:type_name -> sysml.VectorQuantity - 60, // 69: sysml.Value.measurement_ref:type_name -> sysml.MeasurementRef + 60, // 62: sysml.Value.sequence:type_name -> sysml.ValueSequence + 61, // 63: sysml.Value.quantity:type_name -> sysml.Quantity + 59, // 64: sysml.Value.enum_literal:type_name -> sysml.EnumLiteral + 58, // 65: sysml.Value.complex:type_name -> sysml.Complex + 55, // 66: sysml.Value.array:type_name -> sysml.Array + 56, // 67: sysml.Value.vector:type_name -> sysml.Vector + 57, // 68: sysml.Value.vector_quantity:type_name -> sysml.VectorQuantity + 62, // 69: sysml.Value.measurement_ref:type_name -> sysml.MeasurementRef 52, // 70: sysml.Value.function:type_name -> sysml.Function - 51, // 71: sysml.Array.elements:type_name -> sysml.Value - 51, // 72: sysml.Vector.components:type_name -> sysml.Value - 59, // 73: sysml.VectorQuantity.components:type_name -> sysml.Quantity - 51, // 74: sysml.ValueSequence.elements:type_name -> sysml.Value - 61, // 75: sysml.Quantity.unit_term:type_name -> sysml.UnitTerm - 61, // 76: sysml.MeasurementRef.unit_term:type_name -> sysml.UnitTerm - 62, // 77: sysml.UnitTerm.factors:type_name -> sysml.UnitFactor - 64, // 78: sysml.Diagnostic.span:type_name -> sysml.Span - 69, // 79: sysml.QueryRequest.query:type_name -> sysml.Query - 73, // 80: sysml.QueryResponse.elements:type_name -> sysml.QueryResultElement - 70, // 81: sysml.Query.where:type_name -> sysml.Constraint - 71, // 82: sysml.Constraint.primitive:type_name -> sysml.PrimitiveConstraint - 72, // 83: sysml.Constraint.composite:type_name -> sysml.CompositeConstraint - 2, // 84: sysml.PrimitiveConstraint.operator:type_name -> sysml.PrimitiveOperator - 3, // 85: sysml.CompositeConstraint.operator:type_name -> sysml.CompositeOperator - 70, // 86: sysml.CompositeConstraint.constraint:type_name -> sysml.Constraint - 93, // 87: sysml.QueryResultElement.properties:type_name -> sysml.QueryResultElement.PropertiesEntry - 51, // 88: sysml.SweepRange.start:type_name -> sysml.Value - 51, // 89: sysml.SweepRange.end:type_name -> sysml.Value - 51, // 90: sysml.SweepRange.step:type_name -> sysml.Value - 51, // 91: sysml.RunSweepRequest.arguments:type_name -> sysml.Value - 94, // 92: sysml.RunSweepRequest.named_arguments:type_name -> sysml.RunSweepRequest.NamedArgumentsEntry - 74, // 93: sysml.RunSweepRequest.ranges:type_name -> sysml.SweepRange - 14, // 94: sysml.SweepRow.inputs:type_name -> sysml.CalcOutput - 14, // 95: sysml.SweepRow.outputs:type_name -> sysml.CalcOutput - 4, // 96: sysml.SweepRow.verdicts:type_name -> sysml.Verdict - 0, // 97: sysml.SweepRow.failure_reason:type_name -> sysml.FailureReason - 76, // 98: sysml.RunSweepResponse.rows:type_name -> sysml.SweepRow - 63, // 99: sysml.RunSweepResponse.diagnostics:type_name -> sysml.Diagnostic - 0, // 100: sysml.RunSweepResponse.failure_reason:type_name -> sysml.FailureReason - 28, // 101: sysml.RunSweepResponse.instances:type_name -> sysml.Instance - 79, // 102: sysml.RunDocumentQueryRequest.bindings:type_name -> sysml.DocumentQueryBinding - 80, // 103: sysml.DocumentQueryBinding.values:type_name -> sysml.DocumentValue - 59, // 104: sysml.DocumentValue.quantity:type_name -> sysml.Quantity - 80, // 105: sysml.DocumentQueryCell.values:type_name -> sysml.DocumentValue - 80, // 106: sysml.DocumentQueryRow.element:type_name -> sysml.DocumentValue - 82, // 107: sysml.DocumentQueryRow.cells:type_name -> sysml.DocumentQueryCell - 81, // 108: sysml.RunDocumentQueryResponse.columns:type_name -> sysml.DocumentQueryColumn - 83, // 109: sysml.RunDocumentQueryResponse.rows:type_name -> sysml.DocumentQueryRow - 51, // 110: sysml.RunAnalysisRequest.NamedArgumentsEntry.value:type_name -> sysml.Value - 29, // 111: sysml.Instance.FeatureValuesEntry.value:type_name -> sysml.FeatureValue - 51, // 112: sysml.ExecuteActionRequest.InputsEntry.value:type_name -> sysml.Value - 51, // 113: sysml.ExecuteActionResponse.OutputsEntry.value:type_name -> sysml.Value - 51, // 114: sysml.ExecuteStateResponse.FinalContextEntry.value:type_name -> sysml.Value - 51, // 115: sysml.RunSweepRequest.NamedArgumentsEntry.value:type_name -> sysml.Value - 65, // 116: sysml.SysMLService.GetServerInfo:input_type -> sysml.ServerInfoRequest - 17, // 117: sysml.SysMLService.ParseFile:input_type -> sysml.ParseFileRequest - 19, // 118: sysml.SysMLService.ParseSources:input_type -> sysml.ParseSourcesRequest - 22, // 119: sysml.SysMLService.GetSymbol:input_type -> sysml.GetSymbolRequest - 24, // 120: sysml.SysMLService.GetDiagnostics:input_type -> sysml.DiagnosticsRequest - 26, // 121: sysml.SysMLService.Evaluate:input_type -> sysml.EvaluateRequest - 30, // 122: sysml.SysMLService.Instantiate:input_type -> sysml.InstantiateRequest - 32, // 123: sysml.SysMLService.ExecuteAction:input_type -> sysml.ExecuteActionRequest - 34, // 124: sysml.SysMLService.ExecuteState:input_type -> sysml.ExecuteStateRequest - 36, // 125: sysml.SysMLService.Convert:input_type -> sysml.ConvertRequest - 38, // 126: sysml.SysMLService.ApplyEdits:input_type -> sysml.ApplyEditsRequest - 5, // 127: sysml.SysMLService.VerifyConstraint:input_type -> sysml.VerifyConstraintRequest - 7, // 128: sysml.SysMLService.VerifyRequirement:input_type -> sysml.VerifyRequirementRequest - 10, // 129: sysml.SysMLService.VerifySatisfaction:input_type -> sysml.VerifySatisfactionRequest - 12, // 130: sysml.SysMLService.EvaluateCalc:input_type -> sysml.EvaluateCalcRequest - 15, // 131: sysml.SysMLService.RunAnalysis:input_type -> sysml.RunAnalysisRequest - 75, // 132: sysml.SysMLService.RunSweep:input_type -> sysml.RunSweepRequest - 67, // 133: sysml.SysMLService.Query:input_type -> sysml.QueryRequest - 78, // 134: sysml.SysMLService.RunDocumentQuery:input_type -> sysml.RunDocumentQueryRequest - 85, // 135: sysml.SysMLService.RenderDocument:input_type -> sysml.RenderDocumentRequest - 66, // 136: sysml.SysMLService.GetServerInfo:output_type -> sysml.ServerInfoResponse - 21, // 137: sysml.SysMLService.ParseFile:output_type -> sysml.ParseFileResponse - 20, // 138: sysml.SysMLService.ParseSources:output_type -> sysml.ParseSourcesResponse - 23, // 139: sysml.SysMLService.GetSymbol:output_type -> sysml.SymbolResponse - 25, // 140: sysml.SysMLService.GetDiagnostics:output_type -> sysml.DiagnosticsResponse - 27, // 141: sysml.SysMLService.Evaluate:output_type -> sysml.EvaluateResponse - 31, // 142: sysml.SysMLService.Instantiate:output_type -> sysml.InstantiateResponse - 33, // 143: sysml.SysMLService.ExecuteAction:output_type -> sysml.ExecuteActionResponse - 35, // 144: sysml.SysMLService.ExecuteState:output_type -> sysml.ExecuteStateResponse - 37, // 145: sysml.SysMLService.Convert:output_type -> sysml.ConvertResponse - 44, // 146: sysml.SysMLService.ApplyEdits:output_type -> sysml.ApplyEditsResponse - 6, // 147: sysml.SysMLService.VerifyConstraint:output_type -> sysml.VerifyConstraintResponse - 9, // 148: sysml.SysMLService.VerifyRequirement:output_type -> sysml.VerifyRequirementResponse - 11, // 149: sysml.SysMLService.VerifySatisfaction:output_type -> sysml.VerifySatisfactionResponse - 13, // 150: sysml.SysMLService.EvaluateCalc:output_type -> sysml.EvaluateCalcResponse - 16, // 151: sysml.SysMLService.RunAnalysis:output_type -> sysml.RunAnalysisResponse - 77, // 152: sysml.SysMLService.RunSweep:output_type -> sysml.RunSweepResponse - 68, // 153: sysml.SysMLService.Query:output_type -> sysml.QueryResponse - 84, // 154: sysml.SysMLService.RunDocumentQuery:output_type -> sysml.RunDocumentQueryResponse - 86, // 155: sysml.SysMLService.RenderDocument:output_type -> sysml.RenderDocumentResponse - 136, // [136:156] is the sub-list for method output_type - 116, // [116:136] is the sub-list for method input_type - 116, // [116:116] is the sub-list for extension type_name - 116, // [116:116] is the sub-list for extension extendee - 0, // [0:116] is the sub-list for field type_name + 53, // 71: sysml.Value.set:type_name -> sysml.ValueSet + 54, // 72: sysml.Value.tensor_quantity:type_name -> sysml.TensorQuantity + 51, // 73: sysml.ValueSet.elements:type_name -> sysml.Value + 61, // 74: sysml.TensorQuantity.components:type_name -> sysml.Quantity + 51, // 75: sysml.Array.elements:type_name -> sysml.Value + 51, // 76: sysml.Vector.components:type_name -> sysml.Value + 61, // 77: sysml.VectorQuantity.components:type_name -> sysml.Quantity + 51, // 78: sysml.ValueSequence.elements:type_name -> sysml.Value + 63, // 79: sysml.Quantity.unit_term:type_name -> sysml.UnitTerm + 63, // 80: sysml.MeasurementRef.unit_term:type_name -> sysml.UnitTerm + 64, // 81: sysml.UnitTerm.factors:type_name -> sysml.UnitFactor + 66, // 82: sysml.Diagnostic.span:type_name -> sysml.Span + 71, // 83: sysml.QueryRequest.query:type_name -> sysml.Query + 75, // 84: sysml.QueryResponse.elements:type_name -> sysml.QueryResultElement + 72, // 85: sysml.Query.where:type_name -> sysml.Constraint + 73, // 86: sysml.Constraint.primitive:type_name -> sysml.PrimitiveConstraint + 74, // 87: sysml.Constraint.composite:type_name -> sysml.CompositeConstraint + 2, // 88: sysml.PrimitiveConstraint.operator:type_name -> sysml.PrimitiveOperator + 3, // 89: sysml.CompositeConstraint.operator:type_name -> sysml.CompositeOperator + 72, // 90: sysml.CompositeConstraint.constraint:type_name -> sysml.Constraint + 95, // 91: sysml.QueryResultElement.properties:type_name -> sysml.QueryResultElement.PropertiesEntry + 51, // 92: sysml.SweepRange.start:type_name -> sysml.Value + 51, // 93: sysml.SweepRange.end:type_name -> sysml.Value + 51, // 94: sysml.SweepRange.step:type_name -> sysml.Value + 51, // 95: sysml.RunSweepRequest.arguments:type_name -> sysml.Value + 96, // 96: sysml.RunSweepRequest.named_arguments:type_name -> sysml.RunSweepRequest.NamedArgumentsEntry + 76, // 97: sysml.RunSweepRequest.ranges:type_name -> sysml.SweepRange + 14, // 98: sysml.SweepRow.inputs:type_name -> sysml.CalcOutput + 14, // 99: sysml.SweepRow.outputs:type_name -> sysml.CalcOutput + 4, // 100: sysml.SweepRow.verdicts:type_name -> sysml.Verdict + 0, // 101: sysml.SweepRow.failure_reason:type_name -> sysml.FailureReason + 78, // 102: sysml.RunSweepResponse.rows:type_name -> sysml.SweepRow + 65, // 103: sysml.RunSweepResponse.diagnostics:type_name -> sysml.Diagnostic + 0, // 104: sysml.RunSweepResponse.failure_reason:type_name -> sysml.FailureReason + 28, // 105: sysml.RunSweepResponse.instances:type_name -> sysml.Instance + 81, // 106: sysml.RunDocumentQueryRequest.bindings:type_name -> sysml.DocumentQueryBinding + 82, // 107: sysml.DocumentQueryBinding.values:type_name -> sysml.DocumentValue + 61, // 108: sysml.DocumentValue.quantity:type_name -> sysml.Quantity + 82, // 109: sysml.DocumentQueryCell.values:type_name -> sysml.DocumentValue + 82, // 110: sysml.DocumentQueryRow.element:type_name -> sysml.DocumentValue + 84, // 111: sysml.DocumentQueryRow.cells:type_name -> sysml.DocumentQueryCell + 83, // 112: sysml.RunDocumentQueryResponse.columns:type_name -> sysml.DocumentQueryColumn + 85, // 113: sysml.RunDocumentQueryResponse.rows:type_name -> sysml.DocumentQueryRow + 51, // 114: sysml.RunAnalysisRequest.NamedArgumentsEntry.value:type_name -> sysml.Value + 29, // 115: sysml.Instance.FeatureValuesEntry.value:type_name -> sysml.FeatureValue + 51, // 116: sysml.ExecuteActionRequest.InputsEntry.value:type_name -> sysml.Value + 51, // 117: sysml.ExecuteActionResponse.OutputsEntry.value:type_name -> sysml.Value + 51, // 118: sysml.ExecuteStateResponse.FinalContextEntry.value:type_name -> sysml.Value + 51, // 119: sysml.RunSweepRequest.NamedArgumentsEntry.value:type_name -> sysml.Value + 67, // 120: sysml.SysMLService.GetServerInfo:input_type -> sysml.ServerInfoRequest + 17, // 121: sysml.SysMLService.ParseFile:input_type -> sysml.ParseFileRequest + 19, // 122: sysml.SysMLService.ParseSources:input_type -> sysml.ParseSourcesRequest + 22, // 123: sysml.SysMLService.GetSymbol:input_type -> sysml.GetSymbolRequest + 24, // 124: sysml.SysMLService.GetDiagnostics:input_type -> sysml.DiagnosticsRequest + 26, // 125: sysml.SysMLService.Evaluate:input_type -> sysml.EvaluateRequest + 30, // 126: sysml.SysMLService.Instantiate:input_type -> sysml.InstantiateRequest + 32, // 127: sysml.SysMLService.ExecuteAction:input_type -> sysml.ExecuteActionRequest + 34, // 128: sysml.SysMLService.ExecuteState:input_type -> sysml.ExecuteStateRequest + 36, // 129: sysml.SysMLService.Convert:input_type -> sysml.ConvertRequest + 38, // 130: sysml.SysMLService.ApplyEdits:input_type -> sysml.ApplyEditsRequest + 5, // 131: sysml.SysMLService.VerifyConstraint:input_type -> sysml.VerifyConstraintRequest + 7, // 132: sysml.SysMLService.VerifyRequirement:input_type -> sysml.VerifyRequirementRequest + 10, // 133: sysml.SysMLService.VerifySatisfaction:input_type -> sysml.VerifySatisfactionRequest + 12, // 134: sysml.SysMLService.EvaluateCalc:input_type -> sysml.EvaluateCalcRequest + 15, // 135: sysml.SysMLService.RunAnalysis:input_type -> sysml.RunAnalysisRequest + 77, // 136: sysml.SysMLService.RunSweep:input_type -> sysml.RunSweepRequest + 69, // 137: sysml.SysMLService.Query:input_type -> sysml.QueryRequest + 80, // 138: sysml.SysMLService.RunDocumentQuery:input_type -> sysml.RunDocumentQueryRequest + 87, // 139: sysml.SysMLService.RenderDocument:input_type -> sysml.RenderDocumentRequest + 68, // 140: sysml.SysMLService.GetServerInfo:output_type -> sysml.ServerInfoResponse + 21, // 141: sysml.SysMLService.ParseFile:output_type -> sysml.ParseFileResponse + 20, // 142: sysml.SysMLService.ParseSources:output_type -> sysml.ParseSourcesResponse + 23, // 143: sysml.SysMLService.GetSymbol:output_type -> sysml.SymbolResponse + 25, // 144: sysml.SysMLService.GetDiagnostics:output_type -> sysml.DiagnosticsResponse + 27, // 145: sysml.SysMLService.Evaluate:output_type -> sysml.EvaluateResponse + 31, // 146: sysml.SysMLService.Instantiate:output_type -> sysml.InstantiateResponse + 33, // 147: sysml.SysMLService.ExecuteAction:output_type -> sysml.ExecuteActionResponse + 35, // 148: sysml.SysMLService.ExecuteState:output_type -> sysml.ExecuteStateResponse + 37, // 149: sysml.SysMLService.Convert:output_type -> sysml.ConvertResponse + 44, // 150: sysml.SysMLService.ApplyEdits:output_type -> sysml.ApplyEditsResponse + 6, // 151: sysml.SysMLService.VerifyConstraint:output_type -> sysml.VerifyConstraintResponse + 9, // 152: sysml.SysMLService.VerifyRequirement:output_type -> sysml.VerifyRequirementResponse + 11, // 153: sysml.SysMLService.VerifySatisfaction:output_type -> sysml.VerifySatisfactionResponse + 13, // 154: sysml.SysMLService.EvaluateCalc:output_type -> sysml.EvaluateCalcResponse + 16, // 155: sysml.SysMLService.RunAnalysis:output_type -> sysml.RunAnalysisResponse + 79, // 156: sysml.SysMLService.RunSweep:output_type -> sysml.RunSweepResponse + 70, // 157: sysml.SysMLService.Query:output_type -> sysml.QueryResponse + 86, // 158: sysml.SysMLService.RunDocumentQuery:output_type -> sysml.RunDocumentQueryResponse + 88, // 159: sysml.SysMLService.RenderDocument:output_type -> sysml.RenderDocumentResponse + 140, // [140:160] is the sub-list for method output_type + 120, // [120:140] is the sub-list for method input_type + 120, // [120:120] is the sub-list for extension type_name + 120, // [120:120] is the sub-list for extension extendee + 0, // [0:120] is the sub-list for field type_name } func init() { file_sysml_proto_init() } @@ -7540,16 +7712,18 @@ func file_sysml_proto_init() { (*Value_MeasurementRef)(nil), (*Value_Infinity)(nil), (*Value_Function)(nil), + (*Value_Set)(nil), + (*Value_TensorQuantity)(nil), } - file_sysml_proto_msgTypes[55].OneofWrappers = []any{ + file_sysml_proto_msgTypes[57].OneofWrappers = []any{ (*Quantity_IntMagnitude)(nil), (*Quantity_RealMagnitude)(nil), } - file_sysml_proto_msgTypes[66].OneofWrappers = []any{ + file_sysml_proto_msgTypes[68].OneofWrappers = []any{ (*Constraint_Primitive)(nil), (*Constraint_Composite)(nil), } - file_sysml_proto_msgTypes[76].OneofWrappers = []any{ + file_sysml_proto_msgTypes[78].OneofWrappers = []any{ (*DocumentValue_ElementId)(nil), (*DocumentValue_StringValue)(nil), (*DocumentValue_IntValue)(nil), @@ -7564,7 +7738,7 @@ func file_sysml_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_sysml_proto_rawDesc), len(file_sysml_proto_rawDesc)), NumEnums: 4, - NumMessages: 91, + NumMessages: 93, NumExtensions: 0, NumServices: 1, }, diff --git a/api/proto/sysml.proto b/api/proto/sysml.proto index bd5c318e7..9d075b668 100644 --- a/api/proto/sysml.proto +++ b/api/proto/sysml.proto @@ -717,6 +717,8 @@ message Value { // as DocumentValue.infinity is. bool infinity = 16; Function function = 17; // a calc as a value, named by its declaration + ValueSet set = 18; // distinct elements with no order of their own + TensorQuantity tensor_quantity = 19; // shape and one Quantity per component } } @@ -737,6 +739,30 @@ message Function { int64 self_id = 2; } +// ValueSet is a unique, unordered collection — a Collections::Set's elements — +// as distinct from a ValueSequence, whose order is part of its value. Two sets +// are equal when they hold the same elements in any order. The service sends +// the elements in the runtime's canonical order (Booleans, numbers, strings, +// quantities, enumeration literals, objects, each class in its own order), so +// equal sets cross alike; a client may send them in any order, but sending an +// element twice is rejected rather than read as one, since a repeated element +// is what a sequence carries. +message ValueSet { + repeated Value elements = 1; +} + +// TensorQuantity is a Quantities::TensorQuantityValue of any rank: its +// dimensions and, flattened in row-major order under them, one Quantity per +// component, each with its unit and reduction as a scalar Quantity carries them. +// A tensor of rank one is not a VectorQuantity, on the wire as in the runtime. +message TensorQuantity { + // Positive extents, one per rank; their product (one for rank 0) is how many + // components there are, and a tensor not filling them is rejected. + repeated int64 dimensions = 1; + // A named unit sent without its unit_term is rejected as a Quantity's is. + repeated Quantity components = 2; +} + // Array is a Collections::Array: its elements flattened in row-major order // under its dimensions, compared by content rather than by the object read. message Array { @@ -920,6 +946,18 @@ message ServerInfoResponse { // unsupported null, and one is accepted as an action input // or calc argument; without it, one is refused with // UNIMPLEMENTED rather than read as another value. + // "set_values" - a Value carries a unique, unordered collection (a + // Collections::Set's elements) as set, each element once in + // canonical order, rather than reporting it as an + // unsupported null, and one is accepted as an action input + // or calc argument in any order; without it, one is refused + // with UNIMPLEMENTED rather than read as a sequence. + // "tensor_values" - a Value carries a tensor quantity of any rank as + // tensor_quantity, its dimensions and one Quantity per + // row-major component, rather than reporting it as an + // unsupported null, and one is accepted as an action input + // or calc argument; without it, one is refused with + // UNIMPLEMENTED rather than read as another value. // "apply_edits" - the ApplyEdits RPC edits a parsed model's own source, // preserving everything the edit did not touch. // "document_query" - the RunDocumentQuery RPC runs a named document query diff --git a/changes/unreleased/set-and-tensor-values.added.md b/changes/unreleased/set-and-tensor-values.added.md new file mode 100644 index 000000000..b02424c2f --- /dev/null +++ b/changes/unreleased/set-and-tensor-values.added.md @@ -0,0 +1,3 @@ +- **A `Collections::Set` holds a set.** Where the Kernel Data Type Library declares a collection's `elements` unique and unordered — `Set`, `UniqueCollection`, `Map` — the runtime now holds them as a set value: each member once, `size` counting members, equality that ignores the order the members were written in, and `contains`/`containsAll` as membership. A set consumed by an ordered operation (`collect`, `head`, `#`, a comparison against a sequence, a trace, a write into an `ordered` or `nonunique` feature) enumerates in one canonical order — Booleans, then numbers ascending, strings, quantities, enumeration literals, objects — so equal sets behave alike. What the library declares ordered or nonunique (`Bag`, `List`, `Array`, `OrderedSet`, `OrderedMap`, every `SequenceFunctions` result) is unchanged. +- **Tensor quantities of any rank.** A `TensorMeasurementReference` with three or more `dimensions` builds a rank-three-or-higher tensor whose `#` takes one index per dimension; the wrong number of indexes, an index out of its dimension's range, a non-Integer index, a component count off the flattened size and arithmetic between two shapes are each a typed error naming what was wrong, and the shape survives `+`, `-` and the scalar multiplications. +- **Sets and tensor quantities cross gRPC whole.** `Value` gains a `set` arm (the members as `Value`s, in canonical order, readable in any order, a repeated member refused) and a `tensor_quantity` arm (`dimensions` and one `Quantity` per row-major component, at any rank), advertised as the `set_values` and `tensor_values` capabilities. The Go, Python, Node, Rust and Java clients decode both to native types that check their own invariants, send them as calc arguments and refuse them to a service that does not advertise the capability; a service withholding one reports the unsupported null it always did. Neither value has an RDF literal form — the mapping writes the model's expressions, which round trip exactly — and neither compiles natively: `sysml -compile` refuses a calc that uses one with a typed error naming the type. diff --git a/client/opensysml/README.md b/client/opensysml/README.md index e62d11423..a69d39eaf 100644 --- a/client/opensysml/README.md +++ b/client/opensysml/README.md @@ -202,15 +202,23 @@ answering implementation supports, and `ServerInfo.Has` checks one. A request that asks for an unavailable capability is refused with `CodeUnimplemented`; capabilities that describe response population instead omit the fields they name. Check the list first for an operation-specific error (the `Capability*` -constants name the known ones). Four capabilities are checked for you: a `Complex` +constants name the known ones). Six capabilities are checked for you: a `Complex` among `ExecuteAction` inputs or `EvaluateCalc`/`RunAnalysis` arguments needs `complex_values`, an `Array`, `Vector` or `VectorQuantity` needs -`structured_values`, a `MeasurementRef` needs `measurement_refs`, and a `Function` +`structured_values`, a `MeasurementRef` needs `measurement_refs`, a `Function` (a calc held as a value, sent back to bind a calc-typed parameter) needs -`function_values` — each at the top level or nested in a sequence or array; a +`function_values`, a `Set` needs `set_values` and a `TensorQuantity` needs +`tensor_values` — each at the top level or nested in a sequence, set or array; a service without them would read the value as null, so the client refuses with `CodeUnimplemented` before sending anything. +A `Set` arrives with its elements in the service's canonical order — Booleans, +then numbers, strings, quantities, enumeration literals and objects, each class +in its own order — so two equal sets arrive alike; a `Set` you send may list its +elements in any order, but listing one twice is refused by the service rather +than read as one element. A `TensorQuantity` carries its dimensions and one +`Quantity` per component in row-major order, at any rank. + ## Stability The module is `v0`, so the Go compatibility promise does not yet formally bind diff --git a/client/opensysml/client.go b/client/opensysml/client.go index 5131b3535..c7e39ae7a 100644 --- a/client/opensysml/client.go +++ b/client/opensysml/client.go @@ -63,7 +63,8 @@ type Client interface { // parameter name, and reports the outputs it produced. A Complex input // requires the complex_values capability, an Array, Vector or // VectorQuantity input the structured_values one, a MeasurementRef input - // the measurement_refs one and a Function input the function_values one, + // the measurement_refs one, a Function input the function_values one, a Set + // input the set_values one and a TensorQuantity input the tensor_values one, // checked before anything is sent. WithSchedule selects the scheduling // policy, which requires the schedule capability, checked the same way. ExecuteAction(ctx context.Context, model *Model, actionSymbolID string, inputs map[string]Value, opts ...ExecuteOption) (*ActionRun, error) @@ -90,9 +91,9 @@ type Client interface { // EvaluateCalc invokes the named calculation with positional arguments, or, // given none, evaluates a calc usage from its own members. Requires the // verification capability, and the complex_values, structured_values, - // measurement_refs or function_values capability for a Complex, a - // structured, a MeasurementRef or a Function argument, checked before - // anything is sent. + // measurement_refs, function_values, set_values or tensor_values capability + // for a Complex, a structured, a MeasurementRef, a Function, a Set or a + // TensorQuantity argument, checked before anything is sent. EvaluateCalc(ctx context.Context, model *Model, symbolID string, arguments ...Value) (*Calculation, error) // RunAnalysis runs the named analysis case — a definition or a usage — and @@ -511,8 +512,9 @@ func (c *client) call(model *Model) (string, error) { // requireValueCapabilities refuses to send a value of a kind whose capability // the service lacks — a Complex without complex_values, an Array, Vector or // VectorQuantity without structured_values, a MeasurementRef without -// measurement_refs, a Function without function_values — which would read it -// as null rather than refuse it. +// measurement_refs, a Function without function_values, a Set without +// set_values, a TensorQuantity without tensor_values — which would read it as +// null rather than refuse it. func (c *client) requireValueCapabilities(ctx context.Context, values ...Value) error { var needed []string if slices.ContainsFunc(values, carriesComplex) { @@ -527,6 +529,12 @@ func (c *client) requireValueCapabilities(ctx context.Context, values ...Value) if slices.ContainsFunc(values, carriesFunction) { needed = append(needed, CapabilityFunctionValues) } + if slices.ContainsFunc(values, carriesSet) { + needed = append(needed, CapabilitySetValues) + } + if slices.ContainsFunc(values, carriesTensor) { + needed = append(needed, CapabilityTensorValues) + } if len(needed) == 0 { return nil } @@ -605,11 +613,31 @@ func carriesFunction(value Value) bool { return slices.ContainsFunc(nestedValues(value), carriesFunction) } -// nestedValues are the values a value holds: a sequence's elements, an array's. +// carriesSet reports whether a value, or any value nested in it, is a Set. +func carriesSet(value Value) bool { + if _, ok := value.(Set); ok { + return true + } + return slices.ContainsFunc(nestedValues(value), carriesSet) +} + +// carriesTensor reports whether a value, or any value nested in it, is a +// TensorQuantity. +func carriesTensor(value Value) bool { + if _, ok := value.(TensorQuantity); ok { + return true + } + return slices.ContainsFunc(nestedValues(value), carriesTensor) +} + +// nestedValues are the values a value holds: a sequence's or a set's elements, +// an array's. func nestedValues(value Value) []Value { switch v := value.(type) { case Sequence: return v + case Set: + return v case Array: return v.Elements } diff --git a/client/opensysml/convert.go b/client/opensysml/convert.go index 6c6d939d7..0531d4cc4 100644 --- a/client/opensysml/convert.go +++ b/client/opensysml/convert.go @@ -170,6 +170,28 @@ func valueFromProto(value *pb.Value) Value { return Null("unsupported: function naming no calc") } return Function{CalcID: kind.Function.GetCalcId(), Self: InstanceID(kind.Function.GetSelfId())} + case *pb.Value_Set: + out := make(Set, 0, len(kind.Set.GetElements())) + for _, element := range kind.Set.GetElements() { + out = append(out, valueFromProto(element)) + } + return out + case *pb.Value_TensorQuantity: + if err := sysmlgrpc.CheckTensorShape(kind.TensorQuantity.GetDimensions(), len(kind.TensorQuantity.GetComponents())); err != nil { + return Null("unsupported: " + err.Error()) + } + out := TensorQuantity{ + Dimensions: append([]int64(nil), kind.TensorQuantity.GetDimensions()...), + Components: make([]Quantity, 0, len(kind.TensorQuantity.GetComponents())), + } + for _, component := range kind.TensorQuantity.GetComponents() { + quantity, ok := quantityFromProto(component) + if !ok { + return Null("unsupported: tensor quantity with a component without a magnitude") + } + out.Components = append(out.Components, quantity) + } + return out default: // A newer service's arm parses as an unknown field: no kind at all. return Null("unsupported: a value arm this client does not know") @@ -255,6 +277,25 @@ func valueToProto(value Value) (*pb.Value, error) { return nil, &StatusError{Code: CodeInvalidArgument, Message: "a function names no calc"} } return &pb.Value{Kind: &pb.Value_Function{Function: &pb.Function{CalcId: v.CalcID, SelfId: int64(v.Self)}}}, nil + case Set: + set := &pb.ValueSet{Elements: make([]*pb.Value, 0, len(v))} + for _, element := range v { + sent, err := valueToProto(element) + if err != nil { + return nil, err + } + set.Elements = append(set.Elements, sent) + } + return &pb.Value{Kind: &pb.Value_Set{Set: set}}, nil + case TensorQuantity: + tq := &pb.TensorQuantity{ + Dimensions: append([]int64(nil), v.Dimensions...), + Components: make([]*pb.Quantity, 0, len(v.Components)), + } + for _, component := range v.Components { + tq.Components = append(tq.Components, quantityToProto(component)) + } + return &pb.Value{Kind: &pb.Value_TensorQuantity{TensorQuantity: tq}}, nil case Unset: return nil, &StatusError{ Code: CodeInvalidArgument, diff --git a/client/opensysml/set_tensor_test.go b/client/opensysml/set_tensor_test.go new file mode 100644 index 000000000..9348a136e --- /dev/null +++ b/client/opensysml/set_tensor_test.go @@ -0,0 +1,221 @@ +package opensysml_test + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "reflect" + "strings" + "testing" + + "github.com/Open-MBEE/OpenSysML/api/proto/protoconnect" + "github.com/Open-MBEE/OpenSysML/client/opensysml" + sysmlgrpc "github.com/Open-MBEE/OpenSysML/internal/grpc" +) + +const setTensorSource = `package W { + private import ScalarValues::*; + private import Collections::*; + private import Quantities::*; + private import MeasurementReferences::*; + private import SI::*; + private import TensorCalculations::*; + + attribute s : Set { :>> elements = (3, 1, 2, 2, 3); } + attribute e : Set { :>> elements = (); } + attribute mixed : Set { :>> elements = ("b", 2, true, "a"); } + + attribute cubeRef : TensorMeasurementReference { + :>> dimensions = (2, 2, 2); + :>> mRefs = (Pa, Pa, Pa, Pa, Pa, Pa, Pa, Pa); + } + attribute cube : TensorQuantityValue = TensorCalculations::'['((1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0), cubeRef); + + calc def SizeOf { in c : Integer[0..*]; return : Natural = SequenceFunctions::size(c); } + calc sizeOf : SizeOf; + calc def Corner { in t : TensorQuantityValue; return : ScalarQuantityValue = t#(2, 2, 2); } + calc corner : Corner; +}` + +// A set arrives as a Set holding each element once in canonical order, a +// tensor as a TensorQuantity of its rank, over every transport; each sent back +// is read as itself. +func TestSetsAndTensorsCrossEveryTransport(t *testing.T) { + address := startService(t) + for name, client := range map[string]opensysml.Client{ + "in-process": newClient(t), + "connect-proto": dialClient(t, address), + "connect-json": dialClient(t, address, opensysml.WithJSONBody()), + } { + t.Run(name, func(t *testing.T) { + ctx := context.Background() + info, err := client.ServerInfo(ctx) + if err != nil { + t.Fatalf("ServerInfo: %v", err) + } + for _, capability := range []string{opensysml.CapabilitySetValues, opensysml.CapabilityTensorValues} { + if !info.Has(capability) { + t.Errorf("capabilities %v do not name %s", info.Capabilities, capability) + } + } + model := parse(t, client, setTensorSource) + + for expr, want := range map[string]opensysml.Value{ + "W::s.elements": opensysml.Set{opensysml.Int(1), opensysml.Int(2), opensysml.Int(3)}, + "W::e.elements": opensysml.Set{}, + "W::mixed.elements": opensysml.Set{opensysml.Bool(true), opensysml.Int(2), opensysml.String("a"), opensysml.String("b")}, + } { + got, err := client.Evaluate(ctx, model, expr) + if err != nil { + t.Fatalf("Evaluate(%s): %v", expr, err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s = %#v, want %#v", expr, got, want) + } + } + + cube, err := client.Evaluate(ctx, model, "W::cube") + if err != nil { + t.Fatalf("Evaluate(W::cube): %v", err) + } + tq, ok := cube.(opensysml.TensorQuantity) + if !ok { + t.Fatalf("W::cube = %#v, want a TensorQuantity", cube) + } + if !reflect.DeepEqual(tq.Dimensions, []int64{2, 2, 2}) || len(tq.Components) != 8 { + t.Fatalf("W::cube = %v, want dimensions (2, 2, 2) and 8 components", tq) + } + for i, q := range tq.Components { + if q.Magnitude != opensysml.Real(float64(i+1)) || q.Unit != "Pa" || q.Term == nil { + t.Errorf("component %d = %#v, want %d.0 Pa with its reduction", i+1, q, i+1) + } + } + if got := fmt.Sprint(tq); got != "Tensor(2, 2, 2)[1, 2, 3, 4, 5, 6, 7, 8] Pa" { + t.Errorf("String() = %q", got) + } + + // The tensor sent back is indexed by the model as itself. + calc, err := client.EvaluateCalc(ctx, model, "W::corner", tq) + if err != nil { + t.Fatalf("EvaluateCalc(corner): %v", err) + } + if q, ok := calc.Result.(opensysml.Quantity); !ok || q.Magnitude != opensysml.Real(8) || q.Unit != "Pa" { + t.Errorf("corner(cube) = %#v, want 8.0 Pa", calc.Result) + } + + // A set sent in any order is read as its elements. + calc, err = client.EvaluateCalc(ctx, model, "W::sizeOf", opensysml.Set{opensysml.Int(3), opensysml.Int(1), opensysml.Int(2)}) + if err != nil { + t.Fatalf("EvaluateCalc(sizeOf): %v", err) + } + if calc.Result != opensysml.Int(3) { + t.Errorf("sizeOf({3, 1, 2}) = %#v, want 3", calc.Result) + } + + // A set listing an element twice is a failure, not a set of one. + _, err = client.EvaluateCalc(ctx, model, "W::sizeOf", opensysml.Set{opensysml.Int(1), opensysml.Int(1)}) + var failure *opensysml.FailureError + if !errors.As(err, &failure) || !strings.Contains(failure.Error(), "set element is repeated") { + t.Errorf("sizeOf({1, 1}) err = %v, want a failure naming the repeated element", err) + } + // So is a tensor its components do not fill. + short := opensysml.TensorQuantity{Dimensions: []int64{2, 2, 2}, Components: tq.Components[:7]} + _, err = client.EvaluateCalc(ctx, model, "W::corner", short) + if !errors.As(err, &failure) || !strings.Contains(failure.Error(), "tensor components do not fill its dimensions") { + t.Errorf("corner(short) err = %v, want a failure naming the shape", err) + } + }) + } +} + +// A service without set_values or tensor_values would read the input as null, +// so the client refuses to send one, however deeply nested; and what such a +// service reports for the value is an unsupported null naming it. +func TestSetAndTensorInputsNeedTheirCapabilities(t *testing.T) { + svc, err := sysmlgrpc.NewServiceWithUnavailableCapabilitiesForTesting(16, "test", []string{opensysml.CapabilitySetValues, opensysml.CapabilityTensorValues}) + if err != nil { + t.Fatalf("NewServiceWithUnavailableCapabilitiesForTesting: %v", err) + } + t.Cleanup(svc.Close) + mux := http.NewServeMux() + mux.Handle(protoconnect.NewSysMLServiceHandler(sysmlgrpc.NewConnectAdapter(svc))) + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + pascal := opensysml.Quantity{Magnitude: opensysml.Real(1), Unit: "Pa", Term: &opensysml.UnitTerm{ScaleNum: 1, ScaleDen: 1, Factors: []opensysml.UnitFactor{ + {UnitID: "SI::kilogram", Exponent: 1}, {UnitID: "SI::metre", Exponent: -1}, {UnitID: "SI::second", Exponent: -2}, + }}} + line := opensysml.TensorQuantity{Dimensions: []int64{1}, Components: []opensysml.Quantity{pascal}} + set := opensysml.Set{opensysml.Int(1)} + for name, client := range map[string]opensysml.Client{ + "connect-proto": dialClient(t, server.URL), + "connect-json": dialClient(t, server.URL, opensysml.WithJSONBody()), + } { + t.Run(name, func(t *testing.T) { + ctx := context.Background() + model := parse(t, client, setTensorSource) + for label, input := range map[string]opensysml.Value{ + "set": set, + "nested": opensysml.Sequence{opensysml.Int(1), opensysml.Sequence{set}}, + "in an array": opensysml.Array{Dimensions: []int64{1}, Elements: []opensysml.Value{set}}, + } { + _, err := client.EvaluateCalc(ctx, model, "W::sizeOf", input) + wantCapabilityRefusal(t, "EvaluateCalc "+label, err, opensysml.CapabilitySetValues) + } + for label, input := range map[string]opensysml.Value{ + "tensor": line, + "nested": opensysml.Sequence{line}, + "in a set": opensysml.Set{line}, + } { + _, err := client.EvaluateCalc(ctx, model, "W::corner", input) + var status *opensysml.StatusError + if !errors.As(err, &status) || status.Code != opensysml.CodeUnimplemented { + t.Errorf("EvaluateCalc %s: err = %v, want CodeUnimplemented", label, err) + } + } + + for expr, want := range map[string]string{ + "W::s.elements": "unsupported: set Set{1, 2, 3}", + "W::cube": "unsupported: tensor quantity Tensor(2, 2, 2)[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0] [Pa]", + } { + got, err := client.Evaluate(ctx, model, expr) + if err != nil { + t.Fatalf("Evaluate(%s): %v", expr, err) + } + if got != opensysml.Null(want) { + t.Errorf("%s without the capability = %#v, want Null(%q)", expr, got, want) + } + } + }) + } +} + +func wantCapabilityRefusal(t *testing.T, op string, err error, capability string) { + t.Helper() + var status *opensysml.StatusError + if !errors.As(err, &status) || status.Code != opensysml.CodeUnimplemented || !strings.Contains(status.Message, capability) { + t.Errorf("%s: err = %v, want CodeUnimplemented naming %s", op, err, capability) + } +} + +func TestSetAndTensorRenderAsSysMLWrites(t *testing.T) { + metre := opensysml.Quantity{Magnitude: opensysml.Int(3), Unit: "m"} + second := opensysml.Quantity{Magnitude: opensysml.Real(2), Unit: "s"} + for _, testcase := range []struct { + value opensysml.Value + want string + }{ + {opensysml.Set{}, "{}"}, + {opensysml.Set{opensysml.Int(1), opensysml.String("a")}, "{1, a}"}, + {opensysml.Set{opensysml.Set{opensysml.Int(1)}, opensysml.Set{}}, "{{1}, {}}"}, + {opensysml.TensorQuantity{Dimensions: []int64{2}, Components: []opensysml.Quantity{metre, metre}}, "Tensor(2)[3, 3] m"}, + {opensysml.TensorQuantity{Dimensions: []int64{1, 2}, Components: []opensysml.Quantity{metre, second}}, "Tensor(1, 2)[3 m, 2 s]"}, + {opensysml.TensorQuantity{Dimensions: nil, Components: []opensysml.Quantity{metre}}, "Tensor()[3] m"}, + } { + if got := fmt.Sprint(testcase.value); got != testcase.want { + t.Errorf("%#v renders as %q, want %q", testcase.value, got, testcase.want) + } + } +} diff --git a/client/opensysml/structured_internal_test.go b/client/opensysml/structured_internal_test.go index b27d9f718..b556d4c62 100644 --- a/client/opensysml/structured_internal_test.go +++ b/client/opensysml/structured_internal_test.go @@ -117,6 +117,68 @@ func TestMeasurementRefInputIsNotSentWithoutMeasurementRefs(t *testing.T) { } } +// A set or a tensor quantity is refused before it leaves the client when the +// service lacks set_values or tensor_values, however deeply nested. +func TestSetAndTensorInputsAreNotSentWithoutTheirCapabilities(t *testing.T) { + ctx := context.Background() + model := &Model{Hash: "h"} + metre := Quantity{Magnitude: Real(1), Unit: "m"} + old := &oldCaller{t: t, capabilities: []string{CapabilityFeatureValues, CapabilityComplexValues, CapabilityStructuredValues, CapabilityMeasurementRefs}} + c := &client{caller: old} + for label, input := range map[string]Value{ + "set": Set{Int(1)}, + "set nested": Sequence{Int(1), Sequence{Set{}}}, + "set in an array": Array{Dimensions: []int64{1}, Elements: []Value{Set{Int(1)}}}, + "tensor": TensorQuantity{Dimensions: []int64{1}, Components: []Quantity{metre}}, + "tensor nested": Sequence{TensorQuantity{Dimensions: []int64{1}, Components: []Quantity{metre}}}, + "tensor in a set": Set{TensorQuantity{Dimensions: []int64{1}, Components: []Quantity{metre}}}, + "tensor in an array": Array{Dimensions: []int64{1}, Elements: []Value{TensorQuantity{Dimensions: []int64{1}, Components: []Quantity{metre}}}}, + } { + _, err := c.ExecuteAction(ctx, model, "A", map[string]Value{"x": input}) + wantUnimplemented(t, "ExecuteAction "+label, err) + _, err = c.EvaluateCalc(ctx, model, "f", Int(1), input) + wantUnimplemented(t, "EvaluateCalc "+label, err) + } +} + +// A malformed tensor quantity in an answer reads as an unsupported null naming +// the fault; a set reads as its elements, whatever they are. +func TestMalformedTensorAnswersAreNullsNamingTheFault(t *testing.T) { + one := &pb.Quantity{Magnitude: &pb.Quantity_IntMagnitude{IntMagnitude: 1}, Unit: "m"} + tensor := func(dimensions []int64, components ...*pb.Quantity) *pb.Value { + return &pb.Value{Kind: &pb.Value_TensorQuantity{TensorQuantity: &pb.TensorQuantity{Dimensions: dimensions, Components: components}}} + } + for name, tc := range map[string]struct { + value *pb.Value + want string + }{ + "too few components": {tensor([]int64{2, 2}, one, one, one), "do not fill"}, + "too many components": {tensor([]int64{2}, one, one, one), "do not fill"}, + "zero dimension": {tensor([]int64{0}), "not positive"}, + "negative dimension": {tensor([]int64{-1}, one), "not positive"}, + "no magnitude": {tensor([]int64{1}, &pb.Quantity{Unit: "m"}), "without a magnitude"}, + } { + t.Run(name, func(t *testing.T) { + got := valueFromProto(tc.value) + null, ok := got.(Null) + if !ok || !strings.HasPrefix(string(null), "unsupported: ") || !strings.Contains(string(null), tc.want) { + t.Fatalf("read as %#v, want an unsupported Null containing %q", got, tc.want) + } + }) + } + set := &pb.Value{Kind: &pb.Value_Set{Set: &pb.ValueSet{Elements: []*pb.Value{ + {Kind: &pb.Value_IntValue{IntValue: 1}}, + {Kind: &pb.Value_Set{Set: &pb.ValueSet{}}}, + }}}} + if got, want := valueFromProto(set), (Set{Int(1), Set{}}); !reflect.DeepEqual(got, want) { + t.Errorf("set read as %#v, want %#v", got, want) + } + sent, err := valueToProto(Set{Int(1), Set{}}) + if err != nil || !proto.Equal(sent, set) { + t.Errorf("set sent as %v (%v), want %v", sent, err, set) + } +} + // A malformed measurement reference in an answer reads as an unsupported null // naming the fault; a well-formed one reads as itself, reduction and identity // intact. diff --git a/client/opensysml/types.go b/client/opensysml/types.go index 69bd7bcfe..e1779028b 100644 --- a/client/opensysml/types.go +++ b/client/opensysml/types.go @@ -32,6 +32,8 @@ const ( CapabilityStructuredValues = sysmlgrpc.CapabilityStructuredValues CapabilityMeasurementRefs = sysmlgrpc.CapabilityMeasurementRefs CapabilityFunctionValues = sysmlgrpc.CapabilityFunctionValues + CapabilitySetValues = sysmlgrpc.CapabilitySetValues + CapabilityTensorValues = sysmlgrpc.CapabilityTensorValues CapabilityDiagnosticCodes = sysmlgrpc.CapabilityDiagnosticCodes CapabilitySchedule = sysmlgrpc.CapabilitySchedule CapabilityVerificationVerdicts = sysmlgrpc.CapabilityVerificationVerdicts diff --git a/client/opensysml/value.go b/client/opensysml/value.go index 73d6a1493..57cc85aaa 100644 --- a/client/opensysml/value.go +++ b/client/opensysml/value.go @@ -9,8 +9,8 @@ import ( // Value is one evaluated SysML value. It is a sealed sum: the concrete types // are Int, Real, Complex, Bool, String, InstanceID, Sequence, Null, Unset, -// Quantity, EnumLiteral, Array, Vector, VectorQuantity, MeasurementRef and -// Function, and a type switch over them is exhaustive. +// Quantity, EnumLiteral, Array, Vector, VectorQuantity, MeasurementRef, +// Function, Set and TensorQuantity, and a type switch over them is exhaustive. type Value interface { isValue() } @@ -140,6 +140,23 @@ type Vector []Number // reduction included, since the axes need not share a unit. type VectorQuantity []Quantity +// Set is a unique, unordered collection — a Collections::Set's elements — as +// distinct from a Sequence, whose order is part of its value. The service +// sends the elements in its canonical order, so equal sets arrive alike; a +// caller may list them in any order, but listing one twice is refused by the +// service rather than read as one element. +type Set []Value + +// TensorQuantity is a tensor quantity of any rank: one Quantity per component, +// unit and reduction included, in row-major order under its dimensions. A +// tensor of rank one is not a VectorQuantity, here as in the runtime. +type TensorQuantity struct { + // Dimensions are the positive extents, one per rank. + Dimensions []int64 + // Components fill the dimensions in row-major order. + Components []Quantity +} + // String renders the quantity as it was written: magnitude then unit. func (q Quantity) String() string { magnitude := fmt.Sprintf("%v", q.Magnitude) @@ -198,6 +215,42 @@ func (vq VectorQuantity) String() string { return out } +// String renders the set as SysML traces format it: `{1, 2, 3}`, the elements +// in the order held. +func (s Set) String() string { + parts := make([]string, len(s)) + for i, e := range s { + parts[i] = fmt.Sprintf("%v", e) + } + return "{" + strings.Join(parts, ", ") + "}" +} + +// String renders the tensor as SysML formats it: `Tensor(2, 2, 2)[1.0, 2.0, ...] m` +// when every component shares a unit, else each component with its own. +func (tq TensorQuantity) String() string { + dims := make([]string, len(tq.Dimensions)) + for i, d := range tq.Dimensions { + dims[i] = fmt.Sprintf("%d", d) + } + shared := len(tq.Components) > 0 + for _, q := range tq.Components[min(1, len(tq.Components)):] { + shared = shared && q.Unit == tq.Components[0].Unit + } + parts := make([]string, len(tq.Components)) + for i, q := range tq.Components { + if shared { + parts[i] = fmt.Sprintf("%v", q.Magnitude) + } else { + parts[i] = q.String() + } + } + out := "Tensor(" + strings.Join(dims, ", ") + ")[" + strings.Join(parts, ", ") + "]" + if shared && tq.Components[0].Unit != "" { + out += " " + tq.Components[0].Unit + } + return out +} + // String renders the reference as SysML writes the unit: `km`, `m/s`; one // never written down renders its reduction. func (r MeasurementRef) String() string { @@ -267,6 +320,8 @@ func (Vector) isValue() { /* marker: closed Value set */ } func (VectorQuantity) isValue() { /* marker: closed Value set */ } func (MeasurementRef) isValue() { /* marker: closed Value set */ } func (Function) isValue() { /* marker: closed Value set */ } +func (Set) isValue() { /* marker: closed Value set */ } +func (TensorQuantity) isValue() { /* marker: closed Value set */ } func (Int) isNumber() { /* marker: closed Number set */ } func (Real) isNumber() { /* marker: closed Number set */ } diff --git a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/Capabilities.java b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/Capabilities.java index 903bf6efe..8a6cef0d2 100644 --- a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/Capabilities.java +++ b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/Capabilities.java @@ -56,6 +56,12 @@ public final class Capabilities { /** {@code Diagnostic.code} is populated, so an empty code is a finding none was assigned. */ public static final String DIAGNOSTIC_CODES = "diagnostic_codes"; + /** A unique, unordered collection travels as a set rather than as an unsupported null. */ + public static final String SET_VALUES = "set_values"; + + /** A tensor of quantities of any rank travels as itself rather than as an unsupported null. */ + public static final String TENSOR_VALUES = "tensor_values"; + /** The {@code ApplyEdits} RPC edits a parsed model's own source. */ public static final String APPLY_EDITS = "apply_edits"; diff --git a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/Value.java b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/Value.java index b3ec37638..5473cc6a8 100644 --- a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/Value.java +++ b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/Value.java @@ -3,6 +3,7 @@ import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.Set; /** * A value the service evaluated: an immutable variant of {@code sysml.Value}. @@ -347,6 +348,161 @@ public Optional unit() { } } + /** + * A unique, unordered collection: a {@code Collections::Set}'s elements. + * + *

The service sends the members in its canonical order (numbers ascending, then strings, and + * so on), each exactly once; two sets are equal when they hold the same members in any order. + * Only a service advertising the {@code set_values} capability reports one as itself rather + * than as an unsupported {@link NullValue}. + * + * @param elements the members, each once, in the order the service sent them + */ + record SetValue(List elements) implements Value { + /** + * Creates a set, copying the members. + * + * @param elements the members, never {@code null} + * @throws IllegalArgumentException if a member is listed twice + */ + public SetValue { + elements = List.copyOf(elements); + for (int i = 0; i < elements.size(); i++) { + if (elements.subList(0, i).contains(elements.get(i))) { + throw new IllegalArgumentException("set lists a member twice: " + elements.get(i)); + } + } + } + + /** + * Number of members. + * + * @return the size + */ + public int size() { + return elements.size(); + } + + /** + * Whether the set has no members. + * + * @return {@code true} for the empty set + */ + public boolean isEmpty() { + return elements.isEmpty(); + } + + /** + * Whether a value is a member. + * + * @param value the value to look for + * @return {@code true} when the set holds it + */ + public boolean contains(Value value) { + return elements.contains(value); + } + + /** Order-insensitive: the same members in any order are the same set. */ + @Override + public boolean equals(Object other) { + return other instanceof SetValue that + && elements.size() == that.elements.size() + && elements.containsAll(that.elements); + } + + @Override + public int hashCode() { + return Set.copyOf(elements).hashCode(); + } + } + + /** + * A tensor of quantities of any rank: its shape and its components flattened in row-major order, + * each a {@link Quantity} with its own unit. A rank-one tensor stays a tensor, distinct from a + * {@link VectorQuantityValue}. + * + *

Only a service advertising the {@code tensor_values} capability reports one as itself + * rather than as an unsupported {@link NullValue}. + * + * @param dimensions the extent of each dimension, all positive + * @param components the components, row-major, exactly as many as the dimensions multiply to + */ + record TensorQuantityValue(List dimensions, List components) implements Value { + /** + * Creates a tensor, copying the dimensions and the components. + * + * @param dimensions the extent of each dimension, never {@code null} + * @param components the components, never {@code null} + * @throws IllegalArgumentException if a dimension is not positive, or the components do not + * fill the dimensions exactly + */ + public TensorQuantityValue { + dimensions = List.copyOf(dimensions); + components = List.copyOf(components); + long size = 1; + for (long extent : dimensions) { + if (extent <= 0) { + throw new IllegalArgumentException("tensor dimension is not positive: " + extent); + } + size = Math.multiplyExact(size, extent); + } + if (size != components.size()) { + throw new IllegalArgumentException( + "tensor of dimensions " + dimensions + " holds " + components.size() + + " component(s), want " + size); + } + } + + /** + * Number of dimensions. + * + * @return the rank + */ + public int rank() { + return dimensions.size(); + } + + /** + * The component at a multi-index, one coordinate per dimension. + * + * @param index the coordinates, each within its dimension + * @return the component there + * @throws IndexOutOfBoundsException if the index has the wrong rank or a coordinate is outside + * its dimension + */ + public Quantity get(long... index) { + if (index.length != dimensions.size()) { + throw new IndexOutOfBoundsException( + "index has " + index.length + " coordinate(s), tensor has rank " + dimensions.size()); + } + long flat = 0; + for (int i = 0; i < index.length; i++) { + long extent = dimensions.get(i); + if (index[i] < 0 || index[i] >= extent) { + throw new IndexOutOfBoundsException( + "coordinate " + index[i] + " is outside dimension " + i + " of extent " + extent); + } + flat = flat * extent + index[i]; + } + return components.get((int) flat); + } + + /** + * The one unit every component is written in, or empty when they differ. + * + * @return the shared unit as written, when there is one + */ + public Optional unit() { + Optional first = components.get(0).unit(); + for (Quantity component : components) { + if (!component.unit().equals(first)) { + return Optional.empty(); + } + } + return first; + } + } + /** * This value as a {@code double}, for the numeric arms. * diff --git a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/internal/Protos.java b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/internal/Protos.java index 9f742ff88..f2e2551d1 100644 --- a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/internal/Protos.java +++ b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/internal/Protos.java @@ -60,6 +60,8 @@ public static Optional value(org.openmbee.opensysml.proto.Value value) { case VECTOR_QUANTITY -> Optional.of(vectorQuantity(value.getVectorQuantity())); case MEASUREMENT_REF -> Optional.of(measurementRef(value.getMeasurementRef())); case FUNCTION -> Optional.of(function(value.getFunction())); + case SET -> Optional.of(set(value.getSet())); + case TENSOR_QUANTITY -> Optional.of(tensorQuantity(value.getTensorQuantity())); case KIND_NOT_SET -> Optional.empty(); }; } @@ -118,6 +120,33 @@ private static Value vectorQuantity(org.openmbee.opensysml.proto.VectorQuantity return new Value.VectorQuantityValue(components); } + private static Value set(org.openmbee.opensysml.proto.ValueSet set) { + List elements = new ArrayList<>(set.getElementsCount()); + for (org.openmbee.opensysml.proto.Value element : set.getElementsList()) { + elements.add(readable(element)); + } + try { + return new Value.SetValue(elements); + } catch (IllegalArgumentException malformed) { + throw new TransportException( + "the service answered a malformed set: " + malformed.getMessage(), malformed); + } + } + + private static Value tensorQuantity(org.openmbee.opensysml.proto.TensorQuantity tensor) { + List components = new ArrayList<>(tensor.getComponentsCount()); + for (org.openmbee.opensysml.proto.Quantity component : tensor.getComponentsList()) { + components.add(quantity(component)); + } + try { + return new Value.TensorQuantityValue(tensor.getDimensionsList(), components); + } catch (IllegalArgumentException | ArithmeticException malformed) { + throw new TransportException( + "the service answered a malformed tensor quantity: " + malformed.getMessage(), + malformed); + } + } + private static Value measurementRef(org.openmbee.opensysml.proto.MeasurementRef ref) { if (ref.getUnit().isEmpty() && ref.getUnitId().isEmpty() && !ref.hasUnitTerm()) { throw new TransportException( diff --git a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ServerInfoResponse.java b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ServerInfoResponse.java index 8872d5fff..4257eee4e 100644 --- a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ServerInfoResponse.java +++ b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ServerInfoResponse.java @@ -155,6 +155,18 @@ public java.lang.String getVersion() { * unsupported null, and one is accepted as an action input * or calc argument; without it, one is refused with * UNIMPLEMENTED rather than read as another value. + * "set_values" - a Value carries a unique, unordered collection (a + * Collections::Set's elements) as set, each element once in + * canonical order, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument in any order; without it, one is refused + * with UNIMPLEMENTED rather than read as a sequence. + * "tensor_values" - a Value carries a tensor quantity of any rank as + * tensor_quantity, its dimensions and one Quantity per + * row-major component, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument; without it, one is refused with + * UNIMPLEMENTED rather than read as another value. * "apply_edits" - the ApplyEdits RPC edits a parsed model's own source, * preserving everything the edit did not touch. * "document_query" - the RunDocumentQuery RPC runs a named document query @@ -227,6 +239,18 @@ public java.lang.String getVersion() { * unsupported null, and one is accepted as an action input * or calc argument; without it, one is refused with * UNIMPLEMENTED rather than read as another value. + * "set_values" - a Value carries a unique, unordered collection (a + * Collections::Set's elements) as set, each element once in + * canonical order, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument in any order; without it, one is refused + * with UNIMPLEMENTED rather than read as a sequence. + * "tensor_values" - a Value carries a tensor quantity of any rank as + * tensor_quantity, its dimensions and one Quantity per + * row-major component, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument; without it, one is refused with + * UNIMPLEMENTED rather than read as another value. * "apply_edits" - the ApplyEdits RPC edits a parsed model's own source, * preserving everything the edit did not touch. * "document_query" - the RunDocumentQuery RPC runs a named document query @@ -298,6 +322,18 @@ public int getCapabilitiesCount() { * unsupported null, and one is accepted as an action input * or calc argument; without it, one is refused with * UNIMPLEMENTED rather than read as another value. + * "set_values" - a Value carries a unique, unordered collection (a + * Collections::Set's elements) as set, each element once in + * canonical order, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument in any order; without it, one is refused + * with UNIMPLEMENTED rather than read as a sequence. + * "tensor_values" - a Value carries a tensor quantity of any rank as + * tensor_quantity, its dimensions and one Quantity per + * row-major component, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument; without it, one is refused with + * UNIMPLEMENTED rather than read as another value. * "apply_edits" - the ApplyEdits RPC edits a parsed model's own source, * preserving everything the edit did not touch. * "document_query" - the RunDocumentQuery RPC runs a named document query @@ -370,6 +406,18 @@ public java.lang.String getCapabilities(int index) { * unsupported null, and one is accepted as an action input * or calc argument; without it, one is refused with * UNIMPLEMENTED rather than read as another value. + * "set_values" - a Value carries a unique, unordered collection (a + * Collections::Set's elements) as set, each element once in + * canonical order, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument in any order; without it, one is refused + * with UNIMPLEMENTED rather than read as a sequence. + * "tensor_values" - a Value carries a tensor quantity of any rank as + * tensor_quantity, its dimensions and one Quantity per + * row-major component, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument; without it, one is refused with + * UNIMPLEMENTED rather than read as another value. * "apply_edits" - the ApplyEdits RPC edits a parsed model's own source, * preserving everything the edit did not touch. * "document_query" - the RunDocumentQuery RPC runs a named document query @@ -891,6 +939,18 @@ private void ensureCapabilitiesIsMutable() { * unsupported null, and one is accepted as an action input * or calc argument; without it, one is refused with * UNIMPLEMENTED rather than read as another value. + * "set_values" - a Value carries a unique, unordered collection (a + * Collections::Set's elements) as set, each element once in + * canonical order, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument in any order; without it, one is refused + * with UNIMPLEMENTED rather than read as a sequence. + * "tensor_values" - a Value carries a tensor quantity of any rank as + * tensor_quantity, its dimensions and one Quantity per + * row-major component, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument; without it, one is refused with + * UNIMPLEMENTED rather than read as another value. * "apply_edits" - the ApplyEdits RPC edits a parsed model's own source, * preserving everything the edit did not touch. * "document_query" - the RunDocumentQuery RPC runs a named document query @@ -964,6 +1024,18 @@ private void ensureCapabilitiesIsMutable() { * unsupported null, and one is accepted as an action input * or calc argument; without it, one is refused with * UNIMPLEMENTED rather than read as another value. + * "set_values" - a Value carries a unique, unordered collection (a + * Collections::Set's elements) as set, each element once in + * canonical order, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument in any order; without it, one is refused + * with UNIMPLEMENTED rather than read as a sequence. + * "tensor_values" - a Value carries a tensor quantity of any rank as + * tensor_quantity, its dimensions and one Quantity per + * row-major component, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument; without it, one is refused with + * UNIMPLEMENTED rather than read as another value. * "apply_edits" - the ApplyEdits RPC edits a parsed model's own source, * preserving everything the edit did not touch. * "document_query" - the RunDocumentQuery RPC runs a named document query @@ -1035,6 +1107,18 @@ public int getCapabilitiesCount() { * unsupported null, and one is accepted as an action input * or calc argument; without it, one is refused with * UNIMPLEMENTED rather than read as another value. + * "set_values" - a Value carries a unique, unordered collection (a + * Collections::Set's elements) as set, each element once in + * canonical order, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument in any order; without it, one is refused + * with UNIMPLEMENTED rather than read as a sequence. + * "tensor_values" - a Value carries a tensor quantity of any rank as + * tensor_quantity, its dimensions and one Quantity per + * row-major component, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument; without it, one is refused with + * UNIMPLEMENTED rather than read as another value. * "apply_edits" - the ApplyEdits RPC edits a parsed model's own source, * preserving everything the edit did not touch. * "document_query" - the RunDocumentQuery RPC runs a named document query @@ -1107,6 +1191,18 @@ public java.lang.String getCapabilities(int index) { * unsupported null, and one is accepted as an action input * or calc argument; without it, one is refused with * UNIMPLEMENTED rather than read as another value. + * "set_values" - a Value carries a unique, unordered collection (a + * Collections::Set's elements) as set, each element once in + * canonical order, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument in any order; without it, one is refused + * with UNIMPLEMENTED rather than read as a sequence. + * "tensor_values" - a Value carries a tensor quantity of any rank as + * tensor_quantity, its dimensions and one Quantity per + * row-major component, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument; without it, one is refused with + * UNIMPLEMENTED rather than read as another value. * "apply_edits" - the ApplyEdits RPC edits a parsed model's own source, * preserving everything the edit did not touch. * "document_query" - the RunDocumentQuery RPC runs a named document query @@ -1180,6 +1276,18 @@ public java.lang.String getCapabilities(int index) { * unsupported null, and one is accepted as an action input * or calc argument; without it, one is refused with * UNIMPLEMENTED rather than read as another value. + * "set_values" - a Value carries a unique, unordered collection (a + * Collections::Set's elements) as set, each element once in + * canonical order, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument in any order; without it, one is refused + * with UNIMPLEMENTED rather than read as a sequence. + * "tensor_values" - a Value carries a tensor quantity of any rank as + * tensor_quantity, its dimensions and one Quantity per + * row-major component, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument; without it, one is refused with + * UNIMPLEMENTED rather than read as another value. * "apply_edits" - the ApplyEdits RPC edits a parsed model's own source, * preserving everything the edit did not touch. * "document_query" - the RunDocumentQuery RPC runs a named document query @@ -1259,6 +1367,18 @@ public Builder setCapabilities( * unsupported null, and one is accepted as an action input * or calc argument; without it, one is refused with * UNIMPLEMENTED rather than read as another value. + * "set_values" - a Value carries a unique, unordered collection (a + * Collections::Set's elements) as set, each element once in + * canonical order, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument in any order; without it, one is refused + * with UNIMPLEMENTED rather than read as a sequence. + * "tensor_values" - a Value carries a tensor quantity of any rank as + * tensor_quantity, its dimensions and one Quantity per + * row-major component, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument; without it, one is refused with + * UNIMPLEMENTED rather than read as another value. * "apply_edits" - the ApplyEdits RPC edits a parsed model's own source, * preserving everything the edit did not touch. * "document_query" - the RunDocumentQuery RPC runs a named document query @@ -1337,6 +1457,18 @@ public Builder addCapabilities( * unsupported null, and one is accepted as an action input * or calc argument; without it, one is refused with * UNIMPLEMENTED rather than read as another value. + * "set_values" - a Value carries a unique, unordered collection (a + * Collections::Set's elements) as set, each element once in + * canonical order, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument in any order; without it, one is refused + * with UNIMPLEMENTED rather than read as a sequence. + * "tensor_values" - a Value carries a tensor quantity of any rank as + * tensor_quantity, its dimensions and one Quantity per + * row-major component, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument; without it, one is refused with + * UNIMPLEMENTED rather than read as another value. * "apply_edits" - the ApplyEdits RPC edits a parsed model's own source, * preserving everything the edit did not touch. * "document_query" - the RunDocumentQuery RPC runs a named document query @@ -1415,6 +1547,18 @@ public Builder addAllCapabilities( * unsupported null, and one is accepted as an action input * or calc argument; without it, one is refused with * UNIMPLEMENTED rather than read as another value. + * "set_values" - a Value carries a unique, unordered collection (a + * Collections::Set's elements) as set, each element once in + * canonical order, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument in any order; without it, one is refused + * with UNIMPLEMENTED rather than read as a sequence. + * "tensor_values" - a Value carries a tensor quantity of any rank as + * tensor_quantity, its dimensions and one Quantity per + * row-major component, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument; without it, one is refused with + * UNIMPLEMENTED rather than read as another value. * "apply_edits" - the ApplyEdits RPC edits a parsed model's own source, * preserving everything the edit did not touch. * "document_query" - the RunDocumentQuery RPC runs a named document query @@ -1490,6 +1634,18 @@ public Builder clearCapabilities() { * unsupported null, and one is accepted as an action input * or calc argument; without it, one is refused with * UNIMPLEMENTED rather than read as another value. + * "set_values" - a Value carries a unique, unordered collection (a + * Collections::Set's elements) as set, each element once in + * canonical order, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument in any order; without it, one is refused + * with UNIMPLEMENTED rather than read as a sequence. + * "tensor_values" - a Value carries a tensor quantity of any rank as + * tensor_quantity, its dimensions and one Quantity per + * row-major component, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument; without it, one is refused with + * UNIMPLEMENTED rather than read as another value. * "apply_edits" - the ApplyEdits RPC edits a parsed model's own source, * preserving everything the edit did not touch. * "document_query" - the RunDocumentQuery RPC runs a named document query diff --git a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ServerInfoResponseOrBuilder.java b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ServerInfoResponseOrBuilder.java index 132f9ea39..43e21bd97 100644 --- a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ServerInfoResponseOrBuilder.java +++ b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ServerInfoResponseOrBuilder.java @@ -84,6 +84,18 @@ public interface ServerInfoResponseOrBuilder extends * unsupported null, and one is accepted as an action input * or calc argument; without it, one is refused with * UNIMPLEMENTED rather than read as another value. + * "set_values" - a Value carries a unique, unordered collection (a + * Collections::Set's elements) as set, each element once in + * canonical order, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument in any order; without it, one is refused + * with UNIMPLEMENTED rather than read as a sequence. + * "tensor_values" - a Value carries a tensor quantity of any rank as + * tensor_quantity, its dimensions and one Quantity per + * row-major component, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument; without it, one is refused with + * UNIMPLEMENTED rather than read as another value. * "apply_edits" - the ApplyEdits RPC edits a parsed model's own source, * preserving everything the edit did not touch. * "document_query" - the RunDocumentQuery RPC runs a named document query @@ -154,6 +166,18 @@ public interface ServerInfoResponseOrBuilder extends * unsupported null, and one is accepted as an action input * or calc argument; without it, one is refused with * UNIMPLEMENTED rather than read as another value. + * "set_values" - a Value carries a unique, unordered collection (a + * Collections::Set's elements) as set, each element once in + * canonical order, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument in any order; without it, one is refused + * with UNIMPLEMENTED rather than read as a sequence. + * "tensor_values" - a Value carries a tensor quantity of any rank as + * tensor_quantity, its dimensions and one Quantity per + * row-major component, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument; without it, one is refused with + * UNIMPLEMENTED rather than read as another value. * "apply_edits" - the ApplyEdits RPC edits a parsed model's own source, * preserving everything the edit did not touch. * "document_query" - the RunDocumentQuery RPC runs a named document query @@ -223,6 +247,18 @@ public interface ServerInfoResponseOrBuilder extends * unsupported null, and one is accepted as an action input * or calc argument; without it, one is refused with * UNIMPLEMENTED rather than read as another value. + * "set_values" - a Value carries a unique, unordered collection (a + * Collections::Set's elements) as set, each element once in + * canonical order, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument in any order; without it, one is refused + * with UNIMPLEMENTED rather than read as a sequence. + * "tensor_values" - a Value carries a tensor quantity of any rank as + * tensor_quantity, its dimensions and one Quantity per + * row-major component, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument; without it, one is refused with + * UNIMPLEMENTED rather than read as another value. * "apply_edits" - the ApplyEdits RPC edits a parsed model's own source, * preserving everything the edit did not touch. * "document_query" - the RunDocumentQuery RPC runs a named document query @@ -293,6 +329,18 @@ public interface ServerInfoResponseOrBuilder extends * unsupported null, and one is accepted as an action input * or calc argument; without it, one is refused with * UNIMPLEMENTED rather than read as another value. + * "set_values" - a Value carries a unique, unordered collection (a + * Collections::Set's elements) as set, each element once in + * canonical order, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument in any order; without it, one is refused + * with UNIMPLEMENTED rather than read as a sequence. + * "tensor_values" - a Value carries a tensor quantity of any rank as + * tensor_quantity, its dimensions and one Quantity per + * row-major component, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument; without it, one is refused with + * UNIMPLEMENTED rather than read as another value. * "apply_edits" - the ApplyEdits RPC edits a parsed model's own source, * preserving everything the edit did not touch. * "document_query" - the RunDocumentQuery RPC runs a named document query diff --git a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Sysml.java b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Sysml.java index 59245032c..882fcbc18 100644 --- a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Sysml.java +++ b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Sysml.java @@ -301,6 +301,16 @@ public static void registerAllExtensions( static final com.google.protobuf.GeneratedMessage.FieldAccessorTable internal_static_sysml_Function_fieldAccessorTable; + static final com.google.protobuf.Descriptors.Descriptor + internal_static_sysml_ValueSet_descriptor; + static final + com.google.protobuf.GeneratedMessage.FieldAccessorTable + internal_static_sysml_ValueSet_fieldAccessorTable; + static final com.google.protobuf.Descriptors.Descriptor + internal_static_sysml_TensorQuantity_descriptor; + static final + com.google.protobuf.GeneratedMessage.FieldAccessorTable + internal_static_sysml_TensorQuantity_fieldAccessorTable; static final com.google.protobuf.Descriptors.Descriptor internal_static_sysml_Array_descriptor; static final @@ -706,7 +716,7 @@ public static void registerAllExtensions( "ower\030\001 \001(\tR\005lower\022\024\n\005upper\030\002 \001(\tR\005upper\"" + "o\n\rAttributeInfo\022\022\n\004name\030\001 \001(\tR\004name\022\022\n\004" + "type\030\002 \001(\tR\004type\022\"\n\005value\030\003 \001(\0132\014.sysml." + - "ValueR\005value\022\022\n\004unit\030\004 \001(\tR\004unit\"\316\005\n\005Val" + + "ValueR\005value\022\022\n\004unit\030\004 \001(\tR\004unit\"\265\006\n\005Val" + "ue\022\035\n\tint_value\030\001 \001(\003H\000R\010intValue\022\037\n\nrea" + "l_value\030\002 \001(\001H\000R\trealValue\022\037\n\nbool_value" + "\030\003 \001(\010H\000R\tboolValue\022#\n\014string_value\030\004 \001(" + @@ -724,180 +734,187 @@ public static void registerAllExtensions( "ty\022@\n\017measurement_ref\030\017 \001(\0132\025.sysml.Meas" + "urementRefH\000R\016measurementRef\022\034\n\010infinity" + "\030\020 \001(\010H\000R\010infinity\022-\n\010function\030\021 \001(\0132\017.s" + - "ysml.FunctionH\000R\010functionB\006\n\004kind\"<\n\010Fun" + - "ction\022\027\n\007calc_id\030\001 \001(\tR\006calcId\022\027\n\007self_i" + - "d\030\002 \001(\003R\006selfId\"Q\n\005Array\022\036\n\ndimensions\030\001" + - " \003(\003R\ndimensions\022(\n\010elements\030\002 \003(\0132\014.sys" + - "ml.ValueR\010elements\"6\n\006Vector\022,\n\ncomponen" + - "ts\030\001 \003(\0132\014.sysml.ValueR\ncomponents\"A\n\016Ve" + - "ctorQuantity\022/\n\ncomponents\030\001 \003(\0132\017.sysml" + - ".QuantityR\ncomponents\";\n\007Complex\022\022\n\004real" + - "\030\001 \001(\001R\004real\022\034\n\timaginary\030\002 \001(\001R\timagina" + - "ry\"g\n\013EnumLiteral\022\035\n\nliteral_id\030\001 \001(\tR\tl" + - "iteralId\022%\n\016enumeration_id\030\002 \001(\tR\renumer" + - "ationId\022\022\n\004name\030\003 \001(\tR\004name\"9\n\rValueSequ" + - "ence\022(\n\010elements\030\001 \003(\0132\014.sysml.ValueR\010el" + - "ements\"\251\001\n\010Quantity\022%\n\rint_magnitude\030\001 \001" + - "(\003H\000R\014intMagnitude\022\'\n\016real_magnitude\030\002 \001" + - "(\001H\000R\rrealMagnitude\022\022\n\004unit\030\003 \001(\tR\004unit\022" + - ",\n\tunit_term\030\004 \001(\0132\017.sysml.UnitTermR\010uni" + - "tTermB\013\n\tmagnitude\"k\n\016MeasurementRef\022\022\n\004" + - "unit\030\001 \001(\tR\004unit\022,\n\tunit_term\030\002 \001(\0132\017.sy" + - "sml.UnitTermR\010unitTerm\022\027\n\007unit_id\030\003 \001(\tR" + - "\006unitId\"q\n\010UnitTerm\022\033\n\tscale_num\030\001 \001(\001R\010" + - "scaleNum\022\033\n\tscale_den\030\002 \001(\001R\010scaleDen\022+\n" + - "\007factors\030\003 \003(\0132\021.sysml.UnitFactorR\007facto" + - "rs\"A\n\nUnitFactor\022\027\n\007unit_id\030\001 \001(\tR\006unitI" + - "d\022\032\n\010exponent\030\002 \001(\001R\010exponent\"w\n\nDiagnos" + - "tic\022\032\n\010severity\030\001 \001(\tR\010severity\022\030\n\007messa" + - "ge\030\002 \001(\tR\007message\022\037\n\004span\030\003 \001(\0132\013.sysml." + - "SpanR\004span\022\022\n\004code\030\004 \001(\tR\004code\"\212\001\n\004Span\022" + - "\022\n\004file\030\001 \001(\tR\004file\022\035\n\nstart_line\030\002 \001(\005R" + - "\tstartLine\022\033\n\tstart_col\030\003 \001(\005R\010startCol\022" + - "\031\n\010end_line\030\004 \001(\005R\007endLine\022\027\n\007end_col\030\005 " + - "\001(\005R\006endCol\"\023\n\021ServerInfoRequest\"R\n\022Serv" + - "erInfoResponse\022\030\n\007version\030\001 \001(\tR\007version" + - "\022\"\n\014capabilities\030\002 \003(\tR\014capabilities\"p\n\014" + - "QueryRequest\022\035\n\nmodel_hash\030\001 \001(\tR\tmodelH" + - "ash\022\"\n\005query\030\002 \001(\0132\014.sysml.QueryR\005query\022" + - "\035\n\noslc_query\030\003 \001(\tR\toslcQuery\"F\n\rQueryR" + - "esponse\0225\n\010elements\030\001 \003(\0132\031.sysml.QueryR" + - "esultElementR\010elements\"^\n\005Query\022\024\n\005scope" + - "\030\001 \003(\tR\005scope\022\026\n\006select\030\002 \003(\tR\006select\022\'\n" + - "\005where\030\003 \001(\0132\021.sysml.ConstraintR\005where\"\222" + - "\001\n\nConstraint\022:\n\tprimitive\030\001 \001(\0132\032.sysml" + - ".PrimitiveConstraintH\000R\tprimitive\022:\n\tcom" + - "posite\030\002 \001(\0132\032.sysml.CompositeConstraint" + - "H\000R\tcompositeB\014\n\nconstraint\"\227\001\n\023Primitiv" + - "eConstraint\022\030\n\007inverse\030\001 \001(\010R\007inverse\022\032\n" + - "\010property\030\002 \001(\tR\010property\0224\n\010operator\030\003 " + - "\001(\0162\030.sysml.PrimitiveOperatorR\010operator\022" + - "\024\n\005value\030\004 \003(\tR\005value\"~\n\023CompositeConstr" + - "aint\0224\n\010operator\030\001 \001(\0162\030.sysml.Composite" + - "OperatorR\010operator\0221\n\nconstraint\030\002 \003(\0132\021" + - ".sysml.ConstraintR\nconstraint\"\302\001\n\022QueryR" + - "esultElement\022\016\n\002id\030\001 \001(\tR\002id\022\022\n\004type\030\002 \001" + - "(\tR\004type\022I\n\nproperties\030\003 \003(\0132).sysml.Que" + - "ryResultElement.PropertiesEntryR\npropert" + - "ies\032=\n\017PropertiesEntry\022\020\n\003key\030\001 \001(\tR\003key" + - "\022\024\n\005value\030\002 \001(\tR\005value:\0028\001\"\220\001\n\nSweepRang" + - "e\022\034\n\tparameter\030\001 \001(\tR\tparameter\022\"\n\005start" + - "\030\002 \001(\0132\014.sysml.ValueR\005start\022\036\n\003end\030\003 \001(\013" + - "2\014.sysml.ValueR\003end\022 \n\004step\030\004 \001(\0132\014.sysm" + - "l.ValueR\004step\"\244\003\n\017RunSweepRequest\022\035\n\nmod" + - "el_hash\030\001 \001(\tR\tmodelHash\022\033\n\tsymbol_id\030\002 " + - "\001(\tR\010symbolId\022*\n\021subject_symbol_id\030\003 \001(\t" + - "R\017subjectSymbolId\022*\n\targuments\030\004 \003(\0132\014.s" + - "ysml.ValueR\targuments\022S\n\017named_arguments" + - "\030\005 \003(\0132*.sysml.RunSweepRequest.NamedArgu" + - "mentsEntryR\016namedArguments\022)\n\006ranges\030\006 \003" + - "(\0132\021.sysml.SweepRangeR\006ranges\022\030\n\007samples" + - "\030\007 \001(\003R\007samples\022\022\n\004seed\030\010 \001(\004R\004seed\032O\n\023N" + - "amedArgumentsEntry\022\020\n\003key\030\001 \001(\tR\003key\022\"\n\005" + - "value\030\002 \001(\0132\014.sysml.ValueR\005value:\0028\001\"\210\002\n" + - "\010SweepRow\022)\n\006inputs\030\001 \003(\0132\021.sysml.CalcOu" + - "tputR\006inputs\022+\n\007outputs\030\002 \003(\0132\021.sysml.Ca" + - "lcOutputR\007outputs\022*\n\010verdicts\030\003 \003(\0132\016.sy" + - "sml.VerdictR\010verdicts\022%\n\016elapsed_micros\030" + - "\004 \001(\003R\relapsedMicros\022\024\n\005error\030\005 \001(\tR\005err" + - "or\022;\n\016failure_reason\030\006 \001(\0162\024.sysml.Failu" + - "reReasonR\rfailureReason\"\274\002\n\020RunSweepResp" + - "onse\022#\n\004rows\030\001 \003(\0132\017.sysml.SweepRowR\004row" + - "s\022\036\n\nparameters\030\002 \003(\tR\nparameters\022\030\n\007sam" + - "pled\030\003 \001(\010R\007sampled\022\022\n\004seed\030\004 \001(\004R\004seed\022" + - "\024\n\005error\030\005 \001(\tR\005error\0223\n\013diagnostics\030\006 \003" + - "(\0132\021.sysml.DiagnosticR\013diagnostics\022;\n\016fa" + - "ilure_reason\030\007 \001(\0162\024.sysml.FailureReason" + - "R\rfailureReason\022-\n\tinstances\030\010 \003(\0132\017.sys" + - "ml.InstanceR\tinstances\"\214\001\n\027RunDocumentQu" + - "eryRequest\022\035\n\nmodel_hash\030\001 \001(\tR\tmodelHas" + - "h\022\031\n\010query_id\030\002 \001(\tR\007queryId\0227\n\010bindings" + - "\030\003 \003(\0132\033.sysml.DocumentQueryBindingR\010bin" + - "dings\"b\n\024DocumentQueryBinding\022\034\n\tparamet" + - "er\030\001 \001(\tR\tparameter\022,\n\006values\030\002 \003(\0132\024.sy" + - "sml.DocumentValueR\006values\"\256\002\n\rDocumentVa" + - "lue\022\037\n\nelement_id\030\001 \001(\tH\000R\telementId\022#\n\014" + - "string_value\030\002 \001(\tH\000R\013stringValue\022\035\n\tint" + - "_value\030\003 \001(\003H\000R\010intValue\022\037\n\nreal_value\030\004" + - " \001(\001H\000R\trealValue\022\037\n\nbool_value\030\005 \001(\010H\000R" + - "\tboolValue\022\034\n\010infinity\030\006 \001(\010H\000R\010infinity" + - "\022-\n\010quantity\030\010 \001(\0132\017.sysml.QuantityH\000R\010q" + - "uantity\022!\n\014element_type\030\007 \001(\tR\013elementTy" + - "peB\006\n\004kind\")\n\023DocumentQueryColumn\022\022\n\004nam" + - "e\030\001 \001(\tR\004name\"A\n\021DocumentQueryCell\022,\n\006va" + - "lues\030\001 \003(\0132\024.sysml.DocumentValueR\006values" + - "\"r\n\020DocumentQueryRow\022.\n\007element\030\001 \001(\0132\024." + - "sysml.DocumentValueR\007element\022.\n\005cells\030\002 " + - "\003(\0132\030.sysml.DocumentQueryCellR\005cells\"}\n\030" + - "RunDocumentQueryResponse\0224\n\007columns\030\001 \003(" + - "\0132\032.sysml.DocumentQueryColumnR\007columns\022+" + - "\n\004rows\030\002 \003(\0132\027.sysml.DocumentQueryRowR\004r" + - "ows\"W\n\025RenderDocumentRequest\022\035\n\nmodel_ha" + - "sh\030\001 \001(\tR\tmodelHash\022\037\n\013document_id\030\002 \001(\t" + - "R\ndocumentId\"4\n\026RenderDocumentResponse\022\032" + - "\n\010markdown\030\001 \001(\tR\010markdown*\223\001\n\rFailureRe" + - "ason\022\036\n\032FAILURE_REASON_UNSPECIFIED\020\000\022\035\n\031" + - "FAILURE_REASON_EVALUATION\020\001\022\035\n\031FAILURE_R" + - "EASON_WRONG_KIND\020\002\022$\n FAILURE_REASON_AMB" + - "IGUOUS_SUBJECT\020\003*\235\004\n\013EditFailure\022\034\n\030EDIT" + - "_FAILURE_UNSPECIFIED\020\000\022\036\n\032EDIT_FAILURE_N" + - "O_OPERATIONS\020\001\022\037\n\033EDIT_FAILURE_UNKNOWN_T" + - "ARGET\020\002\022!\n\035EDIT_FAILURE_AMBIGUOUS_TARGET" + - "\020\003\022\033\n\027EDIT_FAILURE_NOT_VALUED\020\004\022\036\n\032EDIT_" + - "FAILURE_INVALID_VALUE\020\005\022\035\n\031EDIT_FAILURE_" + - "INVALID_NAME\020\006\022\032\n\026EDIT_FAILURE_NOT_NAMED" + - "\020\007\022\"\n\036EDIT_FAILURE_RENAME_REFERENCED\020\010\022\"" + - "\n\036EDIT_FAILURE_OVERLAPPING_EDITS\020\t\022\037\n\033ED" + - "IT_FAILURE_RESULT_INVALID\020\n\022\036\n\032EDIT_FAIL" + - "URE_OWNER_UNKNOWN\020\013\022$\n EDIT_FAILURE_OWNE" + - "R_NOT_NAMESPACE\020\014\022\035\n\031EDIT_FAILURE_ILLEGA" + - "L_KIND\020\r\022\"\n\036EDIT_FAILURE_MEMBER_NAME_TAK" + - "EN\020\016\022\"\n\036EDIT_FAILURE_DELETE_REFERENCED\020\017" + - "*\222\001\n\021PrimitiveOperator\022\"\n\036PRIMITIVE_OPER" + - "ATOR_UNSPECIFIED\020\000\022\034\n\030PRIMITIVE_OPERATOR" + - "_EQUAL\020\001\022\036\n\032PRIMITIVE_OPERATOR_GREATER\020\002" + - "\022\033\n\027PRIMITIVE_OPERATOR_LESS\020\003*n\n\021Composi" + - "teOperator\022\"\n\036COMPOSITE_OPERATOR_UNSPECI" + - "FIED\020\000\022\032\n\026COMPOSITE_OPERATOR_AND\020\001\022\031\n\025CO" + - "MPOSITE_OPERATOR_OR\020\0022\244\013\n\014SysMLService\022D" + - "\n\rGetServerInfo\022\030.sysml.ServerInfoReques" + - "t\032\031.sysml.ServerInfoResponse\022>\n\tParseFil" + - "e\022\027.sysml.ParseFileRequest\032\030.sysml.Parse" + - "FileResponse\022G\n\014ParseSources\022\032.sysml.Par" + - "seSourcesRequest\032\033.sysml.ParseSourcesRes" + - "ponse\022;\n\tGetSymbol\022\027.sysml.GetSymbolRequ" + - "est\032\025.sysml.SymbolResponse\022G\n\016GetDiagnos" + - "tics\022\031.sysml.DiagnosticsRequest\032\032.sysml." + - "DiagnosticsResponse\022;\n\010Evaluate\022\026.sysml." + - "EvaluateRequest\032\027.sysml.EvaluateResponse" + - "\022D\n\013Instantiate\022\031.sysml.InstantiateReque" + - "st\032\032.sysml.InstantiateResponse\022J\n\rExecut" + - "eAction\022\033.sysml.ExecuteActionRequest\032\034.s" + - "ysml.ExecuteActionResponse\022G\n\014ExecuteSta" + - "te\022\032.sysml.ExecuteStateRequest\032\033.sysml.E" + - "xecuteStateResponse\0228\n\007Convert\022\025.sysml.C" + - "onvertRequest\032\026.sysml.ConvertResponse\022A\n" + - "\nApplyEdits\022\030.sysml.ApplyEditsRequest\032\031." + - "sysml.ApplyEditsResponse\022S\n\020VerifyConstr" + - "aint\022\036.sysml.VerifyConstraintRequest\032\037.s" + - "ysml.VerifyConstraintResponse\022V\n\021VerifyR" + - "equirement\022\037.sysml.VerifyRequirementRequ" + - "est\032 .sysml.VerifyRequirementResponse\022Y\n" + - "\022VerifySatisfaction\022 .sysml.VerifySatisf" + - "actionRequest\032!.sysml.VerifySatisfaction" + - "Response\022G\n\014EvaluateCalc\022\032.sysml.Evaluat" + - "eCalcRequest\032\033.sysml.EvaluateCalcRespons" + - "e\022D\n\013RunAnalysis\022\031.sysml.RunAnalysisRequ" + - "est\032\032.sysml.RunAnalysisResponse\022;\n\010RunSw" + - "eep\022\026.sysml.RunSweepRequest\032\027.sysml.RunS", - "weepResponse\0222\n\005Query\022\023.sysml.QueryReque" + - "st\032\024.sysml.QueryResponse\022S\n\020RunDocumentQ" + - "uery\022\036.sysml.RunDocumentQueryRequest\032\037.s" + - "ysml.RunDocumentQueryResponse\022M\n\016RenderD" + - "ocument\022\034.sysml.RenderDocumentRequest\032\035." + - "sysml.RenderDocumentResponseBJ\n\034org.open" + - "mbee.opensysml.protoP\001Z(github.com/Open-" + - "MBEE/OpenSysML/api/protob\006proto3" + "ysml.FunctionH\000R\010function\022#\n\003set\030\022 \001(\0132\017" + + ".sysml.ValueSetH\000R\003set\022@\n\017tensor_quantit" + + "y\030\023 \001(\0132\025.sysml.TensorQuantityH\000R\016tensor" + + "QuantityB\006\n\004kind\"<\n\010Function\022\027\n\007calc_id\030" + + "\001 \001(\tR\006calcId\022\027\n\007self_id\030\002 \001(\003R\006selfId\"4" + + "\n\010ValueSet\022(\n\010elements\030\001 \003(\0132\014.sysml.Val" + + "ueR\010elements\"a\n\016TensorQuantity\022\036\n\ndimens" + + "ions\030\001 \003(\003R\ndimensions\022/\n\ncomponents\030\002 \003" + + "(\0132\017.sysml.QuantityR\ncomponents\"Q\n\005Array" + + "\022\036\n\ndimensions\030\001 \003(\003R\ndimensions\022(\n\010elem" + + "ents\030\002 \003(\0132\014.sysml.ValueR\010elements\"6\n\006Ve" + + "ctor\022,\n\ncomponents\030\001 \003(\0132\014.sysml.ValueR\n" + + "components\"A\n\016VectorQuantity\022/\n\ncomponen" + + "ts\030\001 \003(\0132\017.sysml.QuantityR\ncomponents\";\n" + + "\007Complex\022\022\n\004real\030\001 \001(\001R\004real\022\034\n\timaginar" + + "y\030\002 \001(\001R\timaginary\"g\n\013EnumLiteral\022\035\n\nlit" + + "eral_id\030\001 \001(\tR\tliteralId\022%\n\016enumeration_" + + "id\030\002 \001(\tR\renumerationId\022\022\n\004name\030\003 \001(\tR\004n" + + "ame\"9\n\rValueSequence\022(\n\010elements\030\001 \003(\0132\014" + + ".sysml.ValueR\010elements\"\251\001\n\010Quantity\022%\n\ri" + + "nt_magnitude\030\001 \001(\003H\000R\014intMagnitude\022\'\n\016re" + + "al_magnitude\030\002 \001(\001H\000R\rrealMagnitude\022\022\n\004u" + + "nit\030\003 \001(\tR\004unit\022,\n\tunit_term\030\004 \001(\0132\017.sys" + + "ml.UnitTermR\010unitTermB\013\n\tmagnitude\"k\n\016Me" + + "asurementRef\022\022\n\004unit\030\001 \001(\tR\004unit\022,\n\tunit" + + "_term\030\002 \001(\0132\017.sysml.UnitTermR\010unitTerm\022\027" + + "\n\007unit_id\030\003 \001(\tR\006unitId\"q\n\010UnitTerm\022\033\n\ts" + + "cale_num\030\001 \001(\001R\010scaleNum\022\033\n\tscale_den\030\002 " + + "\001(\001R\010scaleDen\022+\n\007factors\030\003 \003(\0132\021.sysml.U" + + "nitFactorR\007factors\"A\n\nUnitFactor\022\027\n\007unit" + + "_id\030\001 \001(\tR\006unitId\022\032\n\010exponent\030\002 \001(\001R\010exp" + + "onent\"w\n\nDiagnostic\022\032\n\010severity\030\001 \001(\tR\010s" + + "everity\022\030\n\007message\030\002 \001(\tR\007message\022\037\n\004spa" + + "n\030\003 \001(\0132\013.sysml.SpanR\004span\022\022\n\004code\030\004 \001(\t" + + "R\004code\"\212\001\n\004Span\022\022\n\004file\030\001 \001(\tR\004file\022\035\n\ns" + + "tart_line\030\002 \001(\005R\tstartLine\022\033\n\tstart_col\030" + + "\003 \001(\005R\010startCol\022\031\n\010end_line\030\004 \001(\005R\007endLi" + + "ne\022\027\n\007end_col\030\005 \001(\005R\006endCol\"\023\n\021ServerInf" + + "oRequest\"R\n\022ServerInfoResponse\022\030\n\007versio" + + "n\030\001 \001(\tR\007version\022\"\n\014capabilities\030\002 \003(\tR\014" + + "capabilities\"p\n\014QueryRequest\022\035\n\nmodel_ha" + + "sh\030\001 \001(\tR\tmodelHash\022\"\n\005query\030\002 \001(\0132\014.sys" + + "ml.QueryR\005query\022\035\n\noslc_query\030\003 \001(\tR\tosl" + + "cQuery\"F\n\rQueryResponse\0225\n\010elements\030\001 \003(" + + "\0132\031.sysml.QueryResultElementR\010elements\"^" + + "\n\005Query\022\024\n\005scope\030\001 \003(\tR\005scope\022\026\n\006select\030" + + "\002 \003(\tR\006select\022\'\n\005where\030\003 \001(\0132\021.sysml.Con" + + "straintR\005where\"\222\001\n\nConstraint\022:\n\tprimiti" + + "ve\030\001 \001(\0132\032.sysml.PrimitiveConstraintH\000R\t" + + "primitive\022:\n\tcomposite\030\002 \001(\0132\032.sysml.Com" + + "positeConstraintH\000R\tcompositeB\014\n\nconstra" + + "int\"\227\001\n\023PrimitiveConstraint\022\030\n\007inverse\030\001" + + " \001(\010R\007inverse\022\032\n\010property\030\002 \001(\tR\010propert" + + "y\0224\n\010operator\030\003 \001(\0162\030.sysml.PrimitiveOpe" + + "ratorR\010operator\022\024\n\005value\030\004 \003(\tR\005value\"~\n" + + "\023CompositeConstraint\0224\n\010operator\030\001 \001(\0162\030" + + ".sysml.CompositeOperatorR\010operator\0221\n\nco" + + "nstraint\030\002 \003(\0132\021.sysml.ConstraintR\nconst" + + "raint\"\302\001\n\022QueryResultElement\022\016\n\002id\030\001 \001(\t" + + "R\002id\022\022\n\004type\030\002 \001(\tR\004type\022I\n\nproperties\030\003" + + " \003(\0132).sysml.QueryResultElement.Properti" + + "esEntryR\nproperties\032=\n\017PropertiesEntry\022\020" + + "\n\003key\030\001 \001(\tR\003key\022\024\n\005value\030\002 \001(\tR\005value:\002" + + "8\001\"\220\001\n\nSweepRange\022\034\n\tparameter\030\001 \001(\tR\tpa" + + "rameter\022\"\n\005start\030\002 \001(\0132\014.sysml.ValueR\005st" + + "art\022\036\n\003end\030\003 \001(\0132\014.sysml.ValueR\003end\022 \n\004s" + + "tep\030\004 \001(\0132\014.sysml.ValueR\004step\"\244\003\n\017RunSwe" + + "epRequest\022\035\n\nmodel_hash\030\001 \001(\tR\tmodelHash" + + "\022\033\n\tsymbol_id\030\002 \001(\tR\010symbolId\022*\n\021subject" + + "_symbol_id\030\003 \001(\tR\017subjectSymbolId\022*\n\targ" + + "uments\030\004 \003(\0132\014.sysml.ValueR\targuments\022S\n" + + "\017named_arguments\030\005 \003(\0132*.sysml.RunSweepR" + + "equest.NamedArgumentsEntryR\016namedArgumen" + + "ts\022)\n\006ranges\030\006 \003(\0132\021.sysml.SweepRangeR\006r" + + "anges\022\030\n\007samples\030\007 \001(\003R\007samples\022\022\n\004seed\030" + + "\010 \001(\004R\004seed\032O\n\023NamedArgumentsEntry\022\020\n\003ke" + + "y\030\001 \001(\tR\003key\022\"\n\005value\030\002 \001(\0132\014.sysml.Valu" + + "eR\005value:\0028\001\"\210\002\n\010SweepRow\022)\n\006inputs\030\001 \003(" + + "\0132\021.sysml.CalcOutputR\006inputs\022+\n\007outputs\030" + + "\002 \003(\0132\021.sysml.CalcOutputR\007outputs\022*\n\010ver" + + "dicts\030\003 \003(\0132\016.sysml.VerdictR\010verdicts\022%\n" + + "\016elapsed_micros\030\004 \001(\003R\relapsedMicros\022\024\n\005" + + "error\030\005 \001(\tR\005error\022;\n\016failure_reason\030\006 \001" + + "(\0162\024.sysml.FailureReasonR\rfailureReason\"" + + "\274\002\n\020RunSweepResponse\022#\n\004rows\030\001 \003(\0132\017.sys" + + "ml.SweepRowR\004rows\022\036\n\nparameters\030\002 \003(\tR\np" + + "arameters\022\030\n\007sampled\030\003 \001(\010R\007sampled\022\022\n\004s" + + "eed\030\004 \001(\004R\004seed\022\024\n\005error\030\005 \001(\tR\005error\0223\n" + + "\013diagnostics\030\006 \003(\0132\021.sysml.DiagnosticR\013d" + + "iagnostics\022;\n\016failure_reason\030\007 \001(\0162\024.sys" + + "ml.FailureReasonR\rfailureReason\022-\n\tinsta" + + "nces\030\010 \003(\0132\017.sysml.InstanceR\tinstances\"\214" + + "\001\n\027RunDocumentQueryRequest\022\035\n\nmodel_hash" + + "\030\001 \001(\tR\tmodelHash\022\031\n\010query_id\030\002 \001(\tR\007que" + + "ryId\0227\n\010bindings\030\003 \003(\0132\033.sysml.DocumentQ" + + "ueryBindingR\010bindings\"b\n\024DocumentQueryBi" + + "nding\022\034\n\tparameter\030\001 \001(\tR\tparameter\022,\n\006v" + + "alues\030\002 \003(\0132\024.sysml.DocumentValueR\006value" + + "s\"\256\002\n\rDocumentValue\022\037\n\nelement_id\030\001 \001(\tH" + + "\000R\telementId\022#\n\014string_value\030\002 \001(\tH\000R\013st" + + "ringValue\022\035\n\tint_value\030\003 \001(\003H\000R\010intValue" + + "\022\037\n\nreal_value\030\004 \001(\001H\000R\trealValue\022\037\n\nboo" + + "l_value\030\005 \001(\010H\000R\tboolValue\022\034\n\010infinity\030\006" + + " \001(\010H\000R\010infinity\022-\n\010quantity\030\010 \001(\0132\017.sys" + + "ml.QuantityH\000R\010quantity\022!\n\014element_type\030" + + "\007 \001(\tR\013elementTypeB\006\n\004kind\")\n\023DocumentQu" + + "eryColumn\022\022\n\004name\030\001 \001(\tR\004name\"A\n\021Documen" + + "tQueryCell\022,\n\006values\030\001 \003(\0132\024.sysml.Docum" + + "entValueR\006values\"r\n\020DocumentQueryRow\022.\n\007" + + "element\030\001 \001(\0132\024.sysml.DocumentValueR\007ele" + + "ment\022.\n\005cells\030\002 \003(\0132\030.sysml.DocumentQuer" + + "yCellR\005cells\"}\n\030RunDocumentQueryResponse" + + "\0224\n\007columns\030\001 \003(\0132\032.sysml.DocumentQueryC" + + "olumnR\007columns\022+\n\004rows\030\002 \003(\0132\027.sysml.Doc" + + "umentQueryRowR\004rows\"W\n\025RenderDocumentReq" + + "uest\022\035\n\nmodel_hash\030\001 \001(\tR\tmodelHash\022\037\n\013d" + + "ocument_id\030\002 \001(\tR\ndocumentId\"4\n\026RenderDo" + + "cumentResponse\022\032\n\010markdown\030\001 \001(\tR\010markdo" + + "wn*\223\001\n\rFailureReason\022\036\n\032FAILURE_REASON_U" + + "NSPECIFIED\020\000\022\035\n\031FAILURE_REASON_EVALUATIO" + + "N\020\001\022\035\n\031FAILURE_REASON_WRONG_KIND\020\002\022$\n FA" + + "ILURE_REASON_AMBIGUOUS_SUBJECT\020\003*\235\004\n\013Edi" + + "tFailure\022\034\n\030EDIT_FAILURE_UNSPECIFIED\020\000\022\036" + + "\n\032EDIT_FAILURE_NO_OPERATIONS\020\001\022\037\n\033EDIT_F" + + "AILURE_UNKNOWN_TARGET\020\002\022!\n\035EDIT_FAILURE_" + + "AMBIGUOUS_TARGET\020\003\022\033\n\027EDIT_FAILURE_NOT_V" + + "ALUED\020\004\022\036\n\032EDIT_FAILURE_INVALID_VALUE\020\005\022" + + "\035\n\031EDIT_FAILURE_INVALID_NAME\020\006\022\032\n\026EDIT_F" + + "AILURE_NOT_NAMED\020\007\022\"\n\036EDIT_FAILURE_RENAM" + + "E_REFERENCED\020\010\022\"\n\036EDIT_FAILURE_OVERLAPPI" + + "NG_EDITS\020\t\022\037\n\033EDIT_FAILURE_RESULT_INVALI" + + "D\020\n\022\036\n\032EDIT_FAILURE_OWNER_UNKNOWN\020\013\022$\n E" + + "DIT_FAILURE_OWNER_NOT_NAMESPACE\020\014\022\035\n\031EDI" + + "T_FAILURE_ILLEGAL_KIND\020\r\022\"\n\036EDIT_FAILURE" + + "_MEMBER_NAME_TAKEN\020\016\022\"\n\036EDIT_FAILURE_DEL" + + "ETE_REFERENCED\020\017*\222\001\n\021PrimitiveOperator\022\"" + + "\n\036PRIMITIVE_OPERATOR_UNSPECIFIED\020\000\022\034\n\030PR" + + "IMITIVE_OPERATOR_EQUAL\020\001\022\036\n\032PRIMITIVE_OP" + + "ERATOR_GREATER\020\002\022\033\n\027PRIMITIVE_OPERATOR_L" + + "ESS\020\003*n\n\021CompositeOperator\022\"\n\036COMPOSITE_" + + "OPERATOR_UNSPECIFIED\020\000\022\032\n\026COMPOSITE_OPER" + + "ATOR_AND\020\001\022\031\n\025COMPOSITE_OPERATOR_OR\020\0022\244\013" + + "\n\014SysMLService\022D\n\rGetServerInfo\022\030.sysml." + + "ServerInfoRequest\032\031.sysml.ServerInfoResp" + + "onse\022>\n\tParseFile\022\027.sysml.ParseFileReque" + + "st\032\030.sysml.ParseFileResponse\022G\n\014ParseSou" + + "rces\022\032.sysml.ParseSourcesRequest\032\033.sysml" + + ".ParseSourcesResponse\022;\n\tGetSymbol\022\027.sys" + + "ml.GetSymbolRequest\032\025.sysml.SymbolRespon" + + "se\022G\n\016GetDiagnostics\022\031.sysml.Diagnostics" + + "Request\032\032.sysml.DiagnosticsResponse\022;\n\010E" + + "valuate\022\026.sysml.EvaluateRequest\032\027.sysml." + + "EvaluateResponse\022D\n\013Instantiate\022\031.sysml." + + "InstantiateRequest\032\032.sysml.InstantiateRe" + + "sponse\022J\n\rExecuteAction\022\033.sysml.ExecuteA" + + "ctionRequest\032\034.sysml.ExecuteActionRespon" + + "se\022G\n\014ExecuteState\022\032.sysml.ExecuteStateR" + + "equest\032\033.sysml.ExecuteStateResponse\0228\n\007C" + + "onvert\022\025.sysml.ConvertRequest\032\026.sysml.Co" + + "nvertResponse\022A\n\nApplyEdits\022\030.sysml.Appl" + + "yEditsRequest\032\031.sysml.ApplyEditsResponse" + + "\022S\n\020VerifyConstraint\022\036.sysml.VerifyConst" + + "raintRequest\032\037.sysml.VerifyConstraintRes" + + "ponse\022V\n\021VerifyRequirement\022\037.sysml.Verif" + + "yRequirementRequest\032 .sysml.VerifyRequir" + + "ementResponse\022Y\n\022VerifySatisfaction\022 .sy", + "sml.VerifySatisfactionRequest\032!.sysml.Ve" + + "rifySatisfactionResponse\022G\n\014EvaluateCalc" + + "\022\032.sysml.EvaluateCalcRequest\032\033.sysml.Eva" + + "luateCalcResponse\022D\n\013RunAnalysis\022\031.sysml" + + ".RunAnalysisRequest\032\032.sysml.RunAnalysisR" + + "esponse\022;\n\010RunSweep\022\026.sysml.RunSweepRequ" + + "est\032\027.sysml.RunSweepResponse\0222\n\005Query\022\023." + + "sysml.QueryRequest\032\024.sysml.QueryResponse" + + "\022S\n\020RunDocumentQuery\022\036.sysml.RunDocument" + + "QueryRequest\032\037.sysml.RunDocumentQueryRes" + + "ponse\022M\n\016RenderDocument\022\034.sysml.RenderDo" + + "cumentRequest\032\035.sysml.RenderDocumentResp" + + "onseBJ\n\034org.openmbee.opensysml.protoP\001Z(" + + "github.com/Open-MBEE/OpenSysML/api/proto" + + "b\006proto3" }; descriptor = com.google.protobuf.Descriptors.FileDescriptor .internalBuildGeneratedFileFrom(descriptorData, @@ -1226,135 +1243,147 @@ public static void registerAllExtensions( internal_static_sysml_Value_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_Value_descriptor, - new java.lang.String[] { "IntValue", "RealValue", "BoolValue", "StringValue", "InstanceId", "Sequence", "Null", "Quantity", "EnumLiteral", "Unset", "Complex", "Array", "Vector", "VectorQuantity", "MeasurementRef", "Infinity", "Function", "Kind", }); + new java.lang.String[] { "IntValue", "RealValue", "BoolValue", "StringValue", "InstanceId", "Sequence", "Null", "Quantity", "EnumLiteral", "Unset", "Complex", "Array", "Vector", "VectorQuantity", "MeasurementRef", "Infinity", "Function", "Set", "TensorQuantity", "Kind", }); internal_static_sysml_Function_descriptor = getDescriptor().getMessageType(48); internal_static_sysml_Function_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_Function_descriptor, new java.lang.String[] { "CalcId", "SelfId", }); - internal_static_sysml_Array_descriptor = + internal_static_sysml_ValueSet_descriptor = getDescriptor().getMessageType(49); + internal_static_sysml_ValueSet_fieldAccessorTable = new + com.google.protobuf.GeneratedMessage.FieldAccessorTable( + internal_static_sysml_ValueSet_descriptor, + new java.lang.String[] { "Elements", }); + internal_static_sysml_TensorQuantity_descriptor = + getDescriptor().getMessageType(50); + internal_static_sysml_TensorQuantity_fieldAccessorTable = new + com.google.protobuf.GeneratedMessage.FieldAccessorTable( + internal_static_sysml_TensorQuantity_descriptor, + new java.lang.String[] { "Dimensions", "Components", }); + internal_static_sysml_Array_descriptor = + getDescriptor().getMessageType(51); internal_static_sysml_Array_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_Array_descriptor, new java.lang.String[] { "Dimensions", "Elements", }); internal_static_sysml_Vector_descriptor = - getDescriptor().getMessageType(50); + getDescriptor().getMessageType(52); internal_static_sysml_Vector_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_Vector_descriptor, new java.lang.String[] { "Components", }); internal_static_sysml_VectorQuantity_descriptor = - getDescriptor().getMessageType(51); + getDescriptor().getMessageType(53); internal_static_sysml_VectorQuantity_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_VectorQuantity_descriptor, new java.lang.String[] { "Components", }); internal_static_sysml_Complex_descriptor = - getDescriptor().getMessageType(52); + getDescriptor().getMessageType(54); internal_static_sysml_Complex_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_Complex_descriptor, new java.lang.String[] { "Real", "Imaginary", }); internal_static_sysml_EnumLiteral_descriptor = - getDescriptor().getMessageType(53); + getDescriptor().getMessageType(55); internal_static_sysml_EnumLiteral_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_EnumLiteral_descriptor, new java.lang.String[] { "LiteralId", "EnumerationId", "Name", }); internal_static_sysml_ValueSequence_descriptor = - getDescriptor().getMessageType(54); + getDescriptor().getMessageType(56); internal_static_sysml_ValueSequence_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_ValueSequence_descriptor, new java.lang.String[] { "Elements", }); internal_static_sysml_Quantity_descriptor = - getDescriptor().getMessageType(55); + getDescriptor().getMessageType(57); internal_static_sysml_Quantity_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_Quantity_descriptor, new java.lang.String[] { "IntMagnitude", "RealMagnitude", "Unit", "UnitTerm", "Magnitude", }); internal_static_sysml_MeasurementRef_descriptor = - getDescriptor().getMessageType(56); + getDescriptor().getMessageType(58); internal_static_sysml_MeasurementRef_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_MeasurementRef_descriptor, new java.lang.String[] { "Unit", "UnitTerm", "UnitId", }); internal_static_sysml_UnitTerm_descriptor = - getDescriptor().getMessageType(57); + getDescriptor().getMessageType(59); internal_static_sysml_UnitTerm_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_UnitTerm_descriptor, new java.lang.String[] { "ScaleNum", "ScaleDen", "Factors", }); internal_static_sysml_UnitFactor_descriptor = - getDescriptor().getMessageType(58); + getDescriptor().getMessageType(60); internal_static_sysml_UnitFactor_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_UnitFactor_descriptor, new java.lang.String[] { "UnitId", "Exponent", }); internal_static_sysml_Diagnostic_descriptor = - getDescriptor().getMessageType(59); + getDescriptor().getMessageType(61); internal_static_sysml_Diagnostic_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_Diagnostic_descriptor, new java.lang.String[] { "Severity", "Message", "Span", "Code", }); internal_static_sysml_Span_descriptor = - getDescriptor().getMessageType(60); + getDescriptor().getMessageType(62); internal_static_sysml_Span_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_Span_descriptor, new java.lang.String[] { "File", "StartLine", "StartCol", "EndLine", "EndCol", }); internal_static_sysml_ServerInfoRequest_descriptor = - getDescriptor().getMessageType(61); + getDescriptor().getMessageType(63); internal_static_sysml_ServerInfoRequest_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_ServerInfoRequest_descriptor, new java.lang.String[] { }); internal_static_sysml_ServerInfoResponse_descriptor = - getDescriptor().getMessageType(62); + getDescriptor().getMessageType(64); internal_static_sysml_ServerInfoResponse_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_ServerInfoResponse_descriptor, new java.lang.String[] { "Version", "Capabilities", }); internal_static_sysml_QueryRequest_descriptor = - getDescriptor().getMessageType(63); + getDescriptor().getMessageType(65); internal_static_sysml_QueryRequest_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_QueryRequest_descriptor, new java.lang.String[] { "ModelHash", "Query", "OslcQuery", }); internal_static_sysml_QueryResponse_descriptor = - getDescriptor().getMessageType(64); + getDescriptor().getMessageType(66); internal_static_sysml_QueryResponse_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_QueryResponse_descriptor, new java.lang.String[] { "Elements", }); internal_static_sysml_Query_descriptor = - getDescriptor().getMessageType(65); + getDescriptor().getMessageType(67); internal_static_sysml_Query_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_Query_descriptor, new java.lang.String[] { "Scope", "Select", "Where", }); internal_static_sysml_Constraint_descriptor = - getDescriptor().getMessageType(66); + getDescriptor().getMessageType(68); internal_static_sysml_Constraint_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_Constraint_descriptor, new java.lang.String[] { "Primitive", "Composite", "Constraint", }); internal_static_sysml_PrimitiveConstraint_descriptor = - getDescriptor().getMessageType(67); + getDescriptor().getMessageType(69); internal_static_sysml_PrimitiveConstraint_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_PrimitiveConstraint_descriptor, new java.lang.String[] { "Inverse", "Property", "Operator", "Value", }); internal_static_sysml_CompositeConstraint_descriptor = - getDescriptor().getMessageType(68); + getDescriptor().getMessageType(70); internal_static_sysml_CompositeConstraint_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_CompositeConstraint_descriptor, new java.lang.String[] { "Operator", "Constraint", }); internal_static_sysml_QueryResultElement_descriptor = - getDescriptor().getMessageType(69); + getDescriptor().getMessageType(71); internal_static_sysml_QueryResultElement_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_QueryResultElement_descriptor, @@ -1366,13 +1395,13 @@ public static void registerAllExtensions( internal_static_sysml_QueryResultElement_PropertiesEntry_descriptor, new java.lang.String[] { "Key", "Value", }); internal_static_sysml_SweepRange_descriptor = - getDescriptor().getMessageType(70); + getDescriptor().getMessageType(72); internal_static_sysml_SweepRange_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_SweepRange_descriptor, new java.lang.String[] { "Parameter", "Start", "End", "Step", }); internal_static_sysml_RunSweepRequest_descriptor = - getDescriptor().getMessageType(71); + getDescriptor().getMessageType(73); internal_static_sysml_RunSweepRequest_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_RunSweepRequest_descriptor, @@ -1384,67 +1413,67 @@ public static void registerAllExtensions( internal_static_sysml_RunSweepRequest_NamedArgumentsEntry_descriptor, new java.lang.String[] { "Key", "Value", }); internal_static_sysml_SweepRow_descriptor = - getDescriptor().getMessageType(72); + getDescriptor().getMessageType(74); internal_static_sysml_SweepRow_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_SweepRow_descriptor, new java.lang.String[] { "Inputs", "Outputs", "Verdicts", "ElapsedMicros", "Error", "FailureReason", }); internal_static_sysml_RunSweepResponse_descriptor = - getDescriptor().getMessageType(73); + getDescriptor().getMessageType(75); internal_static_sysml_RunSweepResponse_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_RunSweepResponse_descriptor, new java.lang.String[] { "Rows", "Parameters", "Sampled", "Seed", "Error", "Diagnostics", "FailureReason", "Instances", }); internal_static_sysml_RunDocumentQueryRequest_descriptor = - getDescriptor().getMessageType(74); + getDescriptor().getMessageType(76); internal_static_sysml_RunDocumentQueryRequest_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_RunDocumentQueryRequest_descriptor, new java.lang.String[] { "ModelHash", "QueryId", "Bindings", }); internal_static_sysml_DocumentQueryBinding_descriptor = - getDescriptor().getMessageType(75); + getDescriptor().getMessageType(77); internal_static_sysml_DocumentQueryBinding_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_DocumentQueryBinding_descriptor, new java.lang.String[] { "Parameter", "Values", }); internal_static_sysml_DocumentValue_descriptor = - getDescriptor().getMessageType(76); + getDescriptor().getMessageType(78); internal_static_sysml_DocumentValue_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_DocumentValue_descriptor, new java.lang.String[] { "ElementId", "StringValue", "IntValue", "RealValue", "BoolValue", "Infinity", "Quantity", "ElementType", "Kind", }); internal_static_sysml_DocumentQueryColumn_descriptor = - getDescriptor().getMessageType(77); + getDescriptor().getMessageType(79); internal_static_sysml_DocumentQueryColumn_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_DocumentQueryColumn_descriptor, new java.lang.String[] { "Name", }); internal_static_sysml_DocumentQueryCell_descriptor = - getDescriptor().getMessageType(78); + getDescriptor().getMessageType(80); internal_static_sysml_DocumentQueryCell_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_DocumentQueryCell_descriptor, new java.lang.String[] { "Values", }); internal_static_sysml_DocumentQueryRow_descriptor = - getDescriptor().getMessageType(79); + getDescriptor().getMessageType(81); internal_static_sysml_DocumentQueryRow_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_DocumentQueryRow_descriptor, new java.lang.String[] { "Element", "Cells", }); internal_static_sysml_RunDocumentQueryResponse_descriptor = - getDescriptor().getMessageType(80); + getDescriptor().getMessageType(82); internal_static_sysml_RunDocumentQueryResponse_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_RunDocumentQueryResponse_descriptor, new java.lang.String[] { "Columns", "Rows", }); internal_static_sysml_RenderDocumentRequest_descriptor = - getDescriptor().getMessageType(81); + getDescriptor().getMessageType(83); internal_static_sysml_RenderDocumentRequest_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_RenderDocumentRequest_descriptor, new java.lang.String[] { "ModelHash", "DocumentId", }); internal_static_sysml_RenderDocumentResponse_descriptor = - getDescriptor().getMessageType(82); + getDescriptor().getMessageType(84); internal_static_sysml_RenderDocumentResponse_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_sysml_RenderDocumentResponse_descriptor, diff --git a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/TensorQuantity.java b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/TensorQuantity.java new file mode 100644 index 000000000..e59abe260 --- /dev/null +++ b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/TensorQuantity.java @@ -0,0 +1,1051 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// NO CHECKED-IN PROTOBUF GENCODE +// source: sysml.proto +// Protobuf Java Version: 4.33.1 + +package org.openmbee.opensysml.proto; + +/** + *

+ * TensorQuantity is a Quantities::TensorQuantityValue of any rank: its
+ * dimensions and, flattened in row-major order under them, one Quantity per
+ * component, each with its unit and reduction as a scalar Quantity carries them.
+ * A tensor of rank one is not a VectorQuantity, on the wire as in the runtime.
+ * 
+ * + * Protobuf type {@code sysml.TensorQuantity} + */ +@com.google.protobuf.Generated +public final class TensorQuantity extends + com.google.protobuf.GeneratedMessage implements + // @@protoc_insertion_point(message_implements:sysml.TensorQuantity) + TensorQuantityOrBuilder { +private static final long serialVersionUID = 0L; + static { + com.google.protobuf.RuntimeVersion.validateProtobufGencodeVersion( + com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, + /* major= */ 4, + /* minor= */ 33, + /* patch= */ 1, + /* suffix= */ "", + "TensorQuantity"); + } + // Use TensorQuantity.newBuilder() to construct. + private TensorQuantity(com.google.protobuf.GeneratedMessage.Builder builder) { + super(builder); + } + private TensorQuantity() { + dimensions_ = emptyLongList(); + components_ = java.util.Collections.emptyList(); + } + + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.openmbee.opensysml.proto.Sysml.internal_static_sysml_TensorQuantity_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.openmbee.opensysml.proto.Sysml.internal_static_sysml_TensorQuantity_fieldAccessorTable + .ensureFieldAccessorsInitialized( + org.openmbee.opensysml.proto.TensorQuantity.class, org.openmbee.opensysml.proto.TensorQuantity.Builder.class); + } + + public static final int DIMENSIONS_FIELD_NUMBER = 1; + @SuppressWarnings("serial") + private com.google.protobuf.Internal.LongList dimensions_ = + emptyLongList(); + /** + *
+   * Positive extents, one per rank; their product (one for rank 0) is how many
+   * components there are, and a tensor not filling them is rejected.
+   * 
+ * + * repeated int64 dimensions = 1 [json_name = "dimensions"]; + * @return A list containing the dimensions. + */ + @java.lang.Override + public java.util.List + getDimensionsList() { + return dimensions_; + } + /** + *
+   * Positive extents, one per rank; their product (one for rank 0) is how many
+   * components there are, and a tensor not filling them is rejected.
+   * 
+ * + * repeated int64 dimensions = 1 [json_name = "dimensions"]; + * @return The count of dimensions. + */ + public int getDimensionsCount() { + return dimensions_.size(); + } + /** + *
+   * Positive extents, one per rank; their product (one for rank 0) is how many
+   * components there are, and a tensor not filling them is rejected.
+   * 
+ * + * repeated int64 dimensions = 1 [json_name = "dimensions"]; + * @param index The index of the element to return. + * @return The dimensions at the given index. + */ + public long getDimensions(int index) { + return dimensions_.getLong(index); + } + private int dimensionsMemoizedSerializedSize = -1; + + public static final int COMPONENTS_FIELD_NUMBER = 2; + @SuppressWarnings("serial") + private java.util.List components_; + /** + *
+   * A named unit sent without its unit_term is rejected as a Quantity's is.
+   * 
+ * + * repeated .sysml.Quantity components = 2 [json_name = "components"]; + */ + @java.lang.Override + public java.util.List getComponentsList() { + return components_; + } + /** + *
+   * A named unit sent without its unit_term is rejected as a Quantity's is.
+   * 
+ * + * repeated .sysml.Quantity components = 2 [json_name = "components"]; + */ + @java.lang.Override + public java.util.List + getComponentsOrBuilderList() { + return components_; + } + /** + *
+   * A named unit sent without its unit_term is rejected as a Quantity's is.
+   * 
+ * + * repeated .sysml.Quantity components = 2 [json_name = "components"]; + */ + @java.lang.Override + public int getComponentsCount() { + return components_.size(); + } + /** + *
+   * A named unit sent without its unit_term is rejected as a Quantity's is.
+   * 
+ * + * repeated .sysml.Quantity components = 2 [json_name = "components"]; + */ + @java.lang.Override + public org.openmbee.opensysml.proto.Quantity getComponents(int index) { + return components_.get(index); + } + /** + *
+   * A named unit sent without its unit_term is rejected as a Quantity's is.
+   * 
+ * + * repeated .sysml.Quantity components = 2 [json_name = "components"]; + */ + @java.lang.Override + public org.openmbee.opensysml.proto.QuantityOrBuilder getComponentsOrBuilder( + int index) { + return components_.get(index); + } + + private byte memoizedIsInitialized = -1; + @java.lang.Override + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized == 1) return true; + if (isInitialized == 0) return false; + + memoizedIsInitialized = 1; + return true; + } + + @java.lang.Override + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + getSerializedSize(); + if (getDimensionsList().size() > 0) { + output.writeUInt32NoTag(10); + output.writeUInt32NoTag(dimensionsMemoizedSerializedSize); + } + for (int i = 0; i < dimensions_.size(); i++) { + output.writeInt64NoTag(dimensions_.getLong(i)); + } + for (int i = 0; i < components_.size(); i++) { + output.writeMessage(2, components_.get(i)); + } + getUnknownFields().writeTo(output); + } + + @java.lang.Override + public int getSerializedSize() { + int size = memoizedSize; + if (size != -1) return size; + + size = 0; + { + int dataSize = 0; + for (int i = 0; i < dimensions_.size(); i++) { + dataSize += com.google.protobuf.CodedOutputStream + .computeInt64SizeNoTag(dimensions_.getLong(i)); + } + size += dataSize; + if (!getDimensionsList().isEmpty()) { + size += 1; + size += com.google.protobuf.CodedOutputStream + .computeInt32SizeNoTag(dataSize); + } + dimensionsMemoizedSerializedSize = dataSize; + } + for (int i = 0; i < components_.size(); i++) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(2, components_.get(i)); + } + size += getUnknownFields().getSerializedSize(); + memoizedSize = size; + return size; + } + + @java.lang.Override + public boolean equals(final java.lang.Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof org.openmbee.opensysml.proto.TensorQuantity)) { + return super.equals(obj); + } + org.openmbee.opensysml.proto.TensorQuantity other = (org.openmbee.opensysml.proto.TensorQuantity) obj; + + if (!getDimensionsList() + .equals(other.getDimensionsList())) return false; + if (!getComponentsList() + .equals(other.getComponentsList())) return false; + if (!getUnknownFields().equals(other.getUnknownFields())) return false; + return true; + } + + @java.lang.Override + public int hashCode() { + if (memoizedHashCode != 0) { + return memoizedHashCode; + } + int hash = 41; + hash = (19 * hash) + getDescriptor().hashCode(); + if (getDimensionsCount() > 0) { + hash = (37 * hash) + DIMENSIONS_FIELD_NUMBER; + hash = (53 * hash) + getDimensionsList().hashCode(); + } + if (getComponentsCount() > 0) { + hash = (37 * hash) + COMPONENTS_FIELD_NUMBER; + hash = (53 * hash) + getComponentsList().hashCode(); + } + hash = (29 * hash) + getUnknownFields().hashCode(); + memoizedHashCode = hash; + return hash; + } + + public static org.openmbee.opensysml.proto.TensorQuantity parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static org.openmbee.opensysml.proto.TensorQuantity parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static org.openmbee.opensysml.proto.TensorQuantity parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static org.openmbee.opensysml.proto.TensorQuantity parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static org.openmbee.opensysml.proto.TensorQuantity parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static org.openmbee.opensysml.proto.TensorQuantity parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static org.openmbee.opensysml.proto.TensorQuantity parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static org.openmbee.opensysml.proto.TensorQuantity parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + public static org.openmbee.opensysml.proto.TensorQuantity parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input); + } + + public static org.openmbee.opensysml.proto.TensorQuantity parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input, extensionRegistry); + } + public static org.openmbee.opensysml.proto.TensorQuantity parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static org.openmbee.opensysml.proto.TensorQuantity parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + @java.lang.Override + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder() { + return DEFAULT_INSTANCE.toBuilder(); + } + public static Builder newBuilder(org.openmbee.opensysml.proto.TensorQuantity prototype) { + return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); + } + @java.lang.Override + public Builder toBuilder() { + return this == DEFAULT_INSTANCE + ? new Builder() : new Builder().mergeFrom(this); + } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + *
+   * TensorQuantity is a Quantities::TensorQuantityValue of any rank: its
+   * dimensions and, flattened in row-major order under them, one Quantity per
+   * component, each with its unit and reduction as a scalar Quantity carries them.
+   * A tensor of rank one is not a VectorQuantity, on the wire as in the runtime.
+   * 
+ * + * Protobuf type {@code sysml.TensorQuantity} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder implements + // @@protoc_insertion_point(builder_implements:sysml.TensorQuantity) + org.openmbee.opensysml.proto.TensorQuantityOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.openmbee.opensysml.proto.Sysml.internal_static_sysml_TensorQuantity_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.openmbee.opensysml.proto.Sysml.internal_static_sysml_TensorQuantity_fieldAccessorTable + .ensureFieldAccessorsInitialized( + org.openmbee.opensysml.proto.TensorQuantity.class, org.openmbee.opensysml.proto.TensorQuantity.Builder.class); + } + + // Construct using org.openmbee.opensysml.proto.TensorQuantity.newBuilder() + private Builder() { + + } + + private Builder( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + super(parent); + + } + @java.lang.Override + public Builder clear() { + super.clear(); + bitField0_ = 0; + dimensions_ = emptyLongList(); + if (componentsBuilder_ == null) { + components_ = java.util.Collections.emptyList(); + } else { + components_ = null; + componentsBuilder_.clear(); + } + bitField0_ = (bitField0_ & ~0x00000002); + return this; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return org.openmbee.opensysml.proto.Sysml.internal_static_sysml_TensorQuantity_descriptor; + } + + @java.lang.Override + public org.openmbee.opensysml.proto.TensorQuantity getDefaultInstanceForType() { + return org.openmbee.opensysml.proto.TensorQuantity.getDefaultInstance(); + } + + @java.lang.Override + public org.openmbee.opensysml.proto.TensorQuantity build() { + org.openmbee.opensysml.proto.TensorQuantity result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + @java.lang.Override + public org.openmbee.opensysml.proto.TensorQuantity buildPartial() { + org.openmbee.opensysml.proto.TensorQuantity result = new org.openmbee.opensysml.proto.TensorQuantity(this); + buildPartialRepeatedFields(result); + if (bitField0_ != 0) { buildPartial0(result); } + onBuilt(); + return result; + } + + private void buildPartialRepeatedFields(org.openmbee.opensysml.proto.TensorQuantity result) { + if (componentsBuilder_ == null) { + if (((bitField0_ & 0x00000002) != 0)) { + components_ = java.util.Collections.unmodifiableList(components_); + bitField0_ = (bitField0_ & ~0x00000002); + } + result.components_ = components_; + } else { + result.components_ = componentsBuilder_.build(); + } + } + + private void buildPartial0(org.openmbee.opensysml.proto.TensorQuantity result) { + int from_bitField0_ = bitField0_; + if (((from_bitField0_ & 0x00000001) != 0)) { + dimensions_.makeImmutable(); + result.dimensions_ = dimensions_; + } + } + + @java.lang.Override + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof org.openmbee.opensysml.proto.TensorQuantity) { + return mergeFrom((org.openmbee.opensysml.proto.TensorQuantity)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(org.openmbee.opensysml.proto.TensorQuantity other) { + if (other == org.openmbee.opensysml.proto.TensorQuantity.getDefaultInstance()) return this; + if (!other.dimensions_.isEmpty()) { + if (dimensions_.isEmpty()) { + dimensions_ = other.dimensions_; + dimensions_.makeImmutable(); + bitField0_ |= 0x00000001; + } else { + ensureDimensionsIsMutable(); + dimensions_.addAll(other.dimensions_); + } + onChanged(); + } + if (componentsBuilder_ == null) { + if (!other.components_.isEmpty()) { + if (components_.isEmpty()) { + components_ = other.components_; + bitField0_ = (bitField0_ & ~0x00000002); + } else { + ensureComponentsIsMutable(); + components_.addAll(other.components_); + } + onChanged(); + } + } else { + if (!other.components_.isEmpty()) { + if (componentsBuilder_.isEmpty()) { + componentsBuilder_.dispose(); + componentsBuilder_ = null; + components_ = other.components_; + bitField0_ = (bitField0_ & ~0x00000002); + componentsBuilder_ = + com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders ? + internalGetComponentsFieldBuilder() : null; + } else { + componentsBuilder_.addAllMessages(other.components_); + } + } + } + this.mergeUnknownFields(other.getUnknownFields()); + onChanged(); + return this; + } + + @java.lang.Override + public final boolean isInitialized() { + return true; + } + + @java.lang.Override + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + if (extensionRegistry == null) { + throw new java.lang.NullPointerException(); + } + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + case 8: { + long v = input.readInt64(); + ensureDimensionsIsMutable(); + dimensions_.addLong(v); + break; + } // case 8 + case 10: { + int length = input.readRawVarint32(); + int limit = input.pushLimit(length); + ensureDimensionsIsMutable(); + while (input.getBytesUntilLimit() > 0) { + dimensions_.addLong(input.readInt64()); + } + input.popLimit(limit); + break; + } // case 10 + case 18: { + org.openmbee.opensysml.proto.Quantity m = + input.readMessage( + org.openmbee.opensysml.proto.Quantity.parser(), + extensionRegistry); + if (componentsBuilder_ == null) { + ensureComponentsIsMutable(); + components_.add(m); + } else { + componentsBuilder_.addMessage(m); + } + break; + } // case 18 + default: { + if (!super.parseUnknownField(input, extensionRegistry, tag)) { + done = true; // was an endgroup tag + } + break; + } // default: + } // switch (tag) + } // while (!done) + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.unwrapIOException(); + } finally { + onChanged(); + } // finally + return this; + } + private int bitField0_; + + private com.google.protobuf.Internal.LongList dimensions_ = emptyLongList(); + private void ensureDimensionsIsMutable() { + if (!dimensions_.isModifiable()) { + dimensions_ = makeMutableCopy(dimensions_); + } + bitField0_ |= 0x00000001; + } + /** + *
+     * Positive extents, one per rank; their product (one for rank 0) is how many
+     * components there are, and a tensor not filling them is rejected.
+     * 
+ * + * repeated int64 dimensions = 1 [json_name = "dimensions"]; + * @return A list containing the dimensions. + */ + public java.util.List + getDimensionsList() { + dimensions_.makeImmutable(); + return dimensions_; + } + /** + *
+     * Positive extents, one per rank; their product (one for rank 0) is how many
+     * components there are, and a tensor not filling them is rejected.
+     * 
+ * + * repeated int64 dimensions = 1 [json_name = "dimensions"]; + * @return The count of dimensions. + */ + public int getDimensionsCount() { + return dimensions_.size(); + } + /** + *
+     * Positive extents, one per rank; their product (one for rank 0) is how many
+     * components there are, and a tensor not filling them is rejected.
+     * 
+ * + * repeated int64 dimensions = 1 [json_name = "dimensions"]; + * @param index The index of the element to return. + * @return The dimensions at the given index. + */ + public long getDimensions(int index) { + return dimensions_.getLong(index); + } + /** + *
+     * Positive extents, one per rank; their product (one for rank 0) is how many
+     * components there are, and a tensor not filling them is rejected.
+     * 
+ * + * repeated int64 dimensions = 1 [json_name = "dimensions"]; + * @param index The index to set the value at. + * @param value The dimensions to set. + * @return This builder for chaining. + */ + public Builder setDimensions( + int index, long value) { + + ensureDimensionsIsMutable(); + dimensions_.setLong(index, value); + bitField0_ |= 0x00000001; + onChanged(); + return this; + } + /** + *
+     * Positive extents, one per rank; their product (one for rank 0) is how many
+     * components there are, and a tensor not filling them is rejected.
+     * 
+ * + * repeated int64 dimensions = 1 [json_name = "dimensions"]; + * @param value The dimensions to add. + * @return This builder for chaining. + */ + public Builder addDimensions(long value) { + + ensureDimensionsIsMutable(); + dimensions_.addLong(value); + bitField0_ |= 0x00000001; + onChanged(); + return this; + } + /** + *
+     * Positive extents, one per rank; their product (one for rank 0) is how many
+     * components there are, and a tensor not filling them is rejected.
+     * 
+ * + * repeated int64 dimensions = 1 [json_name = "dimensions"]; + * @param values The dimensions to add. + * @return This builder for chaining. + */ + public Builder addAllDimensions( + java.lang.Iterable values) { + ensureDimensionsIsMutable(); + com.google.protobuf.AbstractMessageLite.Builder.addAll( + values, dimensions_); + bitField0_ |= 0x00000001; + onChanged(); + return this; + } + /** + *
+     * Positive extents, one per rank; their product (one for rank 0) is how many
+     * components there are, and a tensor not filling them is rejected.
+     * 
+ * + * repeated int64 dimensions = 1 [json_name = "dimensions"]; + * @return This builder for chaining. + */ + public Builder clearDimensions() { + dimensions_ = emptyLongList(); + bitField0_ = (bitField0_ & ~0x00000001); + onChanged(); + return this; + } + + private java.util.List components_ = + java.util.Collections.emptyList(); + private void ensureComponentsIsMutable() { + if (!((bitField0_ & 0x00000002) != 0)) { + components_ = new java.util.ArrayList(components_); + bitField0_ |= 0x00000002; + } + } + + private com.google.protobuf.RepeatedFieldBuilder< + org.openmbee.opensysml.proto.Quantity, org.openmbee.opensysml.proto.Quantity.Builder, org.openmbee.opensysml.proto.QuantityOrBuilder> componentsBuilder_; + + /** + *
+     * A named unit sent without its unit_term is rejected as a Quantity's is.
+     * 
+ * + * repeated .sysml.Quantity components = 2 [json_name = "components"]; + */ + public java.util.List getComponentsList() { + if (componentsBuilder_ == null) { + return java.util.Collections.unmodifiableList(components_); + } else { + return componentsBuilder_.getMessageList(); + } + } + /** + *
+     * A named unit sent without its unit_term is rejected as a Quantity's is.
+     * 
+ * + * repeated .sysml.Quantity components = 2 [json_name = "components"]; + */ + public int getComponentsCount() { + if (componentsBuilder_ == null) { + return components_.size(); + } else { + return componentsBuilder_.getCount(); + } + } + /** + *
+     * A named unit sent without its unit_term is rejected as a Quantity's is.
+     * 
+ * + * repeated .sysml.Quantity components = 2 [json_name = "components"]; + */ + public org.openmbee.opensysml.proto.Quantity getComponents(int index) { + if (componentsBuilder_ == null) { + return components_.get(index); + } else { + return componentsBuilder_.getMessage(index); + } + } + /** + *
+     * A named unit sent without its unit_term is rejected as a Quantity's is.
+     * 
+ * + * repeated .sysml.Quantity components = 2 [json_name = "components"]; + */ + public Builder setComponents( + int index, org.openmbee.opensysml.proto.Quantity value) { + if (componentsBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + ensureComponentsIsMutable(); + components_.set(index, value); + onChanged(); + } else { + componentsBuilder_.setMessage(index, value); + } + return this; + } + /** + *
+     * A named unit sent without its unit_term is rejected as a Quantity's is.
+     * 
+ * + * repeated .sysml.Quantity components = 2 [json_name = "components"]; + */ + public Builder setComponents( + int index, org.openmbee.opensysml.proto.Quantity.Builder builderForValue) { + if (componentsBuilder_ == null) { + ensureComponentsIsMutable(); + components_.set(index, builderForValue.build()); + onChanged(); + } else { + componentsBuilder_.setMessage(index, builderForValue.build()); + } + return this; + } + /** + *
+     * A named unit sent without its unit_term is rejected as a Quantity's is.
+     * 
+ * + * repeated .sysml.Quantity components = 2 [json_name = "components"]; + */ + public Builder addComponents(org.openmbee.opensysml.proto.Quantity value) { + if (componentsBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + ensureComponentsIsMutable(); + components_.add(value); + onChanged(); + } else { + componentsBuilder_.addMessage(value); + } + return this; + } + /** + *
+     * A named unit sent without its unit_term is rejected as a Quantity's is.
+     * 
+ * + * repeated .sysml.Quantity components = 2 [json_name = "components"]; + */ + public Builder addComponents( + int index, org.openmbee.opensysml.proto.Quantity value) { + if (componentsBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + ensureComponentsIsMutable(); + components_.add(index, value); + onChanged(); + } else { + componentsBuilder_.addMessage(index, value); + } + return this; + } + /** + *
+     * A named unit sent without its unit_term is rejected as a Quantity's is.
+     * 
+ * + * repeated .sysml.Quantity components = 2 [json_name = "components"]; + */ + public Builder addComponents( + org.openmbee.opensysml.proto.Quantity.Builder builderForValue) { + if (componentsBuilder_ == null) { + ensureComponentsIsMutable(); + components_.add(builderForValue.build()); + onChanged(); + } else { + componentsBuilder_.addMessage(builderForValue.build()); + } + return this; + } + /** + *
+     * A named unit sent without its unit_term is rejected as a Quantity's is.
+     * 
+ * + * repeated .sysml.Quantity components = 2 [json_name = "components"]; + */ + public Builder addComponents( + int index, org.openmbee.opensysml.proto.Quantity.Builder builderForValue) { + if (componentsBuilder_ == null) { + ensureComponentsIsMutable(); + components_.add(index, builderForValue.build()); + onChanged(); + } else { + componentsBuilder_.addMessage(index, builderForValue.build()); + } + return this; + } + /** + *
+     * A named unit sent without its unit_term is rejected as a Quantity's is.
+     * 
+ * + * repeated .sysml.Quantity components = 2 [json_name = "components"]; + */ + public Builder addAllComponents( + java.lang.Iterable values) { + if (componentsBuilder_ == null) { + ensureComponentsIsMutable(); + com.google.protobuf.AbstractMessageLite.Builder.addAll( + values, components_); + onChanged(); + } else { + componentsBuilder_.addAllMessages(values); + } + return this; + } + /** + *
+     * A named unit sent without its unit_term is rejected as a Quantity's is.
+     * 
+ * + * repeated .sysml.Quantity components = 2 [json_name = "components"]; + */ + public Builder clearComponents() { + if (componentsBuilder_ == null) { + components_ = java.util.Collections.emptyList(); + bitField0_ = (bitField0_ & ~0x00000002); + onChanged(); + } else { + componentsBuilder_.clear(); + } + return this; + } + /** + *
+     * A named unit sent without its unit_term is rejected as a Quantity's is.
+     * 
+ * + * repeated .sysml.Quantity components = 2 [json_name = "components"]; + */ + public Builder removeComponents(int index) { + if (componentsBuilder_ == null) { + ensureComponentsIsMutable(); + components_.remove(index); + onChanged(); + } else { + componentsBuilder_.remove(index); + } + return this; + } + /** + *
+     * A named unit sent without its unit_term is rejected as a Quantity's is.
+     * 
+ * + * repeated .sysml.Quantity components = 2 [json_name = "components"]; + */ + public org.openmbee.opensysml.proto.Quantity.Builder getComponentsBuilder( + int index) { + return internalGetComponentsFieldBuilder().getBuilder(index); + } + /** + *
+     * A named unit sent without its unit_term is rejected as a Quantity's is.
+     * 
+ * + * repeated .sysml.Quantity components = 2 [json_name = "components"]; + */ + public org.openmbee.opensysml.proto.QuantityOrBuilder getComponentsOrBuilder( + int index) { + if (componentsBuilder_ == null) { + return components_.get(index); } else { + return componentsBuilder_.getMessageOrBuilder(index); + } + } + /** + *
+     * A named unit sent without its unit_term is rejected as a Quantity's is.
+     * 
+ * + * repeated .sysml.Quantity components = 2 [json_name = "components"]; + */ + public java.util.List + getComponentsOrBuilderList() { + if (componentsBuilder_ != null) { + return componentsBuilder_.getMessageOrBuilderList(); + } else { + return java.util.Collections.unmodifiableList(components_); + } + } + /** + *
+     * A named unit sent without its unit_term is rejected as a Quantity's is.
+     * 
+ * + * repeated .sysml.Quantity components = 2 [json_name = "components"]; + */ + public org.openmbee.opensysml.proto.Quantity.Builder addComponentsBuilder() { + return internalGetComponentsFieldBuilder().addBuilder( + org.openmbee.opensysml.proto.Quantity.getDefaultInstance()); + } + /** + *
+     * A named unit sent without its unit_term is rejected as a Quantity's is.
+     * 
+ * + * repeated .sysml.Quantity components = 2 [json_name = "components"]; + */ + public org.openmbee.opensysml.proto.Quantity.Builder addComponentsBuilder( + int index) { + return internalGetComponentsFieldBuilder().addBuilder( + index, org.openmbee.opensysml.proto.Quantity.getDefaultInstance()); + } + /** + *
+     * A named unit sent without its unit_term is rejected as a Quantity's is.
+     * 
+ * + * repeated .sysml.Quantity components = 2 [json_name = "components"]; + */ + public java.util.List + getComponentsBuilderList() { + return internalGetComponentsFieldBuilder().getBuilderList(); + } + private com.google.protobuf.RepeatedFieldBuilder< + org.openmbee.opensysml.proto.Quantity, org.openmbee.opensysml.proto.Quantity.Builder, org.openmbee.opensysml.proto.QuantityOrBuilder> + internalGetComponentsFieldBuilder() { + if (componentsBuilder_ == null) { + componentsBuilder_ = new com.google.protobuf.RepeatedFieldBuilder< + org.openmbee.opensysml.proto.Quantity, org.openmbee.opensysml.proto.Quantity.Builder, org.openmbee.opensysml.proto.QuantityOrBuilder>( + components_, + ((bitField0_ & 0x00000002) != 0), + getParentForChildren(), + isClean()); + components_ = null; + } + return componentsBuilder_; + } + + // @@protoc_insertion_point(builder_scope:sysml.TensorQuantity) + } + + // @@protoc_insertion_point(class_scope:sysml.TensorQuantity) + private static final org.openmbee.opensysml.proto.TensorQuantity DEFAULT_INSTANCE; + static { + DEFAULT_INSTANCE = new org.openmbee.opensysml.proto.TensorQuantity(); + } + + public static org.openmbee.opensysml.proto.TensorQuantity getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static final com.google.protobuf.Parser + PARSER = new com.google.protobuf.AbstractParser() { + @java.lang.Override + public TensorQuantity parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + Builder builder = newBuilder(); + try { + builder.mergeFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(builder.buildPartial()); + } catch (com.google.protobuf.UninitializedMessageException e) { + throw e.asInvalidProtocolBufferException().setUnfinishedMessage(builder.buildPartial()); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException(e) + .setUnfinishedMessage(builder.buildPartial()); + } + return builder.buildPartial(); + } + }; + + public static com.google.protobuf.Parser parser() { + return PARSER; + } + + @java.lang.Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + @java.lang.Override + public org.openmbee.opensysml.proto.TensorQuantity getDefaultInstanceForType() { + return DEFAULT_INSTANCE; + } + +} + diff --git a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/TensorQuantityOrBuilder.java b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/TensorQuantityOrBuilder.java new file mode 100644 index 000000000..ce3118e2c --- /dev/null +++ b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/TensorQuantityOrBuilder.java @@ -0,0 +1,88 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// NO CHECKED-IN PROTOBUF GENCODE +// source: sysml.proto +// Protobuf Java Version: 4.33.1 + +package org.openmbee.opensysml.proto; + +@com.google.protobuf.Generated +public interface TensorQuantityOrBuilder extends + // @@protoc_insertion_point(interface_extends:sysml.TensorQuantity) + com.google.protobuf.MessageOrBuilder { + + /** + *
+   * Positive extents, one per rank; their product (one for rank 0) is how many
+   * components there are, and a tensor not filling them is rejected.
+   * 
+ * + * repeated int64 dimensions = 1 [json_name = "dimensions"]; + * @return A list containing the dimensions. + */ + java.util.List getDimensionsList(); + /** + *
+   * Positive extents, one per rank; their product (one for rank 0) is how many
+   * components there are, and a tensor not filling them is rejected.
+   * 
+ * + * repeated int64 dimensions = 1 [json_name = "dimensions"]; + * @return The count of dimensions. + */ + int getDimensionsCount(); + /** + *
+   * Positive extents, one per rank; their product (one for rank 0) is how many
+   * components there are, and a tensor not filling them is rejected.
+   * 
+ * + * repeated int64 dimensions = 1 [json_name = "dimensions"]; + * @param index The index of the element to return. + * @return The dimensions at the given index. + */ + long getDimensions(int index); + + /** + *
+   * A named unit sent without its unit_term is rejected as a Quantity's is.
+   * 
+ * + * repeated .sysml.Quantity components = 2 [json_name = "components"]; + */ + java.util.List + getComponentsList(); + /** + *
+   * A named unit sent without its unit_term is rejected as a Quantity's is.
+   * 
+ * + * repeated .sysml.Quantity components = 2 [json_name = "components"]; + */ + org.openmbee.opensysml.proto.Quantity getComponents(int index); + /** + *
+   * A named unit sent without its unit_term is rejected as a Quantity's is.
+   * 
+ * + * repeated .sysml.Quantity components = 2 [json_name = "components"]; + */ + int getComponentsCount(); + /** + *
+   * A named unit sent without its unit_term is rejected as a Quantity's is.
+   * 
+ * + * repeated .sysml.Quantity components = 2 [json_name = "components"]; + */ + java.util.List + getComponentsOrBuilderList(); + /** + *
+   * A named unit sent without its unit_term is rejected as a Quantity's is.
+   * 
+ * + * repeated .sysml.Quantity components = 2 [json_name = "components"]; + */ + org.openmbee.opensysml.proto.QuantityOrBuilder getComponentsOrBuilder( + int index); +} diff --git a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Value.java b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Value.java index c0161a70d..61b07a8a6 100644 --- a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Value.java +++ b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/Value.java @@ -70,6 +70,8 @@ public enum KindCase MEASUREMENT_REF(15), INFINITY(16), FUNCTION(17), + SET(18), + TENSOR_QUANTITY(19), KIND_NOT_SET(0); private final int value; private KindCase(int value) { @@ -104,6 +106,8 @@ public static KindCase forNumber(int value) { case 15: return MEASUREMENT_REF; case 16: return INFINITY; case 17: return FUNCTION; + case 18: return SET; + case 19: return TENSOR_QUANTITY; case 0: return KIND_NOT_SET; default: return null; } @@ -754,6 +758,92 @@ public org.openmbee.opensysml.proto.FunctionOrBuilder getFunctionOrBuilder() { return org.openmbee.opensysml.proto.Function.getDefaultInstance(); } + public static final int SET_FIELD_NUMBER = 18; + /** + *
+   * distinct elements with no order of their own
+   * 
+ * + * .sysml.ValueSet set = 18 [json_name = "set"]; + * @return Whether the set field is set. + */ + @java.lang.Override + public boolean hasSet() { + return kindCase_ == 18; + } + /** + *
+   * distinct elements with no order of their own
+   * 
+ * + * .sysml.ValueSet set = 18 [json_name = "set"]; + * @return The set. + */ + @java.lang.Override + public org.openmbee.opensysml.proto.ValueSet getSet() { + if (kindCase_ == 18) { + return (org.openmbee.opensysml.proto.ValueSet) kind_; + } + return org.openmbee.opensysml.proto.ValueSet.getDefaultInstance(); + } + /** + *
+   * distinct elements with no order of their own
+   * 
+ * + * .sysml.ValueSet set = 18 [json_name = "set"]; + */ + @java.lang.Override + public org.openmbee.opensysml.proto.ValueSetOrBuilder getSetOrBuilder() { + if (kindCase_ == 18) { + return (org.openmbee.opensysml.proto.ValueSet) kind_; + } + return org.openmbee.opensysml.proto.ValueSet.getDefaultInstance(); + } + + public static final int TENSOR_QUANTITY_FIELD_NUMBER = 19; + /** + *
+   * shape and one Quantity per component
+   * 
+ * + * .sysml.TensorQuantity tensor_quantity = 19 [json_name = "tensorQuantity"]; + * @return Whether the tensorQuantity field is set. + */ + @java.lang.Override + public boolean hasTensorQuantity() { + return kindCase_ == 19; + } + /** + *
+   * shape and one Quantity per component
+   * 
+ * + * .sysml.TensorQuantity tensor_quantity = 19 [json_name = "tensorQuantity"]; + * @return The tensorQuantity. + */ + @java.lang.Override + public org.openmbee.opensysml.proto.TensorQuantity getTensorQuantity() { + if (kindCase_ == 19) { + return (org.openmbee.opensysml.proto.TensorQuantity) kind_; + } + return org.openmbee.opensysml.proto.TensorQuantity.getDefaultInstance(); + } + /** + *
+   * shape and one Quantity per component
+   * 
+ * + * .sysml.TensorQuantity tensor_quantity = 19 [json_name = "tensorQuantity"]; + */ + @java.lang.Override + public org.openmbee.opensysml.proto.TensorQuantityOrBuilder getTensorQuantityOrBuilder() { + if (kindCase_ == 19) { + return (org.openmbee.opensysml.proto.TensorQuantity) kind_; + } + return org.openmbee.opensysml.proto.TensorQuantity.getDefaultInstance(); + } + private byte memoizedIsInitialized = -1; @java.lang.Override public final boolean isInitialized() { @@ -825,6 +915,12 @@ public void writeTo(com.google.protobuf.CodedOutputStream output) if (kindCase_ == 17) { output.writeMessage(17, (org.openmbee.opensysml.proto.Function) kind_); } + if (kindCase_ == 18) { + output.writeMessage(18, (org.openmbee.opensysml.proto.ValueSet) kind_); + } + if (kindCase_ == 19) { + output.writeMessage(19, (org.openmbee.opensysml.proto.TensorQuantity) kind_); + } getUnknownFields().writeTo(output); } @@ -906,6 +1002,14 @@ public int getSerializedSize() { size += com.google.protobuf.CodedOutputStream .computeMessageSize(17, (org.openmbee.opensysml.proto.Function) kind_); } + if (kindCase_ == 18) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(18, (org.openmbee.opensysml.proto.ValueSet) kind_); + } + if (kindCase_ == 19) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(19, (org.openmbee.opensysml.proto.TensorQuantity) kind_); + } size += getUnknownFields().getSerializedSize(); memoizedSize = size; return size; @@ -992,6 +1096,14 @@ public boolean equals(final java.lang.Object obj) { if (!getFunction() .equals(other.getFunction())) return false; break; + case 18: + if (!getSet() + .equals(other.getSet())) return false; + break; + case 19: + if (!getTensorQuantity() + .equals(other.getTensorQuantity())) return false; + break; case 0: default: } @@ -1081,6 +1193,14 @@ public int hashCode() { hash = (37 * hash) + FUNCTION_FIELD_NUMBER; hash = (53 * hash) + getFunction().hashCode(); break; + case 18: + hash = (37 * hash) + SET_FIELD_NUMBER; + hash = (53 * hash) + getSet().hashCode(); + break; + case 19: + hash = (37 * hash) + TENSOR_QUANTITY_FIELD_NUMBER; + hash = (53 * hash) + getTensorQuantity().hashCode(); + break; case 0: default: } @@ -1246,6 +1366,12 @@ public Builder clear() { if (functionBuilder_ != null) { functionBuilder_.clear(); } + if (setBuilder_ != null) { + setBuilder_.clear(); + } + if (tensorQuantityBuilder_ != null) { + tensorQuantityBuilder_.clear(); + } kindCase_ = 0; kind_ = null; return this; @@ -1323,6 +1449,14 @@ private void buildPartialOneofs(org.openmbee.opensysml.proto.Value result) { functionBuilder_ != null) { result.kind_ = functionBuilder_.build(); } + if (kindCase_ == 18 && + setBuilder_ != null) { + result.kind_ = setBuilder_.build(); + } + if (kindCase_ == 19 && + tensorQuantityBuilder_ != null) { + result.kind_ = tensorQuantityBuilder_.build(); + } } @java.lang.Override @@ -1410,6 +1544,14 @@ public Builder mergeFrom(org.openmbee.opensysml.proto.Value other) { mergeFunction(other.getFunction()); break; } + case SET: { + mergeSet(other.getSet()); + break; + } + case TENSOR_QUANTITY: { + mergeTensorQuantity(other.getTensorQuantity()); + break; + } case KIND_NOT_SET: { break; } @@ -1545,6 +1687,20 @@ public Builder mergeFrom( kindCase_ = 17; break; } // case 138 + case 146: { + input.readMessage( + internalGetSetFieldBuilder().getBuilder(), + extensionRegistry); + kindCase_ = 18; + break; + } // case 146 + case 154: { + input.readMessage( + internalGetTensorQuantityFieldBuilder().getBuilder(), + extensionRegistry); + kindCase_ = 19; + break; + } // case 154 default: { if (!super.parseUnknownField(input, extensionRegistry, tag)) { done = true; // was an endgroup tag @@ -3629,6 +3785,362 @@ public org.openmbee.opensysml.proto.FunctionOrBuilder getFunctionOrBuilder() { return functionBuilder_; } + private com.google.protobuf.SingleFieldBuilder< + org.openmbee.opensysml.proto.ValueSet, org.openmbee.opensysml.proto.ValueSet.Builder, org.openmbee.opensysml.proto.ValueSetOrBuilder> setBuilder_; + /** + *
+     * distinct elements with no order of their own
+     * 
+ * + * .sysml.ValueSet set = 18 [json_name = "set"]; + * @return Whether the set field is set. + */ + @java.lang.Override + public boolean hasSet() { + return kindCase_ == 18; + } + /** + *
+     * distinct elements with no order of their own
+     * 
+ * + * .sysml.ValueSet set = 18 [json_name = "set"]; + * @return The set. + */ + @java.lang.Override + public org.openmbee.opensysml.proto.ValueSet getSet() { + if (setBuilder_ == null) { + if (kindCase_ == 18) { + return (org.openmbee.opensysml.proto.ValueSet) kind_; + } + return org.openmbee.opensysml.proto.ValueSet.getDefaultInstance(); + } else { + if (kindCase_ == 18) { + return setBuilder_.getMessage(); + } + return org.openmbee.opensysml.proto.ValueSet.getDefaultInstance(); + } + } + /** + *
+     * distinct elements with no order of their own
+     * 
+ * + * .sysml.ValueSet set = 18 [json_name = "set"]; + */ + public Builder setSet(org.openmbee.opensysml.proto.ValueSet value) { + if (setBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + kind_ = value; + onChanged(); + } else { + setBuilder_.setMessage(value); + } + kindCase_ = 18; + return this; + } + /** + *
+     * distinct elements with no order of their own
+     * 
+ * + * .sysml.ValueSet set = 18 [json_name = "set"]; + */ + public Builder setSet( + org.openmbee.opensysml.proto.ValueSet.Builder builderForValue) { + if (setBuilder_ == null) { + kind_ = builderForValue.build(); + onChanged(); + } else { + setBuilder_.setMessage(builderForValue.build()); + } + kindCase_ = 18; + return this; + } + /** + *
+     * distinct elements with no order of their own
+     * 
+ * + * .sysml.ValueSet set = 18 [json_name = "set"]; + */ + public Builder mergeSet(org.openmbee.opensysml.proto.ValueSet value) { + if (setBuilder_ == null) { + if (kindCase_ == 18 && + kind_ != org.openmbee.opensysml.proto.ValueSet.getDefaultInstance()) { + kind_ = org.openmbee.opensysml.proto.ValueSet.newBuilder((org.openmbee.opensysml.proto.ValueSet) kind_) + .mergeFrom(value).buildPartial(); + } else { + kind_ = value; + } + onChanged(); + } else { + if (kindCase_ == 18) { + setBuilder_.mergeFrom(value); + } else { + setBuilder_.setMessage(value); + } + } + kindCase_ = 18; + return this; + } + /** + *
+     * distinct elements with no order of their own
+     * 
+ * + * .sysml.ValueSet set = 18 [json_name = "set"]; + */ + public Builder clearSet() { + if (setBuilder_ == null) { + if (kindCase_ == 18) { + kindCase_ = 0; + kind_ = null; + onChanged(); + } + } else { + if (kindCase_ == 18) { + kindCase_ = 0; + kind_ = null; + } + setBuilder_.clear(); + } + return this; + } + /** + *
+     * distinct elements with no order of their own
+     * 
+ * + * .sysml.ValueSet set = 18 [json_name = "set"]; + */ + public org.openmbee.opensysml.proto.ValueSet.Builder getSetBuilder() { + return internalGetSetFieldBuilder().getBuilder(); + } + /** + *
+     * distinct elements with no order of their own
+     * 
+ * + * .sysml.ValueSet set = 18 [json_name = "set"]; + */ + @java.lang.Override + public org.openmbee.opensysml.proto.ValueSetOrBuilder getSetOrBuilder() { + if ((kindCase_ == 18) && (setBuilder_ != null)) { + return setBuilder_.getMessageOrBuilder(); + } else { + if (kindCase_ == 18) { + return (org.openmbee.opensysml.proto.ValueSet) kind_; + } + return org.openmbee.opensysml.proto.ValueSet.getDefaultInstance(); + } + } + /** + *
+     * distinct elements with no order of their own
+     * 
+ * + * .sysml.ValueSet set = 18 [json_name = "set"]; + */ + private com.google.protobuf.SingleFieldBuilder< + org.openmbee.opensysml.proto.ValueSet, org.openmbee.opensysml.proto.ValueSet.Builder, org.openmbee.opensysml.proto.ValueSetOrBuilder> + internalGetSetFieldBuilder() { + if (setBuilder_ == null) { + if (!(kindCase_ == 18)) { + kind_ = org.openmbee.opensysml.proto.ValueSet.getDefaultInstance(); + } + setBuilder_ = new com.google.protobuf.SingleFieldBuilder< + org.openmbee.opensysml.proto.ValueSet, org.openmbee.opensysml.proto.ValueSet.Builder, org.openmbee.opensysml.proto.ValueSetOrBuilder>( + (org.openmbee.opensysml.proto.ValueSet) kind_, + getParentForChildren(), + isClean()); + kind_ = null; + } + kindCase_ = 18; + onChanged(); + return setBuilder_; + } + + private com.google.protobuf.SingleFieldBuilder< + org.openmbee.opensysml.proto.TensorQuantity, org.openmbee.opensysml.proto.TensorQuantity.Builder, org.openmbee.opensysml.proto.TensorQuantityOrBuilder> tensorQuantityBuilder_; + /** + *
+     * shape and one Quantity per component
+     * 
+ * + * .sysml.TensorQuantity tensor_quantity = 19 [json_name = "tensorQuantity"]; + * @return Whether the tensorQuantity field is set. + */ + @java.lang.Override + public boolean hasTensorQuantity() { + return kindCase_ == 19; + } + /** + *
+     * shape and one Quantity per component
+     * 
+ * + * .sysml.TensorQuantity tensor_quantity = 19 [json_name = "tensorQuantity"]; + * @return The tensorQuantity. + */ + @java.lang.Override + public org.openmbee.opensysml.proto.TensorQuantity getTensorQuantity() { + if (tensorQuantityBuilder_ == null) { + if (kindCase_ == 19) { + return (org.openmbee.opensysml.proto.TensorQuantity) kind_; + } + return org.openmbee.opensysml.proto.TensorQuantity.getDefaultInstance(); + } else { + if (kindCase_ == 19) { + return tensorQuantityBuilder_.getMessage(); + } + return org.openmbee.opensysml.proto.TensorQuantity.getDefaultInstance(); + } + } + /** + *
+     * shape and one Quantity per component
+     * 
+ * + * .sysml.TensorQuantity tensor_quantity = 19 [json_name = "tensorQuantity"]; + */ + public Builder setTensorQuantity(org.openmbee.opensysml.proto.TensorQuantity value) { + if (tensorQuantityBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + kind_ = value; + onChanged(); + } else { + tensorQuantityBuilder_.setMessage(value); + } + kindCase_ = 19; + return this; + } + /** + *
+     * shape and one Quantity per component
+     * 
+ * + * .sysml.TensorQuantity tensor_quantity = 19 [json_name = "tensorQuantity"]; + */ + public Builder setTensorQuantity( + org.openmbee.opensysml.proto.TensorQuantity.Builder builderForValue) { + if (tensorQuantityBuilder_ == null) { + kind_ = builderForValue.build(); + onChanged(); + } else { + tensorQuantityBuilder_.setMessage(builderForValue.build()); + } + kindCase_ = 19; + return this; + } + /** + *
+     * shape and one Quantity per component
+     * 
+ * + * .sysml.TensorQuantity tensor_quantity = 19 [json_name = "tensorQuantity"]; + */ + public Builder mergeTensorQuantity(org.openmbee.opensysml.proto.TensorQuantity value) { + if (tensorQuantityBuilder_ == null) { + if (kindCase_ == 19 && + kind_ != org.openmbee.opensysml.proto.TensorQuantity.getDefaultInstance()) { + kind_ = org.openmbee.opensysml.proto.TensorQuantity.newBuilder((org.openmbee.opensysml.proto.TensorQuantity) kind_) + .mergeFrom(value).buildPartial(); + } else { + kind_ = value; + } + onChanged(); + } else { + if (kindCase_ == 19) { + tensorQuantityBuilder_.mergeFrom(value); + } else { + tensorQuantityBuilder_.setMessage(value); + } + } + kindCase_ = 19; + return this; + } + /** + *
+     * shape and one Quantity per component
+     * 
+ * + * .sysml.TensorQuantity tensor_quantity = 19 [json_name = "tensorQuantity"]; + */ + public Builder clearTensorQuantity() { + if (tensorQuantityBuilder_ == null) { + if (kindCase_ == 19) { + kindCase_ = 0; + kind_ = null; + onChanged(); + } + } else { + if (kindCase_ == 19) { + kindCase_ = 0; + kind_ = null; + } + tensorQuantityBuilder_.clear(); + } + return this; + } + /** + *
+     * shape and one Quantity per component
+     * 
+ * + * .sysml.TensorQuantity tensor_quantity = 19 [json_name = "tensorQuantity"]; + */ + public org.openmbee.opensysml.proto.TensorQuantity.Builder getTensorQuantityBuilder() { + return internalGetTensorQuantityFieldBuilder().getBuilder(); + } + /** + *
+     * shape and one Quantity per component
+     * 
+ * + * .sysml.TensorQuantity tensor_quantity = 19 [json_name = "tensorQuantity"]; + */ + @java.lang.Override + public org.openmbee.opensysml.proto.TensorQuantityOrBuilder getTensorQuantityOrBuilder() { + if ((kindCase_ == 19) && (tensorQuantityBuilder_ != null)) { + return tensorQuantityBuilder_.getMessageOrBuilder(); + } else { + if (kindCase_ == 19) { + return (org.openmbee.opensysml.proto.TensorQuantity) kind_; + } + return org.openmbee.opensysml.proto.TensorQuantity.getDefaultInstance(); + } + } + /** + *
+     * shape and one Quantity per component
+     * 
+ * + * .sysml.TensorQuantity tensor_quantity = 19 [json_name = "tensorQuantity"]; + */ + private com.google.protobuf.SingleFieldBuilder< + org.openmbee.opensysml.proto.TensorQuantity, org.openmbee.opensysml.proto.TensorQuantity.Builder, org.openmbee.opensysml.proto.TensorQuantityOrBuilder> + internalGetTensorQuantityFieldBuilder() { + if (tensorQuantityBuilder_ == null) { + if (!(kindCase_ == 19)) { + kind_ = org.openmbee.opensysml.proto.TensorQuantity.getDefaultInstance(); + } + tensorQuantityBuilder_ = new com.google.protobuf.SingleFieldBuilder< + org.openmbee.opensysml.proto.TensorQuantity, org.openmbee.opensysml.proto.TensorQuantity.Builder, org.openmbee.opensysml.proto.TensorQuantityOrBuilder>( + (org.openmbee.opensysml.proto.TensorQuantity) kind_, + getParentForChildren(), + isClean()); + kind_ = null; + } + kindCase_ = 19; + onChanged(); + return tensorQuantityBuilder_; + } + // @@protoc_insertion_point(builder_scope:sysml.Value) } diff --git a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ValueOrBuilder.java b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ValueOrBuilder.java index 00f57684d..79f9f72bd 100644 --- a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ValueOrBuilder.java +++ b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ValueOrBuilder.java @@ -371,5 +371,59 @@ public interface ValueOrBuilder extends */ org.openmbee.opensysml.proto.FunctionOrBuilder getFunctionOrBuilder(); + /** + *
+   * distinct elements with no order of their own
+   * 
+ * + * .sysml.ValueSet set = 18 [json_name = "set"]; + * @return Whether the set field is set. + */ + boolean hasSet(); + /** + *
+   * distinct elements with no order of their own
+   * 
+ * + * .sysml.ValueSet set = 18 [json_name = "set"]; + * @return The set. + */ + org.openmbee.opensysml.proto.ValueSet getSet(); + /** + *
+   * distinct elements with no order of their own
+   * 
+ * + * .sysml.ValueSet set = 18 [json_name = "set"]; + */ + org.openmbee.opensysml.proto.ValueSetOrBuilder getSetOrBuilder(); + + /** + *
+   * shape and one Quantity per component
+   * 
+ * + * .sysml.TensorQuantity tensor_quantity = 19 [json_name = "tensorQuantity"]; + * @return Whether the tensorQuantity field is set. + */ + boolean hasTensorQuantity(); + /** + *
+   * shape and one Quantity per component
+   * 
+ * + * .sysml.TensorQuantity tensor_quantity = 19 [json_name = "tensorQuantity"]; + * @return The tensorQuantity. + */ + org.openmbee.opensysml.proto.TensorQuantity getTensorQuantity(); + /** + *
+   * shape and one Quantity per component
+   * 
+ * + * .sysml.TensorQuantity tensor_quantity = 19 [json_name = "tensorQuantity"]; + */ + org.openmbee.opensysml.proto.TensorQuantityOrBuilder getTensorQuantityOrBuilder(); + org.openmbee.opensysml.proto.Value.KindCase getKindCase(); } diff --git a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ValueSet.java b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ValueSet.java new file mode 100644 index 000000000..529bcf5f5 --- /dev/null +++ b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ValueSet.java @@ -0,0 +1,742 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// NO CHECKED-IN PROTOBUF GENCODE +// source: sysml.proto +// Protobuf Java Version: 4.33.1 + +package org.openmbee.opensysml.proto; + +/** + *
+ * ValueSet is a unique, unordered collection — a Collections::Set's elements —
+ * as distinct from a ValueSequence, whose order is part of its value. Two sets
+ * are equal when they hold the same elements in any order. The service sends
+ * the elements in the runtime's canonical order (Booleans, numbers, strings,
+ * quantities, enumeration literals, objects, each class in its own order), so
+ * equal sets cross alike; a client may send them in any order, but sending an
+ * element twice is rejected rather than read as one, since a repeated element
+ * is what a sequence carries.
+ * 
+ * + * Protobuf type {@code sysml.ValueSet} + */ +@com.google.protobuf.Generated +public final class ValueSet extends + com.google.protobuf.GeneratedMessage implements + // @@protoc_insertion_point(message_implements:sysml.ValueSet) + ValueSetOrBuilder { +private static final long serialVersionUID = 0L; + static { + com.google.protobuf.RuntimeVersion.validateProtobufGencodeVersion( + com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, + /* major= */ 4, + /* minor= */ 33, + /* patch= */ 1, + /* suffix= */ "", + "ValueSet"); + } + // Use ValueSet.newBuilder() to construct. + private ValueSet(com.google.protobuf.GeneratedMessage.Builder builder) { + super(builder); + } + private ValueSet() { + elements_ = java.util.Collections.emptyList(); + } + + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.openmbee.opensysml.proto.Sysml.internal_static_sysml_ValueSet_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.openmbee.opensysml.proto.Sysml.internal_static_sysml_ValueSet_fieldAccessorTable + .ensureFieldAccessorsInitialized( + org.openmbee.opensysml.proto.ValueSet.class, org.openmbee.opensysml.proto.ValueSet.Builder.class); + } + + public static final int ELEMENTS_FIELD_NUMBER = 1; + @SuppressWarnings("serial") + private java.util.List elements_; + /** + * repeated .sysml.Value elements = 1 [json_name = "elements"]; + */ + @java.lang.Override + public java.util.List getElementsList() { + return elements_; + } + /** + * repeated .sysml.Value elements = 1 [json_name = "elements"]; + */ + @java.lang.Override + public java.util.List + getElementsOrBuilderList() { + return elements_; + } + /** + * repeated .sysml.Value elements = 1 [json_name = "elements"]; + */ + @java.lang.Override + public int getElementsCount() { + return elements_.size(); + } + /** + * repeated .sysml.Value elements = 1 [json_name = "elements"]; + */ + @java.lang.Override + public org.openmbee.opensysml.proto.Value getElements(int index) { + return elements_.get(index); + } + /** + * repeated .sysml.Value elements = 1 [json_name = "elements"]; + */ + @java.lang.Override + public org.openmbee.opensysml.proto.ValueOrBuilder getElementsOrBuilder( + int index) { + return elements_.get(index); + } + + private byte memoizedIsInitialized = -1; + @java.lang.Override + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized == 1) return true; + if (isInitialized == 0) return false; + + memoizedIsInitialized = 1; + return true; + } + + @java.lang.Override + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + for (int i = 0; i < elements_.size(); i++) { + output.writeMessage(1, elements_.get(i)); + } + getUnknownFields().writeTo(output); + } + + @java.lang.Override + public int getSerializedSize() { + int size = memoizedSize; + if (size != -1) return size; + + size = 0; + for (int i = 0; i < elements_.size(); i++) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(1, elements_.get(i)); + } + size += getUnknownFields().getSerializedSize(); + memoizedSize = size; + return size; + } + + @java.lang.Override + public boolean equals(final java.lang.Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof org.openmbee.opensysml.proto.ValueSet)) { + return super.equals(obj); + } + org.openmbee.opensysml.proto.ValueSet other = (org.openmbee.opensysml.proto.ValueSet) obj; + + if (!getElementsList() + .equals(other.getElementsList())) return false; + if (!getUnknownFields().equals(other.getUnknownFields())) return false; + return true; + } + + @java.lang.Override + public int hashCode() { + if (memoizedHashCode != 0) { + return memoizedHashCode; + } + int hash = 41; + hash = (19 * hash) + getDescriptor().hashCode(); + if (getElementsCount() > 0) { + hash = (37 * hash) + ELEMENTS_FIELD_NUMBER; + hash = (53 * hash) + getElementsList().hashCode(); + } + hash = (29 * hash) + getUnknownFields().hashCode(); + memoizedHashCode = hash; + return hash; + } + + public static org.openmbee.opensysml.proto.ValueSet parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static org.openmbee.opensysml.proto.ValueSet parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static org.openmbee.opensysml.proto.ValueSet parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static org.openmbee.opensysml.proto.ValueSet parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static org.openmbee.opensysml.proto.ValueSet parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static org.openmbee.opensysml.proto.ValueSet parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static org.openmbee.opensysml.proto.ValueSet parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static org.openmbee.opensysml.proto.ValueSet parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + public static org.openmbee.opensysml.proto.ValueSet parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input); + } + + public static org.openmbee.opensysml.proto.ValueSet parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseDelimitedWithIOException(PARSER, input, extensionRegistry); + } + public static org.openmbee.opensysml.proto.ValueSet parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input); + } + public static org.openmbee.opensysml.proto.ValueSet parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessage + .parseWithIOException(PARSER, input, extensionRegistry); + } + + @java.lang.Override + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder() { + return DEFAULT_INSTANCE.toBuilder(); + } + public static Builder newBuilder(org.openmbee.opensysml.proto.ValueSet prototype) { + return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); + } + @java.lang.Override + public Builder toBuilder() { + return this == DEFAULT_INSTANCE + ? new Builder() : new Builder().mergeFrom(this); + } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + *
+   * ValueSet is a unique, unordered collection — a Collections::Set's elements —
+   * as distinct from a ValueSequence, whose order is part of its value. Two sets
+   * are equal when they hold the same elements in any order. The service sends
+   * the elements in the runtime's canonical order (Booleans, numbers, strings,
+   * quantities, enumeration literals, objects, each class in its own order), so
+   * equal sets cross alike; a client may send them in any order, but sending an
+   * element twice is rejected rather than read as one, since a repeated element
+   * is what a sequence carries.
+   * 
+ * + * Protobuf type {@code sysml.ValueSet} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder implements + // @@protoc_insertion_point(builder_implements:sysml.ValueSet) + org.openmbee.opensysml.proto.ValueSetOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.openmbee.opensysml.proto.Sysml.internal_static_sysml_ValueSet_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.openmbee.opensysml.proto.Sysml.internal_static_sysml_ValueSet_fieldAccessorTable + .ensureFieldAccessorsInitialized( + org.openmbee.opensysml.proto.ValueSet.class, org.openmbee.opensysml.proto.ValueSet.Builder.class); + } + + // Construct using org.openmbee.opensysml.proto.ValueSet.newBuilder() + private Builder() { + + } + + private Builder( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + super(parent); + + } + @java.lang.Override + public Builder clear() { + super.clear(); + bitField0_ = 0; + if (elementsBuilder_ == null) { + elements_ = java.util.Collections.emptyList(); + } else { + elements_ = null; + elementsBuilder_.clear(); + } + bitField0_ = (bitField0_ & ~0x00000001); + return this; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return org.openmbee.opensysml.proto.Sysml.internal_static_sysml_ValueSet_descriptor; + } + + @java.lang.Override + public org.openmbee.opensysml.proto.ValueSet getDefaultInstanceForType() { + return org.openmbee.opensysml.proto.ValueSet.getDefaultInstance(); + } + + @java.lang.Override + public org.openmbee.opensysml.proto.ValueSet build() { + org.openmbee.opensysml.proto.ValueSet result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + @java.lang.Override + public org.openmbee.opensysml.proto.ValueSet buildPartial() { + org.openmbee.opensysml.proto.ValueSet result = new org.openmbee.opensysml.proto.ValueSet(this); + buildPartialRepeatedFields(result); + if (bitField0_ != 0) { buildPartial0(result); } + onBuilt(); + return result; + } + + private void buildPartialRepeatedFields(org.openmbee.opensysml.proto.ValueSet result) { + if (elementsBuilder_ == null) { + if (((bitField0_ & 0x00000001) != 0)) { + elements_ = java.util.Collections.unmodifiableList(elements_); + bitField0_ = (bitField0_ & ~0x00000001); + } + result.elements_ = elements_; + } else { + result.elements_ = elementsBuilder_.build(); + } + } + + private void buildPartial0(org.openmbee.opensysml.proto.ValueSet result) { + int from_bitField0_ = bitField0_; + } + + @java.lang.Override + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof org.openmbee.opensysml.proto.ValueSet) { + return mergeFrom((org.openmbee.opensysml.proto.ValueSet)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(org.openmbee.opensysml.proto.ValueSet other) { + if (other == org.openmbee.opensysml.proto.ValueSet.getDefaultInstance()) return this; + if (elementsBuilder_ == null) { + if (!other.elements_.isEmpty()) { + if (elements_.isEmpty()) { + elements_ = other.elements_; + bitField0_ = (bitField0_ & ~0x00000001); + } else { + ensureElementsIsMutable(); + elements_.addAll(other.elements_); + } + onChanged(); + } + } else { + if (!other.elements_.isEmpty()) { + if (elementsBuilder_.isEmpty()) { + elementsBuilder_.dispose(); + elementsBuilder_ = null; + elements_ = other.elements_; + bitField0_ = (bitField0_ & ~0x00000001); + elementsBuilder_ = + com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders ? + internalGetElementsFieldBuilder() : null; + } else { + elementsBuilder_.addAllMessages(other.elements_); + } + } + } + this.mergeUnknownFields(other.getUnknownFields()); + onChanged(); + return this; + } + + @java.lang.Override + public final boolean isInitialized() { + return true; + } + + @java.lang.Override + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + if (extensionRegistry == null) { + throw new java.lang.NullPointerException(); + } + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + case 10: { + org.openmbee.opensysml.proto.Value m = + input.readMessage( + org.openmbee.opensysml.proto.Value.parser(), + extensionRegistry); + if (elementsBuilder_ == null) { + ensureElementsIsMutable(); + elements_.add(m); + } else { + elementsBuilder_.addMessage(m); + } + break; + } // case 10 + default: { + if (!super.parseUnknownField(input, extensionRegistry, tag)) { + done = true; // was an endgroup tag + } + break; + } // default: + } // switch (tag) + } // while (!done) + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.unwrapIOException(); + } finally { + onChanged(); + } // finally + return this; + } + private int bitField0_; + + private java.util.List elements_ = + java.util.Collections.emptyList(); + private void ensureElementsIsMutable() { + if (!((bitField0_ & 0x00000001) != 0)) { + elements_ = new java.util.ArrayList(elements_); + bitField0_ |= 0x00000001; + } + } + + private com.google.protobuf.RepeatedFieldBuilder< + org.openmbee.opensysml.proto.Value, org.openmbee.opensysml.proto.Value.Builder, org.openmbee.opensysml.proto.ValueOrBuilder> elementsBuilder_; + + /** + * repeated .sysml.Value elements = 1 [json_name = "elements"]; + */ + public java.util.List getElementsList() { + if (elementsBuilder_ == null) { + return java.util.Collections.unmodifiableList(elements_); + } else { + return elementsBuilder_.getMessageList(); + } + } + /** + * repeated .sysml.Value elements = 1 [json_name = "elements"]; + */ + public int getElementsCount() { + if (elementsBuilder_ == null) { + return elements_.size(); + } else { + return elementsBuilder_.getCount(); + } + } + /** + * repeated .sysml.Value elements = 1 [json_name = "elements"]; + */ + public org.openmbee.opensysml.proto.Value getElements(int index) { + if (elementsBuilder_ == null) { + return elements_.get(index); + } else { + return elementsBuilder_.getMessage(index); + } + } + /** + * repeated .sysml.Value elements = 1 [json_name = "elements"]; + */ + public Builder setElements( + int index, org.openmbee.opensysml.proto.Value value) { + if (elementsBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + ensureElementsIsMutable(); + elements_.set(index, value); + onChanged(); + } else { + elementsBuilder_.setMessage(index, value); + } + return this; + } + /** + * repeated .sysml.Value elements = 1 [json_name = "elements"]; + */ + public Builder setElements( + int index, org.openmbee.opensysml.proto.Value.Builder builderForValue) { + if (elementsBuilder_ == null) { + ensureElementsIsMutable(); + elements_.set(index, builderForValue.build()); + onChanged(); + } else { + elementsBuilder_.setMessage(index, builderForValue.build()); + } + return this; + } + /** + * repeated .sysml.Value elements = 1 [json_name = "elements"]; + */ + public Builder addElements(org.openmbee.opensysml.proto.Value value) { + if (elementsBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + ensureElementsIsMutable(); + elements_.add(value); + onChanged(); + } else { + elementsBuilder_.addMessage(value); + } + return this; + } + /** + * repeated .sysml.Value elements = 1 [json_name = "elements"]; + */ + public Builder addElements( + int index, org.openmbee.opensysml.proto.Value value) { + if (elementsBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + ensureElementsIsMutable(); + elements_.add(index, value); + onChanged(); + } else { + elementsBuilder_.addMessage(index, value); + } + return this; + } + /** + * repeated .sysml.Value elements = 1 [json_name = "elements"]; + */ + public Builder addElements( + org.openmbee.opensysml.proto.Value.Builder builderForValue) { + if (elementsBuilder_ == null) { + ensureElementsIsMutable(); + elements_.add(builderForValue.build()); + onChanged(); + } else { + elementsBuilder_.addMessage(builderForValue.build()); + } + return this; + } + /** + * repeated .sysml.Value elements = 1 [json_name = "elements"]; + */ + public Builder addElements( + int index, org.openmbee.opensysml.proto.Value.Builder builderForValue) { + if (elementsBuilder_ == null) { + ensureElementsIsMutable(); + elements_.add(index, builderForValue.build()); + onChanged(); + } else { + elementsBuilder_.addMessage(index, builderForValue.build()); + } + return this; + } + /** + * repeated .sysml.Value elements = 1 [json_name = "elements"]; + */ + public Builder addAllElements( + java.lang.Iterable values) { + if (elementsBuilder_ == null) { + ensureElementsIsMutable(); + com.google.protobuf.AbstractMessageLite.Builder.addAll( + values, elements_); + onChanged(); + } else { + elementsBuilder_.addAllMessages(values); + } + return this; + } + /** + * repeated .sysml.Value elements = 1 [json_name = "elements"]; + */ + public Builder clearElements() { + if (elementsBuilder_ == null) { + elements_ = java.util.Collections.emptyList(); + bitField0_ = (bitField0_ & ~0x00000001); + onChanged(); + } else { + elementsBuilder_.clear(); + } + return this; + } + /** + * repeated .sysml.Value elements = 1 [json_name = "elements"]; + */ + public Builder removeElements(int index) { + if (elementsBuilder_ == null) { + ensureElementsIsMutable(); + elements_.remove(index); + onChanged(); + } else { + elementsBuilder_.remove(index); + } + return this; + } + /** + * repeated .sysml.Value elements = 1 [json_name = "elements"]; + */ + public org.openmbee.opensysml.proto.Value.Builder getElementsBuilder( + int index) { + return internalGetElementsFieldBuilder().getBuilder(index); + } + /** + * repeated .sysml.Value elements = 1 [json_name = "elements"]; + */ + public org.openmbee.opensysml.proto.ValueOrBuilder getElementsOrBuilder( + int index) { + if (elementsBuilder_ == null) { + return elements_.get(index); } else { + return elementsBuilder_.getMessageOrBuilder(index); + } + } + /** + * repeated .sysml.Value elements = 1 [json_name = "elements"]; + */ + public java.util.List + getElementsOrBuilderList() { + if (elementsBuilder_ != null) { + return elementsBuilder_.getMessageOrBuilderList(); + } else { + return java.util.Collections.unmodifiableList(elements_); + } + } + /** + * repeated .sysml.Value elements = 1 [json_name = "elements"]; + */ + public org.openmbee.opensysml.proto.Value.Builder addElementsBuilder() { + return internalGetElementsFieldBuilder().addBuilder( + org.openmbee.opensysml.proto.Value.getDefaultInstance()); + } + /** + * repeated .sysml.Value elements = 1 [json_name = "elements"]; + */ + public org.openmbee.opensysml.proto.Value.Builder addElementsBuilder( + int index) { + return internalGetElementsFieldBuilder().addBuilder( + index, org.openmbee.opensysml.proto.Value.getDefaultInstance()); + } + /** + * repeated .sysml.Value elements = 1 [json_name = "elements"]; + */ + public java.util.List + getElementsBuilderList() { + return internalGetElementsFieldBuilder().getBuilderList(); + } + private com.google.protobuf.RepeatedFieldBuilder< + org.openmbee.opensysml.proto.Value, org.openmbee.opensysml.proto.Value.Builder, org.openmbee.opensysml.proto.ValueOrBuilder> + internalGetElementsFieldBuilder() { + if (elementsBuilder_ == null) { + elementsBuilder_ = new com.google.protobuf.RepeatedFieldBuilder< + org.openmbee.opensysml.proto.Value, org.openmbee.opensysml.proto.Value.Builder, org.openmbee.opensysml.proto.ValueOrBuilder>( + elements_, + ((bitField0_ & 0x00000001) != 0), + getParentForChildren(), + isClean()); + elements_ = null; + } + return elementsBuilder_; + } + + // @@protoc_insertion_point(builder_scope:sysml.ValueSet) + } + + // @@protoc_insertion_point(class_scope:sysml.ValueSet) + private static final org.openmbee.opensysml.proto.ValueSet DEFAULT_INSTANCE; + static { + DEFAULT_INSTANCE = new org.openmbee.opensysml.proto.ValueSet(); + } + + public static org.openmbee.opensysml.proto.ValueSet getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static final com.google.protobuf.Parser + PARSER = new com.google.protobuf.AbstractParser() { + @java.lang.Override + public ValueSet parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + Builder builder = newBuilder(); + try { + builder.mergeFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(builder.buildPartial()); + } catch (com.google.protobuf.UninitializedMessageException e) { + throw e.asInvalidProtocolBufferException().setUnfinishedMessage(builder.buildPartial()); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException(e) + .setUnfinishedMessage(builder.buildPartial()); + } + return builder.buildPartial(); + } + }; + + public static com.google.protobuf.Parser parser() { + return PARSER; + } + + @java.lang.Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + @java.lang.Override + public org.openmbee.opensysml.proto.ValueSet getDefaultInstanceForType() { + return DEFAULT_INSTANCE; + } + +} + diff --git a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ValueSetOrBuilder.java b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ValueSetOrBuilder.java new file mode 100644 index 000000000..bb829d6a2 --- /dev/null +++ b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ValueSetOrBuilder.java @@ -0,0 +1,36 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// NO CHECKED-IN PROTOBUF GENCODE +// source: sysml.proto +// Protobuf Java Version: 4.33.1 + +package org.openmbee.opensysml.proto; + +@com.google.protobuf.Generated +public interface ValueSetOrBuilder extends + // @@protoc_insertion_point(interface_extends:sysml.ValueSet) + com.google.protobuf.MessageOrBuilder { + + /** + * repeated .sysml.Value elements = 1 [json_name = "elements"]; + */ + java.util.List + getElementsList(); + /** + * repeated .sysml.Value elements = 1 [json_name = "elements"]; + */ + org.openmbee.opensysml.proto.Value getElements(int index); + /** + * repeated .sysml.Value elements = 1 [json_name = "elements"]; + */ + int getElementsCount(); + /** + * repeated .sysml.Value elements = 1 [json_name = "elements"]; + */ + java.util.List + getElementsOrBuilderList(); + /** + * repeated .sysml.Value elements = 1 [json_name = "elements"]; + */ + org.openmbee.opensysml.proto.ValueOrBuilder getElementsOrBuilder( + int index); +} diff --git a/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/ApiIntegrationTest.java b/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/ApiIntegrationTest.java index 6ae443784..51ce23a3a 100644 --- a/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/ApiIntegrationTest.java +++ b/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/ApiIntegrationTest.java @@ -187,6 +187,58 @@ void anArrayAVectorAndAVectorQuantityArriveWholeOverProtobufAndJson() { } } + private static final String SET_AND_TENSOR = + """ + package T { + private import ScalarValues::*; + private import Collections::*; + private import Quantities::*; + private import MeasurementReferences::*; + private import SI::*; + attribute s : Set { :>> elements = (3, 1, 2, 2, 3); } + attribute none : Set { :>> elements = (); } + attribute cubeRef : TensorMeasurementReference { + :>> dimensions = (2, 2, 2); + :>> mRefs = (m, m, m, m, m, m, m, m); + } + attribute cube : TensorQuantityValue = + TensorCalculations::'['((1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0), cubeRef); + } + """; + + @Test + void aSetAndARankThreeTensorArriveWholeOverProtobufAndJson() { + assertTrue(connection.capabilities().has(Capabilities.SET_VALUES)); + assertTrue(connection.capabilities().has(Capabilities.TENSOR_VALUES)); + try (Connection json = + Connection.open(ServiceBinary.options().encoding(Encoding.JSON).build())) { + for (Connection each : List.of(connection, json)) { + Model model = each.parse(SET_AND_TENSOR); + Value.SetValue s = (Value.SetValue) model.eval("T::s.elements"); + assertEquals( + new Value.SetValue( + List.of( + new Value.IntegerValue(1), new Value.IntegerValue(2), new Value.IntegerValue(3))), + s); + assertEquals(List.of(1L, 2L, 3L), s.elements().stream().map(Value::asLong).toList()); + assertEquals(new Value.SetValue(List.of()), model.eval("T::none.elements")); + Value.TensorQuantityValue cube = (Value.TensorQuantityValue) model.eval("T::cube"); + assertEquals(List.of(2L, 2L, 2L), cube.dimensions()); + assertEquals(Optional.of("m"), cube.unit()); + assertEquals( + List.of(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0), + cube.components().stream().map(Quantity::magnitude).toList()); + assertEquals(6.0, cube.get(1, 0, 1).magnitude()); + assertEquals( + List.of(new Quantity.UnitFactor("SI::metre", 1.0)), + cube.get(1, 0, 1).reduction().orElseThrow().factors()); + Value.QuantityValue corner = (Value.QuantityValue) model.eval("T::cube#(2, 1, 2)"); + assertEquals(6.0, corner.quantity().magnitude()); + assertEquals(Optional.of("m"), corner.quantity().unit()); + } + } + } + private static final String MEASUREMENT_REFS = """ package M { diff --git a/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/PublicTypesTest.java b/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/PublicTypesTest.java index 90cd4748e..339a04621 100644 --- a/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/PublicTypesTest.java +++ b/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/PublicTypesTest.java @@ -67,6 +67,98 @@ void aComplexValueIsOneNumberWithBothParts() { assertThrows(IllegalStateException.class, z::asLong); } + @Test + void aSetIsItsMembersInAnyOrderAndNoneTwice() { + Value.SetValue set = + new Value.SetValue( + List.of(new Value.IntegerValue(3), new Value.IntegerValue(1), new Value.IntegerValue(2))); + Value.SetValue reordered = + new Value.SetValue( + List.of(new Value.IntegerValue(1), new Value.IntegerValue(2), new Value.IntegerValue(3))); + assertEquals(reordered, set); + assertEquals(reordered.hashCode(), set.hashCode()); + assertEquals(3, set.size()); + assertFalse(set.isEmpty()); + assertTrue(set.contains(new Value.IntegerValue(2))); + assertFalse(set.contains(new Value.IntegerValue(4))); + assertEquals(List.of(3L, 1L, 2L), set.elements().stream().map(Value::asLong).toList()); + + Value asSequence = + new Value.Sequence( + List.of(new Value.IntegerValue(1), new Value.IntegerValue(2), new Value.IntegerValue(3))); + Value setAsValue = set; + assertNotEquals(asSequence, setAsValue); + assertNotEquals(new Value.SetValue(List.of(new Value.IntegerValue(1))), setAsValue); + + Value.SetValue empty = new Value.SetValue(List.of()); + assertTrue(empty.isEmpty()); + assertEquals(new Value.SetValue(List.of()), empty); + assertEquals( + new Value.SetValue(List.of(empty, set)), new Value.SetValue(List.of(set, empty))); + + List members = new ArrayList<>(List.of(new Value.IntegerValue(1))); + Value.SetValue copied = new Value.SetValue(members); + members.add(new Value.IntegerValue(2)); + assertEquals(1, copied.size()); + List exposed = copied.elements(); + Value added = new Value.IntegerValue(3); + assertThrows(UnsupportedOperationException.class, () -> exposed.add(added)); + + List twice = List.of(new Value.IntegerValue(1), new Value.IntegerValue(1)); + assertThrows(IllegalArgumentException.class, () -> new Value.SetValue(twice)); + } + + @Test + void aTensorQuantityIsShapedAndIndexedInRowMajorOrder() { + List pascals = new ArrayList<>(); + for (int i = 1; i <= 8; i++) { + pascals.add(new Quantity((double) i, Optional.of("Pa"), Optional.empty())); + } + Value.TensorQuantityValue cube = new Value.TensorQuantityValue(List.of(2L, 2L, 2L), pascals); + assertEquals(3, cube.rank()); + assertEquals(Optional.of("Pa"), cube.unit()); + assertEquals(1.0, cube.get(0, 0, 0).magnitude()); + assertEquals(6.0, cube.get(1, 0, 1).magnitude()); + assertEquals(8.0, cube.get(1, 1, 1).magnitude()); + assertEquals(new Value.TensorQuantityValue(List.of(2L, 2L, 2L), pascals), cube); + assertNotEquals(new Value.TensorQuantityValue(List.of(2L, 4L), pascals), cube); + + assertThrows(IndexOutOfBoundsException.class, () -> cube.get(1, 1)); + assertThrows(IndexOutOfBoundsException.class, () -> cube.get(1, 1, 1, 1)); + assertThrows(IndexOutOfBoundsException.class, () -> cube.get(0, 2, 0)); + assertThrows(IndexOutOfBoundsException.class, () -> cube.get(0, -1, 0)); + + Value.TensorQuantityValue line = new Value.TensorQuantityValue(List.of(2L), pascals.subList(0, 2)); + assertEquals(1, line.rank()); + Value lineAsValue = line; + assertNotEquals(new Value.VectorQuantityValue(pascals.subList(0, 2)), lineAsValue); + + Value.TensorQuantityValue mixed = + new Value.TensorQuantityValue( + List.of(2L), + List.of( + new Quantity(1.0, Optional.of("m"), Optional.empty()), + new Quantity(2.0, Optional.of("s"), Optional.empty()))); + assertEquals(Optional.empty(), mixed.unit()); + + List seven = pascals.subList(0, 7); + assertThrows( + IllegalArgumentException.class, () -> new Value.TensorQuantityValue(List.of(2L, 2L, 2L), seven)); + assertThrows( + IllegalArgumentException.class, () -> new Value.TensorQuantityValue(List.of(0L), List.of())); + assertThrows( + IllegalArgumentException.class, + () -> new Value.TensorQuantityValue(List.of(-2L, -4L), pascals)); + assertThrows( + ArithmeticException.class, + () -> new Value.TensorQuantityValue(List.of(Long.MAX_VALUE, 2L), pascals)); + + List shape = new ArrayList<>(List.of(8L)); + Value.TensorQuantityValue copied = new Value.TensorQuantityValue(shape, pascals); + shape.set(0, 4L); + assertEquals(List.of(8L), copied.dimensions()); + } + @Test void anUnsetValueIsNotTheModelsNull() { Value unset = new Value.UnsetValue(); diff --git a/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/internal/ProtosTest.java b/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/internal/ProtosTest.java index d29119f19..3f044d475 100644 --- a/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/internal/ProtosTest.java +++ b/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/internal/ProtosTest.java @@ -16,9 +16,11 @@ import org.openmbee.opensysml.proto.Function; import org.openmbee.opensysml.proto.MeasurementRef; import org.openmbee.opensysml.proto.SymbolInfo; +import org.openmbee.opensysml.proto.TensorQuantity; import org.openmbee.opensysml.proto.UnitFactor; import org.openmbee.opensysml.proto.UnitTerm; import org.openmbee.opensysml.proto.ValueSequence; +import org.openmbee.opensysml.proto.ValueSet; import org.openmbee.opensysml.proto.Vector; import org.openmbee.opensysml.proto.VectorQuantity; import java.util.List; @@ -252,6 +254,140 @@ void aVectorQuantityReadsOneQuantityPerComponentEachWithItsUnit() { assertThrows(IllegalArgumentException.class, () -> new Value.VectorQuantityValue(none)); } + private static org.openmbee.opensysml.proto.Value set( + org.openmbee.opensysml.proto.Value... elements) { + return org.openmbee.opensysml.proto.Value.newBuilder() + .setSet(ValueSet.newBuilder().addAllElements(List.of(elements))) + .build(); + } + + private static org.openmbee.opensysml.proto.Value tensor( + List dimensions, org.openmbee.opensysml.proto.Quantity... components) { + return org.openmbee.opensysml.proto.Value.newBuilder() + .setTensorQuantity( + TensorQuantity.newBuilder() + .addAllDimensions(dimensions) + .addAllComponents(List.of(components))) + .build(); + } + + @Test + void aSetHoldsEachMemberOnceAndComparesInAnyOrder() { + Value.SetValue members = + (Value.SetValue) Protos.value(set(integer(1), integer(2), integer(3))).orElseThrow(); + assertEquals(3, members.size()); + assertTrue(!members.isEmpty()); + assertEquals( + List.of(new Value.IntegerValue(1), new Value.IntegerValue(2), new Value.IntegerValue(3)), + members.elements()); + assertTrue(members.contains(new Value.IntegerValue(2))); + assertTrue(!members.contains(new Value.IntegerValue(4))); + assertTrue(!members.contains(new Value.RealValue(2.0))); + + // The same members in another order are the same set, with the same hash; a sequence is not. + Value.SetValue reordered = + new Value.SetValue( + List.of(new Value.IntegerValue(3), new Value.IntegerValue(1), new Value.IntegerValue(2))); + assertEquals(members, reordered); + assertEquals(members.hashCode(), reordered.hashCode()); + assertNotEquals( + members, + new Value.Sequence( + List.of(new Value.IntegerValue(1), new Value.IntegerValue(2), new Value.IntegerValue(3)))); + assertNotEquals( + members, new Value.SetValue(List.of(new Value.IntegerValue(1), new Value.IntegerValue(2)))); + + // An empty set is a set; a set nests in a set and in a sequence. + Value.SetValue empty = (Value.SetValue) Protos.value(set()).orElseThrow(); + assertTrue(empty.isEmpty()); + Value.SetValue nested = (Value.SetValue) Protos.value(set(set(integer(1)), set())).orElseThrow(); + assertEquals(2, nested.size()); + assertTrue(nested.contains(empty)); + Value.Sequence holding = + (Value.Sequence) + Protos.value( + org.openmbee.opensysml.proto.Value.newBuilder() + .setSequence( + ValueSequence.newBuilder().addElements(set(integer(1))).addElements(integer(2))) + .build()) + .orElseThrow(); + assertEquals(new Value.SetValue(List.of(new Value.IntegerValue(1))), holding.elements().get(0)); + + // A member listed twice is not a set, on the wire or in the constructor. + org.openmbee.opensysml.proto.Value twice = set(integer(1), integer(1)); + TransportException repeated = + assertThrows(TransportException.class, () -> Protos.value(twice)); + assertTrue(repeated.getMessage().contains("twice"), repeated.getMessage()); + List duplicated = List.of(new Value.StringValue("a"), new Value.StringValue("a")); + assertThrows(IllegalArgumentException.class, () -> new Value.SetValue(duplicated)); + org.openmbee.opensysml.proto.Value unknown = + set(org.openmbee.opensysml.proto.Value.getDefaultInstance()); + assertThrows(TransportException.class, () -> Protos.value(unknown)); + } + + @Test + void aTensorQuantityKeepsItsRankShapeAndRowMajorComponents() { + Value.TensorQuantityValue cube = + (Value.TensorQuantityValue) + Protos.value( + tensor( + List.of(2L, 2L, 2L), + metres(1), metres(2), metres(3), metres(4), + metres(5), metres(6), metres(7), metres(8))) + .orElseThrow(); + assertEquals(3, cube.rank()); + assertEquals(List.of(2L, 2L, 2L), cube.dimensions()); + assertEquals(8, cube.components().size()); + assertEquals(Optional.of("m"), cube.unit()); + assertEquals(6.0, cube.get(1, 0, 1).magnitude().doubleValue()); + assertEquals(1.0, cube.get(0, 0, 0).magnitude().doubleValue()); + assertEquals(8.0, cube.get(1, 1, 1).magnitude().doubleValue()); + assertThrows(IndexOutOfBoundsException.class, () -> cube.get(1, 1)); + assertThrows(IndexOutOfBoundsException.class, () -> cube.get(1, 1, 1, 0)); + assertThrows(IndexOutOfBoundsException.class, () -> cube.get(2, 0, 0)); + assertThrows(IndexOutOfBoundsException.class, () -> cube.get(0, -1, 0)); + + // A rank-one tensor stays a tensor, never a vector quantity. + Value line = Protos.value(tensor(List.of(2L), metres(1), metres(2))).orElseThrow(); + assertTrue(line instanceof Value.TensorQuantityValue, line.toString()); + assertNotEquals( + line, + new Value.VectorQuantityValue( + List.of( + new Quantity(1.0, Optional.of("m"), Optional.of(METRE)), + new Quantity(2.0, Optional.of("m"), Optional.of(METRE))))); + + // Components with differing units report no shared one. + org.openmbee.opensysml.proto.Quantity speed = + org.openmbee.opensysml.proto.Quantity.newBuilder().setIntMagnitude(5).setUnit("m/s").build(); + Value.TensorQuantityValue mixed = + (Value.TensorQuantityValue) + Protos.value(tensor(List.of(1L, 2L), metres(1), speed)).orElseThrow(); + assertEquals(Optional.empty(), mixed.unit()); + assertEquals(5L, mixed.get(0, 1).magnitude()); + + // Shape and components must agree, and every dimension is positive. + org.openmbee.opensysml.proto.Value shortOne = tensor(List.of(2L, 2L), metres(1), metres(2), metres(3)); + TransportException few = assertThrows(TransportException.class, () -> Protos.value(shortOne)); + assertTrue(few.getMessage().contains("want 4"), few.getMessage()); + org.openmbee.opensysml.proto.Value scalarWithTwo = tensor(List.of(), metres(1), metres(2)); + assertThrows(TransportException.class, () -> Protos.value(scalarWithTwo)); + org.openmbee.opensysml.proto.Value zero = tensor(List.of(0L)); + TransportException nonPositive = assertThrows(TransportException.class, () -> Protos.value(zero)); + assertTrue(nonPositive.getMessage().contains("not positive"), nonPositive.getMessage()); + org.openmbee.opensysml.proto.Value negative = tensor(List.of(-1L), metres(1)); + assertThrows(TransportException.class, () -> Protos.value(negative)); + org.openmbee.opensysml.proto.Value overflow = tensor(List.of(Long.MAX_VALUE, 2L)); + assertThrows(TransportException.class, () -> Protos.value(overflow)); + org.openmbee.opensysml.proto.Value noMagnitude = + tensor(List.of(1L), org.openmbee.opensysml.proto.Quantity.newBuilder().setUnit("m").build()); + TransportException unmeasured = + assertThrows(TransportException.class, () -> Protos.value(noMagnitude)); + assertTrue(unmeasured.getMessage().contains("no magnitude"), unmeasured.getMessage()); + List none = List.of(); + assertThrows(IllegalArgumentException.class, () -> new Value.TensorQuantityValue(List.of(2L), none)); + } + private static org.openmbee.opensysml.proto.Value measurementRef(MeasurementRef.Builder ref) { return org.openmbee.opensysml.proto.Value.newBuilder().setMeasurementRef(ref).build(); } diff --git a/clients/java/opensysml-conformance/src/main/java/org/openmbee/opensysml/conformance/Rendering.java b/clients/java/opensysml-conformance/src/main/java/org/openmbee/opensysml/conformance/Rendering.java index 8166c373d..55f3b4e24 100644 --- a/clients/java/opensysml-conformance/src/main/java/org/openmbee/opensysml/conformance/Rendering.java +++ b/clients/java/opensysml-conformance/src/main/java/org/openmbee/opensysml/conformance/Rendering.java @@ -12,10 +12,12 @@ import org.openmbee.opensysml.proto.Span; import org.openmbee.opensysml.proto.Specialization; import org.openmbee.opensysml.proto.SymbolInfo; +import org.openmbee.opensysml.proto.TensorQuantity; import org.openmbee.opensysml.proto.TypeInfo; import org.openmbee.opensysml.proto.UnitFactor; import org.openmbee.opensysml.proto.UnitTerm; import org.openmbee.opensysml.proto.ValueSequence; +import org.openmbee.opensysml.proto.ValueSet; import java.util.List; import java.util.Map; @@ -92,6 +94,15 @@ static org.openmbee.opensysml.proto.Value value(Value value) { org.openmbee.opensysml.proto.Function.newBuilder() .setCalcId(function.calcId()) .setSelfId(function.selfId().orElse(0L))); + } else if (value instanceof Value.SetValue set) { + ValueSet.Builder elements = ValueSet.newBuilder(); + set.elements().forEach(element -> elements.addElements(value(element))); + builder.setSet(elements); + } else if (value instanceof Value.TensorQuantityValue tensor) { + TensorQuantity.Builder components = + TensorQuantity.newBuilder().addAllDimensions(tensor.dimensions()); + tensor.components().forEach(component -> components.addComponents(quantity(component))); + builder.setTensorQuantity(components); } else { throw new IllegalStateException("no rendering for " + value.getClass()); } diff --git a/clients/node/README.md b/clients/node/README.md index 0c0e3aac5..dae7ae10b 100644 --- a/clients/node/README.md +++ b/clients/node/README.md @@ -61,6 +61,8 @@ switch (value.kind) { case "array": value.dimensions; value.elements; // row-major, an element is any SysMLValue case "vector": value.components; // { kind: "int" | "real" }[] case "vectorQuantity": value.components; // QuantityValue[], a unit per component + case "set": value.elements; // SysMLValue[], each once, unordered + case "tensorQuantity": value.dimensions; value.components; // any rank, row-major QuantityValue[] case "enum": value.value.name; // and its literal/enumeration ids case "instance": value.id; // an object in the same tree case "sequence": value.elements; // SysMLValue[] @@ -187,10 +189,15 @@ raise a `MissingCapabilityError` naming the service, its version and the way to get one that has it. A direct capability-gated request to a service without the capability is refused with `UNIMPLEMENTED`; response-population capabilities instead omit the fields they name. A service without `structured_values`, -`measurement_refs` or `function_values` sends the value kinds those name (`array`, -`vector`, `vectorQuantity`; `measurementRef`; `function`) as `null` with an -`unsupported: …` reason. A function closing over the bindings of a behavior body -has no wire form and is sent as `null` by every service. +`measurement_refs`, `function_values`, `set_values` or `tensor_values` sends the +value kinds those name (`array`, `vector`, `vectorQuantity`; `measurementRef`; +`function`; `set`; `tensorQuantity`) as `null` with an `unsupported: …` reason. +A function closing over the bindings of a behavior body has no wire form and is +sent as `null` by every service. A `set` arrives +with its elements in the service's canonical order, so two equal sets arrive +alike; one sent to the service may list them in any order, but not twice. A +`tensorQuantity` carries its `dimensions` and one quantity per component, +row-major. ## Failures are typed diff --git a/clients/node/src/core/capabilities.ts b/clients/node/src/core/capabilities.ts index 01bb891c5..52c03c07b 100644 --- a/clients/node/src/core/capabilities.ts +++ b/clients/node/src/core/capabilities.ts @@ -28,6 +28,10 @@ export const CAPABILITY_FUNCTION_VALUES = "function_values"; export const CAPABILITY_INFINITY_VALUE = "infinity_value"; /** `Diagnostic.code` is populated, so an empty code is a finding none was assigned. */ export const CAPABILITY_DIAGNOSTIC_CODES = "diagnostic_codes"; +/** A unique, unordered collection (a `Collections::Set`'s elements) as `Value.set`, rather than an unsupported null. */ +export const CAPABILITY_SET_VALUES = "set_values"; +/** A tensor quantity of any rank as `Value.tensor_quantity`, rather than an unsupported null. */ +export const CAPABILITY_TENSOR_VALUES = "tensor_values"; /** `ParseFileRequest.language`, which declares the language of inline content. */ export const CAPABILITY_INLINE_LANGUAGE = "inline_language"; /** `ParseFileRequest.strict_conformance`. */ diff --git a/clients/node/src/core/index.ts b/clients/node/src/core/index.ts index f6d839eb1..7d9e0e1a4 100644 --- a/clients/node/src/core/index.ts +++ b/clients/node/src/core/index.ts @@ -35,10 +35,12 @@ export { CAPABILITY_MEASUREMENT_REFS, CAPABILITY_QUERY, CAPABILITY_SCHEDULE, + CAPABILITY_SET_VALUES, CAPABILITY_STRICT_CONFORMANCE, CAPABILITY_STRUCTURED_VALUES, CAPABILITY_VERIFICATION_VERDICTS, CAPABILITY_SYMBOL_ATTRIBUTES, + CAPABILITY_TENSOR_VALUES, CAPABILITY_TYPE_FACTS, CAPABILITY_UNSET_VALUE, CAPABILITY_VERIFICATION, @@ -81,6 +83,7 @@ export type { QuantityValue, SysMLValue, SysMLVerdict, + TensorQuantityValue, UnitFactor, UnitFactorization, VerdictSubject, diff --git a/clients/node/src/core/values.ts b/clients/node/src/core/values.ts index c7b377eb3..f872e0b44 100644 --- a/clients/node/src/core/values.ts +++ b/clients/node/src/core/values.ts @@ -8,8 +8,10 @@ import type { Function as FunctionMessage, MeasurementRef, Quantity, + TensorQuantity, UnitTerm, Value, + ValueSet, Vector, VectorQuantity, Verdict, @@ -22,10 +24,12 @@ import { FunctionSchema, MeasurementRefSchema, QuantitySchema, + TensorQuantitySchema, UnitFactorSchema, UnitTermSchema, ValueSchema, ValueSequenceSchema, + ValueSetSchema, VectorQuantitySchema, VectorSchema, } from "../generated/sysml_pb.js"; @@ -100,11 +104,25 @@ export interface ArrayValue { elements: SysMLValue[]; } +/** + * A tensor quantity of any rank: `dimensions` gives the extent of each dimension + * and `components` one quantity per component, flattened row-major as an + * array's elements are. A tensor of rank one is not a `vectorQuantity`. + */ +export interface TensorQuantityValue { + dimensions: bigint[]; + components: QuantityValue[]; +} + /** * A value the service computed. `absent` is the case a service sent no value at * all for, which is distinct from `unset` — a feature that exists and has none. * A `vector` is one value of numeric components, never a sequence of numbers, * and a `vectorQuantity` carries one quantity per component, each with its own unit. + * A `set` is a unique, unordered collection — a `Collections::Set`'s elements — + * distinct from a `sequence`, whose order is part of its value: the service + * sends its elements in canonical order, so equal sets arrive alike, and reads + * one sent in any order, refusing one that lists an element twice. */ export type SysMLValue = | { kind: "int"; value: bigint } @@ -121,6 +139,8 @@ export type SysMLValue = | ({ kind: "array" } & ArrayValue) | { kind: "vector"; components: Magnitude[] } | { kind: "vectorQuantity"; components: QuantityValue[] } + | { kind: "set"; elements: SysMLValue[] } + | ({ kind: "tensorQuantity" } & TensorQuantityValue) | { kind: "null"; reason: string } | { kind: "unset" } | { kind: "infinity" } @@ -153,9 +173,10 @@ export type SysMLVerdict = * * @throws {MalformedValueError} for a value that contradicts itself: an array * whose elements do not fill its dimensions, a vector with a component that - * is not a number, a vector quantity with no components, a quantity - * (alone or as a component) with no magnitude, or a measurement reference - * naming no unit or a unit without its reduction. + * is not a number, a vector quantity with no components, a tensor quantity + * whose components do not fill its dimensions, a quantity (alone or as a + * component) with no magnitude, or a measurement reference naming no unit + * or a unit without its reduction. */ export function decodeValue(value: Value | undefined): SysMLValue { if (value === undefined) { @@ -191,6 +212,10 @@ export function decodeValue(value: Value | undefined): SysMLValue { return { kind: "vector", components: decodeVector(kind.value) }; case "vectorQuantity": return { kind: "vectorQuantity", components: decodeVectorQuantity(kind.value) }; + case "set": + return { kind: "set", elements: decodeSet(kind.value) }; + case "tensorQuantity": + return { kind: "tensorQuantity", ...decodeTensorQuantity(kind.value) }; case "null": return { kind: "null", reason: kind.value }; case "unset": @@ -249,7 +274,7 @@ export function encodeValue(value: SysMLValue): Value { kind: { case: "enumLiteral", value: create(EnumLiteralSchema, value.value) }, }); case "array": - checkArrayShape(value.dimensions, value.elements.length); + checkShape("an array", value.dimensions, value.elements.length); return create(ValueSchema, { kind: { case: "array", @@ -276,6 +301,24 @@ export function encodeValue(value: SysMLValue): Value { value: create(VectorQuantitySchema, { components: value.components.map(encodeQuantity) }), }, }); + case "set": + return create(ValueSchema, { + kind: { + case: "set", + value: create(ValueSetSchema, { elements: value.elements.map(encodeValue) }), + }, + }); + case "tensorQuantity": + checkShape("a tensor quantity", value.dimensions, value.components.length); + return create(ValueSchema, { + kind: { + case: "tensorQuantity", + value: create(TensorQuantitySchema, { + dimensions: value.dimensions, + components: value.components.map(encodeQuantity), + }), + }, + }); case "null": return create(ValueSchema, { kind: { case: "null", value: value.reason } }); case "unset": @@ -356,6 +399,10 @@ export function formatValue(value: SysMLValue): string { return `⟨${value.components.map(formatMagnitude).join(", ")}⟩`; case "vectorQuantity": return `⟨${value.components.map((c) => formatValue({ kind: "quantity", ...c })).join(", ")}⟩`; + case "set": + return `{${value.elements.map(formatValue).join(", ")}}`; + case "tensorQuantity": + return formatTensorQuantity(value); case "null": return value.reason === "" ? "null" : `null (${value.reason})`; case "unset": @@ -375,6 +422,18 @@ function formatMagnitude(magnitude: Magnitude): string { return magnitude.kind === "int" ? magnitude.value.toString() : formatReal(magnitude.value); } +/** `Tensor(2, 2, 2)[1.0, …, 8.0][Pa]` when every component shares a unit; else each with its own. */ +function formatTensorQuantity(tensor: TensorQuantityValue): string { + const dims = tensor.dimensions.join(", "); + const units = new Set(tensor.components.map((c) => c.unit)); + if (units.size === 1 && tensor.components[0]?.unit !== "") { + const body = tensor.components.map((c) => formatMagnitude(c.magnitude)).join(", "); + return `Tensor(${dims})[${body}][${tensor.components[0]?.unit ?? ""}]`; + } + const body = tensor.components.map((c) => formatValue({ kind: "quantity", ...c })).join(", "); + return `Tensor(${dims})[${body}]`; +} + /** `1.5 - 2.0i`, as the REPL prints a Complex; the sign is the imaginary part's. */ function formatComplex(value: ComplexValue): string { const sign = value.imaginary < 0 || Object.is(value.imaginary, -0) ? "-" : "+"; @@ -477,26 +536,35 @@ function encodeMagnitude(magnitude: Magnitude): Value { } /** The flattened size the dimensions demand, refusing a dimension that is not positive. */ -function checkArrayShape(dimensions: bigint[], elementCount: number): void { +function checkShape(what: string, dimensions: bigint[], elementCount: number): void { let size = 1n; for (const extent of dimensions) { if (extent <= 0n) { - throw new MalformedValueError(`an array dimension is ${extent.toString()}, not positive`); + throw new MalformedValueError(`${what} dimension is ${extent.toString()}, not positive`); } size *= extent; } if (size !== BigInt(elementCount)) { throw new MalformedValueError( - `an array of dimensions (${dimensions.join(", ")}) holds ${elementCount} element(s), want ${size.toString()}`, + `${what} of dimensions (${dimensions.join(", ")}) holds ${elementCount} element(s), want ${size.toString()}`, ); } } function decodeArray(array: ArrayMessage): ArrayValue { - checkArrayShape(array.dimensions, array.elements.length); + checkShape("an array", array.dimensions, array.elements.length); return { dimensions: [...array.dimensions], elements: array.elements.map(decodeValue) }; } +function decodeSet(set: ValueSet): SysMLValue[] { + return set.elements.map(decodeValue); +} + +function decodeTensorQuantity(tensor: TensorQuantity): TensorQuantityValue { + checkShape("a tensor quantity", tensor.dimensions, tensor.components.length); + return { dimensions: [...tensor.dimensions], components: tensor.components.map(decodeQuantity) }; +} + function decodeVector(vector: Vector): Magnitude[] { return vector.components.map((component) => { switch (component.kind.case) { diff --git a/clients/node/src/generated/sysml_pb.ts b/clients/node/src/generated/sysml_pb.ts index 40782ec68..aae810dba 100644 --- a/clients/node/src/generated/sysml_pb.ts +++ b/clients/node/src/generated/sysml_pb.ts @@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file sysml.proto. */ export const file_sysml: GenFile = /*@__PURE__*/ - fileDesc("CgtzeXNtbC5wcm90bxIFc3lzbWwi4gEKB1ZlcmRpY3QSDAoEa2luZBgBIAEoCRISCgplbGVtZW50X2lkGAIgASgJEg8KB2VsZW1lbnQYAyABKAkSDQoFaG9sZHMYBCABKAgSEQoJY29uZGl0aW9uGAUgASgJEhMKC2luc3RhbmNlX2lkGAYgASgDEhgKEGluc3RhbmNlX3R5cGVfaWQYByABKAkSDQoFZXJyb3IYCCABKAkSLAoOZmFpbHVyZV9yZWFzb24YCSABKA4yFC5zeXNtbC5GYWlsdXJlUmVhc29uEhYKDnJlcXVpcmVtZW50X2lkGAogASgJIlsKF1ZlcmlmeUNvbnN0cmFpbnRSZXF1ZXN0EhIKCm1vZGVsX2hhc2gYASABKAkSEQoJc3ltYm9sX2lkGAIgASgJEhkKEXN1YmplY3Rfc3ltYm9sX2lkGAMgASgJIpYBChhWZXJpZnlDb25zdHJhaW50UmVzcG9uc2USHwoHdmVyZGljdBgBIAEoCzIOLnN5c21sLlZlcmRpY3QSIgoJaW5zdGFuY2VzGAIgAygLMg8uc3lzbWwuSW5zdGFuY2USDQoFZXJyb3IYAyABKAkSJgoLZGlhZ25vc3RpY3MYBCADKAsyES5zeXNtbC5EaWFnbm9zdGljIlwKGFZlcmlmeVJlcXVpcmVtZW50UmVxdWVzdBISCgptb2RlbF9oYXNoGAEgASgJEhEKCXN5bWJvbF9pZBgCIAEoCRIZChFzdWJqZWN0X3N5bWJvbF9pZBgDIAEoCSJtChNWZXJpZmljYXRpb25WZXJkaWN0Eg8KB2Nhc2VfaWQYASABKAkSDAoEa2luZBgCIAEoCRIOCgZkZXRhaWwYAyABKAkSDwoHc3ViY2FzZRgEIAEoCBIWCg5yZXF1aXJlbWVudF9pZBgFIAEoCSLSAQoZVmVyaWZ5UmVxdWlyZW1lbnRSZXNwb25zZRIfCgd2ZXJkaWN0GAEgASgLMg4uc3lzbWwuVmVyZGljdBIiCglpbnN0YW5jZXMYAiADKAsyDy5zeXNtbC5JbnN0YW5jZRINCgVlcnJvchgDIAEoCRImCgtkaWFnbm9zdGljcxgEIAMoCzIRLnN5c21sLkRpYWdub3N0aWMSOQoVdmVyaWZpY2F0aW9uX3ZlcmRpY3RzGAUgAygLMhouc3lzbWwuVmVyaWZpY2F0aW9uVmVyZGljdCJCChlWZXJpZnlTYXRpc2ZhY3Rpb25SZXF1ZXN0EhIKCm1vZGVsX2hhc2gYASABKAkSEQoJc3ltYm9sX2lkGAIgASgJIoICChpWZXJpZnlTYXRpc2ZhY3Rpb25SZXNwb25zZRIgCgh2ZXJkaWN0cxgBIAMoCzIOLnN5c21sLlZlcmRpY3QSIgoJaW5zdGFuY2VzGAIgAygLMg8uc3lzbWwuSW5zdGFuY2USDQoFZXJyb3IYAyABKAkSJgoLZGlhZ25vc3RpY3MYBCADKAsyES5zeXNtbC5EaWFnbm9zdGljEiwKDmZhaWx1cmVfcmVhc29uGAUgASgOMhQuc3lzbWwuRmFpbHVyZVJlYXNvbhI5ChV2ZXJpZmljYXRpb25fdmVyZGljdHMYBiADKAsyGi5zeXNtbC5WZXJpZmljYXRpb25WZXJkaWN0Il0KE0V2YWx1YXRlQ2FsY1JlcXVlc3QSEgoKbW9kZWxfaGFzaBgBIAEoCRIRCglzeW1ib2xfaWQYAiABKAkSHwoJYXJndW1lbnRzGAMgAygLMgwuc3lzbWwuVmFsdWUivQEKFEV2YWx1YXRlQ2FsY1Jlc3BvbnNlEhwKBnJlc3VsdBgBIAEoCzIMLnN5c21sLlZhbHVlEiIKB291dHB1dHMYAiADKAsyES5zeXNtbC5DYWxjT3V0cHV0Eg0KBWVycm9yGAMgASgJEiYKC2RpYWdub3N0aWNzGAQgAygLMhEuc3lzbWwuRGlhZ25vc3RpYxIsCg5mYWlsdXJlX3JlYXNvbhgFIAEoDjIULnN5c21sLkZhaWx1cmVSZWFzb24iNwoKQ2FsY091dHB1dBIMCgRuYW1lGAEgASgJEhsKBXZhbHVlGAIgASgLMgwuc3lzbWwuVmFsdWUilgIKElJ1bkFuYWx5c2lzUmVxdWVzdBISCgptb2RlbF9oYXNoGAEgASgJEhEKCXN5bWJvbF9pZBgCIAEoCRIZChFzdWJqZWN0X3N5bWJvbF9pZBgDIAEoCRIfCglhcmd1bWVudHMYBCADKAsyDC5zeXNtbC5WYWx1ZRJGCg9uYW1lZF9hcmd1bWVudHMYBSADKAsyLS5zeXNtbC5SdW5BbmFseXNpc1JlcXVlc3QuTmFtZWRBcmd1bWVudHNFbnRyeRIQCghzY2hlZHVsZRgGIAEoCRpDChNOYW1lZEFyZ3VtZW50c0VudHJ5EgsKA2tleRgBIAEoCRIbCgV2YWx1ZRgCIAEoCzIMLnN5c21sLlZhbHVlOgI4ASKfAgoTUnVuQW5hbHlzaXNSZXNwb25zZRIiCgdvdXRwdXRzGAEgAygLMhEuc3lzbWwuQ2FsY091dHB1dBIgCgh2ZXJkaWN0cxgCIAMoCzIOLnN5c21sLlZlcmRpY3QSIgoJaW5zdGFuY2VzGAMgAygLMg8uc3lzbWwuSW5zdGFuY2USDQoFZXJyb3IYBCABKAkSJgoLZGlhZ25vc3RpY3MYBSADKAsyES5zeXNtbC5EaWFnbm9zdGljEiwKDmZhaWx1cmVfcmVhc29uGAYgASgOMhQuc3lzbWwuRmFpbHVyZVJlYXNvbhI5ChV2ZXJpZmljYXRpb25fdmVyZGljdHMYByADKAsyGi5zeXNtbC5WZXJpZmljYXRpb25WZXJkaWN0IowBChBQYXJzZUZpbGVSZXF1ZXN0EhMKCWZpbGVfcGF0aBgBIAEoCUgAEhEKB2NvbnRlbnQYAiABKAlIABIYCgxjb250ZW50X2hhc2gYAyABKAlCAhgBEhAKCGxhbmd1YWdlGAQgASgJEhoKEnN0cmljdF9jb25mb3JtYW5jZRgFIAEoCEIICgZzb3VyY2UiYgoOU291cmNlRG9jdW1lbnQSEwoJZmlsZV9wYXRoGAEgASgJSAASEQoHY29udGVudBgCIAEoCUgAEhAKCGxhbmd1YWdlGAMgASgJEgwKBG5hbWUYBCABKAlCCAoGc291cmNlIlsKE1BhcnNlU291cmNlc1JlcXVlc3QSKAoJZG9jdW1lbnRzGAEgAygLMhUuc3lzbWwuU291cmNlRG9jdW1lbnQSGgoSc3RyaWN0X2NvbmZvcm1hbmNlGAIgASgIIoMBChRQYXJzZVNvdXJjZXNSZXNwb25zZRISCgptb2RlbF9oYXNoGAEgASgJEiAKBXJvb3RzGAIgAygLMhEuc3lzbWwuU3ltYm9sSW5mbxImCgtkaWFnbm9zdGljcxgDIAMoCzIRLnN5c21sLkRpYWdub3N0aWMSDQoFZXJyb3IYBCABKAkifwoRUGFyc2VGaWxlUmVzcG9uc2USEgoKbW9kZWxfaGFzaBgBIAEoCRIfCgRyb290GAIgASgLMhEuc3lzbWwuU3ltYm9sSW5mbxImCgtkaWFnbm9zdGljcxgDIAMoCzIRLnN5c21sLkRpYWdub3N0aWMSDQoFZXJyb3IYBCABKAkiOQoQR2V0U3ltYm9sUmVxdWVzdBISCgptb2RlbF9oYXNoGAEgASgJEhEKCXN5bWJvbF9pZBgCIAEoCSJCCg5TeW1ib2xSZXNwb25zZRIhCgZzeW1ib2wYASABKAsyES5zeXNtbC5TeW1ib2xJbmZvEg0KBWVycm9yGAIgASgJIigKEkRpYWdub3N0aWNzUmVxdWVzdBISCgptb2RlbF9oYXNoGAEgASgJIkwKE0RpYWdub3N0aWNzUmVzcG9uc2USJgoLZGlhZ25vc3RpY3MYASADKAsyES5zeXNtbC5EaWFnbm9zdGljEg0KBWVycm9yGAIgASgJIm8KD0V2YWx1YXRlUmVxdWVzdBISCgptb2RlbF9oYXNoGAEgASgJEhIKCmV4cHJlc3Npb24YAiABKAkSGQoRY29udGV4dF9zeW1ib2xfaWQYAyABKAkSGQoRc3ViamVjdF9zeW1ib2xfaWQYBCABKAkiZwoQRXZhbHVhdGVSZXNwb25zZRIcCgZyZXN1bHQYASABKAsyDC5zeXNtbC5WYWx1ZRINCgVlcnJvchgCIAEoCRImCgtkaWFnbm9zdGljcxgDIAMoCzIRLnN5c21sLkRpYWdub3N0aWMiwgEKCEluc3RhbmNlEgoKAmlkGAEgASgDEhYKDnR5cGVfc3ltYm9sX2lkGAIgASgJEjoKDmZlYXR1cmVfdmFsdWVzGAQgAygLMiIuc3lzbWwuSW5zdGFuY2UuRmVhdHVyZVZhbHVlc0VudHJ5GkkKEkZlYXR1cmVWYWx1ZXNFbnRyeRILCgNrZXkYASABKAkSIgoFdmFsdWUYAiABKAsyEy5zeXNtbC5GZWF0dXJlVmFsdWU6AjgBSgQIAxAEUgVzbG90cyKEAQoMRmVhdHVyZVZhbHVlEhQKDGZlYXR1cmVfbmFtZRgBIAEoCRIbCgV2YWx1ZRgCIAEoCzIMLnN5c21sLlZhbHVlEhwKBnZhbHVlcxgDIAMoCzIMLnN5c21sLlZhbHVlEhQKDG1hdGVyaWFsaXplZBgEIAEoCBINCgVlcnJvchgFIAEoCSI7ChJJbnN0YW50aWF0ZVJlcXVlc3QSEgoKbW9kZWxfaGFzaBgBIAEoCRIRCglzeW1ib2xfaWQYAiABKAkikwEKE0luc3RhbnRpYXRlUmVzcG9uc2USIQoIaW5zdGFuY2UYASABKAsyDy5zeXNtbC5JbnN0YW5jZRINCgVlcnJvchgCIAEoCRImCgtkaWFnbm9zdGljcxgDIAMoCzIRLnN5c21sLkRpYWdub3N0aWMSIgoJaW5zdGFuY2VzGAQgAygLMg8uc3lzbWwuSW5zdGFuY2UizAEKFEV4ZWN1dGVBY3Rpb25SZXF1ZXN0EhIKCm1vZGVsX2hhc2gYASABKAkSGAoQYWN0aW9uX3N5bWJvbF9pZBgCIAEoCRI3CgZpbnB1dHMYAyADKAsyJy5zeXNtbC5FeGVjdXRlQWN0aW9uUmVxdWVzdC5JbnB1dHNFbnRyeRIQCghzY2hlZHVsZRgEIAEoCRo7CgtJbnB1dHNFbnRyeRILCgNrZXkYASABKAkSGwoFdmFsdWUYAiABKAsyDC5zeXNtbC5WYWx1ZToCOAEiyAEKFUV4ZWN1dGVBY3Rpb25SZXNwb25zZRI6CgdvdXRwdXRzGAEgAygLMikuc3lzbWwuRXhlY3V0ZUFjdGlvblJlc3BvbnNlLk91dHB1dHNFbnRyeRINCgVlcnJvchgCIAEoCRImCgtkaWFnbm9zdGljcxgDIAMoCzIRLnN5c21sLkRpYWdub3N0aWMaPAoMT3V0cHV0c0VudHJ5EgsKA2tleRgBIAEoCRIbCgV2YWx1ZRgCIAEoCzIMLnN5c21sLlZhbHVlOgI4ASJsChNFeGVjdXRlU3RhdGVSZXF1ZXN0EhIKCm1vZGVsX2hhc2gYASABKAkSHwoXc3RhdGVfbWFjaGluZV9zeW1ib2xfaWQYAiABKAkSDgoGZXZlbnRzGAMgAygJEhAKCHNjaGVkdWxlGAQgASgJIu4BChRFeGVjdXRlU3RhdGVSZXNwb25zZRIWCg5zdGF0ZXNfdmlzaXRlZBgBIAMoCRJECg1maW5hbF9jb250ZXh0GAIgAygLMi0uc3lzbWwuRXhlY3V0ZVN0YXRlUmVzcG9uc2UuRmluYWxDb250ZXh0RW50cnkSDQoFZXJyb3IYAyABKAkSJgoLZGlhZ25vc3RpY3MYBCADKAsyES5zeXNtbC5EaWFnbm9zdGljGkEKEUZpbmFsQ29udGV4dEVudHJ5EgsKA2tleRgBIAEoCRIbCgV2YWx1ZRgCIAEoCzIMLnN5c21sLlZhbHVlOgI4ASKgAQoOQ29udmVydFJlcXVlc3QSEwoJZmlsZV9wYXRoGAEgASgJSAASEQoHY29udGVudBgCIAEoCUgAEhQKCm1vZGVsX2hhc2gYBiABKAlIABITCgtmcm9tX2Zvcm1hdBgDIAEoCRIRCgl0b19mb3JtYXQYBCABKAkSHgoWdG9sZXJhdGVfc3ludGF4X2Vycm9ycxgFIAEoCEIICgZzb3VyY2UitAEKD0NvbnZlcnRSZXNwb25zZRIPCgdjb250ZW50GAEgASgJEhMKC2Zyb21fZm9ybWF0GAIgASgJEhEKCXRvX2Zvcm1hdBgDIAEoCRINCgVlcnJvchgEIAEoCRImCgtkaWFnbm9zdGljcxgFIAMoCzIRLnN5c21sLkRpYWdub3N0aWMSFAoMZXhwZXJpbWVudGFsGAYgASgIEhsKE2V4cGVyaW1lbnRhbF9ub3RpY2UYByABKAkiUQoRQXBwbHlFZGl0c1JlcXVlc3QSEgoKbW9kZWxfaGFzaBgBIAEoCRIoCgpvcGVyYXRpb25zGAIgAygLMhQuc3lzbWwuRWRpdE9wZXJhdGlvbiK8AQoNRWRpdE9wZXJhdGlvbhIoCglzZXRfdmFsdWUYASABKAsyEy5zeXNtbC5TZXRWYWx1ZUVkaXRIABIjCgZyZW5hbWUYAiABKAsyES5zeXNtbC5SZW5hbWVFZGl0SAASKgoKYWRkX21lbWJlchgDIAEoCzIULnN5c21sLkFkZE1lbWJlckVkaXRIABIjCgZkZWxldGUYBCABKAsyES5zeXNtbC5EZWxldGVFZGl0SABCCwoJb3BlcmF0aW9uIoIBCg1BZGRNZW1iZXJFZGl0Eg0KBW93bmVyGAEgASgJEgwKBGtpbmQYAiABKAkSDAoEbmFtZRgDIAEoCRIMCgR0eXBlGAQgASgJEhQKDG11bHRpcGxpY2l0eRgFIAEoCRINCgV2YWx1ZRgGIAEoCRITCgtzcGVjaWFsaXplcxgHIAMoCSItCgpEZWxldGVFZGl0Eg4KBnRhcmdldBgBIAEoCRIPCgdjYXNjYWRlGAIgASgIIi0KDFNldFZhbHVlRWRpdBIOCgZ0YXJnZXQYASABKAkSDQoFdmFsdWUYAiABKAkiLgoKUmVuYW1lRWRpdBIOCgZ0YXJnZXQYASABKAkSEAoIbmV3X25hbWUYAiABKAkiwgEKEkFwcGx5RWRpdHNSZXNwb25zZRIPCgdjb250ZW50GAEgASgJEiMKB2FwcGxpZWQYAiADKAsyEi5zeXNtbC5BcHBsaWVkRWRpdBINCgVlcnJvchgDIAEoCRIjCgdmYWlsdXJlGAQgASgOMhIuc3lzbWwuRWRpdEZhaWx1cmUSJgoLZGlhZ25vc3RpY3MYBSADKAsyES5zeXNtbC5EaWFnbm9zdGljEhoKEnJlZmVycmluZ19lbGVtZW50cxgGIAMoCSJ6CgtBcHBsaWVkRWRpdBIXCg9vcGVyYXRpb25faW5kZXgYASABKAUSDgoGdGFyZ2V0GAIgASgJEg4KBm9mZnNldBgDIAEoBRIOCgZsZW5ndGgYBCABKAUSEAoIb2xkX3RleHQYBSABKAkSEAoIbmV3X3RleHQYBiABKAki/QIKClN5bWJvbEluZm8SCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRIMCgRraW5kGAMgASgJEjEKCG1ldGFkYXRhGAQgAygLMh8uc3lzbWwuU3ltYm9sSW5mby5NZXRhZGF0YUVudHJ5EhEKCWNoaWxkX2lkcxgFIAMoCRIoCgphdHRyaWJ1dGVzGAYgAygLMhQuc3lzbWwuQXR0cmlidXRlSW5mbxIiCgl0eXBlX2luZm8YByABKAsyDy5zeXNtbC5UeXBlSW5mbxItCgxtdWx0aXBsaWNpdHkYCCABKAsyFy5zeXNtbC5NdWx0aXBsaWNpdHlJbmZvEi4KD3NwZWNpYWxpemF0aW9ucxgJIAMoCzIVLnN5c21sLlNwZWNpYWxpemF0aW9uEiMKG3dpdGhoZWxkX2xpYnJhcnlfYXR0cmlidXRlcxgKIAEoBRovCg1NZXRhZGF0YUVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiWAoOU3BlY2lhbGl6YXRpb24SDAoEa2luZBgBIAEoCRIQCghkZWNsYXJlZBgCIAEoCRIRCgl0YXJnZXRfaWQYAyABKAkSEwoLdGFyZ2V0X2tpbmQYBCABKAkilQEKCFR5cGVJbmZvEhAKCGRlY2xhcmVkGAEgASgJEhMKC3Jlc29sdmVkX2lkGAIgASgJEhUKDXJlc29sdmVkX2tpbmQYAyABKAkSEQoJcHJpbWl0aXZlGAQgASgJEhgKEHByaW1pdGl2ZV9zb3VyY2UYBSABKAkSEAoIcXVhbnRpdHkYBiABKAgSDAoEdW5pdBgHIAEoCSIwChBNdWx0aXBsaWNpdHlJbmZvEg0KBWxvd2VyGAEgASgJEg0KBXVwcGVyGAIgASgJIlYKDUF0dHJpYnV0ZUluZm8SDAoEbmFtZRgBIAEoCRIMCgR0eXBlGAIgASgJEhsKBXZhbHVlGAMgASgLMgwuc3lzbWwuVmFsdWUSDAoEdW5pdBgEIAEoCSKbBAoFVmFsdWUSEwoJaW50X3ZhbHVlGAEgASgDSAASFAoKcmVhbF92YWx1ZRgCIAEoAUgAEhQKCmJvb2xfdmFsdWUYAyABKAhIABIWCgxzdHJpbmdfdmFsdWUYBCABKAlIABIVCgtpbnN0YW5jZV9pZBgFIAEoA0gAEigKCHNlcXVlbmNlGAYgASgLMhQuc3lzbWwuVmFsdWVTZXF1ZW5jZUgAEg4KBG51bGwYByABKAlIABIjCghxdWFudGl0eRgIIAEoCzIPLnN5c21sLlF1YW50aXR5SAASKgoMZW51bV9saXRlcmFsGAkgASgLMhIuc3lzbWwuRW51bUxpdGVyYWxIABIPCgV1bnNldBgKIAEoCEgAEiEKB2NvbXBsZXgYCyABKAsyDi5zeXNtbC5Db21wbGV4SAASHQoFYXJyYXkYDCABKAsyDC5zeXNtbC5BcnJheUgAEh8KBnZlY3RvchgNIAEoCzINLnN5c21sLlZlY3RvckgAEjAKD3ZlY3Rvcl9xdWFudGl0eRgOIAEoCzIVLnN5c21sLlZlY3RvclF1YW50aXR5SAASMAoPbWVhc3VyZW1lbnRfcmVmGA8gASgLMhUuc3lzbWwuTWVhc3VyZW1lbnRSZWZIABISCghpbmZpbml0eRgQIAEoCEgAEiMKCGZ1bmN0aW9uGBEgASgLMg8uc3lzbWwuRnVuY3Rpb25IAEIGCgRraW5kIiwKCEZ1bmN0aW9uEg8KB2NhbGNfaWQYASABKAkSDwoHc2VsZl9pZBgCIAEoAyI7CgVBcnJheRISCgpkaW1lbnNpb25zGAEgAygDEh4KCGVsZW1lbnRzGAIgAygLMgwuc3lzbWwuVmFsdWUiKgoGVmVjdG9yEiAKCmNvbXBvbmVudHMYASADKAsyDC5zeXNtbC5WYWx1ZSI1Cg5WZWN0b3JRdWFudGl0eRIjCgpjb21wb25lbnRzGAEgAygLMg8uc3lzbWwuUXVhbnRpdHkiKgoHQ29tcGxleBIMCgRyZWFsGAEgASgBEhEKCWltYWdpbmFyeRgCIAEoASJHCgtFbnVtTGl0ZXJhbBISCgpsaXRlcmFsX2lkGAEgASgJEhYKDmVudW1lcmF0aW9uX2lkGAIgASgJEgwKBG5hbWUYAyABKAkiLwoNVmFsdWVTZXF1ZW5jZRIeCghlbGVtZW50cxgBIAMoCzIMLnN5c21sLlZhbHVlInwKCFF1YW50aXR5EhcKDWludF9tYWduaXR1ZGUYASABKANIABIYCg5yZWFsX21hZ25pdHVkZRgCIAEoAUgAEgwKBHVuaXQYAyABKAkSIgoJdW5pdF90ZXJtGAQgASgLMg8uc3lzbWwuVW5pdFRlcm1CCwoJbWFnbml0dWRlIlMKDk1lYXN1cmVtZW50UmVmEgwKBHVuaXQYASABKAkSIgoJdW5pdF90ZXJtGAIgASgLMg8uc3lzbWwuVW5pdFRlcm0SDwoHdW5pdF9pZBgDIAEoCSJUCghVbml0VGVybRIRCglzY2FsZV9udW0YASABKAESEQoJc2NhbGVfZGVuGAIgASgBEiIKB2ZhY3RvcnMYAyADKAsyES5zeXNtbC5Vbml0RmFjdG9yIi8KClVuaXRGYWN0b3ISDwoHdW5pdF9pZBgBIAEoCRIQCghleHBvbmVudBgCIAEoASJYCgpEaWFnbm9zdGljEhAKCHNldmVyaXR5GAEgASgJEg8KB21lc3NhZ2UYAiABKAkSGQoEc3BhbhgDIAEoCzILLnN5c21sLlNwYW4SDAoEY29kZRgEIAEoCSJeCgRTcGFuEgwKBGZpbGUYASABKAkSEgoKc3RhcnRfbGluZRgCIAEoBRIRCglzdGFydF9jb2wYAyABKAUSEAoIZW5kX2xpbmUYBCABKAUSDwoHZW5kX2NvbBgFIAEoBSITChFTZXJ2ZXJJbmZvUmVxdWVzdCI7ChJTZXJ2ZXJJbmZvUmVzcG9uc2USDwoHdmVyc2lvbhgBIAEoCRIUCgxjYXBhYmlsaXRpZXMYAiADKAkiUwoMUXVlcnlSZXF1ZXN0EhIKCm1vZGVsX2hhc2gYASABKAkSGwoFcXVlcnkYAiABKAsyDC5zeXNtbC5RdWVyeRISCgpvc2xjX3F1ZXJ5GAMgASgJIjwKDVF1ZXJ5UmVzcG9uc2USKwoIZWxlbWVudHMYASADKAsyGS5zeXNtbC5RdWVyeVJlc3VsdEVsZW1lbnQiSAoFUXVlcnkSDQoFc2NvcGUYASADKAkSDgoGc2VsZWN0GAIgAygJEiAKBXdoZXJlGAMgASgLMhEuc3lzbWwuQ29uc3RyYWludCJ8CgpDb25zdHJhaW50Ei8KCXByaW1pdGl2ZRgBIAEoCzIaLnN5c21sLlByaW1pdGl2ZUNvbnN0cmFpbnRIABIvCgljb21wb3NpdGUYAiABKAsyGi5zeXNtbC5Db21wb3NpdGVDb25zdHJhaW50SABCDAoKY29uc3RyYWludCJzChNQcmltaXRpdmVDb25zdHJhaW50Eg8KB2ludmVyc2UYASABKAgSEAoIcHJvcGVydHkYAiABKAkSKgoIb3BlcmF0b3IYAyABKA4yGC5zeXNtbC5QcmltaXRpdmVPcGVyYXRvchINCgV2YWx1ZRgEIAMoCSJoChNDb21wb3NpdGVDb25zdHJhaW50EioKCG9wZXJhdG9yGAEgASgOMhguc3lzbWwuQ29tcG9zaXRlT3BlcmF0b3ISJQoKY29uc3RyYWludBgCIAMoCzIRLnN5c21sLkNvbnN0cmFpbnQioAEKElF1ZXJ5UmVzdWx0RWxlbWVudBIKCgJpZBgBIAEoCRIMCgR0eXBlGAIgASgJEj0KCnByb3BlcnRpZXMYAyADKAsyKS5zeXNtbC5RdWVyeVJlc3VsdEVsZW1lbnQuUHJvcGVydGllc0VudHJ5GjEKD1Byb3BlcnRpZXNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBInMKClN3ZWVwUmFuZ2USEQoJcGFyYW1ldGVyGAEgASgJEhsKBXN0YXJ0GAIgASgLMgwuc3lzbWwuVmFsdWUSGQoDZW5kGAMgASgLMgwuc3lzbWwuVmFsdWUSGgoEc3RlcBgEIAEoCzIMLnN5c21sLlZhbHVlIsACCg9SdW5Td2VlcFJlcXVlc3QSEgoKbW9kZWxfaGFzaBgBIAEoCRIRCglzeW1ib2xfaWQYAiABKAkSGQoRc3ViamVjdF9zeW1ib2xfaWQYAyABKAkSHwoJYXJndW1lbnRzGAQgAygLMgwuc3lzbWwuVmFsdWUSQwoPbmFtZWRfYXJndW1lbnRzGAUgAygLMiouc3lzbWwuUnVuU3dlZXBSZXF1ZXN0Lk5hbWVkQXJndW1lbnRzRW50cnkSIQoGcmFuZ2VzGAYgAygLMhEuc3lzbWwuU3dlZXBSYW5nZRIPCgdzYW1wbGVzGAcgASgDEgwKBHNlZWQYCCABKAQaQwoTTmFtZWRBcmd1bWVudHNFbnRyeRILCgNrZXkYASABKAkSGwoFdmFsdWUYAiABKAsyDC5zeXNtbC5WYWx1ZToCOAEiyAEKCFN3ZWVwUm93EiEKBmlucHV0cxgBIAMoCzIRLnN5c21sLkNhbGNPdXRwdXQSIgoHb3V0cHV0cxgCIAMoCzIRLnN5c21sLkNhbGNPdXRwdXQSIAoIdmVyZGljdHMYAyADKAsyDi5zeXNtbC5WZXJkaWN0EhYKDmVsYXBzZWRfbWljcm9zGAQgASgDEg0KBWVycm9yGAUgASgJEiwKDmZhaWx1cmVfcmVhc29uGAYgASgOMhQuc3lzbWwuRmFpbHVyZVJlYXNvbiLtAQoQUnVuU3dlZXBSZXNwb25zZRIdCgRyb3dzGAEgAygLMg8uc3lzbWwuU3dlZXBSb3cSEgoKcGFyYW1ldGVycxgCIAMoCRIPCgdzYW1wbGVkGAMgASgIEgwKBHNlZWQYBCABKAQSDQoFZXJyb3IYBSABKAkSJgoLZGlhZ25vc3RpY3MYBiADKAsyES5zeXNtbC5EaWFnbm9zdGljEiwKDmZhaWx1cmVfcmVhc29uGAcgASgOMhQuc3lzbWwuRmFpbHVyZVJlYXNvbhIiCglpbnN0YW5jZXMYCCADKAsyDy5zeXNtbC5JbnN0YW5jZSJuChdSdW5Eb2N1bWVudFF1ZXJ5UmVxdWVzdBISCgptb2RlbF9oYXNoGAEgASgJEhAKCHF1ZXJ5X2lkGAIgASgJEi0KCGJpbmRpbmdzGAMgAygLMhsuc3lzbWwuRG9jdW1lbnRRdWVyeUJpbmRpbmciTwoURG9jdW1lbnRRdWVyeUJpbmRpbmcSEQoJcGFyYW1ldGVyGAEgASgJEiQKBnZhbHVlcxgCIAMoCzIULnN5c21sLkRvY3VtZW50VmFsdWUi1QEKDURvY3VtZW50VmFsdWUSFAoKZWxlbWVudF9pZBgBIAEoCUgAEhYKDHN0cmluZ192YWx1ZRgCIAEoCUgAEhMKCWludF92YWx1ZRgDIAEoA0gAEhQKCnJlYWxfdmFsdWUYBCABKAFIABIUCgpib29sX3ZhbHVlGAUgASgISAASEgoIaW5maW5pdHkYBiABKAhIABIjCghxdWFudGl0eRgIIAEoCzIPLnN5c21sLlF1YW50aXR5SAASFAoMZWxlbWVudF90eXBlGAcgASgJQgYKBGtpbmQiIwoTRG9jdW1lbnRRdWVyeUNvbHVtbhIMCgRuYW1lGAEgASgJIjkKEURvY3VtZW50UXVlcnlDZWxsEiQKBnZhbHVlcxgBIAMoCzIULnN5c21sLkRvY3VtZW50VmFsdWUiYgoQRG9jdW1lbnRRdWVyeVJvdxIlCgdlbGVtZW50GAEgASgLMhQuc3lzbWwuRG9jdW1lbnRWYWx1ZRInCgVjZWxscxgCIAMoCzIYLnN5c21sLkRvY3VtZW50UXVlcnlDZWxsIm4KGFJ1bkRvY3VtZW50UXVlcnlSZXNwb25zZRIrCgdjb2x1bW5zGAEgAygLMhouc3lzbWwuRG9jdW1lbnRRdWVyeUNvbHVtbhIlCgRyb3dzGAIgAygLMhcuc3lzbWwuRG9jdW1lbnRRdWVyeVJvdyJAChVSZW5kZXJEb2N1bWVudFJlcXVlc3QSEgoKbW9kZWxfaGFzaBgBIAEoCRITCgtkb2N1bWVudF9pZBgCIAEoCSIqChZSZW5kZXJEb2N1bWVudFJlc3BvbnNlEhAKCG1hcmtkb3duGAEgASgJKpMBCg1GYWlsdXJlUmVhc29uEh4KGkZBSUxVUkVfUkVBU09OX1VOU1BFQ0lGSUVEEAASHQoZRkFJTFVSRV9SRUFTT05fRVZBTFVBVElPThABEh0KGUZBSUxVUkVfUkVBU09OX1dST05HX0tJTkQQAhIkCiBGQUlMVVJFX1JFQVNPTl9BTUJJR1VPVVNfU1VCSkVDVBADKp0ECgtFZGl0RmFpbHVyZRIcChhFRElUX0ZBSUxVUkVfVU5TUEVDSUZJRUQQABIeChpFRElUX0ZBSUxVUkVfTk9fT1BFUkFUSU9OUxABEh8KG0VESVRfRkFJTFVSRV9VTktOT1dOX1RBUkdFVBACEiEKHUVESVRfRkFJTFVSRV9BTUJJR1VPVVNfVEFSR0VUEAMSGwoXRURJVF9GQUlMVVJFX05PVF9WQUxVRUQQBBIeChpFRElUX0ZBSUxVUkVfSU5WQUxJRF9WQUxVRRAFEh0KGUVESVRfRkFJTFVSRV9JTlZBTElEX05BTUUQBhIaChZFRElUX0ZBSUxVUkVfTk9UX05BTUVEEAcSIgoeRURJVF9GQUlMVVJFX1JFTkFNRV9SRUZFUkVOQ0VEEAgSIgoeRURJVF9GQUlMVVJFX09WRVJMQVBQSU5HX0VESVRTEAkSHwobRURJVF9GQUlMVVJFX1JFU1VMVF9JTlZBTElEEAoSHgoaRURJVF9GQUlMVVJFX09XTkVSX1VOS05PV04QCxIkCiBFRElUX0ZBSUxVUkVfT1dORVJfTk9UX05BTUVTUEFDRRAMEh0KGUVESVRfRkFJTFVSRV9JTExFR0FMX0tJTkQQDRIiCh5FRElUX0ZBSUxVUkVfTUVNQkVSX05BTUVfVEFLRU4QDhIiCh5FRElUX0ZBSUxVUkVfREVMRVRFX1JFRkVSRU5DRUQQDyqSAQoRUHJpbWl0aXZlT3BlcmF0b3ISIgoeUFJJTUlUSVZFX09QRVJBVE9SX1VOU1BFQ0lGSUVEEAASHAoYUFJJTUlUSVZFX09QRVJBVE9SX0VRVUFMEAESHgoaUFJJTUlUSVZFX09QRVJBVE9SX0dSRUFURVIQAhIbChdQUklNSVRJVkVfT1BFUkFUT1JfTEVTUxADKm4KEUNvbXBvc2l0ZU9wZXJhdG9yEiIKHkNPTVBPU0lURV9PUEVSQVRPUl9VTlNQRUNJRklFRBAAEhoKFkNPTVBPU0lURV9PUEVSQVRPUl9BTkQQARIZChVDT01QT1NJVEVfT1BFUkFUT1JfT1IQAjKkCwoMU3lzTUxTZXJ2aWNlEkQKDUdldFNlcnZlckluZm8SGC5zeXNtbC5TZXJ2ZXJJbmZvUmVxdWVzdBoZLnN5c21sLlNlcnZlckluZm9SZXNwb25zZRI+CglQYXJzZUZpbGUSFy5zeXNtbC5QYXJzZUZpbGVSZXF1ZXN0Ghguc3lzbWwuUGFyc2VGaWxlUmVzcG9uc2USRwoMUGFyc2VTb3VyY2VzEhouc3lzbWwuUGFyc2VTb3VyY2VzUmVxdWVzdBobLnN5c21sLlBhcnNlU291cmNlc1Jlc3BvbnNlEjsKCUdldFN5bWJvbBIXLnN5c21sLkdldFN5bWJvbFJlcXVlc3QaFS5zeXNtbC5TeW1ib2xSZXNwb25zZRJHCg5HZXREaWFnbm9zdGljcxIZLnN5c21sLkRpYWdub3N0aWNzUmVxdWVzdBoaLnN5c21sLkRpYWdub3N0aWNzUmVzcG9uc2USOwoIRXZhbHVhdGUSFi5zeXNtbC5FdmFsdWF0ZVJlcXVlc3QaFy5zeXNtbC5FdmFsdWF0ZVJlc3BvbnNlEkQKC0luc3RhbnRpYXRlEhkuc3lzbWwuSW5zdGFudGlhdGVSZXF1ZXN0Ghouc3lzbWwuSW5zdGFudGlhdGVSZXNwb25zZRJKCg1FeGVjdXRlQWN0aW9uEhsuc3lzbWwuRXhlY3V0ZUFjdGlvblJlcXVlc3QaHC5zeXNtbC5FeGVjdXRlQWN0aW9uUmVzcG9uc2USRwoMRXhlY3V0ZVN0YXRlEhouc3lzbWwuRXhlY3V0ZVN0YXRlUmVxdWVzdBobLnN5c21sLkV4ZWN1dGVTdGF0ZVJlc3BvbnNlEjgKB0NvbnZlcnQSFS5zeXNtbC5Db252ZXJ0UmVxdWVzdBoWLnN5c21sLkNvbnZlcnRSZXNwb25zZRJBCgpBcHBseUVkaXRzEhguc3lzbWwuQXBwbHlFZGl0c1JlcXVlc3QaGS5zeXNtbC5BcHBseUVkaXRzUmVzcG9uc2USUwoQVmVyaWZ5Q29uc3RyYWludBIeLnN5c21sLlZlcmlmeUNvbnN0cmFpbnRSZXF1ZXN0Gh8uc3lzbWwuVmVyaWZ5Q29uc3RyYWludFJlc3BvbnNlElYKEVZlcmlmeVJlcXVpcmVtZW50Eh8uc3lzbWwuVmVyaWZ5UmVxdWlyZW1lbnRSZXF1ZXN0GiAuc3lzbWwuVmVyaWZ5UmVxdWlyZW1lbnRSZXNwb25zZRJZChJWZXJpZnlTYXRpc2ZhY3Rpb24SIC5zeXNtbC5WZXJpZnlTYXRpc2ZhY3Rpb25SZXF1ZXN0GiEuc3lzbWwuVmVyaWZ5U2F0aXNmYWN0aW9uUmVzcG9uc2USRwoMRXZhbHVhdGVDYWxjEhouc3lzbWwuRXZhbHVhdGVDYWxjUmVxdWVzdBobLnN5c21sLkV2YWx1YXRlQ2FsY1Jlc3BvbnNlEkQKC1J1bkFuYWx5c2lzEhkuc3lzbWwuUnVuQW5hbHlzaXNSZXF1ZXN0Ghouc3lzbWwuUnVuQW5hbHlzaXNSZXNwb25zZRI7CghSdW5Td2VlcBIWLnN5c21sLlJ1blN3ZWVwUmVxdWVzdBoXLnN5c21sLlJ1blN3ZWVwUmVzcG9uc2USMgoFUXVlcnkSEy5zeXNtbC5RdWVyeVJlcXVlc3QaFC5zeXNtbC5RdWVyeVJlc3BvbnNlElMKEFJ1bkRvY3VtZW50UXVlcnkSHi5zeXNtbC5SdW5Eb2N1bWVudFF1ZXJ5UmVxdWVzdBofLnN5c21sLlJ1bkRvY3VtZW50UXVlcnlSZXNwb25zZRJNCg5SZW5kZXJEb2N1bWVudBIcLnN5c21sLlJlbmRlckRvY3VtZW50UmVxdWVzdBodLnN5c21sLlJlbmRlckRvY3VtZW50UmVzcG9uc2VCKlooZ2l0aHViLmNvbS9PcGVuLU1CRUUvT3BlblN5c01ML2FwaS9wcm90b2IGcHJvdG8z"); + fileDesc("CgtzeXNtbC5wcm90bxIFc3lzbWwi4gEKB1ZlcmRpY3QSDAoEa2luZBgBIAEoCRISCgplbGVtZW50X2lkGAIgASgJEg8KB2VsZW1lbnQYAyABKAkSDQoFaG9sZHMYBCABKAgSEQoJY29uZGl0aW9uGAUgASgJEhMKC2luc3RhbmNlX2lkGAYgASgDEhgKEGluc3RhbmNlX3R5cGVfaWQYByABKAkSDQoFZXJyb3IYCCABKAkSLAoOZmFpbHVyZV9yZWFzb24YCSABKA4yFC5zeXNtbC5GYWlsdXJlUmVhc29uEhYKDnJlcXVpcmVtZW50X2lkGAogASgJIlsKF1ZlcmlmeUNvbnN0cmFpbnRSZXF1ZXN0EhIKCm1vZGVsX2hhc2gYASABKAkSEQoJc3ltYm9sX2lkGAIgASgJEhkKEXN1YmplY3Rfc3ltYm9sX2lkGAMgASgJIpYBChhWZXJpZnlDb25zdHJhaW50UmVzcG9uc2USHwoHdmVyZGljdBgBIAEoCzIOLnN5c21sLlZlcmRpY3QSIgoJaW5zdGFuY2VzGAIgAygLMg8uc3lzbWwuSW5zdGFuY2USDQoFZXJyb3IYAyABKAkSJgoLZGlhZ25vc3RpY3MYBCADKAsyES5zeXNtbC5EaWFnbm9zdGljIlwKGFZlcmlmeVJlcXVpcmVtZW50UmVxdWVzdBISCgptb2RlbF9oYXNoGAEgASgJEhEKCXN5bWJvbF9pZBgCIAEoCRIZChFzdWJqZWN0X3N5bWJvbF9pZBgDIAEoCSJtChNWZXJpZmljYXRpb25WZXJkaWN0Eg8KB2Nhc2VfaWQYASABKAkSDAoEa2luZBgCIAEoCRIOCgZkZXRhaWwYAyABKAkSDwoHc3ViY2FzZRgEIAEoCBIWCg5yZXF1aXJlbWVudF9pZBgFIAEoCSLSAQoZVmVyaWZ5UmVxdWlyZW1lbnRSZXNwb25zZRIfCgd2ZXJkaWN0GAEgASgLMg4uc3lzbWwuVmVyZGljdBIiCglpbnN0YW5jZXMYAiADKAsyDy5zeXNtbC5JbnN0YW5jZRINCgVlcnJvchgDIAEoCRImCgtkaWFnbm9zdGljcxgEIAMoCzIRLnN5c21sLkRpYWdub3N0aWMSOQoVdmVyaWZpY2F0aW9uX3ZlcmRpY3RzGAUgAygLMhouc3lzbWwuVmVyaWZpY2F0aW9uVmVyZGljdCJCChlWZXJpZnlTYXRpc2ZhY3Rpb25SZXF1ZXN0EhIKCm1vZGVsX2hhc2gYASABKAkSEQoJc3ltYm9sX2lkGAIgASgJIoICChpWZXJpZnlTYXRpc2ZhY3Rpb25SZXNwb25zZRIgCgh2ZXJkaWN0cxgBIAMoCzIOLnN5c21sLlZlcmRpY3QSIgoJaW5zdGFuY2VzGAIgAygLMg8uc3lzbWwuSW5zdGFuY2USDQoFZXJyb3IYAyABKAkSJgoLZGlhZ25vc3RpY3MYBCADKAsyES5zeXNtbC5EaWFnbm9zdGljEiwKDmZhaWx1cmVfcmVhc29uGAUgASgOMhQuc3lzbWwuRmFpbHVyZVJlYXNvbhI5ChV2ZXJpZmljYXRpb25fdmVyZGljdHMYBiADKAsyGi5zeXNtbC5WZXJpZmljYXRpb25WZXJkaWN0Il0KE0V2YWx1YXRlQ2FsY1JlcXVlc3QSEgoKbW9kZWxfaGFzaBgBIAEoCRIRCglzeW1ib2xfaWQYAiABKAkSHwoJYXJndW1lbnRzGAMgAygLMgwuc3lzbWwuVmFsdWUivQEKFEV2YWx1YXRlQ2FsY1Jlc3BvbnNlEhwKBnJlc3VsdBgBIAEoCzIMLnN5c21sLlZhbHVlEiIKB291dHB1dHMYAiADKAsyES5zeXNtbC5DYWxjT3V0cHV0Eg0KBWVycm9yGAMgASgJEiYKC2RpYWdub3N0aWNzGAQgAygLMhEuc3lzbWwuRGlhZ25vc3RpYxIsCg5mYWlsdXJlX3JlYXNvbhgFIAEoDjIULnN5c21sLkZhaWx1cmVSZWFzb24iNwoKQ2FsY091dHB1dBIMCgRuYW1lGAEgASgJEhsKBXZhbHVlGAIgASgLMgwuc3lzbWwuVmFsdWUilgIKElJ1bkFuYWx5c2lzUmVxdWVzdBISCgptb2RlbF9oYXNoGAEgASgJEhEKCXN5bWJvbF9pZBgCIAEoCRIZChFzdWJqZWN0X3N5bWJvbF9pZBgDIAEoCRIfCglhcmd1bWVudHMYBCADKAsyDC5zeXNtbC5WYWx1ZRJGCg9uYW1lZF9hcmd1bWVudHMYBSADKAsyLS5zeXNtbC5SdW5BbmFseXNpc1JlcXVlc3QuTmFtZWRBcmd1bWVudHNFbnRyeRIQCghzY2hlZHVsZRgGIAEoCRpDChNOYW1lZEFyZ3VtZW50c0VudHJ5EgsKA2tleRgBIAEoCRIbCgV2YWx1ZRgCIAEoCzIMLnN5c21sLlZhbHVlOgI4ASKfAgoTUnVuQW5hbHlzaXNSZXNwb25zZRIiCgdvdXRwdXRzGAEgAygLMhEuc3lzbWwuQ2FsY091dHB1dBIgCgh2ZXJkaWN0cxgCIAMoCzIOLnN5c21sLlZlcmRpY3QSIgoJaW5zdGFuY2VzGAMgAygLMg8uc3lzbWwuSW5zdGFuY2USDQoFZXJyb3IYBCABKAkSJgoLZGlhZ25vc3RpY3MYBSADKAsyES5zeXNtbC5EaWFnbm9zdGljEiwKDmZhaWx1cmVfcmVhc29uGAYgASgOMhQuc3lzbWwuRmFpbHVyZVJlYXNvbhI5ChV2ZXJpZmljYXRpb25fdmVyZGljdHMYByADKAsyGi5zeXNtbC5WZXJpZmljYXRpb25WZXJkaWN0IowBChBQYXJzZUZpbGVSZXF1ZXN0EhMKCWZpbGVfcGF0aBgBIAEoCUgAEhEKB2NvbnRlbnQYAiABKAlIABIYCgxjb250ZW50X2hhc2gYAyABKAlCAhgBEhAKCGxhbmd1YWdlGAQgASgJEhoKEnN0cmljdF9jb25mb3JtYW5jZRgFIAEoCEIICgZzb3VyY2UiYgoOU291cmNlRG9jdW1lbnQSEwoJZmlsZV9wYXRoGAEgASgJSAASEQoHY29udGVudBgCIAEoCUgAEhAKCGxhbmd1YWdlGAMgASgJEgwKBG5hbWUYBCABKAlCCAoGc291cmNlIlsKE1BhcnNlU291cmNlc1JlcXVlc3QSKAoJZG9jdW1lbnRzGAEgAygLMhUuc3lzbWwuU291cmNlRG9jdW1lbnQSGgoSc3RyaWN0X2NvbmZvcm1hbmNlGAIgASgIIoMBChRQYXJzZVNvdXJjZXNSZXNwb25zZRISCgptb2RlbF9oYXNoGAEgASgJEiAKBXJvb3RzGAIgAygLMhEuc3lzbWwuU3ltYm9sSW5mbxImCgtkaWFnbm9zdGljcxgDIAMoCzIRLnN5c21sLkRpYWdub3N0aWMSDQoFZXJyb3IYBCABKAkifwoRUGFyc2VGaWxlUmVzcG9uc2USEgoKbW9kZWxfaGFzaBgBIAEoCRIfCgRyb290GAIgASgLMhEuc3lzbWwuU3ltYm9sSW5mbxImCgtkaWFnbm9zdGljcxgDIAMoCzIRLnN5c21sLkRpYWdub3N0aWMSDQoFZXJyb3IYBCABKAkiOQoQR2V0U3ltYm9sUmVxdWVzdBISCgptb2RlbF9oYXNoGAEgASgJEhEKCXN5bWJvbF9pZBgCIAEoCSJCCg5TeW1ib2xSZXNwb25zZRIhCgZzeW1ib2wYASABKAsyES5zeXNtbC5TeW1ib2xJbmZvEg0KBWVycm9yGAIgASgJIigKEkRpYWdub3N0aWNzUmVxdWVzdBISCgptb2RlbF9oYXNoGAEgASgJIkwKE0RpYWdub3N0aWNzUmVzcG9uc2USJgoLZGlhZ25vc3RpY3MYASADKAsyES5zeXNtbC5EaWFnbm9zdGljEg0KBWVycm9yGAIgASgJIm8KD0V2YWx1YXRlUmVxdWVzdBISCgptb2RlbF9oYXNoGAEgASgJEhIKCmV4cHJlc3Npb24YAiABKAkSGQoRY29udGV4dF9zeW1ib2xfaWQYAyABKAkSGQoRc3ViamVjdF9zeW1ib2xfaWQYBCABKAkiZwoQRXZhbHVhdGVSZXNwb25zZRIcCgZyZXN1bHQYASABKAsyDC5zeXNtbC5WYWx1ZRINCgVlcnJvchgCIAEoCRImCgtkaWFnbm9zdGljcxgDIAMoCzIRLnN5c21sLkRpYWdub3N0aWMiwgEKCEluc3RhbmNlEgoKAmlkGAEgASgDEhYKDnR5cGVfc3ltYm9sX2lkGAIgASgJEjoKDmZlYXR1cmVfdmFsdWVzGAQgAygLMiIuc3lzbWwuSW5zdGFuY2UuRmVhdHVyZVZhbHVlc0VudHJ5GkkKEkZlYXR1cmVWYWx1ZXNFbnRyeRILCgNrZXkYASABKAkSIgoFdmFsdWUYAiABKAsyEy5zeXNtbC5GZWF0dXJlVmFsdWU6AjgBSgQIAxAEUgVzbG90cyKEAQoMRmVhdHVyZVZhbHVlEhQKDGZlYXR1cmVfbmFtZRgBIAEoCRIbCgV2YWx1ZRgCIAEoCzIMLnN5c21sLlZhbHVlEhwKBnZhbHVlcxgDIAMoCzIMLnN5c21sLlZhbHVlEhQKDG1hdGVyaWFsaXplZBgEIAEoCBINCgVlcnJvchgFIAEoCSI7ChJJbnN0YW50aWF0ZVJlcXVlc3QSEgoKbW9kZWxfaGFzaBgBIAEoCRIRCglzeW1ib2xfaWQYAiABKAkikwEKE0luc3RhbnRpYXRlUmVzcG9uc2USIQoIaW5zdGFuY2UYASABKAsyDy5zeXNtbC5JbnN0YW5jZRINCgVlcnJvchgCIAEoCRImCgtkaWFnbm9zdGljcxgDIAMoCzIRLnN5c21sLkRpYWdub3N0aWMSIgoJaW5zdGFuY2VzGAQgAygLMg8uc3lzbWwuSW5zdGFuY2UizAEKFEV4ZWN1dGVBY3Rpb25SZXF1ZXN0EhIKCm1vZGVsX2hhc2gYASABKAkSGAoQYWN0aW9uX3N5bWJvbF9pZBgCIAEoCRI3CgZpbnB1dHMYAyADKAsyJy5zeXNtbC5FeGVjdXRlQWN0aW9uUmVxdWVzdC5JbnB1dHNFbnRyeRIQCghzY2hlZHVsZRgEIAEoCRo7CgtJbnB1dHNFbnRyeRILCgNrZXkYASABKAkSGwoFdmFsdWUYAiABKAsyDC5zeXNtbC5WYWx1ZToCOAEiyAEKFUV4ZWN1dGVBY3Rpb25SZXNwb25zZRI6CgdvdXRwdXRzGAEgAygLMikuc3lzbWwuRXhlY3V0ZUFjdGlvblJlc3BvbnNlLk91dHB1dHNFbnRyeRINCgVlcnJvchgCIAEoCRImCgtkaWFnbm9zdGljcxgDIAMoCzIRLnN5c21sLkRpYWdub3N0aWMaPAoMT3V0cHV0c0VudHJ5EgsKA2tleRgBIAEoCRIbCgV2YWx1ZRgCIAEoCzIMLnN5c21sLlZhbHVlOgI4ASJsChNFeGVjdXRlU3RhdGVSZXF1ZXN0EhIKCm1vZGVsX2hhc2gYASABKAkSHwoXc3RhdGVfbWFjaGluZV9zeW1ib2xfaWQYAiABKAkSDgoGZXZlbnRzGAMgAygJEhAKCHNjaGVkdWxlGAQgASgJIu4BChRFeGVjdXRlU3RhdGVSZXNwb25zZRIWCg5zdGF0ZXNfdmlzaXRlZBgBIAMoCRJECg1maW5hbF9jb250ZXh0GAIgAygLMi0uc3lzbWwuRXhlY3V0ZVN0YXRlUmVzcG9uc2UuRmluYWxDb250ZXh0RW50cnkSDQoFZXJyb3IYAyABKAkSJgoLZGlhZ25vc3RpY3MYBCADKAsyES5zeXNtbC5EaWFnbm9zdGljGkEKEUZpbmFsQ29udGV4dEVudHJ5EgsKA2tleRgBIAEoCRIbCgV2YWx1ZRgCIAEoCzIMLnN5c21sLlZhbHVlOgI4ASKgAQoOQ29udmVydFJlcXVlc3QSEwoJZmlsZV9wYXRoGAEgASgJSAASEQoHY29udGVudBgCIAEoCUgAEhQKCm1vZGVsX2hhc2gYBiABKAlIABITCgtmcm9tX2Zvcm1hdBgDIAEoCRIRCgl0b19mb3JtYXQYBCABKAkSHgoWdG9sZXJhdGVfc3ludGF4X2Vycm9ycxgFIAEoCEIICgZzb3VyY2UitAEKD0NvbnZlcnRSZXNwb25zZRIPCgdjb250ZW50GAEgASgJEhMKC2Zyb21fZm9ybWF0GAIgASgJEhEKCXRvX2Zvcm1hdBgDIAEoCRINCgVlcnJvchgEIAEoCRImCgtkaWFnbm9zdGljcxgFIAMoCzIRLnN5c21sLkRpYWdub3N0aWMSFAoMZXhwZXJpbWVudGFsGAYgASgIEhsKE2V4cGVyaW1lbnRhbF9ub3RpY2UYByABKAkiUQoRQXBwbHlFZGl0c1JlcXVlc3QSEgoKbW9kZWxfaGFzaBgBIAEoCRIoCgpvcGVyYXRpb25zGAIgAygLMhQuc3lzbWwuRWRpdE9wZXJhdGlvbiK8AQoNRWRpdE9wZXJhdGlvbhIoCglzZXRfdmFsdWUYASABKAsyEy5zeXNtbC5TZXRWYWx1ZUVkaXRIABIjCgZyZW5hbWUYAiABKAsyES5zeXNtbC5SZW5hbWVFZGl0SAASKgoKYWRkX21lbWJlchgDIAEoCzIULnN5c21sLkFkZE1lbWJlckVkaXRIABIjCgZkZWxldGUYBCABKAsyES5zeXNtbC5EZWxldGVFZGl0SABCCwoJb3BlcmF0aW9uIoIBCg1BZGRNZW1iZXJFZGl0Eg0KBW93bmVyGAEgASgJEgwKBGtpbmQYAiABKAkSDAoEbmFtZRgDIAEoCRIMCgR0eXBlGAQgASgJEhQKDG11bHRpcGxpY2l0eRgFIAEoCRINCgV2YWx1ZRgGIAEoCRITCgtzcGVjaWFsaXplcxgHIAMoCSItCgpEZWxldGVFZGl0Eg4KBnRhcmdldBgBIAEoCRIPCgdjYXNjYWRlGAIgASgIIi0KDFNldFZhbHVlRWRpdBIOCgZ0YXJnZXQYASABKAkSDQoFdmFsdWUYAiABKAkiLgoKUmVuYW1lRWRpdBIOCgZ0YXJnZXQYASABKAkSEAoIbmV3X25hbWUYAiABKAkiwgEKEkFwcGx5RWRpdHNSZXNwb25zZRIPCgdjb250ZW50GAEgASgJEiMKB2FwcGxpZWQYAiADKAsyEi5zeXNtbC5BcHBsaWVkRWRpdBINCgVlcnJvchgDIAEoCRIjCgdmYWlsdXJlGAQgASgOMhIuc3lzbWwuRWRpdEZhaWx1cmUSJgoLZGlhZ25vc3RpY3MYBSADKAsyES5zeXNtbC5EaWFnbm9zdGljEhoKEnJlZmVycmluZ19lbGVtZW50cxgGIAMoCSJ6CgtBcHBsaWVkRWRpdBIXCg9vcGVyYXRpb25faW5kZXgYASABKAUSDgoGdGFyZ2V0GAIgASgJEg4KBm9mZnNldBgDIAEoBRIOCgZsZW5ndGgYBCABKAUSEAoIb2xkX3RleHQYBSABKAkSEAoIbmV3X3RleHQYBiABKAki/QIKClN5bWJvbEluZm8SCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRIMCgRraW5kGAMgASgJEjEKCG1ldGFkYXRhGAQgAygLMh8uc3lzbWwuU3ltYm9sSW5mby5NZXRhZGF0YUVudHJ5EhEKCWNoaWxkX2lkcxgFIAMoCRIoCgphdHRyaWJ1dGVzGAYgAygLMhQuc3lzbWwuQXR0cmlidXRlSW5mbxIiCgl0eXBlX2luZm8YByABKAsyDy5zeXNtbC5UeXBlSW5mbxItCgxtdWx0aXBsaWNpdHkYCCABKAsyFy5zeXNtbC5NdWx0aXBsaWNpdHlJbmZvEi4KD3NwZWNpYWxpemF0aW9ucxgJIAMoCzIVLnN5c21sLlNwZWNpYWxpemF0aW9uEiMKG3dpdGhoZWxkX2xpYnJhcnlfYXR0cmlidXRlcxgKIAEoBRovCg1NZXRhZGF0YUVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiWAoOU3BlY2lhbGl6YXRpb24SDAoEa2luZBgBIAEoCRIQCghkZWNsYXJlZBgCIAEoCRIRCgl0YXJnZXRfaWQYAyABKAkSEwoLdGFyZ2V0X2tpbmQYBCABKAkilQEKCFR5cGVJbmZvEhAKCGRlY2xhcmVkGAEgASgJEhMKC3Jlc29sdmVkX2lkGAIgASgJEhUKDXJlc29sdmVkX2tpbmQYAyABKAkSEQoJcHJpbWl0aXZlGAQgASgJEhgKEHByaW1pdGl2ZV9zb3VyY2UYBSABKAkSEAoIcXVhbnRpdHkYBiABKAgSDAoEdW5pdBgHIAEoCSIwChBNdWx0aXBsaWNpdHlJbmZvEg0KBWxvd2VyGAEgASgJEg0KBXVwcGVyGAIgASgJIlYKDUF0dHJpYnV0ZUluZm8SDAoEbmFtZRgBIAEoCRIMCgR0eXBlGAIgASgJEhsKBXZhbHVlGAMgASgLMgwuc3lzbWwuVmFsdWUSDAoEdW5pdBgEIAEoCSLtBAoFVmFsdWUSEwoJaW50X3ZhbHVlGAEgASgDSAASFAoKcmVhbF92YWx1ZRgCIAEoAUgAEhQKCmJvb2xfdmFsdWUYAyABKAhIABIWCgxzdHJpbmdfdmFsdWUYBCABKAlIABIVCgtpbnN0YW5jZV9pZBgFIAEoA0gAEigKCHNlcXVlbmNlGAYgASgLMhQuc3lzbWwuVmFsdWVTZXF1ZW5jZUgAEg4KBG51bGwYByABKAlIABIjCghxdWFudGl0eRgIIAEoCzIPLnN5c21sLlF1YW50aXR5SAASKgoMZW51bV9saXRlcmFsGAkgASgLMhIuc3lzbWwuRW51bUxpdGVyYWxIABIPCgV1bnNldBgKIAEoCEgAEiEKB2NvbXBsZXgYCyABKAsyDi5zeXNtbC5Db21wbGV4SAASHQoFYXJyYXkYDCABKAsyDC5zeXNtbC5BcnJheUgAEh8KBnZlY3RvchgNIAEoCzINLnN5c21sLlZlY3RvckgAEjAKD3ZlY3Rvcl9xdWFudGl0eRgOIAEoCzIVLnN5c21sLlZlY3RvclF1YW50aXR5SAASMAoPbWVhc3VyZW1lbnRfcmVmGA8gASgLMhUuc3lzbWwuTWVhc3VyZW1lbnRSZWZIABISCghpbmZpbml0eRgQIAEoCEgAEiMKCGZ1bmN0aW9uGBEgASgLMg8uc3lzbWwuRnVuY3Rpb25IABIeCgNzZXQYEiABKAsyDy5zeXNtbC5WYWx1ZVNldEgAEjAKD3RlbnNvcl9xdWFudGl0eRgTIAEoCzIVLnN5c21sLlRlbnNvclF1YW50aXR5SABCBgoEa2luZCIsCghGdW5jdGlvbhIPCgdjYWxjX2lkGAEgASgJEg8KB3NlbGZfaWQYAiABKAMiKgoIVmFsdWVTZXQSHgoIZWxlbWVudHMYASADKAsyDC5zeXNtbC5WYWx1ZSJJCg5UZW5zb3JRdWFudGl0eRISCgpkaW1lbnNpb25zGAEgAygDEiMKCmNvbXBvbmVudHMYAiADKAsyDy5zeXNtbC5RdWFudGl0eSI7CgVBcnJheRISCgpkaW1lbnNpb25zGAEgAygDEh4KCGVsZW1lbnRzGAIgAygLMgwuc3lzbWwuVmFsdWUiKgoGVmVjdG9yEiAKCmNvbXBvbmVudHMYASADKAsyDC5zeXNtbC5WYWx1ZSI1Cg5WZWN0b3JRdWFudGl0eRIjCgpjb21wb25lbnRzGAEgAygLMg8uc3lzbWwuUXVhbnRpdHkiKgoHQ29tcGxleBIMCgRyZWFsGAEgASgBEhEKCWltYWdpbmFyeRgCIAEoASJHCgtFbnVtTGl0ZXJhbBISCgpsaXRlcmFsX2lkGAEgASgJEhYKDmVudW1lcmF0aW9uX2lkGAIgASgJEgwKBG5hbWUYAyABKAkiLwoNVmFsdWVTZXF1ZW5jZRIeCghlbGVtZW50cxgBIAMoCzIMLnN5c21sLlZhbHVlInwKCFF1YW50aXR5EhcKDWludF9tYWduaXR1ZGUYASABKANIABIYCg5yZWFsX21hZ25pdHVkZRgCIAEoAUgAEgwKBHVuaXQYAyABKAkSIgoJdW5pdF90ZXJtGAQgASgLMg8uc3lzbWwuVW5pdFRlcm1CCwoJbWFnbml0dWRlIlMKDk1lYXN1cmVtZW50UmVmEgwKBHVuaXQYASABKAkSIgoJdW5pdF90ZXJtGAIgASgLMg8uc3lzbWwuVW5pdFRlcm0SDwoHdW5pdF9pZBgDIAEoCSJUCghVbml0VGVybRIRCglzY2FsZV9udW0YASABKAESEQoJc2NhbGVfZGVuGAIgASgBEiIKB2ZhY3RvcnMYAyADKAsyES5zeXNtbC5Vbml0RmFjdG9yIi8KClVuaXRGYWN0b3ISDwoHdW5pdF9pZBgBIAEoCRIQCghleHBvbmVudBgCIAEoASJYCgpEaWFnbm9zdGljEhAKCHNldmVyaXR5GAEgASgJEg8KB21lc3NhZ2UYAiABKAkSGQoEc3BhbhgDIAEoCzILLnN5c21sLlNwYW4SDAoEY29kZRgEIAEoCSJeCgRTcGFuEgwKBGZpbGUYASABKAkSEgoKc3RhcnRfbGluZRgCIAEoBRIRCglzdGFydF9jb2wYAyABKAUSEAoIZW5kX2xpbmUYBCABKAUSDwoHZW5kX2NvbBgFIAEoBSITChFTZXJ2ZXJJbmZvUmVxdWVzdCI7ChJTZXJ2ZXJJbmZvUmVzcG9uc2USDwoHdmVyc2lvbhgBIAEoCRIUCgxjYXBhYmlsaXRpZXMYAiADKAkiUwoMUXVlcnlSZXF1ZXN0EhIKCm1vZGVsX2hhc2gYASABKAkSGwoFcXVlcnkYAiABKAsyDC5zeXNtbC5RdWVyeRISCgpvc2xjX3F1ZXJ5GAMgASgJIjwKDVF1ZXJ5UmVzcG9uc2USKwoIZWxlbWVudHMYASADKAsyGS5zeXNtbC5RdWVyeVJlc3VsdEVsZW1lbnQiSAoFUXVlcnkSDQoFc2NvcGUYASADKAkSDgoGc2VsZWN0GAIgAygJEiAKBXdoZXJlGAMgASgLMhEuc3lzbWwuQ29uc3RyYWludCJ8CgpDb25zdHJhaW50Ei8KCXByaW1pdGl2ZRgBIAEoCzIaLnN5c21sLlByaW1pdGl2ZUNvbnN0cmFpbnRIABIvCgljb21wb3NpdGUYAiABKAsyGi5zeXNtbC5Db21wb3NpdGVDb25zdHJhaW50SABCDAoKY29uc3RyYWludCJzChNQcmltaXRpdmVDb25zdHJhaW50Eg8KB2ludmVyc2UYASABKAgSEAoIcHJvcGVydHkYAiABKAkSKgoIb3BlcmF0b3IYAyABKA4yGC5zeXNtbC5QcmltaXRpdmVPcGVyYXRvchINCgV2YWx1ZRgEIAMoCSJoChNDb21wb3NpdGVDb25zdHJhaW50EioKCG9wZXJhdG9yGAEgASgOMhguc3lzbWwuQ29tcG9zaXRlT3BlcmF0b3ISJQoKY29uc3RyYWludBgCIAMoCzIRLnN5c21sLkNvbnN0cmFpbnQioAEKElF1ZXJ5UmVzdWx0RWxlbWVudBIKCgJpZBgBIAEoCRIMCgR0eXBlGAIgASgJEj0KCnByb3BlcnRpZXMYAyADKAsyKS5zeXNtbC5RdWVyeVJlc3VsdEVsZW1lbnQuUHJvcGVydGllc0VudHJ5GjEKD1Byb3BlcnRpZXNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBInMKClN3ZWVwUmFuZ2USEQoJcGFyYW1ldGVyGAEgASgJEhsKBXN0YXJ0GAIgASgLMgwuc3lzbWwuVmFsdWUSGQoDZW5kGAMgASgLMgwuc3lzbWwuVmFsdWUSGgoEc3RlcBgEIAEoCzIMLnN5c21sLlZhbHVlIsACCg9SdW5Td2VlcFJlcXVlc3QSEgoKbW9kZWxfaGFzaBgBIAEoCRIRCglzeW1ib2xfaWQYAiABKAkSGQoRc3ViamVjdF9zeW1ib2xfaWQYAyABKAkSHwoJYXJndW1lbnRzGAQgAygLMgwuc3lzbWwuVmFsdWUSQwoPbmFtZWRfYXJndW1lbnRzGAUgAygLMiouc3lzbWwuUnVuU3dlZXBSZXF1ZXN0Lk5hbWVkQXJndW1lbnRzRW50cnkSIQoGcmFuZ2VzGAYgAygLMhEuc3lzbWwuU3dlZXBSYW5nZRIPCgdzYW1wbGVzGAcgASgDEgwKBHNlZWQYCCABKAQaQwoTTmFtZWRBcmd1bWVudHNFbnRyeRILCgNrZXkYASABKAkSGwoFdmFsdWUYAiABKAsyDC5zeXNtbC5WYWx1ZToCOAEiyAEKCFN3ZWVwUm93EiEKBmlucHV0cxgBIAMoCzIRLnN5c21sLkNhbGNPdXRwdXQSIgoHb3V0cHV0cxgCIAMoCzIRLnN5c21sLkNhbGNPdXRwdXQSIAoIdmVyZGljdHMYAyADKAsyDi5zeXNtbC5WZXJkaWN0EhYKDmVsYXBzZWRfbWljcm9zGAQgASgDEg0KBWVycm9yGAUgASgJEiwKDmZhaWx1cmVfcmVhc29uGAYgASgOMhQuc3lzbWwuRmFpbHVyZVJlYXNvbiLtAQoQUnVuU3dlZXBSZXNwb25zZRIdCgRyb3dzGAEgAygLMg8uc3lzbWwuU3dlZXBSb3cSEgoKcGFyYW1ldGVycxgCIAMoCRIPCgdzYW1wbGVkGAMgASgIEgwKBHNlZWQYBCABKAQSDQoFZXJyb3IYBSABKAkSJgoLZGlhZ25vc3RpY3MYBiADKAsyES5zeXNtbC5EaWFnbm9zdGljEiwKDmZhaWx1cmVfcmVhc29uGAcgASgOMhQuc3lzbWwuRmFpbHVyZVJlYXNvbhIiCglpbnN0YW5jZXMYCCADKAsyDy5zeXNtbC5JbnN0YW5jZSJuChdSdW5Eb2N1bWVudFF1ZXJ5UmVxdWVzdBISCgptb2RlbF9oYXNoGAEgASgJEhAKCHF1ZXJ5X2lkGAIgASgJEi0KCGJpbmRpbmdzGAMgAygLMhsuc3lzbWwuRG9jdW1lbnRRdWVyeUJpbmRpbmciTwoURG9jdW1lbnRRdWVyeUJpbmRpbmcSEQoJcGFyYW1ldGVyGAEgASgJEiQKBnZhbHVlcxgCIAMoCzIULnN5c21sLkRvY3VtZW50VmFsdWUi1QEKDURvY3VtZW50VmFsdWUSFAoKZWxlbWVudF9pZBgBIAEoCUgAEhYKDHN0cmluZ192YWx1ZRgCIAEoCUgAEhMKCWludF92YWx1ZRgDIAEoA0gAEhQKCnJlYWxfdmFsdWUYBCABKAFIABIUCgpib29sX3ZhbHVlGAUgASgISAASEgoIaW5maW5pdHkYBiABKAhIABIjCghxdWFudGl0eRgIIAEoCzIPLnN5c21sLlF1YW50aXR5SAASFAoMZWxlbWVudF90eXBlGAcgASgJQgYKBGtpbmQiIwoTRG9jdW1lbnRRdWVyeUNvbHVtbhIMCgRuYW1lGAEgASgJIjkKEURvY3VtZW50UXVlcnlDZWxsEiQKBnZhbHVlcxgBIAMoCzIULnN5c21sLkRvY3VtZW50VmFsdWUiYgoQRG9jdW1lbnRRdWVyeVJvdxIlCgdlbGVtZW50GAEgASgLMhQuc3lzbWwuRG9jdW1lbnRWYWx1ZRInCgVjZWxscxgCIAMoCzIYLnN5c21sLkRvY3VtZW50UXVlcnlDZWxsIm4KGFJ1bkRvY3VtZW50UXVlcnlSZXNwb25zZRIrCgdjb2x1bW5zGAEgAygLMhouc3lzbWwuRG9jdW1lbnRRdWVyeUNvbHVtbhIlCgRyb3dzGAIgAygLMhcuc3lzbWwuRG9jdW1lbnRRdWVyeVJvdyJAChVSZW5kZXJEb2N1bWVudFJlcXVlc3QSEgoKbW9kZWxfaGFzaBgBIAEoCRITCgtkb2N1bWVudF9pZBgCIAEoCSIqChZSZW5kZXJEb2N1bWVudFJlc3BvbnNlEhAKCG1hcmtkb3duGAEgASgJKpMBCg1GYWlsdXJlUmVhc29uEh4KGkZBSUxVUkVfUkVBU09OX1VOU1BFQ0lGSUVEEAASHQoZRkFJTFVSRV9SRUFTT05fRVZBTFVBVElPThABEh0KGUZBSUxVUkVfUkVBU09OX1dST05HX0tJTkQQAhIkCiBGQUlMVVJFX1JFQVNPTl9BTUJJR1VPVVNfU1VCSkVDVBADKp0ECgtFZGl0RmFpbHVyZRIcChhFRElUX0ZBSUxVUkVfVU5TUEVDSUZJRUQQABIeChpFRElUX0ZBSUxVUkVfTk9fT1BFUkFUSU9OUxABEh8KG0VESVRfRkFJTFVSRV9VTktOT1dOX1RBUkdFVBACEiEKHUVESVRfRkFJTFVSRV9BTUJJR1VPVVNfVEFSR0VUEAMSGwoXRURJVF9GQUlMVVJFX05PVF9WQUxVRUQQBBIeChpFRElUX0ZBSUxVUkVfSU5WQUxJRF9WQUxVRRAFEh0KGUVESVRfRkFJTFVSRV9JTlZBTElEX05BTUUQBhIaChZFRElUX0ZBSUxVUkVfTk9UX05BTUVEEAcSIgoeRURJVF9GQUlMVVJFX1JFTkFNRV9SRUZFUkVOQ0VEEAgSIgoeRURJVF9GQUlMVVJFX09WRVJMQVBQSU5HX0VESVRTEAkSHwobRURJVF9GQUlMVVJFX1JFU1VMVF9JTlZBTElEEAoSHgoaRURJVF9GQUlMVVJFX09XTkVSX1VOS05PV04QCxIkCiBFRElUX0ZBSUxVUkVfT1dORVJfTk9UX05BTUVTUEFDRRAMEh0KGUVESVRfRkFJTFVSRV9JTExFR0FMX0tJTkQQDRIiCh5FRElUX0ZBSUxVUkVfTUVNQkVSX05BTUVfVEFLRU4QDhIiCh5FRElUX0ZBSUxVUkVfREVMRVRFX1JFRkVSRU5DRUQQDyqSAQoRUHJpbWl0aXZlT3BlcmF0b3ISIgoeUFJJTUlUSVZFX09QRVJBVE9SX1VOU1BFQ0lGSUVEEAASHAoYUFJJTUlUSVZFX09QRVJBVE9SX0VRVUFMEAESHgoaUFJJTUlUSVZFX09QRVJBVE9SX0dSRUFURVIQAhIbChdQUklNSVRJVkVfT1BFUkFUT1JfTEVTUxADKm4KEUNvbXBvc2l0ZU9wZXJhdG9yEiIKHkNPTVBPU0lURV9PUEVSQVRPUl9VTlNQRUNJRklFRBAAEhoKFkNPTVBPU0lURV9PUEVSQVRPUl9BTkQQARIZChVDT01QT1NJVEVfT1BFUkFUT1JfT1IQAjKkCwoMU3lzTUxTZXJ2aWNlEkQKDUdldFNlcnZlckluZm8SGC5zeXNtbC5TZXJ2ZXJJbmZvUmVxdWVzdBoZLnN5c21sLlNlcnZlckluZm9SZXNwb25zZRI+CglQYXJzZUZpbGUSFy5zeXNtbC5QYXJzZUZpbGVSZXF1ZXN0Ghguc3lzbWwuUGFyc2VGaWxlUmVzcG9uc2USRwoMUGFyc2VTb3VyY2VzEhouc3lzbWwuUGFyc2VTb3VyY2VzUmVxdWVzdBobLnN5c21sLlBhcnNlU291cmNlc1Jlc3BvbnNlEjsKCUdldFN5bWJvbBIXLnN5c21sLkdldFN5bWJvbFJlcXVlc3QaFS5zeXNtbC5TeW1ib2xSZXNwb25zZRJHCg5HZXREaWFnbm9zdGljcxIZLnN5c21sLkRpYWdub3N0aWNzUmVxdWVzdBoaLnN5c21sLkRpYWdub3N0aWNzUmVzcG9uc2USOwoIRXZhbHVhdGUSFi5zeXNtbC5FdmFsdWF0ZVJlcXVlc3QaFy5zeXNtbC5FdmFsdWF0ZVJlc3BvbnNlEkQKC0luc3RhbnRpYXRlEhkuc3lzbWwuSW5zdGFudGlhdGVSZXF1ZXN0Ghouc3lzbWwuSW5zdGFudGlhdGVSZXNwb25zZRJKCg1FeGVjdXRlQWN0aW9uEhsuc3lzbWwuRXhlY3V0ZUFjdGlvblJlcXVlc3QaHC5zeXNtbC5FeGVjdXRlQWN0aW9uUmVzcG9uc2USRwoMRXhlY3V0ZVN0YXRlEhouc3lzbWwuRXhlY3V0ZVN0YXRlUmVxdWVzdBobLnN5c21sLkV4ZWN1dGVTdGF0ZVJlc3BvbnNlEjgKB0NvbnZlcnQSFS5zeXNtbC5Db252ZXJ0UmVxdWVzdBoWLnN5c21sLkNvbnZlcnRSZXNwb25zZRJBCgpBcHBseUVkaXRzEhguc3lzbWwuQXBwbHlFZGl0c1JlcXVlc3QaGS5zeXNtbC5BcHBseUVkaXRzUmVzcG9uc2USUwoQVmVyaWZ5Q29uc3RyYWludBIeLnN5c21sLlZlcmlmeUNvbnN0cmFpbnRSZXF1ZXN0Gh8uc3lzbWwuVmVyaWZ5Q29uc3RyYWludFJlc3BvbnNlElYKEVZlcmlmeVJlcXVpcmVtZW50Eh8uc3lzbWwuVmVyaWZ5UmVxdWlyZW1lbnRSZXF1ZXN0GiAuc3lzbWwuVmVyaWZ5UmVxdWlyZW1lbnRSZXNwb25zZRJZChJWZXJpZnlTYXRpc2ZhY3Rpb24SIC5zeXNtbC5WZXJpZnlTYXRpc2ZhY3Rpb25SZXF1ZXN0GiEuc3lzbWwuVmVyaWZ5U2F0aXNmYWN0aW9uUmVzcG9uc2USRwoMRXZhbHVhdGVDYWxjEhouc3lzbWwuRXZhbHVhdGVDYWxjUmVxdWVzdBobLnN5c21sLkV2YWx1YXRlQ2FsY1Jlc3BvbnNlEkQKC1J1bkFuYWx5c2lzEhkuc3lzbWwuUnVuQW5hbHlzaXNSZXF1ZXN0Ghouc3lzbWwuUnVuQW5hbHlzaXNSZXNwb25zZRI7CghSdW5Td2VlcBIWLnN5c21sLlJ1blN3ZWVwUmVxdWVzdBoXLnN5c21sLlJ1blN3ZWVwUmVzcG9uc2USMgoFUXVlcnkSEy5zeXNtbC5RdWVyeVJlcXVlc3QaFC5zeXNtbC5RdWVyeVJlc3BvbnNlElMKEFJ1bkRvY3VtZW50UXVlcnkSHi5zeXNtbC5SdW5Eb2N1bWVudFF1ZXJ5UmVxdWVzdBofLnN5c21sLlJ1bkRvY3VtZW50UXVlcnlSZXNwb25zZRJNCg5SZW5kZXJEb2N1bWVudBIcLnN5c21sLlJlbmRlckRvY3VtZW50UmVxdWVzdBodLnN5c21sLlJlbmRlckRvY3VtZW50UmVzcG9uc2VCKlooZ2l0aHViLmNvbS9PcGVuLU1CRUUvT3BlblN5c01ML2FwaS9wcm90b2IGcHJvdG8z"); /** * Verdict is one verification's answer: whether the condition held, and, when @@ -2164,6 +2164,22 @@ export type Value = Message<"sysml.Value"> & { */ value: Function; case: "function"; + } | { + /** + * distinct elements with no order of their own + * + * @generated from field: sysml.ValueSet set = 18; + */ + value: ValueSet; + case: "set"; + } | { + /** + * shape and one Quantity per component + * + * @generated from field: sysml.TensorQuantity tensor_quantity = 19; + */ + value: TensorQuantity; + case: "tensorQuantity"; } | { case: undefined; value?: undefined }; }; @@ -2211,6 +2227,64 @@ export type Function = Message<"sysml.Function"> & { export const FunctionSchema: GenMessage = /*@__PURE__*/ messageDesc(file_sysml, 48); +/** + * ValueSet is a unique, unordered collection — a Collections::Set's elements — + * as distinct from a ValueSequence, whose order is part of its value. Two sets + * are equal when they hold the same elements in any order. The service sends + * the elements in the runtime's canonical order (Booleans, numbers, strings, + * quantities, enumeration literals, objects, each class in its own order), so + * equal sets cross alike; a client may send them in any order, but sending an + * element twice is rejected rather than read as one, since a repeated element + * is what a sequence carries. + * + * @generated from message sysml.ValueSet + */ +export type ValueSet = Message<"sysml.ValueSet"> & { + /** + * @generated from field: repeated sysml.Value elements = 1; + */ + elements: Value[]; +}; + +/** + * Describes the message sysml.ValueSet. + * Use `create(ValueSetSchema)` to create a new message. + */ +export const ValueSetSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_sysml, 49); + +/** + * TensorQuantity is a Quantities::TensorQuantityValue of any rank: its + * dimensions and, flattened in row-major order under them, one Quantity per + * component, each with its unit and reduction as a scalar Quantity carries them. + * A tensor of rank one is not a VectorQuantity, on the wire as in the runtime. + * + * @generated from message sysml.TensorQuantity + */ +export type TensorQuantity = Message<"sysml.TensorQuantity"> & { + /** + * Positive extents, one per rank; their product (one for rank 0) is how many + * components there are, and a tensor not filling them is rejected. + * + * @generated from field: repeated int64 dimensions = 1; + */ + dimensions: bigint[]; + + /** + * A named unit sent without its unit_term is rejected as a Quantity's is. + * + * @generated from field: repeated sysml.Quantity components = 2; + */ + components: Quantity[]; +}; + +/** + * Describes the message sysml.TensorQuantity. + * Use `create(TensorQuantitySchema)` to create a new message. + */ +export const TensorQuantitySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_sysml, 50); + /** * Array is a Collections::Array: its elements flattened in row-major order * under its dimensions, compared by content rather than by the object read. @@ -2239,7 +2313,7 @@ export type Array = Message<"sysml.Array"> & { * Use `create(ArraySchema)` to create a new message. */ export const ArraySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 49); + messageDesc(file_sysml, 51); /** * Vector is a VectorValues::NumericalVectorValue: its components in order, @@ -2262,7 +2336,7 @@ export type Vector = Message<"sysml.Vector"> & { * Use `create(VectorSchema)` to create a new message. */ export const VectorSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 50); + messageDesc(file_sysml, 52); /** * VectorQuantity is a Quantities::VectorQuantityValue: one Quantity per axis, @@ -2285,7 +2359,7 @@ export type VectorQuantity = Message<"sysml.VectorQuantity"> & { * Use `create(VectorQuantitySchema)` to create a new message. */ export const VectorQuantitySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 51); + messageDesc(file_sysml, 53); /** * Complex is one complex number in rectangular form. It crosses as one value @@ -2310,7 +2384,7 @@ export type Complex = Message<"sysml.Complex"> & { * Use `create(ComplexSchema)` to create a new message. */ export const ComplexSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 52); + messageDesc(file_sysml, 54); /** * EnumLiteral is one literal of an enumeration definition. A literal is its own @@ -2347,7 +2421,7 @@ export type EnumLiteral = Message<"sysml.EnumLiteral"> & { * Use `create(EnumLiteralSchema)` to create a new message. */ export const EnumLiteralSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 53); + messageDesc(file_sysml, 55); /** * @generated from message sysml.ValueSequence @@ -2364,7 +2438,7 @@ export type ValueSequence = Message<"sysml.ValueSequence"> & { * Use `create(ValueSequenceSchema)` to create a new message. */ export const ValueSequenceSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 54); + messageDesc(file_sysml, 56); /** * Quantity is a magnitude and the measurement reference it is expressed in, sent @@ -2415,7 +2489,7 @@ export type Quantity = Message<"sysml.Quantity"> & { * Use `create(QuantitySchema)` to create a new message. */ export const QuantitySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 55); + messageDesc(file_sysml, 57); /** * MeasurementRef is a MeasurementReferences::ScalarMeasurementReference held as @@ -2463,7 +2537,7 @@ export type MeasurementRef = Message<"sysml.MeasurementRef"> & { * Use `create(MeasurementRefSchema)` to create a new message. */ export const MeasurementRefSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 56); + messageDesc(file_sysml, 58); /** * UnitTerm is a unit reduced to a scale factor over base units: `km/h` reduces @@ -2497,7 +2571,7 @@ export type UnitTerm = Message<"sysml.UnitTerm"> & { * Use `create(UnitTermSchema)` to create a new message. */ export const UnitTermSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 57); + messageDesc(file_sysml, 59); /** * UnitFactor is one base unit raised to an exponent. @@ -2523,7 +2597,7 @@ export type UnitFactor = Message<"sysml.UnitFactor"> & { * Use `create(UnitFactorSchema)` to create a new message. */ export const UnitFactorSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 58); + messageDesc(file_sysml, 60); /** * Diagnostic represents a parse/semantic error or warning @@ -2562,7 +2636,7 @@ export type Diagnostic = Message<"sysml.Diagnostic"> & { * Use `create(DiagnosticSchema)` to create a new message. */ export const DiagnosticSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 59); + messageDesc(file_sysml, 61); /** * Span represents a source location @@ -2601,7 +2675,7 @@ export type Span = Message<"sysml.Span"> & { * Use `create(SpanSchema)` to create a new message. */ export const SpanSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 60); + messageDesc(file_sysml, 62); /** * ServerInfoRequest asks the service to describe itself. It carries no fields; @@ -2617,7 +2691,7 @@ export type ServerInfoRequest = Message<"sysml.ServerInfoRequest"> & { * Use `create(ServerInfoRequestSchema)` to create a new message. */ export const ServerInfoRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 61); + messageDesc(file_sysml, 63); /** * ServerInfoResponse describes the running service. @@ -2683,6 +2757,18 @@ export type ServerInfoResponse = Message<"sysml.ServerInfoResponse"> & { * unsupported null, and one is accepted as an action input * or calc argument; without it, one is refused with * UNIMPLEMENTED rather than read as another value. + * "set_values" - a Value carries a unique, unordered collection (a + * Collections::Set's elements) as set, each element once in + * canonical order, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument in any order; without it, one is refused + * with UNIMPLEMENTED rather than read as a sequence. + * "tensor_values" - a Value carries a tensor quantity of any rank as + * tensor_quantity, its dimensions and one Quantity per + * row-major component, rather than reporting it as an + * unsupported null, and one is accepted as an action input + * or calc argument; without it, one is refused with + * UNIMPLEMENTED rather than read as another value. * "apply_edits" - the ApplyEdits RPC edits a parsed model's own source, * preserving everything the edit did not touch. * "document_query" - the RunDocumentQuery RPC runs a named document query @@ -2707,7 +2793,7 @@ export type ServerInfoResponse = Message<"sysml.ServerInfoResponse"> & { * Use `create(ServerInfoResponseSchema)` to create a new message. */ export const ServerInfoResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 62); + messageDesc(file_sysml, 64); /** * QueryRequest runs a Query against a model the service already parsed. @@ -2740,7 +2826,7 @@ export type QueryRequest = Message<"sysml.QueryRequest"> & { * Use `create(QueryRequestSchema)` to create a new message. */ export const QueryRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 63); + messageDesc(file_sysml, 65); /** * QueryResponse contains the elements the query selected, in the order they are @@ -2762,7 +2848,7 @@ export type QueryResponse = Message<"sysml.QueryResponse"> & { * Use `create(QueryResponseSchema)` to create a new message. */ export const QueryResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 64); + messageDesc(file_sysml, 66); /** * Query is the standard's Query resource (SysML v2 API & Services). Its `@type` @@ -2802,7 +2888,7 @@ export type Query = Message<"sysml.Query"> & { * Use `create(QuerySchema)` to create a new message. */ export const QuerySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 65); + messageDesc(file_sysml, 67); /** * Constraint is the standard's Constraint, whose `@type` discriminates between @@ -2834,7 +2920,7 @@ export type Constraint = Message<"sysml.Constraint"> & { * Use `create(ConstraintSchema)` to create a new message. */ export const ConstraintSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 66); + messageDesc(file_sysml, 68); /** * PrimitiveConstraint compares one property of an element against a value. @@ -2877,7 +2963,7 @@ export type PrimitiveConstraint = Message<"sysml.PrimitiveConstraint"> & { * Use `create(PrimitiveConstraintSchema)` to create a new message. */ export const PrimitiveConstraintSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 67); + messageDesc(file_sysml, 69); /** * CompositeConstraint combines constraints. An empty constraint list fails the @@ -2902,7 +2988,7 @@ export type CompositeConstraint = Message<"sysml.CompositeConstraint"> & { * Use `create(CompositeConstraintSchema)` to create a new message. */ export const CompositeConstraintSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 68); + messageDesc(file_sysml, 70); /** * QueryResultElement is one matched element. `id` and `type` are always @@ -2938,7 +3024,7 @@ export type QueryResultElement = Message<"sysml.QueryResultElement"> & { * Use `create(QueryResultElementSchema)` to create a new message. */ export const QueryResultElementSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 69); + messageDesc(file_sysml, 71); /** * SweepRange is one parameter's range: the endpoints a swept run advances @@ -2981,7 +3067,7 @@ export type SweepRange = Message<"sysml.SweepRange"> & { * Use `create(SweepRangeSchema)` to create a new message. */ export const SweepRangeSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 70); + messageDesc(file_sysml, 72); /** * RunSweepRequest runs one analysis case or calc once per row of a sweep. Every @@ -3056,7 +3142,7 @@ export type RunSweepRequest = Message<"sysml.RunSweepRequest"> & { * Use `create(RunSweepRequestSchema)` to create a new message. */ export const RunSweepRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 71); + messageDesc(file_sysml, 73); /** * SweepRow is one run of a sweep: what it bound, what it produced, how long it @@ -3115,7 +3201,7 @@ export type SweepRow = Message<"sysml.SweepRow"> & { * Use `create(SweepRowSchema)` to create a new message. */ export const SweepRowSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 72); + messageDesc(file_sysml, 74); /** * RunSweepResponse carries the table, one row per run, in the order the runs @@ -3186,7 +3272,7 @@ export type RunSweepResponse = Message<"sysml.RunSweepResponse"> & { * Use `create(RunSweepResponseSchema)` to create a new message. */ export const RunSweepResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 73); + messageDesc(file_sysml, 75); /** * RunDocumentQueryRequest runs a named document query — a calc def @@ -3226,7 +3312,7 @@ export type RunDocumentQueryRequest = Message<"sysml.RunDocumentQueryRequest"> & * Use `create(RunDocumentQueryRequestSchema)` to create a new message. */ export const RunDocumentQueryRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 74); + messageDesc(file_sysml, 76); /** * DocumentQueryBinding binds one entry parameter of a document query. @@ -3250,7 +3336,7 @@ export type DocumentQueryBinding = Message<"sysml.DocumentQueryBinding"> & { * Use `create(DocumentQueryBindingSchema)` to create a new message. */ export const DocumentQueryBindingSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 75); + messageDesc(file_sysml, 77); /** * DocumentValue is one typed document-query value. A request binds a model @@ -3325,7 +3411,7 @@ export type DocumentValue = Message<"sysml.DocumentValue"> & { * Use `create(DocumentValueSchema)` to create a new message. */ export const DocumentValueSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 76); + messageDesc(file_sysml, 78); /** * DocumentQueryColumn is one projected property, in projection order. @@ -3344,7 +3430,7 @@ export type DocumentQueryColumn = Message<"sysml.DocumentQueryColumn"> & { * Use `create(DocumentQueryColumnSchema)` to create a new message. */ export const DocumentQueryColumnSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 77); + messageDesc(file_sysml, 79); /** * DocumentQueryCell is one row's values for one column, in the query's order. @@ -3363,7 +3449,7 @@ export type DocumentQueryCell = Message<"sysml.DocumentQueryCell"> & { * Use `create(DocumentQueryCellSchema)` to create a new message. */ export const DocumentQueryCellSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 78); + messageDesc(file_sysml, 80); /** * DocumentQueryRow is one selected element and its projected cells, one per @@ -3390,7 +3476,7 @@ export type DocumentQueryRow = Message<"sysml.DocumentQueryRow"> & { * Use `create(DocumentQueryRowSchema)` to create a new message. */ export const DocumentQueryRowSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 79); + messageDesc(file_sysml, 81); /** * RunDocumentQueryResponse is the query's answer: its projected columns and its @@ -3417,7 +3503,7 @@ export type RunDocumentQueryResponse = Message<"sysml.RunDocumentQueryResponse"> * Use `create(RunDocumentQueryResponseSchema)` to create a new message. */ export const RunDocumentQueryResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 80); + messageDesc(file_sysml, 82); /** * RenderDocumentRequest renders a named document — a part def specializing @@ -3448,7 +3534,7 @@ export type RenderDocumentRequest = Message<"sysml.RenderDocumentRequest"> & { * Use `create(RenderDocumentRequestSchema)` to create a new message. */ export const RenderDocumentRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 81); + messageDesc(file_sysml, 83); /** * RenderDocumentResponse carries the rendered Markdown, byte-for-byte what the @@ -3468,7 +3554,7 @@ export type RenderDocumentResponse = Message<"sysml.RenderDocumentResponse"> & { * Use `create(RenderDocumentResponseSchema)` to create a new message. */ export const RenderDocumentResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sysml, 82); + messageDesc(file_sysml, 84); /** * FailureReason says what kind of failure an `error` reports, so a client acts diff --git a/clients/node/test/client.test.ts b/clients/node/test/client.test.ts index 9054be4d6..d2d296d3e 100644 --- a/clients/node/test/client.test.ts +++ b/clients/node/test/client.test.ts @@ -14,7 +14,9 @@ import { CAPABILITY_MEASUREMENT_REFS, CAPABILITY_QUERY, CAPABILITY_SCHEDULE, + CAPABILITY_SET_VALUES, CAPABILITY_STRUCTURED_VALUES, + CAPABILITY_TENSOR_VALUES, CAPABILITY_VERIFICATION_VERDICTS, ClosedConnectionError, MissingCapabilityError, @@ -263,6 +265,47 @@ test("a calc held as a value arrives as the function it names, with the object i } }); +const SET_TENSOR_MODEL = `package W { + private import ScalarValues::*; + private import Collections::*; + private import Quantities::*; + private import MeasurementReferences::*; + private import SI::*; + private import TensorCalculations::*; + attribute s : Set { :>> elements = (3, 1, 2, 2, 3); } + attribute e : Set { :>> elements = (); } + attribute cubeRef : TensorMeasurementReference { + :>> dimensions = (2, 2, 2); + :>> mRefs = (Pa, Pa, Pa, Pa, Pa, Pa, Pa, Pa); + } + attribute cube : TensorQuantityValue = TensorCalculations::'['((1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0), cubeRef); +}`; + +test("a set arrives once per element in canonical order, and a tensor with its rank", async () => { + for (const options of [{ protocol: "grpc" as const }, {}, { encoding: "json" as const }]) { + await using connection = await connect(options); + const info = await connection.serverInfo(); + assert.ok(info.has(CAPABILITY_SET_VALUES)); + assert.ok(info.has(CAPABILITY_TENSOR_VALUES)); + await using model = await connection.loads(SET_TENSOR_MODEL); + + const set = await model.eval("W::s.elements"); + assert.deepEqual(set, { kind: "set", elements: [1n, 2n, 3n].map((value) => ({ kind: "int", value })) }); + assert.equal(formatValue(set), "{1, 2, 3}"); + assert.deepEqual(await model.eval("W::e.elements"), { kind: "set", elements: [] }); + + const cube = await model.eval("W::cube"); + assert.ok(cube.kind === "tensorQuantity"); + assert.deepEqual(cube.dimensions, [2n, 2n, 2n]); + assert.deepEqual( + cube.components.map((c) => c.magnitude), + [1, 2, 3, 4, 5, 6, 7, 8].map((value) => ({ kind: "real", value })), + ); + assert.ok(cube.components.every((c) => c.unit === "Pa")); + assert.equal(formatValue(cube), "Tensor(2, 2, 2)[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0][Pa]"); + } +}); + test("a file parses, and a syntax error is a diagnostic, not a thrown call", async () => { const dir = mkdtempSync(join(tmpdir(), "client-test-")); const path = join(dir, "sample.sysml"); diff --git a/clients/node/test/values.test.ts b/clients/node/test/values.test.ts index b84270de7..5e9a4bf2c 100644 --- a/clients/node/test/values.test.ts +++ b/clients/node/test/values.test.ts @@ -11,10 +11,12 @@ import { FunctionSchema, MeasurementRefSchema, QuantitySchema, + TensorQuantitySchema, UnitFactorSchema, UnitTermSchema, ValueSchema, ValueSequenceSchema, + ValueSetSchema, VectorQuantitySchema, VectorSchema, VerdictSchema, @@ -273,6 +275,95 @@ test("a vector quantity carries one quantity per component, each with its unit", ); }); +const setOf = (...elements: ReturnType[]) => + create(ValueSchema, { kind: { case: "set", value: create(ValueSetSchema, { elements }) } }); +const tensor = (dimensions: bigint[], ...components: ReturnType[]) => + create(ValueSchema, { + kind: { case: "tensorQuantity", value: create(TensorQuantitySchema, { dimensions, components }) }, + }); + +test("a set is its elements, each once, in the order the service sent them", () => { + const set = decodeValue(setOf(int(1n), int(2n), int(3n))); + assert.deepEqual(set, { kind: "set", elements: [1n, 2n, 3n].map((value) => ({ kind: "int", value })) }); + assert.equal(formatValue(set), "{1, 2, 3}"); + + // An empty set is a set of nothing, distinct from an empty sequence. + assert.deepEqual(decodeValue(setOf()), { kind: "set", elements: [] }); + assert.equal(formatValue({ kind: "set", elements: [] }), "{}"); + + // A set nests, and is nested, in place. + const nested = decodeValue(setOf(setOf(int(1n)), setOf())); + assert.deepEqual(nested, { + kind: "set", + elements: [{ kind: "set", elements: [{ kind: "int", value: 1n }] }, { kind: "set", elements: [] }], + }); + const sequence = create(ValueSchema, { + kind: { case: "sequence", value: create(ValueSequenceSchema, { elements: [setOf(int(1n)), int(2n)] }) }, + }); + assert.deepEqual(decodeValue(sequence), { + kind: "sequence", + elements: [{ kind: "set", elements: [{ kind: "int", value: 1n }] }, { kind: "int", value: 2n }], + }); + + // Sent in any order: the client does not reorder what a caller wrote. + const sent = encodeValue({ kind: "set", elements: [3n, 1n, 2n].map((value) => ({ kind: "int", value })) }); + assert.equal(sent.kind.case, "set"); + assert.deepEqual( + sent.kind.value.elements.map((e) => e.kind.value), + [3n, 1n, 2n], + ); +}); + +test("a tensor quantity keeps its rank, its shape and its row-major components", () => { + const cube = decodeValue(tensor([2n, 2n, 2n], ...[1, 2, 3, 4, 5, 6, 7, 8].map(metres))); + assert.equal(cube.kind, "tensorQuantity"); + assert.deepEqual(cube.dimensions, [2n, 2n, 2n]); + assert.equal(cube.components.length, 8); + assert.deepEqual(cube.components[7], { magnitude: { kind: "real", value: 8 }, unit: "m", unitTerm: METRE }); + assert.equal(formatValue(cube), "Tensor(2, 2, 2)[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0][m]"); + + // A rank-one tensor stays a tensor, never a vector quantity. + const line = decodeValue(tensor([2n], metres(1), metres(2))); + assert.equal(line.kind, "tensorQuantity"); + assert.deepEqual(line.dimensions, [2n]); + + // Components with differing units each show their own. + const speed = create(QuantitySchema, { + magnitude: { case: "intMagnitude", value: 5n }, + unit: "m/s", + unitTerm: create(UnitTermSchema, { + scaleNum: 1, + scaleDen: 1, + factors: [ + create(UnitFactorSchema, { unitId: "SI::metre", exponent: 1 }), + create(UnitFactorSchema, { unitId: "SI::second", exponent: -1 }), + ], + }), + }); + assert.equal(formatValue(decodeValue(tensor([1n, 2n], metres(1), speed))), "Tensor(1, 2)[1.0[m], 5[m/s]]"); + + // Shape and components must agree, both ways, and every dimension is positive. + const malformed = (message: RegExp) => (error: unknown) => + error instanceof MalformedValueError && message.test(error.message); + assert.throws(() => decodeValue(tensor([2n, 2n], metres(1), metres(2), metres(3))), malformed(/holds 3/)); + assert.throws(() => decodeValue(tensor([2n], metres(1), metres(2), metres(3))), malformed(/holds 3/)); + assert.throws(() => decodeValue(tensor([0n])), malformed(/not positive/)); + assert.throws(() => decodeValue(tensor([-1n], metres(1))), malformed(/not positive/)); + assert.throws(() => decodeValue(tensor([], metres(1), metres(2))), malformed(/holds 2/)); + assert.throws( + () => encodeValue({ kind: "tensorQuantity", dimensions: [2n, 2n], components: [] }), + malformed(/holds 0/), + ); + assert.throws( + () => encodeValue({ kind: "tensorQuantity", dimensions: [0n], components: [] }), + malformed(/not positive/), + ); + + // A component without a magnitude is malformed, never read as zero. + const noMagnitude = create(QuantitySchema, { unit: "m" }); + assert.throws(() => decodeValue(tensor([1n], noMagnitude)), malformed(/no magnitude/)); +}); + const unitTerm = (scaleNum: number, ...factors: [string, number][]) => create(UnitTermSchema, { scaleNum, @@ -403,6 +494,31 @@ test("encodeValue is the inverse of decodeValue, through the wire bytes", () => ], }, { kind: "sequence", elements: [{ kind: "vector", components: [{ kind: "real", value: 1 }] }] }, + { kind: "set", elements: [] }, + { + kind: "set", + elements: [ + { kind: "int", value: 3n }, + { kind: "string", value: "a" }, + { kind: "set", elements: [{ kind: "boolean", value: true }] }, + { kind: "sequence", elements: [{ kind: "int", value: 1n }] }, + ], + }, + { + kind: "tensorQuantity", + dimensions: [2n, 1n, 2n], + components: [ + { magnitude: { kind: "real", value: 1 }, unit: "m", unitTerm: METRE }, + { magnitude: { kind: "int", value: 2n }, unit: "m", unitTerm: METRE }, + { magnitude: { kind: "real", value: 3 }, unit: "m", unitTerm: METRE }, + { magnitude: { kind: "int", value: 4n }, unit: "m", unitTerm: METRE }, + ], + }, + { + kind: "array", + dimensions: [1n], + elements: [{ kind: "set", elements: [{ kind: "int", value: 1n }] }], + }, ]; for (const value of values) { const bytes = toBinary(ValueSchema, encodeValue(value)); diff --git a/clients/python/opensysml/__init__.py b/clients/python/opensysml/__init__.py index 920513260..a13c656f9 100644 --- a/clients/python/opensysml/__init__.py +++ b/clients/python/opensysml/__init__.py @@ -23,7 +23,10 @@ TypeFacts, ) from opensysml.capabilities import MissingCapabilityError, ServerInfo -from opensysml.values import UNSET, Array, Function, MeasurementRef, UnsetType, Vector, VectorQuantity +from opensysml.values import ( + UNSET, Array, Function, MeasurementRef, SetValue, TensorQuantity, UnsetType, Vector, + VectorQuantity, +) from opensysml.verdict import ( AnalysisResult, CalcResult, SweepRow, SweepTable, Verdict, VerificationVerdict, ) @@ -56,7 +59,8 @@ "AttributeFacts", "ServerInfo", "UNSET", "UnsetType", - "Array", "Vector", "VectorQuantity", "MeasurementRef", "Function", + "Array", "Vector", "VectorQuantity", "MeasurementRef", "Function", "SetValue", + "TensorQuantity", "Conversion", "FORMAT_SYSML", "FORMAT_TURTLE", "format_of_path", "ExperimentalFeatureWarning", "is_experimental", "Editor", "EditResult", "AppliedEdit", diff --git a/clients/python/opensysml/capabilities.py b/clients/python/opensysml/capabilities.py index 3c87d44b1..37aa5f179 100644 --- a/clients/python/opensysml/capabilities.py +++ b/clients/python/opensysml/capabilities.py @@ -107,6 +107,19 @@ #: it with ``UNIMPLEMENTED``. CAPABILITY_FUNCTION_VALUES = "function_values" +#: A unique, unordered collection — a ``Collections::Set``'s elements — as +#: ``Value.set``, read as :class:`~opensysml.values.SetValue` with each element +#: once in canonical order. Without it the service sends an unsupported null +#: naming the value, which is an error, and refuses one sent to it with +#: ``UNIMPLEMENTED``. +CAPABILITY_SET_VALUES = "set_values" + +#: A tensor quantity of any rank as ``Value.tensor_quantity``, read as +#: :class:`~opensysml.values.TensorQuantity`. Without it the service sends an +#: unsupported null naming the value, which is an error, and refuses one sent to +#: it with ``UNIMPLEMENTED``. +CAPABILITY_TENSOR_VALUES = "tensor_values" + #: What the body of a verification case answered, as the #: ``verification_verdicts`` of a requirement, satisfaction or analysis #: response, read as :class:`~opensysml.verdict.VerificationVerdict`. Without it diff --git a/clients/python/opensysml/connection.py b/clients/python/opensysml/connection.py index 3c1ae7b56..3717836fa 100644 --- a/clients/python/opensysml/connection.py +++ b/clients/python/opensysml/connection.py @@ -27,7 +27,9 @@ CAPABILITY_QUERY, CAPABILITY_RENDER_DOCUMENT, CAPABILITY_SCHEDULE, + CAPABILITY_SET_VALUES, CAPABILITY_STRUCTURED_VALUES, + CAPABILITY_TENSOR_VALUES, CAPABILITY_VERIFICATION, MissingCapabilityError, ServerInfo, @@ -64,6 +66,8 @@ Function, MeasurementRef, Quantity, + SetValue, + TensorQuantity, Vector, VectorQuantity, _Infinity, @@ -1186,10 +1190,13 @@ def execute_action(self, action_symbol_id, model_hash, inputs=None, MissingCapabilityError: If an input holds a ``complex`` and the service predates ``complex_values``, an :class:`~opensysml.values.Array`, :class:`~opensysml.values.Vector` or :class:`~opensysml.values.VectorQuantity` - and the service predates ``structured_values``, or a + and the service predates ``structured_values``, a :class:`~opensysml.values.MeasurementRef` and the service predates - ``measurement_refs``, or a :class:`~opensysml.values.Function` - and the service predates ``function_values``, or a schedule is + ``measurement_refs``, a :class:`~opensysml.values.Function` + and the service predates ``function_values``, a + :class:`~opensysml.values.SetValue` and the service predates + ``set_values``, a :class:`~opensysml.values.TensorQuantity` + and the service predates ``tensor_values``, or a schedule is given and the service predates ``schedule``; nothing is sent InvalidRequestError: If the schedule names no policy """ @@ -1408,9 +1415,10 @@ def calc(self, symbol_id, model_hash, arguments=None): argument holds a ``complex`` and the service predates ``complex_values``, an array, vector or vector quantity and the service predates ``structured_values``, a measurement - reference and the service predates ``measurement_refs``, or a - function and the service predates ``function_values``; nothing - is sent + reference and the service predates ``measurement_refs``, a + function and the service predates ``function_values``, a set + and the service predates ``set_values``, or a tensor quantity + and the service predates ``tensor_values``; nothing is sent ModelNotFoundError: If the service no longer holds the model """ self._require_verification() @@ -1426,6 +1434,8 @@ def calc(self, symbol_id, model_hash, arguments=None): CAPABILITY_STRUCTURED_VALUES, CAPABILITY_MEASUREMENT_REFS, CAPABILITY_FUNCTION_VALUES, + CAPABILITY_SET_VALUES, + CAPABILITY_TENSOR_VALUES, )) ): response = self._stub.EvaluateCalc(request) @@ -1480,9 +1490,11 @@ def run_analysis(self, symbol_id, model_hash, subject=None, arguments=None, input with no value, a failing step MissingCapabilityError: If the service cannot verify, or an argument holds a ``complex`` and the service predates - ``complex_values``, or an array, vector or vector quantity and - the service predates ``structured_values``, or a schedule is - given and the service predates ``schedule``; nothing is sent + ``complex_values``, an array, vector or vector quantity and + the service predates ``structured_values``, a set and the + service predates ``set_values``, a tensor quantity and the + service predates ``tensor_values``, or a schedule is given + and the service predates ``schedule``; nothing is sent InvalidRequestError: If the schedule names no policy ModelNotFoundError: If the service no longer holds the model """ @@ -1502,6 +1514,9 @@ def run_analysis(self, symbol_id, model_hash, subject=None, arguments=None, CAPABILITY_VERIFICATION, CAPABILITY_COMPLEX_VALUES, CAPABILITY_STRUCTURED_VALUES, + CAPABILITY_MEASUREMENT_REFS, + CAPABILITY_SET_VALUES, + CAPABILITY_TENSOR_VALUES, CAPABILITY_SCHEDULE, )) ): @@ -1728,6 +1743,22 @@ def _require_schedule(self, schedule): upgrade_remedy(CAPABILITY_SCHEDULE), ) + def _require_set_values(self): + """Refuse to send a set a service without ``set_values`` would read as null.""" + require( + self.server_info(), + CAPABILITY_SET_VALUES, + upgrade_remedy(CAPABILITY_SET_VALUES), + ) + + def _require_tensor_values(self): + """Refuse to send a tensor quantity a service without ``tensor_values`` would read as null.""" + require( + self.server_info(), + CAPABILITY_TENSOR_VALUES, + upgrade_remedy(CAPABILITY_TENSOR_VALUES), + ) + def _require_feature_values(self): """Refuse instances from a service that populates only the removed `slots` field.""" require( @@ -1790,6 +1821,12 @@ def _python_to_value(self, py_value): elif isinstance(py_value, VectorQuantity): self._require_structured_values() return sysml_pb2.Value(vector_quantity=py_value.to_pb()) + elif isinstance(py_value, (SetValue, set, frozenset)): + self._require_set_values() + return sysml_pb2.Value(set=SetValue(py_value).to_pb(self._python_to_value)) + elif isinstance(py_value, TensorQuantity): + self._require_tensor_values() + return sysml_pb2.Value(tensor_quantity=py_value.to_pb()) elif isinstance(py_value, EnumLiteral): return sysml_pb2.Value(enum_literal=sysml_pb2.EnumLiteral( literal_id=py_value.literal_id, diff --git a/clients/python/opensysml/proto/sysml_pb2.py b/clients/python/opensysml/proto/sysml_pb2.py index 011b3efc7..af102d2a1 100644 --- a/clients/python/opensysml/proto/sysml_pb2.py +++ b/clients/python/opensysml/proto/sysml_pb2.py @@ -24,7 +24,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0bsysml.proto\x12\x05sysml\"\xe2\x01\n\x07Verdict\x12\x0c\n\x04kind\x18\x01 \x01(\t\x12\x12\n\nelement_id\x18\x02 \x01(\t\x12\x0f\n\x07\x65lement\x18\x03 \x01(\t\x12\r\n\x05holds\x18\x04 \x01(\x08\x12\x11\n\tcondition\x18\x05 \x01(\t\x12\x13\n\x0binstance_id\x18\x06 \x01(\x03\x12\x18\n\x10instance_type_id\x18\x07 \x01(\t\x12\r\n\x05\x65rror\x18\x08 \x01(\t\x12,\n\x0e\x66\x61ilure_reason\x18\t \x01(\x0e\x32\x14.sysml.FailureReason\x12\x16\n\x0erequirement_id\x18\n \x01(\t\"[\n\x17VerifyConstraintRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\x12\x19\n\x11subject_symbol_id\x18\x03 \x01(\t\"\x96\x01\n\x18VerifyConstraintResponse\x12\x1f\n\x07verdict\x18\x01 \x01(\x0b\x32\x0e.sysml.Verdict\x12\"\n\tinstances\x18\x02 \x03(\x0b\x32\x0f.sysml.Instance\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x04 \x03(\x0b\x32\x11.sysml.Diagnostic\"\\\n\x18VerifyRequirementRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\x12\x19\n\x11subject_symbol_id\x18\x03 \x01(\t\"m\n\x13VerificationVerdict\x12\x0f\n\x07\x63\x61se_id\x18\x01 \x01(\t\x12\x0c\n\x04kind\x18\x02 \x01(\t\x12\x0e\n\x06\x64\x65tail\x18\x03 \x01(\t\x12\x0f\n\x07subcase\x18\x04 \x01(\x08\x12\x16\n\x0erequirement_id\x18\x05 \x01(\t\"\xd2\x01\n\x19VerifyRequirementResponse\x12\x1f\n\x07verdict\x18\x01 \x01(\x0b\x32\x0e.sysml.Verdict\x12\"\n\tinstances\x18\x02 \x03(\x0b\x32\x0f.sysml.Instance\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x04 \x03(\x0b\x32\x11.sysml.Diagnostic\x12\x39\n\x15verification_verdicts\x18\x05 \x03(\x0b\x32\x1a.sysml.VerificationVerdict\"B\n\x19VerifySatisfactionRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\"\x82\x02\n\x1aVerifySatisfactionResponse\x12 \n\x08verdicts\x18\x01 \x03(\x0b\x32\x0e.sysml.Verdict\x12\"\n\tinstances\x18\x02 \x03(\x0b\x32\x0f.sysml.Instance\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x04 \x03(\x0b\x32\x11.sysml.Diagnostic\x12,\n\x0e\x66\x61ilure_reason\x18\x05 \x01(\x0e\x32\x14.sysml.FailureReason\x12\x39\n\x15verification_verdicts\x18\x06 \x03(\x0b\x32\x1a.sysml.VerificationVerdict\"]\n\x13\x45valuateCalcRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\x12\x1f\n\targuments\x18\x03 \x03(\x0b\x32\x0c.sysml.Value\"\xbd\x01\n\x14\x45valuateCalcResponse\x12\x1c\n\x06result\x18\x01 \x01(\x0b\x32\x0c.sysml.Value\x12\"\n\x07outputs\x18\x02 \x03(\x0b\x32\x11.sysml.CalcOutput\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x04 \x03(\x0b\x32\x11.sysml.Diagnostic\x12,\n\x0e\x66\x61ilure_reason\x18\x05 \x01(\x0e\x32\x14.sysml.FailureReason\"7\n\nCalcOutput\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value\"\x96\x02\n\x12RunAnalysisRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\x12\x19\n\x11subject_symbol_id\x18\x03 \x01(\t\x12\x1f\n\targuments\x18\x04 \x03(\x0b\x32\x0c.sysml.Value\x12\x46\n\x0fnamed_arguments\x18\x05 \x03(\x0b\x32-.sysml.RunAnalysisRequest.NamedArgumentsEntry\x12\x10\n\x08schedule\x18\x06 \x01(\t\x1a\x43\n\x13NamedArgumentsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value:\x02\x38\x01\"\x9f\x02\n\x13RunAnalysisResponse\x12\"\n\x07outputs\x18\x01 \x03(\x0b\x32\x11.sysml.CalcOutput\x12 \n\x08verdicts\x18\x02 \x03(\x0b\x32\x0e.sysml.Verdict\x12\"\n\tinstances\x18\x03 \x03(\x0b\x32\x0f.sysml.Instance\x12\r\n\x05\x65rror\x18\x04 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x05 \x03(\x0b\x32\x11.sysml.Diagnostic\x12,\n\x0e\x66\x61ilure_reason\x18\x06 \x01(\x0e\x32\x14.sysml.FailureReason\x12\x39\n\x15verification_verdicts\x18\x07 \x03(\x0b\x32\x1a.sysml.VerificationVerdict\"\x8c\x01\n\x10ParseFileRequest\x12\x13\n\tfile_path\x18\x01 \x01(\tH\x00\x12\x11\n\x07\x63ontent\x18\x02 \x01(\tH\x00\x12\x18\n\x0c\x63ontent_hash\x18\x03 \x01(\tB\x02\x18\x01\x12\x10\n\x08language\x18\x04 \x01(\t\x12\x1a\n\x12strict_conformance\x18\x05 \x01(\x08\x42\x08\n\x06source\"b\n\x0eSourceDocument\x12\x13\n\tfile_path\x18\x01 \x01(\tH\x00\x12\x11\n\x07\x63ontent\x18\x02 \x01(\tH\x00\x12\x10\n\x08language\x18\x03 \x01(\t\x12\x0c\n\x04name\x18\x04 \x01(\tB\x08\n\x06source\"[\n\x13ParseSourcesRequest\x12(\n\tdocuments\x18\x01 \x03(\x0b\x32\x15.sysml.SourceDocument\x12\x1a\n\x12strict_conformance\x18\x02 \x01(\x08\"\x83\x01\n\x14ParseSourcesResponse\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12 \n\x05roots\x18\x02 \x03(\x0b\x32\x11.sysml.SymbolInfo\x12&\n\x0b\x64iagnostics\x18\x03 \x03(\x0b\x32\x11.sysml.Diagnostic\x12\r\n\x05\x65rror\x18\x04 \x01(\t\"\x7f\n\x11ParseFileResponse\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x1f\n\x04root\x18\x02 \x01(\x0b\x32\x11.sysml.SymbolInfo\x12&\n\x0b\x64iagnostics\x18\x03 \x03(\x0b\x32\x11.sysml.Diagnostic\x12\r\n\x05\x65rror\x18\x04 \x01(\t\"9\n\x10GetSymbolRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\"B\n\x0eSymbolResponse\x12!\n\x06symbol\x18\x01 \x01(\x0b\x32\x11.sysml.SymbolInfo\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"(\n\x12\x44iagnosticsRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\"L\n\x13\x44iagnosticsResponse\x12&\n\x0b\x64iagnostics\x18\x01 \x03(\x0b\x32\x11.sysml.Diagnostic\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"o\n\x0f\x45valuateRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x12\n\nexpression\x18\x02 \x01(\t\x12\x19\n\x11\x63ontext_symbol_id\x18\x03 \x01(\t\x12\x19\n\x11subject_symbol_id\x18\x04 \x01(\t\"g\n\x10\x45valuateResponse\x12\x1c\n\x06result\x18\x01 \x01(\x0b\x32\x0c.sysml.Value\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x03 \x03(\x0b\x32\x11.sysml.Diagnostic\"\xc2\x01\n\x08Instance\x12\n\n\x02id\x18\x01 \x01(\x03\x12\x16\n\x0etype_symbol_id\x18\x02 \x01(\t\x12:\n\x0e\x66\x65\x61ture_values\x18\x04 \x03(\x0b\x32\".sysml.Instance.FeatureValuesEntry\x1aI\n\x12\x46\x65\x61tureValuesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\"\n\x05value\x18\x02 \x01(\x0b\x32\x13.sysml.FeatureValue:\x02\x38\x01J\x04\x08\x03\x10\x04R\x05slots\"\x84\x01\n\x0c\x46\x65\x61tureValue\x12\x14\n\x0c\x66\x65\x61ture_name\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value\x12\x1c\n\x06values\x18\x03 \x03(\x0b\x32\x0c.sysml.Value\x12\x14\n\x0cmaterialized\x18\x04 \x01(\x08\x12\r\n\x05\x65rror\x18\x05 \x01(\t\";\n\x12InstantiateRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\"\x93\x01\n\x13InstantiateResponse\x12!\n\x08instance\x18\x01 \x01(\x0b\x32\x0f.sysml.Instance\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x03 \x03(\x0b\x32\x11.sysml.Diagnostic\x12\"\n\tinstances\x18\x04 \x03(\x0b\x32\x0f.sysml.Instance\"\xcc\x01\n\x14\x45xecuteActionRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x18\n\x10\x61\x63tion_symbol_id\x18\x02 \x01(\t\x12\x37\n\x06inputs\x18\x03 \x03(\x0b\x32\'.sysml.ExecuteActionRequest.InputsEntry\x12\x10\n\x08schedule\x18\x04 \x01(\t\x1a;\n\x0bInputsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value:\x02\x38\x01\"\xc8\x01\n\x15\x45xecuteActionResponse\x12:\n\x07outputs\x18\x01 \x03(\x0b\x32).sysml.ExecuteActionResponse.OutputsEntry\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x03 \x03(\x0b\x32\x11.sysml.Diagnostic\x1a<\n\x0cOutputsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value:\x02\x38\x01\"l\n\x13\x45xecuteStateRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x1f\n\x17state_machine_symbol_id\x18\x02 \x01(\t\x12\x0e\n\x06\x65vents\x18\x03 \x03(\t\x12\x10\n\x08schedule\x18\x04 \x01(\t\"\xee\x01\n\x14\x45xecuteStateResponse\x12\x16\n\x0estates_visited\x18\x01 \x03(\t\x12\x44\n\rfinal_context\x18\x02 \x03(\x0b\x32-.sysml.ExecuteStateResponse.FinalContextEntry\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x04 \x03(\x0b\x32\x11.sysml.Diagnostic\x1a\x41\n\x11\x46inalContextEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value:\x02\x38\x01\"\xa0\x01\n\x0e\x43onvertRequest\x12\x13\n\tfile_path\x18\x01 \x01(\tH\x00\x12\x11\n\x07\x63ontent\x18\x02 \x01(\tH\x00\x12\x14\n\nmodel_hash\x18\x06 \x01(\tH\x00\x12\x13\n\x0b\x66rom_format\x18\x03 \x01(\t\x12\x11\n\tto_format\x18\x04 \x01(\t\x12\x1e\n\x16tolerate_syntax_errors\x18\x05 \x01(\x08\x42\x08\n\x06source\"\xb4\x01\n\x0f\x43onvertResponse\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\x12\x13\n\x0b\x66rom_format\x18\x02 \x01(\t\x12\x11\n\tto_format\x18\x03 \x01(\t\x12\r\n\x05\x65rror\x18\x04 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x05 \x03(\x0b\x32\x11.sysml.Diagnostic\x12\x14\n\x0c\x65xperimental\x18\x06 \x01(\x08\x12\x1b\n\x13\x65xperimental_notice\x18\x07 \x01(\t\"Q\n\x11\x41pplyEditsRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12(\n\noperations\x18\x02 \x03(\x0b\x32\x14.sysml.EditOperation\"\xbc\x01\n\rEditOperation\x12(\n\tset_value\x18\x01 \x01(\x0b\x32\x13.sysml.SetValueEditH\x00\x12#\n\x06rename\x18\x02 \x01(\x0b\x32\x11.sysml.RenameEditH\x00\x12*\n\nadd_member\x18\x03 \x01(\x0b\x32\x14.sysml.AddMemberEditH\x00\x12#\n\x06\x64\x65lete\x18\x04 \x01(\x0b\x32\x11.sysml.DeleteEditH\x00\x42\x0b\n\toperation\"\x82\x01\n\rAddMemberEdit\x12\r\n\x05owner\x18\x01 \x01(\t\x12\x0c\n\x04kind\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x0c\n\x04type\x18\x04 \x01(\t\x12\x14\n\x0cmultiplicity\x18\x05 \x01(\t\x12\r\n\x05value\x18\x06 \x01(\t\x12\x13\n\x0bspecializes\x18\x07 \x03(\t\"-\n\nDeleteEdit\x12\x0e\n\x06target\x18\x01 \x01(\t\x12\x0f\n\x07\x63\x61scade\x18\x02 \x01(\x08\"-\n\x0cSetValueEdit\x12\x0e\n\x06target\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\".\n\nRenameEdit\x12\x0e\n\x06target\x18\x01 \x01(\t\x12\x10\n\x08new_name\x18\x02 \x01(\t\"\xc2\x01\n\x12\x41pplyEditsResponse\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\x12#\n\x07\x61pplied\x18\x02 \x03(\x0b\x32\x12.sysml.AppliedEdit\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12#\n\x07\x66\x61ilure\x18\x04 \x01(\x0e\x32\x12.sysml.EditFailure\x12&\n\x0b\x64iagnostics\x18\x05 \x03(\x0b\x32\x11.sysml.Diagnostic\x12\x1a\n\x12referring_elements\x18\x06 \x03(\t\"z\n\x0b\x41ppliedEdit\x12\x17\n\x0foperation_index\x18\x01 \x01(\x05\x12\x0e\n\x06target\x18\x02 \x01(\t\x12\x0e\n\x06offset\x18\x03 \x01(\x05\x12\x0e\n\x06length\x18\x04 \x01(\x05\x12\x10\n\x08old_text\x18\x05 \x01(\t\x12\x10\n\x08new_text\x18\x06 \x01(\t\"\xfd\x02\n\nSymbolInfo\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0c\n\x04kind\x18\x03 \x01(\t\x12\x31\n\x08metadata\x18\x04 \x03(\x0b\x32\x1f.sysml.SymbolInfo.MetadataEntry\x12\x11\n\tchild_ids\x18\x05 \x03(\t\x12(\n\nattributes\x18\x06 \x03(\x0b\x32\x14.sysml.AttributeInfo\x12\"\n\ttype_info\x18\x07 \x01(\x0b\x32\x0f.sysml.TypeInfo\x12-\n\x0cmultiplicity\x18\x08 \x01(\x0b\x32\x17.sysml.MultiplicityInfo\x12.\n\x0fspecializations\x18\t \x03(\x0b\x32\x15.sysml.Specialization\x12#\n\x1bwithheld_library_attributes\x18\n \x01(\x05\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"X\n\x0eSpecialization\x12\x0c\n\x04kind\x18\x01 \x01(\t\x12\x10\n\x08\x64\x65\x63lared\x18\x02 \x01(\t\x12\x11\n\ttarget_id\x18\x03 \x01(\t\x12\x13\n\x0btarget_kind\x18\x04 \x01(\t\"\x95\x01\n\x08TypeInfo\x12\x10\n\x08\x64\x65\x63lared\x18\x01 \x01(\t\x12\x13\n\x0bresolved_id\x18\x02 \x01(\t\x12\x15\n\rresolved_kind\x18\x03 \x01(\t\x12\x11\n\tprimitive\x18\x04 \x01(\t\x12\x18\n\x10primitive_source\x18\x05 \x01(\t\x12\x10\n\x08quantity\x18\x06 \x01(\x08\x12\x0c\n\x04unit\x18\x07 \x01(\t\"0\n\x10MultiplicityInfo\x12\r\n\x05lower\x18\x01 \x01(\t\x12\r\n\x05upper\x18\x02 \x01(\t\"V\n\rAttributeInfo\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x1b\n\x05value\x18\x03 \x01(\x0b\x32\x0c.sysml.Value\x12\x0c\n\x04unit\x18\x04 \x01(\t\"\x9b\x04\n\x05Value\x12\x13\n\tint_value\x18\x01 \x01(\x03H\x00\x12\x14\n\nreal_value\x18\x02 \x01(\x01H\x00\x12\x14\n\nbool_value\x18\x03 \x01(\x08H\x00\x12\x16\n\x0cstring_value\x18\x04 \x01(\tH\x00\x12\x15\n\x0binstance_id\x18\x05 \x01(\x03H\x00\x12(\n\x08sequence\x18\x06 \x01(\x0b\x32\x14.sysml.ValueSequenceH\x00\x12\x0e\n\x04null\x18\x07 \x01(\tH\x00\x12#\n\x08quantity\x18\x08 \x01(\x0b\x32\x0f.sysml.QuantityH\x00\x12*\n\x0c\x65num_literal\x18\t \x01(\x0b\x32\x12.sysml.EnumLiteralH\x00\x12\x0f\n\x05unset\x18\n \x01(\x08H\x00\x12!\n\x07\x63omplex\x18\x0b \x01(\x0b\x32\x0e.sysml.ComplexH\x00\x12\x1d\n\x05\x61rray\x18\x0c \x01(\x0b\x32\x0c.sysml.ArrayH\x00\x12\x1f\n\x06vector\x18\r \x01(\x0b\x32\r.sysml.VectorH\x00\x12\x30\n\x0fvector_quantity\x18\x0e \x01(\x0b\x32\x15.sysml.VectorQuantityH\x00\x12\x30\n\x0fmeasurement_ref\x18\x0f \x01(\x0b\x32\x15.sysml.MeasurementRefH\x00\x12\x12\n\x08infinity\x18\x10 \x01(\x08H\x00\x12#\n\x08\x66unction\x18\x11 \x01(\x0b\x32\x0f.sysml.FunctionH\x00\x42\x06\n\x04kind\",\n\x08\x46unction\x12\x0f\n\x07\x63\x61lc_id\x18\x01 \x01(\t\x12\x0f\n\x07self_id\x18\x02 \x01(\x03\";\n\x05\x41rray\x12\x12\n\ndimensions\x18\x01 \x03(\x03\x12\x1e\n\x08\x65lements\x18\x02 \x03(\x0b\x32\x0c.sysml.Value\"*\n\x06Vector\x12 \n\ncomponents\x18\x01 \x03(\x0b\x32\x0c.sysml.Value\"5\n\x0eVectorQuantity\x12#\n\ncomponents\x18\x01 \x03(\x0b\x32\x0f.sysml.Quantity\"*\n\x07\x43omplex\x12\x0c\n\x04real\x18\x01 \x01(\x01\x12\x11\n\timaginary\x18\x02 \x01(\x01\"G\n\x0b\x45numLiteral\x12\x12\n\nliteral_id\x18\x01 \x01(\t\x12\x16\n\x0e\x65numeration_id\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\"/\n\rValueSequence\x12\x1e\n\x08\x65lements\x18\x01 \x03(\x0b\x32\x0c.sysml.Value\"|\n\x08Quantity\x12\x17\n\rint_magnitude\x18\x01 \x01(\x03H\x00\x12\x18\n\x0ereal_magnitude\x18\x02 \x01(\x01H\x00\x12\x0c\n\x04unit\x18\x03 \x01(\t\x12\"\n\tunit_term\x18\x04 \x01(\x0b\x32\x0f.sysml.UnitTermB\x0b\n\tmagnitude\"S\n\x0eMeasurementRef\x12\x0c\n\x04unit\x18\x01 \x01(\t\x12\"\n\tunit_term\x18\x02 \x01(\x0b\x32\x0f.sysml.UnitTerm\x12\x0f\n\x07unit_id\x18\x03 \x01(\t\"T\n\x08UnitTerm\x12\x11\n\tscale_num\x18\x01 \x01(\x01\x12\x11\n\tscale_den\x18\x02 \x01(\x01\x12\"\n\x07\x66\x61\x63tors\x18\x03 \x03(\x0b\x32\x11.sysml.UnitFactor\"/\n\nUnitFactor\x12\x0f\n\x07unit_id\x18\x01 \x01(\t\x12\x10\n\x08\x65xponent\x18\x02 \x01(\x01\"X\n\nDiagnostic\x12\x10\n\x08severity\x18\x01 \x01(\t\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x19\n\x04span\x18\x03 \x01(\x0b\x32\x0b.sysml.Span\x12\x0c\n\x04\x63ode\x18\x04 \x01(\t\"^\n\x04Span\x12\x0c\n\x04\x66ile\x18\x01 \x01(\t\x12\x12\n\nstart_line\x18\x02 \x01(\x05\x12\x11\n\tstart_col\x18\x03 \x01(\x05\x12\x10\n\x08\x65nd_line\x18\x04 \x01(\x05\x12\x0f\n\x07\x65nd_col\x18\x05 \x01(\x05\"\x13\n\x11ServerInfoRequest\";\n\x12ServerInfoResponse\x12\x0f\n\x07version\x18\x01 \x01(\t\x12\x14\n\x0c\x63\x61pabilities\x18\x02 \x03(\t\"S\n\x0cQueryRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x1b\n\x05query\x18\x02 \x01(\x0b\x32\x0c.sysml.Query\x12\x12\n\noslc_query\x18\x03 \x01(\t\"<\n\rQueryResponse\x12+\n\x08\x65lements\x18\x01 \x03(\x0b\x32\x19.sysml.QueryResultElement\"H\n\x05Query\x12\r\n\x05scope\x18\x01 \x03(\t\x12\x0e\n\x06select\x18\x02 \x03(\t\x12 \n\x05where\x18\x03 \x01(\x0b\x32\x11.sysml.Constraint\"|\n\nConstraint\x12/\n\tprimitive\x18\x01 \x01(\x0b\x32\x1a.sysml.PrimitiveConstraintH\x00\x12/\n\tcomposite\x18\x02 \x01(\x0b\x32\x1a.sysml.CompositeConstraintH\x00\x42\x0c\n\nconstraint\"s\n\x13PrimitiveConstraint\x12\x0f\n\x07inverse\x18\x01 \x01(\x08\x12\x10\n\x08property\x18\x02 \x01(\t\x12*\n\x08operator\x18\x03 \x01(\x0e\x32\x18.sysml.PrimitiveOperator\x12\r\n\x05value\x18\x04 \x03(\t\"h\n\x13\x43ompositeConstraint\x12*\n\x08operator\x18\x01 \x01(\x0e\x32\x18.sysml.CompositeOperator\x12%\n\nconstraint\x18\x02 \x03(\x0b\x32\x11.sysml.Constraint\"\xa0\x01\n\x12QueryResultElement\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12=\n\nproperties\x18\x03 \x03(\x0b\x32).sysml.QueryResultElement.PropertiesEntry\x1a\x31\n\x0fPropertiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"s\n\nSweepRange\x12\x11\n\tparameter\x18\x01 \x01(\t\x12\x1b\n\x05start\x18\x02 \x01(\x0b\x32\x0c.sysml.Value\x12\x19\n\x03\x65nd\x18\x03 \x01(\x0b\x32\x0c.sysml.Value\x12\x1a\n\x04step\x18\x04 \x01(\x0b\x32\x0c.sysml.Value\"\xc0\x02\n\x0fRunSweepRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\x12\x19\n\x11subject_symbol_id\x18\x03 \x01(\t\x12\x1f\n\targuments\x18\x04 \x03(\x0b\x32\x0c.sysml.Value\x12\x43\n\x0fnamed_arguments\x18\x05 \x03(\x0b\x32*.sysml.RunSweepRequest.NamedArgumentsEntry\x12!\n\x06ranges\x18\x06 \x03(\x0b\x32\x11.sysml.SweepRange\x12\x0f\n\x07samples\x18\x07 \x01(\x03\x12\x0c\n\x04seed\x18\x08 \x01(\x04\x1a\x43\n\x13NamedArgumentsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value:\x02\x38\x01\"\xc8\x01\n\x08SweepRow\x12!\n\x06inputs\x18\x01 \x03(\x0b\x32\x11.sysml.CalcOutput\x12\"\n\x07outputs\x18\x02 \x03(\x0b\x32\x11.sysml.CalcOutput\x12 \n\x08verdicts\x18\x03 \x03(\x0b\x32\x0e.sysml.Verdict\x12\x16\n\x0e\x65lapsed_micros\x18\x04 \x01(\x03\x12\r\n\x05\x65rror\x18\x05 \x01(\t\x12,\n\x0e\x66\x61ilure_reason\x18\x06 \x01(\x0e\x32\x14.sysml.FailureReason\"\xed\x01\n\x10RunSweepResponse\x12\x1d\n\x04rows\x18\x01 \x03(\x0b\x32\x0f.sysml.SweepRow\x12\x12\n\nparameters\x18\x02 \x03(\t\x12\x0f\n\x07sampled\x18\x03 \x01(\x08\x12\x0c\n\x04seed\x18\x04 \x01(\x04\x12\r\n\x05\x65rror\x18\x05 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x06 \x03(\x0b\x32\x11.sysml.Diagnostic\x12,\n\x0e\x66\x61ilure_reason\x18\x07 \x01(\x0e\x32\x14.sysml.FailureReason\x12\"\n\tinstances\x18\x08 \x03(\x0b\x32\x0f.sysml.Instance\"n\n\x17RunDocumentQueryRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x10\n\x08query_id\x18\x02 \x01(\t\x12-\n\x08\x62indings\x18\x03 \x03(\x0b\x32\x1b.sysml.DocumentQueryBinding\"O\n\x14\x44ocumentQueryBinding\x12\x11\n\tparameter\x18\x01 \x01(\t\x12$\n\x06values\x18\x02 \x03(\x0b\x32\x14.sysml.DocumentValue\"\xd5\x01\n\rDocumentValue\x12\x14\n\nelement_id\x18\x01 \x01(\tH\x00\x12\x16\n\x0cstring_value\x18\x02 \x01(\tH\x00\x12\x13\n\tint_value\x18\x03 \x01(\x03H\x00\x12\x14\n\nreal_value\x18\x04 \x01(\x01H\x00\x12\x14\n\nbool_value\x18\x05 \x01(\x08H\x00\x12\x12\n\x08infinity\x18\x06 \x01(\x08H\x00\x12#\n\x08quantity\x18\x08 \x01(\x0b\x32\x0f.sysml.QuantityH\x00\x12\x14\n\x0c\x65lement_type\x18\x07 \x01(\tB\x06\n\x04kind\"#\n\x13\x44ocumentQueryColumn\x12\x0c\n\x04name\x18\x01 \x01(\t\"9\n\x11\x44ocumentQueryCell\x12$\n\x06values\x18\x01 \x03(\x0b\x32\x14.sysml.DocumentValue\"b\n\x10\x44ocumentQueryRow\x12%\n\x07\x65lement\x18\x01 \x01(\x0b\x32\x14.sysml.DocumentValue\x12\'\n\x05\x63\x65lls\x18\x02 \x03(\x0b\x32\x18.sysml.DocumentQueryCell\"n\n\x18RunDocumentQueryResponse\x12+\n\x07\x63olumns\x18\x01 \x03(\x0b\x32\x1a.sysml.DocumentQueryColumn\x12%\n\x04rows\x18\x02 \x03(\x0b\x32\x17.sysml.DocumentQueryRow\"@\n\x15RenderDocumentRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x13\n\x0b\x64ocument_id\x18\x02 \x01(\t\"*\n\x16RenderDocumentResponse\x12\x10\n\x08markdown\x18\x01 \x01(\t*\x93\x01\n\rFailureReason\x12\x1e\n\x1a\x46\x41ILURE_REASON_UNSPECIFIED\x10\x00\x12\x1d\n\x19\x46\x41ILURE_REASON_EVALUATION\x10\x01\x12\x1d\n\x19\x46\x41ILURE_REASON_WRONG_KIND\x10\x02\x12$\n FAILURE_REASON_AMBIGUOUS_SUBJECT\x10\x03*\x9d\x04\n\x0b\x45\x64itFailure\x12\x1c\n\x18\x45\x44IT_FAILURE_UNSPECIFIED\x10\x00\x12\x1e\n\x1a\x45\x44IT_FAILURE_NO_OPERATIONS\x10\x01\x12\x1f\n\x1b\x45\x44IT_FAILURE_UNKNOWN_TARGET\x10\x02\x12!\n\x1d\x45\x44IT_FAILURE_AMBIGUOUS_TARGET\x10\x03\x12\x1b\n\x17\x45\x44IT_FAILURE_NOT_VALUED\x10\x04\x12\x1e\n\x1a\x45\x44IT_FAILURE_INVALID_VALUE\x10\x05\x12\x1d\n\x19\x45\x44IT_FAILURE_INVALID_NAME\x10\x06\x12\x1a\n\x16\x45\x44IT_FAILURE_NOT_NAMED\x10\x07\x12\"\n\x1e\x45\x44IT_FAILURE_RENAME_REFERENCED\x10\x08\x12\"\n\x1e\x45\x44IT_FAILURE_OVERLAPPING_EDITS\x10\t\x12\x1f\n\x1b\x45\x44IT_FAILURE_RESULT_INVALID\x10\n\x12\x1e\n\x1a\x45\x44IT_FAILURE_OWNER_UNKNOWN\x10\x0b\x12$\n EDIT_FAILURE_OWNER_NOT_NAMESPACE\x10\x0c\x12\x1d\n\x19\x45\x44IT_FAILURE_ILLEGAL_KIND\x10\r\x12\"\n\x1e\x45\x44IT_FAILURE_MEMBER_NAME_TAKEN\x10\x0e\x12\"\n\x1e\x45\x44IT_FAILURE_DELETE_REFERENCED\x10\x0f*\x92\x01\n\x11PrimitiveOperator\x12\"\n\x1ePRIMITIVE_OPERATOR_UNSPECIFIED\x10\x00\x12\x1c\n\x18PRIMITIVE_OPERATOR_EQUAL\x10\x01\x12\x1e\n\x1aPRIMITIVE_OPERATOR_GREATER\x10\x02\x12\x1b\n\x17PRIMITIVE_OPERATOR_LESS\x10\x03*n\n\x11\x43ompositeOperator\x12\"\n\x1e\x43OMPOSITE_OPERATOR_UNSPECIFIED\x10\x00\x12\x1a\n\x16\x43OMPOSITE_OPERATOR_AND\x10\x01\x12\x19\n\x15\x43OMPOSITE_OPERATOR_OR\x10\x02\x32\xa4\x0b\n\x0cSysMLService\x12\x44\n\rGetServerInfo\x12\x18.sysml.ServerInfoRequest\x1a\x19.sysml.ServerInfoResponse\x12>\n\tParseFile\x12\x17.sysml.ParseFileRequest\x1a\x18.sysml.ParseFileResponse\x12G\n\x0cParseSources\x12\x1a.sysml.ParseSourcesRequest\x1a\x1b.sysml.ParseSourcesResponse\x12;\n\tGetSymbol\x12\x17.sysml.GetSymbolRequest\x1a\x15.sysml.SymbolResponse\x12G\n\x0eGetDiagnostics\x12\x19.sysml.DiagnosticsRequest\x1a\x1a.sysml.DiagnosticsResponse\x12;\n\x08\x45valuate\x12\x16.sysml.EvaluateRequest\x1a\x17.sysml.EvaluateResponse\x12\x44\n\x0bInstantiate\x12\x19.sysml.InstantiateRequest\x1a\x1a.sysml.InstantiateResponse\x12J\n\rExecuteAction\x12\x1b.sysml.ExecuteActionRequest\x1a\x1c.sysml.ExecuteActionResponse\x12G\n\x0c\x45xecuteState\x12\x1a.sysml.ExecuteStateRequest\x1a\x1b.sysml.ExecuteStateResponse\x12\x38\n\x07\x43onvert\x12\x15.sysml.ConvertRequest\x1a\x16.sysml.ConvertResponse\x12\x41\n\nApplyEdits\x12\x18.sysml.ApplyEditsRequest\x1a\x19.sysml.ApplyEditsResponse\x12S\n\x10VerifyConstraint\x12\x1e.sysml.VerifyConstraintRequest\x1a\x1f.sysml.VerifyConstraintResponse\x12V\n\x11VerifyRequirement\x12\x1f.sysml.VerifyRequirementRequest\x1a .sysml.VerifyRequirementResponse\x12Y\n\x12VerifySatisfaction\x12 .sysml.VerifySatisfactionRequest\x1a!.sysml.VerifySatisfactionResponse\x12G\n\x0c\x45valuateCalc\x12\x1a.sysml.EvaluateCalcRequest\x1a\x1b.sysml.EvaluateCalcResponse\x12\x44\n\x0bRunAnalysis\x12\x19.sysml.RunAnalysisRequest\x1a\x1a.sysml.RunAnalysisResponse\x12;\n\x08RunSweep\x12\x16.sysml.RunSweepRequest\x1a\x17.sysml.RunSweepResponse\x12\x32\n\x05Query\x12\x13.sysml.QueryRequest\x1a\x14.sysml.QueryResponse\x12S\n\x10RunDocumentQuery\x12\x1e.sysml.RunDocumentQueryRequest\x1a\x1f.sysml.RunDocumentQueryResponse\x12M\n\x0eRenderDocument\x12\x1c.sysml.RenderDocumentRequest\x1a\x1d.sysml.RenderDocumentResponseB*Z(github.com/Open-MBEE/OpenSysML/api/protob\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0bsysml.proto\x12\x05sysml\"\xe2\x01\n\x07Verdict\x12\x0c\n\x04kind\x18\x01 \x01(\t\x12\x12\n\nelement_id\x18\x02 \x01(\t\x12\x0f\n\x07\x65lement\x18\x03 \x01(\t\x12\r\n\x05holds\x18\x04 \x01(\x08\x12\x11\n\tcondition\x18\x05 \x01(\t\x12\x13\n\x0binstance_id\x18\x06 \x01(\x03\x12\x18\n\x10instance_type_id\x18\x07 \x01(\t\x12\r\n\x05\x65rror\x18\x08 \x01(\t\x12,\n\x0e\x66\x61ilure_reason\x18\t \x01(\x0e\x32\x14.sysml.FailureReason\x12\x16\n\x0erequirement_id\x18\n \x01(\t\"[\n\x17VerifyConstraintRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\x12\x19\n\x11subject_symbol_id\x18\x03 \x01(\t\"\x96\x01\n\x18VerifyConstraintResponse\x12\x1f\n\x07verdict\x18\x01 \x01(\x0b\x32\x0e.sysml.Verdict\x12\"\n\tinstances\x18\x02 \x03(\x0b\x32\x0f.sysml.Instance\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x04 \x03(\x0b\x32\x11.sysml.Diagnostic\"\\\n\x18VerifyRequirementRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\x12\x19\n\x11subject_symbol_id\x18\x03 \x01(\t\"m\n\x13VerificationVerdict\x12\x0f\n\x07\x63\x61se_id\x18\x01 \x01(\t\x12\x0c\n\x04kind\x18\x02 \x01(\t\x12\x0e\n\x06\x64\x65tail\x18\x03 \x01(\t\x12\x0f\n\x07subcase\x18\x04 \x01(\x08\x12\x16\n\x0erequirement_id\x18\x05 \x01(\t\"\xd2\x01\n\x19VerifyRequirementResponse\x12\x1f\n\x07verdict\x18\x01 \x01(\x0b\x32\x0e.sysml.Verdict\x12\"\n\tinstances\x18\x02 \x03(\x0b\x32\x0f.sysml.Instance\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x04 \x03(\x0b\x32\x11.sysml.Diagnostic\x12\x39\n\x15verification_verdicts\x18\x05 \x03(\x0b\x32\x1a.sysml.VerificationVerdict\"B\n\x19VerifySatisfactionRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\"\x82\x02\n\x1aVerifySatisfactionResponse\x12 \n\x08verdicts\x18\x01 \x03(\x0b\x32\x0e.sysml.Verdict\x12\"\n\tinstances\x18\x02 \x03(\x0b\x32\x0f.sysml.Instance\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x04 \x03(\x0b\x32\x11.sysml.Diagnostic\x12,\n\x0e\x66\x61ilure_reason\x18\x05 \x01(\x0e\x32\x14.sysml.FailureReason\x12\x39\n\x15verification_verdicts\x18\x06 \x03(\x0b\x32\x1a.sysml.VerificationVerdict\"]\n\x13\x45valuateCalcRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\x12\x1f\n\targuments\x18\x03 \x03(\x0b\x32\x0c.sysml.Value\"\xbd\x01\n\x14\x45valuateCalcResponse\x12\x1c\n\x06result\x18\x01 \x01(\x0b\x32\x0c.sysml.Value\x12\"\n\x07outputs\x18\x02 \x03(\x0b\x32\x11.sysml.CalcOutput\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x04 \x03(\x0b\x32\x11.sysml.Diagnostic\x12,\n\x0e\x66\x61ilure_reason\x18\x05 \x01(\x0e\x32\x14.sysml.FailureReason\"7\n\nCalcOutput\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value\"\x96\x02\n\x12RunAnalysisRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\x12\x19\n\x11subject_symbol_id\x18\x03 \x01(\t\x12\x1f\n\targuments\x18\x04 \x03(\x0b\x32\x0c.sysml.Value\x12\x46\n\x0fnamed_arguments\x18\x05 \x03(\x0b\x32-.sysml.RunAnalysisRequest.NamedArgumentsEntry\x12\x10\n\x08schedule\x18\x06 \x01(\t\x1a\x43\n\x13NamedArgumentsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value:\x02\x38\x01\"\x9f\x02\n\x13RunAnalysisResponse\x12\"\n\x07outputs\x18\x01 \x03(\x0b\x32\x11.sysml.CalcOutput\x12 \n\x08verdicts\x18\x02 \x03(\x0b\x32\x0e.sysml.Verdict\x12\"\n\tinstances\x18\x03 \x03(\x0b\x32\x0f.sysml.Instance\x12\r\n\x05\x65rror\x18\x04 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x05 \x03(\x0b\x32\x11.sysml.Diagnostic\x12,\n\x0e\x66\x61ilure_reason\x18\x06 \x01(\x0e\x32\x14.sysml.FailureReason\x12\x39\n\x15verification_verdicts\x18\x07 \x03(\x0b\x32\x1a.sysml.VerificationVerdict\"\x8c\x01\n\x10ParseFileRequest\x12\x13\n\tfile_path\x18\x01 \x01(\tH\x00\x12\x11\n\x07\x63ontent\x18\x02 \x01(\tH\x00\x12\x18\n\x0c\x63ontent_hash\x18\x03 \x01(\tB\x02\x18\x01\x12\x10\n\x08language\x18\x04 \x01(\t\x12\x1a\n\x12strict_conformance\x18\x05 \x01(\x08\x42\x08\n\x06source\"b\n\x0eSourceDocument\x12\x13\n\tfile_path\x18\x01 \x01(\tH\x00\x12\x11\n\x07\x63ontent\x18\x02 \x01(\tH\x00\x12\x10\n\x08language\x18\x03 \x01(\t\x12\x0c\n\x04name\x18\x04 \x01(\tB\x08\n\x06source\"[\n\x13ParseSourcesRequest\x12(\n\tdocuments\x18\x01 \x03(\x0b\x32\x15.sysml.SourceDocument\x12\x1a\n\x12strict_conformance\x18\x02 \x01(\x08\"\x83\x01\n\x14ParseSourcesResponse\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12 \n\x05roots\x18\x02 \x03(\x0b\x32\x11.sysml.SymbolInfo\x12&\n\x0b\x64iagnostics\x18\x03 \x03(\x0b\x32\x11.sysml.Diagnostic\x12\r\n\x05\x65rror\x18\x04 \x01(\t\"\x7f\n\x11ParseFileResponse\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x1f\n\x04root\x18\x02 \x01(\x0b\x32\x11.sysml.SymbolInfo\x12&\n\x0b\x64iagnostics\x18\x03 \x03(\x0b\x32\x11.sysml.Diagnostic\x12\r\n\x05\x65rror\x18\x04 \x01(\t\"9\n\x10GetSymbolRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\"B\n\x0eSymbolResponse\x12!\n\x06symbol\x18\x01 \x01(\x0b\x32\x11.sysml.SymbolInfo\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"(\n\x12\x44iagnosticsRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\"L\n\x13\x44iagnosticsResponse\x12&\n\x0b\x64iagnostics\x18\x01 \x03(\x0b\x32\x11.sysml.Diagnostic\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"o\n\x0f\x45valuateRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x12\n\nexpression\x18\x02 \x01(\t\x12\x19\n\x11\x63ontext_symbol_id\x18\x03 \x01(\t\x12\x19\n\x11subject_symbol_id\x18\x04 \x01(\t\"g\n\x10\x45valuateResponse\x12\x1c\n\x06result\x18\x01 \x01(\x0b\x32\x0c.sysml.Value\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x03 \x03(\x0b\x32\x11.sysml.Diagnostic\"\xc2\x01\n\x08Instance\x12\n\n\x02id\x18\x01 \x01(\x03\x12\x16\n\x0etype_symbol_id\x18\x02 \x01(\t\x12:\n\x0e\x66\x65\x61ture_values\x18\x04 \x03(\x0b\x32\".sysml.Instance.FeatureValuesEntry\x1aI\n\x12\x46\x65\x61tureValuesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\"\n\x05value\x18\x02 \x01(\x0b\x32\x13.sysml.FeatureValue:\x02\x38\x01J\x04\x08\x03\x10\x04R\x05slots\"\x84\x01\n\x0c\x46\x65\x61tureValue\x12\x14\n\x0c\x66\x65\x61ture_name\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value\x12\x1c\n\x06values\x18\x03 \x03(\x0b\x32\x0c.sysml.Value\x12\x14\n\x0cmaterialized\x18\x04 \x01(\x08\x12\r\n\x05\x65rror\x18\x05 \x01(\t\";\n\x12InstantiateRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\"\x93\x01\n\x13InstantiateResponse\x12!\n\x08instance\x18\x01 \x01(\x0b\x32\x0f.sysml.Instance\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x03 \x03(\x0b\x32\x11.sysml.Diagnostic\x12\"\n\tinstances\x18\x04 \x03(\x0b\x32\x0f.sysml.Instance\"\xcc\x01\n\x14\x45xecuteActionRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x18\n\x10\x61\x63tion_symbol_id\x18\x02 \x01(\t\x12\x37\n\x06inputs\x18\x03 \x03(\x0b\x32\'.sysml.ExecuteActionRequest.InputsEntry\x12\x10\n\x08schedule\x18\x04 \x01(\t\x1a;\n\x0bInputsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value:\x02\x38\x01\"\xc8\x01\n\x15\x45xecuteActionResponse\x12:\n\x07outputs\x18\x01 \x03(\x0b\x32).sysml.ExecuteActionResponse.OutputsEntry\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x03 \x03(\x0b\x32\x11.sysml.Diagnostic\x1a<\n\x0cOutputsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value:\x02\x38\x01\"l\n\x13\x45xecuteStateRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x1f\n\x17state_machine_symbol_id\x18\x02 \x01(\t\x12\x0e\n\x06\x65vents\x18\x03 \x03(\t\x12\x10\n\x08schedule\x18\x04 \x01(\t\"\xee\x01\n\x14\x45xecuteStateResponse\x12\x16\n\x0estates_visited\x18\x01 \x03(\t\x12\x44\n\rfinal_context\x18\x02 \x03(\x0b\x32-.sysml.ExecuteStateResponse.FinalContextEntry\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x04 \x03(\x0b\x32\x11.sysml.Diagnostic\x1a\x41\n\x11\x46inalContextEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value:\x02\x38\x01\"\xa0\x01\n\x0e\x43onvertRequest\x12\x13\n\tfile_path\x18\x01 \x01(\tH\x00\x12\x11\n\x07\x63ontent\x18\x02 \x01(\tH\x00\x12\x14\n\nmodel_hash\x18\x06 \x01(\tH\x00\x12\x13\n\x0b\x66rom_format\x18\x03 \x01(\t\x12\x11\n\tto_format\x18\x04 \x01(\t\x12\x1e\n\x16tolerate_syntax_errors\x18\x05 \x01(\x08\x42\x08\n\x06source\"\xb4\x01\n\x0f\x43onvertResponse\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\x12\x13\n\x0b\x66rom_format\x18\x02 \x01(\t\x12\x11\n\tto_format\x18\x03 \x01(\t\x12\r\n\x05\x65rror\x18\x04 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x05 \x03(\x0b\x32\x11.sysml.Diagnostic\x12\x14\n\x0c\x65xperimental\x18\x06 \x01(\x08\x12\x1b\n\x13\x65xperimental_notice\x18\x07 \x01(\t\"Q\n\x11\x41pplyEditsRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12(\n\noperations\x18\x02 \x03(\x0b\x32\x14.sysml.EditOperation\"\xbc\x01\n\rEditOperation\x12(\n\tset_value\x18\x01 \x01(\x0b\x32\x13.sysml.SetValueEditH\x00\x12#\n\x06rename\x18\x02 \x01(\x0b\x32\x11.sysml.RenameEditH\x00\x12*\n\nadd_member\x18\x03 \x01(\x0b\x32\x14.sysml.AddMemberEditH\x00\x12#\n\x06\x64\x65lete\x18\x04 \x01(\x0b\x32\x11.sysml.DeleteEditH\x00\x42\x0b\n\toperation\"\x82\x01\n\rAddMemberEdit\x12\r\n\x05owner\x18\x01 \x01(\t\x12\x0c\n\x04kind\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x0c\n\x04type\x18\x04 \x01(\t\x12\x14\n\x0cmultiplicity\x18\x05 \x01(\t\x12\r\n\x05value\x18\x06 \x01(\t\x12\x13\n\x0bspecializes\x18\x07 \x03(\t\"-\n\nDeleteEdit\x12\x0e\n\x06target\x18\x01 \x01(\t\x12\x0f\n\x07\x63\x61scade\x18\x02 \x01(\x08\"-\n\x0cSetValueEdit\x12\x0e\n\x06target\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\".\n\nRenameEdit\x12\x0e\n\x06target\x18\x01 \x01(\t\x12\x10\n\x08new_name\x18\x02 \x01(\t\"\xc2\x01\n\x12\x41pplyEditsResponse\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\x12#\n\x07\x61pplied\x18\x02 \x03(\x0b\x32\x12.sysml.AppliedEdit\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12#\n\x07\x66\x61ilure\x18\x04 \x01(\x0e\x32\x12.sysml.EditFailure\x12&\n\x0b\x64iagnostics\x18\x05 \x03(\x0b\x32\x11.sysml.Diagnostic\x12\x1a\n\x12referring_elements\x18\x06 \x03(\t\"z\n\x0b\x41ppliedEdit\x12\x17\n\x0foperation_index\x18\x01 \x01(\x05\x12\x0e\n\x06target\x18\x02 \x01(\t\x12\x0e\n\x06offset\x18\x03 \x01(\x05\x12\x0e\n\x06length\x18\x04 \x01(\x05\x12\x10\n\x08old_text\x18\x05 \x01(\t\x12\x10\n\x08new_text\x18\x06 \x01(\t\"\xfd\x02\n\nSymbolInfo\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0c\n\x04kind\x18\x03 \x01(\t\x12\x31\n\x08metadata\x18\x04 \x03(\x0b\x32\x1f.sysml.SymbolInfo.MetadataEntry\x12\x11\n\tchild_ids\x18\x05 \x03(\t\x12(\n\nattributes\x18\x06 \x03(\x0b\x32\x14.sysml.AttributeInfo\x12\"\n\ttype_info\x18\x07 \x01(\x0b\x32\x0f.sysml.TypeInfo\x12-\n\x0cmultiplicity\x18\x08 \x01(\x0b\x32\x17.sysml.MultiplicityInfo\x12.\n\x0fspecializations\x18\t \x03(\x0b\x32\x15.sysml.Specialization\x12#\n\x1bwithheld_library_attributes\x18\n \x01(\x05\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"X\n\x0eSpecialization\x12\x0c\n\x04kind\x18\x01 \x01(\t\x12\x10\n\x08\x64\x65\x63lared\x18\x02 \x01(\t\x12\x11\n\ttarget_id\x18\x03 \x01(\t\x12\x13\n\x0btarget_kind\x18\x04 \x01(\t\"\x95\x01\n\x08TypeInfo\x12\x10\n\x08\x64\x65\x63lared\x18\x01 \x01(\t\x12\x13\n\x0bresolved_id\x18\x02 \x01(\t\x12\x15\n\rresolved_kind\x18\x03 \x01(\t\x12\x11\n\tprimitive\x18\x04 \x01(\t\x12\x18\n\x10primitive_source\x18\x05 \x01(\t\x12\x10\n\x08quantity\x18\x06 \x01(\x08\x12\x0c\n\x04unit\x18\x07 \x01(\t\"0\n\x10MultiplicityInfo\x12\r\n\x05lower\x18\x01 \x01(\t\x12\r\n\x05upper\x18\x02 \x01(\t\"V\n\rAttributeInfo\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x1b\n\x05value\x18\x03 \x01(\x0b\x32\x0c.sysml.Value\x12\x0c\n\x04unit\x18\x04 \x01(\t\"\xed\x04\n\x05Value\x12\x13\n\tint_value\x18\x01 \x01(\x03H\x00\x12\x14\n\nreal_value\x18\x02 \x01(\x01H\x00\x12\x14\n\nbool_value\x18\x03 \x01(\x08H\x00\x12\x16\n\x0cstring_value\x18\x04 \x01(\tH\x00\x12\x15\n\x0binstance_id\x18\x05 \x01(\x03H\x00\x12(\n\x08sequence\x18\x06 \x01(\x0b\x32\x14.sysml.ValueSequenceH\x00\x12\x0e\n\x04null\x18\x07 \x01(\tH\x00\x12#\n\x08quantity\x18\x08 \x01(\x0b\x32\x0f.sysml.QuantityH\x00\x12*\n\x0c\x65num_literal\x18\t \x01(\x0b\x32\x12.sysml.EnumLiteralH\x00\x12\x0f\n\x05unset\x18\n \x01(\x08H\x00\x12!\n\x07\x63omplex\x18\x0b \x01(\x0b\x32\x0e.sysml.ComplexH\x00\x12\x1d\n\x05\x61rray\x18\x0c \x01(\x0b\x32\x0c.sysml.ArrayH\x00\x12\x1f\n\x06vector\x18\r \x01(\x0b\x32\r.sysml.VectorH\x00\x12\x30\n\x0fvector_quantity\x18\x0e \x01(\x0b\x32\x15.sysml.VectorQuantityH\x00\x12\x30\n\x0fmeasurement_ref\x18\x0f \x01(\x0b\x32\x15.sysml.MeasurementRefH\x00\x12\x12\n\x08infinity\x18\x10 \x01(\x08H\x00\x12#\n\x08\x66unction\x18\x11 \x01(\x0b\x32\x0f.sysml.FunctionH\x00\x12\x1e\n\x03set\x18\x12 \x01(\x0b\x32\x0f.sysml.ValueSetH\x00\x12\x30\n\x0ftensor_quantity\x18\x13 \x01(\x0b\x32\x15.sysml.TensorQuantityH\x00\x42\x06\n\x04kind\",\n\x08\x46unction\x12\x0f\n\x07\x63\x61lc_id\x18\x01 \x01(\t\x12\x0f\n\x07self_id\x18\x02 \x01(\x03\"*\n\x08ValueSet\x12\x1e\n\x08\x65lements\x18\x01 \x03(\x0b\x32\x0c.sysml.Value\"I\n\x0eTensorQuantity\x12\x12\n\ndimensions\x18\x01 \x03(\x03\x12#\n\ncomponents\x18\x02 \x03(\x0b\x32\x0f.sysml.Quantity\";\n\x05\x41rray\x12\x12\n\ndimensions\x18\x01 \x03(\x03\x12\x1e\n\x08\x65lements\x18\x02 \x03(\x0b\x32\x0c.sysml.Value\"*\n\x06Vector\x12 \n\ncomponents\x18\x01 \x03(\x0b\x32\x0c.sysml.Value\"5\n\x0eVectorQuantity\x12#\n\ncomponents\x18\x01 \x03(\x0b\x32\x0f.sysml.Quantity\"*\n\x07\x43omplex\x12\x0c\n\x04real\x18\x01 \x01(\x01\x12\x11\n\timaginary\x18\x02 \x01(\x01\"G\n\x0b\x45numLiteral\x12\x12\n\nliteral_id\x18\x01 \x01(\t\x12\x16\n\x0e\x65numeration_id\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\"/\n\rValueSequence\x12\x1e\n\x08\x65lements\x18\x01 \x03(\x0b\x32\x0c.sysml.Value\"|\n\x08Quantity\x12\x17\n\rint_magnitude\x18\x01 \x01(\x03H\x00\x12\x18\n\x0ereal_magnitude\x18\x02 \x01(\x01H\x00\x12\x0c\n\x04unit\x18\x03 \x01(\t\x12\"\n\tunit_term\x18\x04 \x01(\x0b\x32\x0f.sysml.UnitTermB\x0b\n\tmagnitude\"S\n\x0eMeasurementRef\x12\x0c\n\x04unit\x18\x01 \x01(\t\x12\"\n\tunit_term\x18\x02 \x01(\x0b\x32\x0f.sysml.UnitTerm\x12\x0f\n\x07unit_id\x18\x03 \x01(\t\"T\n\x08UnitTerm\x12\x11\n\tscale_num\x18\x01 \x01(\x01\x12\x11\n\tscale_den\x18\x02 \x01(\x01\x12\"\n\x07\x66\x61\x63tors\x18\x03 \x03(\x0b\x32\x11.sysml.UnitFactor\"/\n\nUnitFactor\x12\x0f\n\x07unit_id\x18\x01 \x01(\t\x12\x10\n\x08\x65xponent\x18\x02 \x01(\x01\"X\n\nDiagnostic\x12\x10\n\x08severity\x18\x01 \x01(\t\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x19\n\x04span\x18\x03 \x01(\x0b\x32\x0b.sysml.Span\x12\x0c\n\x04\x63ode\x18\x04 \x01(\t\"^\n\x04Span\x12\x0c\n\x04\x66ile\x18\x01 \x01(\t\x12\x12\n\nstart_line\x18\x02 \x01(\x05\x12\x11\n\tstart_col\x18\x03 \x01(\x05\x12\x10\n\x08\x65nd_line\x18\x04 \x01(\x05\x12\x0f\n\x07\x65nd_col\x18\x05 \x01(\x05\"\x13\n\x11ServerInfoRequest\";\n\x12ServerInfoResponse\x12\x0f\n\x07version\x18\x01 \x01(\t\x12\x14\n\x0c\x63\x61pabilities\x18\x02 \x03(\t\"S\n\x0cQueryRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x1b\n\x05query\x18\x02 \x01(\x0b\x32\x0c.sysml.Query\x12\x12\n\noslc_query\x18\x03 \x01(\t\"<\n\rQueryResponse\x12+\n\x08\x65lements\x18\x01 \x03(\x0b\x32\x19.sysml.QueryResultElement\"H\n\x05Query\x12\r\n\x05scope\x18\x01 \x03(\t\x12\x0e\n\x06select\x18\x02 \x03(\t\x12 \n\x05where\x18\x03 \x01(\x0b\x32\x11.sysml.Constraint\"|\n\nConstraint\x12/\n\tprimitive\x18\x01 \x01(\x0b\x32\x1a.sysml.PrimitiveConstraintH\x00\x12/\n\tcomposite\x18\x02 \x01(\x0b\x32\x1a.sysml.CompositeConstraintH\x00\x42\x0c\n\nconstraint\"s\n\x13PrimitiveConstraint\x12\x0f\n\x07inverse\x18\x01 \x01(\x08\x12\x10\n\x08property\x18\x02 \x01(\t\x12*\n\x08operator\x18\x03 \x01(\x0e\x32\x18.sysml.PrimitiveOperator\x12\r\n\x05value\x18\x04 \x03(\t\"h\n\x13\x43ompositeConstraint\x12*\n\x08operator\x18\x01 \x01(\x0e\x32\x18.sysml.CompositeOperator\x12%\n\nconstraint\x18\x02 \x03(\x0b\x32\x11.sysml.Constraint\"\xa0\x01\n\x12QueryResultElement\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12=\n\nproperties\x18\x03 \x03(\x0b\x32).sysml.QueryResultElement.PropertiesEntry\x1a\x31\n\x0fPropertiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"s\n\nSweepRange\x12\x11\n\tparameter\x18\x01 \x01(\t\x12\x1b\n\x05start\x18\x02 \x01(\x0b\x32\x0c.sysml.Value\x12\x19\n\x03\x65nd\x18\x03 \x01(\x0b\x32\x0c.sysml.Value\x12\x1a\n\x04step\x18\x04 \x01(\x0b\x32\x0c.sysml.Value\"\xc0\x02\n\x0fRunSweepRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x11\n\tsymbol_id\x18\x02 \x01(\t\x12\x19\n\x11subject_symbol_id\x18\x03 \x01(\t\x12\x1f\n\targuments\x18\x04 \x03(\x0b\x32\x0c.sysml.Value\x12\x43\n\x0fnamed_arguments\x18\x05 \x03(\x0b\x32*.sysml.RunSweepRequest.NamedArgumentsEntry\x12!\n\x06ranges\x18\x06 \x03(\x0b\x32\x11.sysml.SweepRange\x12\x0f\n\x07samples\x18\x07 \x01(\x03\x12\x0c\n\x04seed\x18\x08 \x01(\x04\x1a\x43\n\x13NamedArgumentsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1b\n\x05value\x18\x02 \x01(\x0b\x32\x0c.sysml.Value:\x02\x38\x01\"\xc8\x01\n\x08SweepRow\x12!\n\x06inputs\x18\x01 \x03(\x0b\x32\x11.sysml.CalcOutput\x12\"\n\x07outputs\x18\x02 \x03(\x0b\x32\x11.sysml.CalcOutput\x12 \n\x08verdicts\x18\x03 \x03(\x0b\x32\x0e.sysml.Verdict\x12\x16\n\x0e\x65lapsed_micros\x18\x04 \x01(\x03\x12\r\n\x05\x65rror\x18\x05 \x01(\t\x12,\n\x0e\x66\x61ilure_reason\x18\x06 \x01(\x0e\x32\x14.sysml.FailureReason\"\xed\x01\n\x10RunSweepResponse\x12\x1d\n\x04rows\x18\x01 \x03(\x0b\x32\x0f.sysml.SweepRow\x12\x12\n\nparameters\x18\x02 \x03(\t\x12\x0f\n\x07sampled\x18\x03 \x01(\x08\x12\x0c\n\x04seed\x18\x04 \x01(\x04\x12\r\n\x05\x65rror\x18\x05 \x01(\t\x12&\n\x0b\x64iagnostics\x18\x06 \x03(\x0b\x32\x11.sysml.Diagnostic\x12,\n\x0e\x66\x61ilure_reason\x18\x07 \x01(\x0e\x32\x14.sysml.FailureReason\x12\"\n\tinstances\x18\x08 \x03(\x0b\x32\x0f.sysml.Instance\"n\n\x17RunDocumentQueryRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x10\n\x08query_id\x18\x02 \x01(\t\x12-\n\x08\x62indings\x18\x03 \x03(\x0b\x32\x1b.sysml.DocumentQueryBinding\"O\n\x14\x44ocumentQueryBinding\x12\x11\n\tparameter\x18\x01 \x01(\t\x12$\n\x06values\x18\x02 \x03(\x0b\x32\x14.sysml.DocumentValue\"\xd5\x01\n\rDocumentValue\x12\x14\n\nelement_id\x18\x01 \x01(\tH\x00\x12\x16\n\x0cstring_value\x18\x02 \x01(\tH\x00\x12\x13\n\tint_value\x18\x03 \x01(\x03H\x00\x12\x14\n\nreal_value\x18\x04 \x01(\x01H\x00\x12\x14\n\nbool_value\x18\x05 \x01(\x08H\x00\x12\x12\n\x08infinity\x18\x06 \x01(\x08H\x00\x12#\n\x08quantity\x18\x08 \x01(\x0b\x32\x0f.sysml.QuantityH\x00\x12\x14\n\x0c\x65lement_type\x18\x07 \x01(\tB\x06\n\x04kind\"#\n\x13\x44ocumentQueryColumn\x12\x0c\n\x04name\x18\x01 \x01(\t\"9\n\x11\x44ocumentQueryCell\x12$\n\x06values\x18\x01 \x03(\x0b\x32\x14.sysml.DocumentValue\"b\n\x10\x44ocumentQueryRow\x12%\n\x07\x65lement\x18\x01 \x01(\x0b\x32\x14.sysml.DocumentValue\x12\'\n\x05\x63\x65lls\x18\x02 \x03(\x0b\x32\x18.sysml.DocumentQueryCell\"n\n\x18RunDocumentQueryResponse\x12+\n\x07\x63olumns\x18\x01 \x03(\x0b\x32\x1a.sysml.DocumentQueryColumn\x12%\n\x04rows\x18\x02 \x03(\x0b\x32\x17.sysml.DocumentQueryRow\"@\n\x15RenderDocumentRequest\x12\x12\n\nmodel_hash\x18\x01 \x01(\t\x12\x13\n\x0b\x64ocument_id\x18\x02 \x01(\t\"*\n\x16RenderDocumentResponse\x12\x10\n\x08markdown\x18\x01 \x01(\t*\x93\x01\n\rFailureReason\x12\x1e\n\x1a\x46\x41ILURE_REASON_UNSPECIFIED\x10\x00\x12\x1d\n\x19\x46\x41ILURE_REASON_EVALUATION\x10\x01\x12\x1d\n\x19\x46\x41ILURE_REASON_WRONG_KIND\x10\x02\x12$\n FAILURE_REASON_AMBIGUOUS_SUBJECT\x10\x03*\x9d\x04\n\x0b\x45\x64itFailure\x12\x1c\n\x18\x45\x44IT_FAILURE_UNSPECIFIED\x10\x00\x12\x1e\n\x1a\x45\x44IT_FAILURE_NO_OPERATIONS\x10\x01\x12\x1f\n\x1b\x45\x44IT_FAILURE_UNKNOWN_TARGET\x10\x02\x12!\n\x1d\x45\x44IT_FAILURE_AMBIGUOUS_TARGET\x10\x03\x12\x1b\n\x17\x45\x44IT_FAILURE_NOT_VALUED\x10\x04\x12\x1e\n\x1a\x45\x44IT_FAILURE_INVALID_VALUE\x10\x05\x12\x1d\n\x19\x45\x44IT_FAILURE_INVALID_NAME\x10\x06\x12\x1a\n\x16\x45\x44IT_FAILURE_NOT_NAMED\x10\x07\x12\"\n\x1e\x45\x44IT_FAILURE_RENAME_REFERENCED\x10\x08\x12\"\n\x1e\x45\x44IT_FAILURE_OVERLAPPING_EDITS\x10\t\x12\x1f\n\x1b\x45\x44IT_FAILURE_RESULT_INVALID\x10\n\x12\x1e\n\x1a\x45\x44IT_FAILURE_OWNER_UNKNOWN\x10\x0b\x12$\n EDIT_FAILURE_OWNER_NOT_NAMESPACE\x10\x0c\x12\x1d\n\x19\x45\x44IT_FAILURE_ILLEGAL_KIND\x10\r\x12\"\n\x1e\x45\x44IT_FAILURE_MEMBER_NAME_TAKEN\x10\x0e\x12\"\n\x1e\x45\x44IT_FAILURE_DELETE_REFERENCED\x10\x0f*\x92\x01\n\x11PrimitiveOperator\x12\"\n\x1ePRIMITIVE_OPERATOR_UNSPECIFIED\x10\x00\x12\x1c\n\x18PRIMITIVE_OPERATOR_EQUAL\x10\x01\x12\x1e\n\x1aPRIMITIVE_OPERATOR_GREATER\x10\x02\x12\x1b\n\x17PRIMITIVE_OPERATOR_LESS\x10\x03*n\n\x11\x43ompositeOperator\x12\"\n\x1e\x43OMPOSITE_OPERATOR_UNSPECIFIED\x10\x00\x12\x1a\n\x16\x43OMPOSITE_OPERATOR_AND\x10\x01\x12\x19\n\x15\x43OMPOSITE_OPERATOR_OR\x10\x02\x32\xa4\x0b\n\x0cSysMLService\x12\x44\n\rGetServerInfo\x12\x18.sysml.ServerInfoRequest\x1a\x19.sysml.ServerInfoResponse\x12>\n\tParseFile\x12\x17.sysml.ParseFileRequest\x1a\x18.sysml.ParseFileResponse\x12G\n\x0cParseSources\x12\x1a.sysml.ParseSourcesRequest\x1a\x1b.sysml.ParseSourcesResponse\x12;\n\tGetSymbol\x12\x17.sysml.GetSymbolRequest\x1a\x15.sysml.SymbolResponse\x12G\n\x0eGetDiagnostics\x12\x19.sysml.DiagnosticsRequest\x1a\x1a.sysml.DiagnosticsResponse\x12;\n\x08\x45valuate\x12\x16.sysml.EvaluateRequest\x1a\x17.sysml.EvaluateResponse\x12\x44\n\x0bInstantiate\x12\x19.sysml.InstantiateRequest\x1a\x1a.sysml.InstantiateResponse\x12J\n\rExecuteAction\x12\x1b.sysml.ExecuteActionRequest\x1a\x1c.sysml.ExecuteActionResponse\x12G\n\x0c\x45xecuteState\x12\x1a.sysml.ExecuteStateRequest\x1a\x1b.sysml.ExecuteStateResponse\x12\x38\n\x07\x43onvert\x12\x15.sysml.ConvertRequest\x1a\x16.sysml.ConvertResponse\x12\x41\n\nApplyEdits\x12\x18.sysml.ApplyEditsRequest\x1a\x19.sysml.ApplyEditsResponse\x12S\n\x10VerifyConstraint\x12\x1e.sysml.VerifyConstraintRequest\x1a\x1f.sysml.VerifyConstraintResponse\x12V\n\x11VerifyRequirement\x12\x1f.sysml.VerifyRequirementRequest\x1a .sysml.VerifyRequirementResponse\x12Y\n\x12VerifySatisfaction\x12 .sysml.VerifySatisfactionRequest\x1a!.sysml.VerifySatisfactionResponse\x12G\n\x0c\x45valuateCalc\x12\x1a.sysml.EvaluateCalcRequest\x1a\x1b.sysml.EvaluateCalcResponse\x12\x44\n\x0bRunAnalysis\x12\x19.sysml.RunAnalysisRequest\x1a\x1a.sysml.RunAnalysisResponse\x12;\n\x08RunSweep\x12\x16.sysml.RunSweepRequest\x1a\x17.sysml.RunSweepResponse\x12\x32\n\x05Query\x12\x13.sysml.QueryRequest\x1a\x14.sysml.QueryResponse\x12S\n\x10RunDocumentQuery\x12\x1e.sysml.RunDocumentQueryRequest\x1a\x1f.sysml.RunDocumentQueryResponse\x12M\n\x0eRenderDocument\x12\x1c.sysml.RenderDocumentRequest\x1a\x1d.sysml.RenderDocumentResponseB*Z(github.com/Open-MBEE/OpenSysML/api/protob\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -50,14 +50,14 @@ _globals['_QUERYRESULTELEMENT_PROPERTIESENTRY']._serialized_options = b'8\001' _globals['_RUNSWEEPREQUEST_NAMEDARGUMENTSENTRY']._loaded_options = None _globals['_RUNSWEEPREQUEST_NAMEDARGUMENTSENTRY']._serialized_options = b'8\001' - _globals['_FAILUREREASON']._serialized_start=10479 - _globals['_FAILUREREASON']._serialized_end=10626 - _globals['_EDITFAILURE']._serialized_start=10629 - _globals['_EDITFAILURE']._serialized_end=11170 - _globals['_PRIMITIVEOPERATOR']._serialized_start=11173 - _globals['_PRIMITIVEOPERATOR']._serialized_end=11319 - _globals['_COMPOSITEOPERATOR']._serialized_start=11321 - _globals['_COMPOSITEOPERATOR']._serialized_end=11431 + _globals['_FAILUREREASON']._serialized_start=10680 + _globals['_FAILUREREASON']._serialized_end=10827 + _globals['_EDITFAILURE']._serialized_start=10830 + _globals['_EDITFAILURE']._serialized_end=11371 + _globals['_PRIMITIVEOPERATOR']._serialized_start=11374 + _globals['_PRIMITIVEOPERATOR']._serialized_end=11520 + _globals['_COMPOSITEOPERATOR']._serialized_start=11522 + _globals['_COMPOSITEOPERATOR']._serialized_end=11632 _globals['_VERDICT']._serialized_start=23 _globals['_VERDICT']._serialized_end=249 _globals['_VERIFYCONSTRAINTREQUEST']._serialized_start=251 @@ -165,81 +165,85 @@ _globals['_ATTRIBUTEINFO']._serialized_start=6419 _globals['_ATTRIBUTEINFO']._serialized_end=6505 _globals['_VALUE']._serialized_start=6508 - _globals['_VALUE']._serialized_end=7047 - _globals['_FUNCTION']._serialized_start=7049 - _globals['_FUNCTION']._serialized_end=7093 - _globals['_ARRAY']._serialized_start=7095 - _globals['_ARRAY']._serialized_end=7154 - _globals['_VECTOR']._serialized_start=7156 - _globals['_VECTOR']._serialized_end=7198 - _globals['_VECTORQUANTITY']._serialized_start=7200 - _globals['_VECTORQUANTITY']._serialized_end=7253 - _globals['_COMPLEX']._serialized_start=7255 - _globals['_COMPLEX']._serialized_end=7297 - _globals['_ENUMLITERAL']._serialized_start=7299 - _globals['_ENUMLITERAL']._serialized_end=7370 - _globals['_VALUESEQUENCE']._serialized_start=7372 - _globals['_VALUESEQUENCE']._serialized_end=7419 - _globals['_QUANTITY']._serialized_start=7421 - _globals['_QUANTITY']._serialized_end=7545 - _globals['_MEASUREMENTREF']._serialized_start=7547 - _globals['_MEASUREMENTREF']._serialized_end=7630 - _globals['_UNITTERM']._serialized_start=7632 - _globals['_UNITTERM']._serialized_end=7716 - _globals['_UNITFACTOR']._serialized_start=7718 - _globals['_UNITFACTOR']._serialized_end=7765 - _globals['_DIAGNOSTIC']._serialized_start=7767 - _globals['_DIAGNOSTIC']._serialized_end=7855 - _globals['_SPAN']._serialized_start=7857 - _globals['_SPAN']._serialized_end=7951 - _globals['_SERVERINFOREQUEST']._serialized_start=7953 - _globals['_SERVERINFOREQUEST']._serialized_end=7972 - _globals['_SERVERINFORESPONSE']._serialized_start=7974 - _globals['_SERVERINFORESPONSE']._serialized_end=8033 - _globals['_QUERYREQUEST']._serialized_start=8035 - _globals['_QUERYREQUEST']._serialized_end=8118 - _globals['_QUERYRESPONSE']._serialized_start=8120 - _globals['_QUERYRESPONSE']._serialized_end=8180 - _globals['_QUERY']._serialized_start=8182 - _globals['_QUERY']._serialized_end=8254 - _globals['_CONSTRAINT']._serialized_start=8256 - _globals['_CONSTRAINT']._serialized_end=8380 - _globals['_PRIMITIVECONSTRAINT']._serialized_start=8382 - _globals['_PRIMITIVECONSTRAINT']._serialized_end=8497 - _globals['_COMPOSITECONSTRAINT']._serialized_start=8499 - _globals['_COMPOSITECONSTRAINT']._serialized_end=8603 - _globals['_QUERYRESULTELEMENT']._serialized_start=8606 - _globals['_QUERYRESULTELEMENT']._serialized_end=8766 - _globals['_QUERYRESULTELEMENT_PROPERTIESENTRY']._serialized_start=8717 - _globals['_QUERYRESULTELEMENT_PROPERTIESENTRY']._serialized_end=8766 - _globals['_SWEEPRANGE']._serialized_start=8768 - _globals['_SWEEPRANGE']._serialized_end=8883 - _globals['_RUNSWEEPREQUEST']._serialized_start=8886 - _globals['_RUNSWEEPREQUEST']._serialized_end=9206 + _globals['_VALUE']._serialized_end=7129 + _globals['_FUNCTION']._serialized_start=7131 + _globals['_FUNCTION']._serialized_end=7175 + _globals['_VALUESET']._serialized_start=7177 + _globals['_VALUESET']._serialized_end=7219 + _globals['_TENSORQUANTITY']._serialized_start=7221 + _globals['_TENSORQUANTITY']._serialized_end=7294 + _globals['_ARRAY']._serialized_start=7296 + _globals['_ARRAY']._serialized_end=7355 + _globals['_VECTOR']._serialized_start=7357 + _globals['_VECTOR']._serialized_end=7399 + _globals['_VECTORQUANTITY']._serialized_start=7401 + _globals['_VECTORQUANTITY']._serialized_end=7454 + _globals['_COMPLEX']._serialized_start=7456 + _globals['_COMPLEX']._serialized_end=7498 + _globals['_ENUMLITERAL']._serialized_start=7500 + _globals['_ENUMLITERAL']._serialized_end=7571 + _globals['_VALUESEQUENCE']._serialized_start=7573 + _globals['_VALUESEQUENCE']._serialized_end=7620 + _globals['_QUANTITY']._serialized_start=7622 + _globals['_QUANTITY']._serialized_end=7746 + _globals['_MEASUREMENTREF']._serialized_start=7748 + _globals['_MEASUREMENTREF']._serialized_end=7831 + _globals['_UNITTERM']._serialized_start=7833 + _globals['_UNITTERM']._serialized_end=7917 + _globals['_UNITFACTOR']._serialized_start=7919 + _globals['_UNITFACTOR']._serialized_end=7966 + _globals['_DIAGNOSTIC']._serialized_start=7968 + _globals['_DIAGNOSTIC']._serialized_end=8056 + _globals['_SPAN']._serialized_start=8058 + _globals['_SPAN']._serialized_end=8152 + _globals['_SERVERINFOREQUEST']._serialized_start=8154 + _globals['_SERVERINFOREQUEST']._serialized_end=8173 + _globals['_SERVERINFORESPONSE']._serialized_start=8175 + _globals['_SERVERINFORESPONSE']._serialized_end=8234 + _globals['_QUERYREQUEST']._serialized_start=8236 + _globals['_QUERYREQUEST']._serialized_end=8319 + _globals['_QUERYRESPONSE']._serialized_start=8321 + _globals['_QUERYRESPONSE']._serialized_end=8381 + _globals['_QUERY']._serialized_start=8383 + _globals['_QUERY']._serialized_end=8455 + _globals['_CONSTRAINT']._serialized_start=8457 + _globals['_CONSTRAINT']._serialized_end=8581 + _globals['_PRIMITIVECONSTRAINT']._serialized_start=8583 + _globals['_PRIMITIVECONSTRAINT']._serialized_end=8698 + _globals['_COMPOSITECONSTRAINT']._serialized_start=8700 + _globals['_COMPOSITECONSTRAINT']._serialized_end=8804 + _globals['_QUERYRESULTELEMENT']._serialized_start=8807 + _globals['_QUERYRESULTELEMENT']._serialized_end=8967 + _globals['_QUERYRESULTELEMENT_PROPERTIESENTRY']._serialized_start=8918 + _globals['_QUERYRESULTELEMENT_PROPERTIESENTRY']._serialized_end=8967 + _globals['_SWEEPRANGE']._serialized_start=8969 + _globals['_SWEEPRANGE']._serialized_end=9084 + _globals['_RUNSWEEPREQUEST']._serialized_start=9087 + _globals['_RUNSWEEPREQUEST']._serialized_end=9407 _globals['_RUNSWEEPREQUEST_NAMEDARGUMENTSENTRY']._serialized_start=1800 _globals['_RUNSWEEPREQUEST_NAMEDARGUMENTSENTRY']._serialized_end=1867 - _globals['_SWEEPROW']._serialized_start=9209 - _globals['_SWEEPROW']._serialized_end=9409 - _globals['_RUNSWEEPRESPONSE']._serialized_start=9412 - _globals['_RUNSWEEPRESPONSE']._serialized_end=9649 - _globals['_RUNDOCUMENTQUERYREQUEST']._serialized_start=9651 - _globals['_RUNDOCUMENTQUERYREQUEST']._serialized_end=9761 - _globals['_DOCUMENTQUERYBINDING']._serialized_start=9763 - _globals['_DOCUMENTQUERYBINDING']._serialized_end=9842 - _globals['_DOCUMENTVALUE']._serialized_start=9845 - _globals['_DOCUMENTVALUE']._serialized_end=10058 - _globals['_DOCUMENTQUERYCOLUMN']._serialized_start=10060 - _globals['_DOCUMENTQUERYCOLUMN']._serialized_end=10095 - _globals['_DOCUMENTQUERYCELL']._serialized_start=10097 - _globals['_DOCUMENTQUERYCELL']._serialized_end=10154 - _globals['_DOCUMENTQUERYROW']._serialized_start=10156 - _globals['_DOCUMENTQUERYROW']._serialized_end=10254 - _globals['_RUNDOCUMENTQUERYRESPONSE']._serialized_start=10256 - _globals['_RUNDOCUMENTQUERYRESPONSE']._serialized_end=10366 - _globals['_RENDERDOCUMENTREQUEST']._serialized_start=10368 - _globals['_RENDERDOCUMENTREQUEST']._serialized_end=10432 - _globals['_RENDERDOCUMENTRESPONSE']._serialized_start=10434 - _globals['_RENDERDOCUMENTRESPONSE']._serialized_end=10476 - _globals['_SYSMLSERVICE']._serialized_start=11434 - _globals['_SYSMLSERVICE']._serialized_end=12878 + _globals['_SWEEPROW']._serialized_start=9410 + _globals['_SWEEPROW']._serialized_end=9610 + _globals['_RUNSWEEPRESPONSE']._serialized_start=9613 + _globals['_RUNSWEEPRESPONSE']._serialized_end=9850 + _globals['_RUNDOCUMENTQUERYREQUEST']._serialized_start=9852 + _globals['_RUNDOCUMENTQUERYREQUEST']._serialized_end=9962 + _globals['_DOCUMENTQUERYBINDING']._serialized_start=9964 + _globals['_DOCUMENTQUERYBINDING']._serialized_end=10043 + _globals['_DOCUMENTVALUE']._serialized_start=10046 + _globals['_DOCUMENTVALUE']._serialized_end=10259 + _globals['_DOCUMENTQUERYCOLUMN']._serialized_start=10261 + _globals['_DOCUMENTQUERYCOLUMN']._serialized_end=10296 + _globals['_DOCUMENTQUERYCELL']._serialized_start=10298 + _globals['_DOCUMENTQUERYCELL']._serialized_end=10355 + _globals['_DOCUMENTQUERYROW']._serialized_start=10357 + _globals['_DOCUMENTQUERYROW']._serialized_end=10455 + _globals['_RUNDOCUMENTQUERYRESPONSE']._serialized_start=10457 + _globals['_RUNDOCUMENTQUERYRESPONSE']._serialized_end=10567 + _globals['_RENDERDOCUMENTREQUEST']._serialized_start=10569 + _globals['_RENDERDOCUMENTREQUEST']._serialized_end=10633 + _globals['_RENDERDOCUMENTRESPONSE']._serialized_start=10635 + _globals['_RENDERDOCUMENTRESPONSE']._serialized_end=10677 + _globals['_SYSMLSERVICE']._serialized_start=11635 + _globals['_SYSMLSERVICE']._serialized_end=13079 # @@protoc_insertion_point(module_scope) diff --git a/clients/python/opensysml/proto/sysml_pb2.pyi b/clients/python/opensysml/proto/sysml_pb2.pyi index 94515ccb6..31d11e1c5 100644 --- a/clients/python/opensysml/proto/sysml_pb2.pyi +++ b/clients/python/opensysml/proto/sysml_pb2.pyi @@ -692,7 +692,7 @@ class AttributeInfo(_message.Message): def __init__(self, name: _Optional[str] = ..., type: _Optional[str] = ..., value: _Optional[_Union[Value, _Mapping]] = ..., unit: _Optional[str] = ...) -> None: ... class Value(_message.Message): - __slots__ = ("int_value", "real_value", "bool_value", "string_value", "instance_id", "sequence", "null", "quantity", "enum_literal", "unset", "complex", "array", "vector", "vector_quantity", "measurement_ref", "infinity", "function") + __slots__ = ("int_value", "real_value", "bool_value", "string_value", "instance_id", "sequence", "null", "quantity", "enum_literal", "unset", "complex", "array", "vector", "vector_quantity", "measurement_ref", "infinity", "function", "set", "tensor_quantity") INT_VALUE_FIELD_NUMBER: _ClassVar[int] REAL_VALUE_FIELD_NUMBER: _ClassVar[int] BOOL_VALUE_FIELD_NUMBER: _ClassVar[int] @@ -710,6 +710,8 @@ class Value(_message.Message): MEASUREMENT_REF_FIELD_NUMBER: _ClassVar[int] INFINITY_FIELD_NUMBER: _ClassVar[int] FUNCTION_FIELD_NUMBER: _ClassVar[int] + SET_FIELD_NUMBER: _ClassVar[int] + TENSOR_QUANTITY_FIELD_NUMBER: _ClassVar[int] int_value: int real_value: float bool_value: bool @@ -727,7 +729,9 @@ class Value(_message.Message): measurement_ref: MeasurementRef infinity: bool function: Function - def __init__(self, int_value: _Optional[int] = ..., real_value: _Optional[float] = ..., bool_value: _Optional[bool] = ..., string_value: _Optional[str] = ..., instance_id: _Optional[int] = ..., sequence: _Optional[_Union[ValueSequence, _Mapping]] = ..., null: _Optional[str] = ..., quantity: _Optional[_Union[Quantity, _Mapping]] = ..., enum_literal: _Optional[_Union[EnumLiteral, _Mapping]] = ..., unset: _Optional[bool] = ..., complex: _Optional[_Union[Complex, _Mapping]] = ..., array: _Optional[_Union[Array, _Mapping]] = ..., vector: _Optional[_Union[Vector, _Mapping]] = ..., vector_quantity: _Optional[_Union[VectorQuantity, _Mapping]] = ..., measurement_ref: _Optional[_Union[MeasurementRef, _Mapping]] = ..., infinity: _Optional[bool] = ..., function: _Optional[_Union[Function, _Mapping]] = ...) -> None: ... + set: ValueSet + tensor_quantity: TensorQuantity + def __init__(self, int_value: _Optional[int] = ..., real_value: _Optional[float] = ..., bool_value: _Optional[bool] = ..., string_value: _Optional[str] = ..., instance_id: _Optional[int] = ..., sequence: _Optional[_Union[ValueSequence, _Mapping]] = ..., null: _Optional[str] = ..., quantity: _Optional[_Union[Quantity, _Mapping]] = ..., enum_literal: _Optional[_Union[EnumLiteral, _Mapping]] = ..., unset: _Optional[bool] = ..., complex: _Optional[_Union[Complex, _Mapping]] = ..., array: _Optional[_Union[Array, _Mapping]] = ..., vector: _Optional[_Union[Vector, _Mapping]] = ..., vector_quantity: _Optional[_Union[VectorQuantity, _Mapping]] = ..., measurement_ref: _Optional[_Union[MeasurementRef, _Mapping]] = ..., infinity: _Optional[bool] = ..., function: _Optional[_Union[Function, _Mapping]] = ..., set: _Optional[_Union[ValueSet, _Mapping]] = ..., tensor_quantity: _Optional[_Union[TensorQuantity, _Mapping]] = ...) -> None: ... class Function(_message.Message): __slots__ = ("calc_id", "self_id") @@ -737,6 +741,20 @@ class Function(_message.Message): self_id: int def __init__(self, calc_id: _Optional[str] = ..., self_id: _Optional[int] = ...) -> None: ... +class ValueSet(_message.Message): + __slots__ = ("elements",) + ELEMENTS_FIELD_NUMBER: _ClassVar[int] + elements: _containers.RepeatedCompositeFieldContainer[Value] + def __init__(self, elements: _Optional[_Iterable[_Union[Value, _Mapping]]] = ...) -> None: ... + +class TensorQuantity(_message.Message): + __slots__ = ("dimensions", "components") + DIMENSIONS_FIELD_NUMBER: _ClassVar[int] + COMPONENTS_FIELD_NUMBER: _ClassVar[int] + dimensions: _containers.RepeatedScalarFieldContainer[int] + components: _containers.RepeatedCompositeFieldContainer[Quantity] + def __init__(self, dimensions: _Optional[_Iterable[int]] = ..., components: _Optional[_Iterable[_Union[Quantity, _Mapping]]] = ...) -> None: ... + class Array(_message.Message): __slots__ = ("dimensions", "elements") DIMENSIONS_FIELD_NUMBER: _ClassVar[int] diff --git a/clients/python/opensysml/values.py b/clients/python/opensysml/values.py index 2cca36b51..59b31bcf1 100644 --- a/clients/python/opensysml/values.py +++ b/clients/python/opensysml/values.py @@ -689,6 +689,158 @@ def __str__(self) -> str: return "⟨" + ", ".join(str(c) for c in self.components) + "⟩" +@dataclass(frozen=True) +class SetValue: + """A unique, unordered collection: a ``Collections::Set``'s elements. + + Distinct from a ``list``, whose order is part of its value. The service + sends the elements in its canonical order, so equal sets arrive alike, and + ``elements`` keeps that order for reading; equality ignores it. A set to + send may hold its elements in any order — a Python ``set`` or ``frozenset`` + is accepted too — but one listing an element twice is refused by the service + rather than read as one element. Elements need not be hashable: a nested + list is one. + + Attributes: + elements (tuple): The elements, each once, in the order held + """ + + elements: Tuple[Any, ...] + + def __init__(self, elements: Sequence[Any] = ()) -> None: + object.__setattr__(self, "elements", tuple(elements)) + + def __len__(self) -> int: + return len(self.elements) + + def __iter__(self) -> Iterator[Any]: + return iter(self.elements) + + def __contains__(self, item: Any) -> bool: + return any(element == item for element in self.elements) + + def __eq__(self, other: object) -> bool: + if isinstance(other, (set, frozenset)): + other = SetValue(other) + if not isinstance(other, SetValue): + return NotImplemented + return len(self) == len(other) and all(e in other for e in self) and all(e in self for e in other) + + def __hash__(self) -> int: + return hash(len(self.elements)) + + @classmethod + def from_pb(cls, pb_set, resolve_instance=None) -> "SetValue": + """Build from a ``ValueSet`` protobuf message, elements in the order sent.""" + return cls(value_to_python(v, resolve_instance) for v in pb_set.elements) + + def to_pb(self, encode: Callable[[Any], "sysml_pb2.Value"]) -> "sysml_pb2.ValueSet": + """Encode as a ``ValueSet`` message, each element through ``encode``.""" + return sysml_pb2.ValueSet(elements=[encode(element) for element in self.elements]) + + def __str__(self) -> str: + return "{" + ", ".join(str(e) for e in self.elements) + "}" + + +@dataclass(frozen=True) +class TensorQuantity: + """A tensor quantity of any rank: its shape and one quantity per component. + + ``TensorCalculations::'['((1.0, ..., 8.0), cubeRef)`` over a + ``(2, 2, 2)`` reference is ``TensorQuantity((2, 2, 2), (Quantity(1.0, Pa), + ...))``, the components flattened row-major as an :class:`Array`'s are. A + tensor of rank one is not a :class:`VectorQuantity`, here as in the model. + + Attributes: + dimensions (tuple[int, ...]): Extent of each dimension, all positive + components (tuple[Quantity, ...]): The components, row-major + + Raises: + ValueError: If a dimension is not positive, a component is not a + :class:`Quantity`, or the components do not fill the dimensions. + """ + + dimensions: Tuple[int, ...] + components: Tuple[Quantity, ...] + + def __init__(self, dimensions: Sequence[int], components: Sequence[Quantity]) -> None: + dims = tuple(dimensions) + comps = tuple(components) + for extent in dims: + if isinstance(extent, bool) or not isinstance(extent, int) or extent <= 0: + raise ValueError(f"tensor dimension {extent!r} is not a positive integer") + if len(comps) != math.prod(dims): + raise ValueError( + f"tensor of dimensions {dims} holds {len(comps)} component(s), " + f"want {math.prod(dims)}" + ) + for component in comps: + if not isinstance(component, Quantity): + raise ValueError(f"tensor component {component!r} is not a Quantity") + object.__setattr__(self, "dimensions", dims) + object.__setattr__(self, "components", comps) + + @property + def rank(self) -> int: + """Number of dimensions.""" + return len(self.dimensions) + + @property + def unit(self) -> Optional[Unit]: + """The one unit every component shares, or ``None`` when they differ.""" + units = {component.unit for component in self.components} + return next(iter(units)) if len(units) == 1 else None + + def __len__(self) -> int: + return len(self.components) + + def __iter__(self) -> Iterator[Quantity]: + return iter(self.components) + + def __getitem__(self, index: int | Tuple[int, ...]) -> Quantity: + """The component at a row-major position, or at a full multi-index.""" + if isinstance(index, tuple): + if len(index) != self.rank: + raise IndexError( + f"index {index} has {len(index)} coordinate(s), tensor has rank {self.rank}" + ) + flat = 0 + for extent, coordinate in zip(self.dimensions, index): + if not 0 <= coordinate < extent: + raise IndexError(f"index {index} is outside dimensions {self.dimensions}") + flat = flat * extent + coordinate + return self.components[flat] + return self.components[index] + + @classmethod + def from_pb(cls, pb_tensor) -> "TensorQuantity": + """Build from a ``TensorQuantity`` protobuf message. + + Raises: + UnsupportedValueError: If the message's shape and components + disagree, or a component is a quantity the client cannot read. + """ + try: + return cls(tuple(pb_tensor.dimensions), [Quantity.from_pb(c) for c in pb_tensor.components]) + except ValueError as exc: + raise UnsupportedValueError(f"malformed tensor quantity: {exc}") from exc + + def to_pb(self) -> "sysml_pb2.TensorQuantity": + """Encode as a ``TensorQuantity`` message, one ``Quantity`` per component.""" + return sysml_pb2.TensorQuantity( + dimensions=list(self.dimensions), + components=[c.to_pb() for c in self.components], + ) + + def __str__(self) -> str: + dims = ", ".join(str(d) for d in self.dimensions) + unit = self.unit + if unit is not None: + body = ", ".join(_format_number(c.magnitude) for c in self.components) + return f"Tensor({dims})[{body}] [{unit}]" + return f"Tensor({dims})[{', '.join(str(c) for c in self.components)}]" + + class UnsetType: """A feature holding no value: a valueless feature of a value type. @@ -752,10 +904,11 @@ def value_to_python(pb_value, resolve_instance=None): int, float, complex, bool, str, list, None, :data:`UNSET`, :data:`INFINITY`, a :class:`Quantity`, a :class:`MeasurementRef`, a :class:`Function`, an - :class:`Array`, a :class:`Vector`, a :class:`VectorQuantity`, an - :class:`~opensysml.enumeration.EnumLiteral`, + :class:`Array`, a :class:`Vector`, a :class:`VectorQuantity`, a :class:`SetValue`, a + :class:`TensorQuantity`, an :class:`~opensysml.enumeration.EnumLiteral`, or the resolved instance object. A Complex is one ``complex``, never two - floats; a Vector is one :class:`Vector`, never a list of numbers. + floats; a Vector is one :class:`Vector`, never a list of numbers; a set + is one :class:`SetValue`, never a list. Raises: UnsupportedValueError: If the service reported the value as unsupported, @@ -790,6 +943,10 @@ def value_to_python(pb_value, resolve_instance=None): return Vector.from_pb(pb_value.vector) if kind == 'vector_quantity': return VectorQuantity.from_pb(pb_value.vector_quantity) + if kind == 'set': + return SetValue.from_pb(pb_value.set, resolve_instance) + if kind == 'tensor_quantity': + return TensorQuantity.from_pb(pb_value.tensor_quantity) if kind == 'enum_literal': lit = pb_value.enum_literal return EnumLiteral(lit.literal_id, lit.enumeration_id, lit.name) diff --git a/clients/python/tests/test_set_tensor.py b/clients/python/tests/test_set_tensor.py new file mode 100644 index 000000000..b637042e2 --- /dev/null +++ b/clients/python/tests/test_set_tensor.py @@ -0,0 +1,422 @@ +"""Tests for a set and a tensor quantity as Python values. + +A ``Collections::Set``'s elements travel in an arm of their own, ``Value.set``, +so they must arrive as one :class:`SetValue` holding each element once, equal to +another set whatever the order; a tensor quantity of any rank travels as +``Value.tensor_quantity`` and must arrive as one :class:`TensorQuantity` with +its shape. Neither is the unsupported null a service without the capability sends. +""" + +from unittest.mock import Mock, patch + +import pytest + +from opensysml.capabilities import ( + CAPABILITY_COMPLEX_VALUES, + CAPABILITY_FEATURE_VALUES, + CAPABILITY_MEASUREMENT_REFS, + CAPABILITY_SET_VALUES, + CAPABILITY_STRUCTURED_VALUES, + CAPABILITY_TENSOR_VALUES, + CAPABILITY_VERIFICATION, + MissingCapabilityError, +) +from opensysml.connection import Connection +from opensysml.errors import ExecutionError, UnsupportedValueError +from opensysml.proto import sysml_pb2 +from opensysml.values import ( + Array, + Quantity, + SetValue, + TensorQuantity, + Unit, + UnitFactor, + VectorQuantity, + value_to_python, +) + +from tests.service_gate import skip_or_fail_without_service + + +def pb_int(value): + return sysml_pb2.Value(int_value=value) + + +def pb_set(*elements): + return sysml_pb2.Value(set=sysml_pb2.ValueSet(elements=list(elements))) + + +PB_PASCAL = sysml_pb2.UnitTerm( + scale_num=1.0, scale_den=1.0, + factors=[ + sysml_pb2.UnitFactor(unit_id="SI::kilogram", exponent=1.0), + sysml_pb2.UnitFactor(unit_id="SI::metre", exponent=-1.0), + sysml_pb2.UnitFactor(unit_id="SI::second", exponent=-2.0), + ], +) +PASCAL = Unit( + "Pa", 1.0, 1.0, + (UnitFactor("SI::kilogram", 1.0), UnitFactor("SI::metre", -1.0), UnitFactor("SI::second", -2.0)), + reduction_given=True, +) + + +def pb_pascal(magnitude): + return sysml_pb2.Quantity(real_magnitude=magnitude, unit="Pa", unit_term=PB_PASCAL) + + +def pb_tensor(dimensions, *components): + return sysml_pb2.Value(tensor_quantity=sysml_pb2.TensorQuantity( + dimensions=list(dimensions), components=list(components), + )) + + +CUBE = TensorQuantity((2, 2, 2), [Quantity(float(i), PASCAL) for i in range(1, 9)]) +PB_CUBE = pb_tensor((2, 2, 2), *(pb_pascal(float(i)) for i in range(1, 9))) + + +def make_connection(stub, capabilities): + """Build a Connection over a mock stub reporting ``capabilities``.""" + stub.GetServerInfo.return_value = sysml_pb2.ServerInfoResponse( + version="test", capabilities=list(capabilities) + ) + with patch("grpc.insecure_channel"): + with patch( + "opensysml.proto.sysml_pb2_grpc.SysMLServiceStub", return_value=stub + ): + return Connection(auto_start=False) + + +OLD = ( + CAPABILITY_COMPLEX_VALUES, + CAPABILITY_STRUCTURED_VALUES, + CAPABILITY_MEASUREMENT_REFS, + CAPABILITY_FEATURE_VALUES, + CAPABILITY_VERIFICATION, +) +CURRENT = OLD + (CAPABILITY_SET_VALUES, CAPABILITY_TENSOR_VALUES) + + +# --- Sets: reading --------------------------------------------------------- + + +def test_a_set_decodes_as_its_elements_in_the_order_sent(): + got = value_to_python(pb_set(pb_int(1), pb_int(2), pb_int(3))) + + assert isinstance(got, SetValue) + assert got.elements == (1, 2, 3) + assert len(got) == 3 + assert 2 in got and 4 not in got + assert list(got) == [1, 2, 3] + assert str(got) == "{1, 2, 3}" + + +def test_an_empty_set_is_a_set_of_nothing(): + got = value_to_python(pb_set()) + assert got == SetValue() + assert len(got) == 0 + assert str(got) == "{}" + assert got != [] + + +def test_set_equality_ignores_order_and_a_python_set_compares_equal(): + assert SetValue((1, 2, 3)) == SetValue((3, 1, 2)) + assert SetValue((1, 2, 3)) == {3, 1, 2} + assert {3, 1, 2} == SetValue((1, 2, 3)) + assert SetValue((1, 2, 3)) == frozenset((3, 1, 2)) + assert SetValue((1, 2)) != SetValue((1, 2, 3)) + assert SetValue((1, 2)) != [1, 2] + assert SetValue(([1, 2], "a")) == SetValue(("a", [1, 2])) + + +def test_a_set_nests_and_is_nested_in_place(): + nested = pb_set(pb_set(pb_int(1)), pb_set()) + assert value_to_python(nested) == SetValue((SetValue((1,)), SetValue())) + + sequence = sysml_pb2.Value(sequence=sysml_pb2.ValueSequence(elements=[ + pb_set(pb_int(1)), pb_int(2), + ])) + assert value_to_python(sequence) == [SetValue((1,)), 2] + + array = sysml_pb2.Value(array=sysml_pb2.Array(dimensions=[1], elements=[pb_set(pb_int(1))])) + assert value_to_python(array) == Array((1,), (SetValue((1,)),)) + + +def test_a_set_survives_the_wire_bytes(): + value = pb_set(pb_int(1), sysml_pb2.Value(string_value="a"), pb_set()) + again = sysml_pb2.Value() + again.ParseFromString(value.SerializeToString()) + assert again == value + assert value_to_python(again) == value_to_python(value) + + +def test_a_service_without_set_values_still_reports_unsupported(): + null = sysml_pb2.Value(null="unsupported: set Set{1, 2, 3}") + with pytest.raises(UnsupportedValueError, match="set Set"): + value_to_python(null) + + +# --- Sets: sending --------------------------------------------------------- + + +def test_a_set_is_sent_as_its_own_arm(): + conn = make_connection(Mock(), CURRENT) + + sent = conn._python_to_value(SetValue((3, 1, 2))) + assert sent.WhichOneof("kind") == "set" + assert [e.int_value for e in sent.set.elements] == [3, 1, 2] + assert value_to_python(sent) == SetValue((1, 2, 3)) + + sent = conn._python_to_value({1, 2}) + assert sent.WhichOneof("kind") == "set" + assert value_to_python(sent) == {1, 2} + + sent = conn._python_to_value(frozenset()) + assert sent.WhichOneof("kind") == "set" + assert value_to_python(sent) == SetValue() + + nested = conn._python_to_value([1, SetValue(([2], SetValue()))]) + assert value_to_python(nested) == [1, SetValue(([2], SetValue()))] + + +def test_a_set_is_not_sent_to_a_service_without_the_capability(): + stub = Mock() + conn = make_connection(stub, OLD) + + for value in (SetValue((1,)), {1}, [1, [SetValue()]], Array((1,), (SetValue((1,)),))): + with pytest.raises(MissingCapabilityError) as excinfo: + conn.execute_action("W::act", "hash", inputs={"c": value}) + assert excinfo.value.capability == CAPABILITY_SET_VALUES + with pytest.raises(MissingCapabilityError) as excinfo: + conn.calc("W::sizeOf", "hash", arguments=[value]) + assert excinfo.value.capability == CAPABILITY_SET_VALUES + stub.ExecuteAction.assert_not_called() + stub.EvaluateCalc.assert_not_called() + + # A list still travels: it never needed the capability. + stub.EvaluateCalc.return_value = sysml_pb2.EvaluateCalcResponse(result=pb_int(3)) + assert conn.calc("W::sizeOf", "hash", arguments=[[1, 2, 3]]).value == 3 + stub.EvaluateCalc.assert_called_once() + + +# --- Tensors: reading ------------------------------------------------------ + + +def test_a_tensor_decodes_with_its_shape_and_components(): + got = value_to_python(PB_CUBE) + + assert isinstance(got, TensorQuantity) + assert got == CUBE + assert got.rank == 3 + assert got.dimensions == (2, 2, 2) + assert len(got) == 8 + assert got[7] == Quantity(8.0, PASCAL) + assert got[(1, 1, 1)] == Quantity(8.0, PASCAL) + assert got[(0, 1, 0)] == Quantity(3.0, PASCAL) + assert got.unit == PASCAL + assert str(got) == "Tensor(2, 2, 2)[1, 2, 3, 4, 5, 6, 7, 8] [Pa]" + assert not isinstance(got, VectorQuantity) + + +def test_a_tensor_of_rank_one_is_not_a_vector_quantity(): + got = value_to_python(pb_tensor((2,), pb_pascal(1.0), pb_pascal(2.0))) + assert isinstance(got, TensorQuantity) + assert got.rank == 1 + assert not isinstance(got, VectorQuantity) + + +def test_a_tensor_with_mixed_units_renders_each_component(): + metre = sysml_pb2.Quantity( + int_magnitude=3, unit="m", + unit_term=sysml_pb2.UnitTerm(scale_num=1, scale_den=1, factors=[ + sysml_pb2.UnitFactor(unit_id="SI::metre", exponent=1.0), + ]), + ) + got = value_to_python(pb_tensor((1, 2), metre, pb_pascal(2.0))) + assert got.unit is None + assert str(got) == "Tensor(1, 2)[3 [m], 2 [Pa]]" + + +def test_tensor_indexing_is_shape_checked(): + with pytest.raises(IndexError, match="has 2 coordinate"): + CUBE[(1, 1)] + with pytest.raises(IndexError, match="has 4 coordinate"): + CUBE[(1, 1, 1, 1)] + with pytest.raises(IndexError, match="outside dimensions"): + CUBE[(0, 0, 2)] + with pytest.raises(IndexError, match="outside dimensions"): + CUBE[(-1, 0, 0)] + + +@pytest.mark.parametrize("dimensions, components, message", [ + ((2, 2), [pb_pascal(1.0)] * 3, "holds 3 component"), + ((2,), [pb_pascal(1.0)] * 3, "holds 3 component"), + ((0,), [], "not a positive integer"), + ((-1,), [pb_pascal(1.0)], "not a positive integer"), + ((), [], "holds 0 component"), +]) +def test_a_malformed_tensor_is_reported(dimensions, components, message): + with pytest.raises(UnsupportedValueError, match=message): + value_to_python(pb_tensor(dimensions, *components)) + + +def test_a_tensor_component_without_a_magnitude_is_reported(): + with pytest.raises(UnsupportedValueError): + value_to_python(pb_tensor((1,), sysml_pb2.Quantity(unit="Pa", unit_term=PB_PASCAL))) + + +def test_a_tensor_built_by_hand_is_shape_checked(): + with pytest.raises(ValueError, match="holds 1 component"): + TensorQuantity((2,), [Quantity(1.0, PASCAL)]) + with pytest.raises(ValueError, match="not a positive integer"): + TensorQuantity((0,), []) + with pytest.raises(ValueError, match="not a Quantity"): + TensorQuantity((1,), [1.0]) + + +def test_a_tensor_survives_the_wire_bytes(): + again = sysml_pb2.Value() + again.ParseFromString(PB_CUBE.SerializeToString()) + assert again == PB_CUBE + assert value_to_python(again) == CUBE + + +def test_a_service_without_tensor_values_still_reports_unsupported(): + null = sysml_pb2.Value(null="unsupported: tensor quantity Tensor(2, 2, 2)[1.0] [Pa]") + with pytest.raises(UnsupportedValueError, match="tensor quantity"): + value_to_python(null) + + +# --- Tensors: sending ------------------------------------------------------ + + +def test_a_tensor_is_sent_as_its_own_arm(): + conn = make_connection(Mock(), CURRENT) + + sent = conn._python_to_value(CUBE) + assert sent.WhichOneof("kind") == "tensor_quantity" + assert list(sent.tensor_quantity.dimensions) == [2, 2, 2] + assert [c.real_magnitude for c in sent.tensor_quantity.components] == [float(i) for i in range(1, 9)] + assert value_to_python(sent) == CUBE + + nested = conn._python_to_value([CUBE, SetValue((CUBE,))]) + assert value_to_python(nested) == [CUBE, SetValue((CUBE,))] + + +def test_a_tensor_is_not_sent_to_a_service_without_the_capability(): + stub = Mock() + conn = make_connection(stub, OLD + (CAPABILITY_SET_VALUES,)) + + for value in (CUBE, [CUBE], SetValue((CUBE,)), Array((1,), (CUBE,))): + with pytest.raises(MissingCapabilityError) as excinfo: + conn.execute_action("W::act", "hash", inputs={"t": value}) + assert excinfo.value.capability == CAPABILITY_TENSOR_VALUES + with pytest.raises(MissingCapabilityError) as excinfo: + conn.calc("W::corner", "hash", arguments=[value]) + assert excinfo.value.capability == CAPABILITY_TENSOR_VALUES + stub.ExecuteAction.assert_not_called() + stub.EvaluateCalc.assert_not_called() + + +def test_a_refusal_by_the_service_names_the_capability(): + """A service claiming the capability yet refusing is reported by its own words.""" + import grpc + + class Refusal(grpc.RpcError, grpc.Call): + def code(self): + return grpc.StatusCode.UNIMPLEMENTED + + def details(self): + return 'capability "tensor_values" is unavailable' + + def trailing_metadata(self): + return () + + stub = Mock() + conn = make_connection(stub, CURRENT) + stub.EvaluateCalc.side_effect = Refusal() + + with pytest.raises(MissingCapabilityError) as excinfo: + conn.calc("W::corner", "hash", arguments=[CUBE]) + assert excinfo.value.capability == CAPABILITY_TENSOR_VALUES + + +SET_TENSOR_MODEL = """ +package W { + private import ScalarValues::*; + private import Collections::*; + private import Quantities::*; + private import MeasurementReferences::*; + private import SI::*; + private import TensorCalculations::*; + + attribute s : Set { :>> elements = (3, 1, 2, 2, 3); } + attribute e : Set { :>> elements = (); } + attribute mixed : Set { :>> elements = ("b", 2, true, "a"); } + + attribute cubeRef : TensorMeasurementReference { + :>> dimensions = (2, 2, 2); + :>> mRefs = (Pa, Pa, Pa, Pa, Pa, Pa, Pa, Pa); + } + attribute cube : TensorQuantityValue = TensorCalculations::'['((1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0), cubeRef); + + calc def SizeOf { in c : Integer[0..*]; return : Natural = SequenceFunctions::size(c); } + calc sizeOf : SizeOf; + calc def Corner { in t : TensorQuantityValue; return : ScalarQuantityValue = t#(2, 2, 2); } + calc corner : Corner; +} +""" + + +@pytest.mark.integration +class TestSetsAndTensorsAgainstTheService: + """What a caller actually gets back from the real service for a real model.""" + + def setup_method(self): + import grpc + + try: + self.conn = Connection(auto_start=False) + self.conn._stub.GetDiagnostics(sysml_pb2.DiagnosticsRequest(model_hash="")) + except grpc.RpcError as exc: + if exc.code() != grpc.StatusCode.NOT_FOUND: + self.conn = None + skip_or_fail_without_service( + f"the sysml-grpc service on localhost:50051 answered {exc.code()}" + ) + except Exception as exc: + self.conn = None + skip_or_fail_without_service( + f"no sysml-grpc service could be reached on localhost:50051 ({exc})" + ) + self.model = self.conn.load_from_content(SET_TENSOR_MODEL) + + def teardown_method(self): + conn = self.__dict__.get("conn") + if conn is not None: + conn.close() + + def test_the_service_advertises_both_capabilities(self): + info = self.conn.server_info() + assert CAPABILITY_SET_VALUES in info.capabilities + assert CAPABILITY_TENSOR_VALUES in info.capabilities + + def test_a_set_reads_each_element_once_in_canonical_order(self): + assert self.conn.eval("W::s.elements", self.model.hash).elements == (1, 2, 3) + assert self.conn.eval("W::e.elements", self.model.hash) == SetValue() + assert self.conn.eval("W::mixed.elements", self.model.hash).elements == (True, 2, "a", "b") + + def test_a_tensor_reads_with_its_rank_and_indexes_by_shape(self): + cube = self.conn.eval("W::cube", self.model.hash) + assert isinstance(cube, TensorQuantity) + assert cube.dimensions == (2, 2, 2) + assert cube[(1, 1, 1)].magnitude == 8.0 + assert cube.unit.text == "Pa" + + corner = self.conn.calc("W::corner", self.model.hash, arguments=[cube]).value + assert corner == cube[(1, 1, 1)] + + def test_a_set_sent_in_any_order_is_read_as_its_elements(self): + assert self.conn.calc("W::sizeOf", self.model.hash, arguments=[{3, 1, 2}]).value == 3 + with pytest.raises(ExecutionError, match="set element is repeated"): + self.conn.calc("W::sizeOf", self.model.hash, arguments=[SetValue((1, 1))]) diff --git a/clients/rust/README.md b/clients/rust/README.md index ad438fe3e..2473f4ffd 100644 --- a/clients/rust/README.md +++ b/clients/rust/README.md @@ -144,11 +144,23 @@ for: Decoding a response is never gated on capabilities: if a service sends an enum, unset value, complex number, array, vector, vector quantity, measurement -reference, function, or feature-value arm, the client understands that answer. Consumers -can inspect `Capabilities::has` or use `Capabilities::require` when they need to -gate their own use of `enum_values`, `unset_value`, `complex_values`, -`structured_values`, `measurement_refs`, `function_values`, `feature_values`, or another advertised -operation. +reference, function, set, tensor quantity, or feature-value arm, the client +understands that answer. Consumers can inspect `Capabilities::has` or use +`Capabilities::require` when they need to gate their own use of `enum_values`, +`unset_value`, `complex_values`, `structured_values`, `measurement_refs`, +`function_values`, `set_values`, `tensor_values`, `feature_values`, or another +advertised operation. + +A `Value::Set` is a `Collections::Set`'s elements: each member once, sent in +the service's canonical order (numbers ascending, then strings, and so on), and +equal to another set holding the same members in any order. A +`Value::TensorQuantity` is a `Quantities::TensorQuantityValue` of any rank: +its `dimensions()` and its `components()` flattened row-major, each a +`Quantity` with its own unit; `get(&[i, j, k])` takes one coordinate per +dimension. A rank-one tensor stays a `TensorQuantity`, distinct from a +`VectorQuantity`. A malformed set (a member listed twice) or tensor (a +non-positive dimension, or components that do not fill the shape) is an +`Error::Decode`, never a partial value. ## Conformance runner diff --git a/clients/rust/conformance/sysml.descriptor.binpb b/clients/rust/conformance/sysml.descriptor.binpb index cf6ba5d579466862fc3fd468dd42e207be509051..9bc81da5c482c504861cdccb75a9abc13e048481 100644 GIT binary patch delta 9619 zcmaKyd6*T|nZ|Eb-+S(*kg;<(k`jzSXO=Z|goy^P6VR>T`G9 zthu?Vx#lscDKhKb;kIthH8kZLzxu7Ybq&q+&GXxrMTCl&FfE#$98W@PW#p4zf=%5= zm)5vCQ5^h3&l}p*xvf6K3oduBZhfQcPS4MtlW)j1G&i-kgEc*>H(%(!w#wb|L5Z97 zGO5&WH$Q&+$4jg|!Rp=PtUqr4>+aR6aGRS>-)Oh>(!bB7E35nXGwPd~>l>yw`?*=U z*`mpxSKmC-Z^--k#u>RrKmT>Vd1kJj!Mq07ZRuu&ts;7h1X==Hwn<=#z_JRrCNkxD z=|5!By{dcqO*8A}d@1MGHO%mF+D{_(=V0b)B#8|V8}PhXsfUd}WXh@&h>bsFdR*&n zD{<}gO`bZONxiZ8M|*#5wLYBDZQSvgzR6B`>7&6t`zQ7}nz41n*EjFmKh7$=p0Tqr$C6(&x^)8Kyq@vO+?UfBzu8k~GO2emR@Jt&pH$7S zZ}RK>xefK-nw#s*zfmr8GYE6mtlaeG`h0```adm4Wcbw|ng$M}kFITs3sImFh`4@T zQ?W*0Z_Lkz6k9=e!*tZoo0)IQ#pS~8oVvzlDO)`nzAf$9$DcMY@6(-4Xlu;*Ia+fT ztXzYXcD_F|KWm1J{HD6u%$hzZH2L)n)M~y_j1CdVHGaE(y8PQP17*Uw+BQfFZk$Wk z&(0CY^ty(8L;ZA2nNI38ET5m1t7~ZL?KjMweQ&OjK~r;MeZzfCyBCq!H$186fT@tfxneDJlyT?_OthFCYW zv)`DTldEf{y0M%q)t@)Bu9>+ek}Xvt8;y+w*T;2B-IDapbkrGX)E0Ez&C;l6Gxjx6 ze}U2wG%EqfN;sP-H606tdi~i<+YX5|-FPS88!nQr{K?aQ7Kyn47( z|Ich5^P~2W4w33M)-`-{p!AAMo3Q~j(VL{JVMg!Zdj~of=v~jwI8P?GSJ8Mhx%qW6z{~dFGV&%Nlf4>Go2^}`@@u(tZ@zSAU{L`@@= z702shSAwR4-N!&Q(b``Q*7`8m;OQRa^_rDd@S*>gV1l$)MF7>yrgx>mbCwWxAGPkow6eP0E& z2Yyn~56Mq6x~2P=1lUgkd_V=(!4<<9W8(o8+W_MA11k0!2=f6Idkw_K11k2qpUvb# z6&yY|WdvjNEG+5~Ul$hjh_4G(+>r>QXQ84a=i+LOrxvNyzo=l&p?pORf{T=H<*pT1 zYsA$hD!6cH^+?8;Sdy3kVq!^R0tj#>Jbui>;Sol1G zs}#q9EZ)zHg-4Zb4k{2S2Abmri0q@vX&F@&h|)(@o3=5P$;Xu4E}pcY0u!oa^)cln ziY2R$sftRs#rxvWEuLDdQvVu!?bv%|w;;Jz=`wdR4&5RStq*dqPVC1JmFp9gKm^w- z$MhHwmFrbyJ9k3FWAcgM%~!|36IY%n^2C)Vlw%xa7Ty!8s-rtJ;@Mt$L-4iN1{ZFz zZFn1sJn5kgN%YLZ+hC%%)33Xr;O zOj-j7b)$4&N4cGVv9eLxZ=>XCfTx~TsTYD<{`_R+03@GQ?7Da3_W+&p)U(Ro5}g0@ zqymV^XO%Xk86X~PR_RVrX+XpUbP|Z{X62fEFpI^_k`HN!0TT2tRNAau5LBRX9vID* zn%x@+^%tsB%g*jHSrP+mlKFh_yTAOfh9S)7lVk#6KA$8L2=jSK=AR4^xGi}2^*#Lu z*d-)rTaqsz#BHjyJa)cBBvrfi?gcq8272l@Dz$HO_(t9;ABf~{WbHgGItNM;b_G>$ zE~#ONGrJOJfJp92oB^V9mpJolLuB^l;P9JUA5MLH&&Xzeb-*i3iFCA$U;AJO!15B#6Vo z{FAi>h6Hgq(FjEFaI(UHXgnOPu!zUxkz{cWipmHyixU{l#)}gO^@vc%L{y?UrtIpf zf&Q>Mu4Zx=Hu=G*Q~ibyvTd4~Nz)HN(sN9eRz$@CqW74p?3_UK9#ei-cNj6==BdA^ z)SJP!Q-jNHL+&q1d(lnzHi_|#pvCFY1%|}0PM zA}LLOGYJ{usrOXsgJ8#*-i0B^y{B}GD3BqNfb+=#0YWFx1Oi0%e4-PG&huuAOs9v4 zrx%hQ1VIIwX9+NxCC{?&0ij-y9xO=yhDZ`FCbb2@1twh4d{K5g!fCaE7H=u(n; z5L947l`LG6Pom*x2F#8a(!rw?wwSXpD{uYQW?9lZ%>eCC?}zb^G+w!ASv?{l;3 zzLEbQ5A(>=&Fenyl^%XabRQ&tW)lOF`&Y81rkj9Kc0F?f0?GX=S#DByQO=>B`eQcr zr?91E-B~si!9QkoxqB@|87k%66V9=$ZVil4xhE+n5Xn8+_~rsc<(@1z7ismO(#(6a z@f8fjdIy@G14gslkEQ2;Q1@n8wG$&Mt7Ly@+1A+pL!&jazi3m`?l0PuRk2@eJ|gGl zFi#!Lre57zu&p-MSBD{bG#g)EhDjjDvi1#;MUd?HNwR>l<7Z3F77v7aEPGAQL>Mc_ zve#YjZWSxTJ#{>rI-aFn7&(y`0g}EGNkxD# zPh>rFVF6;~M3xK7J&_(JPlaDp)?Mh4oBpYyo@m=G>XBTY%6jc1XAwrvscgqi?iV6G z{dp_H~tD%~`HQT*MOl9&y zkt$6CY-fr#!fdWZZnU`y_X|{Iq^Ca2rWWe(MQu$f8;PP1v$`Y4Ksr5AJh~kAa;(qQ zFs2HZv$i>SfJk1>W?M!607=5-Y+KLWAmtn>DY_Caa;)_Q#xSoWtq7F4Y*I2=&u!xw**#p?;d};=8+zQQ!h?cdB~9GM|;2{N{YP(Y2l* zG14xf7nn4w9EeW~biB%eC|;oBo&rXi=&8xBov}Uj1D#r;!wsd@q&7d$nM${Zo6gvh zod)#$*oi&?=Hyt^DeIW1QoX zsLA*_T6l0I$E!5Ga6z&++EZ(E>Zdw%TU%$#M`LD<<_sT7+D40KYjxP7jdfcMfDT)$ z<1-wH=vwWViyDwn*J>_ma>*Vo?yl3}ciULQ3jmnwicHDUIvqc|nT5Gdv*$~xMvHgr zlYE0H70_HXfl$|L5-i6B5bAo#(Sx!dkMYz~I<=Wb_|WpF&dcTIOsQ+QFC5a=x==m_ z;!~QPX|b%VF%sFPaFl2La}8tM+myHmM0AsmUk^ZRY|`w8%MFv+r&p9&4;RKn)qh&Y zXBkkYbbOWpAwHeNHAdokM#tA)5OD#`F$jeEjMgTZK&a11G9@Kr1U;*5bAW@O0(&Q1 zS?bSfF0_&prX%UrZQNf~@poJJLWMQ#R>lyw6^W9DZAt!^g}6=f_r-`v8n&}>R80uR z-qR<{Ra)Kpjg8KBeoah^_U+n<_7QAJ{M%(8nHOmQ?FfHeX*CTS8*PF+lH~{_H9K_t z2@Qyz9lC9zM>4ZR^xPHcVI{v9&S_^&t{H2mA-<^Ni!l)5i@KtFlu{tX7xlHhWWSN% ziFapsv7Pmg!x^JyXVHTUigqR&0<$REsVmL?4n)yT*;+VRMG?X;gR|n-|5!TOt*1jYxAfE0^!U96vmxKkx#6AhDA=1O- zfpAAh>l^5i{qjIjj~wI&ih86~4(RwnjWBu+Xdcv4BR#arp>TF5YZ7|o>2j#3M>2Ov zmzo<3v*Z)HuXM8d^&1x*lSh&)10f#Kd{dFN0~Ar+p^8_u zBm|qs!Wo^d;Wdm&^06ezK#0e*V{QvTh{rUyh5iu{+1GUNz3<%Af1E8H@tU^Hs|pD5 zHQmNszkv{6(_Ft_mget5?<*3K`FNAgXr+ee$gc&W}?+BO$c5WWG;IRwPW zTRQ$)3WWNWE;lzpAV%JjQ(`{ejhAGd45xRo<_=?wo|8pAvXoCIw+LoMRC}glIP0U@5&@ns4K@wB8zKDmzz2nNpE=lZr44^Ib71Tj+Rk@0_;HRTP9CV3Iv4 z(Yd56nMKn%U1@F$Ks24x+%BcI=SI9&2ASdlVV+Nl3xs)Iio3vg&!W8) zuB^5SBgflWT)0$pA&a6*#RAG2zNE{OYRekFBnxAu?CBFc^@&b>-`V>2-7L>4n}F;m zT6c-wX%nPRu7vk@x4QLbOxRbF6akUEl5FWfv|f=dJxcBbsoDKbc&NMeW2n*z_dD^H z42&k@J1!9F{SJ5BA4y~rq!kuA;l>`;;t>EsTj<2u1rpjqr?gA7#(-#A==i-7h^B?k z4ZT_Oc6y?x9&}OKMzC9p+K{5>9oe?W6&0Fhnf#D_Kzt&1EE z9jX3AF}c{W&2|pLKcJ~T5dRiCrDm}JBjFBR+)lD5fKj#BsqW@3m)zI#*9<51V`ppK zwN@*uUoFB*9iEo*9;%i1&@#t<)G_ZNFw@H%j%Im%)Jg<_6RzuJ^)3JiAaLSO&p<>2 zC%#Dnu^Kq-J46B0N&w59_=7hHD$oP~jAl#C!wLv>xf6fzu9ea};-pPvloV2+a z0b#CkTAI5g5audz_$v{U#ktxEAG+SUr+;m9q^?f73kY+y<0MR3j;njG_gj;T~ z?*7W12tMKP$xXWXPEq<~(#;?;4K!5&l7T0aZU#bq(xIEBzCdg|Db3u7jme(c=%jug z*7vq%mrX`+qr+(Zuo;)PIEEyU{vOJ{8Fq9NjtE zo~Mex+nw-RmQ`H?pdq#=4FN=Sd$KBkDBUir;&Itzr;39+!hgTXdJLv)0XvFJac76) zm=C+m!rbBThnw%>%`{K#bW*e60cS>@@|YPI1_u#*tl3oDVpl6=b{8zv zHQCD6MU5E|jlgwjbQBS`Fr|P}vt~lFt3(i1kcURrK?LQc@`ize4WRDl*QeY2Uw`iT zeShb7e&=@{-FM$#(;bJ?CH}{U*OeR`=U9x3tNAd^b1XnT3Wm8aFN!i(Fds-10LZGTaB`L7b+Ne>HjM<#Wk?Weo{q^|17 znC|{9emXtONd@Uo3qwAc_QOvzj`6$t8Tn1|W5(;AV2NN^*8d+TlF<4^+lo)7s$pMb z95*&n@@vLil^~igGC`UDABxV02l_}Rbv#p;w|`mra4e5xOjo~E;cmD@@O7q8{rQr5 zp3?%3A9(>Yf~7VP5arjI9(|Hl`uIB2e}Mltkslh5WeQ!snAtGgQTRDl^rN)k_kJYS zW0@YkV?PQ%$1;8U`41E7je$O$NuA3SR(vt1{6-8P%%m=73hu!dvej5#%$N%QS9DP=U0li(-a7b5 z1J87EDd_^Fi%Xf(vM6j2;iXKre}Z&zDKoIje^I)q3G{cF)MJGQ4u$y|48N19=|T@R z(!)IM1d$9P#h{&f5Zig$>zcIUbe`_sBbL(msCIhBjS4B4NF}R}YAHhh#2WeDt@YctW@=$t8$zS&~Z-;WEkP|5_cmLOZ=Hw-kDutSlTj+EzQlDIrn( zm~>r&hZVXs8xNu+lJxB5pLfX9$UwiQQ-5rG|JWRr9f{>@nl-dgoR5@btSK~~c)Es1 zqFR$g1!B1-i3-H$8j0#pR!Hl*LYI^62xZS$mjnY!QxXh_aGfTYQzcQ4q+xyA#goTW z?Z_w*>x*4VBG&78H>DNjdd+UyBGWx8&~NM1rb5M;$I3=w__j>K>Tjh75x&#??#;)=+`mE(ShEg zQ`-us&fZ)$8oMpp1kvd;TE_T(VO(cpo=3*`eiA5%;rmI}Kpeg=S(B0-Exm2kPDRuk zgcP)61fy23N79OPtIn4Djl?o0&^vT$w^o&-at-&-`q}I`4?cANKjaGOb2pZaLAOI< zyN*7_NNhU`P3H#Zd8C7#Ne3W?J9XRvNC!Km1Bq>n#I{R2HZc%V(8dNvt#L7cNOvW} z8Y7G8L+#k(4kE?i^*R~|jP^tAmL`byL*4Bv|23)IF;Z6_7kZyxRl_5leq8KS7ShK_ z(bFoHQuM<~$XM!W?)=dFSnN8q=@JcOtR!Gha!^3{1nodTZ1*HSL45A9TV*;u zRwDg0sX+)SXs-%j)JmRZ{{xYJDm9pw{Ed|)>`mqtLJKBZ@w``dJ*$;L?@dxaR_L=N z^$=1pkxCXmljUWll7-Kb)Q^?w9NAbqfK z_l3u^H)C~Bo8Fu=PI?@79-aDK;q-;E<>P2LqD`g$kxbP%$?Q>Gm~e6XXrAfmXp&bD z)1yhiwV|95Yf_HSJ^qh!js=)yyTU7a9KUv*1un!rdF z*exE!>8mbRg*%CJe4t-h+ltOsJ zb$V5f&rQF*xjr}J;rr(PG8cwj)X3WLQC-k#%MrxW8^s``65fy?{$0+B34#8-oBBh0 zx~uB0em()y-@EbAJwZm+?mE{;AwbjtX!8gXZ@XJ+w>1!HyW4+oVoWdX?hmf>+ohL@ zY~^lh3yJ)lV}lJRR;aS}Mcq_ZWhWwi+hwRo)hrlKDyf=xv~lI%@x&?^caWBhD#`}Fni zj{K19&EdcFPovOC*9Cf;o7xqA*F(LWufuYi z%gXMC&pNrVeBe4e-IB-=O!EiQETvH=DcJ6&Z89NJ4ccLX*lu@w4Ybl|)Z5)_2E|eu zcNC>kRbWp$iFHv~?I>zxq&q}=H?ve9=)btB{cbp=OwBB-$I)M0(}%4vovxRNc86=q z)D1N}lfB)pV>e$A%iXTqEs6)k=We%0;J+%yGM7qZvkn=yVor3#Zr*GwdtK40){^{ZS9?U&7 zCmdF;{?JhGlu!#a+Cm12X`d56-fz_BQE#pM`d&F z(h%rlZt5h+Dp=IG?J=MMW1~kWH;-wTDWN62eS%8=Y(9Qyg?eSz5K!P|fvtXq( zo^YK(QC&1dOAxea2k~^mjc-X{WL%N6SAQ5+CnRf<_mV)LaZ~5ra9%I+ekRF#i3E2x z%KPB_*(Brq$}rE0ww#QYNZLAGr(czutC}_Y-Z(e>Y=4gyy;vdYltrJ7M9?{MU8QqX zbLM`p5nucqtwc031?ObLIj>4d=%heja#P>B;j_Kfsq7?dFS(p-6UpNw3H7oYE~->x zYXEBQa~_Q7MDC;>Itfw4%Hs$AnD4 zBnj`EWC9>E4%+J%i1ZtmjLQiFBK<~^_N46ClLP&0lX}Le%D<}I?T_4d-vcve&Hh=q zv5z{Ros96;hP~=3S!V3TEF3ru_)gculTk4_Nj0%L48L8J=|*_5127JAw^ z_V|a8fNk zK;!|mXE2EEb0)qBfVh3ma0X8%q(+H%Nm!Rtb=5p0Tv8ND8kQspq!r;3N#Nf`Lej7_ zY|p8?5XxmpZcwK^Iq}9Z)j+UEvi=Y)p%T0yd@w&DommEq{C)pA16 zcwL!P1c-8_iSMW&%9VyYYQZYqAiXN=c8z+rw$X7>t}3R{MY+nv>x)*DtE3D+mJM`D zpjVsJAH$EXQ9H|~V7c0GhrWYorbs!g4gYhHx~-09hP5^s7D%LPlVO1f*CxXP>0+%6 zYj)&^#&zN3!RqJukxR_FqMztByy!USB?U5f!r7i3ntfu2fZuD(M zKa#mNQ)(|UwBo1Dl-W;-Ab#2m-{(d|exSF)$A_ru>M7C5`Bsu;5aC;fPYtqmKruDF zD|xw;gwSVucyx%WM<}ll?MaeBgzd(&7YGnxyWs*+8ws&pAO797YIyAwM=D~yaqOp5 z5aD{$-Co{7gzF8L_iZveO@V&Lq&^JayH;(!w+YL4jOi6^0!=bI8;oP`t`M<-_GAL- zWP^!6CW1&en50 zxL(~}!!yDyNsd8;TarQs#Z(G;g)D(4nVqfSOV_Jqd7e>jEh=SpwkESfE6T0XrEI57 z65clB*qbVZA21oUOxL!gLTSa*HdA45bReF#87`kP&rMQ=+l>>>x>o&&@U=xd(;(9A z$xMStx64dFZo_8aFY%=w-pm)hCSt=X+)Ifh? zQlFdl12?EZWv62Mi7^AB7rm)cG#y4>^lEu#+#N}dKrB0w{T;+>hwSfBcBjgu?KR;& zKTf|O#E>eLJHaefKh9yeenX3o-*;r;bxhd(0MLTJIHTUo1TiV@3NJyVRB9G@+AD~lDhv(@aR ze~R{OQi(9yv&C*P?lft;LpM7ai-}g2XJ=B0v?A@4O013|W-iZ}aBYqHVNG+iI?tK3 zy(WPu&zY|FW(lG^Cjs9RDOsEsOxV3v{kXO{I$tj&)dixwV7x>r%khFJrDmsboi?c} zCR|plrcIxQL&HiWz#Wy!sF;@o@%X+2VD#^ous-84~YOPd4Qmo~eSR zUR(trmP@_((hcHssmBU@S$5VL5?^cBFh;ealy%=)R7y0ho@Z~cw4!YF_($Vc1T! diff --git a/clients/rust/opensysml/src/domain.rs b/clients/rust/opensysml/src/domain.rs index 3049715e0..78d6d5550 100644 --- a/clients/rust/opensysml/src/domain.rs +++ b/clients/rust/opensysml/src/domain.rs @@ -254,17 +254,7 @@ impl Array { /// Builds an array, checking that every dimension is positive and that /// the elements fill the dimensions exactly. pub fn new(dimensions: Vec, elements: Vec) -> Result { - let mut size: i64 = 1; - for &extent in &dimensions { - if extent <= 0 { - return Err(Error::Decode(format!( - "array dimension is not positive: {extent}" - ))); - } - size = size.checked_mul(extent).ok_or_else(|| { - Error::Decode(format!("array dimensions {dimensions:?} overflow")) - })?; - } + let size = shape_size("array", &dimensions)?; if u64::try_from(size).ok() != u64::try_from(elements.len()).ok() { return Err(Error::Decode(format!( "array of dimensions {dimensions:?} holds {} element(s), want {size}", @@ -295,17 +285,7 @@ impl Array { /// The element at a multi-index, one coordinate per dimension; `None` when /// the index has the wrong rank or a coordinate is outside its dimension. pub fn get(&self, index: &[i64]) -> Option<&Value> { - if index.len() != self.dimensions.len() { - return None; - } - let mut flat: i64 = 0; - for (&coordinate, &extent) in index.iter().zip(&self.dimensions) { - if coordinate < 0 || coordinate >= extent { - return None; - } - flat = flat * extent + coordinate; - } - self.elements.get(usize::try_from(flat).ok()?) + row_major(&self.dimensions, index).and_then(|flat| self.elements.get(flat)) } } @@ -369,6 +349,153 @@ impl VectorQuantity { } } +/// A unique, unordered collection: a `Collections::Set`'s elements. +/// +/// The service sends the members in its canonical order (numbers ascending, +/// then strings, and so on), each exactly once; two sets are equal when they +/// hold the same members whatever the order. A service advertising +/// `set_values` sends one as itself; an older one sends an unsupported +/// [`Value::Null`] in its place. +#[derive(Clone, Debug)] +pub struct Set { + elements: Vec, +} + +impl Set { + /// Builds a set, refusing one that lists a member twice. + pub fn new(elements: Vec) -> Result { + for (i, element) in elements.iter().enumerate() { + if elements[..i].contains(element) { + return Err(Error::Decode(format!( + "set lists a member twice: {element:?}" + ))); + } + } + Ok(Self { elements }) + } + + /// The members, each once, in the order the service sent them. + pub fn elements(&self) -> &[Value] { + &self.elements + } + + /// Number of members. + pub fn len(&self) -> usize { + self.elements.len() + } + + /// Whether the set has no members. + pub fn is_empty(&self) -> bool { + self.elements.is_empty() + } + + /// Whether `value` is a member. + pub fn contains(&self, value: &Value) -> bool { + self.elements.contains(value) + } +} + +impl PartialEq for Set { + /// Order-insensitive: the same members in any order are the same set. + fn eq(&self, other: &Self) -> bool { + self.len() == other.len() && self.elements.iter().all(|e| other.contains(e)) + } +} + +/// A tensor of quantities of any rank: its shape, and its components +/// flattened in row-major order, each a [`Quantity`] with its own unit. +/// +/// A rank-one tensor stays a tensor, distinct from a [`VectorQuantity`]. A +/// service advertising `tensor_values` sends one as itself; an older one +/// sends an unsupported [`Value::Null`] in its place. +#[derive(Clone, Debug, PartialEq)] +pub struct TensorQuantity { + dimensions: Vec, + components: Vec, +} + +impl TensorQuantity { + /// Builds a tensor, checking that every dimension is positive and that + /// the components fill the dimensions exactly. + pub fn new(dimensions: Vec, components: Vec) -> Result { + let size = shape_size("tensor quantity", &dimensions)?; + if u64::try_from(size).ok() != u64::try_from(components.len()).ok() { + return Err(Error::Decode(format!( + "tensor quantity of dimensions {dimensions:?} holds {} component(s), want {size}", + components.len() + ))); + } + Ok(Self { + dimensions, + components, + }) + } + + /// Extent of each dimension, all positive. + pub fn dimensions(&self) -> &[i64] { + &self.dimensions + } + + /// Number of dimensions. + pub fn rank(&self) -> usize { + self.dimensions.len() + } + + /// The components in row-major order. + pub fn components(&self) -> &[Quantity] { + &self.components + } + + /// The component at a multi-index, one coordinate per dimension; `None` + /// when the index has the wrong rank or a coordinate is outside its + /// dimension. + pub fn get(&self, index: &[i64]) -> Option<&Quantity> { + row_major(&self.dimensions, index).and_then(|flat| self.components.get(flat)) + } + + /// The one unit every component is written in, or `None` when they differ + /// (a rank-0 tensor has exactly one component, so always its unit). + pub fn unit(&self) -> Option<&str> { + let first = &self.components.first()?.unit; + self.components + .iter() + .all(|component| component.unit == *first) + .then_some(first.as_str()) + } +} + +/// The element count a shape describes, refusing a non-positive extent or +/// one that overflows. +fn shape_size(what: &str, dimensions: &[i64]) -> Result { + let mut size: i64 = 1; + for &extent in dimensions { + if extent <= 0 { + return Err(Error::Decode(format!( + "{what} dimension is not positive: {extent}" + ))); + } + size = size + .checked_mul(extent) + .ok_or_else(|| Error::Decode(format!("{what} dimensions {dimensions:?} overflow")))?; + } + Ok(size) +} + +/// The row-major offset of a multi-index, `None` when it is out of shape. +fn row_major(dimensions: &[i64], index: &[i64]) -> Option { + if index.len() != dimensions.len() { + return None; + } + let mut flat: i64 = 0; + for (&coordinate, &extent) in index.iter().zip(dimensions) { + if coordinate < 0 || coordinate >= extent { + return None; + } + flat = flat * extent + coordinate; + } + usize::try_from(flat).ok() +} + /// A measurement unit held as a value by itself, with no magnitude: `SI::m`, /// or `m / s` as an operation composed it. /// @@ -434,6 +561,10 @@ pub enum Value { MeasurementRef(MeasurementRef), /// A calc held as a value. Function(Function), + /// A unique, unordered collection. + Set(Set), + /// A tensor of quantities of any rank. + TensorQuantity(TensorQuantity), /// Explicit null value. Null, /// A materialized feature with no value. @@ -503,6 +634,19 @@ pub(crate) fn value_from_wire(value: wire::Value) -> Result { self_id: (v.self_id != 0).then_some(v.self_id), })) } + wire::value::Kind::Set(v) => Ok(Value::Set(Set::new( + v.elements + .into_iter() + .map(value_from_wire) + .collect::>()?, + )?)), + wire::value::Kind::TensorQuantity(v) => Ok(Value::TensorQuantity(TensorQuantity::new( + v.dimensions, + v.components + .into_iter() + .map(quantity_from_wire) + .collect::>()?, + )?)), wire::value::Kind::EnumLiteral(v) => Ok(Value::EnumLiteral(EnumLiteral { literal_id: v.literal_id, enumeration_id: v.enumeration_id, @@ -541,6 +685,8 @@ fn kind_name(kind: &wire::value::Kind) -> &'static str { wire::value::Kind::VectorQuantity(_) => "vector_quantity", wire::value::Kind::MeasurementRef(_) => "measurement_ref", wire::value::Kind::Function(_) => "function", + wire::value::Kind::Set(_) => "set", + wire::value::Kind::TensorQuantity(_) => "tensor_quantity", } } @@ -1261,6 +1407,194 @@ mod tests { )); } + fn set(elements: Vec) -> wire::Value { + wire::Value { + kind: Some(wire::value::Kind::Set(wire::ValueSet { elements })), + } + } + + fn tensor(dimensions: Vec, components: Vec) -> wire::Value { + wire::Value { + kind: Some(wire::value::Kind::TensorQuantity(wire::TensorQuantity { + dimensions, + components, + })), + } + } + + #[test] + fn a_set_holds_each_member_once_and_compares_in_any_order() { + let Ok(Value::Set(members)) = value_from_wire(set(vec![int(1), int(2), int(3)])) else { + panic!("a set should decode"); + }; + assert_eq!(members.len(), 3); + assert!(!members.is_empty()); + assert_eq!( + members.elements(), + [Value::Integer(1), Value::Integer(2), Value::Integer(3)] + ); + assert!(members.contains(&Value::Integer(2))); + assert!(!members.contains(&Value::Integer(4))); + assert!(!members.contains(&Value::Real(2.0))); + + // The same members in another order are the same set; a sequence is not. + let reordered = Set::new(vec![ + Value::Integer(3), + Value::Integer(1), + Value::Integer(2), + ]) + .expect("three distinct members"); + assert_eq!(members, reordered); + assert_ne!( + Value::Set(members.clone()), + Value::Sequence(vec![ + Value::Integer(1), + Value::Integer(2), + Value::Integer(3) + ]) + ); + assert_ne!( + members, + Set::new(vec![Value::Integer(1), Value::Integer(2)]).expect("two members") + ); + + // An empty set is a set; a set nests. + let Ok(Value::Set(empty)) = value_from_wire(set(vec![])) else { + panic!("an empty set should decode"); + }; + assert!(empty.is_empty()); + let Ok(Value::Set(nested)) = value_from_wire(set(vec![set(vec![int(1)]), set(vec![])])) + else { + panic!("a set of sets should decode"); + }; + assert_eq!(nested.len(), 2); + assert!(nested.contains(&Value::Set(empty))); + let Ok(Value::Sequence(holding)) = value_from_wire(wire::Value { + kind: Some(wire::value::Kind::Sequence(wire::ValueSequence { + elements: vec![set(vec![int(1)]), int(2)], + })), + }) else { + panic!("a sequence holding a set should decode"); + }; + assert!(matches!(holding[0], Value::Set(_))); + + // A member listed twice is not a set. + let twice = value_from_wire(set(vec![int(1), int(1)])); + assert!( + matches!(&twice, Err(Error::Decode(message)) if message.contains("twice")), + "{twice:?}" + ); + assert!(matches!( + value_from_wire(set(vec![wire::Value { kind: None }])), + Err(Error::Decode(message)) if message.contains("no kind") + )); + } + + #[test] + fn a_tensor_quantity_keeps_its_rank_shape_and_row_major_components() { + let Ok(Value::TensorQuantity(cube)) = value_from_wire(tensor( + vec![2, 2, 2], + (1..=8).map(|i| metres(f64::from(i))).collect(), + )) else { + panic!("a (2, 2, 2) tensor should decode"); + }; + assert_eq!(cube.rank(), 3); + assert_eq!(cube.dimensions(), [2, 2, 2]); + assert_eq!(cube.components().len(), 8); + assert_eq!(cube.unit(), Some("m")); + assert_eq!( + cube.get(&[1, 0, 1]).map(|q| q.magnitude), + Some(Magnitude::Real(6.0)) + ); + assert_eq!( + cube.get(&[0, 0, 0]).map(|q| q.magnitude), + Some(Magnitude::Real(1.0)) + ); + assert_eq!( + cube.get(&[1, 1, 1]).map(|q| q.magnitude), + Some(Magnitude::Real(8.0)) + ); + assert_eq!(cube.get(&[1, 1]), None); + assert_eq!(cube.get(&[1, 1, 1, 0]), None); + assert_eq!(cube.get(&[2, 0, 0]), None); + assert_eq!(cube.get(&[0, -1, 0]), None); + + // A rank-one tensor stays a tensor, never a vector quantity. + let Ok(Value::TensorQuantity(line)) = + value_from_wire(tensor(vec![2], vec![metres(1.0), metres(2.0)])) + else { + panic!("a rank-1 tensor should decode"); + }; + assert_eq!(line.rank(), 1); + assert_ne!( + Value::TensorQuantity(line), + Value::VectorQuantity( + VectorQuantity::new(vec![ + Quantity { + magnitude: Magnitude::Real(1.0), + unit: "m".to_owned(), + unit_term: Some(metre_term()), + }, + Quantity { + magnitude: Magnitude::Real(2.0), + unit: "m".to_owned(), + unit_term: Some(metre_term()), + }, + ]) + .expect("two components") + ) + ); + + // Components with differing units report no shared one. + let speed = wire::Quantity { + magnitude: Some(wire::quantity::Magnitude::IntMagnitude(5)), + unit: "m/s".to_owned(), + unit_term: None, + }; + let Ok(Value::TensorQuantity(mixed)) = + value_from_wire(tensor(vec![1, 2], vec![metres(1.0), speed])) + else { + panic!("a mixed tensor should decode"); + }; + assert_eq!(mixed.unit(), None); + assert_eq!( + mixed.get(&[0, 1]).map(|q| q.magnitude), + Some(Magnitude::Integer(5)) + ); + } + + #[test] + fn a_malformed_tensor_quantity_is_refused() { + let short = value_from_wire(tensor( + vec![2, 2], + vec![metres(1.0), metres(2.0), metres(3.0)], + )); + assert!( + matches!(&short, Err(Error::Decode(message)) if message.contains("want 4")), + "{short:?}" + ); + assert!(matches!( + value_from_wire(tensor(vec![], vec![metres(1.0), metres(2.0)])), + Err(Error::Decode(message)) if message.contains("want 1") + )); + assert!(matches!( + value_from_wire(tensor(vec![0], vec![])), + Err(Error::Decode(message)) if message.contains("not positive") + )); + assert!(matches!( + value_from_wire(tensor(vec![-1], vec![metres(1.0)])), + Err(Error::Decode(message)) if message.contains("not positive") + )); + assert!(matches!( + value_from_wire(tensor(vec![i64::MAX, 2], vec![])), + Err(Error::Decode(message)) if message.contains("overflow") + )); + assert!(matches!( + value_from_wire(tensor(vec![1], vec![wire::Quantity::default()])), + Err(Error::Decode(message)) if message.contains("no magnitude") + )); + } + fn measurement_ref( unit: &str, unit_id: &str, diff --git a/clients/rust/opensysml/src/lib.rs b/clients/rust/opensysml/src/lib.rs index f6dfa934e..9c14a7007 100644 --- a/clients/rust/opensysml/src/lib.rs +++ b/clients/rust/opensysml/src/lib.rs @@ -17,7 +17,8 @@ pub use connection::Connection; pub use domain::{ Array, Capabilities, Complex, Diagnostic, EnumLiteral, EvalOptions, Evaluation, FeatureValue, Function, Instance, Instantiation, Language, Magnitude, MeasurementRef, Model, ParseOptions, - Quantity, ServerInfo, Span, Symbol, UnitFactor, UnitTerm, Value, Vector, VectorQuantity, + Quantity, ServerInfo, Set, Span, Symbol, TensorQuantity, UnitFactor, UnitTerm, Value, Vector, + VectorQuantity, }; pub use error::{Error, Status}; diff --git a/clients/rust/opensysml/src/proto/sysml/sysml.rs b/clients/rust/opensysml/src/proto/sysml/sysml.rs index 220041c69..2d1040b81 100644 --- a/clients/rust/opensysml/src/proto/sysml/sysml.rs +++ b/clients/rust/opensysml/src/proto/sysml/sysml.rs @@ -840,7 +840,7 @@ pub struct AttributeInfo { /// Value represents a runtime-evaluable value #[derive(Clone, PartialEq, ::prost::Message)] pub struct Value { - #[prost(oneof="value::Kind", tags="1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17")] + #[prost(oneof="value::Kind", tags="1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19")] pub kind: ::core::option::Option, } /// Nested message and enum types in `Value`. @@ -895,6 +895,12 @@ pub mod value { /// a calc as a value, named by its declaration #[prost(message, tag="17")] Function(super::Function), + /// distinct elements with no order of their own + #[prost(message, tag="18")] + Set(super::ValueSet), + /// shape and one Quantity per component + #[prost(message, tag="19")] + TensorQuantity(super::TensorQuantity), } } /// Function is a calc held as a value: a calc definition, or a calc usage with @@ -916,6 +922,33 @@ pub struct Function { #[prost(int64, tag="2")] pub self_id: i64, } +/// ValueSet is a unique, unordered collection — a Collections::Set's elements — +/// as distinct from a ValueSequence, whose order is part of its value. Two sets +/// are equal when they hold the same elements in any order. The service sends +/// the elements in the runtime's canonical order (Booleans, numbers, strings, +/// quantities, enumeration literals, objects, each class in its own order), so +/// equal sets cross alike; a client may send them in any order, but sending an +/// element twice is rejected rather than read as one, since a repeated element +/// is what a sequence carries. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ValueSet { + #[prost(message, repeated, tag="1")] + pub elements: ::prost::alloc::vec::Vec, +} +/// TensorQuantity is a Quantities::TensorQuantityValue of any rank: its +/// dimensions and, flattened in row-major order under them, one Quantity per +/// component, each with its unit and reduction as a scalar Quantity carries them. +/// A tensor of rank one is not a VectorQuantity, on the wire as in the runtime. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct TensorQuantity { + /// Positive extents, one per rank; their product (one for rank 0) is how many + /// components there are, and a tensor not filling them is rejected. + #[prost(int64, repeated, tag="1")] + pub dimensions: ::prost::alloc::vec::Vec, + /// A named unit sent without its unit_term is rejected as a Quantity's is. + #[prost(message, repeated, tag="2")] + pub components: ::prost::alloc::vec::Vec, +} /// Array is a Collections::Array: its elements flattened in row-major order /// under its dimensions, compared by content rather than by the object read. #[derive(Clone, PartialEq, ::prost::Message)] @@ -1141,6 +1174,18 @@ pub struct ServerInfoResponse { /// unsupported null, and one is accepted as an action input /// or calc argument; without it, one is refused with /// UNIMPLEMENTED rather than read as another value. + /// "set_values" - a Value carries a unique, unordered collection (a + /// Collections::Set's elements) as set, each element once in + /// canonical order, rather than reporting it as an + /// unsupported null, and one is accepted as an action input + /// or calc argument in any order; without it, one is refused + /// with UNIMPLEMENTED rather than read as a sequence. + /// "tensor_values" - a Value carries a tensor quantity of any rank as + /// tensor_quantity, its dimensions and one Quantity per + /// row-major component, rather than reporting it as an + /// unsupported null, and one is accepted as an action input + /// or calc argument; without it, one is refused with + /// UNIMPLEMENTED rather than read as another value. /// "apply_edits" - the ApplyEdits RPC edits a parsed model's own source, /// preserving everything the edit did not touch. /// "document_query" - the RunDocumentQuery RPC runs a named document query diff --git a/clients/rust/opensysml/tests/client.rs b/clients/rust/opensysml/tests/client.rs index bf322873d..1820aaa08 100644 --- a/clients/rust/opensysml/tests/client.rs +++ b/clients/rust/opensysml/tests/client.rs @@ -385,6 +385,75 @@ fn a_calc_held_as_a_value_arrives_as_the_function_it_names() { assert!(scale.self_id.is_some_and(|id| id > 0)); } +#[test] +fn a_set_arrives_once_per_member_and_a_tensor_with_its_rank() { + let Some(connection) = service_or_skip() else { + return; + }; + assert!(connection.capabilities().has("set_values")); + assert!(connection.capabilities().has("tensor_values")); + let model = match connection.parse_content( + "package W { + private import ScalarValues::*; + private import Collections::*; + private import Quantities::*; + private import MeasurementReferences::*; + private import SI::*; + private import TensorCalculations::*; + attribute s : Set { :>> elements = (3, 1, 2, 2, 3); } + attribute e : Set { :>> elements = (); } + attribute cubeRef : TensorMeasurementReference { + :>> dimensions = (2, 2, 2); + :>> mRefs = (Pa, Pa, Pa, Pa, Pa, Pa, Pa, Pa); + } + attribute cube : TensorQuantityValue = + TensorCalculations::'['((1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0), cubeRef); + }", + &Default::default(), + ) { + Ok(model) => model, + Err(error) => panic!("parse failed: {error}"), + }; + let eval = |expr: &str| match model.evaluate(expr, &EvalOptions::default()) { + Ok(evaluation) => evaluation.result, + Err(error) => panic!("evaluating {expr} failed: {error}"), + }; + + let Value::Set(s) = eval("W::s.elements") else { + panic!("W::s.elements should be a set"); + }; + assert_eq!(s.len(), 3); + assert_eq!( + s.elements(), + [Value::Integer(1), Value::Integer(2), Value::Integer(3)] + ); + assert!(s.contains(&Value::Integer(3))); + let Value::Set(e) = eval("W::e.elements") else { + panic!("W::e.elements should be a set"); + }; + assert!(e.is_empty()); + + let Value::TensorQuantity(cube) = eval("W::cube") else { + panic!("W::cube should be a tensor quantity"); + }; + assert_eq!(cube.rank(), 3); + assert_eq!(cube.dimensions(), [2, 2, 2]); + assert_eq!(cube.unit(), Some("Pa")); + assert_eq!( + cube.components() + .iter() + .map(|component| component.magnitude) + .collect::>(), + (1..=8) + .map(|i| Magnitude::Real(f64::from(i))) + .collect::>() + ); + assert_eq!( + cube.get(&[1, 0, 1]).map(|q| q.magnitude), + Some(Magnitude::Real(6.0)) + ); +} + #[test] fn blocking_calls_work_inside_a_runtime() { let Some(connection) = service_or_skip() else { diff --git a/cmd/conformance/pkgclient.go b/cmd/conformance/pkgclient.go index 6570a963d..c46dcba9e 100644 --- a/cmd/conformance/pkgclient.go +++ b/cmd/conformance/pkgclient.go @@ -802,6 +802,18 @@ func valueToProto(value opensysml.Value) *pb.Value { vq.Components = append(vq.Components, quantityToProto(component)) } return &pb.Value{Kind: &pb.Value_VectorQuantity{VectorQuantity: vq}} + case opensysml.Set: + set := &pb.ValueSet{} + for _, element := range v { + set.Elements = append(set.Elements, valueToProto(element)) + } + return &pb.Value{Kind: &pb.Value_Set{Set: set}} + case opensysml.TensorQuantity: + tensor := &pb.TensorQuantity{Dimensions: append([]int64(nil), v.Dimensions...)} + for _, component := range v.Components { + tensor.Components = append(tensor.Components, quantityToProto(component)) + } + return &pb.Value{Kind: &pb.Value_TensorQuantity{TensorQuantity: tensor}} case opensysml.MeasurementRef: return &pb.Value{Kind: &pb.Value_MeasurementRef{MeasurementRef: &pb.MeasurementRef{ Unit: v.Unit, @@ -896,6 +908,22 @@ func valueFromProto(value *pb.Value) (opensysml.Value, bool) { vq = append(vq, quantityFromProto(component)) } return vq, true + case *pb.Value_Set: + set := make(opensysml.Set, 0, len(kind.Set.GetElements())) + for _, element := range kind.Set.GetElements() { + converted, ok := valueFromProto(element) + if !ok { + return nil, false + } + set = append(set, converted) + } + return set, true + case *pb.Value_TensorQuantity: + tensor := opensysml.TensorQuantity{Dimensions: append([]int64(nil), kind.TensorQuantity.GetDimensions()...)} + for _, component := range kind.TensorQuantity.GetComponents() { + tensor.Components = append(tensor.Components, quantityFromProto(component)) + } + return tensor, true case *pb.Value_MeasurementRef: return opensysml.MeasurementRef{ Unit: kind.MeasurementRef.GetUnit(), diff --git a/conformance/fixtures/set_tensor.sysml b/conformance/fixtures/set_tensor.sysml new file mode 100644 index 000000000..d3bb2d30e --- /dev/null +++ b/conformance/fixtures/set_tensor.sysml @@ -0,0 +1,14 @@ +package T { + private import ScalarValues::*; + private import Collections::*; + private import Quantities::*; + private import MeasurementReferences::*; + private import SI::*; + attribute s : Set { :>> elements = (3, 1, 2, 2, 3); } + attribute cubeRef : TensorMeasurementReference { :>> dimensions = (2, 2, 2); :>> mRefs = (m, m, m, m, m, m, m, m); } + attribute cube : TensorQuantityValue = TensorCalculations::'['((1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0), cubeRef); + calc def Members { in c : Integer[0..*]; return : Integer = SequenceFunctions::size(c); } + calc members : Members; + calc def Corner { in t : TensorQuantityValue; return : ScalarQuantityValue = t#(2, 1, 2); } + calc corner : Corner; +} diff --git a/conformance/scenarios/01-server-info.json b/conformance/scenarios/01-server-info.json index 62f2d4b08..fbf329777 100644 --- a/conformance/scenarios/01-server-info.json +++ b/conformance/scenarios/01-server-info.json @@ -27,6 +27,8 @@ "structured_values", "measurement_refs", "function_values", + "set_values", + "tensor_values", "apply_edits", "strict_conformance", "feature_values", diff --git a/conformance/scenarios/04-evaluate.json b/conformance/scenarios/04-evaluate.json index 556fac213..3fbcf92bc 100644 --- a/conformance/scenarios/04-evaluate.json +++ b/conformance/scenarios/04-evaluate.json @@ -322,6 +322,182 @@ } } }, + { + "id": "evaluate/a_set_arrives_as_its_members_in_canonical_order", + "description": "A Set's elements are unique and unordered, so they travel in their own arm with every repeat dropped and in the runtime's canonical order, not as written.", + "rpc": "Evaluate", + "requires_capabilities": [ + "set_values" + ], + "model": { + "fixture": "set_tensor.sysml" + }, + "request": { + "model_hash": "${model_hash}", + "expression": "T::s.elements" + }, + "expect": { + "response": { + "result": { + "set": { + "elements": [ + { + "int_value": 1 + }, + { + "int_value": 2 + }, + { + "int_value": 3 + } + ] + } + } + } + } + }, + { + "id": "evaluate/a_tensor_quantity_arrives_with_its_shape", + "description": "A tensor quantity of any rank travels as its dimensions and one Quantity per row-major component, so a client can rebuild a rank-three shape rather than receive a flat sequence.", + "rpc": "Evaluate", + "requires_capabilities": [ + "tensor_values" + ], + "model": { + "fixture": "set_tensor.sysml" + }, + "request": { + "model_hash": "${model_hash}", + "expression": "T::cube" + }, + "expect": { + "response": { + "result": { + "tensor_quantity": { + "dimensions": [ + 2, + 2, + 2 + ], + "components": [ + { + "real_magnitude": 1.0, + "unit": "m", + "unit_term": { + "factors": [ + { + "unit_id": "SI::metre", + "exponent": 1 + } + ], + "scale_num": 1, + "scale_den": 1 + } + }, + { + "real_magnitude": 2.0, + "unit": "m", + "unit_term": { + "factors": [ + { + "unit_id": "SI::metre", + "exponent": 1 + } + ], + "scale_num": 1, + "scale_den": 1 + } + }, + { + "real_magnitude": 3.0, + "unit": "m", + "unit_term": { + "factors": [ + { + "unit_id": "SI::metre", + "exponent": 1 + } + ], + "scale_num": 1, + "scale_den": 1 + } + }, + { + "real_magnitude": 4.0, + "unit": "m", + "unit_term": { + "factors": [ + { + "unit_id": "SI::metre", + "exponent": 1 + } + ], + "scale_num": 1, + "scale_den": 1 + } + }, + { + "real_magnitude": 5.0, + "unit": "m", + "unit_term": { + "factors": [ + { + "unit_id": "SI::metre", + "exponent": 1 + } + ], + "scale_num": 1, + "scale_den": 1 + } + }, + { + "real_magnitude": 6.0, + "unit": "m", + "unit_term": { + "factors": [ + { + "unit_id": "SI::metre", + "exponent": 1 + } + ], + "scale_num": 1, + "scale_den": 1 + } + }, + { + "real_magnitude": 7.0, + "unit": "m", + "unit_term": { + "factors": [ + { + "unit_id": "SI::metre", + "exponent": 1 + } + ], + "scale_num": 1, + "scale_den": 1 + } + }, + { + "real_magnitude": 8.0, + "unit": "m", + "unit_term": { + "factors": [ + { + "unit_id": "SI::metre", + "exponent": 1 + } + ], + "scale_num": 1, + "scale_den": 1 + } + } + ] + } + } + } + } + }, { "id": "evaluate/a_named_measurement_reference_arrives_with_its_declaration", "description": "A unit held by itself travels in one arm carrying the unit as written, its reduction to base units and the declaration it names, so a client can hand it back as a conversion target.", diff --git a/conformance/scenarios/10-evaluate-calc.json b/conformance/scenarios/10-evaluate-calc.json index 23b680851..d49febb0d 100644 --- a/conformance/scenarios/10-evaluate-calc.json +++ b/conformance/scenarios/10-evaluate-calc.json @@ -91,6 +91,219 @@ "status_message_contains": "structured_values" } }, + { + "id": "evaluate_calc/a_set_argument_reaches_the_calc_as_its_members", + "description": "A set sent as an argument reaches the calc as its members, so size counts them however the client ordered them.", + "rpc": "EvaluateCalc", + "requires_capabilities": [ + "set_values" + ], + "model": { + "fixture": "set_tensor.sysml" + }, + "request": { + "model_hash": "${model_hash}", + "symbol_id": "T::members", + "arguments": [ + { + "set": { + "elements": [ + { + "int_value": 7 + }, + { + "int_value": 5 + }, + { + "int_value": 9 + } + ] + } + } + ] + }, + "expect": { + "response": { + "result": { + "int_value": 3 + } + }, + "absent": [ + "error" + ] + }, + "expect_without_capability": { + "status": "UNIMPLEMENTED", + "status_message_contains": "set_values" + } + }, + { + "id": "evaluate_calc/a_tensor_argument_is_indexed_by_its_shape", + "description": "A tensor sent as an argument reaches the calc with its rank-three shape, so one index per dimension selects the row-major component the shape puts there.", + "rpc": "EvaluateCalc", + "requires_capabilities": [ + "tensor_values" + ], + "model": { + "fixture": "set_tensor.sysml" + }, + "request": { + "model_hash": "${model_hash}", + "symbol_id": "T::corner", + "arguments": [ + { + "tensor_quantity": { + "dimensions": [ + 2, + 2, + 2 + ], + "components": [ + { + "real_magnitude": 1.0, + "unit": "m", + "unit_term": { + "factors": [ + { + "unit_id": "SI::metre", + "exponent": 1 + } + ], + "scale_num": 1, + "scale_den": 1 + } + }, + { + "real_magnitude": 2.0, + "unit": "m", + "unit_term": { + "factors": [ + { + "unit_id": "SI::metre", + "exponent": 1 + } + ], + "scale_num": 1, + "scale_den": 1 + } + }, + { + "real_magnitude": 3.0, + "unit": "m", + "unit_term": { + "factors": [ + { + "unit_id": "SI::metre", + "exponent": 1 + } + ], + "scale_num": 1, + "scale_den": 1 + } + }, + { + "real_magnitude": 4.0, + "unit": "m", + "unit_term": { + "factors": [ + { + "unit_id": "SI::metre", + "exponent": 1 + } + ], + "scale_num": 1, + "scale_den": 1 + } + }, + { + "real_magnitude": 5.0, + "unit": "m", + "unit_term": { + "factors": [ + { + "unit_id": "SI::metre", + "exponent": 1 + } + ], + "scale_num": 1, + "scale_den": 1 + } + }, + { + "real_magnitude": 6.0, + "unit": "m", + "unit_term": { + "factors": [ + { + "unit_id": "SI::metre", + "exponent": 1 + } + ], + "scale_num": 1, + "scale_den": 1 + } + }, + { + "real_magnitude": 7.0, + "unit": "m", + "unit_term": { + "factors": [ + { + "unit_id": "SI::metre", + "exponent": 1 + } + ], + "scale_num": 1, + "scale_den": 1 + } + }, + { + "real_magnitude": 8.0, + "unit": "m", + "unit_term": { + "factors": [ + { + "unit_id": "SI::metre", + "exponent": 1 + } + ], + "scale_num": 1, + "scale_den": 1 + } + } + ] + } + } + ] + }, + "expect": { + "response": { + "result": { + "quantity": { + "real_magnitude": 6.0, + "unit": "m", + "unit_term": { + "factors": [ + { + "unit_id": "SI::metre", + "exponent": 1 + } + ], + "scale_num": 1, + "scale_den": 1 + } + } + } + }, + "absent": [ + "error" + ] + }, + "expect_without_capability": { + "status": "UNIMPLEMENTED", + "status_message_contains": "tensor_values" + } + }, { "id": "evaluate_calc/a_measurement_reference_argument_is_the_unit_the_calc_converts_to", "description": "A bare unit sent as an argument reaches the calc as a measurement reference ConvertQuantity accepts as its target, so 3 km converts to 3000 m.", diff --git a/docs/project/native-compilation.md b/docs/project/native-compilation.md index a8239cd76..5351e68a6 100644 --- a/docs/project/native-compilation.md +++ b/docs/project/native-compilation.md @@ -59,7 +59,9 @@ A calc compiles when everything it reaches is in this subset: | Scalar library functions: `RealFunctions`/`RationalFunctions`/`NumericalFunctions` `sqrt floor round abs max min isZero isUnit`, `IntegerFunctions`/`NaturalFunctions` `abs max min`, `TrigFunctions` (`sin cos tan cot arcsin arccos arctan deg rad pi`), `OpenSysMLMathFunctions` (`exp ln log atan2`) | `libm` / Go `math` with the interpreter's domain, overflow and `Natural` errors | Everything else refuses: String, record (`attribute def`) and enum parameters, results or -attributes, parameter defaults, a calc that `:>`/`:>>`/`redefines` another *and* declares members +attributes, a `Collections::Set` (or any collection object) and a `TensorQuantityValue` wherever +they appear (`type Collections::Set is not Integer, Real or Boolean`; a set has no native layout +and a tensor's components are quantities), parameter defaults, a calc that `:>`/`:>>`/`redefines` another *and* declares members (redefining inherited parameters or body is not compiled), sequences whose elements mix Integer and Real (`==`, `same`, `union` between an `Integer[0..*]` and a `Real[0..*]`, `Integer[0..*] ?? 5.5`), a `collect` body that yields null, a `select` body that is not Boolean, `===` between a Real and an diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index 1d5cbf107..eb972e178 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -685,6 +685,7 @@ by name is refused naming what is missing rather than approximated. | A literal crossing the API boundary keeps its identity: it travels as `Value.enum_literal` carrying the declaration FQN, the enumeration's FQN and the qualified rendering, and an incoming literal is resolved against the model it names rather than reconstructed, so a literal no declaration of that model matches is an error and never a null | `grpc/convert.go` `enumLiteralToProto`/`enumLiteralFromProto`, `api/proto/sysml.proto` `EnumLiteral`; Python `opensysml/enumeration.py` `EnumLiteral`, `values.py`, `connection.py` `_python_to_value` | `grpc/convert_enum_test.go:TestEnumLiteralToProto`, `TestEnumLiteralRoundTrip`, `TestEnumLiteralRoundTripInSequence`, `TestEnumLiteralUnresolvedIsAnError`, `TestInstantiate_EnumTypedFeatureValueCarriesLiteral`; `clients/python/tests/test_enumeration.py`, `test_wire_compat.py:test_enum_literal_is_an_added_value_arm` | ✅ Faithful (advertised as the `enum_values` capability; a literal of an enumeration specializing a scalar crosses as that scalar, as it evaluates to one) | | A Complex crosses the API boundary as one value: `Value.complex` carries the real and imaginary parts as two doubles, so `1.0 + 2.0i` cannot be read as a sequence of two Reals, and an incoming `complex` decodes to one `ValComplex`. Every client this repository ships decodes it to one native number — Go `opensysml.Complex`, Python `complex`, Node `{ kind: "complex" }`, Java `Value.ComplexValue`, Rust `Value::Complex` — and renders it in rectangular form | `grpc/convert.go` `ComplexToProto`/`ProtoToComplex`, `capability_response.go` (`complex_values` arm), `api/proto/sysml.proto` `Complex`; `client/opensysml/value.go` `Complex`; `clients/python/opensysml/values.py`, `connection.py` `_python_to_value`; `clients/node/src/core/values.ts`; `clients/java/.../Value.java` `ComplexValue`, `internal/Protos.java`; `clients/rust/opensysml/src/domain.rs` `Complex` | `grpc/convert_complex_test.go`, `client/opensysml/complex_test.go`, `clients/python/tests/test_complex.py`, `clients/node/test/values.test.ts`, `clients/java/.../ProtosTest.java`, `clients/rust/opensysml/tests/client.rs`; `conformance/scenarios/04-evaluate.json`, `05-instantiate.json` (`complex.sysml`) over gRPC, Connect protobuf and Connect JSON | ✅ Faithful (advertised as the `complex_values` capability; a service withholding it reports an unsupported null naming the number, and a client built before the arm existed reads it as an unknown `Value` arm) | | An Array, a Vector and a VectorQuantity cross the API boundary whole, in both directions: `Value.array` carries `dimensions` and the elements flattened in row-major order (each element a `Value`, so an array of quantities or of arrays nests), `Value.vector` carries the numeric components as `Value`s so an Integer and a Real component stay distinct, and `Value.vector_quantity` carries one `Quantity` per component — magnitude, unit as written and reduced unit term, so a composed unit (`m/s`) and per-component units survive. Every client this repository ships maps them to a native shape that checks its own invariants — Go `opensysml.Array`/`Vector`/`VectorQuantity`, Python `Array`/`Vector`/`VectorQuantity` dataclasses, Node `{ kind: "array" | "vector" | "vectorQuantity" }`, Java `Value.ArrayValue`/`VectorValue`/`VectorQuantityValue`, Rust `Value::Array`/`Vector`/`VectorQuantity` — and one sent as an action input or calc argument decodes to the same runtime value; a malformed one (dimensions the elements do not fill, a non-positive extent, a non-numeric vector component, an empty vector quantity, a unit without its reduction) is a typed error on whichever side sees it first, never a value with a different shape | `grpc/convert.go` `arrayToProto`/`protoToArray`, `vectorToProto`/`protoToVector`, `vectorQuantityToProto`/`protoToVectorQuantity`, `ErrArrayDimensionNotPositive`, `ErrArrayShapeMismatch`, `ErrVectorComponentNotNumeric`, `ErrVectorQuantityEmpty`; `capability_response.go` (`structured_values` arm); `api/proto/sysml.proto` `Array`, `Vector`, `VectorQuantity`; `client/opensysml/value.go`, `convert.go`, `client.go` (`structured_values` preflight); `clients/python/opensysml/values.py`, `connection.py`; `clients/node/src/core/values.ts`; `clients/java/.../Value.java`, `internal/Protos.java`; `clients/rust/opensysml/src/domain.rs` | `grpc/convert_structured_test.go`, `client/opensysml/structured_test.go`, `structured_internal_test.go`, `clients/python/tests/test_structured.py`, `clients/node/test/values.test.ts`, `client.test.ts`, `clients/java/.../ProtosTest.java`, `ApiIntegrationTest.java`, `clients/rust/opensysml/src/domain.rs` tests, `tests/client.rs`; `conformance/scenarios/04-evaluate.json`, `10-evaluate-calc.json` (`structured.sysml`) over gRPC, Connect protobuf and Connect JSON | ✅ Faithful (advertised as the `structured_values` capability; a service withholding it reports an unsupported null naming the value and refuses one sent to it with `UNIMPLEMENTED`, the clients refusing before the round trip; a client built before the arms existed reads each as an unknown `Value` arm) | +| A set and a tensor quantity cross the API boundary whole, in both directions: `Value.set` carries the members as `Value`s (so a set of quantities, of objects or of sets nests), listed in canonical order and readable in any order, and `Value.tensor_quantity` carries `dimensions` and one `Quantity` per row-major component — magnitude, unit as written and reduced unit term — at any rank, a rank-one tensor staying a `tensor_quantity` rather than becoming a `vector_quantity`. Every client this repository ships maps them to a native shape that checks its own invariants — Go `opensysml.Set`/`TensorQuantity`, Python `SetValue`/`TensorQuantity`, Node `{ kind: "set" \| "tensorQuantity" }`, Java `Value.SetValue`/`TensorQuantityValue`, Rust `Value::Set`/`TensorQuantity` — each set order-insensitively equal and refusing a repeated member, each tensor indexed row-major with one index per dimension; a malformed one (a repeated set member, a non-positive dimension, components that do not fill the dimensions, a component without its quantity) is a typed error on whichever side sees it first | `grpc/convert.go` `setToProto`/`protoToSet`, `tensorQuantityToProto`/`protoToTensorQuantity`, `CheckTensorShape`, `ValueCarriesSet`, `ValueCarriesTensor`, `ErrSetElementRepeated`, `ErrTensorDimensionNotPositive`, `ErrTensorShapeMismatch`, `ErrTensorComponentMissing`; `capability_response.go` (`set_values`, `tensor_values` arms); `api/proto/sysml.proto` `ValueSet`, `TensorQuantity`; `client/opensysml/value.go`, `convert.go`, `client.go` (the two preflights); `clients/python/opensysml/values.py`, `connection.py`; `clients/node/src/core/values.ts`; `clients/java/.../Value.java`, `internal/Protos.java`; `clients/rust/opensysml/src/domain.rs` | `grpc/convert_set_tensor_test.go`, `client/opensysml/set_tensor_test.go`, `structured_internal_test.go`, `clients/python/tests/test_set_tensor.py`, `clients/node/test/values.test.ts`, `client.test.ts`, `clients/java/.../ProtosTest.java`, `PublicTypesTest.java`, `ApiIntegrationTest.java`, `clients/rust/opensysml/src/domain.rs` tests, `tests/client.rs`; `conformance/scenarios/01-server-info.json`, `04-evaluate.json`, `10-evaluate-calc.json` (`set_tensor.sysml`) over gRPC, Connect protobuf and Connect JSON | ✅ Faithful (advertised as the `set_values` and `tensor_values` capabilities, each on its own; a service withholding one reports an unsupported null naming the value and refuses one sent to it — nested anywhere in the argument — with `UNIMPLEMENTED`, the clients refusing before the round trip; a client built before the arms existed reads each as an unknown `Value` arm) | | A bare measurement reference crosses the API boundary whole, in both directions: `Value.measurement_ref` carries the unit as written, its reduced unit term (required wherever the unit names one, as `Quantity` requires it) and the canonical id of the declaration a named unit is (`SI::metre`), which a composed unit (`m / s`, a `DerivedUnit`) omits. Every client this repository ships maps it to a typed reference — Go `opensysml.MeasurementRef`, Python `MeasurementRef` (over `Unit`), Node `{ kind: "measurementRef" }`, Java `Value.MeasurementRefValue`, Rust `Value::MeasurementRef` — and one sent as a calc argument decodes to the same `ValMeasurementRef`, so `ConvertQuantity(q, ref)` converts through it; a malformed one (no unit and no id, a named unit without its reduction, an id naming no unit declaration, a reduction disagreeing with the declaration's own) is a typed error on whichever side sees it first | `grpc/convert.go` `MeasurementRefToProto`/`ProtoToMeasurementRef`/`declaredMeasurementRef`, `ValueCarriesMeasurementRef`, `ErrMeasurementRefEmpty`, `ErrMeasurementRefNeedsIndex`; `capability_response.go` (`measurement_refs` arm); `api/proto/sysml.proto` `MeasurementRef`; `client/opensysml/value.go`, `convert.go`, `client.go` (`measurement_refs` preflight); `clients/python/opensysml/values.py`, `connection.py`; `clients/node/src/core/values.ts`; `clients/java/.../Value.java`, `internal/Protos.java`; `clients/rust/opensysml/src/domain.rs` | `grpc/convert_measurement_ref_test.go`, `client/opensysml/measurement_ref_test.go`, `clients/python/tests/test_measurement_ref.py`, `clients/node/test/values.test.ts`, `client.test.ts`, `clients/java/.../ProtosTest.java`, `ApiIntegrationTest.java`, `clients/rust/opensysml/src/domain.rs` tests, `tests/client.rs`; `conformance/scenarios/01-server-info.json`, `04-evaluate.json`, `10-evaluate-calc.json` (`measurement_ref.sysml`) over gRPC, Connect protobuf and Connect JSON | ✅ Faithful (advertised as the `measurement_refs` capability, separate from `structured_values` so a client built against those arms keeps reading a bare reference as `unsupported: measurement reference m`; a service withholding it reports that unsupported null and refuses one sent to it with `UNIMPLEMENTED`, the clients refusing before the round trip) | | A quantity crosses the API boundary in both directions: `Value.quantity` carries the magnitude with the kind it was written in (Integer or Real), the unit as written and the reduced unit term, so a quantity read from the service can be sent back as an input and evaluates against the unit it names — commensurability is decided over the reduction, so a unit named without one is refused client-side rather than compared by bare magnitude | `opensysml/values.py` `Quantity.to_pb`, `Unit.to_pb`/`Unit.reduced`, `connection.py` `_python_to_value`; service side `grpc/convert.go` `ProtoToQuantity`, `ProtoToValueIn` | `clients/python/tests/test_quantity.py`: `test_a_quantity_encodes_as_the_message_the_service_decodes`, `test_an_unreduced_unit_is_refused_before_it_is_sent`, and against a live service `TestQuantityAgainstTheService::test_a_quantity_sent_as_a_calc_argument_round_trips`, `::test_a_quantity_input_binds_into_an_action`, `::test_a_sent_quantity_is_commensurable_with_the_models_own_units` | ✅ Faithful | | A function value crosses the API boundary as `Value.function`: `calc_id`, the qualified name of the calc it is a value of, and `self_id`, the id (within the answering response) of the object it was read off, 0 for none — identity being the pair. One sent as an argument (`EvaluateCalc`, `ExecuteAction`, `RunAnalysis`) is rebound to that calc of the named model and invoked through the calc-typed parameter it binds; an empty `calc_id`, one naming no calc, or any non-zero `self_id` is refused in band (`ErrFunctionUnbound`), never a null — objects live only within the call that created them, so a `self_id` is never matched to whichever object a later call numbered the same — and a function value nested in a sequence or array is found wherever it sits. A value closing over a behavior body's bindings cannot be named by calc and object, so it crosses as `unsupported: function ` (`ErrFunctionNeedsRuntime` on the way in). Every client this repository ships maps the arm to a typed value — Go `opensysml.Function`, Python `opensysml.Function`, Node `{ kind: "function" }`, Java `Value.FunctionValue`, Rust `Value::Function` — and refuses to send one to a service lacking the capability | `grpc/convert.go` `functionToProto`/`functionFromProto`/`ProtoToRuntimeValue`, `ValueCarriesFunction`, `ErrFunctionUnbound`, `ErrFunctionNeedsRuntime`; `capability_response.go` (`function_values` arm); `service.go` `CapabilityFunctionValues`; `api/proto/sysml.proto` `Function`; `client/opensysml/value.go`, `convert.go`, `client.go` (`function_values` preflight); `clients/python/opensysml/values.py`, `connection.py`; `clients/node/src/core/values.ts`; `clients/java/.../Value.java`, `internal/Protos.java`; `clients/rust/opensysml/src/domain.rs` | `grpc/convert_function_test.go:TestFunctionRoundTrip`, `:TestObjectBoundFunctionsDoNotCrossCalls`, `:TestMalformedFunctionsAreRejected`, `:TestFunctionCapability`, `:TestValueCarriesFunction`; `client/opensysml/function_test.go`; `clients/python/tests/test_function.py`; `clients/node/test/values.test.ts`, `client.test.ts`; `clients/java/.../ProtosTest.java`, `ApiIntegrationTest.java`; `clients/rust/opensysml/tests/client.rs`; `conformance/scenarios/01-server-info.json`, `04-evaluate.json`, `10-evaluate-calc.json` (`function.sysml`) over gRPC, Connect protobuf and Connect JSON | ✅ Faithful (advertised as the `function_values` capability; a service withholding it reports the unsupported null and refuses a function argument with `UNIMPLEMENTED`, the clients refusing before the round trip) | @@ -1069,13 +1070,34 @@ the flat sequence of its elements: an order-two product is not), and `TensorCalculations::transform` (needs a coordinate frame); each names itself and the reason. It is carried over by `Adopt` with every component's unit rebound (`TestAdoptRebindsATensorsComponentUnits`), - refused by the solver as a non-scalar, refused as a document-query binding, and - crosses gRPC as an unsupported null naming the value - (`unsupported: tensor quantity Tensor(2, 2)[…]`; - `TestTensorQuantityCrossesAsUnsupported`): the wire `Value` has no tensor arm. - Conformance `instance_tensor_quantity`, `instance_tensor_quantity_failures`; - robustness `tensor_quantity_failure_modes`; `TestTensorQuantity*`, - `TestTensorCalculationResultTypes`, `TestTensorCalculationsBindToTheirDeclaredTypes`. + refused by the solver as a non-scalar and refused as a document-query binding. + **Rank is unbounded**: a `TensorMeasurementReference` with `:>> dimensions = + (2, 2, 2)` and eight `mRefs` builds a rank-three tensor, `#` takes exactly + three indexes (`cube#(2, 1, 2)` is the sixth row-major component), and the + shape checks are the `Array`'s at every rank — too few or too many indexes + `ErrMultiplicityViolation` naming the count and the rank, an index below 1 or + above its dimension `ErrIndexOutOfRange` naming the index and the range, a + non-Integer index `ErrTypeMismatch`, a component count off `flattenedSize` + `ErrMultiplicityViolation`, and `'+'`/`'-'` between two shapes + `ErrMultiplicityViolation` naming both. Arithmetic and printing keep the shape + (`Tensor(2, 2, 2)[…] [Pa]`), and the trace renders the same text + (`FormatTraceValue`). It **crosses gRPC whole** as the `tensor_quantity` arm — + `dimensions` and one `Quantity` per row-major component, advertised as the + `tensor_values` capability; a rank-one tensor stays a `tensor_quantity`, not a + `vector_quantity` (*Values* in the gRPC rows below). It is **not compiled + natively**: `sysml -compile` refuses a calc that declares or builds one with + `codegen.UnsupportedError` (`type Quantities::TensorQuantityValue is not + Integer, Real or Boolean`), and it has **no RDF literal form** of its own — + the mapping writes the model, so a tensor-valued feature is its + `TensorCalculations::'['` invocation and an indexing its `#` tree, both round + tripping exactly ([the mapping](../reference/rdf-mapping.md#expressions)). + Conformance `instance_tensor_quantity`, `instance_tensor_quantity_failures`, + `instance_tensor_rank_three`, `instance_tensor_rank_three_failures`; + robustness `tensor_quantity_failure_modes` (every rank-three failure); + `TestTensorQuantity*`, `TestTensorQuantityRankThree`, + `TestTensorCalculationResultTypes`, `TestTensorCalculationsBindToTheirDeclaredTypes`; + `repl/compile_test.go:TestCompileRefusesWhatItCannotCompile` (`TensorParam`, + `TensorBuilt`); `export/set_tensor_rdf_test.go:TestSetAndTensorValuesRoundTripAsExpressions`. - **`ValCoordinateFrame`** (`coordinate_frame.go`, `frame.go`, `frame_read.go`) is a **coordinate frame or a measurement scale as a value**. The OMG corpus this project pins populates frames — Annex A's SimpleVehicleModel @@ -1361,6 +1383,11 @@ they cannot drift apart. | `includingAt` inserts the values before the 1-based index, shifting the tail right, so the result is longer than the input by the values inserted; `index == size + 1` appends, and any other index outside `1..size + 1` is a typed error (`ErrIndexOutOfRange`) | `runtime/collections.go` `builtinSequenceIncludingAt`, registered in `runtime/builtins.go` | `runtime/collections_test.go` `TestCollectionResults`, `TestCollectionOperationErrors`, `robustness_test.go:testNumericLibraryCallThatHasNoValue`; conformance `calc_sequence_including_at`, `calc_sequence_including_at_appends`, `calc_sequence_including_at_out_of_range`, pilot-exec-diff `w6d:including-at`, `:including-at-appends`, `:including-at-out-of-range` (the reference throws `IndexOutOfBoundsException` out of the vendored body for all three, the valid insertion included) | ⚠️ Approximate: the vendored body drops the element at `index` instead of shifting it right, which would leave `addAt` removing and the library with no insertion. Insertion is implemented on the maintainer's ruling and the vendored body is recorded as an OMG source bug ([omg-issues.md](omg-issues.md)) | | An endpoint of `subsequence` or `excludingAt` that is outside the sequence is a typed error (`ErrIndexOutOfRange`), while an *empty range* inside it is the empty sequence, which is how `tail` is `subsequence(seq, 2)` of a one-element sequence | `runtime/collections.go` `builtinSequenceSubsequence`, `builtinSequenceExcludingAt` | `runtime/collections_test.go` `TestCollectionOperationErrors`, `TestCollectionResults`, pilot-exec-diff `w6d:excluding-at`, `:subsequence` (both agree), `:excluding-at-out-of-range`, `:subsequence-out-of-range` (the reference fails too), `:subsequence-empty-range` | ✅ Faithful (the in-range results match the pinned artifact and it fails on both out-of-range endpoints rather than answering the vendored truncation — by exception out of the library body, where this reports `ErrIndexOutOfRange`. The empty-range half is unrefereeable: the reference emits nothing, which it cannot distinguish from the empty sequence this answers) | | `CollectionFunctions`: `size`, `isEmpty`, `notEmpty`, `contains`, `containsAll`, `head`, `tail`, `last`, `#` over a collection's elements, a set included | `runtime/collections.go`, `runtime/builtins.go` | `runtime/collections_test.go` `TestCollectionScalarResults`, `TestCollectionOperationsOverSets` | ✅ Faithful | +| A `Collections::Collection` whose library redefinition of `elements` is unique and not ordered holds a **set** (`ValSet`), the library's own kind: `Set` ("unique and unordered", `Collections.kerml:104-108`), `UniqueCollection` ("unique and not necessarily ordered", `:39-48`) and `Map` (unique `KeyValuePair`s, `:142-152`). `Bag` inherits the root's `nonunique` (`:22`, `:97-101`) and stays a sequence, as does every `OrderedCollection` (`:30-36`) — `Array`, `List`, `OrderedSet` (`ordered`, `:111-121`), `OrderedMap` (`:162-175`) — and any feature a model declares `ordered` or `nonunique` itself. A set holds each member once (`(3, 1, 2, 2, 3)` is three members, `(1, 2, 3)` given already distinct is the same three, `()` the empty set), so `size` answers 3 and `isEmpty` reads the count; a value written or bound to such a feature is admitted as a set, and a set flowing into a feature that holds a sequence (`ordered`, `nonunique`, or a plain `Integer[0..*]`) becomes the sequence of its members in canonical order | `runtime/set_feature.go` `holdsSet`, `declaredOrderedOrNonunique`, `collectionOf`, `declaredCollection`; `shape.go` `EffectiveFeature.HoldsSet`; `instance.go` `admitted`, `materializeIntrinsic`; `subsetting.go` (optional subsetters); `value.go` `Set`, `NewSet`, `Set.Add`, `Set.Size` | conformance `library_set_elements`, `library_set_elements_already_distinct`, `library_set_elements_empty`, `library_unique_collection_elements`, `library_map_elements`, `library_bag_elements`, `library_ordered_set_elements`, `library_set_operations`; `runtime/set_feature_test.go:TestCollectionElementsHoldTheLibraryKind`, `:TestSetFlowsIntoDeclaredCollections`, `:TestCollectionFunctionsOverCollectionObjects` | ✅ Faithful | +| Two sets are equal when their members are, in whatever order either was given (`CollectionFunctions::'=='` is `col1.elements->equals(col2.elements)`, and a set's `elements` have no order to compare); `contains` and `containsAll` are membership; a set's elements equal a sequence only when the sequence lists the members in canonical order, and a `Set` is never equal to a `Bag` or `OrderedSet` holding the same values, which are sequences | `runtime/eval.go` `valueEqual` (`ValSet` arm); `runtime/collections.go` `builtinCollectionEquals`, `elementsOf`; `value.go` `Set.Equal`, `Set.Contains` | conformance `library_set_operations` (`equalRegardlessOfOrder`, `elementsEqualRegardlessOfOrder`, `membership`, `allMembers`, `emptiness`, `notASequence`); `runtime/set_feature_test.go:TestSetAgainstSequenceComparesCanonically`, `:TestCollectionFunctionsOverCollectionObjects` | ✅ Faithful | +| A set has no order of its own, so every operation that walks its members in order — `collect`, `select`, `head`, `tail`, `#`, `==` against a sequence, a trace rendering it, a write into a sequence-holding feature, the wire — sees them in one **canonical order**: by class (null, Booleans with `false` first, numbers ascending, complex numbers by real then imaginary part, strings lexicographically, quantities by dimension then magnitude, enumeration literals, objects by identity, then every other kind) and within a class by that order, the trace rendering breaking what remains. The order is total, so equal sets enumerate alike and a trace over a set is the same golden however the set was written | `runtime/set_order.go` `canonicalLess`, `canonicalClass`; `value.go` `Set.Elements` (sorted once, cached); `trace.go` `FormatTraceValue` (`ValSet`, the same enumeration) | conformance `calc_set_consumed_by_ordered_operations` and its trace golden; `runtime/set_feature_test.go:TestCanonicalOrderIsTotal`, `:TestSetRendersCanonically`; `runtime/collections_test.go:TestCollectionOperationsOverSets` | ✅ Faithful — the order is this runtime's documented rule, since the library defines none; it is not a claim about the specification | +| What the library declares ordered stays a sequence: every `SequenceFunctions` result is `Anything[0..*] ordered nonunique` — `union`, `intersection`, `including`, `includingAt`, `excluding` included (`SequenceFunctions.kerml:48-63`) — so `union(s.elements, t.elements)` over two `Set`s is the ordered concatenation of their canonical members, repeats kept, not a set; `(s.elements, s.elements)` likewise lists each member twice. The Kernel Function Library declares no `distinct` function, so none is invented: the members of a sequence, each once, are what a `Set`'s `elements` hold | `runtime/collections.go` (`SequenceFunctions` builtins over `elementsOf`) | conformance `library_set_sequence_functions`, `library_set_operations` (`ordered`, `repeatable`, `plain`) | ✅ Faithful | +| A set **crosses gRPC** as the `set` arm (`ValueSet.elements`, each a `Value`, listed in canonical order; an incoming set may list them in any order, and one that repeats a member is `ErrSetElementRepeated`), advertised as `set_values`; a service withholding the capability answers an unsupported null naming the value and refuses one sent to it with `UNIMPLEMENTED`, nested anywhere in the argument. It is **not compiled natively**: `sysml -compile` refuses a calc declaring or reading one with `codegen.UnsupportedError` (`type Collections::Set is not Integer, Real or Boolean`). It has **no RDF literal form** of its own: the mapping writes the model, so a `Set`-valued feature is the expression valuing its `elements`, which round trips exactly | `grpc/convert.go` `setToProto`, `protoToSet`, `ErrSetElementRepeated`; `grpc/capability_response.go` (`set_values`); `codegen/compile.go` `UnsupportedError`; `export/rdf_expr.go` | `grpc/convert_set_tensor_test.go:TestSetRoundTrip`, `:TestMalformedSetsAreRejected`, `:TestSetAndTensorCapabilities`, `:TestValueCarriesSetAndTensor`; `repl/compile_test.go:TestCompileRefusesWhatItCannotCompile` (`SetParam`, `SetElements`, `SetLocal`); `export/set_tensor_rdf_test.go:TestSetAndTensorValuesRoundTripAsExpressions`; the *Values* gRPC rows below | ✅ Faithful (the native and RDF refusals are typed and documented, not layouts) | | `CollectionFunctions::'array#'(arr, indexes)` and `BaseFunctions::'#'` with several indexes select from a **`Collections::Array`** value (`ValArray`, *Structured values* under *KerML Function Library*) by one `Positive` index per dimension in the row-major order `Collections.kerml` documents — `'array#'(a, (2, 1))` over `dimensions = (2, 3)` is the fourth element, as the pinned pilot evaluator answers; a vector or vector quantity is indexed as the one-dimensional Array it specializes; a rank-0 array with no index is null, as the library body says. The count of indexes must be the array's `rank` (`ErrMultiplicityViolation`, naming `arr.rank`), each index within `1..dimensions#(i)` (`ErrIndexOutOfRange`, naming the dimension and its range), and a usage whose `elements` do not fill its `dimensions` is `ErrMultiplicityViolation` naming `flattenedSize`; `rank`, `flattenedSize`, `dimensions` and `elements` of the value read out of it | `runtime/collections.go` `builtinArrayIndex`, `arrayIndex`, `builtinBaseIndex`; `runtime/array.go` `Array.at`, `structuredFeature`, `Context.arrayOfObject`, `Context.declaredArrayValue` | conformance `calc_library_array_value`, `calc_library_array_features`, `calc_library_array_index`, `calc_library_array_index_rank_mismatch`, `calc_library_array_index_out_of_range`, `calc_library_array_empty_rank_zero`, `calc_library_array_specialization_members`, `calc_library_array_specialization_through_calc`, `calc_library_base_index_many`, `calc_library_base_index_many_sequence`; robustness `base_index_with_several_indexes`, `numeric_library_call_that_has_no_value` (`'array#'` over a flat sequence); `repl/runtime_commands_test.go:TestEvalArrayShapedByItsFeatures`; `TestEveryValueKindIsDispatched` | ✅ Faithful | | An operation over an empty collection answers the empty collection and never calls its body, since there is no element to call it with | `runtime/collections.go` `elementsOf` (an empty collection yields no elements) | conformance `calc_collection_ops_over_empty`; `runtime/collections_test.go` `TestCollectionResults` | ✅ Faithful | | `ControlFunctions`: `collect`, `select`, `selectOne`, `reject`, `reduce`, `forAll`, `exists`, `allTrue`, `anyTrue`, `minimize`, `maximize` | `runtime/collections.go`, `runtime/builtins.go` | `runtime/collections_test.go` `TestCollectionResults`, `TestCollectionScalarResults`, `TestCollectionOperationErrors` | ✅ Faithful | @@ -1398,7 +1425,7 @@ wrong answer: | An `Array` written as a *sequence* (`attribute m : Matrix = (1, 2, 3, 4)`) | A sequence of numbers is not an Array — it has no `dimensions` — and binding one to an Array-typed usage is the type mismatch it always was; the library's own shape, `dimensions` and `elements` redefined on the usage, is the one way to write an Array value, and there is no literal notation for one in the language to read. | | A reducer named rather than written (`->reduce min`, as the library's own `minimize` is defined) | A function-valued *name* is not a runtime value: `reduce` takes the body expression form (`->reduce {in a; in b; …}`), and `minimize`/`maximize` are implemented directly rather than through `reduce min`. A named reducer is reported as a type error, not read as a body. | | `SequenceFunctions::add`/`addAt`/`remove`/`removeAt`, `CollectionFunctions` mutators | These are `behavior`s, not functions: they declare an `inout` sequence, so they need mutable accumulation the language layer does not have. Deliberately out of scope. | -| Set coverage in the conformance corpus | No expression of the language produces a set today — a `ValSet` arises only through the embedding API — so the operations over a set are pinned at the unit level (`TestCollectionOperationsOverSets`, `elementsOf`) rather than by a `.sysml` fixture. A set-valued expression is separate work. | +| The uniqueness of an *ordered* unique feature (`OrderedSet::elements`, `OrderedMap::elements`, any multi-valued feature not declared `nonunique`) | Their order is part of the value, so they are held as the sequence written, and the runtime enforces `unique` on no ordered feature: `OrderedSet { :>> elements = (1, 1, 2); }` reads three elements. A set is the one place uniqueness is the value's own definition rather than a constraint on what a feature may hold; checking the constraint on ordered features is separate work. | | `at`, `first`, `reverse` | Not declared by the Kernel Function Library at all (`head`, `#(1)` and `last` are the declared spellings). Not implemented rather than invented. | ### OpenSysML Math Extension Library (non-normative) diff --git a/docs/reference/rdf-mapping.md b/docs/reference/rdf-mapping.md index 84d55454c..6e3ef9bf9 100644 --- a/docs/reference/rdf-mapping.md +++ b/docs/reference/rdf-mapping.md @@ -726,6 +726,8 @@ The rules the tree follows: (`size(ae) == (if isEmpty(af) ? 0 else 2) and …`, `(p ?? q) implies r`, `(a + b)[1]`, `(x as T).f`, `- (1 + 2) ** 2`, `not (p and q)`), one that binds as tightly or tighter is not (`a + b * c`, `if p ? x else - x`, `p hastype T or q`). + An index encloses a sequence, so a multi-dimensional index is written bare + (`cube#(2, 1, 2)`, `m[1, 2]`), never `cube#((2, 1, 2))`. A conditional, being the loosest form, is parenthesized wherever it is an operand or the condition of another conditional; as the operand of `**` the left side must bind tighter than exponentiation, so `(a ** b) ** c` @@ -757,6 +759,26 @@ Tests: `w6g4_rdf_expr_test.go` (structure, ordering, per-position identity, legacy literals, foreign trees, unsupported shapes, round-trip exactness), `result_expression_test.go` (expression bodies, their parameters and members). +### Set and tensor values + +The mapping states a model, not an evaluation of it, so a value the runtime +holds as a set (a `Collections::Set`'s `elements`, any unique, unordered +collection) or as a tensor of any rank (`Quantities::TensorQuantityValue`) +has **no literal form** in RDF. What the graph carries is the expression the +feature is written with — the `(3, 1, 2, 2, 3)` valuing `elements`, the +`TensorCalculations::'['(…, cubeRef)` building the tensor, the `cube#(2, 1, 2)` +indexing it — as the typed tree above, and evaluating the model read back gives +the same set or tensor, in the runtime's canonical order. No `xsd` datatype or +`sysx:` vocabulary encodes an evaluated collection or a tensor's shape, and the +RDF conversion never evaluates: a graph that wanted to state a set's members or a +tensor's components would have to state the expression that yields them. The +gRPC service is where evaluated values travel (the `set` and `tensorQuantity` +arms of [the wire contract](wire-contract.md)). + +Tests: `set_tensor_rdf_test.go` (exactness with and without the source text, +the expression trees a set-valued and a tensor-valued feature state, the +absence of any evaluated form). + ### Result expressions A calculation, case, analysis or verification body may end in a bare expression, diff --git a/docs/reference/wire-contract.md b/docs/reference/wire-contract.md index 7345151cc..569adaf69 100644 --- a/docs/reference/wire-contract.md +++ b/docs/reference/wire-contract.md @@ -192,10 +192,10 @@ Note that `not_found` is also the status for an unknown *symbol* on some methods which (`model not found:`, `symbol not found:`, `file not found:`), and a client that recovers by re-parsing must read it. -## `Value`: sixteen arms, exactly one present +## `Value`: eighteen arms, exactly one present Every value the engine returns — an expression result, a feature of an instance, an action -output, a state-machine context variable — is a `Value`, which is a proto `oneof` of sixteen +output, a state-machine context variable — is a `Value`, which is a proto `oneof` of eighteen arms. In JSON that is **an object with exactly one key**, and the key is the discriminator. A decoder therefore does not look for a `kind` field: it looks at which key is present. The arms, each captured from `Evaluate` against the model at the end of this section: @@ -219,11 +219,14 @@ arms, each captured from `Evaluate` against the model at the end of this section | `measurementRef` | object | `{"result":{"measurementRef":{"unit":"m","unitTerm":{…},"unitId":"SI::metre"}}}` | A measurement reference on its own: a unit, its reduction, and the declaration it names | | `infinity` | boolean | `{"result":{"infinity":true}}` | The unbounded value `*`, which is no number and no string | | `function` | object | `{"result":{"function":{"calcId":"F::Sq"}}}` | A calc held as a value: the calc it names and, when it was read off an object, that object | +| `set` | object | `{"result":{"set":{"elements":[{"intValue":"1"},{"intValue":"2"},{"intValue":"3"}]}}}` | Unordered collection without duplicates; `elements` are `Value`s, listed in canonical order | +| `tensorQuantity` | object | `{"result":{"tensorQuantity":{"dimensions":["2","2","2"],"components":[{"realMagnitude":1,"unit":"m","unitTerm":{…}},…]}}}` | Tensor of quantities of any rank; one `quantity` body per component, row-major | The `array`, `vector` and `vectorQuantity` rows were captured against `conformance/fixtures/structured.sysml` (`S::grid`, `S::v`, `S::d`), `measurementRef` against `conformance/fixtures/measurement_ref.sysml` (`M::u`), `function` against -`conformance/fixtures/function.sysml` (`F::pick`); the rest against the model below, with requests of the form +`conformance/fixtures/function.sysml` (`F::pick`), `set` and `tensorQuantity` against +`conformance/fixtures/set_tensor.sysml` (`T::s.elements`, `T::cube`); the rest against the model below, with requests of the form `{"modelHash":"59c4…a654","expression":"","contextSymbolId":"Rover"}` with `rover.count`, `1.0 / 3.0`, `rover.armed`, `"abc"`, `rover.wheel`, `rover.tags`, `null`, `rover.speed`, `Mode::idle`, `rover.serial` and `rover.z`, and the model was: @@ -292,6 +295,11 @@ decode(v): function → calc := v.function.calcId, require it non-empty, else an error; self := v.function.selfId when present and not "0", an opaque reference under the instanceId rule (this response only), else no object + set → map decode over v.set.elements (absent elements = the empty set); require + no two equal, else an error; keep it a set, not a list + tensorQuantity → shape v.tensorQuantity.dimensions (parse each as int64, every one positive); + components := map the quantity rule over v.tensorQuantity.components; + require len(components) == product(dimensions), else an error anything else → an error: a newer service than this decoder ``` @@ -498,6 +506,48 @@ $ … /Evaluate -d '{"modelHash":"e587…f81e","expression":"F::scaler"}' A service without it sends every function, at any depth, as `{"null":"unsupported: function "}` and refuses a request that carries one. +**`set`.** `elements` is a list of `Value`s with no two equal — what a `Collections::Set`'s +`elements`, or any collection the library declares unique and not ordered, evaluates to: + +```console +$ … /Evaluate -d '{"modelHash":"c409…1a4a","expression":"T::s.elements"}' +{"result":{"set":{"elements":[{"intValue":"1"},{"intValue":"2"},{"intValue":"3"}]}}} +``` + +- The model wrote `(3, 1, 2, 2, 3)`; the set has three members. The service lists them in the + engine's **canonical order** — the order `FormatTraceValue` prints and every ordered + operation on a set reads (nulls, then Booleans, numbers by value, complex numbers, strings, + quantities, enumeration literals, then objects by identity) — so two equal sets are sent + identically, but the order carries no meaning and a client must not read one into it. +- A `set` is not a `sequence`: `(1, 2) == (2, 1)` is false, the sets they populate are equal. + A client compares sets by membership and sends one back in any order it likes. +- An empty set has no `elements` key (default omission). A `set` listing a member twice is + refused on both sides, as is a `set` where the model wants a sequence's order or a sequence + where it wants a set; a set flowing into an ordered parameter is read in canonical order. +- Members nest: a set of sets, or of arrays, needs no second encoding. + +**`tensorQuantity`.** `dimensions` is the shape, `components` the quantities flattened in +row-major order, each a `quantity` body with its own magnitude, `unit` and `unitTerm`: + +```console +$ … /Evaluate -d '{"modelHash":"c409…1a4a","expression":"T::cube"}' +{"result":{"tensorQuantity":{"dimensions":["2","2","2"],"components":[{"realMagnitude":1,"unit":"m","unitTerm":{"scaleNum":1,"scaleDen":1,"factors":[{"unitId":"SI::metre","exponent":1}]}},{"realMagnitude":2,"unit":"m","unitTerm":{…}},…,{"realMagnitude":8,"unit":"m","unitTerm":{…}}]}}} + +$ … /Evaluate -d '{"modelHash":"c409…1a4a","expression":"T::cube#(2, 1, 2)"}' +{"result":{"quantity":{"realMagnitude":6,"unit":"m","unitTerm":{"scaleNum":1,"scaleDen":1,"factors":[{"unitId":"SI::metre","exponent":1}]}}}} +``` + +- `dimensions` follows the `array` rule: `int64` strings, every extent positive, the component + count their product, checked on both sides; the component at `(i, j, k)` of a `(2, 2, 2)` + tensor is `components[(i*2 + j)*2 + k]`. Indexing in the model is one-based and needs one + index per dimension — `T::cube#(2, 1, 2)` above is the sixth component — and the wrong + count or an index outside its dimension is an evaluation failure, not a value. +- A rank-one tensor is a `tensorQuantity`, not a `vectorQuantity`: the model's + `TensorQuantityValue` and `VectorQuantityValue` are different types and stay apart on the + wire. A `vectorQuantity` never arrives with `dimensions`. +- The unit is per component, as in `vectorQuantity`; a component without its `unitTerm` is + refused by the rule under `quantity`. + ### What a client must not do - **Do not compare enum literals by `name`.** Compare `literalId`. @@ -521,6 +571,11 @@ $ … /Evaluate -d '{"modelHash":"e587…f81e","expression":"F::scaler"}' - **Do not send back a `function` that carries a `selfId`.** It is an `instanceId`, with that arm's lifetime: no later call holds the object, and the service refuses the function rather than guess. Only a function over no object (`selfId` absent or `"0"`) is an argument. +- **Do not read a `set` as a `sequence`, or its element order as meaning anything.** Two sets + are equal when their members are; the order the service lists them in is canonical, not + significant, and a `set` sent back may list them in any order — but never twice. +- **Do not index a `tensorQuantity` before checking `len(components) == product(dimensions)`**, + and do not read a rank-one tensor as a `vectorQuantity`. ## Three places a failure can be @@ -952,6 +1007,29 @@ without `measurement_refs` a `measurementRef` argument, and one without `functio `function` argument, with the `unimplemented` Connect error instead, naming the capability; check `GetServerInfo` first. +A `set` or a `tensorQuantity` argument goes back the same way; a set's members reach a +multi-valued parameter in canonical order, and a repeated member or a tensor whose components +do not fill its dimensions is the same kind of in-body failure: + +```console +$ … /EvaluateCalc -d '{"modelHash":"c409…1a4a","symbolId":"T::members","arguments":[{"set":{"elements":[{"intValue":"7"},{"intValue":"5"},{"intValue":"9"}]}}]}' +{"result":{"intValue":"3"}} + +$ … /EvaluateCalc -d '{"modelHash":"c409…1a4a","symbolId":"T::members","arguments":[{"set":{"elements":[{"intValue":"7"},{"intValue":"7"}]}}]}' +{"error":"calc argument could not be read: set element is repeated: element 2, 7","failureReason":"FAILURE_REASON_EVALUATION"} + +$ … /EvaluateCalc -d '{"modelHash":"c409…1a4a","symbolId":"T::corner","arguments":[{"tensorQuantity":{"dimensions":["2","2","3"],"components":[…the eight above…]}}]}' +{"error":"calc argument could not be read: tensor components do not fill its dimensions: 8 elements under dimensions [2 2 3] (flattenedSize 12)","failureReason":"FAILURE_REASON_EVALUATION"} + +$ … /EvaluateCalc -d '{"modelHash":"c409…1a4a","symbolId":"T::corner","arguments":[{"tensorQuantity":{"dimensions":["0"],"components":[]}}]}' +{"error":"calc argument could not be read: tensor dimension is not positive: dimension 1 is 0","failureReason":"FAILURE_REASON_EVALUATION"} +``` + +A service without `set_values` refuses a `set` argument and one without `tensor_values` a +`tensorQuantity` — nested anywhere in the argument — the same way, and answers a set or a +tensor it cannot send as the non-empty `null` arm (`{"null":"unsupported: set Set{1, 2, 3}"}`), +the rule under `null`. + A calc *usage* whose output features are evaluated from its own members (no `arguments`) answers them as `outputs`, a list of `{"name":…,"value":}` in declaration order, in place of `result`; a client reads whichever of the two is present. A symbol that is not a calc diff --git a/internal/core/export/precedence.go b/internal/core/export/precedence.go index d23225d08..098226aed 100644 --- a/internal/core/export/precedence.go +++ b/internal/core/export/precedence.go @@ -57,6 +57,8 @@ func positionBinding(property string) int { type operand struct { text string binding int + // elements is the comma-separated list a sequence operand encloses. + elements string } // at writes the operand where it must bind at least as tightly as min. diff --git a/internal/core/export/rdf_expr.go b/internal/core/export/rdf_expr.go index c861c2837..d25fccf66 100644 --- a/internal/core/export/rdf_expr.go +++ b/internal/core/export/rdf_expr.go @@ -813,15 +813,16 @@ func (d *decoder) operatorForm(node rdf.Term, in *element) (operand, error) { infix, isInfix := infixBinding[operator] switch { case operator == opSequence: - return primary("(" + joinOperands(args, bindConditional) + ")") + elements := joinOperands(args, bindConditional) + return operand{text: "(" + elements + ")", binding: bindPrimary, elements: elements}, nil case operator == opIf && len(args) == 3: // The condition is read below the conditional form; either branch may be one. text := "if " + args[0].at(bindNullCoalesce) + " ? " + args[1].at(bindConditional) + " else " + args[2].at(bindConditional) return operand{text: text, binding: bindConditional}, nil case operator == opIndex && len(args) == 2: - return primary(args[0].at(bindPrimary) + "[" + args[1].text + "]") + return primary(args[0].at(bindPrimary) + "[" + indexText(args[1]) + "]") case operator == opAt && len(args) == 2: - return primary(args[0].at(bindPrimary) + "#(" + args[1].text + ")") + return primary(args[0].at(bindPrimary) + "#(" + indexText(args[1]) + ")") case hasType && len(args) == 1 && isInfix: return operand{text: args[0].at(infix) + " " + operator + " " + typeArgument, binding: infix}, nil case hasType && len(args) == 0: @@ -847,6 +848,15 @@ func (d *decoder) operatorForm(node rdf.Term, in *element) (operand, error) { } } +// indexText writes an index: the brackets enclose a sequence, so a +// multi-dimensional one is its bare elements. +func indexText(index operand) string { + if index.elements != "" { + return index.elements + } + return index.text +} + func (d *decoder) invocationText(node rdf.Term, in *element) (string, error) { function, err := d.expressionReference(node, rdf.SysML+pFunction, in, "an invocation names the function it invokes") diff --git a/internal/core/export/set_tensor_rdf_test.go b/internal/core/export/set_tensor_rdf_test.go new file mode 100644 index 000000000..91bbf310b --- /dev/null +++ b/internal/core/export/set_tensor_rdf_test.go @@ -0,0 +1,80 @@ +package export_test + +import ( + "strings" + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/core/export" +) + +// setTensorModel values a Set from a repeating sequence and builds a rank-three +// tensor: what the runtime holds as a set and a tensor is, in the model, the +// expression each feature is written with. +const setTensorModel = `package P { + private import ScalarValues::*; + private import Collections::*; + private import Quantities::*; + private import MeasurementReferences::*; + private import SI::*; + attribute s : Set { :>> elements = (3, 1, 2, 2, 3); } + attribute e : Set { :>> elements = (); } + attribute nested : Set { :>> elements = (s, e, s); } + attribute cubeRef : TensorMeasurementReference { + :>> dimensions = (2, 2, 2); + :>> mRefs = (Pa, Pa, Pa, Pa, Pa, Pa, Pa, Pa); + } + attribute cube : TensorQuantityValue = TensorCalculations::'['((1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0), cubeRef); + attribute corner : Real = cube#(2, 1, 2); +} +` + +// A set or a tensor has no literal form in RDF: the graph states the expression +// the feature is written with, and the runtime evaluates it again after the +// hop. The trip must therefore be exact, with and without the source text. +func TestSetAndTensorValuesRoundTripAsExpressions(t *testing.T) { + turtle := roundTripsExactly(t, setTensorModel) + text := string(turtle) + for _, want := range []string{ + `sysml:redefines "elements"`, + "a sysml:OperatorExpression ;\n sysx:sourceText \"(3, 1, 2, 2, 3)\"", + `sysx:sourceText "()"`, + "a sysml:InvocationExpression ;\n sysx:sourceText \"TensorCalculations::'['((1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0), cubeRef)\"", + `sysml:function "TensorCalculations::["`, + `sysx:sourceText "cube#(2, 1, 2)"`, + } { + if !strings.Contains(text, want) { + t.Errorf("graph lacks %q:\n%s", want, text) + } + } + for _, never := range []string{"Set{", "Tensor(", "xsd:integer\", \"", "urn:opensysml:set", "urn:opensysml:tensor"} { + if strings.Contains(text, never) { + t.Errorf("graph states an evaluated value %q, which the mapping does not define:\n%s", never, text) + } + } + + stripped := withoutSourceText(t, turtle) + back, err := export.Convert("m.ttl", stripped, export.FormatTurtle, export.FormatSysML) + if err != nil { + t.Fatalf("back to notation from the expression trees alone: %v", err) + } + for _, want := range []string{ + "redefines elements = (3, 1, 2, 2, 3);", + "redefines elements = null;", + "redefines elements = (s, e, s);", + "redefines dimensions = (2, 2, 2);", + "= TensorCalculations::'['((1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0), cubeRef);", + "= cube#(2, 1, 2);", + } { + if !strings.Contains(string(back), want) { + t.Errorf("the expression trees alone should spell %q\n--- notation ---\n%s", want, back) + } + } + again, err := export.Convert("m.sysml", []byte(back), export.FormatSysML, export.FormatTurtle) + if err != nil { + t.Fatalf("to turtle again: %v", err) + } + if lost, gained := tripleSetDiff(t, stripped, withoutSourceText(t, again)); len(lost)+len(gained) > 0 { + t.Errorf("the expression trees alone changed the graph\n--- lost ---\n%s\n--- gained ---\n%s", + strings.Join(lost, "\n"), strings.Join(gained, "\n")) + } +} diff --git a/internal/core/runtime/testdata/conformance/library_set_sequence_functions.expected.json b/internal/core/runtime/testdata/conformance/library_set_sequence_functions.expected.json new file mode 100644 index 000000000..162f57d2e --- /dev/null +++ b/internal/core/runtime/testdata/conformance/library_set_sequence_functions.expected.json @@ -0,0 +1,35 @@ +{ + "type": "instance", + "libraries": true, + "instantiate": "test", + "slots": { + "both": {"type": "Sequence", "elements": [ + {"type": "Integer", "value": 1}, + {"type": "Integer", "value": 2}, + {"type": "Integer", "value": 3}, + {"type": "Integer", "value": 2}, + {"type": "Integer", "value": 4} + ]}, + "common": {"type": "Sequence", "elements": [ + {"type": "Integer", "value": 2} + ]}, + "more": {"type": "Sequence", "elements": [ + {"type": "Integer", "value": 1}, + {"type": "Integer", "value": 2}, + {"type": "Integer", "value": 3}, + {"type": "Integer", "value": 2} + ]}, + "fewer": {"type": "Sequence", "elements": [ + {"type": "Integer", "value": 1}, + {"type": "Integer", "value": 3} + ]}, + "joined": {"type": "Sequence", "elements": [ + {"type": "Integer", "value": 1}, + {"type": "Integer", "value": 2}, + {"type": "Integer", "value": 3}, + {"type": "Integer", "value": 1}, + {"type": "Integer", "value": 2}, + {"type": "Integer", "value": 3} + ]} + } +} diff --git a/internal/core/runtime/testdata/conformance/library_set_sequence_functions.sysml b/internal/core/runtime/testdata/conformance/library_set_sequence_functions.sysml new file mode 100644 index 000000000..b86f3c79b --- /dev/null +++ b/internal/core/runtime/testdata/conformance/library_set_sequence_functions.sysml @@ -0,0 +1,17 @@ +// Every SequenceFunctions result is declared `ordered nonunique` +// (SequenceFunctions.kerml: union, intersection, including, excluding), so a +// set given to one is read as the sequence of its members in canonical order +// and the result is a sequence, repeats and all — never a set. +package test { + private import Collections::*; + private import SequenceFunctions::*; + + attribute s : Set { :>> elements = (3, 1, 2, 2); } + attribute t : Set { :>> elements = (2, 4); } + + attribute both : Integer[0..*] ordered nonunique = union(s.elements, t.elements); + attribute common : Integer[0..*] ordered nonunique = intersection(s.elements, t.elements); + attribute more : Integer[0..*] ordered nonunique = including(s.elements, 2); + attribute fewer : Integer[0..*] ordered nonunique = excluding(s.elements, 2); + attribute joined : Integer[0..*] ordered nonunique = (s.elements, s.elements); +} diff --git a/internal/grpc/capability_response.go b/internal/grpc/capability_response.go index dab040a50..e85470447 100644 --- a/internal/grpc/capability_response.go +++ b/internal/grpc/capability_response.go @@ -110,6 +110,20 @@ func (s *Service) filterValueCapabilities(value *pb.Value) { if !s.capabilities.has(CapabilityInfinityValue) { value.Kind = &pb.Value_Null{Null: "unsupported: unbounded value *"} } + case *pb.Value_Set: + if !s.capabilities.has(CapabilitySetValues) { + shown := displayValue(value) + value.Kind = &pb.Value_Null{Null: "unsupported: " + shown.Kind.String() + " " + runtime.FormatValue(shown)} + return + } + for _, nested := range nestedValues(value) { + s.filterValueCapabilities(nested) + } + case *pb.Value_TensorQuantity: + if !s.capabilities.has(CapabilityTensorValues) { + shown := displayValue(value) + value.Kind = &pb.Value_Null{Null: "unsupported: " + shown.Kind.String() + " " + runtime.FormatValue(shown)} + } } } @@ -154,6 +168,21 @@ func displayValue(pv *pb.Value) runtime.Value { text = describeUnitTerm(k.MeasurementRef.GetUnitTerm()) } return runtime.NewMeasurementRefValue(runtime.Unit{Text: text, Product: semantics.NamedUnitProduct(nil, text, false)}) + case *pb.Value_Set: + set := runtime.NewSet() + for _, elem := range k.Set.GetElements() { + set.Add(displayValue(elem)) + } + return runtime.NewSetValue(set) + case *pb.Value_TensorQuantity: + num := make([]semantics.Value, 0, len(k.TensorQuantity.GetComponents())) + units := make([]runtime.Unit, 0, len(k.TensorQuantity.GetComponents())) + for _, comp := range k.TensorQuantity.GetComponents() { + q := displayQuantity(comp).Quantity() + num = append(num, q.Num) + units = append(units, q.Unit) + } + return runtime.NewTensorQuantityValue(k.TensorQuantity.GetDimensions(), num, units) default: return protoToScalar(pv) } diff --git a/internal/grpc/convert.go b/internal/grpc/convert.go index c9c5ee757..e6faf5ba0 100644 --- a/internal/grpc/convert.go +++ b/internal/grpc/convert.go @@ -295,6 +295,8 @@ func ValueToProtoIn(rt *runtime.Context, val runtime.Value, idx *symbols.Index) } } return &pb.Value{Kind: &pb.Value_Sequence{Sequence: &pb.ValueSequence{Elements: pbElements}}} + case runtime.ValSet: + return &pb.Value{Kind: &pb.Value_Set{Set: setToProto(rt, val.Set(), idx)}} case runtime.ValVariant: // The wire Value has no variant form: the object a selected variant // materialized is reported by identity, a valueless selection as unsupported. @@ -334,8 +336,11 @@ func ValueToProtoIn(rt *runtime.Context, val runtime.Value, idx *symbols.Index) } return &pb.Value{Kind: &pb.Value_Function{Function: functionToProto(val, idx)}} case runtime.ValTensorQuantity: - // No wire arm carries dimensions above one with a unit per component. - return &pb.Value{Kind: &pb.Value_Null{Null: "unsupported: " + val.Kind.String() + " " + runtime.FormatValue(val)}} + ptq := tensorQuantityToProto(val.TensorQuantity()) + if ptq == nil { + return &pb.Value{Kind: &pb.Value_Null{Null: "unsupported: tensor quantity with a non-numeric component"}} + } + return &pb.Value{Kind: &pb.Value_TensorQuantity{TensorQuantity: ptq}} case runtime.ValCoordinateFrame, runtime.ValCoordinateTransformation: // No wire arm carries a frame's axes or a transformation's placement. return &pb.Value{Kind: &pb.Value_Null{Null: "unsupported: " + val.Kind.String() + " " + runtime.FormatValue(val)}} @@ -394,6 +399,36 @@ func arrayToProto(rt *runtime.Context, a *runtime.Array, idx *symbols.Index) *pb return pa } +// setToProto marshals a set's distinct elements in canonical order, each +// converted as any value is. +func setToProto(rt *runtime.Context, s *runtime.Set, idx *symbols.Index) *pb.ValueSet { + ps := &pb.ValueSet{} + if s == nil { + return ps + } + for _, elem := range s.Elements() { + ps.Elements = append(ps.Elements, ValueToProtoIn(rt, elem, idx)) + } + return ps +} + +// tensorQuantityToProto marshals a tensor as its dimensions and one Quantity +// per row-major component; nil if a component's magnitude is not a number. +func tensorQuantityToProto(tq *runtime.TensorQuantity) *pb.TensorQuantity { + if tq == nil { + return nil + } + ptq := &pb.TensorQuantity{Dimensions: slices.Clone(tq.Dimensions)} + for i := range tq.Num { + pq := QuantityToProto(&runtime.Quantity{Num: tq.Num[i], Unit: tq.Units[i]}) + if pq == nil { + return nil + } + ptq.Components = append(ptq.Components, pq) + } + return ptq +} + // vectorToProto marshals a vector's components, Integer and Real kept apart. func vectorToProto(v *runtime.Vector) *pb.Vector { pv := &pb.Vector{} @@ -533,8 +568,42 @@ var ( // ErrFunctionUnbound reports a Function naming no calc of the model read as // a function, or an object the runtime does not hold. ErrFunctionUnbound = errors.New("function names no calc of this model") + + // ErrSetElementRepeated reports a set sent with an element twice, which a + // set holds once; a sender meaning both meant a sequence. + ErrSetElementRepeated = errors.New("set element is repeated") + + // ErrTensorDimensionNotPositive reports a tensor sent with a dimension of no + // extent, which its TensorMeasurementReference declares as Positive. + ErrTensorDimensionNotPositive = errors.New("tensor dimension is not positive") + + // ErrTensorShapeMismatch reports a tensor whose components do not fill its + // dimensions, so no row-major reading of them has that shape. + ErrTensorShapeMismatch = errors.New("tensor components do not fill its dimensions") + + // ErrTensorComponentMissing reports a tensor component sent as an empty + // message, which carries no quantity. + ErrTensorComponentMissing = errors.New("tensor component carries no quantity") ) +// ValueCarriesSet reports whether a value, or any value nested in it, is a +// ValueSet: the kind set_values governs. +func ValueCarriesSet(pv *pb.Value) bool { + return valueCarries(pv, func(v *pb.Value) bool { + _, ok := v.GetKind().(*pb.Value_Set) + return ok + }) +} + +// ValueCarriesTensor reports whether a value, or any value nested in it, is a +// TensorQuantity: the kind tensor_values governs. +func ValueCarriesTensor(pv *pb.Value) bool { + return valueCarries(pv, func(v *pb.Value) bool { + _, ok := v.GetKind().(*pb.Value_TensorQuantity) + return ok + }) +} + // ValueCarriesMeasurementRef reports whether a value, or any value nested in // it, is a MeasurementRef: the kind measurement_refs governs. func ValueCarriesMeasurementRef(pv *pb.Value) bool { @@ -596,12 +665,14 @@ func valueCarries(pv *pb.Value, is func(*pb.Value) bool) bool { return false } -// nestedValues lists the Values a value holds directly: a sequence's elements, -// an array's elements and a vector's components. +// nestedValues lists the Values a value holds directly: a sequence's or a set's +// elements, an array's elements and a vector's components. func nestedValues(pv *pb.Value) []*pb.Value { switch k := pv.GetKind().(type) { case *pb.Value_Sequence: return k.Sequence.GetElements() + case *pb.Value_Set: + return k.Set.GetElements() case *pb.Value_Array: return k.Array.GetElements() case *pb.Value_Vector: @@ -646,6 +717,10 @@ func ProtoToRuntimeValue(rt *runtime.Context, pv *pb.Value, idx *symbols.Index, } } return runtime.NewSequenceValue(seq), nil + case *pb.Value_Set: + return protoToSet(k.Set, idx, sem) + case *pb.Value_TensorQuantity: + return protoToTensorQuantity(k.TensorQuantity, idx, sem) case *pb.Value_Array: return protoToArray(rt, k.Array, idx, sem) case *pb.Value_Vector: @@ -693,6 +768,47 @@ func functionFromProto(rt *runtime.Context, fn *pb.Function, idx *symbols.Index) return runtime.Value{}, fmt.Errorf("%w: %s is not a calc", ErrFunctionUnbound, fn.GetCalcId()) } +// protoToSet rebuilds a set from elements sent in any order, refusing one sent +// twice rather than reading the two as one. +func protoToSet(ps *pb.ValueSet, idx *symbols.Index, sem *semantics.Model) (runtime.Value, error) { + set := runtime.NewSet() + for i, elem := range ps.GetElements() { + val, err := ProtoToValueIn(elem, idx, sem) + if err != nil { + return runtime.Value{}, err + } + if set.Contains(val) { + return runtime.Value{}, fmt.Errorf("%w: element %d, %s", ErrSetElementRepeated, i+1, runtime.FormatValue(val)) + } + set.Add(val) + } + return runtime.NewSetValue(set), nil +} + +// protoToTensorQuantity rebuilds a tensor of any rank, refusing a shape its +// components do not fill, each component read exactly as a scalar Quantity is. +func protoToTensorQuantity(ptq *pb.TensorQuantity, idx *symbols.Index, sem *semantics.Model) (runtime.Value, error) { + dimensions := slices.Clone(ptq.GetDimensions()) + if err := CheckTensorShape(dimensions, len(ptq.GetComponents())); err != nil { + return runtime.Value{}, err + } + num := make([]semantics.Value, 0, len(ptq.GetComponents())) + units := make([]runtime.Unit, 0, len(ptq.GetComponents())) + for i, comp := range ptq.GetComponents() { + if comp == nil { + return runtime.Value{}, fmt.Errorf("%w: component %d", ErrTensorComponentMissing, i+1) + } + val, err := ProtoToQuantity(comp, idx, sem) + if err != nil { + return runtime.Value{}, fmt.Errorf("component %d: %w", i+1, err) + } + q := val.Quantity() + num = append(num, q.Num) + units = append(units, q.Unit) + } + return runtime.NewTensorQuantityValue(dimensions, num, units), nil +} + // protoToArray rebuilds an array, refusing a shape its elements do not fill // rather than reading them under some other shape. func protoToArray(rt *runtime.Context, pa *pb.Array, idx *symbols.Index, sem *semantics.Model) (runtime.Value, error) { @@ -714,19 +830,29 @@ func protoToArray(rt *runtime.Context, pa *pb.Array, idx *symbols.Index, sem *se // CheckArrayShape reports whether count elements fill dimensions in row-major // order: every dimension positive and their product (one for rank 0) count. func CheckArrayShape(dimensions []int64, count int) error { + return checkShape(dimensions, count, ErrArrayDimensionNotPositive, ErrArrayShapeMismatch) +} + +// CheckTensorShape is CheckArrayShape for a tensor's components, reported as +// the tensor errors. +func CheckTensorShape(dimensions []int64, count int) error { + return checkShape(dimensions, count, ErrTensorDimensionNotPositive, ErrTensorShapeMismatch) +} + +func checkShape(dimensions []int64, count int, notPositive, mismatch error) error { size := int64(1) for i, d := range dimensions { if d < 1 { - return fmt.Errorf("%w: dimension %d is %d", ErrArrayDimensionNotPositive, i+1, d) + return fmt.Errorf("%w: dimension %d is %d", notPositive, i+1, d) } if size > math.MaxInt64/d { - return fmt.Errorf("%w: flattenedSize of dimensions %v exceeds the Integer range", ErrArrayShapeMismatch, dimensions) + return fmt.Errorf("%w: flattenedSize of dimensions %v exceeds the Integer range", mismatch, dimensions) } size *= d } if size != int64(count) { return fmt.Errorf("%w: %d elements under dimensions %v (flattenedSize %d)", - ErrArrayShapeMismatch, count, dimensions, size) + mismatch, count, dimensions, size) } return nil } diff --git a/internal/grpc/convert_set_tensor_test.go b/internal/grpc/convert_set_tensor_test.go new file mode 100644 index 000000000..da89b58c2 --- /dev/null +++ b/internal/grpc/convert_set_tensor_test.go @@ -0,0 +1,510 @@ +package grpc + +import ( + "context" + "errors" + "strings" + "testing" + + "connectrpc.com/connect" + + pb "github.com/Open-MBEE/OpenSysML/api/proto" + "github.com/Open-MBEE/OpenSysML/internal/core/runtime" + "github.com/Open-MBEE/OpenSysML/internal/core/semantics" +) + +// setTensorWireModel yields a set and tensors of rank two and three as feature +// values, and takes each back as a calc argument that reads it. +const setTensorWireModel = ` +package W { + private import ScalarValues::*; + private import Collections::*; + private import CollectionFunctions::*; + private import Quantities::*; + private import MeasurementReferences::*; + private import SI::*; + private import TensorCalculations::*; + + attribute s : Set { :>> elements = (3, 1, 2, 2, 3); } + attribute e : Set { :>> elements = (); } + attribute mixed : Set { :>> elements = ("b", 2, true, 1.5, "a", 3 [m]); } + + attribute cubeRef : TensorMeasurementReference { + :>> dimensions = (2, 2, 2); + :>> mRefs = (Pa, Pa, Pa, Pa, Pa, Pa, Pa, Pa); + } + attribute planeRef : TensorMeasurementReference { + :>> dimensions = (2, 2); + :>> mRefs = (m, m, m, m); + } + attribute cube : TensorQuantityValue = TensorCalculations::'['((1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0), cubeRef); + attribute plane : TensorQuantityValue = TensorCalculations::'['((1, 2, 3, 4), planeRef); + attribute tensors : TensorQuantityValue[*] ordered = (cube, plane); + + calc def SizeOf { in c : Integer[0..*]; return : Natural = SequenceFunctions::size(c); } + calc sizeOf : SizeOf; + calc def Corner { in t : TensorQuantityValue; return : ScalarQuantityValue = t#(2, 2, 2); } + calc corner : Corner; + calc def Rank { in t : TensorQuantityValue; return : Natural = t.order; } + calc rank : Rank; +} +` + +func mustSetTensorModel(t *testing.T, srv *Service) string { + t.Helper() + return mustParse(t, srv, setTensorWireModel) +} + +func setOf(elements ...*pb.Value) *pb.Value { + return &pb.Value{Kind: &pb.Value_Set{Set: &pb.ValueSet{Elements: elements}}} +} + +func tensorQuantityValue(dimensions []int64, components ...*pb.Quantity) *pb.Value { + return &pb.Value{Kind: &pb.Value_TensorQuantity{TensorQuantity: &pb.TensorQuantity{Dimensions: dimensions, Components: components}}} +} + +func stringValue(s string) *pb.Value { return &pb.Value{Kind: &pb.Value_StringValue{StringValue: s}} } +func boolValue(b bool) *pb.Value { return &pb.Value{Kind: &pb.Value_BoolValue{BoolValue: b}} } + +// A set crosses as its own arm holding each distinct element once, in the +// runtime's canonical order, and reads back as an equal set whatever order it +// is sent in. +func TestSetRoundTrip(t *testing.T) { + srv := mustNewService(t, 4) + modelHash := mustSetTensorModel(t, srv) + cached, ok := srv.cache.Get(modelHash) + if !ok { + t.Fatal("parsed model is not cached") + } + idx, sem := cached.Index, NewSymbolContext(cached.Index).Semantics + + cases := []struct { + expr string + want string + }{ + {"W::s.elements", "Set{1, 2, 3}"}, + {"W::e.elements", "Set{}"}, + {"W::mixed.elements", `Set{true, 1.5, 2, "a", "b", 3 [m]}`}, + } + for _, tc := range cases { + t.Run(tc.expr, func(t *testing.T) { + pv := mustEvaluate(t, srv, modelHash, tc.expr) + if pv.GetSet() == nil { + t.Fatalf("%s crossed as %T: %v", tc.expr, pv.GetKind(), pv) + } + shown := displayValue(pv) + if got := runtime.FormatValue(shown); got != tc.want { + t.Errorf("%s crossed as %s, want %s", tc.expr, got, tc.want) + } + // The elements are sent in canonical order, not the order written. + var sent []string + for _, elem := range pv.GetSet().GetElements() { + sent = append(sent, runtime.FormatValue(displayValue(elem))) + } + if got := "Set{" + strings.Join(sent, ", ") + "}"; got != tc.want { + t.Errorf("%s elements on the wire in order %s, want %s", tc.expr, got, tc.want) + } + + back, err := ProtoToValueIn(pv, idx, sem) + if err != nil { + t.Fatalf("ProtoToValueIn: %v", err) + } + if back.Kind != runtime.ValSet { + t.Fatalf("read back as %s", back.Kind) + } + if got := runtime.FormatValue(back); got != tc.want { + t.Errorf("round trip = %s, want %s", got, tc.want) + } + }) + } + + // A client may send the elements in any order: the set read is the same. + written := setOf(intValue(3), intValue(1), intValue(2)) + canonical := setOf(intValue(1), intValue(2), intValue(3)) + a, err := ProtoToValueIn(written, idx, sem) + if err != nil { + t.Fatal(err) + } + b, err := ProtoToValueIn(canonical, idx, sem) + if err != nil { + t.Fatal(err) + } + if !a.Set().Equal(b.Set()) || a.Set().Size() != 3 { + t.Errorf("sets sent in two orders read back as %s and %s", runtime.FormatValue(a), runtime.FormatValue(b)) + } + + // The empty set, a set of sets and a set nested in a sequence read back as + // themselves, the nested sets deduplicated by set equality. + empty, err := ProtoToValueIn(setOf(), idx, sem) + if err != nil || empty.Kind != runtime.ValSet || empty.Set().Size() != 0 { + t.Errorf("empty set read back as %v, %v", empty, err) + } + nested := setOf(written, setOf()) + back, err := ProtoToValueIn(nested, idx, sem) + if err != nil { + t.Fatal(err) + } + if got := runtime.FormatValue(back); got != "Set{Set{1, 2, 3}, Set{}}" { + t.Errorf("set of sets read back as %s", got) + } + if got := runtime.FormatValue(displayValue(ValueToProto(back, idx))); got != "Set{Set{1, 2, 3}, Set{}}" { + t.Errorf("set of sets crossed as %s", got) + } + seq := &pb.Value{Kind: &pb.Value_Sequence{Sequence: &pb.ValueSequence{Elements: []*pb.Value{written, intValue(4)}}}} + back, err = ProtoToValueIn(seq, idx, sem) + if err != nil { + t.Fatal(err) + } + if got := runtime.FormatValue(back); got != "[Set{1, 2, 3}, 4]" { + t.Errorf("sequence holding a set read back as %s", got) + } +} + +// A set sent with an element twice is refused with a typed error rather than +// read as the set holding it once, so a sequence sent under the wrong arm is +// never silently deduplicated; a malformed element is refused as it is anywhere. +func TestMalformedSetsAreRejected(t *testing.T) { + srv := mustNewService(t, 4) + modelHash := mustSetTensorModel(t, srv) + cached, _ := srv.cache.Get(modelHash) + idx, sem := cached.Index, NewSymbolContext(cached.Index).Semantics + + cases := []struct { + name string + val *pb.Value + want error + }{ + {"repeated integer", setOf(intValue(1), intValue(2), intValue(1)), ErrSetElementRepeated}, + {"repeated string", setOf(stringValue("a"), stringValue("a")), ErrSetElementRepeated}, + {"repeated nested set", setOf(setOf(intValue(1)), setOf(intValue(1))), ErrSetElementRepeated}, + {"nested sets equal in another order", setOf(setOf(intValue(1), intValue(2)), setOf(intValue(2), intValue(1))), ErrSetElementRepeated}, + {"Integer and the equal Real", setOf(intValue(1), realValue(1)), ErrSetElementRepeated}, + {"unset element", setOf(&pb.Value{Kind: &pb.Value_Unset{Unset: true}}), ErrUnsetNotAccepted}, + {"malformed element", setOf(arrayValue([]int64{0})), ErrArrayDimensionNotPositive}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + _, err := ProtoToValueIn(tc.val, idx, sem) + if !errors.Is(err, tc.want) { + t.Fatalf("ProtoToValueIn = %v, want %v", err, tc.want) + } + }) + } +} + +// A tensor quantity crosses at any rank as its dimensions and one Quantity per +// row-major component, unit and reduction included, and reads back as the same +// tensor; a rank-two tensor and a rank-one tensor are not a Vector or a +// VectorQuantity. +func TestTensorQuantityRoundTrip(t *testing.T) { + srv := mustNewService(t, 4) + modelHash := mustSetTensorModel(t, srv) + cached, _ := srv.cache.Get(modelHash) + idx, sem := cached.Index, NewSymbolContext(cached.Index).Semantics + + cases := []struct { + expr string + dims []int64 + want string + }{ + {"W::cube", []int64{2, 2, 2}, "Tensor(2, 2, 2)[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0] [Pa]"}, + {"W::plane", []int64{2, 2}, "Tensor(2, 2)[1, 2, 3, 4] [m]"}, + {"W::cube + W::cube", []int64{2, 2, 2}, "Tensor(2, 2, 2)[2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0] [Pa]"}, + } + for _, tc := range cases { + t.Run(tc.expr, func(t *testing.T) { + pv := mustEvaluate(t, srv, modelHash, tc.expr) + ptq := pv.GetTensorQuantity() + if ptq == nil { + t.Fatalf("%s crossed as %T: %v", tc.expr, pv.GetKind(), pv) + } + if len(ptq.GetDimensions()) != len(tc.dims) { + t.Fatalf("%s crossed with dimensions %v, want %v", tc.expr, ptq.GetDimensions(), tc.dims) + } + for i, d := range tc.dims { + if ptq.GetDimensions()[i] != d { + t.Errorf("%s dimension %d = %d, want %d", tc.expr, i+1, ptq.GetDimensions()[i], d) + } + } + for i, comp := range ptq.GetComponents() { + if comp.GetUnitTerm() == nil || comp.GetUnit() == "" { + t.Errorf("component %d crossed as %v, want a unit with its reduction", i+1, comp) + } + } + if got := runtime.FormatValue(displayValue(pv)); got != tc.want { + t.Errorf("%s crossed as %s, want %s", tc.expr, got, tc.want) + } + + back, err := ProtoToValueIn(pv, idx, sem) + if err != nil { + t.Fatalf("ProtoToValueIn: %v", err) + } + if back.Kind != runtime.ValTensorQuantity { + t.Fatalf("read back as %s", back.Kind) + } + if got := runtime.FormatValue(back); got != tc.want { + t.Errorf("round trip = %s, want %s", got, tc.want) + } + sent := mustEvaluateTensor(t, srv, modelHash, tc.expr) + for i := range sent.Units { + if !back.TensorQuantity().Units[i].Term.Same(sent.Units[i].Term) { + t.Errorf("component %d reduction = %v, want %v", i+1, back.TensorQuantity().Units[i].Term, sent.Units[i].Term) + } + } + }) + } + + // A tensor of rank one is its own kind, as it is in the runtime. + metre := mustEvaluateQuantity(t, srv, modelHash, "3 [SI::m]") + line, err := ProtoToValueIn(tensorQuantityValue([]int64{2}, metre, metre), idx, sem) + if err != nil { + t.Fatal(err) + } + if line.Kind != runtime.ValTensorQuantity || runtime.FormatValue(line) != "Tensor(2)[3, 3] [SI::m]" { + t.Errorf("rank-one tensor read back as %s %s", line.Kind, runtime.FormatValue(line)) + } + if pv := ValueToProto(line, idx); pv.GetTensorQuantity() == nil { + t.Errorf("rank-one tensor crossed as %T", pv.GetKind()) + } + + // A tensor with no magnitude in a component still has no wire form. + num := []semantics.Value{{Kind: semantics.ValInt, Int: 1}, {}} + units := []runtime.Unit{line.TensorQuantity().Units[0], line.TensorQuantity().Units[0]} + pv := ValueToProto(runtime.NewTensorQuantityValue([]int64{2}, num, units), idx) + if pv.GetNull() != "unsupported: tensor quantity with a non-numeric component" { + t.Errorf("tensor with a non-numeric component crossed as %v", pv) + } +} + +// mustEvaluateTensor evaluates expr through the runtime and returns the tensor. +func mustEvaluateTensor(t *testing.T, srv *Service, modelHash, expr string) *runtime.TensorQuantity { + t.Helper() + cached, _ := srv.cache.Get(modelHash) + idx, sem := cached.Index, NewSymbolContext(cached.Index).Semantics + val, err := ProtoToValueIn(mustEvaluate(t, srv, modelHash, expr), idx, sem) + if err != nil || val.Kind != runtime.ValTensorQuantity { + t.Fatalf("%s = %v, %v; want a tensor", expr, val, err) + } + return val.TensorQuantity() +} + +// A malformed tensor is refused with a typed error naming what is wrong, never +// read under another shape or with a unit it did not send. +func TestMalformedTensorQuantitiesAreRejected(t *testing.T) { + srv := mustNewService(t, 4) + modelHash := mustSetTensorModel(t, srv) + cached, _ := srv.cache.Get(modelHash) + idx, sem := cached.Index, NewSymbolContext(cached.Index).Semantics + metre := mustEvaluateQuantity(t, srv, modelHash, "3 [SI::m]") + unreduced := &pb.Quantity{Magnitude: &pb.Quantity_IntMagnitude{IntMagnitude: 3}, Unit: "SI::m"} + + cases := []struct { + name string + val *pb.Value + want error + }{ + {"too few components", tensorQuantityValue([]int64{2, 2, 2}, metre, metre, metre, metre, metre, metre, metre), ErrTensorShapeMismatch}, + {"too many components", tensorQuantityValue([]int64{2, 2}, metre, metre, metre, metre, metre), ErrTensorShapeMismatch}, + {"rank 0 without its component", tensorQuantityValue(nil), ErrTensorShapeMismatch}, + {"zero dimension", tensorQuantityValue([]int64{0, 2}), ErrTensorDimensionNotPositive}, + {"negative dimension", tensorQuantityValue([]int64{2, -1}), ErrTensorDimensionNotPositive}, + {"overflowing shape", tensorQuantityValue([]int64{1 << 40, 1 << 40}, metre), ErrTensorShapeMismatch}, + {"unreduced unit", tensorQuantityValue([]int64{1}, unreduced), ErrUnitNotReduced}, + {"missing component", tensorQuantityValue([]int64{1}, nil), ErrTensorComponentMissing}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + _, err := ProtoToValueIn(tc.val, idx, sem) + if !errors.Is(err, tc.want) { + t.Fatalf("ProtoToValueIn = %v, want %v", err, tc.want) + } + }) + } +} + +// Sets and tensors cross every surface a value does: a feature value, a +// sequence element, a calc argument the model reads as the kind it is. +func TestSetAndTensorCrossEveryValueSurface(t *testing.T) { + ctx := context.Background() + srv := mustNewService(t, 4) + modelHash := mustSetTensorModel(t, srv) + + inst, err := srv.Instantiate(ctx, &pb.InstantiateRequest{ModelHash: modelHash, SymbolId: "W"}) + if err != nil || inst.Error != "" { + t.Fatalf("Instantiate: err = %v, error = %q", err, inst.GetError()) + } + if fv := inst.Instance.FeatureValues["cube"]; fv == nil || fv.Error != "" || fv.Value.GetTensorQuantity() == nil { + t.Errorf("feature value cube = %v, want a tensor quantity", fv) + } + tensors := mustEvaluate(t, srv, modelHash, "W::tensors") + elems := tensors.GetSequence().GetElements() + if len(elems) != 2 || elems[0].GetTensorQuantity() == nil || elems[1].GetTensorQuantity() == nil { + t.Fatalf("W::tensors = %v, want a sequence of two tensors", tensors) + } + + cube := mustEvaluate(t, srv, modelHash, "W::cube") + calc, err := srv.EvaluateCalc(ctx, &pb.EvaluateCalcRequest{ModelHash: modelHash, SymbolId: "W::corner", Arguments: []*pb.Value{cube}}) + if err != nil || calc.Error != "" { + t.Fatalf("EvaluateCalc(corner): err = %v, error = %q", err, calc.GetError()) + } + if got := calc.Result.GetQuantity(); got == nil || got.GetRealMagnitude() != 8 || got.GetUnit() != "Pa" { + t.Errorf("corner(cube) = %v, want 8.0 [Pa]", calc.Result) + } + calc, err = srv.EvaluateCalc(ctx, &pb.EvaluateCalcRequest{ModelHash: modelHash, SymbolId: "W::rank", Arguments: []*pb.Value{cube}}) + if err != nil || calc.Error != "" { + t.Fatalf("EvaluateCalc(rank): err = %v, error = %q", err, calc.GetError()) + } + if calc.Result.GetIntValue() != 3 { + t.Errorf("rank(cube) = %v, want 3", calc.Result) + } + + // A set sent for a nonunique parameter is read as the sequence of its + // elements, in whatever order a client sends them. + calc, err = srv.EvaluateCalc(ctx, &pb.EvaluateCalcRequest{ModelHash: modelHash, SymbolId: "W::sizeOf", Arguments: []*pb.Value{ + setOf(intValue(3), intValue(1), intValue(2)), + }}) + if err != nil || calc.Error != "" { + t.Fatalf("EvaluateCalc(sizeOf): err = %v, error = %q", err, calc.GetError()) + } + if calc.Result.GetIntValue() != 3 { + t.Errorf("sizeOf({3, 1, 2}) = %v, want 3", calc.Result) + } + + // A malformed argument is an in-band error, as a malformed quantity is. + calc, err = srv.EvaluateCalc(ctx, &pb.EvaluateCalcRequest{ModelHash: modelHash, SymbolId: "W::sizeOf", Arguments: []*pb.Value{ + setOf(intValue(1), intValue(1)), + }}) + if err != nil { + t.Fatalf("EvaluateCalc(repeated): %v", err) + } + if !strings.Contains(calc.Error, ErrSetElementRepeated.Error()) { + t.Errorf("EvaluateCalc(repeated) error = %q, want one naming %v", calc.Error, ErrSetElementRepeated) + } + calc, err = srv.EvaluateCalc(ctx, &pb.EvaluateCalcRequest{ModelHash: modelHash, SymbolId: "W::rank", Arguments: []*pb.Value{ + tensorQuantityValue([]int64{2}, mustEvaluateQuantity(t, srv, modelHash, "3 [SI::m]")), + }}) + if err != nil { + t.Fatalf("EvaluateCalc(misshapen): %v", err) + } + if !strings.Contains(calc.Error, ErrTensorShapeMismatch.Error()) { + t.Errorf("EvaluateCalc(misshapen) error = %q, want one naming %v", calc.Error, ErrTensorShapeMismatch) + } +} + +// Each arm is advertised under its own capability. A service withholding one +// names the value as unsupported — what a client built before the arm existed +// was always sent — nested values included, and refuses one sent to it rather +// than reading it as another value. +func TestSetAndTensorCapabilities(t *testing.T) { + ctx := context.Background() + for _, want := range []string{CapabilitySetValues, CapabilityTensorValues} { + found := false + for _, c := range Capabilities() { + found = found || c == want + } + if !found { + t.Errorf("capabilities %v do not include %q", Capabilities(), want) + } + } + + noSets := mustNewServiceWithout(t, CapabilitySetValues) + modelHash := mustSetTensorModel(t, noSets) + for expr, want := range map[string]string{ + "W::s.elements": "unsupported: set Set{1, 2, 3}", + "W::e.elements": "unsupported: set Set{}", + "W::mixed.elements": `unsupported: set Set{true, 1.5, 2, "a", "b", 3 [m]}`, + } { + got := mustEvaluate(t, noSets, modelHash, expr) + if got.GetSet() != nil || got.GetSequence() != nil { + t.Errorf("%s crossed as %T without %s: %v", expr, got.GetKind(), CapabilitySetValues, got) + } + if got.GetNull() != want { + t.Errorf("%s without %s = %v, want null %q", expr, CapabilitySetValues, got, want) + } + } + // Tensors still cross without set_values, and a set's elements are filtered + // like any values when the arm itself crosses. + if got := mustEvaluate(t, noSets, modelHash, "W::cube"); got.GetTensorQuantity() == nil { + t.Errorf("W::cube without %s = %v, want a tensor", CapabilitySetValues, got) + } + noComplex := mustNewServiceWithout(t, CapabilityComplexValues) + pv := setOf(&pb.Value{Kind: &pb.Value_Complex{Complex: ComplexToProto(complex(0, 1))}}) + noComplex.filterValueCapabilities(pv) + if pv.GetSet() == nil || !strings.Contains(pv.GetSet().GetElements()[0].GetNull(), "complex number") { + t.Errorf("set of a complex without complex_values = %v, want the element withheld", pv) + } + + noTensors := mustNewServiceWithout(t, CapabilityTensorValues) + modelHash = mustSetTensorModel(t, noTensors) + for expr, want := range map[string]string{ + "W::cube": "unsupported: tensor quantity Tensor(2, 2, 2)[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0] [Pa]", + "W::plane": "unsupported: tensor quantity Tensor(2, 2)[1, 2, 3, 4] [m]", + } { + got := mustEvaluate(t, noTensors, modelHash, expr) + if got.GetTensorQuantity() != nil || got.GetVectorQuantity() != nil || got.GetArray() != nil { + t.Errorf("%s crossed as %T without %s: %v", expr, got.GetKind(), CapabilityTensorValues, got) + } + if got.GetNull() != want { + t.Errorf("%s without %s = %v, want null %q", expr, CapabilityTensorValues, got, want) + } + } + tensors := mustEvaluate(t, noTensors, modelHash, "W::tensors") + if elems := tensors.GetSequence().GetElements(); len(elems) != 2 || !strings.HasPrefix(elems[0].GetNull(), "unsupported: tensor quantity") { + t.Errorf("W::tensors without %s = %v, want the elements withheld", CapabilityTensorValues, tensors) + } + if got := mustEvaluate(t, noTensors, modelHash, "W::s.elements"); got.GetSet() == nil { + t.Errorf("W::s.elements without %s = %v, want a set", CapabilityTensorValues, got) + } + + // Sent to a service without the capability, each is refused, nested included. + metre := mustEvaluateQuantity(t, noTensors, modelHash, "3 [SI::m]") + line := tensorQuantityValue([]int64{1}, metre) + sequence := func(elements ...*pb.Value) *pb.Value { + return &pb.Value{Kind: &pb.Value_Sequence{Sequence: &pb.ValueSequence{Elements: elements}}} + } + for name, input := range map[string]*pb.Value{"tensor": line, "nested": sequence(line), "in a set": setOf(line)} { + _, err := noTensors.EvaluateCalc(ctx, &pb.EvaluateCalcRequest{ModelHash: modelHash, SymbolId: "W::rank", Arguments: []*pb.Value{input}}) + if connect.CodeOf(err) != connect.CodeUnimplemented || !strings.Contains(err.Error(), CapabilityTensorValues) { + t.Errorf("EvaluateCalc with %s argument without %s: err = %v, want UNIMPLEMENTED naming the capability", name, CapabilityTensorValues, err) + } + } + modelHash = mustSetTensorModel(t, noSets) + set := setOf(intValue(1)) + for name, input := range map[string]*pb.Value{"set": set, "nested": sequence(set), "in an array": arrayValue([]int64{1}, set)} { + _, err := noSets.EvaluateCalc(ctx, &pb.EvaluateCalcRequest{ModelHash: modelHash, SymbolId: "W::sizeOf", Arguments: []*pb.Value{input}}) + if connect.CodeOf(err) != connect.CodeUnimplemented || !strings.Contains(err.Error(), CapabilitySetValues) { + t.Errorf("EvaluateCalc with %s argument without %s: err = %v, want UNIMPLEMENTED naming the capability", name, CapabilitySetValues, err) + } + } +} + +func TestValueCarriesSetAndTensor(t *testing.T) { + one := intValue(1) + set := setOf(one) + tensor := tensorQuantityValue([]int64{1}, &pb.Quantity{Unit: "m"}) + sequence := func(elements ...*pb.Value) *pb.Value { + return &pb.Value{Kind: &pb.Value_Sequence{Sequence: &pb.ValueSequence{Elements: elements}}} + } + for _, tc := range []struct { + name string + value *pb.Value + set, tensr bool + }{ + {"nil", nil, false, false}, + {"int", one, false, false}, + {"set", set, true, false}, + {"tensor", tensor, false, true}, + {"sequence of ints", sequence(one, one), false, false}, + {"sequence with a set", sequence(one, sequence(set)), true, false}, + {"array of tensors", arrayValue([]int64{1}, tensor), false, true}, + {"set of tensors", setOf(tensor), true, true}, + {"vector quantity", vectorQuantityValue(&pb.Quantity{Unit: "m"}), false, false}, + } { + if got := ValueCarriesSet(tc.value); got != tc.set { + t.Errorf("ValueCarriesSet(%s) = %v, want %v", tc.name, got, tc.set) + } + if got := ValueCarriesTensor(tc.value); got != tc.tensr { + t.Errorf("ValueCarriesTensor(%s) = %v, want %v", tc.name, got, tc.tensr) + } + } +} diff --git a/internal/grpc/convert_structured_test.go b/internal/grpc/convert_structured_test.go index f53a7500c..d4ffb5214 100644 --- a/internal/grpc/convert_structured_test.go +++ b/internal/grpc/convert_structured_test.go @@ -523,28 +523,6 @@ func TestStructuredInputNeedsStructuredValues(t *testing.T) { } } -// The wire Value has no tensor arm, so a tensor quantity crosses as an unsupported -// null naming it, never flattened to a sequence, an array or one quantity. -func TestTensorQuantityCrossesAsUnsupported(t *testing.T) { - metre := semantics.Unit{ - Text: "m", - Product: semantics.OpaqueUnitProduct("m", semantics.UnitTerm{Scale: semantics.UnitScale(1)}), - } - num := make([]semantics.Value, 4) - units := make([]runtime.Unit, 4) - for i := range num { - num[i] = semantics.Value{Kind: semantics.ValReal, Real: float64(i + 1)} - units[i] = metre - } - pv := ValueToProto(runtime.NewTensorQuantityValue([]int64{2, 2}, num, units), nil) - if pv.GetSequence() != nil || pv.GetArray() != nil || pv.GetQuantity() != nil || pv.GetVector() != nil { - t.Fatalf("a tensor quantity crossed as %T: %v", pv.GetKind(), pv) - } - if got, want := pv.GetNull(), "unsupported: tensor quantity Tensor(2, 2)[1.0, 2.0, 3.0, 4.0] [m]"; got != want { - t.Errorf("ValueToProto(tensor) = %v, want null %q", pv, want) - } -} - // The wire Value has no frame arm either: a coordinate frame and a transformation // cross as unsupported nulls naming them, never as sequences of their axes. func TestCoordinateFrameCrossesAsUnsupported(t *testing.T) { diff --git a/internal/grpc/service.go b/internal/grpc/service.go index 143d14414..133aad4a0 100644 --- a/internal/grpc/service.go +++ b/internal/grpc/service.go @@ -109,6 +109,14 @@ const CapabilityMeasurementRefs = "measurement_refs" // as an unsupported null. const CapabilityFunctionValues = "function_values" +// CapabilitySetValues names the capability of carrying a unique, unordered +// collection as Value.set, rather than reporting it as an unsupported null. +const CapabilitySetValues = "set_values" + +// CapabilityTensorValues names the capability of carrying a tensor quantity of +// any rank as Value.tensor_quantity, rather than as an unsupported null. +const CapabilityTensorValues = "tensor_values" + // CapabilityVerificationVerdicts names the capability of reporting what the body // of a verification case answered as VerificationVerdict, and of running a // verification case through RunAnalysis. @@ -137,8 +145,9 @@ var capabilities = []string{ CapabilityApplyEdits, CapabilityAuthoring, CapabilityInlineLanguage, CapabilityStrictConformance, CapabilityDocumentQuery, CapabilityRenderDocument, CapabilityParseSources, CapabilityComplexValues, CapabilityStructuredValues, - CapabilityMeasurementRefs, CapabilityFunctionValues, CapabilityVerificationVerdicts, - CapabilityInfinityValue, CapabilityDiagnosticCodes, CapabilitySchedule, + CapabilityMeasurementRefs, CapabilityFunctionValues, CapabilitySetValues, + CapabilityTensorValues, CapabilityVerificationVerdicts, CapabilityInfinityValue, + CapabilityDiagnosticCodes, CapabilitySchedule, } type capabilityAvailability struct { @@ -299,7 +308,17 @@ func (s *Service) requireValueCapabilities(pv *pb.Value) error { } } if ValueCarriesInfinity(pv) { - return s.requireCapability(CapabilityInfinityValue) + if err := s.requireCapability(CapabilityInfinityValue); err != nil { + return err + } + } + if ValueCarriesSet(pv) { + if err := s.requireCapability(CapabilitySetValues); err != nil { + return err + } + } + if ValueCarriesTensor(pv) { + return s.requireCapability(CapabilityTensorValues) } return nil } diff --git a/internal/repl/compile_test.go b/internal/repl/compile_test.go index ba1ad7273..3aac2ee6d 100644 --- a/internal/repl/compile_test.go +++ b/internal/repl/compile_test.go @@ -426,6 +426,11 @@ func TestCompileRefusesWhatItCannotCompile(t *testing.T) { {"MixedUnion", "union over Integer and Real collections"}, {"CalcParam", "parameter f binds a function value"}, {"FunctionArgument", "parameter f binds a function value"}, + {"SetParam", "type Collections::Set is not Integer, Real or Boolean"}, + {"SetElements", "type Collections::Set is not Integer, Real or Boolean"}, + {"SetLocal", "type Collections::Set is not Integer, Real or Boolean"}, + {"TensorParam", "type Quantities::TensorQuantityValue is not Integer, Real or Boolean"}, + {"TensorBuilt", "type Quantities::TensorQuantityValue is not Integer, Real or Boolean"}, } { _, err := s.CompileCalc("Refused::" + tc.calc) if err == nil { diff --git a/internal/repl/testdata/compile_calcs.sysml b/internal/repl/testdata/compile_calcs.sysml index 05b5875b0..5b61b69a9 100644 --- a/internal/repl/testdata/compile_calcs.sysml +++ b/internal/repl/testdata/compile_calcs.sysml @@ -368,6 +368,18 @@ package Refused { private import ScalarValues::*; private import SequenceFunctions::*; private import ControlFunctions::*; + private import Collections::*; + private import Quantities::*; + private import MeasurementReferences::*; + private import SI::*; + + // Sets and tensors have no native layout: a calc taking, holding or building one is refused. + calc def SetParam { in s : Set; return : Integer = CollectionFunctions::size(s); } + calc def SetElements { in s : Set; return : Integer[0..*] = s.elements; } + calc def SetLocal { in n : Integer; attribute s : Set { :>> elements = (n, n); } return : Integer = CollectionFunctions::size(s); } + calc def TensorParam { in t : TensorQuantityValue; return : Real = t.num#(1, 1, 1); } + attribute cubeRef : TensorMeasurementReference { :>> dimensions = (2, 2, 2); :>> mRefs = (m, m, m, m, m, m, m, m); } + calc def TensorBuilt { in x : Real; return : TensorQuantityValue = TensorCalculations::'['((x, x, x, x, x, x, x, x), cubeRef); } attribute def Point { attribute x : Real; attribute y : Real; } enum def Color { red; green; } From 8974008be9023ef7e09c3c402a55a1df2a131411 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 02:20:02 +0000 Subject: [PATCH 03/20] docs(compliance): record ordered unique features as approximate Co-Authored-By: jason.han --- docs/project/spec-compliance.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index eb972e178..905833444 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -1387,6 +1387,7 @@ they cannot drift apart. | Two sets are equal when their members are, in whatever order either was given (`CollectionFunctions::'=='` is `col1.elements->equals(col2.elements)`, and a set's `elements` have no order to compare); `contains` and `containsAll` are membership; a set's elements equal a sequence only when the sequence lists the members in canonical order, and a `Set` is never equal to a `Bag` or `OrderedSet` holding the same values, which are sequences | `runtime/eval.go` `valueEqual` (`ValSet` arm); `runtime/collections.go` `builtinCollectionEquals`, `elementsOf`; `value.go` `Set.Equal`, `Set.Contains` | conformance `library_set_operations` (`equalRegardlessOfOrder`, `elementsEqualRegardlessOfOrder`, `membership`, `allMembers`, `emptiness`, `notASequence`); `runtime/set_feature_test.go:TestSetAgainstSequenceComparesCanonically`, `:TestCollectionFunctionsOverCollectionObjects` | ✅ Faithful | | A set has no order of its own, so every operation that walks its members in order — `collect`, `select`, `head`, `tail`, `#`, `==` against a sequence, a trace rendering it, a write into a sequence-holding feature, the wire — sees them in one **canonical order**: by class (null, Booleans with `false` first, numbers ascending, complex numbers by real then imaginary part, strings lexicographically, quantities by dimension then magnitude, enumeration literals, objects by identity, then every other kind) and within a class by that order, the trace rendering breaking what remains. The order is total, so equal sets enumerate alike and a trace over a set is the same golden however the set was written | `runtime/set_order.go` `canonicalLess`, `canonicalClass`; `value.go` `Set.Elements` (sorted once, cached); `trace.go` `FormatTraceValue` (`ValSet`, the same enumeration) | conformance `calc_set_consumed_by_ordered_operations` and its trace golden; `runtime/set_feature_test.go:TestCanonicalOrderIsTotal`, `:TestSetRendersCanonically`; `runtime/collections_test.go:TestCollectionOperationsOverSets` | ✅ Faithful — the order is this runtime's documented rule, since the library defines none; it is not a claim about the specification | | What the library declares ordered stays a sequence: every `SequenceFunctions` result is `Anything[0..*] ordered nonunique` — `union`, `intersection`, `including`, `includingAt`, `excluding` included (`SequenceFunctions.kerml:48-63`) — so `union(s.elements, t.elements)` over two `Set`s is the ordered concatenation of their canonical members, repeats kept, not a set; `(s.elements, s.elements)` likewise lists each member twice. The Kernel Function Library declares no `distinct` function, so none is invented: the members of a sequence, each once, are what a `Set`'s `elements` hold | `runtime/collections.go` (`SequenceFunctions` builtins over `elementsOf`) | conformance `library_set_sequence_functions`, `library_set_operations` (`ordered`, `repeatable`, `plain`) | ✅ Faithful | +| `OrderedSet::elements` and `OrderedMap::elements` are declared `ordered` *and* unique (`UniqueCollection` under `OrderedCollection`, `Collections.kerml`), as is any multi-valued feature not declared `nonunique`. Their order is part of the value, so they are held as the sequence written; their uniqueness is not enforced — `OrderedSet { :>> elements = (1, 1, 2); }` reads three elements and `size` answers 3. A set is the one place uniqueness is the value's own definition; checking `unique` as a constraint on an ordered feature's values is separate work | `runtime/set_feature.go` `holdsSet` (declines any feature declared `ordered`) | conformance `library_ordered_set_elements` (order kept) | ⚠️ Approximate — order faithful, uniqueness unchecked | | A set **crosses gRPC** as the `set` arm (`ValueSet.elements`, each a `Value`, listed in canonical order; an incoming set may list them in any order, and one that repeats a member is `ErrSetElementRepeated`), advertised as `set_values`; a service withholding the capability answers an unsupported null naming the value and refuses one sent to it with `UNIMPLEMENTED`, nested anywhere in the argument. It is **not compiled natively**: `sysml -compile` refuses a calc declaring or reading one with `codegen.UnsupportedError` (`type Collections::Set is not Integer, Real or Boolean`). It has **no RDF literal form** of its own: the mapping writes the model, so a `Set`-valued feature is the expression valuing its `elements`, which round trips exactly | `grpc/convert.go` `setToProto`, `protoToSet`, `ErrSetElementRepeated`; `grpc/capability_response.go` (`set_values`); `codegen/compile.go` `UnsupportedError`; `export/rdf_expr.go` | `grpc/convert_set_tensor_test.go:TestSetRoundTrip`, `:TestMalformedSetsAreRejected`, `:TestSetAndTensorCapabilities`, `:TestValueCarriesSetAndTensor`; `repl/compile_test.go:TestCompileRefusesWhatItCannotCompile` (`SetParam`, `SetElements`, `SetLocal`); `export/set_tensor_rdf_test.go:TestSetAndTensorValuesRoundTripAsExpressions`; the *Values* gRPC rows below | ✅ Faithful (the native and RDF refusals are typed and documented, not layouts) | | `CollectionFunctions::'array#'(arr, indexes)` and `BaseFunctions::'#'` with several indexes select from a **`Collections::Array`** value (`ValArray`, *Structured values* under *KerML Function Library*) by one `Positive` index per dimension in the row-major order `Collections.kerml` documents — `'array#'(a, (2, 1))` over `dimensions = (2, 3)` is the fourth element, as the pinned pilot evaluator answers; a vector or vector quantity is indexed as the one-dimensional Array it specializes; a rank-0 array with no index is null, as the library body says. The count of indexes must be the array's `rank` (`ErrMultiplicityViolation`, naming `arr.rank`), each index within `1..dimensions#(i)` (`ErrIndexOutOfRange`, naming the dimension and its range), and a usage whose `elements` do not fill its `dimensions` is `ErrMultiplicityViolation` naming `flattenedSize`; `rank`, `flattenedSize`, `dimensions` and `elements` of the value read out of it | `runtime/collections.go` `builtinArrayIndex`, `arrayIndex`, `builtinBaseIndex`; `runtime/array.go` `Array.at`, `structuredFeature`, `Context.arrayOfObject`, `Context.declaredArrayValue` | conformance `calc_library_array_value`, `calc_library_array_features`, `calc_library_array_index`, `calc_library_array_index_rank_mismatch`, `calc_library_array_index_out_of_range`, `calc_library_array_empty_rank_zero`, `calc_library_array_specialization_members`, `calc_library_array_specialization_through_calc`, `calc_library_base_index_many`, `calc_library_base_index_many_sequence`; robustness `base_index_with_several_indexes`, `numeric_library_call_that_has_no_value` (`'array#'` over a flat sequence); `repl/runtime_commands_test.go:TestEvalArrayShapedByItsFeatures`; `TestEveryValueKindIsDispatched` | ✅ Faithful | | An operation over an empty collection answers the empty collection and never calls its body, since there is no element to call it with | `runtime/collections.go` `elementsOf` (an empty collection yields no elements) | conformance `calc_collection_ops_over_empty`; `runtime/collections_test.go` `TestCollectionResults` | ✅ Faithful | @@ -1425,7 +1426,6 @@ wrong answer: | An `Array` written as a *sequence* (`attribute m : Matrix = (1, 2, 3, 4)`) | A sequence of numbers is not an Array — it has no `dimensions` — and binding one to an Array-typed usage is the type mismatch it always was; the library's own shape, `dimensions` and `elements` redefined on the usage, is the one way to write an Array value, and there is no literal notation for one in the language to read. | | A reducer named rather than written (`->reduce min`, as the library's own `minimize` is defined) | A function-valued *name* is not a runtime value: `reduce` takes the body expression form (`->reduce {in a; in b; …}`), and `minimize`/`maximize` are implemented directly rather than through `reduce min`. A named reducer is reported as a type error, not read as a body. | | `SequenceFunctions::add`/`addAt`/`remove`/`removeAt`, `CollectionFunctions` mutators | These are `behavior`s, not functions: they declare an `inout` sequence, so they need mutable accumulation the language layer does not have. Deliberately out of scope. | -| The uniqueness of an *ordered* unique feature (`OrderedSet::elements`, `OrderedMap::elements`, any multi-valued feature not declared `nonunique`) | Their order is part of the value, so they are held as the sequence written, and the runtime enforces `unique` on no ordered feature: `OrderedSet { :>> elements = (1, 1, 2); }` reads three elements. A set is the one place uniqueness is the value's own definition rather than a constraint on what a feature may hold; checking the constraint on ordered features is separate work. | | `at`, `first`, `reverse` | Not declared by the Kernel Function Library at all (`head`, `#(1)` and `last` are the declared spellings). Not implemented rather than invented. | ### OpenSysML Math Extension Library (non-normative) From 6f80a7efb2d78efc72a79912d5d4f23e1e1db982 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 02:23:18 +0000 Subject: [PATCH 04/20] test(grpc): drop an unused helper from the set and tensor tests Co-Authored-By: jason.han --- internal/grpc/convert_set_tensor_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/grpc/convert_set_tensor_test.go b/internal/grpc/convert_set_tensor_test.go index da89b58c2..906c06d58 100644 --- a/internal/grpc/convert_set_tensor_test.go +++ b/internal/grpc/convert_set_tensor_test.go @@ -64,7 +64,6 @@ func tensorQuantityValue(dimensions []int64, components ...*pb.Quantity) *pb.Val } func stringValue(s string) *pb.Value { return &pb.Value{Kind: &pb.Value_StringValue{StringValue: s}} } -func boolValue(b bool) *pb.Value { return &pb.Value{Kind: &pb.Value_BoolValue{BoolValue: b}} } // A set crosses as its own arm holding each distinct element once, in the // runtime's canonical order, and reads back as an equal set whatever order it From 5083138678a69fd458b543df8f26469681f36f6d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 03:03:41 +0000 Subject: [PATCH 05/20] fix(runtime): total canonical set order, one key per equal collection, clients refuse repeated set members MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit canonicalLess is now a three-way total order: same-rendering values fall back to declaration identity (enumeration literals, variants) or to their elements (sequences, sets, arrays), so equal sets enumerate alike whatever order their members were inserted in. A Set and a Sequence that valueEqual accepts as equal now receive the same valueKeyFunc key, so they share a bucket and deduplicate. The Go, Python and Node clients validate an incoming set's uniqueness by their own value equality — nested sets, sequences and quantities included — as the Rust and Java clients already did. Co-Authored-By: jason.han --- .../unreleased/set-and-tensor-values.added.md | 2 +- client/opensysml/README.md | 9 +- client/opensysml/convert.go | 8 +- client/opensysml/equal.go | 67 ++++++++ client/opensysml/structured_internal_test.go | 57 +++++++ clients/node/README.md | 8 +- clients/node/src/core/index.ts | 2 +- clients/node/src/core/values.ts | 146 +++++++++++++++++- clients/node/test/values.test.ts | 50 ++++++ clients/python/opensysml/values.py | 33 +++- clients/python/tests/test_set_tensor.py | 32 ++++ internal/core/runtime/set_feature_test.go | 59 +++++++ internal/core/runtime/set_order.go | 109 ++++++++++--- internal/core/runtime/value_equality.go | 34 +--- 14 files changed, 549 insertions(+), 67 deletions(-) create mode 100644 client/opensysml/equal.go diff --git a/changes/unreleased/set-and-tensor-values.added.md b/changes/unreleased/set-and-tensor-values.added.md index b02424c2f..f6bd62084 100644 --- a/changes/unreleased/set-and-tensor-values.added.md +++ b/changes/unreleased/set-and-tensor-values.added.md @@ -1,3 +1,3 @@ - **A `Collections::Set` holds a set.** Where the Kernel Data Type Library declares a collection's `elements` unique and unordered — `Set`, `UniqueCollection`, `Map` — the runtime now holds them as a set value: each member once, `size` counting members, equality that ignores the order the members were written in, and `contains`/`containsAll` as membership. A set consumed by an ordered operation (`collect`, `head`, `#`, a comparison against a sequence, a trace, a write into an `ordered` or `nonunique` feature) enumerates in one canonical order — Booleans, then numbers ascending, strings, quantities, enumeration literals, objects — so equal sets behave alike. What the library declares ordered or nonunique (`Bag`, `List`, `Array`, `OrderedSet`, `OrderedMap`, every `SequenceFunctions` result) is unchanged. - **Tensor quantities of any rank.** A `TensorMeasurementReference` with three or more `dimensions` builds a rank-three-or-higher tensor whose `#` takes one index per dimension; the wrong number of indexes, an index out of its dimension's range, a non-Integer index, a component count off the flattened size and arithmetic between two shapes are each a typed error naming what was wrong, and the shape survives `+`, `-` and the scalar multiplications. -- **Sets and tensor quantities cross gRPC whole.** `Value` gains a `set` arm (the members as `Value`s, in canonical order, readable in any order, a repeated member refused) and a `tensor_quantity` arm (`dimensions` and one `Quantity` per row-major component, at any rank), advertised as the `set_values` and `tensor_values` capabilities. The Go, Python, Node, Rust and Java clients decode both to native types that check their own invariants, send them as calc arguments and refuse them to a service that does not advertise the capability; a service withholding one reports the unsupported null it always did. Neither value has an RDF literal form — the mapping writes the model's expressions, which round trip exactly — and neither compiles natively: `sysml -compile` refuses a calc that uses one with a typed error naming the type. +- **Sets and tensor quantities cross gRPC whole.** `Value` gains a `set` arm (the members as `Value`s, in canonical order, readable in any order, a repeated member refused by the service and by every client) and a `tensor_quantity` arm (`dimensions` and one `Quantity` per row-major component, at any rank), advertised as the `set_values` and `tensor_values` capabilities. The Go, Python, Node, Rust and Java clients decode both to native types that check their own invariants, send them as calc arguments and refuse them to a service that does not advertise the capability; a service withholding one reports the unsupported null it always did. Neither value has an RDF literal form — the mapping writes the model's expressions, which round trip exactly — and neither compiles natively: `sysml -compile` refuses a calc that uses one with a typed error naming the type. diff --git a/client/opensysml/README.md b/client/opensysml/README.md index a69d39eaf..1d8d0a92b 100644 --- a/client/opensysml/README.md +++ b/client/opensysml/README.md @@ -214,10 +214,13 @@ service without them would read the value as null, so the client refuses with A `Set` arrives with its elements in the service's canonical order — Booleans, then numbers, strings, quantities, enumeration literals and objects, each class -in its own order — so two equal sets arrive alike; a `Set` you send may list its +in its own order — so two equal sets arrive alike, and one that lists a member +twice reads as an unsupported `Null` naming it; a `Set` you send may list its elements in any order, but listing one twice is refused by the service rather -than read as one element. A `TensorQuantity` carries its dimensions and one -`Quantity` per component in row-major order, at any rank. +than read as one element. `Set.Contains` tests membership and `Equal` compares +any two values — sets by membership, sequences in order, an `Int` never a +`Real`. A `TensorQuantity` carries its dimensions and one `Quantity` per +component in row-major order, at any rank. ## Stability diff --git a/client/opensysml/convert.go b/client/opensysml/convert.go index 0531d4cc4..01334a261 100644 --- a/client/opensysml/convert.go +++ b/client/opensysml/convert.go @@ -1,6 +1,8 @@ package opensysml import ( + "fmt" + pb "github.com/Open-MBEE/OpenSysML/api/proto" sysmlgrpc "github.com/Open-MBEE/OpenSysML/internal/grpc" ) @@ -173,7 +175,11 @@ func valueFromProto(value *pb.Value) Value { case *pb.Value_Set: out := make(Set, 0, len(kind.Set.GetElements())) for _, element := range kind.Set.GetElements() { - out = append(out, valueFromProto(element)) + member := valueFromProto(element) + if out.Contains(member) { + return Null(fmt.Sprintf("unsupported: set lists a member twice: %v", member)) + } + out = append(out, member) } return out case *pb.Value_TensorQuantity: diff --git a/client/opensysml/equal.go b/client/opensysml/equal.go new file mode 100644 index 000000000..ac862c59f --- /dev/null +++ b/client/opensysml/equal.go @@ -0,0 +1,67 @@ +package opensysml + +import "slices" + +// Equal reports whether two values are the same value: the same kind holding +// the same contents. An Int is never a Real, a Sequence's order counts, a +// Set's does not, a Null is one whatever its reason, and a nil Value equals +// only another nil. +func Equal(a, b Value) bool { + switch x := a.(type) { + case nil: + return b == nil + case Null: + _, ok := b.(Null) + return ok + case Sequence: + y, ok := b.(Sequence) + return ok && slices.EqualFunc(x, y, Equal) + case Set: + y, ok := b.(Set) + if !ok || len(x) != len(y) { + return false + } + for _, e := range x { + if !y.Contains(e) { + return false + } + } + return true + case Array: + y, ok := b.(Array) + return ok && slices.Equal(x.Dimensions, y.Dimensions) && slices.EqualFunc(x.Elements, y.Elements, Equal) + case Vector: + y, ok := b.(Vector) + return ok && slices.Equal(x, y) + case VectorQuantity: + y, ok := b.(VectorQuantity) + return ok && slices.EqualFunc(x, y, quantityEqual) + case TensorQuantity: + y, ok := b.(TensorQuantity) + return ok && slices.Equal(x.Dimensions, y.Dimensions) && slices.EqualFunc(x.Components, y.Components, quantityEqual) + case Quantity: + y, ok := b.(Quantity) + return ok && quantityEqual(x, y) + case MeasurementRef: + y, ok := b.(MeasurementRef) + return ok && x.Unit == y.Unit && x.UnitID == y.UnitID && unitTermEqual(x.Term, y.Term) + default: + return a == b + } +} + +// Contains reports whether value is a member of the set. +func (s Set) Contains(value Value) bool { + return slices.ContainsFunc(s, func(e Value) bool { return Equal(e, value) }) +} + +func quantityEqual(a, b Quantity) bool { + return a.Magnitude == b.Magnitude && a.Unit == b.Unit && unitTermEqual(a.Term, b.Term) +} + +func unitTermEqual(a, b *UnitTerm) bool { + if a == nil || b == nil { + return a == b + } + return a.ScaleNum == b.ScaleNum && a.ScaleDen == b.ScaleDen && slices.Equal(a.Factors, b.Factors) +} diff --git a/client/opensysml/structured_internal_test.go b/client/opensysml/structured_internal_test.go index b556d4c62..73ab70093 100644 --- a/client/opensysml/structured_internal_test.go +++ b/client/opensysml/structured_internal_test.go @@ -179,6 +179,63 @@ func TestMalformedTensorAnswersAreNullsNamingTheFault(t *testing.T) { } } +// A set in an answer listing a member twice — by value, nested collections and +// quantities included — reads as an unsupported null naming the member; one +// whose members only look alike reads as its elements. +func TestRepeatedSetMembersAreNullsNamingTheFault(t *testing.T) { + pbInt := func(n int64) *pb.Value { return &pb.Value{Kind: &pb.Value_IntValue{IntValue: n}} } + pbReal := func(x float64) *pb.Value { return &pb.Value{Kind: &pb.Value_RealValue{RealValue: x}} } + pbSeq := func(elements ...*pb.Value) *pb.Value { + return &pb.Value{Kind: &pb.Value_Sequence{Sequence: &pb.ValueSequence{Elements: elements}}} + } + pbSet := func(elements ...*pb.Value) *pb.Value { + return &pb.Value{Kind: &pb.Value_Set{Set: &pb.ValueSet{Elements: elements}}} + } + pbQty := func(n int64, unit string) *pb.Value { + return &pb.Value{Kind: &pb.Value_Quantity{Quantity: &pb.Quantity{Magnitude: &pb.Quantity_IntMagnitude{IntMagnitude: n}, Unit: unit}}} + } + for name, value := range map[string]*pb.Value{ + "integer twice": pbSet(pbInt(1), pbInt(2), pbInt(1)), + "sequence twice": pbSet(pbSeq(pbInt(1), pbInt(2)), pbSeq(pbInt(1), pbInt(2))), + "set twice, reordered": pbSet(pbSet(pbInt(1), pbInt(2)), pbSet(pbInt(2), pbInt(1))), + "quantity twice": pbSet(pbQty(1, "m"), pbQty(1, "m")), + "empty set twice": pbSet(pbSet(), pbSet()), + } { + t.Run(name, func(t *testing.T) { + got := valueFromProto(value) + null, ok := got.(Null) + if !ok || !strings.HasPrefix(string(null), "unsupported: set lists a member twice: ") { + t.Fatalf("read as %#v, want an unsupported Null naming the repeated member", got) + } + }) + } + for name, tc := range map[string]struct { + value *pb.Value + want Set + }{ + "integer and real": {pbSet(pbInt(1), pbReal(1)), Set{Int(1), Real(1)}}, + "sequence and set": {pbSet(pbSeq(pbInt(1)), pbSet(pbInt(1))), Set{Sequence{Int(1)}, Set{Int(1)}}}, + "sequences reordered": {pbSet(pbSeq(pbInt(1), pbInt(2)), pbSeq(pbInt(2), pbInt(1))), Set{Sequence{Int(1), Int(2)}, Sequence{Int(2), Int(1)}}}, + "quantities in a unit": {pbSet(pbQty(1, "m"), pbQty(1, "km")), Set{Quantity{Magnitude: Int(1), Unit: "m"}, Quantity{Magnitude: Int(1), Unit: "km"}}}, + "empty and singleton": {pbSet(pbSet(), pbSet(pbSet())), Set{Set{}, Set{Set{}}}}, + } { + t.Run(name, func(t *testing.T) { + if got := valueFromProto(tc.value); !reflect.DeepEqual(got, tc.want) { + t.Errorf("read as %#v, want %#v", got, tc.want) + } + }) + } + if !Equal(Set{Int(1), Sequence{Int(2), Int(3)}}, Set{Sequence{Int(2), Int(3)}, Int(1)}) { + t.Error("sets holding the same members in another order are not Equal") + } + if Equal(Set{Int(1)}, Sequence{Int(1)}) || Equal(Int(1), Real(1)) || Equal(nil, Null("")) { + t.Error("values of different kinds are Equal") + } + if !Equal(Null("a"), Null("b")) || !Equal(Unset{}, Unset{}) || Equal(Unset{}, Null("")) { + t.Error("a Null is not one whatever its reason, or an Unset is not one") + } +} + // A malformed measurement reference in an answer reads as an unsupported null // naming the fault; a well-formed one reads as itself, reduction and identity // intact. diff --git a/clients/node/README.md b/clients/node/README.md index dae7ae10b..e2010e5cb 100644 --- a/clients/node/README.md +++ b/clients/node/README.md @@ -195,9 +195,11 @@ value kinds those name (`array`, `vector`, `vectorQuantity`; `measurementRef`; A function closing over the bindings of a behavior body has no wire form and is sent as `null` by every service. A `set` arrives with its elements in the service's canonical order, so two equal sets arrive -alike; one sent to the service may list them in any order, but not twice. A -`tensorQuantity` carries its `dimensions` and one quantity per component, -row-major. +alike, and one listing a member twice is a `MalformedValueError`; one sent to +the service may list them in any order, but not twice. `valuesEqual` is the +membership test: sets by membership, sequences in order, an `int` never a +`real`. A `tensorQuantity` carries its `dimensions` and one quantity per +component, row-major. ## Failures are typed diff --git a/clients/node/src/core/index.ts b/clients/node/src/core/index.ts index 7d9e0e1a4..1d8fead7c 100644 --- a/clients/node/src/core/index.ts +++ b/clients/node/src/core/index.ts @@ -72,7 +72,7 @@ export { export type { FailureCause, ModelDiagnostic } from "./errors.js"; export { fromHandshakeError, fromRpcError, statusName } from "./status.js"; export type { NotFoundSubject } from "./status.js"; -export { decodeValue, decodeVerdict, encodeValue, formatValue } from "./values.js"; +export { decodeValue, decodeVerdict, encodeValue, formatValue, valuesEqual } from "./values.js"; export type { ArrayValue, ComplexValue, diff --git a/clients/node/src/core/values.ts b/clients/node/src/core/values.ts index f872e0b44..da29bc5da 100644 --- a/clients/node/src/core/values.ts +++ b/clients/node/src/core/values.ts @@ -173,10 +173,10 @@ export type SysMLVerdict = * * @throws {MalformedValueError} for a value that contradicts itself: an array * whose elements do not fill its dimensions, a vector with a component that - * is not a number, a vector quantity with no components, a tensor quantity - * whose components do not fill its dimensions, a quantity (alone or as a - * component) with no magnitude, or a measurement reference naming no unit - * or a unit without its reduction. + * is not a number, a vector quantity with no components, a set listing a + * member twice, a tensor quantity whose components do not fill its + * dimensions, a quantity (alone or as a component) with no magnitude, or a + * measurement reference naming no unit or a unit without its reduction. */ export function decodeValue(value: Value | undefined): SysMLValue { if (value === undefined) { @@ -557,7 +557,143 @@ function decodeArray(array: ArrayMessage): ArrayValue { } function decodeSet(set: ValueSet): SysMLValue[] { - return set.elements.map(decodeValue); + const elements: SysMLValue[] = []; + for (const element of set.elements) { + const member = decodeValue(element); + if (elements.some((held) => valuesEqual(held, member))) { + throw new MalformedValueError( + `a set lists a member twice: ${formatValue(member)}`, + ); + } + elements.push(member); + } + return elements; +} + +/** + * Whether two values are the same value: the same kind holding the same + * contents. An `int` is never a `real`, a `sequence`'s order counts and a + * `set`'s does not; a `null` is the same whatever its reason. + */ +export function valuesEqual(a: SysMLValue, b: SysMLValue): boolean { + switch (a.kind) { + case "int": + return b.kind === "int" && a.value === b.value; + case "real": + return b.kind === "real" && a.value === b.value; + case "complex": + return ( + b.kind === "complex" && + a.value.real === b.value.real && + a.value.imaginary === b.value.imaginary + ); + case "boolean": + return b.kind === "boolean" && a.value === b.value; + case "string": + return b.kind === "string" && a.value === b.value; + case "instance": + return b.kind === "instance" && a.id === b.id; + case "sequence": + return b.kind === "sequence" && elementsEqual(a.elements, b.elements); + case "quantity": + return b.kind === "quantity" && quantitiesEqual(a, b); + case "measurementRef": + return ( + b.kind === "measurementRef" && + a.unit === b.unit && + a.unitId === b.unitId && + unitTermsEqual(a.unitTerm, b.unitTerm) + ); + case "enum": + return ( + b.kind === "enum" && + a.value.literalId === b.value.literalId && + a.value.enumerationId === b.value.enumerationId && + a.value.name === b.value.name + ); + case "array": + return ( + b.kind === "array" && + dimensionsEqual(a.dimensions, b.dimensions) && + elementsEqual(a.elements, b.elements) + ); + case "vector": + return b.kind === "vector" && magnitudesEqual(a.components, b.components); + case "vectorQuantity": + return ( + b.kind === "vectorQuantity" && + componentsEqual(a.components, b.components) + ); + case "set": + return ( + b.kind === "set" && + a.elements.length === b.elements.length && + a.elements.every((e) => b.elements.some((o) => valuesEqual(e, o))) + ); + case "tensorQuantity": + return ( + b.kind === "tensorQuantity" && + dimensionsEqual(a.dimensions, b.dimensions) && + componentsEqual(a.components, b.components) + ); + case "null": + case "unset": + case "absent": + return b.kind === a.kind; + } +} + +function elementsEqual(a: SysMLValue[], b: SysMLValue[]): boolean { + return ( + a.length === b.length && + a.every((e, i) => valuesEqual(e, b[i])) + ); +} + +function dimensionsEqual(a: bigint[], b: bigint[]): boolean { + return a.length === b.length && a.every((d, i) => d === b[i]); +} + +function magnitudesEqual(a: Magnitude[], b: Magnitude[]): boolean { + return ( + a.length === b.length && + a.every((m, i) => m.kind === b[i]?.kind && m.value === b[i]?.value) + ); +} + +function componentsEqual(a: QuantityValue[], b: QuantityValue[]): boolean { + return ( + a.length === b.length && + a.every((q, i) => quantitiesEqual(q, b[i])) + ); +} + +function quantitiesEqual(a: QuantityValue, b: QuantityValue): boolean { + return ( + a.magnitude.kind === b.magnitude.kind && + a.magnitude.value === b.magnitude.value && + a.unit === b.unit && + unitTermsEqual(a.unitTerm, b.unitTerm) + ); +} + +function unitTermsEqual( + a: UnitFactorization | undefined, + b: UnitFactorization | undefined, +): boolean { + if (a === undefined || b === undefined) { + return a === b; + } + return ( + a.scaleNum === b.scaleNum && + a.scaleDen === b.scaleDen && + a.factors.length === b.factors.length && + a.factors.every( + (f, i) => + f.unitId === b.factors[i]?.unitId && + f.exponent === b.factors[i]?.exponent, + ) + ); } function decodeTensorQuantity(tensor: TensorQuantity): TensorQuantityValue { diff --git a/clients/node/test/values.test.ts b/clients/node/test/values.test.ts index 5e9a4bf2c..8ef943262 100644 --- a/clients/node/test/values.test.ts +++ b/clients/node/test/values.test.ts @@ -28,6 +28,7 @@ import { encodeValue, failureCause, formatValue, + valuesEqual, type SysMLValue, } from "../src/core/values.js"; @@ -314,6 +315,55 @@ test("a set is its elements, each once, in the order the service sent them", () ); }); +const seqOf = (...elements: ReturnType[]) => + create(ValueSchema, { kind: { case: "sequence", value: create(ValueSequenceSchema, { elements }) } }); +const bool = (value: boolean) => create(ValueSchema, { kind: { case: "boolValue", value } }); +const quantity = (q: ReturnType) => create(ValueSchema, { kind: { case: "quantity", value: q } }); + +test("a set that lists a member twice is malformed, judged by value", () => { + const twice = [ + setOf(int(1n), int(2n), int(1n)), + setOf(seqOf(int(1n), int(2n)), seqOf(int(1n), int(2n))), + setOf(setOf(int(1n), int(2n)), setOf(int(2n), int(1n))), + setOf(setOf(), setOf()), + setOf(quantity(metres(1)), quantity(metres(1))), + setOf(bool(true), seqOf(), bool(true)), + setOf(array([1n], int(1n)), array([1n], int(1n))), + ]; + for (const set of twice) { + assert.throws(() => decodeValue(set), { + name: "MalformedValueError", + message: /^a set lists a member twice: /, + }); + } + + // Members that merely look alike are distinct: an int is never a real, a + // sequence's order counts, a sequence is never a set of the same elements. + const alike = [ + setOf(int(1n), real(1)), + setOf(bool(true), int(1n)), + setOf(seqOf(int(1n), int(2n)), seqOf(int(2n), int(1n))), + setOf(seqOf(int(1n)), setOf(int(1n))), + setOf(setOf(), setOf(setOf())), + setOf(array([1n, 2n], int(1n), int(2n)), array([2n, 1n], int(1n), int(2n))), + setOf(quantity(metres(1)), quantity(metres(2))), + ]; + for (const set of alike) { + const decoded = decodeValue(set); + assert.equal(decoded.kind, "set"); + assert.equal(decoded.elements.length, 2); + assert.ok(!valuesEqual(decoded.elements[0], decoded.elements[1])); + } + + // valuesEqual is the membership test itself: sets by membership, sequences in order. + const a = decodeValue(setOf(int(1n), setOf(int(2n), int(3n)))); + const b = decodeValue(setOf(setOf(int(3n), int(2n)), int(1n))); + assert.ok(valuesEqual(a, b)); + assert.ok(!valuesEqual(a, decodeValue(setOf(int(1n), setOf(int(2n)))))); + assert.ok(valuesEqual({ kind: "null", reason: "x" }, { kind: "null", reason: "y" })); + assert.ok(!valuesEqual({ kind: "unset" }, { kind: "absent" })); +}); + test("a tensor quantity keeps its rank, its shape and its row-major components", () => { const cube = decodeValue(tensor([2n, 2n, 2n], ...[1, 2, 3, 4, 5, 6, 7, 8].map(metres))); assert.equal(cube.kind, "tensorQuantity"); diff --git a/clients/python/opensysml/values.py b/clients/python/opensysml/values.py index 59b31bcf1..1a109c43d 100644 --- a/clients/python/opensysml/values.py +++ b/clients/python/opensysml/values.py @@ -2,7 +2,7 @@ import math from dataclasses import dataclass, field -from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence, Tuple, Union +from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Sequence, Tuple, Union from opensysml.enumeration import EnumLiteral from opensysml.errors import FeatureValueError, OpenSysMLError, UnsupportedValueError @@ -707,7 +707,7 @@ class SetValue: elements: Tuple[Any, ...] - def __init__(self, elements: Sequence[Any] = ()) -> None: + def __init__(self, elements: Iterable[Any] = ()) -> None: object.__setattr__(self, "elements", tuple(elements)) def __len__(self) -> int: @@ -717,7 +717,7 @@ def __iter__(self) -> Iterator[Any]: return iter(self.elements) def __contains__(self, item: Any) -> bool: - return any(element == item for element in self.elements) + return any(same_value(element, item) for element in self.elements) def __eq__(self, other: object) -> bool: if isinstance(other, (set, frozenset)): @@ -731,8 +731,18 @@ def __hash__(self) -> int: @classmethod def from_pb(cls, pb_set, resolve_instance=None) -> "SetValue": - """Build from a ``ValueSet`` protobuf message, elements in the order sent.""" - return cls(value_to_python(v, resolve_instance) for v in pb_set.elements) + """Build from a ``ValueSet`` protobuf message, elements in the order sent. + + Raises: + UnsupportedValueError: If the message lists a member twice. + """ + elements: List[Any] = [] + for pb_value in pb_set.elements: + element = value_to_python(pb_value, resolve_instance) + if any(same_value(element, held) for held in elements): + raise UnsupportedValueError(f"malformed set: member listed twice: {element}") + elements.append(element) + return cls(elements) def to_pb(self, encode: Callable[[Any], "sysml_pb2.Value"]) -> "sysml_pb2.ValueSet": """Encode as a ``ValueSet`` message, each element through ``encode``.""" @@ -742,6 +752,19 @@ def __str__(self) -> str: return "{" + ", ".join(str(e) for e in self.elements) + "}" +def same_value(a: Any, b: Any) -> bool: + """Whether two decoded values are the same value, as :class:`SetValue` membership judges it. + + ``==`` decides, except that a ``bool`` is never a number — ``True`` and ``1`` + are distinct values in a model — in a nested ``list`` too. + """ + if isinstance(a, bool) or isinstance(b, bool): + return isinstance(a, bool) and isinstance(b, bool) and a == b + if isinstance(a, list) and isinstance(b, list): + return len(a) == len(b) and all(same_value(x, y) for x, y in zip(a, b)) + return a == b + + @dataclass(frozen=True) class TensorQuantity: """A tensor quantity of any rank: its shape and one quantity per component. diff --git a/clients/python/tests/test_set_tensor.py b/clients/python/tests/test_set_tensor.py index b637042e2..9ff3c0f5c 100644 --- a/clients/python/tests/test_set_tensor.py +++ b/clients/python/tests/test_set_tensor.py @@ -142,6 +142,38 @@ def test_a_set_nests_and_is_nested_in_place(): assert value_to_python(array) == Array((1,), (SetValue((1,)),)) +def pb_seq(*elements): + return sysml_pb2.Value(sequence=sysml_pb2.ValueSequence(elements=list(elements))) + + +@pytest.mark.parametrize("elements", [ + (pb_int(1), pb_int(2), pb_int(1)), + (pb_int(1), sysml_pb2.Value(real_value=1.0)), + (pb_seq(pb_int(1), pb_int(2)), pb_seq(pb_int(1), pb_int(2))), + (pb_set(pb_int(1), pb_int(2)), pb_set(pb_int(2), pb_int(1))), + (pb_set(), pb_set()), + (sysml_pb2.Value(quantity=pb_pascal(1.0)), sysml_pb2.Value(quantity=pb_pascal(1.0))), + (sysml_pb2.Value(bool_value=True), pb_seq(), sysml_pb2.Value(bool_value=True)), +]) +def test_a_set_listing_a_member_twice_is_malformed(elements): + with pytest.raises(UnsupportedValueError, match="malformed set: member listed twice"): + value_to_python(pb_set(*elements)) + + +@pytest.mark.parametrize("elements, expected", [ + ((sysml_pb2.Value(bool_value=True), pb_int(1)), SetValue((True, 1))), + ((pb_seq(pb_int(1), pb_int(2)), pb_seq(pb_int(2), pb_int(1))), SetValue(([1, 2], [2, 1]))), + ((pb_seq(pb_int(1)), pb_set(pb_int(1))), SetValue(([1], SetValue((1,))))), + ((pb_seq(sysml_pb2.Value(bool_value=True)), pb_seq(pb_int(1))), SetValue(([True], [1]))), + ((pb_set(), pb_set(pb_set())), SetValue((SetValue(), SetValue((SetValue(),))))), +]) +def test_members_that_only_look_alike_are_distinct(elements, expected): + got = value_to_python(pb_set(*elements)) + assert len(got) == len(elements) + assert got == expected + assert 1 not in SetValue((True,)) and True not in SetValue((1,)) + + def test_a_set_survives_the_wire_bytes(): value = pb_set(pb_int(1), sysml_pb2.Value(string_value="a"), pb_set()) again = sysml_pb2.Value() diff --git a/internal/core/runtime/set_feature_test.go b/internal/core/runtime/set_feature_test.go index cf232b7ff..8c7693fb9 100644 --- a/internal/core/runtime/set_feature_test.go +++ b/internal/core/runtime/set_feature_test.go @@ -30,6 +30,10 @@ const setModel = `package test { attribute arr : Array { :>> elements = (3, 1, 2, 2); :>> dimensions = (4); } attribute nested : Set { :>> elements = (s, e, s); } attribute mixed : Set { :>> elements = (2, "a", true, 1.5, 1, "a"); } + enum def Color { red; green; } + package Other { enum def Color { red; } } + attribute lits : Set { :>> elements = (Color::red, Other::Color::red, Color::green); } + attribute stil : Set { :>> elements = (Other::Color::red, Color::green, Color::red); } attribute plain : Integer[*] = s.elements; attribute ordered : Integer[*] ordered = s.elements; @@ -254,3 +258,58 @@ func TestCanonicalOrderIsTotal(t *testing.T) { t.Errorf("large integers = %v, want them ascending", got) } } + +// TestSameNamedLiteralsOrderByDeclaration pins that two distinct literals whose +// enumerations share a name — rendered alike — still take one position each, +// so equal sets enumerate alike whatever order they were written in. +func TestSameNamedLiteralsOrderByDeclaration(t *testing.T) { + ctx, scope := setModelContext(t) + lits, stil := mustEvalIn(t, ctx, scope, "lits.elements"), mustEvalIn(t, ctx, scope, "stil.elements") + if lits.Set().Size() != 3 || !valueEqual(lits, stil) { + t.Fatalf("lits = %s, stil = %s, want three equal members", FormatValue(lits), FormatValue(stil)) + } + if got, want := FormatTraceValue(lits), "{Color::green, Color::red, Color::red}"; got != want { + t.Errorf("trace = %s, want %s", got, want) + } + a, b := lits.Set().Elements(), stil.Set().Elements() + for i := range a { + if a[i].Literal() != b[i].Literal() { + t.Errorf("element %d differs between insertion orders: %s in %v, %s in %v", i, symbols.FQNOf(a[i].Literal()), a, symbols.FQNOf(b[i].Literal()), b) + } + } + if a[1].Literal() == a[2].Literal() || symbols.FQNOf(a[1].Literal()) != "test::Color::red" || symbols.FQNOf(a[2].Literal()) != "test::Other::Color::red" { + t.Errorf("same-named literals = %s, %s, want test::Color::red before test::Other::Color::red", symbols.FQNOf(a[1].Literal()), symbols.FQNOf(a[2].Literal())) + } +} + +// TestSetMembersEqualAcrossCollectionKinds pins that a sequence and the set it +// equals — valueEqual holds across the two kinds — share one key, so a set +// admits only one of them and finds either. +func TestSetMembersEqualAcrossCollectionKinds(t *testing.T) { + ints := func(ns ...int64) []Value { + vals := make([]Value, len(ns)) + for i, n := range ns { + vals[i] = Value{Kind: ValConst, Const: semantics.Value{Kind: semantics.ValInt, Int: n}} + } + return vals + } + seq, set, other := sequenceOf(ints(1, 2)), setOf(ints(2, 1)), sequenceOf(ints(2, 1)) + if valueKeyFunc(seq) != valueKeyFunc(set) { + t.Errorf("keys differ for %s and %s", FormatValue(seq), FormatValue(set)) + } + for _, members := range [][]Value{{seq, set, other}, {set, seq, other}, {other, set, seq}} { + outer := setOf(members).Set() + if outer.Size() != 2 { + t.Errorf("set of %v has %d members, want 2", members, outer.Size()) + } + for _, m := range []Value{seq, set, other} { + if !outer.Contains(m) { + t.Errorf("set of %v lacks %s", members, FormatValue(m)) + } + } + } + nested := setOf([]Value{setOf(ints(1, 2)), setOf(ints(2, 1))}).Set() + if nested.Size() != 1 { + t.Errorf("set of two equal sets has %d members, want 1", nested.Size()) + } +} diff --git a/internal/core/runtime/set_order.go b/internal/core/runtime/set_order.go index 3f3e4840e..d6ad00baf 100644 --- a/internal/core/runtime/set_order.go +++ b/internal/core/runtime/set_order.go @@ -1,9 +1,12 @@ package runtime import ( + "cmp" "math" + "strings" "github.com/Open-MBEE/OpenSysML/internal/core/semantics" + "github.com/Open-MBEE/OpenSysML/internal/core/symbols" ) // canonicalLess is the total order a set enumerates its elements in, so that @@ -12,41 +15,105 @@ import ( // (null, Booleans, numbers, complex numbers, strings, quantities, enumeration // literals, objects, then every other kind), then within a class by their own // order where they have one (numeric, lexicographic, dimension then magnitude, -// object identity) and by their trace rendering otherwise. +// declaration, object identity), then by their trace rendering, and finally by +// their elements, so that only equal values compare as neither before nor after. func canonicalLess(a, b Value) bool { + return canonicalCompare(a, b) < 0 +} + +// canonicalCompare orders a before b as negative, after as positive, and equal +// values — valueEqual ones, whose position a set never depends on — as zero. +func canonicalCompare(a, b Value) int { ca, cb := canonicalClass(a), canonicalClass(b) if ca != cb { - return ca < cb + return cmp.Compare(ca, cb) } switch ca { + case classNull: + return 0 case classBool: - return !a.Const.Bool && b.Const.Bool + return compareBool(a.Const.Bool, b.Const.Bool) case classNumber: - return numberLess(a.Const, b.Const) + return compareNumbers(a.Const, b.Const) case classComplex: x, y := a.Complex(), b.Complex() - if real(x) != real(y) { - return real(x) < real(y) - } - if imag(x) != imag(y) { - return imag(x) < imag(y) + if c := cmp.Compare(real(x), real(y)); c != 0 { + return c } + return cmp.Compare(imag(x), imag(y)) case classString: - return a.Str() < b.Str() + return strings.Compare(a.Str(), b.Str()) case classQuantity: qa, qb := a.Quantity(), b.Quantity() - if da, db := qa.Unit.Term.DimensionKey(), qb.Unit.Term.DimensionKey(); da != db { - return da < db + if c := strings.Compare(qa.Unit.Term.DimensionKey(), qb.Unit.Term.DimensionKey()); c != 0 { + return c } - if c, err := semantics.CompareMagnitudes(*qa, *qb); err == nil && c != 0 { - return c < 0 + if c, err := semantics.CompareMagnitudes(*qa, *qb); err == nil { + return c } + case classEnumLiteral: + return compareSymbols(a.Literal(), b.Literal()) case classObject: - if a.Instance != b.Instance { - return a.Instance < b.Instance + if a.Kind != b.Kind { + return cmp.Compare(a.Kind, b.Kind) + } + if a.Kind == ValVariant { + return compareSymbols(a.Variant(), b.Variant()) + } + return cmp.Compare(a.Instance, b.Instance) + } + if c := strings.Compare(FormatTraceValue(a), FormatTraceValue(b)); c != 0 { + return c + } + if a.Kind != b.Kind { + return cmp.Compare(a.Kind, b.Kind) + } + switch a.Kind { + case ValSequence, ValSet: + return compareElements(elementsOf(a), elementsOf(b)) + case ValArray: + return compareElements(a.Array().Elements, b.Array().Elements) + } + return 0 +} + +// compareElements orders two element lists lexicographically, a shorter prefix first. +func compareElements(a, b []Value) int { + for i := 0; i < len(a) && i < len(b); i++ { + if c := canonicalCompare(a[i], b[i]); c != 0 { + return c } } - return FormatTraceValue(a) < FormatTraceValue(b) + return cmp.Compare(len(a), len(b)) +} + +// compareSymbols orders declarations by qualified name, then by the document +// and position declaring them, so same-named literals of different +// enumerations still order deterministically. +func compareSymbols(a, b *symbols.Symbol) int { + if a == b { + return 0 + } + if a == nil || b == nil { + return compareBool(a != nil, b != nil) + } + if c := strings.Compare(symbols.FQNOf(a), symbols.FQNOf(b)); c != 0 { + return c + } + if c := strings.Compare(a.DocName, b.DocName); c != 0 { + return c + } + return cmp.Compare(a.DeclSpan.Offset, b.DeclSpan.Offset) +} + +func compareBool(a, b bool) int { + if a == b { + return 0 + } + if !a { + return -1 + } + return 1 } const ( @@ -88,12 +155,12 @@ func canonicalClass(v Value) int { return classOther } -// numberLess orders the numeric constants, infinity above every finite number. -func numberLess(a, b semantics.Value) bool { +// compareNumbers orders the numeric constants, infinity above every finite number. +func compareNumbers(a, b semantics.Value) int { if a.Kind == semantics.ValInt && b.Kind == semantics.ValInt { - return a.Int < b.Int + return cmp.Compare(a.Int, b.Int) } - return numberOf(a) < numberOf(b) + return cmp.Compare(numberOf(a), numberOf(b)) } func numberOf(v semantics.Value) float64 { diff --git a/internal/core/runtime/value_equality.go b/internal/core/runtime/value_equality.go index 645ed1961..72e6b246d 100644 --- a/internal/core/runtime/value_equality.go +++ b/internal/core/runtime/value_equality.go @@ -26,7 +26,7 @@ type valueKey struct { // valueKeyFunc extracts a comparable key from a Value. Values valueEqual holds // equal share a key: a whole number has the Integer's whatever kind carries it, -// and every empty value has null's. +// every empty value has null's, and a set has the key of its canonical sequence. func valueKeyFunc(v Value) valueKey { if isEmptyValue(v) { return valueKey{kind: ValNull} @@ -58,10 +58,9 @@ func valueKeyFunc(v Value) valueKey { key.strVal = v.Str() case ValInstance: key.instID = v.Instance - case ValSequence: - key.colHash = hashSequence(v.Sequence()) - case ValSet: - key.colHash = hashSet(v.Set()) + case ValSequence, ValSet: + key.kind = ValSequence + key.colHash = hashElements(elementsOf(v)) case ValVariant: key.variant = v.Variant() case ValEnumLiteral: @@ -88,13 +87,10 @@ func valueKeyFunc(v Value) valueKey { return key } -// hashSequence computes a content-based hash for a Sequence. -func hashSequence(seq *Sequence) uint64 { - if seq == nil { - return 0 - } +// hashElements computes a content-based hash over elements in order. +func hashElements(elements []Value) uint64 { h := fnv.New64a() - for _, elem := range seq.elements { + for _, elem := range elements { k := valueKeyFunc(elem) // #nosec G115 G104 -- truncation is deliberate for a hash, and // hash.Hash.Write is documented never to return an error. @@ -106,19 +102,3 @@ func hashSequence(seq *Sequence) uint64 { } return h.Sum64() } - -// hashSet computes a content-based hash for a Set (order-invariant). -func hashSet(set *Set) uint64 { - if set == nil { - return 0 - } - var sum uint64 - for _, bucket := range set.elements { - for _, elem := range bucket { - k := valueKeyFunc(elem) - // #nosec G115 -- wrapping is intended: this is a hash, not arithmetic. - sum += uint64(k.intVal) - } - } - return sum -} From 24e400dd33ad2a36a24f25afe92edb0876a62ef9 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 03:22:10 +0000 Subject: [PATCH 06/20] fix(runtime): order like-rendered set members by contents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit canonicalCompare falls through to the contents of every structured kind whose trace text can coincide — array shape and elements, vector and tensor components, quantity unit, measurement reference, frame and transformation keys, expression span — so two unequal values never share a position and equal sets enumerate alike whatever their insertion order. Co-Authored-By: jason.han --- docs/project/spec-compliance.md | 2 +- internal/core/runtime/set_feature_test.go | 56 +++++++++++++++++++++ internal/core/runtime/set_order.go | 59 ++++++++++++++++++++++- 3 files changed, 115 insertions(+), 2 deletions(-) diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index 905833444..0f7e7d792 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -1385,7 +1385,7 @@ they cannot drift apart. | `CollectionFunctions`: `size`, `isEmpty`, `notEmpty`, `contains`, `containsAll`, `head`, `tail`, `last`, `#` over a collection's elements, a set included | `runtime/collections.go`, `runtime/builtins.go` | `runtime/collections_test.go` `TestCollectionScalarResults`, `TestCollectionOperationsOverSets` | ✅ Faithful | | A `Collections::Collection` whose library redefinition of `elements` is unique and not ordered holds a **set** (`ValSet`), the library's own kind: `Set` ("unique and unordered", `Collections.kerml:104-108`), `UniqueCollection` ("unique and not necessarily ordered", `:39-48`) and `Map` (unique `KeyValuePair`s, `:142-152`). `Bag` inherits the root's `nonunique` (`:22`, `:97-101`) and stays a sequence, as does every `OrderedCollection` (`:30-36`) — `Array`, `List`, `OrderedSet` (`ordered`, `:111-121`), `OrderedMap` (`:162-175`) — and any feature a model declares `ordered` or `nonunique` itself. A set holds each member once (`(3, 1, 2, 2, 3)` is three members, `(1, 2, 3)` given already distinct is the same three, `()` the empty set), so `size` answers 3 and `isEmpty` reads the count; a value written or bound to such a feature is admitted as a set, and a set flowing into a feature that holds a sequence (`ordered`, `nonunique`, or a plain `Integer[0..*]`) becomes the sequence of its members in canonical order | `runtime/set_feature.go` `holdsSet`, `declaredOrderedOrNonunique`, `collectionOf`, `declaredCollection`; `shape.go` `EffectiveFeature.HoldsSet`; `instance.go` `admitted`, `materializeIntrinsic`; `subsetting.go` (optional subsetters); `value.go` `Set`, `NewSet`, `Set.Add`, `Set.Size` | conformance `library_set_elements`, `library_set_elements_already_distinct`, `library_set_elements_empty`, `library_unique_collection_elements`, `library_map_elements`, `library_bag_elements`, `library_ordered_set_elements`, `library_set_operations`; `runtime/set_feature_test.go:TestCollectionElementsHoldTheLibraryKind`, `:TestSetFlowsIntoDeclaredCollections`, `:TestCollectionFunctionsOverCollectionObjects` | ✅ Faithful | | Two sets are equal when their members are, in whatever order either was given (`CollectionFunctions::'=='` is `col1.elements->equals(col2.elements)`, and a set's `elements` have no order to compare); `contains` and `containsAll` are membership; a set's elements equal a sequence only when the sequence lists the members in canonical order, and a `Set` is never equal to a `Bag` or `OrderedSet` holding the same values, which are sequences | `runtime/eval.go` `valueEqual` (`ValSet` arm); `runtime/collections.go` `builtinCollectionEquals`, `elementsOf`; `value.go` `Set.Equal`, `Set.Contains` | conformance `library_set_operations` (`equalRegardlessOfOrder`, `elementsEqualRegardlessOfOrder`, `membership`, `allMembers`, `emptiness`, `notASequence`); `runtime/set_feature_test.go:TestSetAgainstSequenceComparesCanonically`, `:TestCollectionFunctionsOverCollectionObjects` | ✅ Faithful | -| A set has no order of its own, so every operation that walks its members in order — `collect`, `select`, `head`, `tail`, `#`, `==` against a sequence, a trace rendering it, a write into a sequence-holding feature, the wire — sees them in one **canonical order**: by class (null, Booleans with `false` first, numbers ascending, complex numbers by real then imaginary part, strings lexicographically, quantities by dimension then magnitude, enumeration literals, objects by identity, then every other kind) and within a class by that order, the trace rendering breaking what remains. The order is total, so equal sets enumerate alike and a trace over a set is the same golden however the set was written | `runtime/set_order.go` `canonicalLess`, `canonicalClass`; `value.go` `Set.Elements` (sorted once, cached); `trace.go` `FormatTraceValue` (`ValSet`, the same enumeration) | conformance `calc_set_consumed_by_ordered_operations` and its trace golden; `runtime/set_feature_test.go:TestCanonicalOrderIsTotal`, `:TestSetRendersCanonically`; `runtime/collections_test.go:TestCollectionOperationsOverSets` | ✅ Faithful — the order is this runtime's documented rule, since the library defines none; it is not a claim about the specification | +| A set has no order of its own, so every operation that walks its members in order — `collect`, `select`, `head`, `tail`, `#`, `==` against a sequence, a trace rendering it, a write into a sequence-holding feature, the wire — sees them in one **canonical order**: by class (null, Booleans with `false` first, numbers ascending, complex numbers by real then imaginary part, strings lexicographically, quantities by dimension then magnitude, enumeration literals by declaration, objects by identity, then every other kind) and within a class by that order, then by the trace rendering, then by contents — elements, components, unit reduction — so only equal values share a position. The order is total, so equal sets enumerate alike and a trace over a set is the same golden however the set was written | `runtime/set_order.go` `canonicalLess`, `canonicalClass`; `value.go` `Set.Elements` (sorted once, cached); `trace.go` `FormatTraceValue` (`ValSet`, the same enumeration) | conformance `calc_set_consumed_by_ordered_operations` and its trace golden; `runtime/set_feature_test.go:TestCanonicalOrderIsTotal`, `:TestSameNamedLiteralsOrderByDeclaration`, `:TestLikeRenderedValuesOrderByContents`, `:TestSetRendersCanonically`; `runtime/collections_test.go:TestCollectionOperationsOverSets` | ✅ Faithful — the order is this runtime's documented rule, since the library defines none; it is not a claim about the specification | | What the library declares ordered stays a sequence: every `SequenceFunctions` result is `Anything[0..*] ordered nonunique` — `union`, `intersection`, `including`, `includingAt`, `excluding` included (`SequenceFunctions.kerml:48-63`) — so `union(s.elements, t.elements)` over two `Set`s is the ordered concatenation of their canonical members, repeats kept, not a set; `(s.elements, s.elements)` likewise lists each member twice. The Kernel Function Library declares no `distinct` function, so none is invented: the members of a sequence, each once, are what a `Set`'s `elements` hold | `runtime/collections.go` (`SequenceFunctions` builtins over `elementsOf`) | conformance `library_set_sequence_functions`, `library_set_operations` (`ordered`, `repeatable`, `plain`) | ✅ Faithful | | `OrderedSet::elements` and `OrderedMap::elements` are declared `ordered` *and* unique (`UniqueCollection` under `OrderedCollection`, `Collections.kerml`), as is any multi-valued feature not declared `nonunique`. Their order is part of the value, so they are held as the sequence written; their uniqueness is not enforced — `OrderedSet { :>> elements = (1, 1, 2); }` reads three elements and `size` answers 3. A set is the one place uniqueness is the value's own definition; checking `unique` as a constraint on an ordered feature's values is separate work | `runtime/set_feature.go` `holdsSet` (declines any feature declared `ordered`) | conformance `library_ordered_set_elements` (order kept) | ⚠️ Approximate — order faithful, uniqueness unchecked | | A set **crosses gRPC** as the `set` arm (`ValueSet.elements`, each a `Value`, listed in canonical order; an incoming set may list them in any order, and one that repeats a member is `ErrSetElementRepeated`), advertised as `set_values`; a service withholding the capability answers an unsupported null naming the value and refuses one sent to it with `UNIMPLEMENTED`, nested anywhere in the argument. It is **not compiled natively**: `sysml -compile` refuses a calc declaring or reading one with `codegen.UnsupportedError` (`type Collections::Set is not Integer, Real or Boolean`). It has **no RDF literal form** of its own: the mapping writes the model, so a `Set`-valued feature is the expression valuing its `elements`, which round trips exactly | `grpc/convert.go` `setToProto`, `protoToSet`, `ErrSetElementRepeated`; `grpc/capability_response.go` (`set_values`); `codegen/compile.go` `UnsupportedError`; `export/rdf_expr.go` | `grpc/convert_set_tensor_test.go:TestSetRoundTrip`, `:TestMalformedSetsAreRejected`, `:TestSetAndTensorCapabilities`, `:TestValueCarriesSetAndTensor`; `repl/compile_test.go:TestCompileRefusesWhatItCannotCompile` (`SetParam`, `SetElements`, `SetLocal`); `export/set_tensor_rdf_test.go:TestSetAndTensorValuesRoundTripAsExpressions`; the *Values* gRPC rows below | ✅ Faithful (the native and RDF refusals are typed and documented, not layouts) | diff --git a/internal/core/runtime/set_feature_test.go b/internal/core/runtime/set_feature_test.go index 8c7693fb9..51b25f542 100644 --- a/internal/core/runtime/set_feature_test.go +++ b/internal/core/runtime/set_feature_test.go @@ -313,3 +313,59 @@ func TestSetMembersEqualAcrossCollectionKinds(t *testing.T) { t.Errorf("set of two equal sets has %d members, want 1", nested.Size()) } } + +// TestLikeRenderedValuesOrderByContents pins that two unequal structured values +// whose trace text is the same — a unit spelt alike that reduces differently — +// still take one position each, whatever order they were added in. +func TestLikeRenderedValuesOrderByContents(t *testing.T) { + metre := &symbols.Symbol{Name: "metre"} + unit := func(scale float64) Unit { + return Unit{ + Text: "m", + Product: semantics.NamedUnitProduct(metre, "m", false), + Term: semantics.UnitTerm{Scale: semantics.UnitScale(scale), Factors: []semantics.UnitFactor{{Unit: metre, Exponent: 1}}}, + } + } + nums := func(ns ...int64) []semantics.Value { + out := make([]semantics.Value, len(ns)) + for i, n := range ns { + out[i] = semantics.Value{Kind: semantics.ValInt, Int: n} + } + return out + } + m, km := unit(1), unit(1000) + pairs := map[string][2]Value{ + "tensor": { + NewTensorQuantityValue([]int64{2}, nums(1, 2), []Unit{m, m}), + NewTensorQuantityValue([]int64{2}, nums(1, 2), []Unit{km, km}), + }, + "vector": { + NewVectorQuantityValue(nums(1, 2), []Unit{m, m}), + NewVectorQuantityValue(nums(1, 2), []Unit{km, km}), + }, + "mref": {NewMeasurementRefValue(m), NewMeasurementRefValue(km)}, + "array": { + NewArrayValue([]int64{1}, []Value{NewMeasurementRefValue(m)}), + NewArrayValue([]int64{1}, []Value{NewMeasurementRefValue(km)}), + }, + } + for name, pair := range pairs { + a, b := pair[0], pair[1] + if FormatTraceValue(a) != FormatTraceValue(b) || valueEqual(a, b) { + t.Fatalf("%s: %s and %s should render alike and differ", name, FormatTraceValue(a), FormatTraceValue(b)) + } + if canonicalCompare(a, b) == 0 || canonicalCompare(a, b) != -canonicalCompare(b, a) { + t.Errorf("%s: compare(a, b) = %d, compare(b, a) = %d, want opposite and non-zero", name, canonicalCompare(a, b), canonicalCompare(b, a)) + } + first, second := setOf([]Value{a, b}).Set(), setOf([]Value{b, a}).Set() + if first.Size() != 2 || !first.Equal(second) { + t.Fatalf("%s: sets differ: %s, %s", name, FormatValue(NewSetValue(first)), FormatValue(NewSetValue(second))) + } + x, y := first.Elements(), second.Elements() + for i := range x { + if !valueEqual(x[i], y[i]) { + t.Errorf("%s: element %d differs between insertion orders", name, i) + } + } + } +} diff --git a/internal/core/runtime/set_order.go b/internal/core/runtime/set_order.go index d6ad00baf..fdd66e832 100644 --- a/internal/core/runtime/set_order.go +++ b/internal/core/runtime/set_order.go @@ -68,15 +68,72 @@ func canonicalCompare(a, b Value) int { if a.Kind != b.Kind { return cmp.Compare(a.Kind, b.Kind) } + return compareContents(a, b) +} + +// compareContents orders two values of one kind that render alike by what +// valueEqual compares: shape and elements, components, unit, or reference key. +func compareContents(a, b Value) int { switch a.Kind { case ValSequence, ValSet: return compareElements(elementsOf(a), elementsOf(b)) case ValArray: - return compareElements(a.Array().Elements, b.Array().Elements) + x, y := a.Array(), b.Array() + return cmp.Or(compareInt64s(x.Dimensions, y.Dimensions), compareElements(x.Elements, y.Elements)) + case ValVector: + return compareElements(constValues(a.Vector().Elements), constValues(b.Vector().Elements)) + case ValVectorQuantity: + x, y := a.VectorQuantity(), b.VectorQuantity() + return cmp.Or( + compareElements(vectorComponents(x), vectorComponents(y)), + strings.Compare(x.Frame.key(), y.Frame.key()), + ) + case ValTensorQuantity: + x, y := a.TensorQuantity(), b.TensorQuantity() + return cmp.Or(compareInt64s(x.Dimensions, y.Dimensions), compareElements(x.components(), y.components())) + case ValQuantity: + // Two quantities of one dimension whose magnitudes will not compare: by + // unit, then by the number written. + x, y := a.Quantity(), b.Quantity() + return cmp.Or( + strings.Compare((&MeasurementRef{Unit: x.Unit}).key(), (&MeasurementRef{Unit: y.Unit}).key()), + compareNumbers(x.Num, y.Num), + ) + case ValMeasurementRef: + return strings.Compare(a.MeasurementRef().key(), b.MeasurementRef().key()) + case ValCoordinateFrame: + return strings.Compare(a.CoordinateFrame().key(), b.CoordinateFrame().key()) + case ValCoordinateTransformation: + return strings.Compare(a.CoordinateTransformation().key(), b.CoordinateTransformation().key()) + case ValExpr: + if a.Expr() == nil || b.Expr() == nil { + return compareBool(a.Expr() != nil, b.Expr() != nil) + } + x, y := a.Expr().Span(), b.Expr().Span() + return cmp.Or(cmp.Compare(x.Offset, y.Offset), cmp.Compare(x.Len, y.Len)) } return 0 } +// vectorComponents is every axis of the vector as a scalar quantity value. +func vectorComponents(vq *VectorQuantity) []Value { + out := make([]Value, len(vq.Num)) + for i := range out { + out[i] = NewQuantityValue(vq.component(i)) + } + return out +} + +// compareInt64s orders two shapes lexicographically, a shorter prefix first. +func compareInt64s(a, b []int64) int { + for i := 0; i < len(a) && i < len(b); i++ { + if c := cmp.Compare(a[i], b[i]); c != 0 { + return c + } + } + return cmp.Compare(len(a), len(b)) +} + // compareElements orders two element lists lexicographically, a shorter prefix first. func compareElements(a, b []Value) int { for i := 0; i < len(a) && i < len(b); i++ { From bfc0dd4e0c496a15bc01f659dcc380ffe690da20 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 05:00:36 +0000 Subject: [PATCH 07/20] fix(clients): judge set members by value across clients, order mixed numbers exactly, refuse overflowing tensor shapes Co-Authored-By: jason.han --- client/opensysml/README.md | 6 +- client/opensysml/equal.go | 60 ++++- client/opensysml/structured_internal_test.go | 73 +++++- .../java/org/openmbee/opensysml/Value.java | 169 ++++++++++++- .../openmbee/opensysml/PublicTypesTest.java | 93 ++++++- clients/node/src/core/values.ts | 56 +++-- clients/node/test/values.test.ts | 52 +++- clients/python/tests/test_set_tensor.py | 6 + clients/rust/README.md | 6 +- clients/rust/opensysml/src/domain.rs | 235 ++++++++++++++++-- docs/reference/wire-contract.md | 7 + internal/core/runtime/set_feature_test.go | 58 +++++ internal/core/runtime/set_order.go | 27 +- 13 files changed, 781 insertions(+), 67 deletions(-) diff --git a/client/opensysml/README.md b/client/opensysml/README.md index 1d8d0a92b..b5d719782 100644 --- a/client/opensysml/README.md +++ b/client/opensysml/README.md @@ -218,8 +218,10 @@ in its own order — so two equal sets arrive alike, and one that lists a member twice reads as an unsupported `Null` naming it; a `Set` you send may list its elements in any order, but listing one twice is refused by the service rather than read as one element. `Set.Contains` tests membership and `Equal` compares -any two values — sets by membership, sequences in order, an `Int` never a -`Real`. A `TensorQuantity` carries its dimensions and one `Quantity` per +any two values as the service does — sets by membership, sequences in order, +numbers by value, so `Int(1)` is `Real(1)` and a `Complex` on the real axis is +its real part, exactly across the whole `Int` range; a `Quantity` is one in its +unit as written. A `TensorQuantity` carries its dimensions and one `Quantity` per component in row-major order, at any rank. ## Stability diff --git a/client/opensysml/equal.go b/client/opensysml/equal.go index ac862c59f..008ada601 100644 --- a/client/opensysml/equal.go +++ b/client/opensysml/equal.go @@ -1,15 +1,22 @@ package opensysml -import "slices" +import ( + "math" + "slices" +) -// Equal reports whether two values are the same value: the same kind holding -// the same contents. An Int is never a Real, a Sequence's order counts, a -// Set's does not, a Null is one whatever its reason, and a nil Value equals -// only another nil. +// Equal reports whether two values are the same value to the model, as the +// service judges a Set's membership: numbers by value, so a whole Real is the +// Int of its value and a Complex on the real axis is its real part, exactly +// across the whole Int range; a Sequence's order counts and a Set's does not; +// a Quantity is one in its unit as written; a Null is one whatever its reason; +// and a nil Value equals only another nil. func Equal(a, b Value) bool { switch x := a.(type) { case nil: return b == nil + case Int, Real, Complex: + return numbersEqual(a, b) case Null: _, ok := b.(Null) return ok @@ -32,7 +39,7 @@ func Equal(a, b Value) bool { return ok && slices.Equal(x.Dimensions, y.Dimensions) && slices.EqualFunc(x.Elements, y.Elements, Equal) case Vector: y, ok := b.(Vector) - return ok && slices.Equal(x, y) + return ok && slices.EqualFunc(x, y, func(m, n Number) bool { return numbersEqual(m, n) }) case VectorQuantity: y, ok := b.(VectorQuantity) return ok && slices.EqualFunc(x, y, quantityEqual) @@ -55,8 +62,47 @@ func (s Set) Contains(value Value) bool { return slices.ContainsFunc(s, func(e Value) bool { return Equal(e, value) }) } +// numbersEqual compares an Int, Real or Complex with any value by numeric +// value; a Complex off the real axis equals only the same Complex. +func numbersEqual(a, b Value) bool { + if z, ok := a.(Complex); ok { + if imag(z) != 0 { + return a == b + } + a = Real(real(z)) + } + if z, ok := b.(Complex); ok { + if imag(z) != 0 { + return false + } + b = Real(real(z)) + } + switch x := a.(type) { + case Int: + switch y := b.(type) { + case Int: + return x == y + case Real: + return realIsInt(float64(y), int64(x)) + } + case Real: + switch y := b.(type) { + case Int: + return realIsInt(float64(x), int64(y)) + case Real: + return x == y + } + } + return false +} + +// realIsInt reports whether r is exactly the integer n, never rounding n. +func realIsInt(r float64, n int64) bool { + return r == math.Trunc(r) && r >= math.MinInt64 && r < -math.MinInt64 && int64(r) == n +} + func quantityEqual(a, b Quantity) bool { - return a.Magnitude == b.Magnitude && a.Unit == b.Unit && unitTermEqual(a.Term, b.Term) + return numbersEqual(a.Magnitude, b.Magnitude) && a.Unit == b.Unit && unitTermEqual(a.Term, b.Term) } func unitTermEqual(a, b *UnitTerm) bool { diff --git a/client/opensysml/structured_internal_test.go b/client/opensysml/structured_internal_test.go index 73ab70093..865542c29 100644 --- a/client/opensysml/structured_internal_test.go +++ b/client/opensysml/structured_internal_test.go @@ -2,6 +2,7 @@ package opensysml import ( "context" + "math" "reflect" "strings" "testing" @@ -179,12 +180,16 @@ func TestMalformedTensorAnswersAreNullsNamingTheFault(t *testing.T) { } } -// A set in an answer listing a member twice — by value, nested collections and -// quantities included — reads as an unsupported null naming the member; one -// whose members only look alike reads as its elements. +// A set in an answer listing a member twice — by value as the model judges it, +// so an Integer and the whole Real of its value are one member, nested +// collections and quantities included — reads as an unsupported null naming +// the member; one whose members only look alike reads as its elements. func TestRepeatedSetMembersAreNullsNamingTheFault(t *testing.T) { pbInt := func(n int64) *pb.Value { return &pb.Value{Kind: &pb.Value_IntValue{IntValue: n}} } pbReal := func(x float64) *pb.Value { return &pb.Value{Kind: &pb.Value_RealValue{RealValue: x}} } + pbComplex := func(re, im float64) *pb.Value { + return &pb.Value{Kind: &pb.Value_Complex{Complex: &pb.Complex{Real: re, Imaginary: im}}} + } pbSeq := func(elements ...*pb.Value) *pb.Value { return &pb.Value{Kind: &pb.Value_Sequence{Sequence: &pb.ValueSequence{Elements: elements}}} } @@ -194,11 +199,18 @@ func TestRepeatedSetMembersAreNullsNamingTheFault(t *testing.T) { pbQty := func(n int64, unit string) *pb.Value { return &pb.Value{Kind: &pb.Value_Quantity{Quantity: &pb.Quantity{Magnitude: &pb.Quantity_IntMagnitude{IntMagnitude: n}, Unit: unit}}} } + pbRealQty := func(x float64, unit string) *pb.Value { + return &pb.Value{Kind: &pb.Value_Quantity{Quantity: &pb.Quantity{Magnitude: &pb.Quantity_RealMagnitude{RealMagnitude: x}, Unit: unit}}} + } for name, value := range map[string]*pb.Value{ "integer twice": pbSet(pbInt(1), pbInt(2), pbInt(1)), + "integer and real": pbSet(pbInt(1), pbReal(1)), + "real and complex": pbSet(pbReal(1.5), pbComplex(1.5, 0)), + "integer and complex": pbSet(pbInt(2), pbComplex(2, 0)), "sequence twice": pbSet(pbSeq(pbInt(1), pbInt(2)), pbSeq(pbInt(1), pbInt(2))), "set twice, reordered": pbSet(pbSet(pbInt(1), pbInt(2)), pbSet(pbInt(2), pbInt(1))), "quantity twice": pbSet(pbQty(1, "m"), pbQty(1, "m")), + "quantity by real": pbSet(pbQty(1, "m"), pbRealQty(1, "m")), "empty set twice": pbSet(pbSet(), pbSet()), } { t.Run(name, func(t *testing.T) { @@ -213,11 +225,14 @@ func TestRepeatedSetMembersAreNullsNamingTheFault(t *testing.T) { value *pb.Value want Set }{ - "integer and real": {pbSet(pbInt(1), pbReal(1)), Set{Int(1), Real(1)}}, - "sequence and set": {pbSet(pbSeq(pbInt(1)), pbSet(pbInt(1))), Set{Sequence{Int(1)}, Set{Int(1)}}}, - "sequences reordered": {pbSet(pbSeq(pbInt(1), pbInt(2)), pbSeq(pbInt(2), pbInt(1))), Set{Sequence{Int(1), Int(2)}, Sequence{Int(2), Int(1)}}}, - "quantities in a unit": {pbSet(pbQty(1, "m"), pbQty(1, "km")), Set{Quantity{Magnitude: Int(1), Unit: "m"}, Quantity{Magnitude: Int(1), Unit: "km"}}}, - "empty and singleton": {pbSet(pbSet(), pbSet(pbSet())), Set{Set{}, Set{Set{}}}}, + "integer and near real": {pbSet(pbInt(1<<53+1), pbReal(1<<53)), Set{Int(1<<53 + 1), Real(1 << 53)}}, + "integer and fraction": {pbSet(pbInt(1), pbReal(1.5)), Set{Int(1), Real(1.5)}}, + "real and imaginary": {pbSet(pbReal(1), pbComplex(1, 1)), Set{Real(1), Complex(complex(1, 1))}}, + "integer and boolean": {pbSet(pbInt(1), &pb.Value{Kind: &pb.Value_BoolValue{BoolValue: true}}), Set{Int(1), Bool(true)}}, + "sequence and set": {pbSet(pbSeq(pbInt(1)), pbSet(pbInt(1))), Set{Sequence{Int(1)}, Set{Int(1)}}}, + "sequences reordered": {pbSet(pbSeq(pbInt(1), pbInt(2)), pbSeq(pbInt(2), pbInt(1))), Set{Sequence{Int(1), Int(2)}, Sequence{Int(2), Int(1)}}}, + "quantities in a unit": {pbSet(pbQty(1, "m"), pbQty(1, "km")), Set{Quantity{Magnitude: Int(1), Unit: "m"}, Quantity{Magnitude: Int(1), Unit: "km"}}}, + "empty and singleton": {pbSet(pbSet(), pbSet(pbSet())), Set{Set{}, Set{Set{}}}}, } { t.Run(name, func(t *testing.T) { if got := valueFromProto(tc.value); !reflect.DeepEqual(got, tc.want) { @@ -228,7 +243,7 @@ func TestRepeatedSetMembersAreNullsNamingTheFault(t *testing.T) { if !Equal(Set{Int(1), Sequence{Int(2), Int(3)}}, Set{Sequence{Int(2), Int(3)}, Int(1)}) { t.Error("sets holding the same members in another order are not Equal") } - if Equal(Set{Int(1)}, Sequence{Int(1)}) || Equal(Int(1), Real(1)) || Equal(nil, Null("")) { + if Equal(Set{Int(1)}, Sequence{Int(1)}) || Equal(Int(1), Bool(true)) || Equal(nil, Null("")) { t.Error("values of different kinds are Equal") } if !Equal(Null("a"), Null("b")) || !Equal(Unset{}, Unset{}) || Equal(Unset{}, Null("")) { @@ -236,6 +251,46 @@ func TestRepeatedSetMembersAreNullsNamingTheFault(t *testing.T) { } } +// Equal judges numbers as the service does: by value across Int, Real and a +// Complex on the real axis, exactly — an Int beyond a double's precision is +// not the Real it would round to — and inside quantities, vectors and sets. +func TestEqualJudgesNumbersByValue(t *testing.T) { + for name, tc := range map[string]struct { + a, b Value + want bool + }{ + "int and whole real": {Int(1), Real(1), true}, + "int and fraction": {Int(1), Real(1.5), false}, + "int and real beyond 2^53": {Int(1<<53 + 1), Real(1 << 53), false}, + "int and real at 2^53": {Int(1 << 53), Real(1 << 53), true}, + "max int and 2^63": {Int(math.MaxInt64), Real(-math.MinInt64), false}, + "min int and -2^63": {Int(math.MinInt64), Real(math.MinInt64), true}, + "int and infinity": {Int(0), Real(math.Inf(1)), false}, + "zero and negative zero": {Int(0), Real(math.Copysign(0, -1)), true}, + "real and real-axis complex": {Real(2.5), Complex(complex(2.5, 0)), true}, + "int and real-axis complex": {Int(2), Complex(complex(2, 0)), true}, + "int and imaginary": {Int(2), Complex(complex(2, 1)), false}, + "complex twice": {Complex(complex(2, 1)), Complex(complex(2, 1)), true}, + "int and bool": {Int(1), Bool(true), false}, + "int and string": {Int(1), String("1"), false}, + "quantity by int and real": {Quantity{Magnitude: Int(1), Unit: "m"}, Quantity{Magnitude: Real(1), Unit: "m"}, true}, + "quantity in another unit": {Quantity{Magnitude: Int(1), Unit: "m"}, Quantity{Magnitude: Int(1), Unit: "km"}, false}, + "vector by int and real": {Vector{Int(1), Real(2)}, Vector{Real(1), Int(2)}, true}, + "vector and sequence": {Vector{Int(1)}, Sequence{Int(1)}, false}, + "set by int and real": {Set{Int(1), Real(2.5)}, Set{Real(2.5), Real(1)}, true}, + "set and near real": {Set{Int(1<<53 + 1)}, Set{Real(1 << 53)}, false}, + } { + t.Run(name, func(t *testing.T) { + if got := Equal(tc.a, tc.b); got != tc.want { + t.Errorf("Equal(%#v, %#v) = %v, want %v", tc.a, tc.b, got, tc.want) + } + if got := Equal(tc.b, tc.a); got != tc.want { + t.Errorf("Equal(%#v, %#v) = %v, want %v", tc.b, tc.a, got, tc.want) + } + }) + } +} + // A malformed measurement reference in an answer reads as an unsupported null // naming the fault; a well-formed one reads as itself, reduction and identity // intact. diff --git a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/Value.java b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/Value.java index 5473cc6a8..4118176c0 100644 --- a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/Value.java +++ b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/Value.java @@ -1,9 +1,9 @@ package org.openmbee.opensysml; +import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.Set; /** * A value the service evaluated: an immutable variant of {@code sysml.Value}. @@ -356,6 +356,9 @@ public Optional unit() { * Only a service advertising the {@code set_values} capability reports one as itself rather * than as an unsupported {@link NullValue}. * + *

Membership, and so equality and the refusal of a member listed twice, are judged by {@link + * Value#sameValue}, as the service judges them: {@code 1} and {@code 1.0} are one member. + * * @param elements the members, each once, in the order the service sent them */ record SetValue(List elements) implements Value { @@ -368,12 +371,21 @@ record SetValue(List elements) implements Value { public SetValue { elements = List.copyOf(elements); for (int i = 0; i < elements.size(); i++) { - if (elements.subList(0, i).contains(elements.get(i))) { + if (holds(elements.subList(0, i), elements.get(i))) { throw new IllegalArgumentException("set lists a member twice: " + elements.get(i)); } } } + private static boolean holds(List members, Value value) { + for (Value member : members) { + if (member.sameValue(value)) { + return true; + } + } + return false; + } + /** * Number of members. * @@ -399,20 +411,30 @@ public boolean isEmpty() { * @return {@code true} when the set holds it */ public boolean contains(Value value) { - return elements.contains(value); + return holds(elements, value); } /** Order-insensitive: the same members in any order are the same set. */ @Override public boolean equals(Object other) { - return other instanceof SetValue that - && elements.size() == that.elements.size() - && elements.containsAll(that.elements); + if (!(other instanceof SetValue that) || elements.size() != that.elements.size()) { + return false; + } + for (Value element : elements) { + if (!that.contains(element)) { + return false; + } + } + return true; } @Override public int hashCode() { - return Set.copyOf(elements).hashCode(); + int hash = 0; + for (Value element : elements) { + hash += valueHash(element); + } + return hash; } } @@ -433,8 +455,8 @@ record TensorQuantityValue(List dimensions, List components) imp * * @param dimensions the extent of each dimension, never {@code null} * @param components the components, never {@code null} - * @throws IllegalArgumentException if a dimension is not positive, or the components do not - * fill the dimensions exactly + * @throws IllegalArgumentException if a dimension is not positive, the dimensions overflow a + * {@code long}, or the components do not fill the dimensions exactly */ public TensorQuantityValue { dimensions = List.copyOf(dimensions); @@ -444,7 +466,11 @@ record TensorQuantityValue(List dimensions, List components) imp if (extent <= 0) { throw new IllegalArgumentException("tensor dimension is not positive: " + extent); } - size = Math.multiplyExact(size, extent); + try { + size = Math.multiplyExact(size, extent); + } catch (ArithmeticException overflow) { + throw new IllegalArgumentException("tensor dimensions overflow: " + dimensions, overflow); + } } if (size != components.size()) { throw new IllegalArgumentException( @@ -503,6 +529,129 @@ public Optional unit() { } } + /** + * Whether this is the same value as another to the model, as the service judges a set's + * membership: numbers by value, so a whole {@link RealValue} is the {@link IntegerValue} of its + * value and a {@link ComplexValue} on the real axis is its real part, exactly across the whole + * {@code long} range; a sequence's order counts and a set's does not; a quantity is one in its + * unit as written. Every other arm compares as {@link Object#equals} does, which stays + * structural: {@code new IntegerValue(1).equals(new RealValue(1.0))} is {@code false}. + * + * @param other the value to compare with + * @return {@code true} when the model would not tell the two apart + */ + default boolean sameValue(Value other) { + Objects.requireNonNull(other, "other"); + if (this instanceof IntegerValue || this instanceof RealValue || this instanceof ComplexValue) { + return numbersEqual(this, other); + } + if (this instanceof Sequence a && other instanceof Sequence b) { + return sameValues(a.elements(), b.elements()); + } + if (this instanceof QuantityValue a && other instanceof QuantityValue b) { + return quantitiesEqual(a.quantity(), b.quantity()); + } + if (this instanceof ArrayValue a && other instanceof ArrayValue b) { + return a.dimensions().equals(b.dimensions()) && sameValues(a.elements(), b.elements()); + } + if (this instanceof VectorValue a && other instanceof VectorValue b) { + return sameValues(a.components(), b.components()); + } + if (this instanceof VectorQuantityValue a && other instanceof VectorQuantityValue b) { + return sameQuantities(a.components(), b.components()); + } + if (this instanceof TensorQuantityValue a && other instanceof TensorQuantityValue b) { + return a.dimensions().equals(b.dimensions()) + && sameQuantities(a.components(), b.components()); + } + return equals(other); + } + + private static boolean numbersEqual(Value a, Value b) { + Number x = onRealAxis(a); + Number y = onRealAxis(b); + return x != null && y != null ? magnitudesEqual(x, y) : a.equals(b); + } + + /** A number's magnitude as a {@link Long} or {@link Double}; {@code null} off the real axis. */ + private static Number onRealAxis(Value value) { + if (value instanceof IntegerValue integer) { + return integer.value(); + } + if (value instanceof RealValue real) { + return real.value(); + } + if (value instanceof ComplexValue complex && complex.imaginary() == 0.0) { + return complex.real(); + } + return null; + } + + private static boolean magnitudesEqual(Number a, Number b) { + if (a instanceof Long x) { + return b instanceof Long y ? x.longValue() == y : realIsLong(b.doubleValue(), x); + } + return b instanceof Long y ? realIsLong(a.doubleValue(), y) : a.doubleValue() == b.doubleValue(); + } + + // Whether r is exactly the integer n, never rounding n. + private static boolean realIsLong(double r, long n) { + return r == Math.rint(r) && r >= -0x1p63 && r < 0x1p63 && (long) r == n; + } + + private static boolean sameValues(List a, List b) { + if (a.size() != b.size()) { + return false; + } + Iterator others = b.iterator(); + for (Value value : a) { + if (!value.sameValue(others.next())) { + return false; + } + } + return true; + } + + private static boolean quantitiesEqual(Quantity a, Quantity b) { + return magnitudesEqual(a.magnitude(), b.magnitude()) + && a.unit().equals(b.unit()) + && a.reduction().equals(b.reduction()); + } + + private static boolean sameQuantities(List a, List b) { + if (a.size() != b.size()) { + return false; + } + Iterator others = b.iterator(); + for (Quantity quantity : a) { + if (!quantitiesEqual(quantity, others.next())) { + return false; + } + } + return true; + } + + /** A hash consistent with {@link #sameValue}: values the model equates hash alike. */ + private static int valueHash(Value value) { + Number magnitude = onRealAxis(value); + if (magnitude != null) { + return Double.hashCode(magnitude.doubleValue() + 0.0); + } + if (value instanceof QuantityValue quantity) { + return Double.hashCode(quantity.quantity().magnitude().doubleValue() + 0.0) + ^ quantity.quantity().unit().hashCode(); + } + if (value instanceof SetValue + || value instanceof Sequence + || value instanceof ArrayValue + || value instanceof VectorValue + || value instanceof VectorQuantityValue + || value instanceof TensorQuantityValue) { + return value.getClass().hashCode(); + } + return value.hashCode(); + } + /** * This value as a {@code double}, for the numeric arms. * diff --git a/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/PublicTypesTest.java b/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/PublicTypesTest.java index 339a04621..18d024975 100644 --- a/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/PublicTypesTest.java +++ b/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/PublicTypesTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -108,6 +109,90 @@ void aSetIsItsMembersInAnyOrderAndNoneTwice() { assertThrows(IllegalArgumentException.class, () -> new Value.SetValue(twice)); } + @Test + void aSetJudgesItsMembersAsTheModelDoes() { + Value one = new Value.IntegerValue(1); + Value oneReal = new Value.RealValue(1.0); + Value oneComplex = new Value.ComplexValue(1.0, 0.0); + Value twoPointFive = new Value.RealValue(2.5); + Value.SetValue set = new Value.SetValue(List.of(one, twoPointFive)); + + // An Integer and the whole Real of its value are one member, as is a Complex on the real axis. + assertTrue(set.contains(oneReal)); + assertTrue(set.contains(oneComplex)); + assertTrue(set.contains(new Value.ComplexValue(2.5, 0.0))); + assertFalse(set.contains(new Value.RealValue(1.5))); + assertFalse(set.contains(new Value.BooleanValue(true))); + Value.SetValue byReals = new Value.SetValue(List.of(twoPointFive, oneReal)); + assertEquals(byReals, set); + assertEquals(byReals.hashCode(), set.hashCode()); + assertNotEquals(one, oneReal); + for (List twice : + List.>of( + List.of(one, oneReal), + List.of(oneReal, oneComplex), + List.of(one, oneComplex), + List.of(new Value.IntegerValue(0), new Value.RealValue(-0.0)), + List.of(new Value.QuantityValue(metres(1L)), new Value.QuantityValue(metres(1.0))))) { + assertThrows(IllegalArgumentException.class, () -> new Value.SetValue(twice), twice::toString); + } + + // Members that only look alike stay apart: nearby numbers beyond 2^53, a Complex off the + // axis, a Boolean, another unit, another order of a sequence, another shape of an array. + Value big = new Value.IntegerValue((1L << 53) + 1); + Value bigReal = new Value.RealValue(0x1p53); + for (List apart : + List.>of( + List.of(big, bigReal), + List.of(new Value.IntegerValue(Long.MAX_VALUE), new Value.RealValue(0x1p63)), + List.of(one, new Value.RealValue(1.5)), + List.of(oneReal, new Value.ComplexValue(1.0, 1.0)), + List.of(one, new Value.BooleanValue(true)), + List.of(one, new Value.StringValue("1")), + List.of( + new Value.QuantityValue(metres(1L)), + new Value.QuantityValue(new Quantity(1L, Optional.of("km"), Optional.empty()))), + List.of( + new Value.Sequence(List.of(one, twoPointFive)), + new Value.Sequence(List.of(twoPointFive, one))), + List.of( + new Value.ArrayValue(List.of(2L, 1L), List.of(one, one)), + new Value.ArrayValue(List.of(1L, 2L), List.of(one, one))), + List.of(new Value.Sequence(List.of(one)), new Value.SetValue(List.of(one))))) { + assertEquals(2, new Value.SetValue(apart).size(), apart::toString); + } + assertNotEquals(new Value.SetValue(List.of(big)), new Value.SetValue(List.of(bigReal))); + assertEquals( + new Value.SetValue(List.of(new Value.IntegerValue(Long.MIN_VALUE))), + new Value.SetValue(List.of(new Value.RealValue(-0x1p63)))); + + // Numbers nested in sequences, vectors, arrays and quantities are judged the same way. + assertTrue( + new Value.Sequence(List.of(one, twoPointFive)) + .sameValue(new Value.Sequence(List.of(oneReal, twoPointFive)))); + assertTrue( + new Value.VectorValue(List.of(one, new Value.RealValue(2.0))) + .sameValue(new Value.VectorValue(List.of(oneReal, new Value.IntegerValue(2))))); + assertTrue( + new Value.ArrayValue(List.of(1L), List.of(one)) + .sameValue(new Value.ArrayValue(List.of(1L), List.of(oneComplex)))); + assertTrue( + new Value.VectorQuantityValue(List.of(metres(1L))) + .sameValue(new Value.VectorQuantityValue(List.of(metres(1.0))))); + assertTrue( + new Value.TensorQuantityValue(List.of(1L, 1L), List.of(metres(1L))) + .sameValue(new Value.TensorQuantityValue(List.of(1L, 1L), List.of(metres(1.0))))); + assertFalse( + new Value.TensorQuantityValue(List.of(1L, 1L), List.of(metres(1L))) + .sameValue(new Value.TensorQuantityValue(List.of(1L), List.of(metres(1.0))))); + assertFalse(new Value.NullValue().sameValue(new Value.UnsetValue())); + assertTrue(new Value.NullValue().sameValue(new Value.NullValue())); + } + + private static Quantity metres(Number magnitude) { + return new Quantity(magnitude, Optional.of("m"), Optional.empty()); + } + @Test void aTensorQuantityIsShapedAndIndexedInRowMajorOrder() { List pascals = new ArrayList<>(); @@ -149,9 +234,11 @@ void aTensorQuantityIsShapedAndIndexedInRowMajorOrder() { assertThrows( IllegalArgumentException.class, () -> new Value.TensorQuantityValue(List.of(-2L, -4L), pascals)); - assertThrows( - ArithmeticException.class, - () -> new Value.TensorQuantityValue(List.of(Long.MAX_VALUE, 2L), pascals)); + IllegalArgumentException overflow = + assertThrows( + IllegalArgumentException.class, + () -> new Value.TensorQuantityValue(List.of(Long.MAX_VALUE, 2L), pascals)); + assertInstanceOf(ArithmeticException.class, overflow.getCause()); List shape = new ArrayList<>(List.of(8L)); Value.TensorQuantityValue copied = new Value.TensorQuantityValue(shape, pascals); diff --git a/clients/node/src/core/values.ts b/clients/node/src/core/values.ts index da29bc5da..9be5dddc8 100644 --- a/clients/node/src/core/values.ts +++ b/clients/node/src/core/values.ts @@ -571,22 +571,18 @@ function decodeSet(set: ValueSet): SysMLValue[] { } /** - * Whether two values are the same value: the same kind holding the same - * contents. An `int` is never a `real`, a `sequence`'s order counts and a - * `set`'s does not; a `null` is the same whatever its reason. + * Whether two values are the same value to the model, as the service judges a + * set's membership: numbers by value, so a whole `real` is the `int` of its + * value and a `complex` on the real axis is its real part, exactly across the + * whole `int` range; a `sequence`'s order counts and a `set`'s does not; a + * `null` is the same whatever its reason. */ export function valuesEqual(a: SysMLValue, b: SysMLValue): boolean { switch (a.kind) { case "int": - return b.kind === "int" && a.value === b.value; case "real": - return b.kind === "real" && a.value === b.value; case "complex": - return ( - b.kind === "complex" && - a.value.real === b.value.real && - a.value.imaginary === b.value.imaginary - ); + return numbersEqual(a, b); case "boolean": return b.kind === "boolean" && a.value === b.value; case "string": @@ -654,11 +650,40 @@ function dimensionsEqual(a: bigint[], b: bigint[]): boolean { return a.length === b.length && a.every((d, i) => d === b[i]); } +type NumberValue = Magnitude | { kind: "complex"; value: ComplexValue }; + +function numbersEqual(a: NumberValue, b: SysMLValue): boolean { + if (a.kind === "complex") { + if (a.value.imaginary !== 0) { + return ( + b.kind === "complex" && + a.value.real === b.value.real && + a.value.imaginary === b.value.imaginary + ); + } + a = { kind: "real", value: a.value.real }; + } + if (b.kind === "complex") { + if (b.value.imaginary !== 0) { + return false; + } + b = { kind: "real", value: b.value.real }; + } + if (a.kind === "int") { + if (b.kind === "int") return a.value === b.value; + return b.kind === "real" && realIsInt(b.value, a.value); + } + if (b.kind === "int") return realIsInt(a.value, b.value); + return b.kind === "real" && a.value === b.value; +} + +// Whether r is exactly the integer n, never rounding n. +function realIsInt(r: number, n: bigint): boolean { + return Number.isInteger(r) && r >= -(2 ** 63) && r < 2 ** 63 && BigInt(r) === n; +} + function magnitudesEqual(a: Magnitude[], b: Magnitude[]): boolean { - return ( - a.length === b.length && - a.every((m, i) => m.kind === b[i]?.kind && m.value === b[i]?.value) - ); + return a.length === b.length && a.every((m, i) => numbersEqual(m, b[i])); } function componentsEqual(a: QuantityValue[], b: QuantityValue[]): boolean { @@ -670,8 +695,7 @@ function componentsEqual(a: QuantityValue[], b: QuantityValue[]): boolean { function quantitiesEqual(a: QuantityValue, b: QuantityValue): boolean { return ( - a.magnitude.kind === b.magnitude.kind && - a.magnitude.value === b.magnitude.value && + numbersEqual(a.magnitude, b.magnitude) && a.unit === b.unit && unitTermsEqual(a.unitTerm, b.unitTerm) ); diff --git a/clients/node/test/values.test.ts b/clients/node/test/values.test.ts index 8ef943262..52d5c962b 100644 --- a/clients/node/test/values.test.ts +++ b/clients/node/test/values.test.ts @@ -319,10 +319,18 @@ const seqOf = (...elements: ReturnType[]) => create(ValueSchema, { kind: { case: "sequence", value: create(ValueSequenceSchema, { elements }) } }); const bool = (value: boolean) => create(ValueSchema, { kind: { case: "boolValue", value } }); const quantity = (q: ReturnType) => create(ValueSchema, { kind: { case: "quantity", value: q } }); +const complex = (real: number, imaginary: number) => + create(ValueSchema, { kind: { case: "complex", value: create(ComplexSchema, { real, imaginary }) } }); +const intMetres = (value: bigint) => + create(QuantitySchema, { ...metres(Number(value)), magnitude: { case: "intMagnitude", value } }); test("a set that lists a member twice is malformed, judged by value", () => { const twice = [ setOf(int(1n), int(2n), int(1n)), + setOf(int(1n), real(1)), + setOf(real(1.5), complex(1.5, 0)), + setOf(int(2n), complex(2, 0)), + setOf(quantity(metres(1)), quantity(intMetres(1n))), setOf(seqOf(int(1n), int(2n)), seqOf(int(1n), int(2n))), setOf(setOf(int(1n), int(2n)), setOf(int(2n), int(1n))), setOf(setOf(), setOf()), @@ -337,10 +345,13 @@ test("a set that lists a member twice is malformed, judged by value", () => { }); } - // Members that merely look alike are distinct: an int is never a real, a - // sequence's order counts, a sequence is never a set of the same elements. + // Members that merely look alike are distinct: an int is not a real of another + // value, even one it would round to; a sequence's order counts; a sequence is + // never a set of the same elements. const alike = [ - setOf(int(1n), real(1)), + setOf(int(1n), real(1.5)), + setOf(int(2n ** 53n + 1n), real(2 ** 53)), + setOf(real(1), complex(1, 1)), setOf(bool(true), int(1n)), setOf(seqOf(int(1n), int(2n)), seqOf(int(2n), int(1n))), setOf(seqOf(int(1n)), setOf(int(1n))), @@ -364,6 +375,41 @@ test("a set that lists a member twice is malformed, judged by value", () => { assert.ok(!valuesEqual({ kind: "unset" }, { kind: "absent" })); }); +test("valuesEqual judges numbers by value, as the service does", () => { + const i = (value: bigint): SysMLValue => ({ kind: "int", value }); + const r = (value: number): SysMLValue => ({ kind: "real", value }); + const c = (real: number, imaginary: number): SysMLValue => ({ kind: "complex", value: { real, imaginary } }); + const cases: [SysMLValue, SysMLValue, boolean][] = [ + [i(1n), r(1), true], + [i(1n), r(1.5), false], + [i(2n ** 53n + 1n), r(2 ** 53), false], + [i(2n ** 53n), r(2 ** 53), true], + [i(2n ** 63n - 1n), r(2 ** 63), false], + [i(-(2n ** 63n)), r(-(2 ** 63)), true], + [i(0n), r(Infinity), false], + [i(0n), r(-0), true], + [r(2.5), c(2.5, 0), true], + [i(2n), c(2, 0), true], + [i(2n), c(2, 1), false], + [c(2, 1), c(2, 1), true], + [i(1n), { kind: "boolean", value: true }, false], + [i(1n), { kind: "string", value: "1" }, false], + [decodeValue(quantity(intMetres(1n))), decodeValue(quantity(metres(1))), true], + [decodeValue(quantity(intMetres(1n))), decodeValue(quantity(metres(2))), false], + [ + { kind: "vector", components: [{ kind: "int", value: 1n }, { kind: "real", value: 2 }] }, + { kind: "vector", components: [{ kind: "real", value: 1 }, { kind: "int", value: 2n }] }, + true, + ], + [{ kind: "set", elements: [i(1n), r(2.5)] }, { kind: "set", elements: [r(2.5), r(1)] }, true], + [{ kind: "set", elements: [i(2n ** 53n + 1n)] }, { kind: "set", elements: [r(2 ** 53)] }, false], + ]; + for (const [a, b, want] of cases) { + assert.equal(valuesEqual(a, b), want, `${formatValue(a)} vs ${formatValue(b)}`); + assert.equal(valuesEqual(b, a), want, `${formatValue(b)} vs ${formatValue(a)}`); + } +}); + test("a tensor quantity keeps its rank, its shape and its row-major components", () => { const cube = decodeValue(tensor([2n, 2n, 2n], ...[1, 2, 3, 4, 5, 6, 7, 8].map(metres))); assert.equal(cube.kind, "tensorQuantity"); diff --git a/clients/python/tests/test_set_tensor.py b/clients/python/tests/test_set_tensor.py index 9ff3c0f5c..d6ffad99c 100644 --- a/clients/python/tests/test_set_tensor.py +++ b/clients/python/tests/test_set_tensor.py @@ -149,6 +149,9 @@ def pb_seq(*elements): @pytest.mark.parametrize("elements", [ (pb_int(1), pb_int(2), pb_int(1)), (pb_int(1), sysml_pb2.Value(real_value=1.0)), + (pb_int(2), sysml_pb2.Value(complex=sysml_pb2.Complex(real=2.0, imaginary=0.0))), + (sysml_pb2.Value(real_value=2.5), sysml_pb2.Value(complex=sysml_pb2.Complex(real=2.5))), + (pb_int(0), sysml_pb2.Value(real_value=-0.0)), (pb_seq(pb_int(1), pb_int(2)), pb_seq(pb_int(1), pb_int(2))), (pb_set(pb_int(1), pb_int(2)), pb_set(pb_int(2), pb_int(1))), (pb_set(), pb_set()), @@ -162,6 +165,9 @@ def test_a_set_listing_a_member_twice_is_malformed(elements): @pytest.mark.parametrize("elements, expected", [ ((sysml_pb2.Value(bool_value=True), pb_int(1)), SetValue((True, 1))), + ((pb_int(1), sysml_pb2.Value(real_value=1.5)), SetValue((1, 1.5))), + ((pb_int(2 ** 53 + 1), sysml_pb2.Value(real_value=float(2 ** 53))), SetValue((2 ** 53 + 1, float(2 ** 53)))), + ((pb_int(1), sysml_pb2.Value(complex=sysml_pb2.Complex(real=1.0, imaginary=1.0))), SetValue((1, 1 + 1j))), ((pb_seq(pb_int(1), pb_int(2)), pb_seq(pb_int(2), pb_int(1))), SetValue(([1, 2], [2, 1]))), ((pb_seq(pb_int(1)), pb_set(pb_int(1))), SetValue(([1], SetValue((1,))))), ((pb_seq(sysml_pb2.Value(bool_value=True)), pb_seq(pb_int(1))), SetValue(([True], [1]))), diff --git a/clients/rust/README.md b/clients/rust/README.md index 2473f4ffd..5eeb8f8f6 100644 --- a/clients/rust/README.md +++ b/clients/rust/README.md @@ -153,7 +153,11 @@ advertised operation. A `Value::Set` is a `Collections::Set`'s elements: each member once, sent in the service's canonical order (numbers ascending, then strings, and so on), and -equal to another set holding the same members in any order. A +equal to another set holding the same members in any order. Membership is +judged by `Value::same_value`, as the service judges it: `Integer(1)` and +`Real(1.0)` are one member, `Real(1.5)` and a `Complex` of `1.5 + 0.0i` are one +member, exactly across the whole `i64` range, while `==` on `Value` stays +structural. A `Value::TensorQuantity` is a `Quantities::TensorQuantityValue` of any rank: its `dimensions()` and its `components()` flattened row-major, each a `Quantity` with its own unit; `get(&[i, j, k])` takes one coordinate per diff --git a/clients/rust/opensysml/src/domain.rs b/clients/rust/opensysml/src/domain.rs index 78d6d5550..1c9ac1d2c 100644 --- a/clients/rust/opensysml/src/domain.rs +++ b/clients/rust/opensysml/src/domain.rs @@ -353,19 +353,20 @@ impl VectorQuantity { /// /// The service sends the members in its canonical order (numbers ascending, /// then strings, and so on), each exactly once; two sets are equal when they -/// hold the same members whatever the order. A service advertising -/// `set_values` sends one as itself; an older one sends an unsupported -/// [`Value::Null`] in its place. +/// hold the same members whatever the order, judged by +/// [`Value::same_value`]. A service advertising `set_values` sends one as +/// itself; an older one sends an unsupported [`Value::Null`] in its place. #[derive(Clone, Debug)] pub struct Set { elements: Vec, } impl Set { - /// Builds a set, refusing one that lists a member twice. + /// Builds a set, refusing one that lists a member twice by + /// [`Value::same_value`]. pub fn new(elements: Vec) -> Result { for (i, element) in elements.iter().enumerate() { - if elements[..i].contains(element) { + if elements[..i].iter().any(|e| e.same_value(element)) { return Err(Error::Decode(format!( "set lists a member twice: {element:?}" ))); @@ -389,9 +390,9 @@ impl Set { self.elements.is_empty() } - /// Whether `value` is a member. + /// Whether `value` is a member, by [`Value::same_value`]. pub fn contains(&self, value: &Value) -> bool { - self.elements.contains(value) + self.elements.iter().any(|e| e.same_value(value)) } } @@ -573,6 +574,82 @@ pub enum Value { Infinity, } +impl Value { + /// Whether two values are the same value to the model, as the service + /// judges a set's membership: numbers by value, so a whole [`Value::Real`] + /// is the [`Value::Integer`] of its value and a [`Value::Complex`] on the + /// real axis is its real part, exactly across the whole `i64` range; a + /// sequence's order counts and a set's does not; a quantity is one in its + /// unit as written. Every other arm compares as `==` does. + pub fn same_value(&self, other: &Value) -> bool { + match (self, other) { + (Value::Integer(_) | Value::Real(_) | Value::Complex(_), _) => { + numbers_equal(self, other) + } + (Value::Sequence(a), Value::Sequence(b)) => sequences_equal(a, b), + (Value::Quantity(a), Value::Quantity(b)) => quantities_equal(a, b), + (Value::Array(a), Value::Array(b)) => { + a.dimensions == b.dimensions && sequences_equal(&a.elements, &b.elements) + } + (Value::Vector(a), Value::Vector(b)) => { + a.components.len() == b.components.len() + && a.components + .iter() + .zip(&b.components) + .all(|(m, n)| magnitudes_equal(*m, *n)) + } + (Value::VectorQuantity(a), Value::VectorQuantity(b)) => { + components_equal(&a.components, &b.components) + } + (Value::TensorQuantity(a), Value::TensorQuantity(b)) => { + a.dimensions == b.dimensions && components_equal(&a.components, &b.components) + } + _ => self == other, + } + } +} + +fn numbers_equal(a: &Value, b: &Value) -> bool { + let on_axis = |v: &Value| match *v { + Value::Complex(z) if z.imaginary == 0.0 => Some(Magnitude::Real(z.real)), + Value::Integer(n) => Some(Magnitude::Integer(n)), + Value::Real(r) => Some(Magnitude::Real(r)), + _ => None, + }; + match (on_axis(a), on_axis(b)) { + (Some(x), Some(y)) => magnitudes_equal(x, y), + _ => a == b, + } +} + +fn magnitudes_equal(a: Magnitude, b: Magnitude) -> bool { + match (a, b) { + (Magnitude::Integer(x), Magnitude::Integer(y)) => x == y, + (Magnitude::Real(x), Magnitude::Real(y)) => x == y, + (Magnitude::Integer(n), Magnitude::Real(r)) + | (Magnitude::Real(r), Magnitude::Integer(n)) => real_is_int(r, n), + } +} + +// Whether `r` is exactly the integer `n`, never rounding `n`. +fn real_is_int(r: f64, n: i64) -> bool { + r.fract() == 0.0 + && (-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0).contains(&r) + && r as i64 == n +} + +fn sequences_equal(a: &[Value], b: &[Value]) -> bool { + a.len() == b.len() && a.iter().zip(b).all(|(x, y)| x.same_value(y)) +} + +fn quantities_equal(a: &Quantity, b: &Quantity) -> bool { + magnitudes_equal(a.magnitude, b.magnitude) && a.unit == b.unit && a.unit_term == b.unit_term +} + +fn components_equal(a: &[Quantity], b: &[Quantity]) -> bool { + a.len() == b.len() && a.iter().zip(b).all(|(x, y)| quantities_equal(x, y)) +} + pub(crate) fn value_from_wire(value: wire::Value) -> Result { let Some(kind) = value.kind else { return Err(Error::Decode("Value has no kind".to_owned())); @@ -1435,7 +1512,8 @@ mod tests { ); assert!(members.contains(&Value::Integer(2))); assert!(!members.contains(&Value::Integer(4))); - assert!(!members.contains(&Value::Real(2.0))); + assert!(members.contains(&Value::Real(2.0))); + assert!(!members.contains(&Value::Real(2.5))); // The same members in another order are the same set; a sequence is not. let reordered = Set::new(vec![ @@ -1478,18 +1556,147 @@ mod tests { }; assert!(matches!(holding[0], Value::Set(_))); - // A member listed twice is not a set. - let twice = value_from_wire(set(vec![int(1), int(1)])); - assert!( - matches!(&twice, Err(Error::Decode(message)) if message.contains("twice")), - "{twice:?}" - ); + // A member listed twice is not a set, judged as the model does: an + // Integer and the whole Real of its value are one member. + for twice in [ + set(vec![int(1), int(1)]), + set(vec![int(1), real(1.0)]), + set(vec![real(1.5), complex(1.5, 0.0)]), + set(vec![int(2), complex(2.0, 0.0)]), + ] { + let twice = value_from_wire(twice); + assert!( + matches!(&twice, Err(Error::Decode(message)) if message.contains("twice")), + "{twice:?}" + ); + } + for alike in [ + set(vec![int(1), real(1.5)]), + set(vec![int((1 << 53) + 1), real(9_007_199_254_740_992.0)]), + set(vec![real(1.0), complex(1.0, 1.0)]), + set(vec![int(1), bool(true)]), + ] { + let Ok(Value::Set(two)) = value_from_wire(alike) else { + panic!("members that only look alike should decode"); + }; + assert_eq!(two.len(), 2); + } assert!(matches!( value_from_wire(set(vec![wire::Value { kind: None }])), Err(Error::Decode(message)) if message.contains("no kind") )); } + fn complex(real: f64, imaginary: f64) -> wire::Value { + wire::Value { + kind: Some(wire::value::Kind::Complex(wire::Complex { + real, + imaginary, + })), + } + } + + fn bool(value: bool) -> wire::Value { + wire::Value { + kind: Some(wire::value::Kind::BoolValue(value)), + } + } + + /// `same_value` judges numbers as the service does: by value across + /// Integer, Real and a Complex on the real axis, exactly, inside + /// quantities, vectors and sets; `==` stays structural. + #[test] + fn same_value_judges_numbers_by_value() { + let z = |real, imaginary| Value::Complex(Complex { real, imaginary }); + let metre = |magnitude| { + Value::Quantity(Quantity { + magnitude, + unit: "m".to_owned(), + unit_term: None, + }) + }; + let cases = [ + (Value::Integer(1), Value::Real(1.0), true), + (Value::Integer(1), Value::Real(1.5), false), + ( + Value::Integer((1 << 53) + 1), + Value::Real(9_007_199_254_740_992.0), + false, + ), + ( + Value::Integer(1 << 53), + Value::Real(9_007_199_254_740_992.0), + true, + ), + ( + Value::Integer(i64::MAX), + Value::Real(9_223_372_036_854_775_808.0), + false, + ), + ( + Value::Integer(i64::MIN), + Value::Real(-9_223_372_036_854_775_808.0), + true, + ), + (Value::Integer(0), Value::Real(f64::INFINITY), false), + (Value::Integer(0), Value::Real(-0.0), true), + (Value::Real(2.5), z(2.5, 0.0), true), + (Value::Integer(2), z(2.0, 0.0), true), + (Value::Integer(2), z(2.0, 1.0), false), + (z(2.0, 1.0), z(2.0, 1.0), true), + (Value::Integer(1), Value::Boolean(true), false), + (Value::Integer(1), Value::Text("1".to_owned()), false), + ( + metre(Magnitude::Integer(1)), + metre(Magnitude::Real(1.0)), + true, + ), + ( + metre(Magnitude::Integer(1)), + metre(Magnitude::Real(2.0)), + false, + ), + ( + Value::Vector(Vector { + components: vec![Magnitude::Integer(1), Magnitude::Real(2.0)], + }), + Value::Vector(Vector { + components: vec![Magnitude::Real(1.0), Magnitude::Integer(2)], + }), + true, + ), + ( + Value::Sequence(vec![Value::Integer(1), Value::Integer(2)]), + Value::Sequence(vec![Value::Real(1.0), Value::Real(2.0)]), + true, + ), + ( + Value::Sequence(vec![Value::Integer(1), Value::Integer(2)]), + Value::Sequence(vec![Value::Integer(2), Value::Integer(1)]), + false, + ), + ( + Value::Set(Set::new(vec![Value::Integer(1), Value::Real(2.5)]).unwrap()), + Value::Set(Set::new(vec![Value::Real(2.5), Value::Real(1.0)]).unwrap()), + true, + ), + ( + Value::Set(Set::new(vec![Value::Integer((1 << 53) + 1)]).unwrap()), + Value::Set(Set::new(vec![Value::Real(9_007_199_254_740_992.0)]).unwrap()), + false, + ), + ]; + for (a, b, want) in cases { + assert_eq!(a.same_value(&b), want, "{a:?} vs {b:?}"); + assert_eq!(b.same_value(&a), want, "{b:?} vs {a:?}"); + } + assert_ne!(Value::Integer(1), Value::Real(1.0)); + assert_eq!( + Set::new(vec![Value::Integer(1)]).unwrap(), + Set::new(vec![Value::Real(1.0)]).unwrap() + ); + } + #[test] fn a_tensor_quantity_keeps_its_rank_shape_and_row_major_components() { let Ok(Value::TensorQuantity(cube)) = value_from_wire(tensor( diff --git a/docs/reference/wire-contract.md b/docs/reference/wire-contract.md index 569adaf69..08d6d0e77 100644 --- a/docs/reference/wire-contract.md +++ b/docs/reference/wire-contract.md @@ -521,6 +521,13 @@ $ … /Evaluate -d '{"modelHash":"c409…1a4a","expression":"T::s.elements"}' identically, but the order carries no meaning and a client must not read one into it. - A `set` is not a `sequence`: `(1, 2) == (2, 1)` is false, the sets they populate are equal. A client compares sets by membership and sends one back in any order it likes. +- Membership is the engine's value equality: numbers by value, so `1` and `1.0` are one member + and so are `1.5` and the complex `1.5 + 0.0i`, exactly — an Integer past 2^53 is not the Real + it would round to; a Boolean is never a number; sequences in order; sets by membership; + quantities by magnitude, converting commensurable units, so `1 [m]` and `100 [cm]` are one + member. The bundled clients' equality helpers judge numbers, Booleans, sequences and sets the + same way; a quantity they compare in its unit as written, so a set holding `1 [m]` and + `100 [cm]` decodes as two members in a client where the service would hold one. - An empty set has no `elements` key (default omission). A `set` listing a member twice is refused on both sides, as is a `set` where the model wants a sequence's order or a sequence where it wants a set; a set flowing into an ordered parameter is read in canonical order. diff --git a/internal/core/runtime/set_feature_test.go b/internal/core/runtime/set_feature_test.go index 51b25f542..987241aca 100644 --- a/internal/core/runtime/set_feature_test.go +++ b/internal/core/runtime/set_feature_test.go @@ -1,6 +1,7 @@ package runtime import ( + "math" "testing" "github.com/Open-MBEE/OpenSysML/internal/core/semantics" @@ -259,6 +260,63 @@ func TestCanonicalOrderIsTotal(t *testing.T) { } } +// TestMixedNumbersOrderExactly pins that an Integer is never rounded to a +// double to place it among Reals: beyond 2^53 a nearby Real takes its own +// position, and the same members added in either order enumerate alike. +func TestMixedNumbersOrderExactly(t *testing.T) { + integer := func(n int64) Value { + return Value{Kind: ValConst, Const: semantics.Value{Kind: semantics.ValInt, Int: n}} + } + dbl := func(r float64) Value { return realConst(r) } + for _, tc := range []struct { + a, b Value + want int + }{ + {integer(1<<53 + 1), dbl(1 << 53), 1}, + {integer(1<<53 - 1), dbl(1 << 53), -1}, + {integer(1 << 53), dbl(1 << 53), 0}, + {integer(math.MaxInt64), dbl(1 << 62), 1}, + {integer(math.MaxInt64), dbl(1 << 63), -1}, + {integer(math.MinInt64), dbl(math.MinInt64), 0}, + {integer(math.MinInt64), dbl(-(1 << 64)), 1}, + {integer(2), dbl(2.5), -1}, + {integer(3), dbl(2.5), 1}, + {integer(-3), dbl(-2.5), -1}, + {integer(-2), dbl(-2.5), 1}, + {integer(0), dbl(math.Inf(1)), -1}, + {integer(0), dbl(math.Inf(-1)), 1}, + } { + if got := canonicalCompare(tc.a, tc.b); got != tc.want { + t.Errorf("compare(%s, %s) = %d, want %d", FormatValue(tc.a), FormatValue(tc.b), got, tc.want) + } + if got := canonicalCompare(tc.b, tc.a); got != -tc.want { + t.Errorf("compare(%s, %s) = %d, want %d", FormatValue(tc.b), FormatValue(tc.a), got, -tc.want) + } + } + + members := []Value{integer(1<<53 + 1), dbl(1 << 53), integer(1<<53 - 1), dbl(1<<53 + 2)} + want := "{9007199254740991, 9007199254740992.0, 9007199254740993, 9007199254740994.0}" + var sets []*Set + for _, order := range [][]int{{0, 1, 2, 3}, {3, 2, 1, 0}, {1, 3, 0, 2}} { + set := NewSet() + for _, i := range order { + set.Add(members[i]) + } + if set.Size() != len(members) { + t.Fatalf("added in %v: %d members, want %d", order, set.Size(), len(members)) + } + if got := FormatTraceValue(NewSetValue(set)); got != want { + t.Errorf("added in %v: %s, want %s", order, got, want) + } + sets = append(sets, set) + } + for _, set := range sets[1:] { + if !sets[0].Equal(set) { + t.Errorf("sets of the same members differ: %s vs %s", FormatTraceValue(NewSetValue(sets[0])), FormatTraceValue(NewSetValue(set))) + } + } +} + // TestSameNamedLiteralsOrderByDeclaration pins that two distinct literals whose // enumerations share a name — rendered alike — still take one position each, // so equal sets enumerate alike whatever order they were written in. diff --git a/internal/core/runtime/set_order.go b/internal/core/runtime/set_order.go index fdd66e832..4fc13c5c9 100644 --- a/internal/core/runtime/set_order.go +++ b/internal/core/runtime/set_order.go @@ -212,14 +212,37 @@ func canonicalClass(v Value) int { return classOther } -// compareNumbers orders the numeric constants, infinity above every finite number. +// compareNumbers orders the numeric constants exactly, infinity above every finite number. func compareNumbers(a, b semantics.Value) int { - if a.Kind == semantics.ValInt && b.Kind == semantics.ValInt { + switch { + case a.Kind == semantics.ValInt && b.Kind == semantics.ValInt: return cmp.Compare(a.Int, b.Int) + case a.Kind == semantics.ValInt && b.Kind == semantics.ValReal: + return compareIntReal(a.Int, b.Real) + case a.Kind == semantics.ValReal && b.Kind == semantics.ValInt: + return -compareIntReal(b.Int, a.Real) } return cmp.Compare(numberOf(a), numberOf(b)) } +// compareIntReal orders an Integer against a Real without rounding the Integer +// to float64: by whole part first, then by the Real's fraction. +func compareIntReal(i int64, r float64) int { + switch { + case math.IsNaN(r): + return cmp.Compare(0.0, r) + case r >= -float64(math.MinInt64): + return -1 + case r < float64(math.MinInt64): + return 1 + } + whole := math.Trunc(r) + if c := cmp.Compare(i, int64(whole)); c != 0 { + return c + } + return cmp.Compare(0, r-whole) +} + func numberOf(v semantics.Value) float64 { if v.Kind == semantics.ValInfinity { return math.Inf(1) From 6cd262a3bf6a9ebbafc165819cad5269d8bf38ac Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 05:41:37 +0000 Subject: [PATCH 08/20] fix(clients): judge set members as the service does, across commensurable units MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A set listing a member twice, by each client's value equality, is now refused before it is sent (Go, Node) or on construction and decoding (Python, Rust, Java), so a malformed set never compares equal to a valid one. Client quantity equality converts through the unit's reduction to base units — exactly while the magnitude is an integer and the scale a whole ratio — so 1 m and 100 cm are one member, km/h and m/s are commensurable, and metres never equal seconds; a quantity without a reduction is still compared in its unit as written. The Java membership test expecting 2.0 outside a set holding 2 is corrected to the numeric equality the other clients already implement. Co-Authored-By: jason.han --- .agents/skills/testing-sysml-repl/SKILL.md | 29 ++ client/opensysml/README.md | 15 +- client/opensysml/convert.go | 6 + client/opensysml/equal.go | 113 +++++++- client/opensysml/set_tensor_test.go | 13 +- client/opensysml/structured_internal_test.go | 173 ++++++++++-- client/opensysml/value.go | 4 +- .../java/org/openmbee/opensysml/Value.java | 74 +++++- .../openmbee/opensysml/PublicTypesTest.java | 86 ++++++ .../opensysml/internal/ProtosTest.java | 3 +- clients/node/README.md | 14 +- clients/node/src/core/values.ts | 98 +++++-- clients/node/test/values.test.ts | 112 ++++++++ clients/python/opensysml/values.py | 57 +++- clients/python/tests/test_set_tensor.py | 62 ++++- clients/rust/README.md | 4 +- clients/rust/opensysml/src/domain.rs | 247 +++++++++++++++++- docs/reference/wire-contract.md | 6 +- 18 files changed, 1021 insertions(+), 95 deletions(-) diff --git a/.agents/skills/testing-sysml-repl/SKILL.md b/.agents/skills/testing-sysml-repl/SKILL.md index c82e56796..0d95165f4 100644 --- a/.agents/skills/testing-sysml-repl/SKILL.md +++ b/.agents/skills/testing-sysml-repl/SKILL.md @@ -5766,3 +5766,32 @@ and use Shift+PageUp. expressions (`m.eval('3 * 4')`) as the service-survival check. A throwaway venv (`python3 -m venv ~/pr-venv && ~/pr-venv/bin/pip install -e clients/python`) is ~1 min; drive the freshly built `./bin/sysml-grpc -port 50123` with `auto_start=False`. + +## Sets, tensors, and capability refusal + +- Use `conformance/fixtures/set_tensor.sysml` for an m-based rank-three tensor and + a duplicate/out-of-order set. Evaluate `s.elements`, not `s`, to obtain the + `SetValue` rather than the Collection object. `TensorQuantity.dimensions` is + `(2, 2, 2)` and all eight `components` should retain unit text `m`. +- If a qualified compound expression such as `T::cube#(2,1,2)` reports an + unresolved reference after importing quantity libraries, pin the scope with + `%eval in T : cube#(2,1,2)`. In noninteractive `-e`, use `cube#(2,1,2)` + in the file's package context; `-e 'in T : ...'` is not REPL-command syntax. + Python's corresponding option is `context_symbol_id="T"`. +- SysML tensor indices are one-based, while Python `cube[(1,0,1)]` is zero-based; + both address the sixth component of shape `(2,2,2)`, expected `6.0 m`. +- For a real capability-refusal check, run a second freshly built service with + `OPENSYSML_TEST_WITHHOLD_CAPABILITIES=set_values,tensor_values` + and `-port 50124 -health-port 0`. Confirm absent capabilities, a working scalar + evaluation, then `MissingCapabilityError` for direct and nested outbound values. + Count delegated `EvaluateCalc` calls to demonstrate refusal before sending; + `Connection._stub` is a read-only property, backed by `_service`. +- Library-backed collection regression probes need valid declarations: + `Collections::Array` requires dimensions matching its elements, and Map / + OrderedMap elements must be `KeyValuePair` objects, not raw integers. +- In Konsole, use `Ctrl++` (not Ctrl+Shift++) to enlarge the font. Display version + with shell `bin/sysml --version`; `%version` is not a REPL command. + +### Devin Secrets Needed + +None for these local REPL/gRPC checks. diff --git a/client/opensysml/README.md b/client/opensysml/README.md index b5d719782..a85858cdb 100644 --- a/client/opensysml/README.md +++ b/client/opensysml/README.md @@ -216,12 +216,15 @@ A `Set` arrives with its elements in the service's canonical order — Booleans, then numbers, strings, quantities, enumeration literals and objects, each class in its own order — so two equal sets arrive alike, and one that lists a member twice reads as an unsupported `Null` naming it; a `Set` you send may list its -elements in any order, but listing one twice is refused by the service rather -than read as one element. `Set.Contains` tests membership and `Equal` compares -any two values as the service does — sets by membership, sequences in order, -numbers by value, so `Int(1)` is `Real(1)` and a `Complex` on the real axis is -its real part, exactly across the whole `Int` range; a `Quantity` is one in its -unit as written. A `TensorQuantity` carries its dimensions and one `Quantity` per +elements in any order, but one listing an element twice, by `Equal`, is refused +with `CodeInvalidArgument` before it is sent rather than read as one element. +`Set.Contains` tests membership and `Equal` compares any two values as the +service does — sets by membership, sequences in order, numbers by value, so +`Int(1)` is `Real(1)` and a `Complex` on the real axis is its real part, exactly +across the whole `Int` range; a `Quantity` by magnitude through its `Term`, so +`1 [m]` is `100 [cm]` (exactly, while the magnitude is an `Int` and the scale a +whole ratio), and one without a `Term` in its unit as written. A +`TensorQuantity` carries its dimensions and one `Quantity` per component in row-major order, at any rank. ## Stability diff --git a/client/opensysml/convert.go b/client/opensysml/convert.go index 01334a261..7fc6f8ede 100644 --- a/client/opensysml/convert.go +++ b/client/opensysml/convert.go @@ -284,6 +284,12 @@ func valueToProto(value Value) (*pb.Value, error) { } return &pb.Value{Kind: &pb.Value_Function{Function: &pb.Function{CalcId: v.CalcID, SelfId: int64(v.Self)}}}, nil case Set: + if twice, ok := v.repeated(); ok { + return nil, &StatusError{ + Code: CodeInvalidArgument, + Message: fmt.Sprintf("set lists a member twice: %v", twice), + } + } set := &pb.ValueSet{Elements: make([]*pb.Value, 0, len(v))} for _, element := range v { sent, err := valueToProto(element) diff --git a/client/opensysml/equal.go b/client/opensysml/equal.go index 008ada601..574f152a7 100644 --- a/client/opensysml/equal.go +++ b/client/opensysml/equal.go @@ -2,15 +2,18 @@ package opensysml import ( "math" + "math/big" "slices" ) // Equal reports whether two values are the same value to the model, as the // service judges a Set's membership: numbers by value, so a whole Real is the // Int of its value and a Complex on the real axis is its real part, exactly -// across the whole Int range; a Sequence's order counts and a Set's does not; -// a Quantity is one in its unit as written; a Null is one whatever its reason; -// and a nil Value equals only another nil. +// across the whole Int range; a Sequence's order counts and a Set's does not, +// nor does a member it lists twice; a Quantity is one with any commensurable +// quantity of the same magnitude over their base units, so 1 m is 100 cm, +// while one carrying no reduction is compared in its unit as written; a Null +// is one whatever its reason; and a nil Value equals only another nil. func Equal(a, b Value) bool { switch x := a.(type) { case nil: @@ -25,15 +28,7 @@ func Equal(a, b Value) bool { return ok && slices.EqualFunc(x, y, Equal) case Set: y, ok := b.(Set) - if !ok || len(x) != len(y) { - return false - } - for _, e := range x { - if !y.Contains(e) { - return false - } - } - return true + return ok && x.subsetOf(y) && y.subsetOf(x) case Array: y, ok := b.(Array) return ok && slices.Equal(x.Dimensions, y.Dimensions) && slices.EqualFunc(x.Elements, y.Elements, Equal) @@ -62,6 +57,25 @@ func (s Set) Contains(value Value) bool { return slices.ContainsFunc(s, func(e Value) bool { return Equal(e, value) }) } +func (s Set) subsetOf(t Set) bool { + for _, e := range s { + if !t.Contains(e) { + return false + } + } + return true +} + +// repeated returns the first member the set lists twice, if any. +func (s Set) repeated() (Value, bool) { + for i, e := range s { + if s[:i].Contains(e) { + return e, true + } + } + return nil, false +} + // numbersEqual compares an Int, Real or Complex with any value by numeric // value; a Complex off the real axis equals only the same Complex. func numbersEqual(a, b Value) bool { @@ -102,7 +116,80 @@ func realIsInt(r float64, n int64) bool { } func quantityEqual(a, b Quantity) bool { - return numbersEqual(a.Magnitude, b.Magnitude) && a.Unit == b.Unit && unitTermEqual(a.Term, b.Term) + if a.Term == nil || b.Term == nil { + return numbersEqual(a.Magnitude, b.Magnitude) && a.Unit == b.Unit && unitTermEqual(a.Term, b.Term) + } + if !a.Term.commensurable(*b.Term) || a.Term.zeroScale() || b.Term.zeroScale() { + return false + } + if x, y, ok := exactBaseMagnitudes(a, b); ok { + return x.Cmp(y) == 0 + } + return a.baseMagnitude() == b.baseMagnitude() +} + +// exactBaseMagnitudes expresses two Int magnitudes over their base units as +// rationals, which is exact only while both scales are whole. +func exactBaseMagnitudes(a, b Quantity) (*big.Rat, *big.Rat, bool) { + x, xok := a.Magnitude.(Int) + y, yok := b.Magnitude.(Int) + if !xok || !yok || !a.Term.whole() || !b.Term.whole() { + return nil, nil, false + } + return a.Term.scaled(int64(x)), b.Term.scaled(int64(y)), true +} + +func (q Quantity) baseMagnitude() float64 { + var m float64 + switch x := q.Magnitude.(type) { + case Int: + m = float64(x) + case Real: + m = float64(x) + } + return m * q.Term.ScaleNum / q.Term.ScaleDen +} + +// exponents sums the term's factors by base unit, dropping those that cancel, +// so two reductions over the same base units compare however they are listed. +func (t UnitTerm) exponents() map[string]float64 { + totals := make(map[string]float64, len(t.Factors)) + for _, f := range t.Factors { + totals[f.UnitID] += f.Exponent + } + for id, exponent := range totals { + if exponent == 0 { + delete(totals, id) + } + } + return totals +} + +// commensurable reports whether a magnitude in t converts into other. +func (t UnitTerm) commensurable(other UnitTerm) bool { + x, y := t.exponents(), other.exponents() + if len(x) != len(y) { + return false + } + for id, exponent := range x { + if y[id] != exponent { + return false + } + } + return true +} + +func (t UnitTerm) zeroScale() bool { return t.ScaleNum == 0 || t.ScaleDen == 0 } + +func (t UnitTerm) whole() bool { return isWhole(t.ScaleNum) && isWhole(t.ScaleDen) } + +func isWhole(f float64) bool { return f == math.Trunc(f) && !math.IsInf(f, 0) } + +// scaled is magnitude times the term's scale, exactly; the scale must be whole. +func (t UnitTerm) scaled(magnitude int64) *big.Rat { + num, den := new(big.Rat).SetFloat64(t.ScaleNum), new(big.Rat).SetFloat64(t.ScaleDen) + m := new(big.Rat).SetInt64(magnitude) + return m.Mul(m, num.Quo(num, den)) } func unitTermEqual(a, b *UnitTerm) bool { diff --git a/client/opensysml/set_tensor_test.go b/client/opensysml/set_tensor_test.go index 9348a136e..65b8fb278 100644 --- a/client/opensysml/set_tensor_test.go +++ b/client/opensysml/set_tensor_test.go @@ -114,15 +114,16 @@ func TestSetsAndTensorsCrossEveryTransport(t *testing.T) { t.Errorf("sizeOf({3, 1, 2}) = %#v, want 3", calc.Result) } - // A set listing an element twice is a failure, not a set of one. - _, err = client.EvaluateCalc(ctx, model, "W::sizeOf", opensysml.Set{opensysml.Int(1), opensysml.Int(1)}) - var failure *opensysml.FailureError - if !errors.As(err, &failure) || !strings.Contains(failure.Error(), "set element is repeated") { - t.Errorf("sizeOf({1, 1}) err = %v, want a failure naming the repeated element", err) + // A set listing an element twice is refused before it is sent, not sent as a set of one. + _, err = client.EvaluateCalc(ctx, model, "W::sizeOf", opensysml.Set{opensysml.Int(1), opensysml.Real(1)}) + var status *opensysml.StatusError + if !errors.As(err, &status) || status.Code != opensysml.CodeInvalidArgument || !strings.Contains(status.Message, "set lists a member twice") { + t.Errorf("sizeOf({1, 1.0}) err = %v, want an invalid-argument status naming the repeated element", err) } - // So is a tensor its components do not fill. + // A tensor its components do not fill is a failure. short := opensysml.TensorQuantity{Dimensions: []int64{2, 2, 2}, Components: tq.Components[:7]} _, err = client.EvaluateCalc(ctx, model, "W::corner", short) + var failure *opensysml.FailureError if !errors.As(err, &failure) || !strings.Contains(failure.Error(), "tensor components do not fill its dimensions") { t.Errorf("corner(short) err = %v, want a failure naming the shape", err) } diff --git a/client/opensysml/structured_internal_test.go b/client/opensysml/structured_internal_test.go index 865542c29..ad1d856a5 100644 --- a/client/opensysml/structured_internal_test.go +++ b/client/opensysml/structured_internal_test.go @@ -2,6 +2,7 @@ package opensysml import ( "context" + "errors" "math" "reflect" "strings" @@ -259,26 +260,29 @@ func TestEqualJudgesNumbersByValue(t *testing.T) { a, b Value want bool }{ - "int and whole real": {Int(1), Real(1), true}, - "int and fraction": {Int(1), Real(1.5), false}, - "int and real beyond 2^53": {Int(1<<53 + 1), Real(1 << 53), false}, - "int and real at 2^53": {Int(1 << 53), Real(1 << 53), true}, - "max int and 2^63": {Int(math.MaxInt64), Real(-math.MinInt64), false}, - "min int and -2^63": {Int(math.MinInt64), Real(math.MinInt64), true}, - "int and infinity": {Int(0), Real(math.Inf(1)), false}, - "zero and negative zero": {Int(0), Real(math.Copysign(0, -1)), true}, - "real and real-axis complex": {Real(2.5), Complex(complex(2.5, 0)), true}, - "int and real-axis complex": {Int(2), Complex(complex(2, 0)), true}, - "int and imaginary": {Int(2), Complex(complex(2, 1)), false}, - "complex twice": {Complex(complex(2, 1)), Complex(complex(2, 1)), true}, - "int and bool": {Int(1), Bool(true), false}, - "int and string": {Int(1), String("1"), false}, - "quantity by int and real": {Quantity{Magnitude: Int(1), Unit: "m"}, Quantity{Magnitude: Real(1), Unit: "m"}, true}, - "quantity in another unit": {Quantity{Magnitude: Int(1), Unit: "m"}, Quantity{Magnitude: Int(1), Unit: "km"}, false}, - "vector by int and real": {Vector{Int(1), Real(2)}, Vector{Real(1), Int(2)}, true}, - "vector and sequence": {Vector{Int(1)}, Sequence{Int(1)}, false}, - "set by int and real": {Set{Int(1), Real(2.5)}, Set{Real(2.5), Real(1)}, true}, - "set and near real": {Set{Int(1<<53 + 1)}, Set{Real(1 << 53)}, false}, + "int and whole real": {Int(1), Real(1), true}, + "int and fraction": {Int(1), Real(1.5), false}, + "int and real beyond 2^53": {Int(1<<53 + 1), Real(1 << 53), false}, + "int and real at 2^53": {Int(1 << 53), Real(1 << 53), true}, + "max int and 2^63": {Int(math.MaxInt64), Real(-math.MinInt64), false}, + "min int and -2^63": {Int(math.MinInt64), Real(math.MinInt64), true}, + "int and infinity": {Int(0), Real(math.Inf(1)), false}, + "zero and negative zero": {Int(0), Real(math.Copysign(0, -1)), true}, + "real and real-axis complex": {Real(2.5), Complex(complex(2.5, 0)), true}, + "int and real-axis complex": {Int(2), Complex(complex(2, 0)), true}, + "int and imaginary": {Int(2), Complex(complex(2, 1)), false}, + "complex twice": {Complex(complex(2, 1)), Complex(complex(2, 1)), true}, + "int and bool": {Int(1), Bool(true), false}, + "int and string": {Int(1), String("1"), false}, + "quantity by int and real": {Quantity{Magnitude: Int(1), Unit: "m"}, Quantity{Magnitude: Real(1), Unit: "m"}, true}, + "quantity in another unit": {Quantity{Magnitude: Int(1), Unit: "m"}, Quantity{Magnitude: Int(1), Unit: "km"}, false}, + "vector by int and real": {Vector{Int(1), Real(2)}, Vector{Real(1), Int(2)}, true}, + "vector and sequence": {Vector{Int(1)}, Sequence{Int(1)}, false}, + "set by int and real": {Set{Int(1), Real(2.5)}, Set{Real(2.5), Real(1)}, true}, + "set and near real": {Set{Int(1<<53 + 1)}, Set{Real(1 << 53)}, false}, + "set listing one twice": {Set{Int(1), Int(1)}, Set{Int(1), Int(2)}, false}, + "set listing one twice, sizes alike": {Set{Int(1), Int(1), Int(2)}, Set{Int(1), Int(2), Int(2)}, true}, + "set listing one twice, one": {Set{Int(1), Real(1)}, Set{Int(1)}, true}, } { t.Run(name, func(t *testing.T) { if got := Equal(tc.a, tc.b); got != tc.want { @@ -291,6 +295,135 @@ func TestEqualJudgesNumbersByValue(t *testing.T) { } } +// Equal judges quantities as the service does: commensurable ones by their +// magnitude over the base units, exactly while both are Ints over whole +// scales; ones carrying no reduction in their unit as written. +func TestEqualJudgesQuantitiesAcrossUnits(t *testing.T) { + term := func(num, den float64, factors ...UnitFactor) *UnitTerm { + return &UnitTerm{ScaleNum: num, ScaleDen: den, Factors: factors} + } + metre := UnitFactor{UnitID: "SI::metre", Exponent: 1} + second := UnitFactor{UnitID: "SI::second", Exponent: 1} + perSecond := UnitFactor{UnitID: "SI::second", Exponent: -1} + m := func(magnitude Number) Quantity { + return Quantity{Magnitude: magnitude, Unit: "m", Term: term(1, 1, metre)} + } + cm := func(magnitude Number) Quantity { + return Quantity{Magnitude: magnitude, Unit: "cm", Term: term(1, 100, metre)} + } + cmDecimal := func(magnitude Number) Quantity { + return Quantity{Magnitude: magnitude, Unit: "cm", Term: term(0.01, 1, metre)} + } + km := func(magnitude Number) Quantity { + return Quantity{Magnitude: magnitude, Unit: "km", Term: term(1000, 1, metre)} + } + s := func(magnitude Number) Quantity { + return Quantity{Magnitude: magnitude, Unit: "s", Term: term(1, 1, second)} + } + kmh := func(magnitude Number) Quantity { + return Quantity{Magnitude: magnitude, Unit: "km/h", Term: term(1000, 3600, metre, perSecond)} + } + ms := func(magnitude Number) Quantity { + return Quantity{Magnitude: magnitude, Unit: "m/s", Term: term(1, 1, perSecond, metre)} + } + named := func(magnitude Number, unit string) Quantity { return Quantity{Magnitude: magnitude, Unit: unit} } + for name, tc := range map[string]struct { + a, b Value + want bool + }{ + "metre and centimetres": {m(Int(1)), cm(Int(100)), true}, + "metre and centimetres, decimal": {m(Int(1)), cmDecimal(Int(100)), true}, + "metre and centimetres, real": {m(Real(1)), cm(Real(100)), true}, + "metre and centimetres, mixed": {m(Int(1)), cm(Real(100)), true}, + "metre and one centimetre": {m(Int(1)), cm(Int(1)), false}, + "metres and kilometre": {m(Int(1000)), km(Int(1)), true}, + "metres and kilometre, real": {m(Real(1000)), km(Int(1)), true}, + "metres and kilometre, off by 1": {m(Int(1001)), km(Int(1)), false}, + "metres and kilometres beyond 2^53": {m(Int(1000 * (1<<53 + 1))), km(Int(1<<53 + 1)), true}, + "metres and kilometres beyond 2^53, off": {m(Int(1000*(1<<53+1) + 1)), km(Int(1<<53 + 1)), false}, + "metre and second": {m(Int(1)), s(Int(1)), false}, + "speeds": {kmh(Real(5.4)), ms(Real(1.5)), true}, + "speeds, int": {kmh(Int(36)), ms(Int(10)), true}, + "speeds, unlike": {kmh(Int(36)), ms(Int(11)), false}, + "speed and length": {kmh(Int(1)), m(Int(1)), false}, + "unit named twice over": {m(Int(1)), Quantity{Magnitude: Int(1), Unit: "m", Term: term(1, 1, metre, perSecond, second)}, true}, + "named alike, no reduction": {named(Int(1), "m"), named(Real(1), "m"), true}, + "named unlike, no reduction": {named(Int(1), "m"), named(Int(100), "cm"), false}, + "reduction on one side": {named(Int(1), "m"), m(Int(1)), false}, + "zero scale": {Quantity{Magnitude: Int(0), Unit: "x", Term: term(0, 1, metre)}, m(Int(0)), false}, + "set of lengths in any unit": {Set{m(Int(1)), km(Int(2))}, Set{m(Int(2000)), cm(Int(100))}, true}, + "set of lengths, one unlike": {Set{m(Int(1)), km(Int(2))}, Set{m(Int(2000)), cm(Int(1))}, false}, + "vector quantities across units": {VectorQuantity{m(Int(1)), km(Int(1))}, VectorQuantity{cm(Int(100)), m(Int(1000))}, true}, + "tensor quantities across units": { + TensorQuantity{Dimensions: []int64{1, 1}, Components: []Quantity{m(Int(1))}}, + TensorQuantity{Dimensions: []int64{1, 1}, Components: []Quantity{cm(Int(100))}}, + true, + }, + } { + t.Run(name, func(t *testing.T) { + if got := Equal(tc.a, tc.b); got != tc.want { + t.Errorf("Equal(%#v, %#v) = %v, want %v", tc.a, tc.b, got, tc.want) + } + if got := Equal(tc.b, tc.a); got != tc.want { + t.Errorf("Equal(%#v, %#v) = %v, want %v", tc.b, tc.a, got, tc.want) + } + }) + } + + // Membership and duplicate detection follow: a set holding 1 m holds 100 + // cm, and one listing both is refused before it is sent. + lengths := Set{m(Int(1)), s(Int(1))} + if !lengths.Contains(cm(Int(100))) || lengths.Contains(cm(Int(1))) { + t.Errorf("Set{1 m, 1 s}.Contains: 100 cm %v, 1 cm %v", lengths.Contains(cm(Int(100))), lengths.Contains(cm(Int(1)))) + } + var status *StatusError + if _, err := valueToProto(Set{m(Int(1)), cm(Int(100))}); !errors.As(err, &status) || status.Code != CodeInvalidArgument { + t.Errorf("valueToProto(Set{1 m, 100 cm}) = %v, want an invalid-argument StatusError", err) + } + if _, err := valueToProto(Set{m(Int(1)), cm(Int(1))}); err != nil { + t.Errorf("valueToProto(Set{1 m, 1 cm}) = %v", err) + } +} + +// A set a caller assembles with a member listed twice, by Equal, is refused +// before it is sent, as the service would refuse it; one whose members only +// look alike is sent. +func TestRepeatedSetMembersAreNotSent(t *testing.T) { + for name, set := range map[string]Set{ + "integer twice": {Int(1), Int(2), Int(1)}, + "integer and real": {Int(1), Real(1)}, + "real and complex": {Real(1.5), Complex(complex(1.5, 0))}, + "sequence twice": {Sequence{Int(1), Int(2)}, Sequence{Int(1), Int(2)}}, + "set twice, reordered": {Set{Int(1), Int(2)}, Set{Int(2), Int(1)}}, + "nested": {Int(3), Set{Int(1), Int(1)}}, + } { + t.Run(name, func(t *testing.T) { + _, err := valueToProto(set) + var status *StatusError + if !errors.As(err, &status) || status.Code != CodeInvalidArgument || !strings.HasPrefix(status.Message, "set lists a member twice: ") { + t.Fatalf("sent with err %v, want an invalid-argument StatusError naming the repeated member", err) + } + }) + } + for name, set := range map[string]Set{ + "integer and near real": {Int(1<<53 + 1), Real(1 << 53)}, + "integer and boolean": {Int(1), Bool(true)}, + "sequence and set": {Sequence{Int(1)}, Set{Int(1)}}, + "sequences reordered": {Sequence{Int(1), Int(2)}, Sequence{Int(2), Int(1)}}, + "empty and singleton": {Set{}, Set{Set{}}}, + } { + t.Run(name, func(t *testing.T) { + sent, err := valueToProto(set) + if err != nil { + t.Fatalf("refused: %v", err) + } + if got := valueFromProto(sent); !reflect.DeepEqual(got, set) { + t.Errorf("read back as %#v, want %#v", got, set) + } + }) + } +} + // A malformed measurement reference in an answer reads as an unsupported null // naming the fault; a well-formed one reads as itself, reduction and identity // intact. diff --git a/client/opensysml/value.go b/client/opensysml/value.go index 57cc85aaa..1462f3f47 100644 --- a/client/opensysml/value.go +++ b/client/opensysml/value.go @@ -143,8 +143,8 @@ type VectorQuantity []Quantity // Set is a unique, unordered collection — a Collections::Set's elements — as // distinct from a Sequence, whose order is part of its value. The service // sends the elements in its canonical order, so equal sets arrive alike; a -// caller may list them in any order, but listing one twice is refused by the -// service rather than read as one element. +// caller may list them in any order, but one listing an element twice, by +// Equal, is refused when sent rather than read as one element. type Set []Value // TensorQuantity is a tensor quantity of any rank: one Quantity per component, diff --git a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/Value.java b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/Value.java index 4118176c0..f5d9b006e 100644 --- a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/Value.java +++ b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/Value.java @@ -1,7 +1,11 @@ package org.openmbee.opensysml; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -533,8 +537,10 @@ public Optional unit() { * Whether this is the same value as another to the model, as the service judges a set's * membership: numbers by value, so a whole {@link RealValue} is the {@link IntegerValue} of its * value and a {@link ComplexValue} on the real axis is its real part, exactly across the whole - * {@code long} range; a sequence's order counts and a set's does not; a quantity is one in its - * unit as written. Every other arm compares as {@link Object#equals} does, which stays + * {@code long} range; a sequence's order counts and a set's does not; a quantity is compared + * over its base units, so {@code 1 [m]} is {@code 100 [cm]} — exactly while integer magnitudes + * scale by whole factors — and one lacking a reduction is compared in its unit as written. + * Every other arm compares as {@link Object#equals} does, which stays * structural: {@code new IntegerValue(1).equals(new RealValue(1.0))} is {@code false}. * * @param other the value to compare with @@ -613,9 +619,63 @@ private static boolean sameValues(List a, List b) { } private static boolean quantitiesEqual(Quantity a, Quantity b) { - return magnitudesEqual(a.magnitude(), b.magnitude()) - && a.unit().equals(b.unit()) - && a.reduction().equals(b.reduction()); + if (a.reduction().isEmpty() || b.reduction().isEmpty()) { + return magnitudesEqual(a.magnitude(), b.magnitude()) + && a.unit().equals(b.unit()) + && a.reduction().equals(b.reduction()); + } + Quantity.UnitTerm x = a.reduction().get(); + Quantity.UnitTerm y = b.reduction().get(); + if (!exponents(x).equals(exponents(y)) || zeroScale(x) || zeroScale(y)) { + return false; + } + BigInteger[] m = exactBaseMagnitude(a); + BigInteger[] n = exactBaseMagnitude(b); + if (m != null && n != null) { + return m[0].multiply(n[1]).equals(n[0].multiply(m[1])); + } + return baseMagnitude(a) == baseMagnitude(b); + } + + /** The base-unit exponents, repeated units summed and cancelled ones dropped. */ + private static Map exponents(Quantity.UnitTerm term) { + Map totals = new HashMap<>(); + for (Quantity.UnitFactor factor : term.factors()) { + totals.merge(factor.unitId(), factor.exponent(), Double::sum); + } + totals.values().removeIf(exponent -> exponent == 0.0); + return totals; + } + + private static boolean zeroScale(Quantity.UnitTerm term) { + return term.scaleNumerator() == 0.0 || term.scaleDenominator() == 0.0; + } + + private static double baseMagnitude(Quantity quantity) { + Quantity.UnitTerm term = quantity.reduction().get(); + return quantity.magnitude().doubleValue() * term.scaleNumerator() / term.scaleDenominator(); + } + + /** The base magnitude as an exact numerator/denominator while an integer scales by whole factors. */ + private static BigInteger[] exactBaseMagnitude(Quantity quantity) { + Quantity.UnitTerm term = quantity.reduction().get(); + if (!(quantity.magnitude() instanceof Long magnitude) + || !isWhole(term.scaleNumerator()) + || !isWhole(term.scaleDenominator())) { + return null; + } + return new BigInteger[] { + BigInteger.valueOf(magnitude).multiply(wholeOf(term.scaleNumerator())), + wholeOf(term.scaleDenominator()) + }; + } + + private static boolean isWhole(double scale) { + return scale == Math.rint(scale) && !Double.isInfinite(scale); + } + + private static BigInteger wholeOf(double scale) { + return new BigDecimal(scale).toBigIntegerExact(); } private static boolean sameQuantities(List a, List b) { @@ -638,6 +698,10 @@ private static int valueHash(Value value) { return Double.hashCode(magnitude.doubleValue() + 0.0); } if (value instanceof QuantityValue quantity) { + Optional reduction = quantity.quantity().reduction(); + if (reduction.isPresent()) { + return exponents(reduction.get()).hashCode(); + } return Double.hashCode(quantity.quantity().magnitude().doubleValue() + 0.0) ^ quantity.quantity().unit().hashCode(); } diff --git a/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/PublicTypesTest.java b/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/PublicTypesTest.java index 18d024975..0339fb336 100644 --- a/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/PublicTypesTest.java +++ b/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/PublicTypesTest.java @@ -193,6 +193,92 @@ private static Quantity metres(Number magnitude) { return new Quantity(magnitude, Optional.of("m"), Optional.empty()); } + private static Quantity reduced( + Number magnitude, String unit, double scaleNum, double scaleDen, Quantity.UnitFactor... factors) { + return new Quantity( + magnitude, + Optional.of(unit), + Optional.of(new Quantity.UnitTerm(scaleNum, scaleDen, List.of(factors)))); + } + + @Test + void quantitiesAreTheSameValueOverTheirBaseUnits() { + Quantity.UnitFactor metre = new Quantity.UnitFactor("SI::metre", 1.0); + Quantity.UnitFactor second = new Quantity.UnitFactor("SI::second", 1.0); + Quantity.UnitFactor perSecond = new Quantity.UnitFactor("SI::second", -1.0); + Value m = new Value.QuantityValue(reduced(1L, "m", 1.0, 1.0, metre)); + Value cm = new Value.QuantityValue(reduced(100L, "cm", 1.0, 100.0, metre)); + Value km = new Value.QuantityValue(reduced(1L, "km", 1000.0, 1.0, metre)); + long huge = (1L << 53) + 1; + + // 1 m is 100 cm however the scale is written, exactly for integers over whole scales. + assertTrue(m.sameValue(cm)); + assertTrue(cm.sameValue(m)); + assertTrue(m.sameValue(new Value.QuantityValue(reduced(100.0, "cm", 0.01, 1.0, metre)))); + assertTrue(new Value.QuantityValue(reduced(1.0, "m", 1.0, 1.0, metre)).sameValue(cm)); + assertFalse(m.sameValue(new Value.QuantityValue(reduced(1L, "cm", 1.0, 100.0, metre)))); + assertTrue(new Value.QuantityValue(reduced(1000L, "m", 1.0, 1.0, metre)).sameValue(km)); + assertFalse(new Value.QuantityValue(reduced(1001L, "m", 1.0, 1.0, metre)).sameValue(km)); + assertTrue( + new Value.QuantityValue(reduced(1000 * huge, "m", 1.0, 1.0, metre)) + .sameValue(new Value.QuantityValue(reduced(huge, "km", 1000.0, 1.0, metre)))); + assertFalse( + new Value.QuantityValue(reduced(1000 * huge + 1, "m", 1.0, 1.0, metre)) + .sameValue(new Value.QuantityValue(reduced(huge, "km", 1000.0, 1.0, metre)))); + + // Different dimensions, or a scale nothing converts through, are never the same value. + assertFalse(m.sameValue(new Value.QuantityValue(reduced(1L, "s", 1.0, 1.0, second)))); + Value zeroScale = new Value.QuantityValue(reduced(0L, "x", 0.0, 1.0, metre)); + assertFalse(m.sameValue(zeroScale)); + assertFalse(zeroScale.sameValue(zeroScale)); + + // Compound units compare over their summed, cancelled factors. + assertTrue( + new Value.QuantityValue(reduced(5.4, "km/h", 1000.0, 3600.0, metre, perSecond)) + .sameValue(new Value.QuantityValue(reduced(1.5, "m/s", 1.0, 1.0, perSecond, metre)))); + assertTrue( + new Value.QuantityValue(reduced(36L, "km/h", 1000.0, 3600.0, metre, perSecond)) + .sameValue(new Value.QuantityValue(reduced(10L, "m/s", 1.0, 1.0, metre, perSecond)))); + assertFalse( + new Value.QuantityValue(reduced(36L, "km/h", 1000.0, 3600.0, metre, perSecond)) + .sameValue(new Value.QuantityValue(reduced(11L, "m/s", 1.0, 1.0, metre, perSecond)))); + assertTrue(m.sameValue(new Value.QuantityValue(reduced(1L, "m·s/s", 1.0, 1.0, metre, perSecond, second)))); + + // Without a reduction, the unit as written is all there is to compare. + assertTrue(new Value.QuantityValue(metres(1L)).sameValue(new Value.QuantityValue(metres(1.0)))); + assertFalse( + new Value.QuantityValue(metres(1L)) + .sameValue(new Value.QuantityValue(new Quantity(100L, Optional.of("cm"), Optional.empty())))); + assertFalse(new Value.QuantityValue(metres(1L)).sameValue(m)); + + // Membership, duplicate detection and set equality follow. + Value.SetValue lengths = new Value.SetValue(List.of(m, km)); + assertTrue(lengths.contains(cm)); + assertFalse(lengths.contains(new Value.QuantityValue(reduced(1L, "cm", 1.0, 100.0, metre)))); + assertThrows(IllegalArgumentException.class, () -> new Value.SetValue(List.of(m, cm))); + assertEquals( + 2, new Value.SetValue(List.of(m, new Value.QuantityValue(reduced(1L, "cm", 1.0, 100.0, metre)))).size()); + Value.SetValue rewritten = + new Value.SetValue(List.of(new Value.QuantityValue(reduced(1000L, "m", 1.0, 1.0, metre)), cm)); + assertEquals(rewritten, lengths); + assertEquals(rewritten.hashCode(), lengths.hashCode()); + assertNotEquals( + new Value.SetValue( + List.of( + new Value.QuantityValue(reduced(1000L, "m", 1.0, 1.0, metre)), + new Value.QuantityValue(reduced(1L, "cm", 1.0, 100.0, metre)))), + lengths); + assertTrue( + new Value.VectorQuantityValue(List.of(reduced(1L, "m", 1.0, 1.0, metre), reduced(1L, "km", 1000.0, 1.0, metre))) + .sameValue( + new Value.VectorQuantityValue( + List.of(reduced(100L, "cm", 1.0, 100.0, metre), reduced(1000L, "m", 1.0, 1.0, metre))))); + assertTrue( + new Value.TensorQuantityValue(List.of(1L, 1L), List.of(reduced(1L, "m", 1.0, 1.0, metre))) + .sameValue( + new Value.TensorQuantityValue(List.of(1L, 1L), List.of(reduced(100L, "cm", 1.0, 100.0, metre))))); + } + @Test void aTensorQuantityIsShapedAndIndexedInRowMajorOrder() { List pascals = new ArrayList<>(); diff --git a/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/internal/ProtosTest.java b/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/internal/ProtosTest.java index 3f044d475..235eb4a29 100644 --- a/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/internal/ProtosTest.java +++ b/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/internal/ProtosTest.java @@ -282,7 +282,8 @@ void aSetHoldsEachMemberOnceAndComparesInAnyOrder() { members.elements()); assertTrue(members.contains(new Value.IntegerValue(2))); assertTrue(!members.contains(new Value.IntegerValue(4))); - assertTrue(!members.contains(new Value.RealValue(2.0))); + assertTrue(members.contains(new Value.RealValue(2.0))); + assertTrue(!members.contains(new Value.RealValue(2.5))); // The same members in another order are the same set, with the same hash; a sequence is not. Value.SetValue reordered = diff --git a/clients/node/README.md b/clients/node/README.md index e2010e5cb..1d5d5256c 100644 --- a/clients/node/README.md +++ b/clients/node/README.md @@ -195,11 +195,15 @@ value kinds those name (`array`, `vector`, `vectorQuantity`; `measurementRef`; A function closing over the bindings of a behavior body has no wire form and is sent as `null` by every service. A `set` arrives with its elements in the service's canonical order, so two equal sets arrive -alike, and one listing a member twice is a `MalformedValueError`; one sent to -the service may list them in any order, but not twice. `valuesEqual` is the -membership test: sets by membership, sequences in order, an `int` never a -`real`. A `tensorQuantity` carries its `dimensions` and one quantity per -component, row-major. +alike, and one listing a member twice is a `MalformedValueError`, whether it +arrives or is about to be sent; one sent to the service may list its members in +any order. `valuesEqual` is the membership test, as the service judges it: sets +by membership, sequences in order, numbers by value — `1` and `1.0` are one +member, exactly across the whole `int` range — and a quantity by magnitude +through its `unitTerm`, so `1 [m]` is `100 [cm]` (exactly, while the magnitude +is an `int` and the scale a whole ratio); one without a `unitTerm` is compared +in its unit as written. A `tensorQuantity` carries its `dimensions` and one +quantity per component, row-major. ## Failures are typed diff --git a/clients/node/src/core/values.ts b/clients/node/src/core/values.ts index 9be5dddc8..b57690074 100644 --- a/clients/node/src/core/values.ts +++ b/clients/node/src/core/values.ts @@ -305,7 +305,9 @@ export function encodeValue(value: SysMLValue): Value { return create(ValueSchema, { kind: { case: "set", - value: create(ValueSetSchema, { elements: value.elements.map(encodeValue) }), + value: create(ValueSetSchema, { + elements: uniqueMembers(value.elements).map(encodeValue), + }), }, }); case "tensorQuantity": @@ -557,24 +559,27 @@ function decodeArray(array: ArrayMessage): ArrayValue { } function decodeSet(set: ValueSet): SysMLValue[] { - const elements: SysMLValue[] = []; - for (const element of set.elements) { - const member = decodeValue(element); - if (elements.some((held) => valuesEqual(held, member))) { + return uniqueMembers(set.elements.map(decodeValue)); +} + +/** Returns the members as given, refusing one listed twice by {@link valuesEqual}. */ +function uniqueMembers(members: SysMLValue[]): SysMLValue[] { + members.forEach((member, i) => { + if (members.slice(0, i).some((held) => valuesEqual(held, member))) { throw new MalformedValueError( `a set lists a member twice: ${formatValue(member)}`, ); } - elements.push(member); - } - return elements; + }); + return members; } /** * Whether two values are the same value to the model, as the service judges a * set's membership: numbers by value, so a whole `real` is the `int` of its * value and a `complex` on the real axis is its real part, exactly across the - * whole `int` range; a `sequence`'s order counts and a `set`'s does not; a + * whole `int` range; a `sequence`'s order counts and a `set`'s does not, nor + * does a member it lists twice; a quantity is the same over its base units; a * `null` is the same whatever its reason. */ export function valuesEqual(a: SysMLValue, b: SysMLValue): boolean { @@ -623,8 +628,8 @@ export function valuesEqual(a: SysMLValue, b: SysMLValue): boolean { case "set": return ( b.kind === "set" && - a.elements.length === b.elements.length && - a.elements.every((e) => b.elements.some((o) => valuesEqual(e, o))) + subsetOf(a.elements, b.elements) && + subsetOf(b.elements, a.elements) ); case "tensorQuantity": return ( @@ -646,6 +651,10 @@ function elementsEqual(a: SysMLValue[], b: SysMLValue[]): boolean { ); } +function subsetOf(members: SysMLValue[], of: SysMLValue[]): boolean { + return members.every((e) => of.some((o) => valuesEqual(e, o))); +} + function dimensionsEqual(a: bigint[], b: bigint[]): boolean { return a.length === b.length && a.every((d, i) => d === b[i]); } @@ -693,12 +702,69 @@ function componentsEqual(a: QuantityValue[], b: QuantityValue[]): boolean { ); } +// Commensurable quantities are equal over their base units, as the service +// judges them; one carrying no reduction is compared in its unit as written. function quantitiesEqual(a: QuantityValue, b: QuantityValue): boolean { - return ( - numbersEqual(a.magnitude, b.magnitude) && - a.unit === b.unit && - unitTermsEqual(a.unitTerm, b.unitTerm) - ); + if (a.unitTerm === undefined || b.unitTerm === undefined) { + return ( + numbersEqual(a.magnitude, b.magnitude) && + a.unit === b.unit && + unitTermsEqual(a.unitTerm, b.unitTerm) + ); + } + if ( + !commensurable(a.unitTerm, b.unitTerm) || + zeroScale(a.unitTerm) || + zeroScale(b.unitTerm) + ) { + return false; + } + if ( + a.magnitude.kind === "int" && + b.magnitude.kind === "int" && + wholeScale(a.unitTerm) && + wholeScale(b.unitTerm) + ) { + // m₁·n₁/d₁ = m₂·n₂/d₂ exactly, cross-multiplied in bigint. + return ( + a.magnitude.value * BigInt(a.unitTerm.scaleNum) * BigInt(b.unitTerm.scaleDen) === + b.magnitude.value * BigInt(b.unitTerm.scaleNum) * BigInt(a.unitTerm.scaleDen) + ); + } + return baseMagnitude(a.magnitude, a.unitTerm) === baseMagnitude(b.magnitude, b.unitTerm); +} + +function baseMagnitude(magnitude: Magnitude, term: UnitFactorization): number { + return (Number(magnitude.value) * term.scaleNum) / term.scaleDen; +} + +// The reduction as base unit → exponent, repeated base units summed and +// cancelled ones dropped, so two reductions compare however they are listed. +function exponents(term: UnitFactorization): Map { + const totals = new Map(); + for (const factor of term.factors) { + totals.set(factor.unitId, (totals.get(factor.unitId) ?? 0) + factor.exponent); + } + for (const [unitId, exponent] of totals) { + if (exponent === 0) { + totals.delete(unitId); + } + } + return totals; +} + +function commensurable(a: UnitFactorization, b: UnitFactorization): boolean { + const x = exponents(a); + const y = exponents(b); + return x.size === y.size && [...x].every(([unitId, exponent]) => y.get(unitId) === exponent); +} + +function zeroScale(term: UnitFactorization): boolean { + return term.scaleNum === 0 || term.scaleDen === 0; +} + +function wholeScale(term: UnitFactorization): boolean { + return Number.isInteger(term.scaleNum) && Number.isInteger(term.scaleDen); } function unitTermsEqual( diff --git a/clients/node/test/values.test.ts b/clients/node/test/values.test.ts index 52d5c962b..976d2d49c 100644 --- a/clients/node/test/values.test.ts +++ b/clients/node/test/values.test.ts @@ -29,6 +29,7 @@ import { failureCause, formatValue, valuesEqual, + type QuantityValue, type SysMLValue, } from "../src/core/values.js"; @@ -403,6 +404,10 @@ test("valuesEqual judges numbers by value, as the service does", () => { ], [{ kind: "set", elements: [i(1n), r(2.5)] }, { kind: "set", elements: [r(2.5), r(1)] }, true], [{ kind: "set", elements: [i(2n ** 53n + 1n)] }, { kind: "set", elements: [r(2 ** 53)] }, false], + // A set assembled with a member listed twice equals only a set of the same members. + [{ kind: "set", elements: [i(1n), i(1n)] }, { kind: "set", elements: [i(1n), i(2n)] }, false], + [{ kind: "set", elements: [i(1n), i(1n), i(2n)] }, { kind: "set", elements: [i(1n), i(2n), i(2n)] }, true], + [{ kind: "set", elements: [i(1n), r(1)] }, { kind: "set", elements: [i(1n)] }, true], ]; for (const [a, b, want] of cases) { assert.equal(valuesEqual(a, b), want, `${formatValue(a)} vs ${formatValue(b)}`); @@ -410,6 +415,113 @@ test("valuesEqual judges numbers by value, as the service does", () => { } }); +test("valuesEqual judges quantities over their base units, as the service does", () => { + const term = (scaleNum: number, scaleDen: number, ...factors: [string, number][]) => ({ + scaleNum, + scaleDen, + factors: factors.map(([unitId, exponent]) => ({ unitId, exponent })), + }); + const q = ( + magnitude: bigint | number, + unit: string, + unitTerm?: ReturnType, + ): { kind: "quantity" } & QuantityValue => ({ + kind: "quantity", + magnitude: typeof magnitude === "bigint" ? { kind: "int", value: magnitude } : { kind: "real", value: magnitude }, + unit, + ...(unitTerm === undefined ? {} : { unitTerm }), + }); + const m = (v: bigint | number) => q(v, "m", term(1, 1, ["SI::metre", 1])); + const cm = (v: bigint | number) => q(v, "cm", term(1, 100, ["SI::metre", 1])); + const cmDecimal = (v: bigint | number) => q(v, "cm", term(0.01, 1, ["SI::metre", 1])); + const km = (v: bigint | number) => q(v, "km", term(1000, 1, ["SI::metre", 1])); + const s = (v: bigint | number) => q(v, "s", term(1, 1, ["SI::second", 1])); + const kmh = (v: bigint | number) => q(v, "km/h", term(1000, 3600, ["SI::metre", 1], ["SI::second", -1])); + const ms = (v: bigint | number) => q(v, "m/s", term(1, 1, ["SI::second", -1], ["SI::metre", 1])); + const huge = 2n ** 53n + 1n; + const cases: [SysMLValue, SysMLValue, boolean][] = [ + [m(1n), cm(100n), true], + [m(1n), cmDecimal(100n), true], + [m(1), cm(100), true], + [m(1n), cm(100), true], + [m(1n), cm(1n), false], + [m(1000n), km(1n), true], + [m(1000), km(1n), true], + [m(1001n), km(1n), false], + [m(1000n * huge), km(huge), true], + [m(1000n * huge + 1n), km(huge), false], + [m(1n), s(1n), false], + [kmh(5.4), ms(1.5), true], + [kmh(36n), ms(10n), true], + [kmh(36n), ms(11n), false], + [kmh(1n), m(1n), false], + [m(1n), q(1n, "m", term(1, 1, ["SI::metre", 1], ["SI::second", -1], ["SI::second", 1])), true], + [q(1n, "m"), q(1, "m"), true], + [q(1n, "m"), q(100n, "cm"), false], + [q(1n, "m"), m(1n), false], + [q(0n, "x", term(0, 1, ["SI::metre", 1])), m(0n), false], + [{ kind: "set", elements: [m(1n), km(2n)] }, { kind: "set", elements: [m(2000n), cm(100n)] }, true], + [{ kind: "set", elements: [m(1n), km(2n)] }, { kind: "set", elements: [m(2000n), cm(1n)] }, false], + [ + { kind: "vectorQuantity", components: [m(1n), km(1n)] }, + { kind: "vectorQuantity", components: [cm(100n), m(1000n)] }, + true, + ], + [ + { kind: "tensorQuantity", dimensions: [1n, 1n], components: [m(1n)] }, + { kind: "tensorQuantity", dimensions: [1n, 1n], components: [cm(100n)] }, + true, + ], + ]; + for (const [a, b, want] of cases) { + assert.equal(valuesEqual(a, b), want, `${formatValue(a)} vs ${formatValue(b)}`); + assert.equal(valuesEqual(b, a), want, `${formatValue(b)} vs ${formatValue(a)}`); + } + + // Membership and duplicate detection follow: a set holding 1 m holds 100 cm, + // and one listing both is refused, arriving or about to be sent. + assert.throws(() => encodeValue({ kind: "set", elements: [m(1n), cm(100n)] }), { + name: "MalformedValueError", + message: /^a set lists a member twice: /, + }); + const sent = decodeValue(encodeValue({ kind: "set", elements: [m(1n), cm(1n)] })); + assert.equal(sent.kind, "set"); + assert.equal(sent.elements.length, 2); + assert.throws(() => decodeValue(encodeValue({ kind: "set", elements: [m(1000), km(1n)] })), { + name: "MalformedValueError", + message: /^a set lists a member twice: /, + }); +}); + +test("a set assembled with a member listed twice is refused before it is sent", () => { + const i = (value: bigint): SysMLValue => ({ kind: "int", value }); + const r = (value: number): SysMLValue => ({ kind: "real", value }); + const set = (...elements: SysMLValue[]): SysMLValue => ({ kind: "set", elements }); + const seq = (...elements: SysMLValue[]): SysMLValue => ({ kind: "sequence", elements }); + for (const twice of [ + set(i(1n), i(2n), i(1n)), + set(i(1n), r(1)), + set(r(1.5), { kind: "complex", value: { real: 1.5, imaginary: 0 } }), + set(seq(i(1n), i(2n)), seq(i(1n), i(2n))), + set(set(i(1n), i(2n)), set(i(2n), i(1n))), + set(i(3n), set(i(1n), i(1n))), + ]) { + assert.throws(() => encodeValue(twice), { + name: "MalformedValueError", + message: /^a set lists a member twice: /, + }); + } + for (const alike of [ + set(i(2n ** 53n + 1n), r(2 ** 53)), + set(i(1n), { kind: "boolean", value: true }), + set(seq(i(1n)), set(i(1n))), + set(seq(i(1n), i(2n)), seq(i(2n), i(1n))), + set(set(), set(set())), + ]) { + assert.deepEqual(decodeValue(encodeValue(alike)), alike); + } +}); + test("a tensor quantity keeps its rank, its shape and its row-major components", () => { const cube = decodeValue(tensor([2n, 2n, 2n], ...[1, 2, 3, 4, 5, 6, 7, 8].map(metres))); assert.equal(cube.kind, "tensorQuantity"); diff --git a/clients/python/opensysml/values.py b/clients/python/opensysml/values.py index 1a109c43d..af44df44f 100644 --- a/clients/python/opensysml/values.py +++ b/clients/python/opensysml/values.py @@ -2,6 +2,7 @@ import math from dataclasses import dataclass, field +from fractions import Fraction from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Sequence, Tuple, Union from opensysml.enumeration import EnumLiteral @@ -115,6 +116,11 @@ def reduced(self) -> bool: or (self.scale_num, self.scale_den) != (1.0, 1.0) ) + @property + def zero_scale(self) -> bool: + """Whether the scale factor is zero or undefined, so no magnitude converts through it.""" + return self.scale_num == 0 or self.scale_den == 0 + @property def dimensionless(self) -> bool: """Whether the unit reduces to no base unit, as a count or a ratio does.""" @@ -262,14 +268,33 @@ def to(self, unit: Unit) -> "Quantity": def __eq__(self, other: object) -> bool: if not isinstance(other, Quantity): return NotImplemented - if not self.unit.commensurable(other.unit): + if not (self.unit.reduced and other.unit.reduced): + # Nothing to convert over: the same quantity is one in the same unit as written. + return self.unit == other.unit and self.magnitude == other.magnitude + if not self.unit.commensurable(other.unit) or self.unit.zero_scale or other.unit.zero_scale: return False + mine, theirs = self._exact_base_magnitude(), other._exact_base_magnitude() + if mine is not None and theirs is not None: + return mine == theirs return self.base_magnitude() == other.base_magnitude() + def _exact_base_magnitude(self) -> Optional[Fraction]: + """The base magnitude as a Fraction while it can be exact: an int over a whole scale.""" + if isinstance(self.magnitude, bool) or not isinstance(self.magnitude, int): + return None + num, den = self.unit.scale_num, self.unit.scale_den + if not (float(num).is_integer() and float(den).is_integer()): + return None + return Fraction(self.magnitude) * Fraction(int(num), int(den)) + def __hash__(self) -> int: # Keyed on the base-unit form, so commensurable equal quantities — `1 # [km]` and `1000 [m]` — hash alike, as equality requires. - return hash((self.base_magnitude(), tuple(sorted(self.unit.exponents().items())))) + if not self.unit.reduced or self.unit.zero_scale: + return hash((self.magnitude, self.unit)) + exact = self._exact_base_magnitude() + base = self.base_magnitude() if exact is None else exact + return hash((base, tuple(sorted(self.unit.exponents().items())))) def __lt__(self, other: "Quantity") -> bool: return self._compare(other, "order") < 0 @@ -697,18 +722,26 @@ class SetValue: sends the elements in its canonical order, so equal sets arrive alike, and ``elements`` keeps that order for reading; equality ignores it. A set to send may hold its elements in any order — a Python ``set`` or ``frozenset`` - is accepted too — but one listing an element twice is refused by the service - rather than read as one element. Elements need not be hashable: a nested - list is one. + is accepted too — but one listing an element twice, by :func:`same_value`, + is refused rather than read as one element. Elements need not be hashable: + a nested list is one. Attributes: elements (tuple): The elements, each once, in the order held + + Raises: + ValueError: If an element is listed twice. """ elements: Tuple[Any, ...] def __init__(self, elements: Iterable[Any] = ()) -> None: - object.__setattr__(self, "elements", tuple(elements)) + held: List[Any] = [] + for element in elements: + if any(same_value(element, other) for other in held): + raise ValueError(f"set lists a member twice: {element!r}") + held.append(element) + object.__setattr__(self, "elements", tuple(held)) def __len__(self) -> int: return len(self.elements) @@ -736,13 +769,11 @@ def from_pb(cls, pb_set, resolve_instance=None) -> "SetValue": Raises: UnsupportedValueError: If the message lists a member twice. """ - elements: List[Any] = [] - for pb_value in pb_set.elements: - element = value_to_python(pb_value, resolve_instance) - if any(same_value(element, held) for held in elements): - raise UnsupportedValueError(f"malformed set: member listed twice: {element}") - elements.append(element) - return cls(elements) + elements = [value_to_python(pb_value, resolve_instance) for pb_value in pb_set.elements] + try: + return cls(elements) + except ValueError as exc: + raise UnsupportedValueError(f"malformed set: {exc}") from exc def to_pb(self, encode: Callable[[Any], "sysml_pb2.Value"]) -> "sysml_pb2.ValueSet": """Encode as a ``ValueSet`` message, each element through ``encode``.""" diff --git a/clients/python/tests/test_set_tensor.py b/clients/python/tests/test_set_tensor.py index d6ffad99c..1cde8ec46 100644 --- a/clients/python/tests/test_set_tensor.py +++ b/clients/python/tests/test_set_tensor.py @@ -159,10 +159,70 @@ def pb_seq(*elements): (sysml_pb2.Value(bool_value=True), pb_seq(), sysml_pb2.Value(bool_value=True)), ]) def test_a_set_listing_a_member_twice_is_malformed(elements): - with pytest.raises(UnsupportedValueError, match="malformed set: member listed twice"): + with pytest.raises(UnsupportedValueError, match="malformed set: set lists a member twice"): value_to_python(pb_set(*elements)) +@pytest.mark.parametrize("elements", [ + (1, 2, 1), + (1, 1.0), + (2, 2 + 0j), + (0, -0.0), + ([1, 2], [1, 2]), + (SetValue((1, 2)), SetValue((2, 1))), + (SetValue(), SetValue()), + (Quantity(1.0, Unit("m", factors=(UnitFactor("SI::metre", 1.0),))), + Quantity(1, Unit("m", factors=(UnitFactor("SI::metre", 1.0),)))), + (True, [], True), +]) +def test_a_set_is_never_assembled_with_a_member_twice(elements): + with pytest.raises(ValueError, match="set lists a member twice"): + SetValue(elements) + assert len(SetValue((1, 2 ** 53 + 1, float(2 ** 53), True, [1, 2], [2, 1], [1], SetValue((1,))))) == 8 + + +def length(text, magnitude, scale_num=1.0, scale_den=1.0, factors=(("SI::metre", 1.0),)): + return Quantity(magnitude, Unit(text, scale_num, scale_den, tuple(UnitFactor(*f) for f in factors))) + + +def test_quantities_are_the_same_value_over_their_base_units(): + """As the service judges them: 1 m is 100 cm, exactly while both are ints over whole scales.""" + m, cm, km = length("m", 1), length("cm", 100, 1.0, 100.0), length("km", 1, 1000.0) + assert m == cm == length("cm", 100, 0.01) + assert length("m", 1.0) == length("cm", 100.0, 1.0, 100.0) == cm + assert m != length("cm", 1, 1.0, 100.0) + assert length("m", 1000) == km and length("m", 1001) != km + huge = 2 ** 53 + 1 + assert length("m", 1000 * huge) == length("km", huge, 1000.0) + assert length("m", 1000 * huge + 1) != length("km", huge, 1000.0) + assert m != length("s", 1, factors=(("SI::second", 1.0),)) + speed = (("SI::metre", 1.0), ("SI::second", -1.0)) + assert length("km/h", 5.4, 1000.0, 3600.0, speed) == length("m/s", 1.5, factors=reversed(speed)) + assert length("km/h", 36, 1000.0, 3600.0, speed) == length("m/s", 10, factors=speed) + assert length("km/h", 36, 1000.0, 3600.0, speed) != length("m/s", 11, factors=speed) + assert m == length("m", 1, factors=(("SI::metre", 1.0), ("SI::second", -1.0), ("SI::second", 1.0))) + assert m != length("x", 0, 0.0) and length("x", 0, 0.0) != length("x", 0, 0.0) + # A unit named without its reduction compares as written. + assert Quantity(1, Unit("m")) == Quantity(1.0, Unit("m")) + assert Quantity(1, Unit("m")) != Quantity(100, Unit("cm")) + assert Quantity(1, Unit("m")) != Quantity(1, Unit("s")) + assert Quantity(1, Unit("m")) != m + assert hash(m) == hash(cm) == hash(length("m", 1.0)) + + # Membership, duplicate detection and set equality follow. + lengths = SetValue((m, length("s", 1, factors=(("SI::second", 1.0),)))) + assert cm in lengths and length("cm", 1, 1.0, 100.0) not in lengths + with pytest.raises(ValueError, match="set lists a member twice"): + SetValue((m, cm)) + with pytest.raises(UnsupportedValueError, match="malformed set: set lists a member twice"): + value_to_python(pb_set(sysml_pb2.Value(quantity=m.to_pb()), sysml_pb2.Value(quantity=cm.to_pb()))) + assert len(SetValue((m, length("cm", 1, 1.0, 100.0)))) == 2 + assert SetValue((m, length("km", 2, 1000.0))) == SetValue((length("m", 2000), cm)) + assert SetValue((m, length("km", 2, 1000.0))) != SetValue((length("m", 2000), length("cm", 1, 1.0, 100.0))) + assert VectorQuantity((m, km)) == VectorQuantity((cm, length("m", 1000))) + assert TensorQuantity((1, 1), (m,)) == TensorQuantity((1, 1), (cm,)) + + @pytest.mark.parametrize("elements, expected", [ ((sysml_pb2.Value(bool_value=True), pb_int(1)), SetValue((True, 1))), ((pb_int(1), sysml_pb2.Value(real_value=1.5)), SetValue((1, 1.5))), diff --git a/clients/rust/README.md b/clients/rust/README.md index 5eeb8f8f6..2071367fa 100644 --- a/clients/rust/README.md +++ b/clients/rust/README.md @@ -156,7 +156,9 @@ the service's canonical order (numbers ascending, then strings, and so on), and equal to another set holding the same members in any order. Membership is judged by `Value::same_value`, as the service judges it: `Integer(1)` and `Real(1.0)` are one member, `Real(1.5)` and a `Complex` of `1.5 + 0.0i` are one -member, exactly across the whole `i64` range, while `==` on `Value` stays +member, exactly across the whole `i64` range, and a `Quantity` is judged by +magnitude through its `unit_term`, so `1 m` and `100 cm` are one member (one +without a `unit_term` in its unit as written); `==` on `Value` stays structural. A `Value::TensorQuantity` is a `Quantities::TensorQuantityValue` of any rank: its `dimensions()` and its `components()` flattened row-major, each a diff --git a/clients/rust/opensysml/src/domain.rs b/clients/rust/opensysml/src/domain.rs index 1c9ac1d2c..a1bebd28e 100644 --- a/clients/rust/opensysml/src/domain.rs +++ b/clients/rust/opensysml/src/domain.rs @@ -579,8 +579,10 @@ impl Value { /// judges a set's membership: numbers by value, so a whole [`Value::Real`] /// is the [`Value::Integer`] of its value and a [`Value::Complex`] on the /// real axis is its real part, exactly across the whole `i64` range; a - /// sequence's order counts and a set's does not; a quantity is one in its - /// unit as written. Every other arm compares as `==` does. + /// sequence's order counts and a set's does not; a quantity is compared + /// over its base units, so `1 [m]` is `100 [cm]` — exactly while integer + /// magnitudes scale by whole factors — and one lacking a reduction is + /// compared in its unit as written. Every other arm compares as `==` does. pub fn same_value(&self, other: &Value) -> bool { match (self, other) { (Value::Integer(_) | Value::Real(_) | Value::Complex(_), _) => { @@ -642,8 +644,89 @@ fn sequences_equal(a: &[Value], b: &[Value]) -> bool { a.len() == b.len() && a.iter().zip(b).all(|(x, y)| x.same_value(y)) } +// As the service judges it: over the base units when both carry a reduction, +// exactly while integer magnitudes scale by whole factors. fn quantities_equal(a: &Quantity, b: &Quantity) -> bool { - magnitudes_equal(a.magnitude, b.magnitude) && a.unit == b.unit && a.unit_term == b.unit_term + let (Some(x), Some(y)) = (&a.unit_term, &b.unit_term) else { + return magnitudes_equal(a.magnitude, b.magnitude) + && a.unit == b.unit + && a.unit_term == b.unit_term; + }; + if !commensurable(x, y) || zero_scale(x) || zero_scale(y) { + return false; + } + if let (Some(m), Some(n)) = (exact_base_magnitude(a), exact_base_magnitude(b)) { + return m == n; + } + base_magnitude(a.magnitude, x) == base_magnitude(b.magnitude, y) +} + +// The base unit exponents, repeated units summed and cancelled ones dropped. +fn exponents(term: &UnitTerm) -> HashMap<&str, f64> { + let mut totals: HashMap<&str, f64> = HashMap::new(); + for factor in &term.factors { + *totals.entry(factor.unit_id.as_str()).or_insert(0.0) += factor.exponent; + } + totals.retain(|_, exponent| *exponent != 0.0); + totals +} + +fn commensurable(a: &UnitTerm, b: &UnitTerm) -> bool { + exponents(a) == exponents(b) +} + +fn zero_scale(term: &UnitTerm) -> bool { + term.scale_num == 0.0 || term.scale_den == 0.0 +} + +fn base_magnitude(magnitude: Magnitude, term: &UnitTerm) -> f64 { + let m = match magnitude { + Magnitude::Integer(n) => n as f64, + Magnitude::Real(r) => r, + }; + m * term.scale_num / term.scale_den +} + +// The base magnitude as an exact rational (numerator, denominator), while an +// integer magnitude scales by whole factors that fit. +fn exact_base_magnitude(q: &Quantity) -> Option { + let Magnitude::Integer(n) = q.magnitude else { + return None; + }; + let term = q.unit_term.as_ref()?; + let num = i128::from(whole(term.scale_num)?); + let den = i128::from(whole(term.scale_den)?); + Some(ExactRational::new(i128::from(n).checked_mul(num)?, den)) +} + +fn whole(scale: f64) -> Option { + (scale.fract() == 0.0 + && (-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0).contains(&scale)) + .then_some(scale as i64) +} + +#[derive(Clone, Copy, Debug, PartialEq)] +struct ExactRational { + num: i128, + den: i128, +} + +impl ExactRational { + fn new(num: i128, den: i128) -> Self { + let g = gcd(num.unsigned_abs(), den.unsigned_abs()) as i128; + let sign = if den < 0 { -1 } else { 1 }; + Self { + num: sign * num / g, + den: sign * den / g, + } + } +} + +fn gcd(mut a: u128, mut b: u128) -> u128 { + while b != 0 { + (a, b) = (b, a % b); + } + a.max(1) } fn components_equal(a: &[Quantity], b: &[Quantity]) -> bool { @@ -1697,6 +1780,164 @@ mod tests { ); } + /// `same_value` judges quantities over their base units, as the service + /// does, so equivalent quantities are one set member however written. + #[test] + fn same_value_judges_quantities_across_units() { + let term = |scale_num, scale_den, factors: &[(&str, f64)]| { + Some(UnitTerm { + scale_num, + scale_den, + factors: factors + .iter() + .map(|(unit_id, exponent)| UnitFactor { + unit_id: (*unit_id).to_owned(), + exponent: *exponent, + }) + .collect(), + }) + }; + let quantity = |magnitude, unit: &str, unit_term| { + Value::Quantity(Quantity { + magnitude, + unit: unit.to_owned(), + unit_term, + }) + }; + let metre = &[("SI::metre", 1.0)][..]; + let speed = &[("SI::metre", 1.0), ("SI::second", -1.0)][..]; + let m = |n| quantity(Magnitude::Integer(n), "m", term(1.0, 1.0, metre)); + let cm = |n| quantity(Magnitude::Integer(n), "cm", term(1.0, 100.0, metre)); + let km = |n| quantity(Magnitude::Integer(n), "km", term(1000.0, 1.0, metre)); + let huge = (1 << 53) + 1; + let cases = [ + (m(1), cm(100), true), + ( + m(1), + quantity(Magnitude::Real(100.0), "cm", term(0.01, 1.0, metre)), + true, + ), + ( + quantity(Magnitude::Real(1.0), "m", term(1.0, 1.0, metre)), + cm(100), + true, + ), + (m(1), cm(1), false), + (m(1000), km(1), true), + (m(1001), km(1), false), + (m(1000 * huge), km(huge), true), + (m(1000 * huge + 1), km(huge), false), + ( + m(1), + quantity( + Magnitude::Integer(1), + "s", + term(1.0, 1.0, &[("SI::second", 1.0)]), + ), + false, + ), + ( + quantity(Magnitude::Real(5.4), "km/h", term(1000.0, 3600.0, speed)), + quantity( + Magnitude::Real(1.5), + "m/s", + term(1.0, 1.0, &[("SI::second", -1.0), ("SI::metre", 1.0)]), + ), + true, + ), + ( + quantity(Magnitude::Integer(36), "km/h", term(1000.0, 3600.0, speed)), + quantity(Magnitude::Integer(10), "m/s", term(1.0, 1.0, speed)), + true, + ), + ( + quantity(Magnitude::Integer(36), "km/h", term(1000.0, 3600.0, speed)), + quantity(Magnitude::Integer(11), "m/s", term(1.0, 1.0, speed)), + false, + ), + ( + m(1), + quantity( + Magnitude::Integer(1), + "m·s/s", + term( + 1.0, + 1.0, + &[ + ("SI::metre", 1.0), + ("SI::second", -1.0), + ("SI::second", 1.0), + ], + ), + ), + true, + ), + ( + m(0), + quantity(Magnitude::Integer(0), "x", term(0.0, 1.0, metre)), + false, + ), + ( + quantity(Magnitude::Integer(0), "x", term(0.0, 1.0, metre)), + quantity(Magnitude::Integer(0), "x", term(0.0, 1.0, metre)), + false, + ), + // Without a reduction, the unit as written is all there is to compare. + ( + quantity(Magnitude::Integer(1), "m", None), + quantity(Magnitude::Real(1.0), "m", None), + true, + ), + ( + quantity(Magnitude::Integer(1), "m", None), + quantity(Magnitude::Integer(100), "cm", None), + false, + ), + ( + quantity(Magnitude::Integer(1), "m", None), + quantity(Magnitude::Integer(1), "s", None), + false, + ), + (quantity(Magnitude::Integer(1), "m", None), m(1), false), + ( + Value::Set(Set::new(vec![m(1), km(2)]).unwrap()), + Value::Set(Set::new(vec![m(2000), cm(100)]).unwrap()), + true, + ), + ( + Value::Set(Set::new(vec![m(1), km(2)]).unwrap()), + Value::Set(Set::new(vec![m(2000), cm(1)]).unwrap()), + false, + ), + ]; + for (a, b, want) in cases { + assert_eq!(a.same_value(&b), want, "{a:?} vs {b:?}"); + assert_eq!(b.same_value(&a), want, "{b:?} vs {a:?}"); + } + + let component = |v| match v { + Value::Quantity(q) => q, + other => panic!("not a quantity: {other:?}"), + }; + let lengths = Set::new(vec![m(1), m(2)]).unwrap(); + assert!(lengths.contains(&cm(100))); + assert!(!lengths.contains(&cm(1))); + assert!(Set::new(vec![m(1), cm(100)]).is_err()); + assert_eq!(Set::new(vec![m(1), cm(1)]).unwrap().len(), 2); + assert!(Value::VectorQuantity( + VectorQuantity::new(vec![component(m(1)), component(km(1))]).unwrap() + ) + .same_value(&Value::VectorQuantity( + VectorQuantity::new(vec![component(cm(100)), component(m(1000))]).unwrap() + ))); + assert!(Value::TensorQuantity( + TensorQuantity::new(vec![1, 1], vec![component(m(1))]).unwrap() + ) + .same_value(&Value::TensorQuantity( + TensorQuantity::new(vec![1, 1], vec![component(cm(100))]).unwrap() + ))); + } + #[test] fn a_tensor_quantity_keeps_its_rank_shape_and_row_major_components() { let Ok(Value::TensorQuantity(cube)) = value_from_wire(tensor( diff --git a/docs/reference/wire-contract.md b/docs/reference/wire-contract.md index 08d6d0e77..a40407089 100644 --- a/docs/reference/wire-contract.md +++ b/docs/reference/wire-contract.md @@ -525,9 +525,9 @@ $ … /Evaluate -d '{"modelHash":"c409…1a4a","expression":"T::s.elements"}' and so are `1.5` and the complex `1.5 + 0.0i`, exactly — an Integer past 2^53 is not the Real it would round to; a Boolean is never a number; sequences in order; sets by membership; quantities by magnitude, converting commensurable units, so `1 [m]` and `100 [cm]` are one - member. The bundled clients' equality helpers judge numbers, Booleans, sequences and sets the - same way; a quantity they compare in its unit as written, so a set holding `1 [m]` and - `100 [cm]` decodes as two members in a client where the service would hold one. + member. The bundled clients' equality helpers judge the same way, converting a quantity + through its `unitTerm` (exactly, while the magnitude is an integer and the scale a whole + ratio); a quantity sent without a `unitTerm` they compare in its unit as written. - An empty set has no `elements` key (default omission). A `set` listing a member twice is refused on both sides, as is a `set` where the model wants a sequence's order or a sequence where it wants a set; a set flowing into an ordered parameter is read in canonical order. From add74f02673f19cb00327b89610ad368e6d70ca5 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 06:00:45 +0000 Subject: [PATCH 09/20] fix(clients): compare measurement references by reduction and enum literals by id Client value equality now matches the engine's: a MeasurementRef is one reduction at one scale however it is spelt or which declaration names it (SI::'m/s' is m/s, km/m is m/mm), except that a named unit of dimension one reduces to nothing and so is only its own declaration (rad is not sr); an EnumLiteral is its literal id alone, whatever enumeration id or display name accompanies it. Applies to Go, Python, Node, Rust and Java, with the set-membership consequences: equivalent references are one member and a set listing both is refused. The Python duplicate-set test asserts the ValueError raised at construction, where the check now lives. Co-Authored-By: jason.han --- client/opensysml/equal.go | 33 +++- client/opensysml/structured_internal_test.go | 77 +++++++++ .../java/org/openmbee/opensysml/Value.java | 37 ++++- .../openmbee/opensysml/PublicTypesTest.java | 70 ++++++++ clients/node/src/core/values.ts | 41 +++-- clients/node/test/values.test.ts | 75 +++++++++ clients/python/opensysml/values.py | 33 +++- clients/python/tests/test_set_tensor.py | 61 ++++++- clients/rust/opensysml/src/domain.rs | 156 +++++++++++++++++- docs/reference/wire-contract.md | 10 +- 10 files changed, 569 insertions(+), 24 deletions(-) diff --git a/client/opensysml/equal.go b/client/opensysml/equal.go index 574f152a7..26e325e2b 100644 --- a/client/opensysml/equal.go +++ b/client/opensysml/equal.go @@ -12,8 +12,11 @@ import ( // across the whole Int range; a Sequence's order counts and a Set's does not, // nor does a member it lists twice; a Quantity is one with any commensurable // quantity of the same magnitude over their base units, so 1 m is 100 cm, -// while one carrying no reduction is compared in its unit as written; a Null -// is one whatever its reason; and a nil Value equals only another nil. +// while one carrying no reduction is compared in its unit as written; a +// MeasurementRef is one reduction at one scale however spelt, except that a +// named unit of dimension one is only its own declaration (rad is not sr); an +// EnumLiteral is its LiteralID, whatever else describes it; a Null is one +// whatever its reason; and a nil Value equals only another nil. func Equal(a, b Value) bool { switch x := a.(type) { case nil: @@ -46,7 +49,10 @@ func Equal(a, b Value) bool { return ok && quantityEqual(x, y) case MeasurementRef: y, ok := b.(MeasurementRef) - return ok && x.Unit == y.Unit && x.UnitID == y.UnitID && unitTermEqual(x.Term, y.Term) + return ok && measurementRefEqual(x, y) + case EnumLiteral: + y, ok := b.(EnumLiteral) + return ok && x.LiteralID == y.LiteralID default: return a == b } @@ -150,6 +156,27 @@ func (q Quantity) baseMagnitude() float64 { return m * q.Term.ScaleNum / q.Term.ScaleDen } +// measurementRefEqual holds for one reduction at one scale (SI::'m/s' is m/s, km/m +// is m/mm); a named unit reducing to nothing is only the declaration it names. +func measurementRefEqual(a, b MeasurementRef) bool { + if a.Term == nil || b.Term == nil { + return a.Unit == b.Unit && a.UnitID == b.UnitID && unitTermEqual(a.Term, b.Term) + } + if !a.Term.same(*b.Term) { + return false + } + if len(a.Term.exponents()) == 0 && (a.UnitID != "" || b.UnitID != "") { + return a.UnitID == b.UnitID + } + return true +} + +// same reports one reduction: commensurable at one scale, however the ratio is written. +func (t UnitTerm) same(other UnitTerm) bool { + return t.commensurable(other) && !t.zeroScale() && !other.zeroScale() && + t.ScaleNum*other.ScaleDen == other.ScaleNum*t.ScaleDen +} + // exponents sums the term's factors by base unit, dropping those that cancel, // so two reductions over the same base units compare however they are listed. func (t UnitTerm) exponents() map[string]float64 { diff --git a/client/opensysml/structured_internal_test.go b/client/opensysml/structured_internal_test.go index ad1d856a5..f6143af32 100644 --- a/client/opensysml/structured_internal_test.go +++ b/client/opensysml/structured_internal_test.go @@ -385,6 +385,83 @@ func TestEqualJudgesQuantitiesAcrossUnits(t *testing.T) { } } +// A measurement reference is one reduction at one scale however it is spelt; +// a named unit of dimension one is only its own declaration. +func TestEqualJudgesMeasurementRefsByReduction(t *testing.T) { + metre := UnitFactor{UnitID: "SI::metre", Exponent: 1} + perMetre := UnitFactor{UnitID: "SI::metre", Exponent: -1} + perSecond := UnitFactor{UnitID: "SI::second", Exponent: -1} + ref := func(unit, id string, num, den float64, factors ...UnitFactor) MeasurementRef { + return MeasurementRef{Unit: unit, UnitID: id, Term: &UnitTerm{ScaleNum: num, ScaleDen: den, Factors: factors}} + } + namedSpeed := ref("SI::'m/s'", "SI::'m/s'", 1, 1, metre, perSecond) + composedSpeed := ref("m / s", "", 1, 1, perSecond, metre) + km := ref("km", "SI::kilometre", 1000, 1, metre) + rad := ref("rad", "SI::radian", 1, 1) + sr := ref("sr", "SI::steradian", 1, 1) + ratio := ref("m / m", "", 1, 1, metre, perMetre) + for name, tc := range map[string]struct { + a, b Value + want bool + }{ + "named and composed, factors reordered": {namedSpeed, composedSpeed, true}, + "aliases": {km, ref("km", "SI::km", 2000, 2, metre), true}, + "scale as ratio or decimal": {ref("km/m", "", 1000, 1), ref("m/mm", "", 1, 0.001), true}, + "unlike scale": {km, ref("m", "SI::metre", 1, 1, metre), false}, + "unlike dimension": {ref("m", "SI::metre", 1, 1, metre), ref("s", "SI::second", 1, 1, UnitFactor{UnitID: "SI::second", Exponent: 1}), false}, + "zero scale": {ref("x", "", 0, 1, metre), ref("x", "", 0, 1, metre), false}, + "named dimension one": {rad, sr, false}, + "named dimension one, spelt twice": {rad, ref("SI::rad", "SI::radian", 1, 1, metre, perMetre), true}, + "named and composed dimension one": {rad, ratio, false}, + "composed dimension one": {ratio, ref("", "", 1, 1), true}, + "no reduction, alike": {MeasurementRef{Unit: "m", UnitID: "SI::metre"}, MeasurementRef{Unit: "m", UnitID: "SI::metre"}, true}, + "no reduction on one side": {MeasurementRef{Unit: "m", UnitID: "SI::metre"}, ref("m", "SI::metre", 1, 1, metre), false}, + "set of references": {Set{namedSpeed, rad}, Set{rad, composedSpeed}, true}, + "set of references, one unlike": {Set{namedSpeed, rad}, Set{sr, composedSpeed}, false}, + } { + t.Run(name, func(t *testing.T) { + if got := Equal(tc.a, tc.b); got != tc.want { + t.Errorf("Equal(%#v, %#v) = %v, want %v", tc.a, tc.b, got, tc.want) + } + if got := Equal(tc.b, tc.a); got != tc.want { + t.Errorf("Equal(%#v, %#v) = %v, want %v", tc.b, tc.a, got, tc.want) + } + }) + } + if !(Set{namedSpeed}).Contains(composedSpeed) || (Set{rad}).Contains(sr) { + t.Error("membership does not follow Equal") + } + var status *StatusError + for name, set := range map[string]Set{"named and composed": {namedSpeed, composedSpeed}, "aliases": {km, ref("km", "SI::km", 2000, 2, metre)}} { + if _, err := valueToProto(set); !errors.As(err, &status) || status.Code != CodeInvalidArgument { + t.Errorf("valueToProto(%s) = %v, want an invalid-argument StatusError", name, err) + } + } + if _, err := valueToProto(Set{rad, sr}); err != nil { + t.Errorf("valueToProto(Set{rad, sr}) = %v", err) + } +} + +// An enumeration literal is its LiteralID; its name and enumeration describe it. +func TestEqualJudgesEnumLiteralsByID(t *testing.T) { + red := EnumLiteral{LiteralID: "D::Color::red", EnumerationID: "D::Color", Name: "Color::red"} + same := EnumLiteral{LiteralID: "D::Color::red", EnumerationID: "E::Palette", Name: "red"} + green := EnumLiteral{LiteralID: "D::Color::green", EnumerationID: "D::Color", Name: "Color::red"} + if !Equal(red, same) || Equal(red, green) || !(Set{red}).Contains(EnumLiteral{LiteralID: "D::Color::red"}) { + t.Errorf("Equal(red, same) = %v, Equal(red, green) = %v", Equal(red, same), Equal(red, green)) + } + if !Equal(Set{red, green}, Set{EnumLiteral{LiteralID: "D::Color::green"}, same}) { + t.Error("sets of literals compare by LiteralID") + } + var status *StatusError + if _, err := valueToProto(Set{red, same}); !errors.As(err, &status) || status.Code != CodeInvalidArgument { + t.Errorf("valueToProto(Set{red, same}) = %v, want an invalid-argument StatusError", err) + } + if _, err := valueToProto(Set{red, green}); err != nil { + t.Errorf("valueToProto(Set{red, green}) = %v", err) + } +} + // A set a caller assembles with a member listed twice, by Equal, is refused // before it is sent, as the service would refuse it; one whose members only // look alike is sent. diff --git a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/Value.java b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/Value.java index f5d9b006e..8fbdaf9e3 100644 --- a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/Value.java +++ b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/Value.java @@ -539,7 +539,10 @@ public Optional unit() { * value and a {@link ComplexValue} on the real axis is its real part, exactly across the whole * {@code long} range; a sequence's order counts and a set's does not; a quantity is compared * over its base units, so {@code 1 [m]} is {@code 100 [cm]} — exactly while integer magnitudes - * scale by whole factors — and one lacking a reduction is compared in its unit as written. + * scale by whole factors — and one lacking a reduction is compared in its unit as written; a + * {@link MeasurementRefValue} is one reduction at one scale however spelt, except that a named + * unit of dimension one is only its own declaration ({@code rad} is not {@code sr}); an {@link + * EnumerationValue} is its {@link EnumLiteral#literalId()}, whatever else describes it. * Every other arm compares as {@link Object#equals} does, which stays * structural: {@code new IntegerValue(1).equals(new RealValue(1.0))} is {@code false}. * @@ -570,9 +573,35 @@ default boolean sameValue(Value other) { return a.dimensions().equals(b.dimensions()) && sameQuantities(a.components(), b.components()); } + if (this instanceof MeasurementRefValue a && other instanceof MeasurementRefValue b) { + return measurementRefsEqual(a, b); + } + if (this instanceof EnumerationValue a && other instanceof EnumerationValue b) { + return a.literal().literalId().equals(b.literal().literalId()); + } return equals(other); } + // One reduction at one scale (SI::'m/s' is m/s, km/m is m/mm); a named unit + // reducing to nothing is only the declaration it names. + private static boolean measurementRefsEqual(MeasurementRefValue a, MeasurementRefValue b) { + if (!sameReduction(a.reduction(), b.reduction())) { + return false; + } + if (exponents(a.reduction()).isEmpty() && (a.unitId().isPresent() || b.unitId().isPresent())) { + return a.unitId().equals(b.unitId()); + } + return true; + } + + /** One reduction: commensurable at one scale, however the ratio is written. */ + private static boolean sameReduction(Quantity.UnitTerm x, Quantity.UnitTerm y) { + return exponents(x).equals(exponents(y)) + && !zeroScale(x) + && !zeroScale(y) + && x.scaleNumerator() * y.scaleDenominator() == y.scaleNumerator() * x.scaleDenominator(); + } + private static boolean numbersEqual(Value a, Value b) { Number x = onRealAxis(a); Number y = onRealAxis(b); @@ -705,6 +734,12 @@ private static int valueHash(Value value) { return Double.hashCode(quantity.quantity().magnitude().doubleValue() + 0.0) ^ quantity.quantity().unit().hashCode(); } + if (value instanceof MeasurementRefValue ref) { + return exponents(ref.reduction()).hashCode(); + } + if (value instanceof EnumerationValue literal) { + return literal.literal().literalId().hashCode(); + } if (value instanceof SetValue || value instanceof Sequence || value instanceof ArrayValue diff --git a/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/PublicTypesTest.java b/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/PublicTypesTest.java index 0339fb336..3d9377120 100644 --- a/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/PublicTypesTest.java +++ b/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/PublicTypesTest.java @@ -279,6 +279,76 @@ void quantitiesAreTheSameValueOverTheirBaseUnits() { new Value.TensorQuantityValue(List.of(1L, 1L), List.of(reduced(100L, "cm", 1.0, 100.0, metre))))); } + private static Value reference( + String unit, String unitId, double scaleNum, double scaleDen, Quantity.UnitFactor... factors) { + return new Value.MeasurementRefValue( + unit, + new Quantity.UnitTerm(scaleNum, scaleDen, List.of(factors)), + Optional.ofNullable(unitId)); + } + + @Test + void measurementRefsAreTheSameValueOverOneReduction() { + Quantity.UnitFactor metre = new Quantity.UnitFactor("SI::metre", 1.0); + Quantity.UnitFactor perMetre = new Quantity.UnitFactor("SI::metre", -1.0); + Quantity.UnitFactor perSecond = new Quantity.UnitFactor("SI::second", -1.0); + Value namedSpeed = reference("SI::'m/s'", "SI::'m/s'", 1.0, 1.0, metre, perSecond); + Value composedSpeed = reference("m / s", null, 1.0, 1.0, perSecond, metre); + Value km = reference("km", "SI::kilometre", 1000.0, 1.0, metre); + Value kmAlias = reference("km", "SI::km", 2000.0, 2.0, metre); + Value rad = reference("rad", "SI::radian", 1.0, 1.0); + Value sr = reference("sr", "SI::steradian", 1.0, 1.0); + Value ratio = reference("m / m", null, 1.0, 1.0, metre, perMetre); + + // One reduction at one scale, however it is spelt or which declaration names it. + assertTrue(namedSpeed.sameValue(composedSpeed)); + assertTrue(composedSpeed.sameValue(namedSpeed)); + assertTrue(km.sameValue(kmAlias)); + assertTrue(reference("km/m", null, 1000.0, 1.0).sameValue(reference("m/mm", null, 1.0, 0.001))); + assertFalse(km.sameValue(reference("m", "SI::metre", 1.0, 1.0, metre))); + assertFalse( + reference("m", "SI::metre", 1.0, 1.0, metre) + .sameValue(reference("s", "SI::second", 1.0, 1.0, new Quantity.UnitFactor("SI::second", 1.0)))); + Value zeroScale = reference("x", null, 0.0, 1.0, metre); + assertFalse(zeroScale.sameValue(zeroScale)); + + // A named unit of dimension one reduces to nothing, so it is only itself. + assertFalse(rad.sameValue(sr)); + assertTrue(rad.sameValue(reference("SI::rad", "SI::radian", 1.0, 1.0, metre, perMetre))); + assertFalse(rad.sameValue(ratio)); + assertTrue(ratio.sameValue(reference("", null, 1.0, 1.0))); + + // Membership, duplicate detection and set equality follow. + assertTrue(new Value.SetValue(List.of(namedSpeed)).contains(composedSpeed)); + assertFalse(new Value.SetValue(List.of(rad)).contains(sr)); + assertThrows( + IllegalArgumentException.class, () -> new Value.SetValue(List.of(namedSpeed, composedSpeed))); + assertThrows(IllegalArgumentException.class, () -> new Value.SetValue(List.of(km, kmAlias))); + assertEquals(2, new Value.SetValue(List.of(rad, sr)).size()); + Value.SetValue references = new Value.SetValue(List.of(namedSpeed, rad)); + Value.SetValue rewritten = new Value.SetValue(List.of(rad, composedSpeed)); + assertEquals(rewritten, references); + assertEquals(rewritten.hashCode(), references.hashCode()); + assertNotEquals(new Value.SetValue(List.of(sr, composedSpeed)), references); + } + + @Test + void enumerationLiteralsAreTheSameValueByLiteralId() { + Value red = new Value.EnumerationValue(new EnumLiteral("D::Color::red", "D::Color", "Color::red")); + Value same = new Value.EnumerationValue(new EnumLiteral("D::Color::red", "E::Palette", "red")); + Value green = new Value.EnumerationValue(new EnumLiteral("D::Color::green", "D::Color", "Color::red")); + assertTrue(red.sameValue(same)); + assertFalse(red.sameValue(green)); + assertTrue(new Value.SetValue(List.of(red)).contains(new Value.EnumerationValue(new EnumLiteral("D::Color::red", "", "")))); + assertThrows(IllegalArgumentException.class, () -> new Value.SetValue(List.of(red, same))); + assertEquals(2, new Value.SetValue(List.of(red, green)).size()); + Value.SetValue colours = new Value.SetValue(List.of(red, green)); + Value.SetValue rewritten = + new Value.SetValue(List.of(new Value.EnumerationValue(new EnumLiteral("D::Color::green", "", "")), same)); + assertEquals(rewritten, colours); + assertEquals(rewritten.hashCode(), colours.hashCode()); + } + @Test void aTensorQuantityIsShapedAndIndexedInRowMajorOrder() { List pascals = new ArrayList<>(); diff --git a/clients/node/src/core/values.ts b/clients/node/src/core/values.ts index b57690074..48923b103 100644 --- a/clients/node/src/core/values.ts +++ b/clients/node/src/core/values.ts @@ -580,7 +580,10 @@ function uniqueMembers(members: SysMLValue[]): SysMLValue[] { * value and a `complex` on the real axis is its real part, exactly across the * whole `int` range; a `sequence`'s order counts and a `set`'s does not, nor * does a member it lists twice; a quantity is the same over its base units; a - * `null` is the same whatever its reason. + * `measurementRef` is one reduction at one scale however spelt, except that a + * named unit of dimension one is only its own declaration (`rad` is not `sr`); + * an `enum` is its `literalId`, whatever else describes it; a `null` is the + * same whatever its reason. */ export function valuesEqual(a: SysMLValue, b: SysMLValue): boolean { switch (a.kind) { @@ -599,19 +602,9 @@ export function valuesEqual(a: SysMLValue, b: SysMLValue): boolean { case "quantity": return b.kind === "quantity" && quantitiesEqual(a, b); case "measurementRef": - return ( - b.kind === "measurementRef" && - a.unit === b.unit && - a.unitId === b.unitId && - unitTermsEqual(a.unitTerm, b.unitTerm) - ); + return b.kind === "measurementRef" && measurementRefsEqual(a, b); case "enum": - return ( - b.kind === "enum" && - a.value.literalId === b.value.literalId && - a.value.enumerationId === b.value.enumerationId && - a.value.name === b.value.name - ); + return b.kind === "enum" && a.value.literalId === b.value.literalId; case "array": return ( b.kind === "array" && @@ -763,6 +756,28 @@ function zeroScale(term: UnitFactorization): boolean { return term.scaleNum === 0 || term.scaleDen === 0; } +/** One reduction: commensurable at one scale, however the ratio is written. */ +function sameReduction(a: UnitFactorization, b: UnitFactorization): boolean { + return ( + commensurable(a, b) && + !zeroScale(a) && + !zeroScale(b) && + a.scaleNum * b.scaleDen === b.scaleNum * a.scaleDen + ); +} + +// One reduction at one scale (`SI::'m/s'` is `m/s`, `km/m` is `m/mm`); a named +// unit reducing to nothing is only the declaration it names. +function measurementRefsEqual(a: MeasurementRefValue, b: MeasurementRefValue): boolean { + if (!sameReduction(a.unitTerm, b.unitTerm)) { + return false; + } + if (exponents(a.unitTerm).size === 0 && (a.unitId !== undefined || b.unitId !== undefined)) { + return a.unitId === b.unitId; + } + return true; +} + function wholeScale(term: UnitFactorization): boolean { return Number.isInteger(term.scaleNum) && Number.isInteger(term.scaleDen); } diff --git a/clients/node/test/values.test.ts b/clients/node/test/values.test.ts index 976d2d49c..6e31f4500 100644 --- a/clients/node/test/values.test.ts +++ b/clients/node/test/values.test.ts @@ -493,6 +493,81 @@ test("valuesEqual judges quantities over their base units, as the service does", }); }); +test("valuesEqual judges measurement references by their reduction, as the service does", () => { + const ref = ( + unit: string, + unitId: string | undefined, + scaleNum: number, + scaleDen: number, + ...factors: [string, number][] + ): SysMLValue => ({ + kind: "measurementRef", + unit, + unitTerm: { scaleNum, scaleDen, factors: factors.map(([unitId, exponent]) => ({ unitId, exponent })) }, + ...(unitId === undefined ? {} : { unitId }), + }); + const namedSpeed = ref("SI::'m/s'", "SI::'m/s'", 1, 1, ["SI::metre", 1], ["SI::second", -1]); + const composedSpeed = ref("m / s", undefined, 1, 1, ["SI::second", -1], ["SI::metre", 1]); + const km = ref("km", "SI::kilometre", 1000, 1, ["SI::metre", 1]); + const rad = ref("rad", "SI::radian", 1, 1); + const sr = ref("sr", "SI::steradian", 1, 1); + const ratio = ref("m / m", undefined, 1, 1, ["SI::metre", 1], ["SI::metre", -1]); + const cases: [SysMLValue, SysMLValue, boolean][] = [ + [namedSpeed, composedSpeed, true], + [km, ref("km", "SI::km", 2000, 2, ["SI::metre", 1]), true], + [ref("km/m", undefined, 1000, 1), ref("m/mm", undefined, 1, 0.001), true], + [km, ref("m", "SI::metre", 1, 1, ["SI::metre", 1]), false], + [ref("m", "SI::metre", 1, 1, ["SI::metre", 1]), ref("s", "SI::second", 1, 1, ["SI::second", 1]), false], + [ref("x", undefined, 0, 1, ["SI::metre", 1]), ref("x", undefined, 0, 1, ["SI::metre", 1]), false], + [rad, sr, false], + [rad, ref("SI::rad", "SI::radian", 1, 1, ["SI::metre", 1], ["SI::metre", -1]), true], + [rad, ratio, false], + [ratio, ref("", undefined, 1, 1), true], + [{ kind: "set", elements: [namedSpeed, rad] }, { kind: "set", elements: [rad, composedSpeed] }, true], + [{ kind: "set", elements: [namedSpeed, rad] }, { kind: "set", elements: [sr, composedSpeed] }, false], + ]; + for (const [a, b, want] of cases) { + assert.equal(valuesEqual(a, b), want, `${formatValue(a)} vs ${formatValue(b)}`); + assert.equal(valuesEqual(b, a), want, `${formatValue(b)} vs ${formatValue(a)}`); + } + for (const twice of [[namedSpeed, composedSpeed], [km, ref("km", "SI::km", 2000, 2, ["SI::metre", 1])]]) { + assert.throws(() => encodeValue({ kind: "set", elements: twice }), { + name: "MalformedValueError", + message: /^a set lists a member twice: /, + }); + } + assert.throws(() => decodeValue(setOf(encodeValue(rad), encodeValue(ref("SI::rad", "SI::radian", 1, 1)))), { + name: "MalformedValueError", + message: /^a set lists a member twice: /, + }); + const sent = decodeValue(encodeValue({ kind: "set", elements: [rad, sr] })); + assert.equal(sent.kind, "set"); + assert.equal(sent.elements.length, 2); +}); + +test("valuesEqual judges enumeration literals by their literalId, as the service does", () => { + const lit = (literalId: string, enumerationId = "", name = ""): SysMLValue => ({ + kind: "enum", + value: { literalId, enumerationId, name }, + }); + const red = lit("D::Color::red", "D::Color", "Color::red"); + const same = lit("D::Color::red", "E::Palette", "red"); + const green = lit("D::Color::green", "D::Color", "Color::red"); + assert.equal(valuesEqual(red, same), true); + assert.equal(valuesEqual(red, green), false); + assert.equal( + valuesEqual({ kind: "set", elements: [red, green] }, { kind: "set", elements: [lit("D::Color::green"), same] }), + true, + ); + assert.throws(() => encodeValue({ kind: "set", elements: [red, same] }), { + name: "MalformedValueError", + message: /^a set lists a member twice: /, + }); + const sent = decodeValue(encodeValue({ kind: "set", elements: [red, green] })); + assert.equal(sent.kind, "set"); + assert.equal(sent.elements.length, 2); +}); + test("a set assembled with a member listed twice is refused before it is sent", () => { const i = (value: bigint): SysMLValue => ({ kind: "int", value }); const r = (value: number): SysMLValue => ({ kind: "real", value }); diff --git a/clients/python/opensysml/values.py b/clients/python/opensysml/values.py index af44df44f..3d23d6a68 100644 --- a/clients/python/opensysml/values.py +++ b/clients/python/opensysml/values.py @@ -138,6 +138,15 @@ def commensurable(self, other: "Unit") -> bool: """Whether a magnitude in this unit converts into ``other``.""" return self.exponents() == other.exponents() + def same_reduction(self, other: "Unit") -> bool: + """Whether both reduce to one thing at one scale, however the ratio is written.""" + return ( + self.commensurable(other) + and not self.zero_scale + and not other.zero_scale + and self.scale_num * other.scale_den == other.scale_num * self.scale_den + ) + def reduction(self) -> str: """The reduction as text ("1000/3600·SI::m·SI::s^-1"), for a diagnostic.""" parts = [] @@ -367,7 +376,7 @@ def __repr__(self) -> str: return f"Quantity({self.magnitude!r}, {self.unit!r})" -@dataclass(frozen=True) +@dataclass(frozen=True, eq=False) class MeasurementRef: """A measurement unit held as a value by itself, with no magnitude. @@ -376,6 +385,11 @@ class MeasurementRef: and what ``ConvertQuantity`` takes as its target. It carries the unit as a :class:`Quantity` does — text and reduction — plus the declaration it names. + Two references are equal when they are one reduction at one scale, however + spelt: ``SI::'m/s'`` is ``m / s`` and ``km / m`` is ``m / mm``. A named unit + of dimension one reduces to nothing, so it is only its own declaration: + ``rad`` is not ``sr``. + Attributes: unit (Unit): The unit as written and its reduction to base units unit_id (str): FQN of the one unit declaration the reference names @@ -421,6 +435,23 @@ def to_pb(self) -> "sysml_pb2.MeasurementRef": unit=self.unit.text, unit_term=self.unit.to_pb(), unit_id=self.unit_id ) + def __eq__(self, other: object) -> bool: + if not isinstance(other, MeasurementRef): + return NotImplemented + if not (self.unit.reduced and other.unit.reduced): + return self.unit == other.unit and self.unit_id == other.unit_id + if not self.unit.same_reduction(other.unit): + return False + if self.unit.dimensionless and (self.unit_id or other.unit_id): + return self.unit_id == other.unit_id + return True + + def __hash__(self) -> int: + if not self.unit.reduced: + return hash((self.unit, self.unit_id)) + exponents = tuple(sorted(self.unit.exponents().items())) + return hash((exponents, self.unit_id if not exponents else "")) + def __str__(self) -> str: return str(self.unit) diff --git a/clients/python/tests/test_set_tensor.py b/clients/python/tests/test_set_tensor.py index 1cde8ec46..1ea63ecec 100644 --- a/clients/python/tests/test_set_tensor.py +++ b/clients/python/tests/test_set_tensor.py @@ -22,10 +22,12 @@ MissingCapabilityError, ) from opensysml.connection import Connection +from opensysml.enumeration import EnumLiteral from opensysml.errors import ExecutionError, UnsupportedValueError from opensysml.proto import sysml_pb2 from opensysml.values import ( Array, + MeasurementRef, Quantity, SetValue, TensorQuantity, @@ -223,6 +225,61 @@ def test_quantities_are_the_same_value_over_their_base_units(): assert TensorQuantity((1, 1), (m,)) == TensorQuantity((1, 1), (cm,)) +def ref(text, unit_id="", scale_num=1.0, scale_den=1.0, factors=(("SI::metre", 1.0),)): + unit = Unit(text, scale_num, scale_den, tuple(UnitFactor(*f) for f in factors), reduction_given=True) + return MeasurementRef(unit, unit_id) + + +def test_measurement_refs_are_the_same_value_over_one_reduction(): + """As the service judges them: one reduction at one scale, however spelt.""" + speed = (("SI::metre", 1.0), ("SI::second", -1.0)) + named = ref("SI::'m/s'", "SI::'m/s'", factors=speed) + composed = ref("m / s", factors=reversed(speed)) + assert named == composed and hash(named) == hash(composed) + assert ref("km/m", scale_num=1000.0) == ref("m/mm", scale_num=1.0, scale_den=0.001) + assert ref("km", "SI::kilometre", 1000.0) == ref("km", "SI::km", 2000.0, 2.0) + assert ref("km", "SI::kilometre", 1000.0) != ref("m", "SI::metre") + assert ref("m", "SI::metre") != ref("s", "SI::second", factors=(("SI::second", 1.0),)) + assert ref("x", scale_num=0.0) != ref("x", scale_num=0.0) + # A named unit of dimension one reduces to nothing, so it is only itself. + rad, sr = ref("rad", "SI::radian", factors=()), ref("sr", "SI::steradian", factors=()) + assert rad != sr and rad == ref("SI::rad", "SI::radian", factors=(("SI::metre", 1.0), ("SI::metre", -1.0))) + assert ref("m/m", factors=(("SI::metre", 1.0), ("SI::metre", -1.0))) == ref("", factors=()) + assert rad != ref("m/m", factors=(("SI::metre", 1.0), ("SI::metre", -1.0))) + # One named without its reduction compares as written. + assert MeasurementRef(Unit("m"), "SI::metre") == MeasurementRef(Unit("m"), "SI::metre") + assert MeasurementRef(Unit("m"), "SI::metre") != ref("m", "SI::metre") + + # Membership, duplicate detection and set equality follow. + assert composed in SetValue((named,)) and sr not in SetValue((rad,)) + for twice in ((named, composed), (rad, ref("SI::rad", "SI::radian", factors=()))): + with pytest.raises(ValueError, match="set lists a member twice"): + SetValue(twice) + with pytest.raises(UnsupportedValueError, match="malformed set: set lists a member twice"): + value_to_python(pb_set( + sysml_pb2.Value(measurement_ref=named.to_pb()), + sysml_pb2.Value(measurement_ref=composed.to_pb()), + )) + assert len(SetValue((rad, sr))) == 2 + assert SetValue((named, rad)) == SetValue((rad, composed)) + + +def test_enumeration_literals_are_the_same_value_by_literal_id(): + red = EnumLiteral("D::Color::red", "D::Color", "Color::red") + same = EnumLiteral("D::Color::red", "E::Palette", "red") + assert red == same and EnumLiteral("D::Color::red") in SetValue((red,)) + assert red != EnumLiteral("D::Color::green", "D::Color", "Color::red") + with pytest.raises(ValueError, match="set lists a member twice"): + SetValue((red, same)) + with pytest.raises(UnsupportedValueError, match="malformed set: set lists a member twice"): + value_to_python(pb_set( + sysml_pb2.Value(enum_literal=sysml_pb2.EnumLiteral( + literal_id="D::Color::red", enumeration_id="D::Color", name="Color::red")), + sysml_pb2.Value(enum_literal=sysml_pb2.EnumLiteral(literal_id="D::Color::red")), + )) + assert SetValue((red, EnumLiteral("D::Color::green"))) == SetValue((EnumLiteral("D::Color::green", name="g"), same)) + + @pytest.mark.parametrize("elements, expected", [ ((sysml_pb2.Value(bool_value=True), pb_int(1)), SetValue((True, 1))), ((pb_int(1), sysml_pb2.Value(real_value=1.5)), SetValue((1, 1.5))), @@ -516,5 +573,5 @@ def test_a_tensor_reads_with_its_rank_and_indexes_by_shape(self): def test_a_set_sent_in_any_order_is_read_as_its_elements(self): assert self.conn.calc("W::sizeOf", self.model.hash, arguments=[{3, 1, 2}]).value == 3 - with pytest.raises(ExecutionError, match="set element is repeated"): - self.conn.calc("W::sizeOf", self.model.hash, arguments=[SetValue((1, 1))]) + with pytest.raises(ValueError, match="set lists a member twice"): + SetValue((1, 1)) diff --git a/clients/rust/opensysml/src/domain.rs b/clients/rust/opensysml/src/domain.rs index a1bebd28e..5d9630d2e 100644 --- a/clients/rust/opensysml/src/domain.rs +++ b/clients/rust/opensysml/src/domain.rs @@ -582,7 +582,11 @@ impl Value { /// sequence's order counts and a set's does not; a quantity is compared /// over its base units, so `1 [m]` is `100 [cm]` — exactly while integer /// magnitudes scale by whole factors — and one lacking a reduction is - /// compared in its unit as written. Every other arm compares as `==` does. + /// compared in its unit as written; a measurement reference is one + /// reduction at one scale however spelt, except that a named unit of + /// dimension one is only its own declaration (`rad` is not `sr`); an + /// enumeration literal is its `literal_id`, whatever else describes it. + /// Every other arm compares as `==` does. pub fn same_value(&self, other: &Value) -> bool { match (self, other) { (Value::Integer(_) | Value::Real(_) | Value::Complex(_), _) => { @@ -606,11 +610,33 @@ impl Value { (Value::TensorQuantity(a), Value::TensorQuantity(b)) => { a.dimensions == b.dimensions && components_equal(&a.components, &b.components) } + (Value::MeasurementRef(a), Value::MeasurementRef(b)) => measurement_refs_equal(a, b), + (Value::EnumLiteral(a), Value::EnumLiteral(b)) => a.literal_id == b.literal_id, _ => self == other, } } } +// One reduction at one scale (`SI::'m/s'` is `m/s`, `km/m` is `m/mm`); a named +// unit reducing to nothing is only the declaration it names. +fn measurement_refs_equal(a: &MeasurementRef, b: &MeasurementRef) -> bool { + if !same_reduction(&a.unit_term, &b.unit_term) { + return false; + } + if exponents(&a.unit_term).is_empty() && (a.unit_id.is_some() || b.unit_id.is_some()) { + return a.unit_id == b.unit_id; + } + true +} + +// One reduction: commensurable at one scale, however the ratio is written. +fn same_reduction(a: &UnitTerm, b: &UnitTerm) -> bool { + commensurable(a, b) + && !zero_scale(a) + && !zero_scale(b) + && a.scale_num * b.scale_den == b.scale_num * a.scale_den +} + fn numbers_equal(a: &Value, b: &Value) -> bool { let on_axis = |v: &Value| match *v { Value::Complex(z) if z.imaginary == 0.0 => Some(Magnitude::Real(z.real)), @@ -1938,6 +1964,134 @@ mod tests { ))); } + #[test] + fn same_value_judges_measurement_refs_by_reduction() { + let reference = + |unit: &str, unit_id: Option<&str>, scale_num, scale_den, factors: &[(&str, f64)]| { + Value::MeasurementRef(MeasurementRef { + unit: unit.to_owned(), + unit_id: unit_id.map(str::to_owned), + unit_term: UnitTerm { + scale_num, + scale_den, + factors: factors + .iter() + .map(|(unit_id, exponent)| UnitFactor { + unit_id: (*unit_id).to_owned(), + exponent: *exponent, + }) + .collect(), + }, + }) + }; + let metre = &[("SI::metre", 1.0)][..]; + let ratio_factors = &[("SI::metre", 1.0), ("SI::metre", -1.0)][..]; + let named_speed = || { + reference( + "SI::'m/s'", + Some("SI::'m/s'"), + 1.0, + 1.0, + &[("SI::metre", 1.0), ("SI::second", -1.0)], + ) + }; + let composed_speed = || { + reference( + "m / s", + None, + 1.0, + 1.0, + &[("SI::second", -1.0), ("SI::metre", 1.0)], + ) + }; + let km = || reference("km", Some("SI::kilometre"), 1000.0, 1.0, metre); + let km_alias = || reference("km", Some("SI::km"), 2000.0, 2.0, metre); + let rad = || reference("rad", Some("SI::radian"), 1.0, 1.0, &[]); + let sr = || reference("sr", Some("SI::steradian"), 1.0, 1.0, &[]); + let ratio = || reference("m / m", None, 1.0, 1.0, ratio_factors); + let set = |members| Value::Set(Set::new(members).unwrap()); + let cases = [ + (named_speed(), composed_speed(), true), + (km(), km_alias(), true), + ( + reference("km/m", None, 1000.0, 1.0, &[]), + reference("m/mm", None, 1.0, 0.001, &[]), + true, + ), + ( + km(), + reference("m", Some("SI::metre"), 1.0, 1.0, metre), + false, + ), + ( + reference("m", Some("SI::metre"), 1.0, 1.0, metre), + reference("s", Some("SI::second"), 1.0, 1.0, &[("SI::second", 1.0)]), + false, + ), + ( + reference("x", None, 0.0, 1.0, metre), + reference("x", None, 0.0, 1.0, metre), + false, + ), + (rad(), sr(), false), + ( + rad(), + reference("SI::rad", Some("SI::radian"), 1.0, 1.0, ratio_factors), + true, + ), + (rad(), ratio(), false), + (ratio(), reference("", None, 1.0, 1.0, &[]), true), + ( + set(vec![named_speed(), rad()]), + set(vec![rad(), composed_speed()]), + true, + ), + ( + set(vec![named_speed(), rad()]), + set(vec![sr(), composed_speed()]), + false, + ), + ]; + for (a, b, want) in cases { + assert_eq!(a.same_value(&b), want, "{a:?} vs {b:?}"); + assert_eq!(b.same_value(&a), want, "{b:?} vs {a:?}"); + } + + assert!(Set::new(vec![named_speed()]) + .unwrap() + .contains(&composed_speed())); + assert!(!Set::new(vec![rad()]).unwrap().contains(&sr())); + assert!(Set::new(vec![named_speed(), composed_speed()]).is_err()); + assert!(Set::new(vec![km(), km_alias()]).is_err()); + assert_eq!(Set::new(vec![rad(), sr()]).unwrap().len(), 2); + } + + #[test] + fn same_value_judges_enum_literals_by_literal_id() { + let literal = |literal_id: &str, enumeration_id: &str, name: &str| { + Value::EnumLiteral(EnumLiteral { + literal_id: literal_id.to_owned(), + enumeration_id: enumeration_id.to_owned(), + name: name.to_owned(), + }) + }; + let red = || literal("D::Color::red", "D::Color", "Color::red"); + let same = || literal("D::Color::red", "E::Palette", "red"); + let green = || literal("D::Color::green", "D::Color", "Color::red"); + assert!(red().same_value(&same())); + assert!(!red().same_value(&green())); + assert!(Set::new(vec![red()]) + .unwrap() + .contains(&literal("D::Color::red", "", ""))); + assert!( + Value::Set(Set::new(vec![red(), green()]).unwrap()).same_value(&Value::Set( + Set::new(vec![literal("D::Color::green", "", ""), same()]).unwrap() + )) + ); + assert!(Set::new(vec![red(), same()]).is_err()); + assert_eq!(Set::new(vec![red(), green()]).unwrap().len(), 2); + } + #[test] fn a_tensor_quantity_keeps_its_rank_shape_and_row_major_components() { let Ok(Value::TensorQuantity(cube)) = value_from_wire(tensor( diff --git a/docs/reference/wire-contract.md b/docs/reference/wire-contract.md index a40407089..a55d22147 100644 --- a/docs/reference/wire-contract.md +++ b/docs/reference/wire-contract.md @@ -525,9 +525,13 @@ $ … /Evaluate -d '{"modelHash":"c409…1a4a","expression":"T::s.elements"}' and so are `1.5` and the complex `1.5 + 0.0i`, exactly — an Integer past 2^53 is not the Real it would round to; a Boolean is never a number; sequences in order; sets by membership; quantities by magnitude, converting commensurable units, so `1 [m]` and `100 [cm]` are one - member. The bundled clients' equality helpers judge the same way, converting a quantity - through its `unitTerm` (exactly, while the magnitude is an integer and the scale a whole - ratio); a quantity sent without a `unitTerm` they compare in its unit as written. + member; a `measurementRef` by its reduction at its scale, however it is spelt or which + declaration names it (`SI::'m/s'` and `m / s` are one member, `km / m` and `m / mm` too), + except that a named unit of dimension one reduces to nothing and so is only its own + declaration (`rad` is not `sr`); an `enumLiteral` by its `literalId` alone. The bundled + clients' equality helpers judge the same way, converting a quantity through its `unitTerm` + (exactly, while the magnitude is an integer and the scale a whole ratio); a quantity sent + without a `unitTerm` they compare in its unit as written. - An empty set has no `elements` key (default omission). A `set` listing a member twice is refused on both sides, as is a `set` where the model wants a sequence's order or a sequence where it wants a set; a set flowing into an ordered parameter is read in canonical order. From c3710a1c5ac1605340b5694961f2f8e617f29fd1 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 06:10:09 +0000 Subject: [PATCH 10/20] fix(clients): keep Python set membership aligned with the engine for instance refs and arrays An unresolved instance reference decoded to a plain int, so a set holding instance 1 and Integer 1 was rejected as a duplicate; Array equality compared elements with Python ==, so nested true and 1 collided. Unresolved references are now an InstanceRef (an int subclass sent back as instance_id), and Array compares its elements as same_value does. Co-Authored-By: jason.han --- clients/python/opensysml/__init__.py | 6 ++-- clients/python/opensysml/connection.py | 7 +++-- clients/python/opensysml/instance.py | 6 ++-- clients/python/opensysml/values.py | 42 +++++++++++++++++++++---- clients/python/tests/test_set_tensor.py | 32 +++++++++++++++++++ 5 files changed, 79 insertions(+), 14 deletions(-) diff --git a/clients/python/opensysml/__init__.py b/clients/python/opensysml/__init__.py index a13c656f9..bb24fcaae 100644 --- a/clients/python/opensysml/__init__.py +++ b/clients/python/opensysml/__init__.py @@ -24,8 +24,8 @@ ) from opensysml.capabilities import MissingCapabilityError, ServerInfo from opensysml.values import ( - UNSET, Array, Function, MeasurementRef, SetValue, TensorQuantity, UnsetType, Vector, - VectorQuantity, + UNSET, Array, Function, InstanceRef, MeasurementRef, SetValue, TensorQuantity, UnsetType, + Vector, VectorQuantity, ) from opensysml.verdict import ( AnalysisResult, CalcResult, SweepRow, SweepTable, Verdict, VerificationVerdict, @@ -60,7 +60,7 @@ "ServerInfo", "UNSET", "UnsetType", "Array", "Vector", "VectorQuantity", "MeasurementRef", "Function", "SetValue", - "TensorQuantity", + "TensorQuantity", "InstanceRef", "Conversion", "FORMAT_SYSML", "FORMAT_TURTLE", "format_of_path", "ExperimentalFeatureWarning", "is_experimental", "Editor", "EditResult", "AppliedEdit", diff --git a/clients/python/opensysml/connection.py b/clients/python/opensysml/connection.py index 3717836fa..b5aaf57a2 100644 --- a/clients/python/opensysml/connection.py +++ b/clients/python/opensysml/connection.py @@ -64,6 +64,7 @@ from opensysml.values import ( Array, Function, + InstanceRef, MeasurementRef, Quantity, SetValue, @@ -1786,6 +1787,8 @@ def _python_to_value(self, py_value): if isinstance(py_value, bool): return sysml_pb2.Value(bool_value=py_value) + elif isinstance(py_value, InstanceRef): + return sysml_pb2.Value(instance_id=int(py_value)) elif isinstance(py_value, int): return sysml_pb2.Value(int_value=py_value) elif isinstance(py_value, float): @@ -1842,8 +1845,8 @@ def _python_to_value(self, py_value): def _value_to_python(self, pb_value): """Convert protobuf Value to Python type. - Instance references outside an Instantiate response are returned as - their integer id; there is no instance graph to resolve them against. + Instance references outside an Instantiate response are returned as an + :class:`InstanceRef`; there is no instance graph to resolve them against. """ return value_to_python(pb_value) diff --git a/clients/python/opensysml/instance.py b/clients/python/opensysml/instance.py index ce25e8bb0..0605d0a98 100644 --- a/clients/python/opensysml/instance.py +++ b/clients/python/opensysml/instance.py @@ -1,7 +1,7 @@ """Instance class wrapping runtime-materialized objects.""" from opensysml.errors import FeatureValueError -from opensysml.values import UNSET, feature_value_to_python, value_to_python +from opensysml.values import UNSET, InstanceRef, feature_value_to_python, value_to_python class Instance: @@ -96,13 +96,13 @@ def get(self, feature_name, default=None): return feature_value_to_python(feature_name, pb_value, self._resolve_instance) def _resolve_instance(self, instance_id): - """Resolve an instance id to an Instance, or the bare id if unreachable.""" + """Resolve an instance id to an Instance, or an InstanceRef if unreachable.""" wrapper = self._wrappers.get(instance_id) if wrapper is not None: return wrapper pb_child = self._graph.get(instance_id) if pb_child is None: - return instance_id + return InstanceRef(instance_id) return Instance(pb_child, self._graph, self._wrappers) def __getattr__(self, name): diff --git a/clients/python/opensysml/values.py b/clients/python/opensysml/values.py index 3d23d6a68..0c136164c 100644 --- a/clients/python/opensysml/values.py +++ b/clients/python/opensysml/values.py @@ -521,7 +521,7 @@ def _format_number(value: Magnitude) -> str: return f"{value:g}" if isinstance(value, float) else str(value) -@dataclass(frozen=True) +@dataclass(frozen=True, eq=False) class Array: """A multidimensional array: its shape and its elements in row-major order. @@ -530,7 +530,8 @@ class Array: value a feature can hold, an :class:`Array` or a :class:`Quantity` included; ``elements`` holds them flattened as the model states them, with the last dimension varying fastest, and :meth:`nested` unfolds them. A rank-0 array - holds exactly one element. + holds exactly one element. Two arrays are equal when their shapes agree and + each element is the :func:`same_value` as its counterpart. Attributes: dimensions (tuple[int, ...]): Extent of each dimension, all positive @@ -569,6 +570,16 @@ def __len__(self) -> int: def __iter__(self) -> Iterator[Any]: return iter(self.elements) + def __eq__(self, other: object) -> bool: + if not isinstance(other, Array): + return NotImplemented + return self.dimensions == other.dimensions and all( + same_value(x, y) for x, y in zip(self.elements, other.elements) + ) + + def __hash__(self) -> int: + return hash(self.dimensions) + def __getitem__(self, index: int | Tuple[int, ...]) -> Any: """The element at a row-major position, or at a full multi-index.""" if isinstance(index, tuple): @@ -818,15 +829,33 @@ def same_value(a: Any, b: Any) -> bool: """Whether two decoded values are the same value, as :class:`SetValue` membership judges it. ``==`` decides, except that a ``bool`` is never a number — ``True`` and ``1`` - are distinct values in a model — in a nested ``list`` too. + are distinct values in a model — and an :class:`InstanceRef` is never an + Integer, in a nested ``list`` or :class:`Array` too. """ if isinstance(a, bool) or isinstance(b, bool): return isinstance(a, bool) and isinstance(b, bool) and a == b + if isinstance(a, InstanceRef) or isinstance(b, InstanceRef): + return isinstance(a, InstanceRef) and isinstance(b, InstanceRef) and a == b if isinstance(a, list) and isinstance(b, list): return len(a) == len(b) and all(same_value(x, y) for x, y in zip(a, b)) return a == b +class InstanceRef(int): + """A reference to an instance the client has no instance graph to resolve. + + It is the instance's integer id, so it reads and compares as one where an + ``int`` is expected; but it is not the Integer of that value in a model, so + :func:`same_value` keeps the two apart, and sent back it is again an + instance reference. + """ + + __slots__ = () + + def __repr__(self) -> str: + return f"InstanceRef({int(self)})" + + @dataclass(frozen=True) class TensorQuantity: """A tensor quantity of any rank: its shape and one quantity per component. @@ -983,7 +1012,8 @@ def value_to_python(pb_value, resolve_instance=None): Args: pb_value: sysml_pb2.Value message resolve_instance: optional callable mapping an instance id to an object; - when omitted, instance references are returned as their integer id. + when omitted, instance references are returned as an + :class:`InstanceRef`, an ``int`` holding the id. Returns: int, float, complex, bool, str, list, None, :data:`UNSET`, @@ -991,7 +1021,7 @@ def value_to_python(pb_value, resolve_instance=None): :class:`Quantity`, a :class:`MeasurementRef`, a :class:`Function`, an :class:`Array`, a :class:`Vector`, a :class:`VectorQuantity`, a :class:`SetValue`, a :class:`TensorQuantity`, an :class:`~opensysml.enumeration.EnumLiteral`, - or the resolved instance object. A Complex is one ``complex``, never two + an :class:`InstanceRef`, or the resolved instance object. A Complex is one ``complex``, never two floats; a Vector is one :class:`Vector`, never a list of numbers; a set is one :class:`SetValue`, never a list. @@ -1018,7 +1048,7 @@ def value_to_python(pb_value, resolve_instance=None): return Function.from_pb(pb_value.function) if kind == 'instance_id': if resolve_instance is None: - return pb_value.instance_id + return InstanceRef(pb_value.instance_id) return resolve_instance(pb_value.instance_id) if kind == 'sequence': return [value_to_python(v, resolve_instance) for v in pb_value.sequence.elements] diff --git a/clients/python/tests/test_set_tensor.py b/clients/python/tests/test_set_tensor.py index 1ea63ecec..e2358b7e6 100644 --- a/clients/python/tests/test_set_tensor.py +++ b/clients/python/tests/test_set_tensor.py @@ -27,6 +27,7 @@ from opensysml.proto import sysml_pb2 from opensysml.values import ( Array, + InstanceRef, MeasurementRef, Quantity, SetValue, @@ -148,6 +149,14 @@ def pb_seq(*elements): return sysml_pb2.Value(sequence=sysml_pb2.ValueSequence(elements=list(elements))) +def pb_array(dimensions, *elements): + return sysml_pb2.Value(array=sysml_pb2.Array(dimensions=list(dimensions), elements=list(elements))) + + +def pb_instance(instance_id): + return sysml_pb2.Value(instance_id=instance_id) + + @pytest.mark.parametrize("elements", [ (pb_int(1), pb_int(2), pb_int(1)), (pb_int(1), sysml_pb2.Value(real_value=1.0)), @@ -159,6 +168,8 @@ def pb_seq(*elements): (pb_set(), pb_set()), (sysml_pb2.Value(quantity=pb_pascal(1.0)), sysml_pb2.Value(quantity=pb_pascal(1.0))), (sysml_pb2.Value(bool_value=True), pb_seq(), sysml_pb2.Value(bool_value=True)), + (pb_instance(1), pb_int(2), pb_instance(1)), + (pb_array((2,), pb_int(1), pb_int(2)), pb_array((2,), pb_int(1), sysml_pb2.Value(real_value=2.0))), ]) def test_a_set_listing_a_member_twice_is_malformed(elements): with pytest.raises(UnsupportedValueError, match="malformed set: set lists a member twice"): @@ -176,6 +187,8 @@ def test_a_set_listing_a_member_twice_is_malformed(elements): (Quantity(1.0, Unit("m", factors=(UnitFactor("SI::metre", 1.0),))), Quantity(1, Unit("m", factors=(UnitFactor("SI::metre", 1.0),)))), (True, [], True), + (InstanceRef(1), 2, InstanceRef(1)), + (Array((2,), (1, 2)), Array((2,), (1, 2.0))), ]) def test_a_set_is_never_assembled_with_a_member_twice(elements): with pytest.raises(ValueError, match="set lists a member twice"): @@ -289,6 +302,11 @@ def test_enumeration_literals_are_the_same_value_by_literal_id(): ((pb_seq(pb_int(1)), pb_set(pb_int(1))), SetValue(([1], SetValue((1,))))), ((pb_seq(sysml_pb2.Value(bool_value=True)), pb_seq(pb_int(1))), SetValue(([True], [1]))), ((pb_set(), pb_set(pb_set())), SetValue((SetValue(), SetValue((SetValue(),))))), + ((pb_instance(1), pb_int(1)), SetValue((InstanceRef(1), 1))), + ((pb_array((1,), sysml_pb2.Value(bool_value=True)), pb_array((1,), pb_int(1))), + SetValue((Array((1,), (True,)), Array((1,), (1,))))), + ((pb_array((1,), pb_instance(1)), pb_array((1,), pb_int(1))), + SetValue((Array((1,), (InstanceRef(1),)), Array((1,), (1,))))), ]) def test_members_that_only_look_alike_are_distinct(elements, expected): got = value_to_python(pb_set(*elements)) @@ -297,6 +315,20 @@ def test_members_that_only_look_alike_are_distinct(elements, expected): assert 1 not in SetValue((True,)) and True not in SetValue((1,)) +def test_an_unresolved_instance_reference_is_its_id_but_not_an_integer(): + ref = value_to_python(pb_instance(7)) + assert isinstance(ref, InstanceRef) and ref == 7 and repr(ref) == "InstanceRef(7)" + assert ref in SetValue((InstanceRef(7),)) and ref not in SetValue((7,)) + assert Array((1,), (ref,)) != Array((1,), (7,)) + assert Array((2,), (True, 1)) != Array((2,), (1, 1)) + assert Array((2,), (1, 2)) == Array((2,), (1, 2.0)) + + conn = make_connection(Mock(), CURRENT) + sent = conn._python_to_value(SetValue((ref, 7))) + assert [e.WhichOneof("kind") for e in sent.set.elements] == ["instance_id", "int_value"] + assert sent.set.elements[0].instance_id == 7 + + def test_a_set_survives_the_wire_bytes(): value = pb_set(pb_int(1), sysml_pb2.Value(string_value="a"), pb_set()) again = sysml_pb2.Value() From 76a68c5c843afa5aba0f2b4b06a363f09815f29b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 06:17:37 +0000 Subject: [PATCH 11/20] fix(clients): keep Python instance references out of numeric values InstanceRef is no longer an int subclass, so Vector, Quantity, Array and TensorQuantity dimensions and typed.as_int/as_float/as_complex refuse it instead of encoding it as int_value; Connection still sends it as instance_id. Co-Authored-By: jason.han --- clients/python/opensysml/connection.py | 2 +- clients/python/opensysml/values.py | 26 +++++++++++------------ clients/python/tests/test_instance.py | 8 ++++--- clients/python/tests/test_set_tensor.py | 28 ++++++++++++++++++++++--- clients/python/tests/test_unset.py | 4 ++-- 5 files changed, 46 insertions(+), 22 deletions(-) diff --git a/clients/python/opensysml/connection.py b/clients/python/opensysml/connection.py index b5aaf57a2..e5f2fbd19 100644 --- a/clients/python/opensysml/connection.py +++ b/clients/python/opensysml/connection.py @@ -1788,7 +1788,7 @@ def _python_to_value(self, py_value): if isinstance(py_value, bool): return sysml_pb2.Value(bool_value=py_value) elif isinstance(py_value, InstanceRef): - return sysml_pb2.Value(instance_id=int(py_value)) + return sysml_pb2.Value(instance_id=py_value.id) elif isinstance(py_value, int): return sysml_pb2.Value(int_value=py_value) elif isinstance(py_value, float): diff --git a/clients/python/opensysml/values.py b/clients/python/opensysml/values.py index 0c136164c..61342fcda 100644 --- a/clients/python/opensysml/values.py +++ b/clients/python/opensysml/values.py @@ -829,31 +829,31 @@ def same_value(a: Any, b: Any) -> bool: """Whether two decoded values are the same value, as :class:`SetValue` membership judges it. ``==`` decides, except that a ``bool`` is never a number — ``True`` and ``1`` - are distinct values in a model — and an :class:`InstanceRef` is never an - Integer, in a nested ``list`` or :class:`Array` too. + are distinct values in a model — in a nested ``list`` or :class:`Array` too. """ if isinstance(a, bool) or isinstance(b, bool): return isinstance(a, bool) and isinstance(b, bool) and a == b - if isinstance(a, InstanceRef) or isinstance(b, InstanceRef): - return isinstance(a, InstanceRef) and isinstance(b, InstanceRef) and a == b if isinstance(a, list) and isinstance(b, list): return len(a) == len(b) and all(same_value(x, y) for x, y in zip(a, b)) return a == b -class InstanceRef(int): +@dataclass(frozen=True) +class InstanceRef: """A reference to an instance the client has no instance graph to resolve. - It is the instance's integer id, so it reads and compares as one where an - ``int`` is expected; but it is not the Integer of that value in a model, so - :func:`same_value` keeps the two apart, and sent back it is again an - instance reference. + It holds the instance's id and is nothing else: not the Integer of that + value, so it is never equal to one nor accepted where a number is, and sent + back it is again an instance reference. + + Attributes: + id (int): The instance's id """ - __slots__ = () + id: int - def __repr__(self) -> str: - return f"InstanceRef({int(self)})" + def __str__(self) -> str: + return f"instance({self.id})" @dataclass(frozen=True) @@ -1013,7 +1013,7 @@ def value_to_python(pb_value, resolve_instance=None): pb_value: sysml_pb2.Value message resolve_instance: optional callable mapping an instance id to an object; when omitted, instance references are returned as an - :class:`InstanceRef`, an ``int`` holding the id. + :class:`InstanceRef` holding the id. Returns: int, float, complex, bool, str, list, None, :data:`UNSET`, diff --git a/clients/python/tests/test_instance.py b/clients/python/tests/test_instance.py index 526ae5657..6aa0a1d66 100644 --- a/clients/python/tests/test_instance.py +++ b/clients/python/tests/test_instance.py @@ -3,6 +3,7 @@ from opensysml.errors import FeatureValueError from opensysml.proto import sysml_pb2 from opensysml.instance import Instance +from opensysml.values import InstanceRef def scalar_feature(name, **value_kwargs): @@ -156,8 +157,8 @@ def test_nested_instance_resolution(): assert inst.engine is engine -def test_unresolvable_instance_id_falls_back_to_id(): - """Without the child in the graph, the bare id is returned.""" +def test_unresolvable_instance_id_falls_back_to_a_reference(): + """Without the child in the graph, a reference holding the id is returned.""" pb_inst = sysml_pb2.Instance( id=1, type_symbol_id="Test::P", @@ -165,7 +166,8 @@ def test_unresolvable_instance_id_falls_back_to_id(): ) inst = Instance(pb_inst) - assert inst.engine == 42 + assert inst.engine == InstanceRef(42) + assert inst.engine != 42 def test_error_slot_raises_slot_error(): diff --git a/clients/python/tests/test_set_tensor.py b/clients/python/tests/test_set_tensor.py index e2358b7e6..ae5c3c0d9 100644 --- a/clients/python/tests/test_set_tensor.py +++ b/clients/python/tests/test_set_tensor.py @@ -21,9 +21,10 @@ CAPABILITY_VERIFICATION, MissingCapabilityError, ) +from opensysml import typed from opensysml.connection import Connection from opensysml.enumeration import EnumLiteral -from opensysml.errors import ExecutionError, UnsupportedValueError +from opensysml.errors import ExecutionError, TypeMismatchError, UnsupportedValueError from opensysml.proto import sysml_pb2 from opensysml.values import ( Array, @@ -34,6 +35,7 @@ TensorQuantity, Unit, UnitFactor, + Vector, VectorQuantity, value_to_python, ) @@ -315,9 +317,10 @@ def test_members_that_only_look_alike_are_distinct(elements, expected): assert 1 not in SetValue((True,)) and True not in SetValue((1,)) -def test_an_unresolved_instance_reference_is_its_id_but_not_an_integer(): +def test_an_unresolved_instance_reference_holds_its_id_but_is_not_an_integer(): ref = value_to_python(pb_instance(7)) - assert isinstance(ref, InstanceRef) and ref == 7 and repr(ref) == "InstanceRef(7)" + assert isinstance(ref, InstanceRef) and ref.id == 7 and ref == InstanceRef(7) + assert ref != 7 and not isinstance(ref, int) and str(ref) == "instance(7)" assert ref in SetValue((InstanceRef(7),)) and ref not in SetValue((7,)) assert Array((1,), (ref,)) != Array((1,), (7,)) assert Array((2,), (True, 1)) != Array((2,), (1, 1)) @@ -327,6 +330,25 @@ def test_an_unresolved_instance_reference_is_its_id_but_not_an_integer(): sent = conn._python_to_value(SetValue((ref, 7))) assert [e.WhichOneof("kind") for e in sent.set.elements] == ["instance_id", "int_value"] assert sent.set.elements[0].instance_id == 7 + assert value_to_python(sent) == SetValue((InstanceRef(7), 7)) + + +def test_an_instance_reference_is_refused_where_a_number_is_meant(): + ref = InstanceRef(7) + with pytest.raises(ValueError, match="not a number"): + Vector((1, ref)) + with pytest.raises(ValueError, match="not a positive integer"): + Array((ref,), (1,)) + with pytest.raises(ValueError, match="not a positive integer"): + TensorQuantity((ref,), [Quantity(1.0, PASCAL)]) + with pytest.raises(TypeError): + Quantity(1.0, PASCAL) * ref + with pytest.raises(TypeMismatchError): + typed.as_int("n", ref) + with pytest.raises(TypeMismatchError): + typed.as_float("x", ref) + with pytest.raises(TypeMismatchError): + typed.as_complex("z", ref) def test_a_set_survives_the_wire_bytes(): diff --git a/clients/python/tests/test_unset.py b/clients/python/tests/test_unset.py index 0c7ab80c9..42f2b1fa7 100644 --- a/clients/python/tests/test_unset.py +++ b/clients/python/tests/test_unset.py @@ -9,7 +9,7 @@ from opensysml.errors import FeatureValueError from opensysml.instance import Instance from opensysml.proto import sysml_pb2 -from opensysml.values import UNSET, UnsetType, feature_value_to_python, value_to_python +from opensysml.values import UNSET, InstanceRef, UnsetType, feature_value_to_python, value_to_python import pytest @@ -57,7 +57,7 @@ def test_a_valued_slot_and_an_object_valued_one_are_unaffected(): object_valued = sysml_pb2.FeatureValue( feature_name="engine", value=sysml_pb2.Value(instance_id=7), materialized=True ) - assert feature_value_to_python("engine", object_valued) == 7 + assert feature_value_to_python("engine", object_valued) == InstanceRef(7) def test_an_unmaterialized_slot_is_still_an_error(): From 0cecf9817568d81fdf40dfbd4cc43f9685376005 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 06:20:24 +0000 Subject: [PATCH 12/20] fix(clients): refuse a non-integer id when building a Python InstanceRef Co-Authored-By: jason.han --- clients/python/opensysml/values.py | 7 +++++++ clients/python/tests/test_set_tensor.py | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/clients/python/opensysml/values.py b/clients/python/opensysml/values.py index 61342fcda..8c2f6c424 100644 --- a/clients/python/opensysml/values.py +++ b/clients/python/opensysml/values.py @@ -848,10 +848,17 @@ class InstanceRef: Attributes: id (int): The instance's id + + Raises: + ValueError: If the id is not an integer. """ id: int + def __post_init__(self) -> None: + if isinstance(self.id, bool) or not isinstance(self.id, int): + raise ValueError(f"instance id {self.id!r} is not an integer") + def __str__(self) -> str: return f"instance({self.id})" diff --git a/clients/python/tests/test_set_tensor.py b/clients/python/tests/test_set_tensor.py index ae5c3c0d9..e07733ba5 100644 --- a/clients/python/tests/test_set_tensor.py +++ b/clients/python/tests/test_set_tensor.py @@ -333,6 +333,12 @@ def test_an_unresolved_instance_reference_holds_its_id_but_is_not_an_integer(): assert value_to_python(sent) == SetValue((InstanceRef(7), 7)) +@pytest.mark.parametrize("instance_id", [True, 7.0, "7", None]) +def test_an_instance_reference_holds_only_an_integer_id(instance_id): + with pytest.raises(ValueError, match="is not an integer"): + InstanceRef(instance_id) + + def test_an_instance_reference_is_refused_where_a_number_is_meant(): ref = InstanceRef(7) with pytest.raises(ValueError, match="not a number"): From 7ab013e6cd073b10393284ea9743898a7e9a22d2 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 12:52:26 +0000 Subject: [PATCH 13/20] fix(clients): judge the unbounded value by kind in Node set membership Co-Authored-By: jason.han --- clients/node/src/core/values.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/clients/node/src/core/values.ts b/clients/node/src/core/values.ts index 48923b103..2eeed921f 100644 --- a/clients/node/src/core/values.ts +++ b/clients/node/src/core/values.ts @@ -630,6 +630,7 @@ export function valuesEqual(a: SysMLValue, b: SysMLValue): boolean { dimensionsEqual(a.dimensions, b.dimensions) && componentsEqual(a.components, b.components) ); + case "infinity": case "null": case "unset": case "absent": From 8b2f1f6311b7112a08a0e9398a5ba51134a140d5 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 13:01:14 +0000 Subject: [PATCH 14/20] fix(clients): refuse a malformed tensor quantity in the Go client before it is sent Co-Authored-By: jason.han --- client/opensysml/README.md | 4 +- client/opensysml/convert.go | 3 ++ client/opensysml/set_tensor_test.go | 7 ++-- client/opensysml/structured_internal_test.go | 42 ++++++++++++++++++++ 4 files changed, 51 insertions(+), 5 deletions(-) diff --git a/client/opensysml/README.md b/client/opensysml/README.md index a85858cdb..babe7192c 100644 --- a/client/opensysml/README.md +++ b/client/opensysml/README.md @@ -225,7 +225,9 @@ across the whole `Int` range; a `Quantity` by magnitude through its `Term`, so `1 [m]` is `100 [cm]` (exactly, while the magnitude is an `Int` and the scale a whole ratio), and one without a `Term` in its unit as written. A `TensorQuantity` carries its dimensions and one `Quantity` per -component in row-major order, at any rank. +component in row-major order, at any rank; one whose dimensions are not all +positive, or whose components do not fill them, is refused with +`CodeInvalidArgument` before it is sent. ## Stability diff --git a/client/opensysml/convert.go b/client/opensysml/convert.go index 7fc6f8ede..3f26c7d4b 100644 --- a/client/opensysml/convert.go +++ b/client/opensysml/convert.go @@ -300,6 +300,9 @@ func valueToProto(value Value) (*pb.Value, error) { } return &pb.Value{Kind: &pb.Value_Set{Set: set}}, nil case TensorQuantity: + if err := sysmlgrpc.CheckTensorShape(v.Dimensions, len(v.Components)); err != nil { + return nil, &StatusError{Code: CodeInvalidArgument, Message: err.Error()} + } tq := &pb.TensorQuantity{ Dimensions: append([]int64(nil), v.Dimensions...), Components: make([]*pb.Quantity, 0, len(v.Components)), diff --git a/client/opensysml/set_tensor_test.go b/client/opensysml/set_tensor_test.go index 65b8fb278..66265302d 100644 --- a/client/opensysml/set_tensor_test.go +++ b/client/opensysml/set_tensor_test.go @@ -120,12 +120,11 @@ func TestSetsAndTensorsCrossEveryTransport(t *testing.T) { if !errors.As(err, &status) || status.Code != opensysml.CodeInvalidArgument || !strings.Contains(status.Message, "set lists a member twice") { t.Errorf("sizeOf({1, 1.0}) err = %v, want an invalid-argument status naming the repeated element", err) } - // A tensor its components do not fill is a failure. + // A tensor its components do not fill is refused before it is sent. short := opensysml.TensorQuantity{Dimensions: []int64{2, 2, 2}, Components: tq.Components[:7]} _, err = client.EvaluateCalc(ctx, model, "W::corner", short) - var failure *opensysml.FailureError - if !errors.As(err, &failure) || !strings.Contains(failure.Error(), "tensor components do not fill its dimensions") { - t.Errorf("corner(short) err = %v, want a failure naming the shape", err) + if !errors.As(err, &status) || status.Code != opensysml.CodeInvalidArgument || !strings.Contains(status.Message, "tensor components do not fill its dimensions") { + t.Errorf("corner(short) err = %v, want an invalid-argument status naming the shape", err) } }) } diff --git a/client/opensysml/structured_internal_test.go b/client/opensysml/structured_internal_test.go index f6143af32..63a7a1f96 100644 --- a/client/opensysml/structured_internal_test.go +++ b/client/opensysml/structured_internal_test.go @@ -501,6 +501,48 @@ func TestRepeatedSetMembersAreNotSent(t *testing.T) { } } +func TestMalformedTensorsAreNotSent(t *testing.T) { + pascal := Quantity{Magnitude: Real(1), Unit: "Pa"} + components := func(n int) []Quantity { + out := make([]Quantity, n) + for i := range out { + out[i] = pascal + } + return out + } + for name, tc := range map[string]struct { + tensor TensorQuantity + want string + }{ + "too few components": {TensorQuantity{Dimensions: []int64{2, 2, 2}, Components: components(7)}, "tensor components do not fill its dimensions"}, + "too many components": {TensorQuantity{Dimensions: []int64{2}, Components: components(3)}, "tensor components do not fill its dimensions"}, + "zero dimension": {TensorQuantity{Dimensions: []int64{2, 0}, Components: nil}, "tensor dimension is not positive"}, + "negative dimension": {TensorQuantity{Dimensions: []int64{-1}, Components: components(1)}, "tensor dimension is not positive"}, + "overflowing shape": {TensorQuantity{Dimensions: []int64{1 << 40, 1 << 40}, Components: components(1)}, "tensor components do not fill its dimensions"}, + "nested in a set": {TensorQuantity{Dimensions: []int64{2}, Components: components(1)}, "tensor components do not fill its dimensions"}, + } { + t.Run(name, func(t *testing.T) { + var input Value = tc.tensor + if name == "nested in a set" { + input = Set{Int(1), tc.tensor} + } + _, err := valueToProto(input) + var status *StatusError + if !errors.As(err, &status) || status.Code != CodeInvalidArgument || !strings.HasPrefix(status.Message, tc.want) { + t.Fatalf("sent with err %v, want an invalid-argument StatusError starting %q", err, tc.want) + } + }) + } + scalar := TensorQuantity{Dimensions: nil, Components: components(1)} + sent, err := valueToProto(scalar) + if err != nil { + t.Fatalf("rank-0 tensor refused: %v", err) + } + if got := valueFromProto(sent); !reflect.DeepEqual(got, scalar) { + t.Errorf("rank-0 tensor read back as %#v, want %#v", got, scalar) + } +} + // A malformed measurement reference in an answer reads as an unsupported null // naming the fault; a well-formed one reads as itself, reduction and identity // intact. From e6bc95be8f4f03a280912662a53ea37977f62ab5 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 14:14:56 +0000 Subject: [PATCH 15/20] fix(clients): judge functions by calc and object in Node set membership Co-Authored-By: jason.han --- clients/node/src/core/values.ts | 11 +++++++++-- clients/node/test/values.test.ts | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/clients/node/src/core/values.ts b/clients/node/src/core/values.ts index 2eeed921f..ac6e339ea 100644 --- a/clients/node/src/core/values.ts +++ b/clients/node/src/core/values.ts @@ -582,8 +582,9 @@ function uniqueMembers(members: SysMLValue[]): SysMLValue[] { * does a member it lists twice; a quantity is the same over its base units; a * `measurementRef` is one reduction at one scale however spelt, except that a * named unit of dimension one is only its own declaration (`rad` is not `sr`); - * an `enum` is its `literalId`, whatever else describes it; a `null` is the - * same whatever its reason. + * an `enum` is its `literalId`, whatever else describes it; a `function` is + * its `calcId` read against its `selfId`; a `null` is the same whatever its + * reason. */ export function valuesEqual(a: SysMLValue, b: SysMLValue): boolean { switch (a.kind) { @@ -605,6 +606,12 @@ export function valuesEqual(a: SysMLValue, b: SysMLValue): boolean { return b.kind === "measurementRef" && measurementRefsEqual(a, b); case "enum": return b.kind === "enum" && a.value.literalId === b.value.literalId; + case "function": + return ( + b.kind === "function" && + a.calcId === b.calcId && + a.selfId === b.selfId + ); case "array": return ( b.kind === "array" && diff --git a/clients/node/test/values.test.ts b/clients/node/test/values.test.ts index 6e31f4500..8cfb4ba9f 100644 --- a/clients/node/test/values.test.ts +++ b/clients/node/test/values.test.ts @@ -568,6 +568,25 @@ test("valuesEqual judges enumeration literals by their literalId, as the service assert.equal(sent.elements.length, 2); }); +test("valuesEqual judges functions by the calc read against its object, as the service does", () => { + const sq: SysMLValue = { kind: "function", calcId: "M::Sq" }; + const cube: SysMLValue = { kind: "function", calcId: "M::Cube" }; + const scale: SysMLValue = { kind: "function", calcId: "M::scale", selfId: 7n }; + assert.equal(valuesEqual(sq, { kind: "function", calcId: "M::Sq" }), true); + assert.equal(valuesEqual(sq, cube), false); + assert.equal(valuesEqual(scale, { kind: "function", calcId: "M::scale", selfId: 8n }), false); + assert.equal(valuesEqual(scale, { kind: "function", calcId: "M::scale" }), false); + assert.equal(valuesEqual(sq, { kind: "string", value: "M::Sq" }), false); + assert.equal( + valuesEqual({ kind: "set", elements: [sq, cube] }, { kind: "set", elements: [cube, sq] }), + true, + ); + assert.throws(() => encodeValue({ kind: "set", elements: [sq, cube, { kind: "function", calcId: "M::Sq" }] }), { + name: "MalformedValueError", + message: /^a set lists a member twice: /, + }); +}); + test("a set assembled with a member listed twice is refused before it is sent", () => { const i = (value: bigint): SysMLValue => ({ kind: "int", value }); const r = (value: number): SysMLValue => ({ kind: "real", value }); From 3f762ff88f6990bfbfada702609a563263d4ddd1 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 14:24:23 +0000 Subject: [PATCH 16/20] fix(grpc): bind functions nested in set arguments with the active runtime Co-Authored-By: jason.han --- internal/grpc/convert.go | 6 ++-- internal/grpc/convert_function_test.go | 48 ++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/internal/grpc/convert.go b/internal/grpc/convert.go index e6faf5ba0..0293ebb8e 100644 --- a/internal/grpc/convert.go +++ b/internal/grpc/convert.go @@ -718,7 +718,7 @@ func ProtoToRuntimeValue(rt *runtime.Context, pv *pb.Value, idx *symbols.Index, } return runtime.NewSequenceValue(seq), nil case *pb.Value_Set: - return protoToSet(k.Set, idx, sem) + return protoToSet(rt, k.Set, idx, sem) case *pb.Value_TensorQuantity: return protoToTensorQuantity(k.TensorQuantity, idx, sem) case *pb.Value_Array: @@ -770,10 +770,10 @@ func functionFromProto(rt *runtime.Context, fn *pb.Function, idx *symbols.Index) // protoToSet rebuilds a set from elements sent in any order, refusing one sent // twice rather than reading the two as one. -func protoToSet(ps *pb.ValueSet, idx *symbols.Index, sem *semantics.Model) (runtime.Value, error) { +func protoToSet(rt *runtime.Context, ps *pb.ValueSet, idx *symbols.Index, sem *semantics.Model) (runtime.Value, error) { set := runtime.NewSet() for i, elem := range ps.GetElements() { - val, err := ProtoToValueIn(elem, idx, sem) + val, err := ProtoToRuntimeValue(rt, elem, idx, sem) if err != nil { return runtime.Value{}, err } diff --git a/internal/grpc/convert_function_test.go b/internal/grpc/convert_function_test.go index 99d2801e5..c06e3b5b2 100644 --- a/internal/grpc/convert_function_test.go +++ b/internal/grpc/convert_function_test.go @@ -3,6 +3,7 @@ package grpc import ( "context" "errors" + "slices" "strings" "testing" @@ -130,6 +131,47 @@ func TestFunctionRoundTrip(t *testing.T) { t.Fatalf("F::fns = %v, want a sequence of the functions Sq and Cube", fns) } + // A function binds wherever it is nested: in a set, in a set held in a + // sequence, and in a sequence held in a set. + sqCube := []*pb.Value{functionValue("F::Sq", 0), functionValue("F::Cube", 0)} + sequenceOf := func(elements ...*pb.Value) *pb.Value { + return &pb.Value{Kind: &pb.Value_Sequence{Sequence: &pb.ValueSequence{Elements: elements}}} + } + for name, nested := range map[string]*pb.Value{ + "set": setOf(sqCube...), + "set in sequence": sequenceOf(setOf(sqCube...)), + "sequence in set": setOf(sequenceOf(sqCube...)), + } { + rt, _, release := srv.newRuntime(cached) + back, err := ProtoToRuntimeValue(rt, nested, idx, sem) + if err != nil { + release() + t.Fatalf("ProtoToRuntimeValue(functions in a %s): %v", name, err) + } + var got []string + var walk func(v runtime.Value) + walk = func(v runtime.Value) { + switch v.Kind { + case runtime.ValSet: + for _, m := range v.Set().Elements() { + walk(m) + } + case runtime.ValSequence: + for _, m := range v.Sequence().Elements() { + walk(m) + } + default: + got = append(got, v.Kind.String()+" "+runtime.FormatValue(v)) + } + } + walk(back) + slices.Sort(got) + if want := []string{"function F::Cube", "function F::Sq"}; !slices.Equal(got, want) { + t.Errorf("functions in a %s read back as %s holding %v, want %v", name, back.Kind, got, want) + } + release() + } + // A function read in applies as the argument of a calc and an action. calc, err := srv.EvaluateCalc(ctx, &pb.EvaluateCalcRequest{ModelHash: modelHash, SymbolId: "F::apply", Arguments: []*pb.Value{functionValue("F::Sq", 0), realValue(3)}}) if err != nil || calc.Error != "" { @@ -264,6 +306,11 @@ func TestMalformedFunctionsAreRejected(t *testing.T) { intValue(1), functionValue("F::Nope", 0), }}}}, ErrFunctionUnbound}, {"nested in an array", arrayValue([]int64{1}, functionValue("", 0)), ErrFunctionUnbound}, + {"nested in a set", setOf(intValue(1), functionValue("F::Nope", 0)), ErrFunctionUnbound}, + {"nested in a set in a sequence", &pb.Value{Kind: &pb.Value_Sequence{Sequence: &pb.ValueSequence{Elements: []*pb.Value{ + setOf(functionValue("F::Sq", 12345)), + }}}}, ErrFunctionUnbound}, + {"listed twice in a set", setOf(functionValue("F::Sq", 0), functionValue("F::Sq", 0)), ErrSetElementRepeated}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -280,6 +327,7 @@ func TestMalformedFunctionsAreRejected(t *testing.T) { for name, val := range map[string]*pb.Value{ "bare": functionValue("F::Sq", 0), "in array": arrayValue([]int64{1}, functionValue("F::Sq", 0)), + "in set": setOf(functionValue("F::Sq", 0)), } { if _, err := ProtoToValueIn(val, idx, sem); !errors.Is(err, ErrFunctionNeedsRuntime) { t.Errorf("%s without a runtime: err = %v, want %v", name, err, ErrFunctionNeedsRuntime) From f425b9e59039962278ee2b20d34e49af6be1f027 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 20:25:37 +0000 Subject: [PATCH 17/20] fix(runtime): order alike-rendered functions by identity and keep a set apart from its sequence as a member canonicalCompare falls through to the calc, bound object and enclosing run when two function values render alike, so equal sets of functions enumerate and cross the wire in one order however they were written. Set membership no longer equates a set with the sequence of its members: valueEqual and valueKeyFunc keep the two kinds apart, matching every client, while the == operator and binding agreement still read a set flowing into an ordered context as its canonical sequence (equalValues). Co-Authored-By: jason.han --- client/opensysml/set_tensor_test.go | 6 ++ clients/rust/opensysml/src/domain.rs | 5 ++ docs/project/spec-compliance.md | 4 +- docs/reference/wire-contract.md | 4 +- internal/core/runtime/action_frame.go | 4 +- internal/core/runtime/binding.go | 4 +- internal/core/runtime/eval.go | 19 ++++-- internal/core/runtime/set_feature_test.go | 79 ++++++++++++++++++++--- internal/core/runtime/set_order.go | 17 ++++- internal/core/runtime/value_equality.go | 3 +- internal/grpc/convert_set_tensor_test.go | 55 ++++++++++++++++ 11 files changed, 175 insertions(+), 25 deletions(-) diff --git a/client/opensysml/set_tensor_test.go b/client/opensysml/set_tensor_test.go index 66265302d..6687198cf 100644 --- a/client/opensysml/set_tensor_test.go +++ b/client/opensysml/set_tensor_test.go @@ -120,6 +120,12 @@ func TestSetsAndTensorsCrossEveryTransport(t *testing.T) { if !errors.As(err, &status) || status.Code != opensysml.CodeInvalidArgument || !strings.Contains(status.Message, "set lists a member twice") { t.Errorf("sizeOf({1, 1.0}) err = %v, want an invalid-argument status naming the repeated element", err) } + // A set is not the sequence of its members, on either side: what the client admits, the service reads. + one, two := opensysml.Int(1), opensysml.Int(2) + calc, err = client.EvaluateCalc(ctx, model, "W::sizeOf", opensysml.Set{opensysml.Set{one, two}, opensysml.Sequence{one, two}, opensysml.Sequence{two, one}}) + if err != nil || calc.Result != opensysml.Int(3) { + t.Errorf("sizeOf({{1, 2}, (1, 2), (2, 1)}) = %#v, %v, want 3", calc, err) + } // A tensor its components do not fill is refused before it is sent. short := opensysml.TensorQuantity{Dimensions: []int64{2, 2, 2}, Components: tq.Components[:7]} _, err = client.EvaluateCalc(ctx, model, "W::corner", short) diff --git a/clients/rust/opensysml/src/domain.rs b/clients/rust/opensysml/src/domain.rs index 5d9630d2e..c89c384c2 100644 --- a/clients/rust/opensysml/src/domain.rs +++ b/clients/rust/opensysml/src/domain.rs @@ -1794,6 +1794,11 @@ mod tests { Value::Set(Set::new(vec![Value::Real(9_007_199_254_740_992.0)]).unwrap()), false, ), + ( + Value::Set(Set::new(vec![Value::Integer(1), Value::Integer(2)]).unwrap()), + Value::Sequence(vec![Value::Integer(1), Value::Integer(2)]), + false, + ), ]; for (a, b, want) in cases { assert_eq!(a.same_value(&b), want, "{a:?} vs {b:?}"); diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index 0f7e7d792..a2717ec9f 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -1384,8 +1384,8 @@ they cannot drift apart. | An endpoint of `subsequence` or `excludingAt` that is outside the sequence is a typed error (`ErrIndexOutOfRange`), while an *empty range* inside it is the empty sequence, which is how `tail` is `subsequence(seq, 2)` of a one-element sequence | `runtime/collections.go` `builtinSequenceSubsequence`, `builtinSequenceExcludingAt` | `runtime/collections_test.go` `TestCollectionOperationErrors`, `TestCollectionResults`, pilot-exec-diff `w6d:excluding-at`, `:subsequence` (both agree), `:excluding-at-out-of-range`, `:subsequence-out-of-range` (the reference fails too), `:subsequence-empty-range` | ✅ Faithful (the in-range results match the pinned artifact and it fails on both out-of-range endpoints rather than answering the vendored truncation — by exception out of the library body, where this reports `ErrIndexOutOfRange`. The empty-range half is unrefereeable: the reference emits nothing, which it cannot distinguish from the empty sequence this answers) | | `CollectionFunctions`: `size`, `isEmpty`, `notEmpty`, `contains`, `containsAll`, `head`, `tail`, `last`, `#` over a collection's elements, a set included | `runtime/collections.go`, `runtime/builtins.go` | `runtime/collections_test.go` `TestCollectionScalarResults`, `TestCollectionOperationsOverSets` | ✅ Faithful | | A `Collections::Collection` whose library redefinition of `elements` is unique and not ordered holds a **set** (`ValSet`), the library's own kind: `Set` ("unique and unordered", `Collections.kerml:104-108`), `UniqueCollection` ("unique and not necessarily ordered", `:39-48`) and `Map` (unique `KeyValuePair`s, `:142-152`). `Bag` inherits the root's `nonunique` (`:22`, `:97-101`) and stays a sequence, as does every `OrderedCollection` (`:30-36`) — `Array`, `List`, `OrderedSet` (`ordered`, `:111-121`), `OrderedMap` (`:162-175`) — and any feature a model declares `ordered` or `nonunique` itself. A set holds each member once (`(3, 1, 2, 2, 3)` is three members, `(1, 2, 3)` given already distinct is the same three, `()` the empty set), so `size` answers 3 and `isEmpty` reads the count; a value written or bound to such a feature is admitted as a set, and a set flowing into a feature that holds a sequence (`ordered`, `nonunique`, or a plain `Integer[0..*]`) becomes the sequence of its members in canonical order | `runtime/set_feature.go` `holdsSet`, `declaredOrderedOrNonunique`, `collectionOf`, `declaredCollection`; `shape.go` `EffectiveFeature.HoldsSet`; `instance.go` `admitted`, `materializeIntrinsic`; `subsetting.go` (optional subsetters); `value.go` `Set`, `NewSet`, `Set.Add`, `Set.Size` | conformance `library_set_elements`, `library_set_elements_already_distinct`, `library_set_elements_empty`, `library_unique_collection_elements`, `library_map_elements`, `library_bag_elements`, `library_ordered_set_elements`, `library_set_operations`; `runtime/set_feature_test.go:TestCollectionElementsHoldTheLibraryKind`, `:TestSetFlowsIntoDeclaredCollections`, `:TestCollectionFunctionsOverCollectionObjects` | ✅ Faithful | -| Two sets are equal when their members are, in whatever order either was given (`CollectionFunctions::'=='` is `col1.elements->equals(col2.elements)`, and a set's `elements` have no order to compare); `contains` and `containsAll` are membership; a set's elements equal a sequence only when the sequence lists the members in canonical order, and a `Set` is never equal to a `Bag` or `OrderedSet` holding the same values, which are sequences | `runtime/eval.go` `valueEqual` (`ValSet` arm); `runtime/collections.go` `builtinCollectionEquals`, `elementsOf`; `value.go` `Set.Equal`, `Set.Contains` | conformance `library_set_operations` (`equalRegardlessOfOrder`, `elementsEqualRegardlessOfOrder`, `membership`, `allMembers`, `emptiness`, `notASequence`); `runtime/set_feature_test.go:TestSetAgainstSequenceComparesCanonically`, `:TestCollectionFunctionsOverCollectionObjects` | ✅ Faithful | -| A set has no order of its own, so every operation that walks its members in order — `collect`, `select`, `head`, `tail`, `#`, `==` against a sequence, a trace rendering it, a write into a sequence-holding feature, the wire — sees them in one **canonical order**: by class (null, Booleans with `false` first, numbers ascending, complex numbers by real then imaginary part, strings lexicographically, quantities by dimension then magnitude, enumeration literals by declaration, objects by identity, then every other kind) and within a class by that order, then by the trace rendering, then by contents — elements, components, unit reduction — so only equal values share a position. The order is total, so equal sets enumerate alike and a trace over a set is the same golden however the set was written | `runtime/set_order.go` `canonicalLess`, `canonicalClass`; `value.go` `Set.Elements` (sorted once, cached); `trace.go` `FormatTraceValue` (`ValSet`, the same enumeration) | conformance `calc_set_consumed_by_ordered_operations` and its trace golden; `runtime/set_feature_test.go:TestCanonicalOrderIsTotal`, `:TestSameNamedLiteralsOrderByDeclaration`, `:TestLikeRenderedValuesOrderByContents`, `:TestSetRendersCanonically`; `runtime/collections_test.go:TestCollectionOperationsOverSets` | ✅ Faithful — the order is this runtime's documented rule, since the library defines none; it is not a claim about the specification | +| Two sets are equal when their members are, in whatever order either was given (`CollectionFunctions::'=='` is `col1.elements->equals(col2.elements)`, and a set's `elements` have no order to compare); `contains` and `containsAll` are membership; `==` reads a set against a sequence as its canonical sequence (an ordered context), so a set's elements equal a sequence only when the sequence lists the members in canonical order, while as a set's member a set is never the sequence of its members; a `Set` is never equal to a `Bag` or `OrderedSet` holding the same values, which are sequences | `runtime/eval.go` `equalValues`, `valueEqual` (`ValSet` arm); `runtime/collections.go` `builtinCollectionEquals`, `elementsOf`; `value.go` `Set.Equal`, `Set.Contains` | conformance `library_set_operations` (`equalRegardlessOfOrder`, `elementsEqualRegardlessOfOrder`, `membership`, `allMembers`, `emptiness`, `notASequence`); `runtime/set_feature_test.go:TestSetAgainstSequenceComparesCanonically`, `:TestSetIsNotASequenceAsAMember`, `:TestCollectionFunctionsOverCollectionObjects`; `grpc/convert_set_tensor_test.go:TestMalformedSetsAreRejected` | ✅ Faithful | +| A set has no order of its own, so every operation that walks its members in order — `collect`, `select`, `head`, `tail`, `#`, `==` against a sequence, a trace rendering it, a write into a sequence-holding feature, the wire — sees them in one **canonical order**: by class (null, Booleans with `false` first, numbers ascending, complex numbers by real then imaginary part, strings lexicographically, quantities by dimension then magnitude, enumeration literals by declaration, objects by identity, then every other kind) and within a class by that order, then by the trace rendering, then by contents — elements, components, unit reduction, the calc, object and run a function is a value of — so only equal values share a position. The order is total, so equal sets enumerate alike and a trace over a set is the same golden however the set was written | `runtime/set_order.go` `canonicalLess`, `canonicalClass`; `value.go` `Set.Elements` (sorted once, cached); `trace.go` `FormatTraceValue` (`ValSet`, the same enumeration) | conformance `calc_set_consumed_by_ordered_operations` and its trace golden; `runtime/set_feature_test.go:TestCanonicalOrderIsTotal`, `:TestSameNamedLiteralsOrderByDeclaration`, `:TestFunctionsRenderedAlikeOrderByIdentity`, `:TestLikeRenderedValuesOrderByContents`, `:TestSetRendersCanonically`; `runtime/collections_test.go:TestCollectionOperationsOverSets` | ✅ Faithful — the order is this runtime's documented rule, since the library defines none; it is not a claim about the specification | | What the library declares ordered stays a sequence: every `SequenceFunctions` result is `Anything[0..*] ordered nonunique` — `union`, `intersection`, `including`, `includingAt`, `excluding` included (`SequenceFunctions.kerml:48-63`) — so `union(s.elements, t.elements)` over two `Set`s is the ordered concatenation of their canonical members, repeats kept, not a set; `(s.elements, s.elements)` likewise lists each member twice. The Kernel Function Library declares no `distinct` function, so none is invented: the members of a sequence, each once, are what a `Set`'s `elements` hold | `runtime/collections.go` (`SequenceFunctions` builtins over `elementsOf`) | conformance `library_set_sequence_functions`, `library_set_operations` (`ordered`, `repeatable`, `plain`) | ✅ Faithful | | `OrderedSet::elements` and `OrderedMap::elements` are declared `ordered` *and* unique (`UniqueCollection` under `OrderedCollection`, `Collections.kerml`), as is any multi-valued feature not declared `nonunique`. Their order is part of the value, so they are held as the sequence written; their uniqueness is not enforced — `OrderedSet { :>> elements = (1, 1, 2); }` reads three elements and `size` answers 3. A set is the one place uniqueness is the value's own definition; checking `unique` as a constraint on an ordered feature's values is separate work | `runtime/set_feature.go` `holdsSet` (declines any feature declared `ordered`) | conformance `library_ordered_set_elements` (order kept) | ⚠️ Approximate — order faithful, uniqueness unchecked | | A set **crosses gRPC** as the `set` arm (`ValueSet.elements`, each a `Value`, listed in canonical order; an incoming set may list them in any order, and one that repeats a member is `ErrSetElementRepeated`), advertised as `set_values`; a service withholding the capability answers an unsupported null naming the value and refuses one sent to it with `UNIMPLEMENTED`, nested anywhere in the argument. It is **not compiled natively**: `sysml -compile` refuses a calc declaring or reading one with `codegen.UnsupportedError` (`type Collections::Set is not Integer, Real or Boolean`). It has **no RDF literal form** of its own: the mapping writes the model, so a `Set`-valued feature is the expression valuing its `elements`, which round trips exactly | `grpc/convert.go` `setToProto`, `protoToSet`, `ErrSetElementRepeated`; `grpc/capability_response.go` (`set_values`); `codegen/compile.go` `UnsupportedError`; `export/rdf_expr.go` | `grpc/convert_set_tensor_test.go:TestSetRoundTrip`, `:TestMalformedSetsAreRejected`, `:TestSetAndTensorCapabilities`, `:TestValueCarriesSetAndTensor`; `repl/compile_test.go:TestCompileRefusesWhatItCannotCompile` (`SetParam`, `SetElements`, `SetLocal`); `export/set_tensor_rdf_test.go:TestSetAndTensorValuesRoundTripAsExpressions`; the *Values* gRPC rows below | ✅ Faithful (the native and RDF refusals are typed and documented, not layouts) | diff --git a/docs/reference/wire-contract.md b/docs/reference/wire-contract.md index a55d22147..7d9b8196b 100644 --- a/docs/reference/wire-contract.md +++ b/docs/reference/wire-contract.md @@ -520,7 +520,9 @@ $ … /Evaluate -d '{"modelHash":"c409…1a4a","expression":"T::s.elements"}' quantities, enumeration literals, then objects by identity) — so two equal sets are sent identically, but the order carries no meaning and a client must not read one into it. - A `set` is not a `sequence`: `(1, 2) == (2, 1)` is false, the sets they populate are equal. - A client compares sets by membership and sends one back in any order it likes. + A client compares sets by membership and sends one back in any order it likes. As members + of a set, a `set` and the `sequence` of its members are two members, on both sides; only + `==` in the model, an ordered context, reads the set as its canonical sequence. - Membership is the engine's value equality: numbers by value, so `1` and `1.0` are one member and so are `1.5` and the complex `1.5 + 0.0i`, exactly — an Integer past 2^53 is not the Real it would round to; a Boolean is never a number; sequences in order; sets by membership; diff --git a/internal/core/runtime/action_frame.go b/internal/core/runtime/action_frame.go index b38d89462..59b32db3f 100644 --- a/internal/core/runtime/action_frame.go +++ b/internal/core/runtime/action_frame.go @@ -773,7 +773,7 @@ func (e *performances) bindInputPins(perf *actionFrame, activation int64) error return err } if alreadyBound { - if held := perf.data[perf.key(end.Pin)]; !valueEqual(held, value) { + if held := perf.data[perf.key(end.Pin)]; !equalValues(held, value) { return &BindingConflictError{ Target: end.pinText(), Left: bindingEndText(earlier.Other), @@ -811,7 +811,7 @@ func (e *performances) bindOutputPins(perf *actionFrame) error { if !ok { continue } - if other, held := e.otherEndHeld(perf, end); held && valueEqual(other, value) { + if other, held := e.otherEndHeld(perf, end); held && equalValues(other, value) { continue } default: diff --git a/internal/core/runtime/binding.go b/internal/core/runtime/binding.go index ae828a6e8..c96bd8342 100644 --- a/internal/core/runtime/binding.go +++ b/internal/core/runtime/binding.go @@ -330,7 +330,7 @@ func (ctx *Context) resolveBindingSet(owner, targetInst *Instance, target *Featu // two of them must agree on its values, a sequence like a scalar. if len(attempts) > 1 { for _, attempt := range attempts[1:] { - if !valueEqual(attempts[0].value, attempt.value) { + if !equalValues(attempts[0].value, attempt.value) { return result, &BindingConflictError{ Target: bindingLocationText(bindingLocation{instance: targetInst, name: key.feature}), Left: attempts[0].contributor, @@ -581,7 +581,7 @@ func (ctx *Context) attemptBinding(owner, targetInst *Instance, target *FeatureV } return attempt } - if !valueEqual(leftValue, rightValue) { + if !equalValues(leftValue, rightValue) { attempt.err = &BindingConflictError{ Left: ctx.bindingEndpointText(binding, 0), Right: ctx.bindingEndpointText(binding, 1), LeftValue: leftValue, RightValue: rightValue, diff --git a/internal/core/runtime/eval.go b/internal/core/runtime/eval.go index 7889e6f67..cedbf76e2 100644 --- a/internal/core/runtime/eval.go +++ b/internal/core/runtime/eval.go @@ -1958,7 +1958,7 @@ func (ctx *Context) equalityValues(op ast.OperatorKind, left, right Value) (Valu } } - equal := valueEqual(left, right) + equal := equalValues(left, right) if op == ast.OpNeq { equal = !equal } @@ -2621,7 +2621,17 @@ func qualifiedNameToString(qn *ast.QualifiedName) string { return strings.Join(parts, "::") } -// valueEqual checks deep equality of two runtime values. +// equalValues is `==` over two operands: a set meeting a sequence flows into the +// ordered context and is compared as its canonical sequence; otherwise valueEqual. +func equalValues(a, b Value) bool { + if a.Kind == ValSet && b.Kind == ValSequence || a.Kind == ValSequence && b.Kind == ValSet { + return sequenceEqual(sequenceOf(elementsOf(a)).Sequence(), sequenceOf(elementsOf(b)).Sequence()) + } + return valueEqual(a, b) +} + +// valueEqual checks deep equality of two runtime values: whether they are one +// value, as a set's membership judges. A set is never the sequence of its members. func valueEqual(a, b Value) bool { if isEmptyValue(a) || isEmptyValue(b) { return isEmptyValue(a) && isEmptyValue(b) @@ -2630,11 +2640,6 @@ func valueEqual(a, b Value) bool { if a.Kind == ValComplex || b.Kind == ValComplex { return complexEqual(a, b) } - // A set compared with a sequence flows into the ordered context: its - // canonical sequence is compared. - if a.Kind == ValSet && b.Kind == ValSequence || a.Kind == ValSequence && b.Kind == ValSet { - return sequenceEqual(sequenceOf(elementsOf(a)).Sequence(), sequenceOf(elementsOf(b)).Sequence()) - } if a.Kind != b.Kind { return false } diff --git a/internal/core/runtime/set_feature_test.go b/internal/core/runtime/set_feature_test.go index 987241aca..392341ead 100644 --- a/internal/core/runtime/set_feature_test.go +++ b/internal/core/runtime/set_feature_test.go @@ -340,10 +340,10 @@ func TestSameNamedLiteralsOrderByDeclaration(t *testing.T) { } } -// TestSetMembersEqualAcrossCollectionKinds pins that a sequence and the set it -// equals — valueEqual holds across the two kinds — share one key, so a set -// admits only one of them and finds either. -func TestSetMembersEqualAcrossCollectionKinds(t *testing.T) { +// TestSetIsNotASequenceAsAMember pins that a set and the sequence `==` reads it +// as are two members of a set — a set is never the sequence of its members — +// while two equal sets, or any two empty values, are one. +func TestSetIsNotASequenceAsAMember(t *testing.T) { ints := func(ns ...int64) []Value { vals := make([]Value, len(ns)) for i, n := range ns { @@ -352,24 +352,87 @@ func TestSetMembersEqualAcrossCollectionKinds(t *testing.T) { return vals } seq, set, other := sequenceOf(ints(1, 2)), setOf(ints(2, 1)), sequenceOf(ints(2, 1)) - if valueKeyFunc(seq) != valueKeyFunc(set) { - t.Errorf("keys differ for %s and %s", FormatValue(seq), FormatValue(set)) + if !equalValues(seq, set) || equalValues(other, set) { + t.Errorf("== reads %s against %s and %s in canonical order", FormatValue(set), FormatValue(seq), FormatValue(other)) + } + if valueEqual(seq, set) || valueKeyFunc(seq) == valueKeyFunc(set) { + t.Errorf("%s and %s are one member", FormatValue(seq), FormatValue(set)) } for _, members := range [][]Value{{seq, set, other}, {set, seq, other}, {other, set, seq}} { outer := setOf(members).Set() - if outer.Size() != 2 { - t.Errorf("set of %v has %d members, want 2", members, outer.Size()) + if outer.Size() != 3 { + t.Errorf("set of %v has %d members, want 3", members, outer.Size()) } for _, m := range []Value{seq, set, other} { if !outer.Contains(m) { t.Errorf("set of %v lacks %s", members, FormatValue(m)) } } + if got, want := FormatTraceValue(NewSetValue(outer)), "{(1, 2), (2, 1), {1, 2}}"; got != want { + t.Errorf("set of %v renders %s, want %s", members, got, want) + } } nested := setOf([]Value{setOf(ints(1, 2)), setOf(ints(2, 1))}).Set() if nested.Size() != 1 { t.Errorf("set of two equal sets has %d members, want 1", nested.Size()) } + // A set holding a sequence is not the set holding the set of its elements. + if valueEqual(setOf([]Value{seq}), setOf([]Value{set})) { + t.Errorf("%s and %s are one value", FormatValue(setOf([]Value{seq})), FormatValue(setOf([]Value{set}))) + } + empties := setOf([]Value{{Kind: ValNull}, sequenceOf(nil), setOf(nil)}).Set() + if empties.Size() != 1 { + t.Errorf("null, () and {} are %d members, want 1", empties.Size()) + } +} + +// TestFunctionsRenderedAlikeOrderByIdentity pins that two distinct functions +// with one trace rendering — one calc read against two objects, or within two +// runs, or two declarations of one name — take one position each, so equal +// sets of functions enumerate alike whatever order they were written in. +func TestFunctionsRenderedAlikeOrderByIdentity(t *testing.T) { + shape := &calcShape{Sym: &symbols.Symbol{Name: "scale"}, Name: "P::scale"} + twin := &calcShape{Sym: &symbols.Symbol{Name: "scale", DocName: "other"}, Name: "P::scale"} + fn := func(shape *calcShape, self *Instance, run int64) Value { + f := &functionValue{shape: shape, self: self} + if run != 0 { + f.enclosing = []frame{{vars: map[string]Value{"k": integerValue(1)}, run: run}} + } + return Value{Kind: ValFunction, ref: f} + } + one, two := &Instance{ID: 1}, &Instance{ID: 2} + pairs := map[string][2]Value{ + "two objects": {fn(shape, one, 0), fn(shape, two, 0)}, + "two runs": {fn(shape, one, 1), fn(shape, one, 2)}, + "two declarations": {fn(shape, one, 0), fn(twin, one, 0)}, + "no object": {fn(shape, nil, 0), fn(shape, one, 0)}, + } + for name, pair := range pairs { + a, b := pair[0], pair[1] + if FormatTraceValue(a) != FormatTraceValue(b) { + t.Fatalf("%s: %s and %s render apart", name, FormatTraceValue(a), FormatTraceValue(b)) + } + if valueEqual(a, b) { + t.Errorf("%s: %s compares equal to its twin", name, FormatValue(a)) + } + c := canonicalCompare(a, b) + if c == 0 || canonicalCompare(b, a) != -c { + t.Errorf("%s: compare = %d, reversed = %d, want opposite non-zero", name, c, canonicalCompare(b, a)) + } + if canonicalCompare(a, a) != 0 || canonicalCompare(b, fn(b.function().shape, b.FunctionSelf(), b.functionRun())) != 0 { + t.Errorf("%s: a function compares non-zero against itself", name) + } + ab, ba := setOf([]Value{a, b}).Set(), setOf([]Value{b, a}).Set() + if ab.Size() != 2 || !ab.Equal(ba) { + t.Fatalf("%s: sets of %s and %s are not two equal members", name, FormatValue(a), FormatValue(b)) + } + x, y := ab.Elements(), ba.Elements() + for i := range x { + if !valueEqual(x[i], y[i]) { + t.Errorf("%s: element %d differs between insertion orders", name, i) + } + } + } } // TestLikeRenderedValuesOrderByContents pins that two unequal structured values diff --git a/internal/core/runtime/set_order.go b/internal/core/runtime/set_order.go index 4fc13c5c9..c2bd235d4 100644 --- a/internal/core/runtime/set_order.go +++ b/internal/core/runtime/set_order.go @@ -72,7 +72,8 @@ func canonicalCompare(a, b Value) int { } // compareContents orders two values of one kind that render alike by what -// valueEqual compares: shape and elements, components, unit, or reference key. +// valueEqual compares: shape and elements, components, unit, reference key, or +// the calc, object and run a function is a value of. func compareContents(a, b Value) int { switch a.Kind { case ValSequence, ValSet: @@ -111,10 +112,24 @@ func compareContents(a, b Value) int { } x, y := a.Expr().Span(), b.Expr().Span() return cmp.Or(cmp.Compare(x.Offset, y.Offset), cmp.Compare(x.Len, y.Len)) + case ValFunction: + return cmp.Or( + compareSymbols(a.Function(), b.Function()), + cmp.Compare(instanceID(a.FunctionSelf()), instanceID(b.FunctionSelf())), + cmp.Compare(a.functionRun(), b.functionRun()), + ) } return 0 } +// instanceID is the identity of the object a function closes over, 0 for none. +func instanceID(inst *Instance) int64 { + if inst == nil { + return 0 + } + return inst.ID +} + // vectorComponents is every axis of the vector as a scalar quantity value. func vectorComponents(vq *VectorQuantity) []Value { out := make([]Value, len(vq.Num)) diff --git a/internal/core/runtime/value_equality.go b/internal/core/runtime/value_equality.go index 72e6b246d..2117976c0 100644 --- a/internal/core/runtime/value_equality.go +++ b/internal/core/runtime/value_equality.go @@ -26,7 +26,7 @@ type valueKey struct { // valueKeyFunc extracts a comparable key from a Value. Values valueEqual holds // equal share a key: a whole number has the Integer's whatever kind carries it, -// every empty value has null's, and a set has the key of its canonical sequence. +// every empty value has null's, and a set hashes its members in canonical order. func valueKeyFunc(v Value) valueKey { if isEmptyValue(v) { return valueKey{kind: ValNull} @@ -59,7 +59,6 @@ func valueKeyFunc(v Value) valueKey { case ValInstance: key.instID = v.Instance case ValSequence, ValSet: - key.kind = ValSequence key.colHash = hashElements(elementsOf(v)) case ValVariant: key.variant = v.Variant() diff --git a/internal/grpc/convert_set_tensor_test.go b/internal/grpc/convert_set_tensor_test.go index 906c06d58..c6814716c 100644 --- a/internal/grpc/convert_set_tensor_test.go +++ b/internal/grpc/convert_set_tensor_test.go @@ -59,6 +59,10 @@ func setOf(elements ...*pb.Value) *pb.Value { return &pb.Value{Kind: &pb.Value_Set{Set: &pb.ValueSet{Elements: elements}}} } +func sequenceValue(elements ...*pb.Value) *pb.Value { + return &pb.Value{Kind: &pb.Value_Sequence{Sequence: &pb.ValueSequence{Elements: elements}}} +} + func tensorQuantityValue(dimensions []int64, components ...*pb.Quantity) *pb.Value { return &pb.Value{Kind: &pb.Value_TensorQuantity{TensorQuantity: &pb.TensorQuantity{Dimensions: dimensions, Components: components}}} } @@ -178,6 +182,8 @@ func TestMalformedSetsAreRejected(t *testing.T) { {"repeated nested set", setOf(setOf(intValue(1)), setOf(intValue(1))), ErrSetElementRepeated}, {"nested sets equal in another order", setOf(setOf(intValue(1), intValue(2)), setOf(intValue(2), intValue(1))), ErrSetElementRepeated}, {"Integer and the equal Real", setOf(intValue(1), realValue(1)), ErrSetElementRepeated}, + {"null and the empty sequence", setOf(&pb.Value{Kind: &pb.Value_Null{}}, sequenceValue()), ErrSetElementRepeated}, + {"empty set and the empty sequence", setOf(setOf(), sequenceValue()), ErrSetElementRepeated}, {"unset element", setOf(&pb.Value{Kind: &pb.Value_Unset{Unset: true}}), ErrUnsetNotAccepted}, {"malformed element", setOf(arrayValue([]int64{0})), ErrArrayDimensionNotPositive}, } @@ -189,6 +195,55 @@ func TestMalformedSetsAreRejected(t *testing.T) { } }) } + + // A set is not the sequence of its members: the two are distinct members of a set. + val, err := ProtoToValueIn(setOf(setOf(intValue(1), intValue(2)), sequenceValue(intValue(1), intValue(2)), sequenceValue(intValue(2), intValue(1))), idx, sem) + if err != nil || val.Kind != runtime.ValSet || val.Set().Size() != 3 { + t.Fatalf("a set holding a set and the two sequences of its members = %s, %v, want three members", runtime.FormatValue(val), err) + } +} + +// functionSetModel reads one calc against two objects, so two function values +// render alike, and lists them in a set both ways round. +const functionSetModel = ` +package G { + private import ScalarValues::*; + private import Collections::*; + calc def Unary { in v : Real; return : Real; } + part def Holder { attribute k : Real; calc scale :> Unary { in :>> v; return : Real = v * k; } } + part a : Holder { :>> k = 2.0; } + part b : Holder { :>> k = 3.0; } + attribute ab : Set { :>> elements = (a.scale, b.scale); } + attribute ba : Set { :>> elements = (b.scale, a.scale); } +} +` + +// Two functions that render alike — one calc read against two objects — are +// two members, sent in one order however the set was written. +func TestSetOfAlikeFunctionsCrossesInOneOrder(t *testing.T) { + srv := mustNewService(t, 4) + modelHash := mustParse(t, srv, functionSetModel) + if eq := mustEvaluate(t, srv, modelHash, "G::ab == G::ba"); !eq.GetBoolValue() { + t.Fatalf("G::ab == G::ba = %v, want true", eq) + } + if ab := mustEvaluate(t, srv, modelHash, "G::ab.elements").GetSet(); ab == nil || len(ab.GetElements()) != 2 { + t.Fatalf("G::ab.elements = %v, want a set of two functions", ab) + } + // Both sets flow into one sequence, so their members' objects are numbered + // within one response and the two enumerations can be compared. + both := mustEvaluate(t, srv, modelHash, "(G::ab.elements, G::ba.elements)").GetSequence().GetElements() + if len(both) != 4 { + t.Fatalf("(G::ab.elements, G::ba.elements) = %v, want four functions", both) + } + for i := range 2 { + x, y := both[i].GetFunction(), both[i+2].GetFunction() + if x == nil || y == nil || x.GetCalcId() != "G::Holder::scale" || x.GetCalcId() != y.GetCalcId() || x.GetSelfId() == 0 || x.GetSelfId() != y.GetSelfId() { + t.Errorf("member %d: %v in ab, %v in ba, want the function G::Holder::scale over one object in both", i, x, y) + } + } + if both[0].GetFunction().GetSelfId() == both[1].GetFunction().GetSelfId() { + t.Errorf("G::ab.elements = %v, want two functions over two objects", both[:2]) + } } // A tensor quantity crosses at any rank as its dimensions and one Quantity per From 0787ad743f67ec4f754b9d36870a913b09370a5f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 20:45:04 +0000 Subject: [PATCH 18/20] fix(runtime): place equal set members alike and withhold sets with unsendable members canonicalCompare orders each value by the representative valueEqual reads it as, so a real-axis complex number sits among the numbers and an empty collection with null, and equal sets holding different representatives enumerate alike; a for loop over a set visits that same canonical order rather than a rendering sort. A set holding a member with no wire form, or one the service withholds, crosses as one unsupported null naming the set instead of nulls in the members' places, which would read back as a repeated member. Co-Authored-By: jason.han --- docs/project/spec-compliance.md | 6 +-- docs/reference/wire-contract.md | 13 ++++- internal/core/runtime/set_feature_test.go | 51 +++++++++++++++++++ internal/core/runtime/set_order.go | 41 +++++++++++---- internal/core/runtime/statements.go | 17 +++---- internal/core/runtime/statements_test.go | 4 +- internal/grpc/capability_response.go | 5 ++ internal/grpc/convert.go | 36 ++++++++++--- internal/grpc/convert_set_tensor_test.go | 61 ++++++++++++++++++++--- 9 files changed, 191 insertions(+), 43 deletions(-) diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index a2717ec9f..0ef0ddeb5 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -516,7 +516,7 @@ by name is refused naming what is missing rather than approximated. | Conditional statement (`if { … } else { … }`) | `lower/action_graph.go` lowerStatement/lowerBlock (`If`); `runtime/action_statements.go` execIf, execBlock | `action_if_else_then_branch.sysml`, `action_if_else_else_branch.sysml`, `action_if_no_else.sysml`, `action_nested_loop_if.sysml` + trace golden, `lower/action_body_test.go:TestActionBodyLoopAndConditionalLowering`, `passes/typecheck_test.go:TestTypeCheckNonBooleanControlFlowConditions` | ✅ Faithful (the condition is evaluated outside both branches; each branch body is a namespace of its own, so the names it declares do not reach the enclosing behavior) | | Pre-condition loop (`while { … }`) | `lower/action_graph.go` lowerStatement (`Loop`, `ast.LoopWhile`); `runtime/action_statements.go` execLoop | `action_while_loop.sysml` + trace golden, `action_while_loop_zero_iterations.sysml`, `parse/action_loop_forms.golden` | ✅ Faithful (tested before every iteration, so the body may run no times) | | Post-condition loop (`loop { … } until ;`) | `parser/behavior.go` parseLoopAction; `lower/action_graph.go` (`ast.LoopUntil`); `runtime/action_statements.go` execLoop | `action_loop_until.sysml`, `action_loop_until_repeats.sysml` + trace golden | ✅ Faithful (tested after every iteration, so the body runs at least once) | -| Iteration over a collection (`for in { … }`) | `ast/behavior.go` `WhileLoopActionNode.Variable`/`Collection`; `symbols/builder.go` (the variable is a member of the loop's own scope); `runtime/statements.go` forLoop, forElements | `action_for_loop.sysml`, `action_for_over_produced_collections.sysml` + trace golden, `parse/action_loop_forms.golden`, `action_for_over_a_part_collection.sysml` + trace golden, `statements_test.go:TestForElementsOrder`, `robustness_test.go:for_over_a_value_no_expression_makes_iterable` | ✅ Faithful (the collection is evaluated once, before the loop is entered. Every collection the expression layer produces is iterated: a sequence in the order the expression built it — a literal sequence as written, a range ascending and a descending range empty (`range.go` rangeSequence), a filter in the order of the collection it filtered, a collection-valued function's result as returned — a set in the order its canonical rendering sorts in, since a set has no order of its own, and `null`, which holds no element, not at all. A `for` input that is not a collection reports `ErrTypeMismatch` naming what it was given) | +| Iteration over a collection (`for in { … }`) | `ast/behavior.go` `WhileLoopActionNode.Variable`/`Collection`; `symbols/builder.go` (the variable is a member of the loop's own scope); `runtime/statements.go` forLoop, forElements | `action_for_loop.sysml`, `action_for_over_produced_collections.sysml` + trace golden, `parse/action_loop_forms.golden`, `action_for_over_a_part_collection.sysml` + trace golden, `statements_test.go:TestForElementsOrder`, `robustness_test.go:for_over_a_value_no_expression_makes_iterable` | ✅ Faithful (the collection is evaluated once, before the loop is entered. Every collection the expression layer produces is iterated: a sequence in the order the expression built it — a literal sequence as written, a range ascending and a descending range empty (`range.go` rangeSequence), a filter in the order of the collection it filtered, a collection-valued function's result as returned — a set in its canonical order (`set_order.go`), since a set has no order of its own, and `null`, which holds no element, not at all. A `for` input that is not a collection reports `ErrTypeMismatch` naming what it was given) | | A `for` input that is not a collection is reported, where a general collection reader coerces one | `runtime/statements.go` `forElements` (`ErrTypeMismatch`), deliberately stricter than `runtime/collections.go` `elementsOf`, which keeps reading a scalar as the one-element collection KerML makes of it — a `for` over a scalar is a modelling error, and a single silent iteration hides it, while the general readers (a collection operator's argument, a multiplicity check) legitimately coerce | `action_for_over_a_scalar.sysml` (typed error), `action_for_over_a_part_collection.sysml` (a valid collection input, nested in a part) + trace golden, `statements_test.go:TestForElementsRejectsANonCollection`, `:TestElementsOfStillCoercesAScalar`, `robustness_test.go:for_over_a_scalar` | ✅ Faithful (ruling: `for` requires a collection; `elementsOf` is unchanged for its other callers) | | A non-terminating loop ends the execution rather than hanging it | `runtime/action_statements.go` execLoop (a step per iteration), `context.go` incrementStep | `action_loop_step_budget.sysml`, `robustness_test.go:non_terminating_loop_exhausts_step_budget` | ✅ Faithful (reports `ErrStepLimitExceeded`, the same failure as any other runaway evaluation) | | A legitimately long loop runs under a raised budget | `budget.go` `BudgetsFromEnv` (`OPENSYSML_MAX_STEPS`) resolved at the REPL/CLI and gRPC entry points | `budget_test.go:TestRaisedBudgetRunsLongerLoop` | ✅ Faithful (a 10 000-iteration loop that exhausts a 100 000-step budget completes under the default) | @@ -1385,10 +1385,10 @@ they cannot drift apart. | `CollectionFunctions`: `size`, `isEmpty`, `notEmpty`, `contains`, `containsAll`, `head`, `tail`, `last`, `#` over a collection's elements, a set included | `runtime/collections.go`, `runtime/builtins.go` | `runtime/collections_test.go` `TestCollectionScalarResults`, `TestCollectionOperationsOverSets` | ✅ Faithful | | A `Collections::Collection` whose library redefinition of `elements` is unique and not ordered holds a **set** (`ValSet`), the library's own kind: `Set` ("unique and unordered", `Collections.kerml:104-108`), `UniqueCollection` ("unique and not necessarily ordered", `:39-48`) and `Map` (unique `KeyValuePair`s, `:142-152`). `Bag` inherits the root's `nonunique` (`:22`, `:97-101`) and stays a sequence, as does every `OrderedCollection` (`:30-36`) — `Array`, `List`, `OrderedSet` (`ordered`, `:111-121`), `OrderedMap` (`:162-175`) — and any feature a model declares `ordered` or `nonunique` itself. A set holds each member once (`(3, 1, 2, 2, 3)` is three members, `(1, 2, 3)` given already distinct is the same three, `()` the empty set), so `size` answers 3 and `isEmpty` reads the count; a value written or bound to such a feature is admitted as a set, and a set flowing into a feature that holds a sequence (`ordered`, `nonunique`, or a plain `Integer[0..*]`) becomes the sequence of its members in canonical order | `runtime/set_feature.go` `holdsSet`, `declaredOrderedOrNonunique`, `collectionOf`, `declaredCollection`; `shape.go` `EffectiveFeature.HoldsSet`; `instance.go` `admitted`, `materializeIntrinsic`; `subsetting.go` (optional subsetters); `value.go` `Set`, `NewSet`, `Set.Add`, `Set.Size` | conformance `library_set_elements`, `library_set_elements_already_distinct`, `library_set_elements_empty`, `library_unique_collection_elements`, `library_map_elements`, `library_bag_elements`, `library_ordered_set_elements`, `library_set_operations`; `runtime/set_feature_test.go:TestCollectionElementsHoldTheLibraryKind`, `:TestSetFlowsIntoDeclaredCollections`, `:TestCollectionFunctionsOverCollectionObjects` | ✅ Faithful | | Two sets are equal when their members are, in whatever order either was given (`CollectionFunctions::'=='` is `col1.elements->equals(col2.elements)`, and a set's `elements` have no order to compare); `contains` and `containsAll` are membership; `==` reads a set against a sequence as its canonical sequence (an ordered context), so a set's elements equal a sequence only when the sequence lists the members in canonical order, while as a set's member a set is never the sequence of its members; a `Set` is never equal to a `Bag` or `OrderedSet` holding the same values, which are sequences | `runtime/eval.go` `equalValues`, `valueEqual` (`ValSet` arm); `runtime/collections.go` `builtinCollectionEquals`, `elementsOf`; `value.go` `Set.Equal`, `Set.Contains` | conformance `library_set_operations` (`equalRegardlessOfOrder`, `elementsEqualRegardlessOfOrder`, `membership`, `allMembers`, `emptiness`, `notASequence`); `runtime/set_feature_test.go:TestSetAgainstSequenceComparesCanonically`, `:TestSetIsNotASequenceAsAMember`, `:TestCollectionFunctionsOverCollectionObjects`; `grpc/convert_set_tensor_test.go:TestMalformedSetsAreRejected` | ✅ Faithful | -| A set has no order of its own, so every operation that walks its members in order — `collect`, `select`, `head`, `tail`, `#`, `==` against a sequence, a trace rendering it, a write into a sequence-holding feature, the wire — sees them in one **canonical order**: by class (null, Booleans with `false` first, numbers ascending, complex numbers by real then imaginary part, strings lexicographically, quantities by dimension then magnitude, enumeration literals by declaration, objects by identity, then every other kind) and within a class by that order, then by the trace rendering, then by contents — elements, components, unit reduction, the calc, object and run a function is a value of — so only equal values share a position. The order is total, so equal sets enumerate alike and a trace over a set is the same golden however the set was written | `runtime/set_order.go` `canonicalLess`, `canonicalClass`; `value.go` `Set.Elements` (sorted once, cached); `trace.go` `FormatTraceValue` (`ValSet`, the same enumeration) | conformance `calc_set_consumed_by_ordered_operations` and its trace golden; `runtime/set_feature_test.go:TestCanonicalOrderIsTotal`, `:TestSameNamedLiteralsOrderByDeclaration`, `:TestFunctionsRenderedAlikeOrderByIdentity`, `:TestLikeRenderedValuesOrderByContents`, `:TestSetRendersCanonically`; `runtime/collections_test.go:TestCollectionOperationsOverSets` | ✅ Faithful — the order is this runtime's documented rule, since the library defines none; it is not a claim about the specification | +| A set has no order of its own, so every operation that walks its members in order — `collect`, `select`, `head`, `tail`, `#`, `==` against a sequence, a trace rendering it, a write into a sequence-holding feature, the wire — sees them in one **canonical order**: by class (null, Booleans with `false` first, numbers ascending, complex numbers by real then imaginary part, strings lexicographically, quantities by dimension then magnitude, enumeration literals by declaration, objects by identity, then every other kind), each value placed by the value it equals whichever kind carries it (a complex number on the real axis among the numbers, an empty sequence or set with null), and within a class by that order, then by kind, then by contents — elements, components, unit reduction, the calc, object and run a function is a value of — so exactly the `valueEqual` values share a position. The order is total, so equal sets enumerate alike and a trace over a set is the same golden however the set was written | `runtime/set_order.go` `canonicalLess`, `canonicalClass`; `value.go` `Set.Elements` (sorted once, cached); `trace.go` `FormatTraceValue` (`ValSet`, the same enumeration) | conformance `calc_set_consumed_by_ordered_operations` and its trace golden; `runtime/set_feature_test.go:TestCanonicalOrderIsTotal`, `:TestSameNamedLiteralsOrderByDeclaration`, `:TestFunctionsRenderedAlikeOrderByIdentity`, `:TestLikeRenderedValuesOrderByContents`, `:TestEqualSetsWithOtherRepresentativesEnumerateAlike`, `:TestSetRendersCanonically`; `runtime/collections_test.go:TestCollectionOperationsOverSets`; `runtime/statements_test.go:TestForElementsOrder` | ✅ Faithful — the order is this runtime's documented rule, since the library defines none; it is not a claim about the specification | | What the library declares ordered stays a sequence: every `SequenceFunctions` result is `Anything[0..*] ordered nonunique` — `union`, `intersection`, `including`, `includingAt`, `excluding` included (`SequenceFunctions.kerml:48-63`) — so `union(s.elements, t.elements)` over two `Set`s is the ordered concatenation of their canonical members, repeats kept, not a set; `(s.elements, s.elements)` likewise lists each member twice. The Kernel Function Library declares no `distinct` function, so none is invented: the members of a sequence, each once, are what a `Set`'s `elements` hold | `runtime/collections.go` (`SequenceFunctions` builtins over `elementsOf`) | conformance `library_set_sequence_functions`, `library_set_operations` (`ordered`, `repeatable`, `plain`) | ✅ Faithful | | `OrderedSet::elements` and `OrderedMap::elements` are declared `ordered` *and* unique (`UniqueCollection` under `OrderedCollection`, `Collections.kerml`), as is any multi-valued feature not declared `nonunique`. Their order is part of the value, so they are held as the sequence written; their uniqueness is not enforced — `OrderedSet { :>> elements = (1, 1, 2); }` reads three elements and `size` answers 3. A set is the one place uniqueness is the value's own definition; checking `unique` as a constraint on an ordered feature's values is separate work | `runtime/set_feature.go` `holdsSet` (declines any feature declared `ordered`) | conformance `library_ordered_set_elements` (order kept) | ⚠️ Approximate — order faithful, uniqueness unchecked | -| A set **crosses gRPC** as the `set` arm (`ValueSet.elements`, each a `Value`, listed in canonical order; an incoming set may list them in any order, and one that repeats a member is `ErrSetElementRepeated`), advertised as `set_values`; a service withholding the capability answers an unsupported null naming the value and refuses one sent to it with `UNIMPLEMENTED`, nested anywhere in the argument. It is **not compiled natively**: `sysml -compile` refuses a calc declaring or reading one with `codegen.UnsupportedError` (`type Collections::Set is not Integer, Real or Boolean`). It has **no RDF literal form** of its own: the mapping writes the model, so a `Set`-valued feature is the expression valuing its `elements`, which round trips exactly | `grpc/convert.go` `setToProto`, `protoToSet`, `ErrSetElementRepeated`; `grpc/capability_response.go` (`set_values`); `codegen/compile.go` `UnsupportedError`; `export/rdf_expr.go` | `grpc/convert_set_tensor_test.go:TestSetRoundTrip`, `:TestMalformedSetsAreRejected`, `:TestSetAndTensorCapabilities`, `:TestValueCarriesSetAndTensor`; `repl/compile_test.go:TestCompileRefusesWhatItCannotCompile` (`SetParam`, `SetElements`, `SetLocal`); `export/set_tensor_rdf_test.go:TestSetAndTensorValuesRoundTripAsExpressions`; the *Values* gRPC rows below | ✅ Faithful (the native and RDF refusals are typed and documented, not layouts) | +| A set **crosses gRPC** as the `set` arm (`ValueSet.elements`, each a `Value`, listed in canonical order; an incoming set may list them in any order, and one that repeats a member is `ErrSetElementRepeated`), advertised as `set_values`; a service withholding the capability answers an unsupported null naming the value and refuses one sent to it with `UNIMPLEMENTED`, nested anywhere in the argument. A set holding a member with no wire form — no arm carries it, or the service withholds its arm — is withheld whole as an unsupported null naming the set and the member's reason, never sent with nulls in the members' places, which two members rendering alike would make a repeated member. It is **not compiled natively**: `sysml -compile` refuses a calc declaring or reading one with `codegen.UnsupportedError` (`type Collections::Set is not Integer, Real or Boolean`). It has **no RDF literal form** of its own: the mapping writes the model, so a `Set`-valued feature is the expression valuing its `elements`, which round trips exactly | `grpc/convert.go` `setToProto`, `protoToSet`, `ErrSetElementRepeated`; `grpc/capability_response.go` (`set_values`); `codegen/compile.go` `UnsupportedError`; `export/rdf_expr.go` | `grpc/convert_set_tensor_test.go:TestSetRoundTrip`, `:TestMalformedSetsAreRejected`, `:TestSetHoldingAMemberWithNoWireFormIsWithheldWhole`, `:TestSetAndTensorCapabilities`, `:TestValueCarriesSetAndTensor`; `repl/compile_test.go:TestCompileRefusesWhatItCannotCompile` (`SetParam`, `SetElements`, `SetLocal`); `export/set_tensor_rdf_test.go:TestSetAndTensorValuesRoundTripAsExpressions`; the *Values* gRPC rows below | ✅ Faithful (the native and RDF refusals are typed and documented, not layouts) | | `CollectionFunctions::'array#'(arr, indexes)` and `BaseFunctions::'#'` with several indexes select from a **`Collections::Array`** value (`ValArray`, *Structured values* under *KerML Function Library*) by one `Positive` index per dimension in the row-major order `Collections.kerml` documents — `'array#'(a, (2, 1))` over `dimensions = (2, 3)` is the fourth element, as the pinned pilot evaluator answers; a vector or vector quantity is indexed as the one-dimensional Array it specializes; a rank-0 array with no index is null, as the library body says. The count of indexes must be the array's `rank` (`ErrMultiplicityViolation`, naming `arr.rank`), each index within `1..dimensions#(i)` (`ErrIndexOutOfRange`, naming the dimension and its range), and a usage whose `elements` do not fill its `dimensions` is `ErrMultiplicityViolation` naming `flattenedSize`; `rank`, `flattenedSize`, `dimensions` and `elements` of the value read out of it | `runtime/collections.go` `builtinArrayIndex`, `arrayIndex`, `builtinBaseIndex`; `runtime/array.go` `Array.at`, `structuredFeature`, `Context.arrayOfObject`, `Context.declaredArrayValue` | conformance `calc_library_array_value`, `calc_library_array_features`, `calc_library_array_index`, `calc_library_array_index_rank_mismatch`, `calc_library_array_index_out_of_range`, `calc_library_array_empty_rank_zero`, `calc_library_array_specialization_members`, `calc_library_array_specialization_through_calc`, `calc_library_base_index_many`, `calc_library_base_index_many_sequence`; robustness `base_index_with_several_indexes`, `numeric_library_call_that_has_no_value` (`'array#'` over a flat sequence); `repl/runtime_commands_test.go:TestEvalArrayShapedByItsFeatures`; `TestEveryValueKindIsDispatched` | ✅ Faithful | | An operation over an empty collection answers the empty collection and never calls its body, since there is no element to call it with | `runtime/collections.go` `elementsOf` (an empty collection yields no elements) | conformance `calc_collection_ops_over_empty`; `runtime/collections_test.go` `TestCollectionResults` | ✅ Faithful | | `ControlFunctions`: `collect`, `select`, `selectOne`, `reject`, `reduce`, `forAll`, `exists`, `allTrue`, `anyTrue`, `minimize`, `maximize` | `runtime/collections.go`, `runtime/builtins.go` | `runtime/collections_test.go` `TestCollectionResults`, `TestCollectionScalarResults`, `TestCollectionOperationErrors` | ✅ Faithful | diff --git a/docs/reference/wire-contract.md b/docs/reference/wire-contract.md index 7d9b8196b..a60e7e035 100644 --- a/docs/reference/wire-contract.md +++ b/docs/reference/wire-contract.md @@ -517,8 +517,10 @@ $ … /Evaluate -d '{"modelHash":"c409…1a4a","expression":"T::s.elements"}' - The model wrote `(3, 1, 2, 2, 3)`; the set has three members. The service lists them in the engine's **canonical order** — the order `FormatTraceValue` prints and every ordered operation on a set reads (nulls, then Booleans, numbers by value, complex numbers, strings, - quantities, enumeration literals, then objects by identity) — so two equal sets are sent - identically, but the order carries no meaning and a client must not read one into it. + quantities, enumeration literals, then objects by identity), each member placed by the value + it equals whichever arm carries it (`1.0 + 0.0i` among the numbers, an empty collection with + `null`) — so two equal sets are sent identically, but the order carries no meaning and a + client must not read one into it. - A `set` is not a `sequence`: `(1, 2) == (2, 1)` is false, the sets they populate are equal. A client compares sets by membership and sends one back in any order it likes. As members of a set, a `set` and the `sequence` of its members are two members, on both sides; only @@ -538,6 +540,13 @@ $ … /Evaluate -d '{"modelHash":"c409…1a4a","expression":"T::s.elements"}' refused on both sides, as is a `set` where the model wants a sequence's order or a sequence where it wants a set; a set flowing into an ordered parameter is read in canonical order. - Members nest: a set of sets, or of arrays, needs no second encoding. +- A set holding a member that has no wire form — one the rule under `null` would send as a + non-empty `null`, whether because no arm carries it (a coordinate frame, a function closing + over a body) or because the service withholds its arm (a complex number without + `complex_values`) — is withheld **whole**, as `{"null":"unsupported: set Set{…} holding + "}`. Two such members would otherwise cross as two equal nulls, which a + set may not hold; a `sequence` of the same members keeps each in its place, so a set nested + in one is the null in the set's place. This also applies to a set nesting such a set. **`tensorQuantity`.** `dimensions` is the shape, `components` the quantities flattened in row-major order, each a `quantity` body with its own magnitude, `unit` and `unitTerm`: diff --git a/internal/core/runtime/set_feature_test.go b/internal/core/runtime/set_feature_test.go index 392341ead..86df48a5b 100644 --- a/internal/core/runtime/set_feature_test.go +++ b/internal/core/runtime/set_feature_test.go @@ -435,6 +435,57 @@ func TestFunctionsRenderedAlikeOrderByIdentity(t *testing.T) { } } +// TestEqualSetsWithOtherRepresentativesEnumerateAlike pins that a member takes +// the place of the value it equals whichever kind carries it: a real-axis +// Complex sits among the numbers, `()` and `{}` with null, so two equal sets +// holding different representatives enumerate, render and cross the wire alike. +func TestEqualSetsWithOtherRepresentativesEnumerateAlike(t *testing.T) { + ints := func(ns ...int64) []Value { + vals := make([]Value, len(ns)) + for i, n := range ns { + vals[i] = integerValue(n) + } + return vals + } + for _, tc := range []struct { + name string + one, other []Value + }{ + {"real-axis complex", []Value{integerValue(3), integerValue(2)}, []Value{integerValue(3), NewComplex(2)}}, + {"real-axis complex among reals", []Value{realConst(2.5), realConst(1.5)}, []Value{realConst(2.5), NewComplex(1.5)}}, + {"empty sequence", []Value{boolValue(true), {Kind: ValNull}}, []Value{boolValue(true), sequenceOf(nil)}}, + {"empty set", []Value{boolValue(true), {Kind: ValNull}}, []Value{boolValue(true), setOf(nil)}}, + {"nested", []Value{sequenceOf([]Value{{Kind: ValNull}, integerValue(2)}), sequenceOf([]Value{boolValue(true), integerValue(1)})}, + []Value{sequenceOf([]Value{setOf(nil), integerValue(2)}), sequenceOf([]Value{boolValue(true), integerValue(1)})}}, + {"integer and real spelt apart", []Value{sequenceOf([]Value{integerValue(2)}), sequenceOf(ints(2, 1))}, + []Value{sequenceOf([]Value{realConst(2)}), sequenceOf(ints(2, 1))}}, + {"complex element", []Value{sequenceOf(ints(10)), sequenceOf(ints(9))}, + []Value{sequenceOf([]Value{NewComplex(10)}), sequenceOf(ints(9))}}, + } { + one, other := setOf(tc.one).Set(), setOf(tc.other).Set() + if !one.Equal(other) || one.Size() != 2 { + t.Fatalf("%s: %s and %s are not equal sets of two", tc.name, FormatValue(setOf(tc.one)), FormatValue(setOf(tc.other))) + } + x, y := one.Elements(), other.Elements() + for i := range x { + if !valueEqual(x[i], y[i]) { + t.Errorf("%s: element %d is %s in one set, %s in the other", tc.name, i, FormatValue(x[i]), FormatValue(y[i])) + } + } + if !valueEqual(sequenceOf(x), sequenceOf(y)) { + t.Errorf("%s: canonical sequences %s and %s differ", tc.name, FormatValue(sequenceOf(x)), FormatValue(sequenceOf(y))) + } + for _, elems := range [][]Value{tc.one, tc.other} { + for i, j := 0, len(elems)-1; i < j; i, j = i+1, j-1 { + elems[i], elems[j] = elems[j], elems[i] + } + if !valueEqual(sequenceOf(setOf(elems).Set().Elements()), sequenceOf(x)) { + t.Errorf("%s: reversed insertion enumerates %s", tc.name, FormatValue(setOf(elems))) + } + } + } +} + // TestLikeRenderedValuesOrderByContents pins that two unequal structured values // whose trace text is the same — a unit spelt alike that reduces differently — // still take one position each, whatever order they were added in. diff --git a/internal/core/runtime/set_order.go b/internal/core/runtime/set_order.go index c2bd235d4..2c7a02ee7 100644 --- a/internal/core/runtime/set_order.go +++ b/internal/core/runtime/set_order.go @@ -15,8 +15,9 @@ import ( // (null, Booleans, numbers, complex numbers, strings, quantities, enumeration // literals, objects, then every other kind), then within a class by their own // order where they have one (numeric, lexicographic, dimension then magnitude, -// declaration, object identity), then by their trace rendering, and finally by -// their elements, so that only equal values compare as neither before nor after. +// declaration, object identity), and otherwise by kind and then by what +// valueEqual compares, so that only equal values compare as neither before nor +// after, whichever kind carries them. func canonicalLess(a, b Value) bool { return canonicalCompare(a, b) < 0 } @@ -24,13 +25,14 @@ func canonicalLess(a, b Value) bool { // canonicalCompare orders a before b as negative, after as positive, and equal // values — valueEqual ones, whose position a set never depends on — as zero. func canonicalCompare(a, b Value) int { + a, b = canonicalRepresentative(a), canonicalRepresentative(b) ca, cb := canonicalClass(a), canonicalClass(b) if ca != cb { return cmp.Compare(ca, cb) } switch ca { case classNull: - return 0 + return cmp.Compare(a.Kind, b.Kind) case classBool: return compareBool(a.Const.Bool, b.Const.Bool) case classNumber: @@ -62,19 +64,34 @@ func canonicalCompare(a, b Value) int { } return cmp.Compare(a.Instance, b.Instance) } - if c := strings.Compare(FormatTraceValue(a), FormatTraceValue(b)); c != 0 { - return c - } if a.Kind != b.Kind { return cmp.Compare(a.Kind, b.Kind) } return compareContents(a, b) } -// compareContents orders two values of one kind that render alike by what -// valueEqual compares: shape and elements, components, unit, reference key, or -// the calc, object and run a function is a value of. +// canonicalRepresentative is the value valueEqual reads v as, so equal values +// of different kinds take one place: null for every empty collection, the Real +// for a complex number on the real axis. +func canonicalRepresentative(v Value) Value { + if isEmptyValue(v) { + return Value{Kind: ValNull} + } + if v.Kind == ValComplex { + if re, ok := v.realPart(); ok { + return realConst(re) + } + } + return v +} + +// compareContents orders two structured values of one kind by what valueEqual +// compares: shape and elements, components, unit, reference key, or the calc, +// object and run a function is a value of. func compareContents(a, b Value) int { + if a.ref == nil || b.ref == nil { + return compareBool(a.ref != nil, b.ref != nil) + } switch a.Kind { case ValSequence, ValSet: return compareElements(elementsOf(a), elementsOf(b)) @@ -111,7 +128,11 @@ func compareContents(a, b Value) int { return compareBool(a.Expr() != nil, b.Expr() != nil) } x, y := a.Expr().Span(), b.Expr().Span() - return cmp.Or(cmp.Compare(x.Offset, y.Offset), cmp.Compare(x.Len, y.Len)) + return cmp.Or( + cmp.Compare(x.Offset, y.Offset), + cmp.Compare(x.Len, y.Len), + strings.Compare(FormatTraceValue(a), FormatTraceValue(b)), + ) case ValFunction: return cmp.Or( compareSymbols(a.Function(), b.Function()), diff --git a/internal/core/runtime/statements.go b/internal/core/runtime/statements.go index de4548852..7eab9eea9 100644 --- a/internal/core/runtime/statements.go +++ b/internal/core/runtime/statements.go @@ -3,7 +3,6 @@ package runtime import ( "fmt" "maps" - "sort" "github.com/Open-MBEE/OpenSysML/internal/core/ast" "github.com/Open-MBEE/OpenSysML/internal/core/lower" @@ -614,11 +613,11 @@ func stmtLabel(stmt lower.Statement) string { // forElements returns the elements a `for` loop visits, in visiting order: a // sequence in the order the expression built it (a range ascending, a filter as -// the collection it filtered), and a set in the order its canonical rendering -// sorts in since a set carries no order of its own. A `for` input that is not a -// collection is reported rather than read as the one-element collection -// elementsOf coerces it to: iterating a scalar is a modelling error, and a -// single silent iteration hides it. +// the collection it filtered), and a set in its canonical order since a set +// carries no order of its own. A `for` input that is not a collection is +// reported rather than read as the one-element collection elementsOf coerces +// it to: iterating a scalar is a modelling error, and a single silent +// iteration hides it. func forElements(value Value) ([]Value, error) { switch value.Kind { case ValSequence: @@ -630,11 +629,7 @@ func forElements(value Value) ([]Value, error) { if value.Set() == nil { return nil, nil } - elements := value.Set().Elements() - sort.Slice(elements, func(i, j int) bool { - return FormatTraceValue(elements[i]) < FormatTraceValue(elements[j]) - }) - return elements, nil + return value.Set().Elements(), nil case ValNull: // An absent value holds no elements, which is the empty collection: zero // iterations, not an error. diff --git a/internal/core/runtime/statements_test.go b/internal/core/runtime/statements_test.go index 8212a2f0c..696632f5c 100644 --- a/internal/core/runtime/statements_test.go +++ b/internal/core/runtime/statements_test.go @@ -8,7 +8,7 @@ import ( ) // A `for` visits a sequence in the order the expression that built it produced, -// and a set in the order its canonical rendering sorts in. +// and a set in its canonical order, numbers ascending. func TestForElementsOrder(t *testing.T) { set := NewSet() for _, n := range []int64{30, 4, 100, 4} { @@ -21,7 +21,7 @@ func TestForElementsOrder(t *testing.T) { }{ "a sequence keeps its own order": {sequenceOf([]Value{integerValue(3), integerValue(1), integerValue(2)}), []int64{3, 1, 2}}, "an empty sequence visits nothing": {sequenceOf(nil), nil}, - "a set sorts by its rendering": {NewSetValue(set), []int64{100, 30, 4}}, + "a set visits canonically": {NewSetValue(set), []int64{4, 30, 100}}, "an empty set visits nothing": {NewSetValue(NewSet()), nil}, "null visits nothing": {nullValue(), nil}, } diff --git a/internal/grpc/capability_response.go b/internal/grpc/capability_response.go index e85470447..8d4a9e403 100644 --- a/internal/grpc/capability_response.go +++ b/internal/grpc/capability_response.go @@ -116,8 +116,13 @@ func (s *Service) filterValueCapabilities(value *pb.Value) { value.Kind = &pb.Value_Null{Null: "unsupported: " + shown.Kind.String() + " " + runtime.FormatValue(shown)} return } + shown := displayValue(value) for _, nested := range nestedValues(value) { s.filterValueCapabilities(nested) + if reason, ok := unsupportedReason(nested); ok { + value.Kind = unsupportedSet(shown, reason).Kind + return + } } case *pb.Value_TensorQuantity: if !s.capabilities.has(CapabilityTensorValues) { diff --git a/internal/grpc/convert.go b/internal/grpc/convert.go index 0293ebb8e..0972a31f9 100644 --- a/internal/grpc/convert.go +++ b/internal/grpc/convert.go @@ -7,6 +7,7 @@ import ( "maps" "math" "slices" + "strings" pb "github.com/Open-MBEE/OpenSysML/api/proto" "github.com/Open-MBEE/OpenSysML/internal/core/ast" @@ -296,7 +297,7 @@ func ValueToProtoIn(rt *runtime.Context, val runtime.Value, idx *symbols.Index) } return &pb.Value{Kind: &pb.Value_Sequence{Sequence: &pb.ValueSequence{Elements: pbElements}}} case runtime.ValSet: - return &pb.Value{Kind: &pb.Value_Set{Set: setToProto(rt, val.Set(), idx)}} + return setToProto(rt, val, idx) case runtime.ValVariant: // The wire Value has no variant form: the object a selected variant // materialized is reported by identity, a valueless selection as unsupported. @@ -400,16 +401,35 @@ func arrayToProto(rt *runtime.Context, a *runtime.Array, idx *symbols.Index) *pb } // setToProto marshals a set's distinct elements in canonical order, each -// converted as any value is. -func setToProto(rt *runtime.Context, s *runtime.Set, idx *symbols.Index) *pb.ValueSet { +// converted as any value is. A member with no wire form withholds the whole +// set: sent as nulls, two such members would read as one repeated. +func setToProto(rt *runtime.Context, val runtime.Value, idx *symbols.Index) *pb.Value { ps := &pb.ValueSet{} - if s == nil { - return ps + if s := val.Set(); s != nil { + for _, elem := range s.Elements() { + pv := ValueToProtoIn(rt, elem, idx) + if reason, ok := unsupportedReason(pv); ok { + return unsupportedSet(val, reason) + } + ps.Elements = append(ps.Elements, pv) + } } - for _, elem := range s.Elements() { - ps.Elements = append(ps.Elements, ValueToProtoIn(rt, elem, idx)) + return &pb.Value{Kind: &pb.Value_Set{Set: ps}} +} + +// unsupportedReason reads the non-empty null arm a value without a wire form +// crosses as; the SysML `null` is the empty one. +func unsupportedReason(pv *pb.Value) (string, bool) { + if null, ok := pv.GetKind().(*pb.Value_Null); ok && null.Null != "" { + return strings.TrimPrefix(null.Null, "unsupported: "), true } - return ps + return "", false +} + +// unsupportedSet is the null arm a set holding a member without a wire form +// crosses as, naming the set and the member's reason. +func unsupportedSet(shown runtime.Value, reason string) *pb.Value { + return &pb.Value{Kind: &pb.Value_Null{Null: "unsupported: " + runtime.ValSet.String() + " " + runtime.FormatValue(shown) + " holding " + reason}} } // tensorQuantityToProto marshals a tensor as its dimensions and one Quantity diff --git a/internal/grpc/convert_set_tensor_test.go b/internal/grpc/convert_set_tensor_test.go index c6814716c..2ed4d0f02 100644 --- a/internal/grpc/convert_set_tensor_test.go +++ b/internal/grpc/convert_set_tensor_test.go @@ -147,10 +147,10 @@ func TestSetRoundTrip(t *testing.T) { if err != nil { t.Fatal(err) } - if got := runtime.FormatValue(back); got != "Set{Set{1, 2, 3}, Set{}}" { + if got := runtime.FormatValue(back); got != "Set{Set{}, Set{1, 2, 3}}" { t.Errorf("set of sets read back as %s", got) } - if got := runtime.FormatValue(displayValue(ValueToProto(back, idx))); got != "Set{Set{1, 2, 3}, Set{}}" { + if got := runtime.FormatValue(displayValue(ValueToProto(back, idx))); got != "Set{Set{}, Set{1, 2, 3}}" { t.Errorf("set of sets crossed as %s", got) } seq := &pb.Value{Kind: &pb.Value_Sequence{Sequence: &pb.ValueSequence{Elements: []*pb.Value{written, intValue(4)}}}} @@ -203,6 +203,53 @@ func TestMalformedSetsAreRejected(t *testing.T) { } } +// A set holding a member with no wire form is withheld whole. Two frames read +// from two objects are two members that render alike; sent as two nulls they +// would read back as one repeated, so the null names the set instead. A +// sequence of the same still crosses member by member, its places kept. +func TestSetHoldingAMemberWithNoWireFormIsWithheldWhole(t *testing.T) { + frames := []runtime.Value{ + runtime.NewCoordinateFrameValue(&runtime.CoordinateFrame{Object: 1, Text: "spatialCF"}), + runtime.NewCoordinateFrameValue(&runtime.CoordinateFrame{Object: 2, Text: "spatialCF"}), + } + set := runtime.NewSet() + for _, frame := range frames { + set.Add(frame) + } + if set.Size() != 2 { + t.Fatalf("two frames read from two objects make a set of %d", set.Size()) + } + const want = "unsupported: set Set{spatialCF [], spatialCF []} holding coordinate frame spatialCF []" + if pv := ValueToProto(runtime.NewSetValue(set), nil); pv.GetNull() != want { + t.Errorf("set of two frames crossed as %v, want null %q", pv, want) + } + seq := runtime.NewSequence() + for _, frame := range frames { + seq.Append(frame) + } + seq.Append(runtime.NewSetValue(set)) + pv := ValueToProto(runtime.NewSequenceValue(seq), nil) + elems := pv.GetSequence().GetElements() + if len(elems) != 3 || elems[0].GetNull() != "unsupported: coordinate frame spatialCF []" || elems[2].GetNull() != want { + t.Errorf("sequence of two frames and their set crossed as %v", pv) + } + + // One member with a wire form beside one without withholds the set too, + // wherever the member lies. + mixed := runtime.NewSet() + mixed.Add(runtime.NewStringValue("a")) + mixed.Add(frames[0]) + if pv := ValueToProto(runtime.NewSetValue(mixed), nil); pv.GetNull() != `unsupported: set Set{"a", spatialCF []} holding coordinate frame spatialCF []` { + t.Errorf("set of a string and a frame crossed as %v", pv) + } + outer := runtime.NewSet() + outer.Add(runtime.NewSetValue(mixed)) + outer.Add(runtime.NewStringValue("b")) + if pv := ValueToProto(runtime.NewSetValue(outer), nil); !strings.HasPrefix(pv.GetNull(), `unsupported: set Set{"b", Set{"a", spatialCF []}} holding set `) { + t.Errorf("set nesting the set crossed as %v", pv) + } +} + // functionSetModel reads one calc against two objects, so two function values // render alike, and lists them in a set both ways round. const functionSetModel = ` @@ -476,16 +523,16 @@ func TestSetAndTensorCapabilities(t *testing.T) { t.Errorf("%s without %s = %v, want null %q", expr, CapabilitySetValues, got, want) } } - // Tensors still cross without set_values, and a set's elements are filtered - // like any values when the arm itself crosses. + // Tensors still cross without set_values, and a set holding a member the + // service withholds is withheld whole, not sent with a null in its place. if got := mustEvaluate(t, noSets, modelHash, "W::cube"); got.GetTensorQuantity() == nil { t.Errorf("W::cube without %s = %v, want a tensor", CapabilitySetValues, got) } noComplex := mustNewServiceWithout(t, CapabilityComplexValues) - pv := setOf(&pb.Value{Kind: &pb.Value_Complex{Complex: ComplexToProto(complex(0, 1))}}) + pv := setOf(intValue(1), &pb.Value{Kind: &pb.Value_Complex{Complex: ComplexToProto(complex(0, 1))}}) noComplex.filterValueCapabilities(pv) - if pv.GetSet() == nil || !strings.Contains(pv.GetSet().GetElements()[0].GetNull(), "complex number") { - t.Errorf("set of a complex without complex_values = %v, want the element withheld", pv) + if want := "unsupported: set Set{1, 0.0 + 1.0i} holding complex number 0.0 + 1.0i"; pv.GetNull() != want { + t.Errorf("set of a complex without complex_values = %v, want null %q", pv, want) } noTensors := mustNewServiceWithout(t, CapabilityTensorValues) From f00ea9855730986b7ea41b05a215130a3014576f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 21:11:32 +0000 Subject: [PATCH 19/20] fix(clients): read null and the empty collections as one set member; refuse a quantity without a magnitude Set membership in the Go, Python, Node, Java and Rust clients now judges a null, an empty sequence and an empty set as one value, as the engine does, so a set spelling the absent value twice is refused before it is sent or when it is decoded. The Go client refuses a Quantity carrying no magnitude with CodeInvalidArgument wherever it is sent (scalar, vector or tensor component) instead of putting an empty quantity on the wire. Co-Authored-By: jason.han --- client/opensysml/convert.go | 26 +++++- client/opensysml/equal.go | 22 ++++- client/opensysml/structured_internal_test.go | 60 +++++++++--- .../java/org/openmbee/opensysml/Value.java | 17 +++- .../openmbee/opensysml/PublicTypesTest.java | 25 +++++ .../opensysml/internal/ProtosTest.java | 20 ++++ clients/node/src/core/values.ts | 19 +++- clients/node/test/values.test.ts | 30 ++++++ clients/python/opensysml/values.py | 11 ++- clients/python/tests/test_set_tensor.py | 24 +++++ clients/rust/opensysml/src/domain.rs | 93 ++++++++++++++++++- docs/reference/wire-contract.md | 9 +- 12 files changed, 321 insertions(+), 35 deletions(-) diff --git a/client/opensysml/convert.go b/client/opensysml/convert.go index 3f26c7d4b..f6c0c7b1f 100644 --- a/client/opensysml/convert.go +++ b/client/opensysml/convert.go @@ -236,7 +236,11 @@ func valueToProto(value Value) (*pb.Value, error) { } return &pb.Value{Kind: &pb.Value_Sequence{Sequence: sequence}}, nil case Quantity: - return &pb.Value{Kind: &pb.Value_Quantity{Quantity: quantityToProto(v)}}, nil + sent, err := quantityToProto(v) + if err != nil { + return nil, err + } + return &pb.Value{Kind: &pb.Value_Quantity{Quantity: sent}}, nil case EnumLiteral: return &pb.Value{Kind: &pb.Value_EnumLiteral{EnumLiteral: &pb.EnumLiteral{ LiteralId: v.LiteralID, @@ -269,7 +273,11 @@ func valueToProto(value Value) (*pb.Value, error) { case VectorQuantity: vq := &pb.VectorQuantity{Components: make([]*pb.Quantity, 0, len(v))} for _, component := range v { - vq.Components = append(vq.Components, quantityToProto(component)) + sent, err := quantityToProto(component) + if err != nil { + return nil, err + } + vq.Components = append(vq.Components, sent) } return &pb.Value{Kind: &pb.Value_VectorQuantity{VectorQuantity: vq}}, nil case MeasurementRef: @@ -308,7 +316,11 @@ func valueToProto(value Value) (*pb.Value, error) { Components: make([]*pb.Quantity, 0, len(v.Components)), } for _, component := range v.Components { - tq.Components = append(tq.Components, quantityToProto(component)) + sent, err := quantityToProto(component) + if err != nil { + return nil, err + } + tq.Components = append(tq.Components, sent) } return &pb.Value{Kind: &pb.Value_TensorQuantity{TensorQuantity: tq}}, nil case Unset: @@ -321,16 +333,20 @@ func valueToProto(value Value) (*pb.Value, error) { } } -func quantityToProto(quantity Quantity) *pb.Quantity { +// quantityToProto marshals a quantity, refusing one without a magnitude — +// the zero Quantity — which the service would reject. +func quantityToProto(quantity Quantity) (*pb.Quantity, error) { out := &pb.Quantity{Unit: quantity.Unit} switch magnitude := quantity.Magnitude.(type) { case Int: out.Magnitude = &pb.Quantity_IntMagnitude{IntMagnitude: int64(magnitude)} case Real: out.Magnitude = &pb.Quantity_RealMagnitude{RealMagnitude: float64(magnitude)} + default: + return nil, &StatusError{Code: CodeInvalidArgument, Message: fmt.Sprintf("quantity in %q carries no magnitude", quantity.Unit)} } out.UnitTerm = unitTermToProto(quantity.Term) - return out + return out, nil } func unitTermToProto(term *UnitTerm) *pb.UnitTerm { diff --git a/client/opensysml/equal.go b/client/opensysml/equal.go index 26e325e2b..524130998 100644 --- a/client/opensysml/equal.go +++ b/client/opensysml/equal.go @@ -16,16 +16,17 @@ import ( // MeasurementRef is one reduction at one scale however spelt, except that a // named unit of dimension one is only its own declaration (rad is not sr); an // EnumLiteral is its LiteralID, whatever else describes it; a Null is one -// whatever its reason; and a nil Value equals only another nil. +// whatever its reason, and one with an empty Sequence or Set, the model's +// absent value however spelt; and a nil Value equals only another nil. func Equal(a, b Value) bool { + if empty(a) || empty(b) { + return empty(a) && empty(b) + } switch x := a.(type) { case nil: return b == nil case Int, Real, Complex: return numbersEqual(a, b) - case Null: - _, ok := b.(Null) - return ok case Sequence: y, ok := b.(Sequence) return ok && slices.EqualFunc(x, y, Equal) @@ -58,6 +59,19 @@ func Equal(a, b Value) bool { } } +// empty reports the model's absent value: a Null, or a collection with no members. +func empty(v Value) bool { + switch x := v.(type) { + case Null: + return true + case Sequence: + return len(x) == 0 + case Set: + return len(x) == 0 + } + return false +} + // Contains reports whether value is a member of the set. func (s Set) Contains(value Value) bool { return slices.ContainsFunc(s, func(e Value) bool { return Equal(e, value) }) diff --git a/client/opensysml/structured_internal_test.go b/client/opensysml/structured_internal_test.go index 63a7a1f96..318948c3a 100644 --- a/client/opensysml/structured_internal_test.go +++ b/client/opensysml/structured_internal_test.go @@ -197,6 +197,7 @@ func TestRepeatedSetMembersAreNullsNamingTheFault(t *testing.T) { pbSet := func(elements ...*pb.Value) *pb.Value { return &pb.Value{Kind: &pb.Value_Set{Set: &pb.ValueSet{Elements: elements}}} } + pbNull := &pb.Value{Kind: &pb.Value_Null{Null: ""}} pbQty := func(n int64, unit string) *pb.Value { return &pb.Value{Kind: &pb.Value_Quantity{Quantity: &pb.Quantity{Magnitude: &pb.Quantity_IntMagnitude{IntMagnitude: n}, Unit: unit}}} } @@ -204,15 +205,19 @@ func TestRepeatedSetMembersAreNullsNamingTheFault(t *testing.T) { return &pb.Value{Kind: &pb.Value_Quantity{Quantity: &pb.Quantity{Magnitude: &pb.Quantity_RealMagnitude{RealMagnitude: x}, Unit: unit}}} } for name, value := range map[string]*pb.Value{ - "integer twice": pbSet(pbInt(1), pbInt(2), pbInt(1)), - "integer and real": pbSet(pbInt(1), pbReal(1)), - "real and complex": pbSet(pbReal(1.5), pbComplex(1.5, 0)), - "integer and complex": pbSet(pbInt(2), pbComplex(2, 0)), - "sequence twice": pbSet(pbSeq(pbInt(1), pbInt(2)), pbSeq(pbInt(1), pbInt(2))), - "set twice, reordered": pbSet(pbSet(pbInt(1), pbInt(2)), pbSet(pbInt(2), pbInt(1))), - "quantity twice": pbSet(pbQty(1, "m"), pbQty(1, "m")), - "quantity by real": pbSet(pbQty(1, "m"), pbRealQty(1, "m")), - "empty set twice": pbSet(pbSet(), pbSet()), + "integer twice": pbSet(pbInt(1), pbInt(2), pbInt(1)), + "integer and real": pbSet(pbInt(1), pbReal(1)), + "real and complex": pbSet(pbReal(1.5), pbComplex(1.5, 0)), + "integer and complex": pbSet(pbInt(2), pbComplex(2, 0)), + "sequence twice": pbSet(pbSeq(pbInt(1), pbInt(2)), pbSeq(pbInt(1), pbInt(2))), + "set twice, reordered": pbSet(pbSet(pbInt(1), pbInt(2)), pbSet(pbInt(2), pbInt(1))), + "quantity twice": pbSet(pbQty(1, "m"), pbQty(1, "m")), + "quantity by real": pbSet(pbQty(1, "m"), pbRealQty(1, "m")), + "empty set twice": pbSet(pbSet(), pbSet()), + "null and empty sequence": pbSet(pbNull, pbSeq()), + "null and empty set": pbSet(pbInt(1), pbNull, pbSet()), + "empty sequence and set": pbSet(pbSeq(), pbSet()), + "empties nested": pbSet(pbSet(pbNull, pbInt(1)), pbSet(pbInt(1), pbSeq())), } { t.Run(name, func(t *testing.T) { got := valueFromProto(value) @@ -250,6 +255,12 @@ func TestRepeatedSetMembersAreNullsNamingTheFault(t *testing.T) { if !Equal(Null("a"), Null("b")) || !Equal(Unset{}, Unset{}) || Equal(Unset{}, Null("")) { t.Error("a Null is not one whatever its reason, or an Unset is not one") } + if !Equal(Null(""), Sequence{}) || !Equal(Null("unsupported: x"), Set{}) || !Equal(Sequence{}, Set{}) || !Equal(Set(nil), Sequence{}) { + t.Error("a Null and the empty collections are not one value") + } + if Equal(Null(""), Sequence{Int(1)}) || Equal(Set{}, Set{Set{}}) || Equal(Sequence{}, Unset{}) || Equal(Set{}, nil) { + t.Error("an empty value equals one that is not") + } } // Equal judges numbers as the service does: by value across Int, Real and a @@ -467,12 +478,16 @@ func TestEqualJudgesEnumLiteralsByID(t *testing.T) { // look alike is sent. func TestRepeatedSetMembersAreNotSent(t *testing.T) { for name, set := range map[string]Set{ - "integer twice": {Int(1), Int(2), Int(1)}, - "integer and real": {Int(1), Real(1)}, - "real and complex": {Real(1.5), Complex(complex(1.5, 0))}, - "sequence twice": {Sequence{Int(1), Int(2)}, Sequence{Int(1), Int(2)}}, - "set twice, reordered": {Set{Int(1), Int(2)}, Set{Int(2), Int(1)}}, - "nested": {Int(3), Set{Int(1), Int(1)}}, + "integer twice": {Int(1), Int(2), Int(1)}, + "integer and real": {Int(1), Real(1)}, + "real and complex": {Real(1.5), Complex(complex(1.5, 0))}, + "sequence twice": {Sequence{Int(1), Int(2)}, Sequence{Int(1), Int(2)}}, + "set twice, reordered": {Set{Int(1), Int(2)}, Set{Int(2), Int(1)}}, + "nested": {Int(3), Set{Int(1), Int(1)}}, + "null and empty sequence": {Null(""), Sequence{}}, + "null and empty set": {Int(1), Null(""), Set{}}, + "empty sequence and set": {Sequence{}, Set{}}, + "empty sequences reordered": {Set{Set{}, Int(1)}, Set{Int(1), Sequence{}}}, } { t.Run(name, func(t *testing.T) { _, err := valueToProto(set) @@ -488,6 +503,7 @@ func TestRepeatedSetMembersAreNotSent(t *testing.T) { "sequence and set": {Sequence{Int(1)}, Set{Int(1)}}, "sequences reordered": {Sequence{Int(1), Int(2)}, Sequence{Int(2), Int(1)}}, "empty and singleton": {Set{}, Set{Set{}}}, + "null and a singleton": {Null(""), Sequence{Int(1)}, Set{Int(1)}}, } { t.Run(name, func(t *testing.T) { sent, err := valueToProto(set) @@ -520,6 +536,7 @@ func TestMalformedTensorsAreNotSent(t *testing.T) { "negative dimension": {TensorQuantity{Dimensions: []int64{-1}, Components: components(1)}, "tensor dimension is not positive"}, "overflowing shape": {TensorQuantity{Dimensions: []int64{1 << 40, 1 << 40}, Components: components(1)}, "tensor components do not fill its dimensions"}, "nested in a set": {TensorQuantity{Dimensions: []int64{2}, Components: components(1)}, "tensor components do not fill its dimensions"}, + "no magnitude": {TensorQuantity{Dimensions: []int64{2}, Components: []Quantity{pascal, {Unit: "Pa"}}}, `quantity in "Pa" carries no magnitude`}, } { t.Run(name, func(t *testing.T) { var input Value = tc.tensor @@ -541,6 +558,19 @@ func TestMalformedTensorsAreNotSent(t *testing.T) { if got := valueFromProto(sent); !reflect.DeepEqual(got, scalar) { t.Errorf("rank-0 tensor read back as %#v, want %#v", got, scalar) } + for name, input := range map[string]Value{ + "quantity": Quantity{Unit: "Pa"}, + "vector quantity": VectorQuantity{pascal, {Unit: "Pa"}}, + "in a sequence": Sequence{Int(1), Quantity{Unit: "Pa"}}, + } { + t.Run("no magnitude/"+name, func(t *testing.T) { + _, err := valueToProto(input) + var status *StatusError + if !errors.As(err, &status) || status.Code != CodeInvalidArgument || status.Message != `quantity in "Pa" carries no magnitude` { + t.Fatalf("sent with err %v, want an invalid-argument StatusError naming the magnitude", err) + } + }) + } } // A malformed measurement reference in an answer reads as an unsupported null diff --git a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/Value.java b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/Value.java index 8fbdaf9e3..8ebe7581b 100644 --- a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/Value.java +++ b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/Value.java @@ -542,15 +542,19 @@ public Optional unit() { * scale by whole factors — and one lacking a reduction is compared in its unit as written; a * {@link MeasurementRefValue} is one reduction at one scale however spelt, except that a named * unit of dimension one is only its own declaration ({@code rad} is not {@code sr}); an {@link - * EnumerationValue} is its {@link EnumLiteral#literalId()}, whatever else describes it. - * Every other arm compares as {@link Object#equals} does, which stays - * structural: {@code new IntegerValue(1).equals(new RealValue(1.0))} is {@code false}. + * EnumerationValue} is its {@link EnumLiteral#literalId()}, whatever else describes it; a {@link + * NullValue}, an empty sequence and an empty set are one value, the model's absent value however + * spelt. Every other arm compares as {@link Object#equals} does, which stays structural: {@code + * new IntegerValue(1).equals(new RealValue(1.0))} is {@code false}. * * @param other the value to compare with * @return {@code true} when the model would not tell the two apart */ default boolean sameValue(Value other) { Objects.requireNonNull(other, "other"); + if (isEmpty(this) || isEmpty(other)) { + return isEmpty(this) && isEmpty(other); + } if (this instanceof IntegerValue || this instanceof RealValue || this instanceof ComplexValue) { return numbersEqual(this, other); } @@ -582,6 +586,13 @@ default boolean sameValue(Value other) { return equals(other); } + /** The model's absent value: a null, or a collection with no members. */ + private static boolean isEmpty(Value value) { + return value instanceof NullValue + || (value instanceof Sequence sequence && sequence.elements().isEmpty()) + || (value instanceof SetValue set && set.elements().isEmpty()); + } + // One reduction at one scale (SI::'m/s' is m/s, km/m is m/mm); a named unit // reducing to nothing is only the declaration it names. private static boolean measurementRefsEqual(MeasurementRefValue a, MeasurementRefValue b) { diff --git a/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/PublicTypesTest.java b/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/PublicTypesTest.java index 3d9377120..5d5022f38 100644 --- a/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/PublicTypesTest.java +++ b/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/PublicTypesTest.java @@ -187,6 +187,31 @@ void aSetJudgesItsMembersAsTheModelDoes() { .sameValue(new Value.TensorQuantityValue(List.of(1L), List.of(metres(1.0))))); assertFalse(new Value.NullValue().sameValue(new Value.UnsetValue())); assertTrue(new Value.NullValue().sameValue(new Value.NullValue())); + + // A null and the empty collections are one value: the absent value however spelt. + Value nul = new Value.NullValue(); + Value emptySequence = new Value.Sequence(List.of()); + Value emptySet = new Value.SetValue(List.of()); + for (Value x : List.of(nul, emptySequence, emptySet)) { + for (Value y : List.of(nul, emptySequence, emptySet)) { + assertTrue(x.sameValue(y), x + " vs " + y); + assertThrows( + IllegalArgumentException.class, () -> new Value.SetValue(List.of(one, x, y)), x + " " + y); + } + assertFalse(x.sameValue(new Value.UnsetValue())); + assertFalse(x.sameValue(new Value.SetValue(List.of(emptySet)))); + assertFalse(x.sameValue(new Value.Sequence(List.of(one)))); + assertFalse(x.sameValue(new Value.BooleanValue(false))); + } + assertEquals(1, new Value.SetValue(List.of(nul)).size()); + assertTrue(new Value.SetValue(List.of(nul)).contains(emptySequence)); + assertTrue(new Value.SetValue(List.of(emptySet)).contains(nul)); + assertEquals(new Value.SetValue(List.of(one, nul)), new Value.SetValue(List.of(emptySet, one))); + assertTrue( + new Value.Sequence(List.of(one, nul)) + .sameValue(new Value.Sequence(List.of(one, emptySequence)))); + assertEquals( + 2, new Value.SetValue(List.of(nul, new Value.Sequence(List.of(emptySequence)))).size()); } private static Quantity metres(Number magnitude) { diff --git a/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/internal/ProtosTest.java b/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/internal/ProtosTest.java index 235eb4a29..a877399dd 100644 --- a/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/internal/ProtosTest.java +++ b/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/internal/ProtosTest.java @@ -324,6 +324,26 @@ void aSetHoldsEachMemberOnceAndComparesInAnyOrder() { org.openmbee.opensysml.proto.Value unknown = set(org.openmbee.opensysml.proto.Value.getDefaultInstance()); assertThrows(TransportException.class, () -> Protos.value(unknown)); + + // A null and the empty collections are one member: the absent value however spelt. + org.openmbee.opensysml.proto.Value nul = + org.openmbee.opensysml.proto.Value.newBuilder().setNull("").build(); + org.openmbee.opensysml.proto.Value emptySequence = + org.openmbee.opensysml.proto.Value.newBuilder() + .setSequence(ValueSequence.newBuilder()) + .build(); + for (org.openmbee.opensysml.proto.Value spelt : + List.of( + set(nul, emptySequence), + set(integer(1), nul, set()), + set(emptySequence, set()), + set(set(nul, integer(1)), set(integer(1), emptySequence)))) { + TransportException absent = assertThrows(TransportException.class, () -> Protos.value(spelt)); + assertTrue(absent.getMessage().contains("twice"), absent.getMessage()); + } + assertEquals(2, ((Value.SetValue) Protos.value(set(nul, integer(0))).orElseThrow()).size()); + assertEquals( + 2, ((Value.SetValue) Protos.value(set(set(), set(set()))).orElseThrow()).size()); } @Test diff --git a/clients/node/src/core/values.ts b/clients/node/src/core/values.ts index ac6e339ea..c4e088c26 100644 --- a/clients/node/src/core/values.ts +++ b/clients/node/src/core/values.ts @@ -584,9 +584,13 @@ function uniqueMembers(members: SysMLValue[]): SysMLValue[] { * named unit of dimension one is only its own declaration (`rad` is not `sr`); * an `enum` is its `literalId`, whatever else describes it; a `function` is * its `calcId` read against its `selfId`; a `null` is the same whatever its - * reason. + * reason, and the same as an empty `sequence` or `set` — the model's absent + * value however spelt. */ export function valuesEqual(a: SysMLValue, b: SysMLValue): boolean { + if (isEmpty(a) || isEmpty(b)) { + return isEmpty(a) && isEmpty(b); + } switch (a.kind) { case "int": case "real": @@ -645,6 +649,19 @@ export function valuesEqual(a: SysMLValue, b: SysMLValue): boolean { } } +/** The model's absent value: a `null`, or a collection with no members. */ +function isEmpty(value: SysMLValue): boolean { + switch (value.kind) { + case "null": + return true; + case "sequence": + case "set": + return value.elements.length === 0; + default: + return false; + } +} + function elementsEqual(a: SysMLValue[], b: SysMLValue[]): boolean { return ( a.length === b.length && diff --git a/clients/node/test/values.test.ts b/clients/node/test/values.test.ts index 8cfb4ba9f..99ef213ed 100644 --- a/clients/node/test/values.test.ts +++ b/clients/node/test/values.test.ts @@ -324,6 +324,7 @@ const complex = (real: number, imaginary: number) => create(ValueSchema, { kind: { case: "complex", value: create(ComplexSchema, { real, imaginary }) } }); const intMetres = (value: bigint) => create(QuantitySchema, { ...metres(Number(value)), magnitude: { case: "intMagnitude", value } }); +const nul = () => create(ValueSchema, { kind: { case: "null", value: "" } }); test("a set that lists a member twice is malformed, judged by value", () => { const twice = [ @@ -338,6 +339,10 @@ test("a set that lists a member twice is malformed, judged by value", () => { setOf(quantity(metres(1)), quantity(metres(1))), setOf(bool(true), seqOf(), bool(true)), setOf(array([1n], int(1n)), array([1n], int(1n))), + setOf(nul(), seqOf()), + setOf(int(1n), nul(), setOf()), + setOf(seqOf(), setOf()), + setOf(setOf(nul(), int(1n)), setOf(int(1n), seqOf())), ]; for (const set of twice) { assert.throws(() => decodeValue(set), { @@ -359,6 +364,8 @@ test("a set that lists a member twice is malformed, judged by value", () => { setOf(setOf(), setOf(setOf())), setOf(array([1n, 2n], int(1n), int(2n)), array([2n, 1n], int(1n), int(2n))), setOf(quantity(metres(1)), quantity(metres(2))), + setOf(nul(), seqOf(int(1n))), + setOf(nul(), int(0n)), ]; for (const set of alike) { const decoded = decodeValue(set); @@ -374,6 +381,29 @@ test("a set that lists a member twice is malformed, judged by value", () => { assert.ok(!valuesEqual(a, decodeValue(setOf(int(1n), setOf(int(2n)))))); assert.ok(valuesEqual({ kind: "null", reason: "x" }, { kind: "null", reason: "y" })); assert.ok(!valuesEqual({ kind: "unset" }, { kind: "absent" })); + + // A null and the empty collections are one value: the absent value however spelt. + const empties: SysMLValue[] = [ + { kind: "null", reason: "" }, + { kind: "null", reason: "unsupported: x" }, + { kind: "sequence", elements: [] }, + { kind: "set", elements: [] }, + ]; + for (const x of empties) { + for (const y of empties) { + assert.ok(valuesEqual(x, y), `${formatValue(x)} vs ${formatValue(y)}`); + } + assert.ok(!valuesEqual(x, { kind: "unset" })); + assert.ok(!valuesEqual(x, { kind: "absent" })); + assert.ok(!valuesEqual(x, { kind: "set", elements: [{ kind: "set", elements: [] }] })); + assert.ok(!valuesEqual(x, { kind: "boolean", value: false })); + } + for (const [x, y] of [[empties[0], empties[2]], [empties[0], empties[3]], [empties[2], empties[3]]]) { + assert.throws(() => encodeValue({ kind: "set", elements: [{ kind: "int", value: 1n }, x, y] }), { + name: "MalformedValueError", + message: /^a set lists a member twice: /, + }); + } }); test("valuesEqual judges numbers by value, as the service does", () => { diff --git a/clients/python/opensysml/values.py b/clients/python/opensysml/values.py index 8c2f6c424..efd00dffb 100644 --- a/clients/python/opensysml/values.py +++ b/clients/python/opensysml/values.py @@ -829,8 +829,12 @@ def same_value(a: Any, b: Any) -> bool: """Whether two decoded values are the same value, as :class:`SetValue` membership judges it. ``==`` decides, except that a ``bool`` is never a number — ``True`` and ``1`` - are distinct values in a model — in a nested ``list`` or :class:`Array` too. + are distinct values in a model — in a nested ``list`` or :class:`Array` too, + and that ``None``, an empty ``list`` and an empty set are one value, the + model's absent value however spelt. """ + if _is_empty(a) or _is_empty(b): + return _is_empty(a) and _is_empty(b) if isinstance(a, bool) or isinstance(b, bool): return isinstance(a, bool) and isinstance(b, bool) and a == b if isinstance(a, list) and isinstance(b, list): @@ -838,6 +842,11 @@ def same_value(a: Any, b: Any) -> bool: return a == b +def _is_empty(value: Any) -> bool: + """The model's absent value: ``None`` or a collection with no members.""" + return value is None or (isinstance(value, (list, set, frozenset, SetValue)) and len(value) == 0) + + @dataclass(frozen=True) class InstanceRef: """A reference to an instance the client has no instance graph to resolve. diff --git a/clients/python/tests/test_set_tensor.py b/clients/python/tests/test_set_tensor.py index e07733ba5..805683294 100644 --- a/clients/python/tests/test_set_tensor.py +++ b/clients/python/tests/test_set_tensor.py @@ -31,12 +31,14 @@ InstanceRef, MeasurementRef, Quantity, + UNSET, SetValue, TensorQuantity, Unit, UnitFactor, Vector, VectorQuantity, + same_value, value_to_python, ) @@ -172,6 +174,10 @@ def pb_instance(instance_id): (sysml_pb2.Value(bool_value=True), pb_seq(), sysml_pb2.Value(bool_value=True)), (pb_instance(1), pb_int(2), pb_instance(1)), (pb_array((2,), pb_int(1), pb_int(2)), pb_array((2,), pb_int(1), sysml_pb2.Value(real_value=2.0))), + (sysml_pb2.Value(null=""), pb_seq()), + (pb_int(1), sysml_pb2.Value(null=""), pb_set()), + (pb_seq(), pb_set()), + (pb_set(sysml_pb2.Value(null=""), pb_int(1)), pb_set(pb_int(1), pb_seq())), ]) def test_a_set_listing_a_member_twice_is_malformed(elements): with pytest.raises(UnsupportedValueError, match="malformed set: set lists a member twice"): @@ -191,6 +197,12 @@ def test_a_set_listing_a_member_twice_is_malformed(elements): (True, [], True), (InstanceRef(1), 2, InstanceRef(1)), (Array((2,), (1, 2)), Array((2,), (1, 2.0))), + (None, []), + (1, None, SetValue()), + ([], SetValue()), + (None, set()), + ([], frozenset()), + (SetValue((SetValue(), 1)), SetValue((1, []))), ]) def test_a_set_is_never_assembled_with_a_member_twice(elements): with pytest.raises(ValueError, match="set lists a member twice"): @@ -198,6 +210,18 @@ def test_a_set_is_never_assembled_with_a_member_twice(elements): assert len(SetValue((1, 2 ** 53 + 1, float(2 ** 53), True, [1, 2], [2, 1], [1], SetValue((1,))))) == 8 +def test_null_and_the_empty_collections_are_one_value(): + """As the service judges them: the absent value, however spelt.""" + for empty in ([], SetValue(), set(), frozenset()): + assert same_value(None, empty) and same_value(empty, None) and same_value(empty, []) + assert empty in SetValue((1, None)) and None in SetValue((empty,)) + assert SetValue((None, 1)) == SetValue((1, [])) == SetValue((SetValue(), 1)) + assert not same_value(None, [1]) and not same_value([], SetValue((SetValue(),))) + assert not same_value(None, 0) and not same_value([], False) and not same_value(None, UNSET) + assert len(SetValue((None, [1], SetValue((1,)), SetValue((SetValue(),))))) == 4 + assert value_to_python(pb_set(sysml_pb2.Value(null=""), pb_seq(pb_int(1)))) == SetValue((None, [1])) + + def length(text, magnitude, scale_num=1.0, scale_den=1.0, factors=(("SI::metre", 1.0),)): return Quantity(magnitude, Unit(text, scale_num, scale_den, tuple(UnitFactor(*f) for f in factors))) diff --git a/clients/rust/opensysml/src/domain.rs b/clients/rust/opensysml/src/domain.rs index c89c384c2..032de0b9c 100644 --- a/clients/rust/opensysml/src/domain.rs +++ b/clients/rust/opensysml/src/domain.rs @@ -585,9 +585,13 @@ impl Value { /// compared in its unit as written; a measurement reference is one /// reduction at one scale however spelt, except that a named unit of /// dimension one is only its own declaration (`rad` is not `sr`); an - /// enumeration literal is its `literal_id`, whatever else describes it. - /// Every other arm compares as `==` does. + /// enumeration literal is its `literal_id`, whatever else describes it; + /// [`Value::Null`], an empty sequence and an empty set are one value, the + /// model's absent value however spelt. Every other arm compares as `==` does. pub fn same_value(&self, other: &Value) -> bool { + if self.is_absent() || other.is_absent() { + return self.is_absent() && other.is_absent(); + } match (self, other) { (Value::Integer(_) | Value::Real(_) | Value::Complex(_), _) => { numbers_equal(self, other) @@ -615,6 +619,16 @@ impl Value { _ => self == other, } } + + /// The model's absent value: a null, or a collection with no members. + fn is_absent(&self) -> bool { + match self { + Value::Null => true, + Value::Sequence(elements) => elements.is_empty(), + Value::Set(set) => set.is_empty(), + _ => false, + } + } } // One reduction at one scale (`SI::'m/s'` is `m/s`, `km/m` is `m/mm`); a named @@ -1696,6 +1710,81 @@ mod tests { )); } + /// A null and the empty collections are one member: the absent value + /// however spelt, on the wire and when a set is built locally. + #[test] + fn empty_members_are_one_value() { + let null = || wire::Value { + kind: Some(wire::value::Kind::Null(String::new())), + }; + let sequence = |elements| wire::Value { + kind: Some(wire::value::Kind::Sequence(wire::ValueSequence { + elements, + })), + }; + for twice in [ + set(vec![null(), sequence(vec![])]), + set(vec![int(1), null(), set(vec![])]), + set(vec![sequence(vec![]), set(vec![])]), + set(vec![ + set(vec![null(), int(1)]), + set(vec![int(1), sequence(vec![])]), + ]), + ] { + let twice = value_from_wire(twice); + assert!( + matches!(&twice, Err(Error::Decode(message)) if message.contains("twice")), + "{twice:?}" + ); + } + for alike in [ + set(vec![null(), sequence(vec![int(1)])]), + set(vec![null(), int(0)]), + set(vec![set(vec![]), set(vec![set(vec![])])]), + ] { + let Ok(Value::Set(two)) = value_from_wire(alike) else { + panic!("members that only look alike should decode"); + }; + assert_eq!(two.len(), 2); + } + + let empties = [ + Value::Null, + Value::Sequence(vec![]), + Value::Set(Set::new(vec![]).expect("empty set")), + ]; + for x in &empties { + for y in &empties { + assert!(x.same_value(y), "{x:?} vs {y:?}"); + assert!( + Set::new(vec![Value::Integer(1), x.clone(), y.clone()]).is_err(), + "{x:?} {y:?}" + ); + } + assert!(!x.same_value(&Value::Unset)); + assert!(!x.same_value(&Value::Boolean(false))); + assert!(!x.same_value(&Value::Sequence(vec![Value::Integer(1)]))); + assert!(!x.same_value(&Value::Set( + Set::new(vec![Value::Sequence(vec![])]).expect("one member") + ))); + } + let holding_null = Set::new(vec![Value::Integer(1), Value::Null]).expect("two members"); + assert!(holding_null.contains(&Value::Sequence(vec![]))); + assert_eq!( + holding_null, + Set::new(vec![ + Value::Set(Set::new(vec![]).expect("empty set")), + Value::Integer(1), + ]) + .expect("two members") + ); + assert!( + Value::Sequence(vec![Value::Integer(1), Value::Null]).same_value(&Value::Sequence( + vec![Value::Integer(1), Value::Sequence(vec![])] + )) + ); + } + fn complex(real: f64, imaginary: f64) -> wire::Value { wire::Value { kind: Some(wire::value::Kind::Complex(wire::Complex { diff --git a/docs/reference/wire-contract.md b/docs/reference/wire-contract.md index a60e7e035..d3be736de 100644 --- a/docs/reference/wire-contract.md +++ b/docs/reference/wire-contract.md @@ -532,10 +532,11 @@ $ … /Evaluate -d '{"modelHash":"c409…1a4a","expression":"T::s.elements"}' member; a `measurementRef` by its reduction at its scale, however it is spelt or which declaration names it (`SI::'m/s'` and `m / s` are one member, `km / m` and `m / mm` too), except that a named unit of dimension one reduces to nothing and so is only its own - declaration (`rad` is not `sr`); an `enumLiteral` by its `literalId` alone. The bundled - clients' equality helpers judge the same way, converting a quantity through its `unitTerm` - (exactly, while the magnitude is an integer and the scale a whole ratio); a quantity sent - without a `unitTerm` they compare in its unit as written. + declaration (`rad` is not `sr`); an `enumLiteral` by its `literalId` alone; a `null`, an + empty `sequence` and an empty `set` as one member, the model's absent value however spelt + (`unset` stays apart). The bundled clients' equality helpers judge the same way, converting + a quantity through its `unitTerm` (exactly, while the magnitude is an integer and the scale + a whole ratio); a quantity sent without a `unitTerm` they compare in its unit as written. - An empty set has no `elements` key (default omission). A `set` listing a member twice is refused on both sides, as is a `set` where the model wants a sequence's order or a sequence where it wants a set; a set flowing into an ordered parameter is read in canonical order. From c516533652a6425377b7bd4f63dfd2be91190edf Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 21:27:54 +0000 Subject: [PATCH 20/20] fix(java): hash the absent value alike however it is spelt A null, an empty Sequence and an empty SetValue compare equal under sameValue, so valueHash now hashes them to one code: sets that differ only in how their absent member is spelt hash and key alike. Co-Authored-By: jason.han --- .../src/main/java/org/openmbee/opensysml/Value.java | 3 +++ .../java/org/openmbee/opensysml/PublicTypesTest.java | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/Value.java b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/Value.java index 8ebe7581b..48e456c59 100644 --- a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/Value.java +++ b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/Value.java @@ -733,6 +733,9 @@ private static boolean sameQuantities(List a, List b) { /** A hash consistent with {@link #sameValue}: values the model equates hash alike. */ private static int valueHash(Value value) { + if (isEmpty(value)) { + return 0; + } Number magnitude = onRealAxis(value); if (magnitude != null) { return Double.hashCode(magnitude.doubleValue() + 0.0); diff --git a/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/PublicTypesTest.java b/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/PublicTypesTest.java index 5d5022f38..8c15da059 100644 --- a/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/PublicTypesTest.java +++ b/clients/java/opensysml-client/src/test/java/org/openmbee/opensysml/PublicTypesTest.java @@ -14,6 +14,7 @@ import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -207,6 +208,15 @@ void aSetJudgesItsMembersAsTheModelDoes() { assertTrue(new Value.SetValue(List.of(nul)).contains(emptySequence)); assertTrue(new Value.SetValue(List.of(emptySet)).contains(nul)); assertEquals(new Value.SetValue(List.of(one, nul)), new Value.SetValue(List.of(emptySet, one))); + assertEquals( + new Value.SetValue(List.of(one, nul)).hashCode(), + new Value.SetValue(List.of(emptySequence, one)).hashCode()); + assertEquals( + new Value.SetValue(List.of(one, nul)).hashCode(), + new Value.SetValue(List.of(emptySet, one)).hashCode()); + Map keyed = new HashMap<>(); + keyed.put(new Value.SetValue(List.of(nul)), "absent"); + assertEquals("absent", keyed.get(new Value.SetValue(List.of(emptySet)))); assertTrue( new Value.Sequence(List.of(one, nul)) .sameValue(new Value.Sequence(List.of(one, emptySequence))));