Skip to content

Commit 21d0b3a

Browse files
authored
Make SimpleModel immutable (#5760)
1 parent 424c284 commit 21d0b3a

File tree

10 files changed

+208
-106
lines changed

10 files changed

+208
-106
lines changed

components/gantry/multiaxis/multiaxis.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,15 +92,18 @@ func newMultiAxis(
9292
return nil, err
9393
}
9494

95-
model := referenceframe.NewSimpleModel(mAx.Name().Name)
95+
frames := make([]referenceframe.Frame, 0, len(mAx.subAxes))
9696
for _, subAx := range mAx.subAxes {
9797
k, err := subAx.Kinematics(ctx)
9898
if err != nil {
9999
return nil, err
100100
}
101-
model.SetOrdTransforms(append(model.OrdTransforms(), k))
101+
frames = append(frames, k)
102+
}
103+
mAx.model, err = referenceframe.NewSerialModel(mAx.Name().Name, frames)
104+
if err != nil {
105+
return nil, err
102106
}
103-
mAx.model = model
104107

105108
return mAx, nil
106109
}

components/gantry/multiaxis/multiaxis_test.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ import (
1717
rutils "go.viam.com/rdk/utils"
1818
)
1919

20-
func createFakeOneaAxis(length float64, positions []float64) *inject.Gantry {
21-
fakesingleaxis := inject.NewGantry("fake")
20+
func createFakeOneaAxis(name string, length float64, positions []float64) *inject.Gantry {
21+
fakesingleaxis := inject.NewGantry(name)
2222
fakesingleaxis.PositionFunc = func(ctx context.Context, extra map[string]interface{}) ([]float64, error) {
2323
return positions, nil
2424
}
@@ -38,7 +38,7 @@ func createFakeOneaAxis(length float64, positions []float64) *inject.Gantry {
3838
return nil
3939
}
4040
fakesingleaxis.KinematicsFunc = func(ctx context.Context) (referenceframe.Model, error) {
41-
return referenceframe.NewSimpleModel(""), nil
41+
return referenceframe.NewSimpleModel(name), nil
4242
}
4343
return fakesingleaxis
4444
}
@@ -49,21 +49,21 @@ func createFakeDeps() resource.Dependencies {
4949
return []float64{1}, nil
5050
}
5151
fakeGantry1.KinematicsFunc = func(ctx context.Context) (referenceframe.Model, error) {
52-
return referenceframe.NewSimpleModel(""), nil
52+
return referenceframe.NewSimpleModel("1"), nil
5353
}
5454
fakeGantry2 := inject.NewGantry("2")
5555
fakeGantry2.LengthsFunc = func(ctx context.Context, extra map[string]interface{}) ([]float64, error) {
5656
return []float64{1}, nil
5757
}
5858
fakeGantry2.KinematicsFunc = func(ctx context.Context) (referenceframe.Model, error) {
59-
return referenceframe.NewSimpleModel(""), nil
59+
return referenceframe.NewSimpleModel("2"), nil
6060
}
6161
fakeGantry3 := inject.NewGantry("3")
6262
fakeGantry3.LengthsFunc = func(ctx context.Context, extra map[string]interface{}) ([]float64, error) {
6363
return []float64{1}, nil
6464
}
6565
fakeGantry3.KinematicsFunc = func(ctx context.Context) (referenceframe.Model, error) {
66-
return referenceframe.NewSimpleModel(""), nil
66+
return referenceframe.NewSimpleModel("3"), nil
6767
}
6868
fakeMotor := &fm.Motor{
6969
Named: motor.Named("fm1").AsNamed(),
@@ -78,14 +78,14 @@ func createFakeDeps() resource.Dependencies {
7878
}
7979

8080
var threeAxes = []gantry.Gantry{
81-
createFakeOneaAxis(1, []float64{1}),
82-
createFakeOneaAxis(2, []float64{5}),
83-
createFakeOneaAxis(3, []float64{9}),
81+
createFakeOneaAxis("axis1", 1, []float64{1}),
82+
createFakeOneaAxis("axis2", 2, []float64{5}),
83+
createFakeOneaAxis("axis3", 3, []float64{9}),
8484
}
8585

8686
var twoAxes = []gantry.Gantry{
87-
createFakeOneaAxis(5, []float64{1}),
88-
createFakeOneaAxis(6, []float64{5}),
87+
createFakeOneaAxis("axis5", 5, []float64{1}),
88+
createFakeOneaAxis("axis6", 6, []float64{5}),
8989
}
9090

9191
func TestValidate(t *testing.T) {

referenceframe/errors.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,9 @@ func NewParentFrameNotInMapOfParentsError(parentFrameName string) error {
7878
func NewReservedWordError(configType, reservedWord string) error {
7979
return errors.Errorf("reserved word: cannot name a %s '%s'", configType, reservedWord)
8080
}
81+
82+
// NewDuplicateFrameNameError returns an error indicating that multiple frames
83+
// with the same name were provided.
84+
func NewDuplicateFrameNameError(frameName string) error {
85+
return errors.Errorf("duplicate frame name %q in serial model", frameName)
86+
}

referenceframe/frame.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -815,8 +815,8 @@ func framesAlmostEqual(frame1, frame2 Frame, epsilon float64) (bool, error) {
815815
}
816816
case *SimpleModel:
817817
f2 := frame2.(*SimpleModel)
818-
ordTransforms1 := f1.OrdTransforms()
819-
ordTransforms2 := f2.OrdTransforms()
818+
ordTransforms1 := f1.ordTransforms
819+
ordTransforms2 := f2.ordTransforms
820820
if len(ordTransforms1) != len(ordTransforms2) {
821821
return false, nil
822822
} else {
@@ -836,8 +836,8 @@ func framesAlmostEqual(frame1, frame2 Frame, epsilon float64) (bool, error) {
836836
return true, nil
837837
}
838838

839-
// Clone makes a copy of a Frame.
840-
func Clone(f Frame) (Frame, error) {
839+
// clone makes a copy of a Frame.
840+
func clone(f Frame) (Frame, error) {
841841
t := reflect.TypeOf(f)
842842
var newFrame Frame
843843

referenceframe/frame_system_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,9 @@ func TestFrameModelPart(t *testing.T) {
106106
test.That(t, partAgain.FrameConfig.pose, test.ShouldResemble, part.FrameConfig.pose)
107107
test.That(t, partAgain.ModelFrame.Name, test.ShouldEqual, part.ModelFrame.Name)
108108
test.That(t,
109-
len(partAgain.ModelFrame.(*SimpleModel).OrdTransforms()),
109+
len(partAgain.ModelFrame.DoF()),
110110
test.ShouldEqual,
111-
len(part.ModelFrame.(*SimpleModel).OrdTransforms()),
111+
len(part.ModelFrame.DoF()),
112112
)
113113
}
114114

referenceframe/linear_inputs_test.go

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,31 +54,27 @@ func TestLinearInputsLimits(t *testing.T) {
5454
// frame and a 2dof simple model. We envision the 2dof simple model as an arm with 2 joints and
5555
// 3 links.
5656
fs := NewEmptyFrameSystem("fs")
57-
fs.AddFrame(NewZeroStaticFrame("0dof"), fs.World())
57+
err := fs.AddFrame(NewZeroStaticFrame("0dof"), fs.World())
58+
test.That(t, err, test.ShouldBeNil)
5859

5960
rotFrame, err := NewRotationalFrame("1dof", spatial.R4AA{RX: 1, RY: 0, RZ: 0}, Limit{-10, 10})
6061
test.That(t, err, test.ShouldBeNil)
61-
fs.AddFrame(rotFrame, fs.World())
62+
err = fs.AddFrame(rotFrame, fs.World())
63+
test.That(t, err, test.ShouldBeNil)
6264

63-
// Dan: It's unclear my by-hand construction of an arm and adding it to the frame system is
64-
// completely kosher. For the purpose of this test though, I see it behaving as I need it to.
6565
baseArmFrame := NewZeroStaticFrame("base")
6666
shoulderArmFrame, err := NewRotationalFrame("shoulder", spatial.R4AA{RX: 1, RY: 0, RZ: 0}, Limit{-10, 10})
6767
test.That(t, err, test.ShouldBeNil)
6868
upperArmFrame := NewZeroStaticFrame("upperArm")
69-
elbowArmFrame, err := NewRotationalFrame("shoulder", spatial.R4AA{RX: 1, RY: 0, RZ: 0}, Limit{-10, 10})
69+
elbowArmFrame, err := NewRotationalFrame("elbow", spatial.R4AA{RX: 1, RY: 0, RZ: 0}, Limit{-10, 10})
7070
test.That(t, err, test.ShouldBeNil)
7171
handArmFrame := NewZeroStaticFrame("hand")
72-
armFrame := NewSimpleModel("arm")
73-
armFrame.SetOrdTransforms([]Frame{
72+
armFrame, err := NewSerialModel("arm", []Frame{
7473
baseArmFrame, shoulderArmFrame, upperArmFrame, elbowArmFrame, handArmFrame,
7574
})
76-
fs.AddFrame(armFrame, fs.World())
77-
fs.AddFrame(baseArmFrame, armFrame)
78-
fs.AddFrame(shoulderArmFrame, baseArmFrame)
79-
fs.AddFrame(upperArmFrame, shoulderArmFrame)
80-
fs.AddFrame(elbowArmFrame, upperArmFrame)
81-
fs.AddFrame(handArmFrame, elbowArmFrame)
75+
test.That(t, err, test.ShouldBeNil)
76+
err = fs.AddFrame(armFrame, fs.World())
77+
test.That(t, err, test.ShouldBeNil)
8278

8379
// Create inputs, do a transform call that succeeds.
8480
li := NewLinearInputs()

referenceframe/model.go

Lines changed: 84 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import (
2424
type Model interface {
2525
Frame
2626
ModelConfig() *ModelConfigJSON
27-
ModelPieceFrames([]Input) (map[string]Frame, error)
2827
}
2928

3029
// KinematicModelFromProtobuf returns a model from a protobuf message representing it.
@@ -137,6 +136,78 @@ func NewSimpleModel(name string) *SimpleModel {
137136
}
138137
}
139138

139+
// NewSerialModel is a convenience constructor that builds a Model from a serial chain of frames.
140+
// Returns an error if duplicate frame names are detected.
141+
func NewSerialModel(name string, frames []Frame) (*SimpleModel, error) {
142+
seen := make(map[string]bool)
143+
for _, f := range frames {
144+
frameName := f.Name()
145+
if seen[frameName] {
146+
return nil, NewDuplicateFrameNameError(frameName)
147+
}
148+
seen[frameName] = true
149+
}
150+
151+
m := NewSimpleModel(name)
152+
m.setOrdTransforms(frames)
153+
return m, nil
154+
}
155+
156+
// NewModelWithLimitOverrides constructs a new model identical to base but with the specified
157+
// joint limits overridden. Overrides are keyed by frame name. Each override replaces the
158+
// first DoF limit of the matching frame.
159+
func NewModelWithLimitOverrides(base *SimpleModel, overrides map[string]Limit) (*SimpleModel, error) {
160+
clonedFrames := make([]Frame, len(base.ordTransforms))
161+
for i, f := range base.ordTransforms {
162+
cloned, err := clone(f)
163+
if err != nil {
164+
return nil, fmt.Errorf("cloning frame %q: %w", f.Name(), err)
165+
}
166+
clonedFrames[i] = cloned
167+
}
168+
169+
for name, limit := range overrides {
170+
found := false
171+
for _, f := range clonedFrames {
172+
if f.Name() == name && len(f.DoF()) > 0 {
173+
f.DoF()[0] = limit
174+
found = true
175+
break
176+
}
177+
}
178+
if !found {
179+
return nil, fmt.Errorf("frame %q not found or has no DoF", name)
180+
}
181+
}
182+
183+
m := NewSimpleModel(base.name)
184+
m.setOrdTransforms(clonedFrames)
185+
m.modelConfig = base.modelConfig
186+
return m, nil
187+
}
188+
189+
// MoveableFrameNames returns the names of frames with non-zero DoF, in order.
190+
func (m *SimpleModel) MoveableFrameNames() []string {
191+
var names []string
192+
for _, f := range m.ordTransforms {
193+
if len(f.DoF()) > 0 {
194+
names = append(names, f.Name())
195+
}
196+
}
197+
return names
198+
}
199+
200+
// setOrdTransforms sets the internal ordered transforms and recomputes limits.
201+
func (m *SimpleModel) setOrdTransforms(fs []Frame) {
202+
m.ordTransforms = fs
203+
m.limits = []Limit{}
204+
for _, transform := range m.ordTransforms {
205+
if len(transform.DoF()) > 0 {
206+
m.limits = append(m.limits, transform.DoF()...)
207+
}
208+
}
209+
}
210+
140211
// GenerateRandomConfiguration generates a list of radian joint positions that are random but valid for each joint.
141212
func GenerateRandomConfiguration(m Model, randSeed *rand.Rand) []float64 {
142213
limits := m.DoF()
@@ -176,7 +247,7 @@ func (m *SimpleModel) Transform(inputs []Input) (spatialmath.Pose, error) {
176247
func (m *SimpleModel) Interpolate(from, to []Input, by float64) ([]Input, error) {
177248
interp := make([]Input, 0, len(from))
178249
posIdx := 0
179-
for _, transform := range m.OrdTransforms() {
250+
for _, transform := range m.ordTransforms {
180251
dof := len(transform.DoF()) + posIdx
181252
fromSubset := from[posIdx:dof]
182253
toSubset := to[posIdx:dof]
@@ -191,36 +262,28 @@ func (m *SimpleModel) Interpolate(from, to []Input, by float64) ([]Input, error)
191262
return interp, nil
192263
}
193264

194-
// OrdTransforms gets the OrdTransforms.
195-
func (m *SimpleModel) OrdTransforms() []Frame {
196-
return m.ordTransforms
197-
}
198-
199265
// InputFromProtobuf converts pb.JointPosition to inputs.
200266
func (m *SimpleModel) InputFromProtobuf(jp *pb.JointPositions) []Input {
201267
inputs := make([]Input, 0, len(jp.Values))
202268
posIdx := 0
203-
for _, transform := range m.OrdTransforms() {
269+
for _, transform := range m.ordTransforms {
204270
dof := len(transform.DoF()) + posIdx
205271
jPos := jp.Values[posIdx:dof]
206272
posIdx = dof
207-
208273
inputs = append(inputs, transform.InputFromProtobuf(&pb.JointPositions{Values: jPos})...)
209274
}
210-
211275
return inputs
212276
}
213277

214278
// ProtobufFromInput converts inputs to pb.JointPosition.
215279
func (m *SimpleModel) ProtobufFromInput(input []Input) *pb.JointPositions {
216280
jPos := &pb.JointPositions{}
217281
posIdx := 0
218-
for _, transform := range m.OrdTransforms() {
282+
for _, transform := range m.ordTransforms {
219283
dof := len(transform.DoF()) + posIdx
220284
jPos.Values = append(jPos.Values, transform.ProtobufFromInput(input[posIdx:dof]).Values...)
221285
posIdx = dof
222286
}
223-
224287
return jPos
225288
}
226289

@@ -271,22 +334,11 @@ func (m *SimpleModel) DoF() []Limit {
271334
return m.limits
272335
}
273336

274-
// SetOrdTransforms sets the ordTransforms.
275-
func (m *SimpleModel) SetOrdTransforms(fs []Frame) {
276-
m.ordTransforms = fs
277-
m.limits = []Limit{}
278-
for _, transform := range m.ordTransforms {
279-
if len(transform.DoF()) > 0 {
280-
m.limits = append(m.limits, transform.DoF()...)
281-
}
282-
}
283-
}
284-
285337
// MarshalJSON serializes a Model.
286338
func (m *SimpleModel) MarshalJSON() ([]byte, error) {
287339
type serialized struct {
288340
Name string `json:"name"`
289-
Model *ModelConfigJSON `json:"model"`
341+
Model *ModelConfigJSON `json:"model,omitempty"`
290342
Limits []Limit `json:"limits"`
291343
}
292344
ser := serialized{
@@ -301,7 +353,7 @@ func (m *SimpleModel) MarshalJSON() ([]byte, error) {
301353
func (m *SimpleModel) UnmarshalJSON(data []byte) error {
302354
type serialized struct {
303355
Name string `json:"name"`
304-
Model *ModelConfigJSON `json:"model"`
356+
Model *ModelConfigJSON `json:"model,omitempty"`
305357
Limits []Limit `json:"limits"`
306358
}
307359
var ser serialized
@@ -323,28 +375,14 @@ func (m *SimpleModel) UnmarshalJSON(data []byte) error {
323375
if !ok {
324376
return fmt.Errorf("could not parse config for simple model, name: %v", ser.Name)
325377
}
326-
m.SetOrdTransforms(newModel.OrdTransforms())
378+
m.ordTransforms = newModel.ordTransforms
327379
}
328380
m.baseFrame = baseFrame{name: frameName, limits: ser.Limits}
329381
m.modelConfig = ser.Model
330382

331383
return nil
332384
}
333385

334-
// ModelPieceFrames takes a list of inputs and returns a map of frame names to their corresponding static frames,
335-
// effectively breaking the model into its kinematic pieces.
336-
func (m *SimpleModel) ModelPieceFrames(inputs []Input) (map[string]Frame, error) {
337-
poses, err := m.inputsToFrames(inputs, true)
338-
if err != nil {
339-
return nil, err
340-
}
341-
frameMap := map[string]Frame{}
342-
for _, sFrame := range poses {
343-
frameMap[sFrame.Name()] = sFrame
344-
}
345-
return frameMap, nil
346-
}
347-
348386
// inputsToFrames takes a model and a list of joint angles in radians and computes the dual quaternion representing the
349387
// cartesian position of each of the links up to and including the end effector. This is useful for when conversions
350388
// between quaternions and OV are not needed.
@@ -353,7 +391,7 @@ func (m *SimpleModel) inputsToFrames(inputs []Input, collectAll bool) ([]*static
353391
return nil, NewIncorrectDoFError(len(inputs), len(m.DoF()))
354392
}
355393

356-
poses := make([]*staticFrame, 0, len(m.OrdTransforms()))
394+
poses := make([]*staticFrame, 0, len(m.ordTransforms))
357395
// Start at ((1+0i+0j+0k)+(+0+0i+0j+0k)ϵ)
358396
composedTransformation := spatialmath.NewZeroPose()
359397
posIdx := 0
@@ -505,15 +543,16 @@ func New2DMobileModelFrame(name string, limits []Limit, collisionGeometry spatia
505543
return nil, err
506544
}
507545

508-
model := NewSimpleModel(name)
546+
var frames []Frame
509547
if len(limits) == 3 {
510548
theta, err := NewRotationalFrame("theta", *spatialmath.NewR4AA(), limits[2])
511549
if err != nil {
512550
return nil, err
513551
}
514-
model.SetOrdTransforms([]Frame{x, y, theta, geometry})
552+
frames = []Frame{x, y, theta, geometry}
515553
} else {
516-
model.SetOrdTransforms([]Frame{x, y, geometry})
554+
frames = []Frame{x, y, geometry}
517555
}
518-
return model, nil
556+
557+
return NewSerialModel(name, frames)
519558
}

0 commit comments

Comments
 (0)