forked from sisl/ConstructionBots.jl
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathconstruction_schedule.jl
1616 lines (1467 loc) · 66.9 KB
/
construction_schedule.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
export
start_config,
goal_config,
cargo_start_config,
cargo_goal_config,
cargo_deployed_config,
cargo_loaded_config,
entity,
ConstructionPredicate,
EntityConfigPredicate,
RobotStart,
ObjectStart,
AssemblyComplete,
AssemblyStart,
EntityGo,
RobotGo,
TransportUnitGo,
LiftIntoPlace,
FormTransportUnit,
DepositCargo,
BuildPhasePredicate,
OpenBuildStep,
CloseBuildStep,
ProjectComplete
"""
abstract type ConstructionPredicate
Abstract type for ConstructionSchedule nodes.
"""
abstract type ConstructionPredicate end
start_config(n) = n.start_config
goal_config(n) = n.goal_config
entity(n) = n.entity
add_node!(g::AbstractCustomNGraph, n::ConstructionPredicate) = add_node!(g, n, node_id(n))
get_vtx(g::AbstractCustomNGraph, n::ConstructionPredicate) = get_vtx(g, node_id(n))
for op in (:start_config, :goal_config, :entity,
:cargo_start_config, :cargo_goal_config,
:cargo_deployed_config, :cargo_loaded_config)
@eval begin
$op(n::CustomNode) = $op(node_val(n))
$op(n::ScheduleNode) = $op(n.node)
end
end
abstract type EntityConfigPredicate{E} <: ConstructionPredicate end
start_config(n::EntityConfigPredicate) = n.config
goal_config(n::EntityConfigPredicate) = n.config
struct RobotStart <: EntityConfigPredicate{RobotNode}
entity::RobotNode
config::TransformNode
end
struct ObjectStart <: EntityConfigPredicate{ObjectNode}
entity::ObjectNode
config::TransformNode
end
struct AssemblyComplete <: EntityConfigPredicate{AssemblyNode}
entity::AssemblyNode
config::TransformNode
inner_staging_circle::GeomNode # inner staging circle
outer_staging_circle::GeomNode # outer staging circle
end
function AssemblyComplete(n::SceneNode, t::TransformNode)
inner_staging_circle = GeomNode(LazySets.Ball2(zeros(SVector{3,Float64}), 0.0))
outer_staging_circle = GeomNode(LazySets.Ball2(zeros(SVector{3,Float64}), 0.0))
node = AssemblyComplete(n, t, inner_staging_circle, outer_staging_circle)
set_parent!(inner_staging_circle, t)
set_parent!(outer_staging_circle, t)
node
end
struct AssemblyStart <: EntityConfigPredicate{AssemblyNode}
entity::AssemblyNode
config::TransformNode
end
for T in (:RobotStart, :ObjectStart, :AssemblyComplete, :AssemblyStart)
@eval begin
$T(n::SceneNode) = $T(n, TransformNode())
$T(n::ConstructionPredicate) = $T(entity(n), goal_config(n)) # shared config
end
end
"""
abstract type EntityGo{E}
Encodes going from state start_config(n) to state goal_config(n).
"""
abstract type EntityGo{E} <: ConstructionPredicate end
struct RobotGo{R} <: EntityGo{RobotNode}
entity::R
start_config::TransformNode
goal_config::TransformNode
id::ActionID # required because there may be multiple RobotGo nodes for one robot
end
struct TransportUnitGo <: EntityGo{TransportUnitNode}
entity::TransportUnitNode
start_config::TransformNode
goal_config::TransformNode
end
struct LiftIntoPlace{C} <: EntityGo{C}
entity::C # AssemblyNode or ObjectNode
start_config::TransformNode
# path plan?
goal_config::TransformNode
end
"""
abstract type BuildPhasePredicate <: ConstructionPredicate
References a building phase. Interface:
- `get_assembly(::BuildPhasePredicate)` => ::AssemblyNode - the assembly to be
modified by this build phase
- `assembly_components(::BuildPhasePredicate)` => TransformDict{Union{ObjectID,AssemblyID}}
- the components to be added to the assembly during this phase
"""
abstract type BuildPhasePredicate <: ConstructionPredicate end
get_assembly(n::BuildPhasePredicate) = n.assembly
assembly_components(n::BuildPhasePredicate) = n.components
assembly_components(n::ConstructionPredicate) = assembly_components(entity(n))
get_assembly(n::CustomNode) = get_assembly(node_val(n))
assembly_components(n::CustomNode) = assembly_components(node_val(n))
"""
extract_building_phase(n::BuildingStep,tree::SceneTree,model_spec,id_map)
extract the assembly and set of subcomponents from a BuildingStep.
"""
function extract_building_phase(n, tree::SceneTree, model_spec, id_map)
@assert matches_template(BuildingStep, n)
assembly_id = id_map[node_val(n).parent]
@assert isa(assembly_id, AssemblyID)
assembly = get_node(tree, assembly_id)
parts = typeof(assembly_components(assembly))()
for child_id in get_build_step_components(model_spec, id_map, n)
@assert has_component(assembly, child_id) "$child_id is not a child of assembly $assembly"
parts[child_id] = child_transform(assembly, child_id)
end
# for vp in inneighbors(model_spec,n)
# child_id = get_referenced_component(model_spec,id_map,get_vtx_id(model_spec,vp))
# # child_id = get(id_map,get_vtx_id(model_spec,vp),nothing)
# if !(child_id === nothing)
# @assert has_component(assembly, child_id) "$child_id is not a child of assembly $assembly"
# parts[child_id] = child_transform(assembly,child_id)
# end
# end
return assembly, parts
end
# struct BuildPhase
# components::TransformDict{Union{AssemblyID,ObjectID}}
# staging_circle::GeomNode # staging circle - surrounds the staging area--requires tf relative to assembly
# bounding_circle::GeomNode # bounding circle - surrounds the current assembly--requires tf relative to assembly
# end
"""
OpenBuildStep <: ConstructionPredicate
Marks the beginning of a new building phase.
Fields:
- assembly::AssemblyNode - the assembly to be modified by this build phase
- components::Dict{Union{AssemblyID,ObjectID}} - the components to be added to
the assembly during this phase
"""
struct OpenBuildStep <: BuildPhasePredicate
assembly::AssemblyNode
components::TransformDict{Union{AssemblyID,ObjectID}}
staging_circle::GeomNode # staging circle - surrounds the staging area--requires tf relative to assembly
bounding_circle::GeomNode # bounding circle - surrounds the current assembly--requires tf relative to assembly
id::Int # id of this build step
end
struct CloseBuildStep <: BuildPhasePredicate
assembly::AssemblyNode
components::TransformDict{Union{AssemblyID,ObjectID}}
staging_circle::GeomNode
bounding_circle::GeomNode
id::Int # id of this build step
end
for T in (:OpenBuildStep, :CloseBuildStep)
@eval begin
$T(a::AssemblyNode, c::Dict, g1::GeomNode, g2::GeomNode) = $T(a, c, g1, g2, get_id(get_unique_id(TemplatedID{$T})))
$T(a::AssemblyNode, c::Dict) = $T(a, c,
GeomNode(LazySets.Ball2(zeros(SVector{3,Float64}), 0.0)),
GeomNode(LazySets.Ball2(zeros(SVector{3,Float64}), 0.0))
)
$T(n::CustomNode, args...) = $T(extract_building_phase(n, args...)...)
$T(n::BuildPhasePredicate) = $T(n.assembly, n.components,
n.staging_circle, n.bounding_circle, n.id)
end
end
node_id(n::T) where {T<:BuildPhasePredicate} = TemplatedID{T}(n.id)
# add_node!(g::AbstractCustomNGraph, n::P) where {P<:BuildPhasePredicate} = add_node!(g,n,get_unique_id(TemplatedID{P}))
"""
FormTransportUnit <: ConstructionPredicate
TransportUnit must remain motionless as the cargo moves from its start
configuration to its `FormTransportUnit.cargo_goal_config` configuration.
"""
struct FormTransportUnit <: ConstructionPredicate
entity::TransportUnitNode
config::TransformNode
cargo_start_config::TransformNode
cargo_goal_config::TransformNode # offset from config by child_transform(entity,cargo_id(entity))
end
"""
DepositCargo <: ConstructionPredicate
TransportUnit must remain motionless as the cargo moves from its transport
configuration to its `LiftIntoPlace.start_config` configuration.
"""
struct DepositCargo <: ConstructionPredicate
entity::TransportUnitNode
config::TransformNode
cargo_start_config::TransformNode # offset from config by child_transform(entity,cargo_id(entity))
cargo_goal_config::TransformNode
end
start_config(n::Union{DepositCargo,FormTransportUnit}) = n.config
goal_config(n::Union{DepositCargo,FormTransportUnit}) = n.config
cargo_start_config(n::Union{DepositCargo,FormTransportUnit}) = n.cargo_start_config
cargo_goal_config(n::Union{DepositCargo,FormTransportUnit}) = n.cargo_goal_config
cargo_loaded_config(n::DepositCargo) = cargo_start_config(n)
cargo_loaded_config(n::FormTransportUnit) = cargo_goal_config(n)
cargo_deployed_config(n::DepositCargo) = cargo_goal_config(n)
cargo_deployed_config(n::FormTransportUnit) = cargo_start_config(n)
for T in (:DepositCargo, :FormTransportUnit)
@eval begin
function $T(n::TransportUnitNode)
t = $T(n, TransformNode(), TransformNode(), TransformNode())
# cargo_goal -> cargo_start -> entity_config
set_parent!(cargo_loaded_config(t), cargo_deployed_config(t))
set_parent!(start_config(t), cargo_loaded_config(t))
# set child transform cargo_start -> entity_config
set_local_transform!(start_config(t), inv(child_transform(n, cargo_id(n))))
return t
end
end
end
for T in (:RobotGo, :TransportUnitGo) #,:LiftIntoPlace)
@eval begin
function $T(n::SceneNode)
t = $T(n, TransformNode(), TransformNode())
set_parent!(goal_config(t), start_config(t))
return t
end
end
end
RobotGo(n::RobotNode, start, goal) = RobotGo(n, start, goal, get_unique_id(ActionID))
function LiftIntoPlace(n::Union{ObjectNode,AssemblyNode})
l = LiftIntoPlace(n, TransformNode(), TransformNode())
set_parent!(start_config(l), goal_config(l)) # point to parent
return l
end
robot_team(n::ConstructionPredicate) = robot_team(entity(n))
cargo_node_type(n::TransportUnitNode) = cargo_type(n) == AssemblyNode ? AssemblyComplete : ObjectStart
cargo_node_type(n::ConstructionPredicate) = cargo_node_type(entity(n))
struct ProjectComplete <: ConstructionPredicate
project_id::Int
config::TransformNode
end
ProjectComplete(id) = ProjectComplete(id, TransformNode())
ProjectComplete() = ProjectComplete(get_id(get_unique_id(TemplatedID{ProjectComplete})))
node_id(n::ProjectComplete) = TemplatedID{ProjectComplete}(n.project_id)
required_predecessors(::ConstructionPredicate) = Dict()
required_successors(::ConstructionPredicate) = Dict()
eligible_predecessors(n::ConstructionPredicate) = required_predecessors(n)
eligible_successors(n::ConstructionPredicate) = required_successors(n)
required_predecessors(::RobotStart) = Dict()
required_successors(::RobotStart) = Dict(RobotGo => 1)
required_predecessors(::RobotGo) = Dict(Union{RobotStart,DepositCargo,RobotGo} => 1)
required_successors(::RobotGo) = Dict()
eligible_successors(::RobotGo) = Dict(Union{RobotGo,FormTransportUnit} => 1)
required_predecessors(n::FormTransportUnit) = Dict(RobotGo => length(robot_team(n)), cargo_node_type(n) => 1)
required_successors(::FormTransportUnit) = Dict(TransportUnitGo => 1)
required_predecessors(::TransportUnitGo) = Dict(FormTransportUnit => 1)
required_successors(::TransportUnitGo) = Dict(DepositCargo => 1)
required_predecessors(::DepositCargo) = Dict(TransportUnitGo => 1, OpenBuildStep => 1)
required_successors(n::DepositCargo) = Dict(LiftIntoPlace => 1, RobotGo => length(robot_team(n)))
required_predecessors(::LiftIntoPlace) = Dict(DepositCargo => 1)
required_successors(::LiftIntoPlace) = Dict(CloseBuildStep => 1)
required_predecessors(n::CloseBuildStep) = Dict(LiftIntoPlace => num_components(n))
required_successors(::CloseBuildStep) = Dict(Union{OpenBuildStep,AssemblyComplete} => 1)
required_predecessors(::OpenBuildStep) = Dict(Union{AssemblyStart,CloseBuildStep} => 1)
required_successors(n::OpenBuildStep) = Dict(DepositCargo => num_components(n))
required_predecessors(::ObjectStart) = Dict()
required_successors(::ObjectStart) = Dict(FormTransportUnit => 1)
# required_predecessors( n::AssemblyComplete) = Dict(Union{ObjectStart,CloseBuildStep}=>1)
required_predecessors(::AssemblyComplete) = Dict(CloseBuildStep => 1)
required_successors(::AssemblyComplete) = Dict(Union{FormTransportUnit,ProjectComplete} => 1)
required_predecessors(::AssemblyStart) = Dict()
required_successors(::AssemblyStart) = Dict(OpenBuildStep => 1)
required_predecessors(::ProjectComplete) = Dict(AssemblyComplete => 1)
required_successors(::ProjectComplete) = Dict()
for T in (
:ObjectStart,
:RobotStart,
# :RobotGo,
:AssemblyStart,
:AssemblyComplete,
:FormTransportUnit,
:TransportUnitGo,
:DepositCargo,
:LiftIntoPlace,
)
@eval begin
function node_id(n::$T)
TemplatedID{Tuple{$T,typeof(node_id(entity(n)))}}(get_id(node_id(entity(n))))
end
end
end
node_id(n::RobotGo) = n.id
# for T in (
# :ObjectStart,
# :RobotStart,
# :RobotGo,
# :AssemblyStart,
# :AssemblyComplete,
# )
# @eval begin
# node_id(n::$T) = TemplatedID{$T}(get_id(node_id(entity(n))))
# end
# end
"""
get_previous_build_step(model_spec,v)
Return the node representing the previous build step in the model spec.
"""
function get_previous_build_step(model_spec, v;
skip_first=matches_template(SubModelPlan, get_node(model_spec, v)),
)
vp = depth_first_search(model_spec, get_vtx(model_spec, v),
u -> matches_template(BuildingStep, get_node(model_spec, u)),
u -> (get_vtx(model_spec, v) == u || !matches_template(SubModelPlan, get_node(model_spec, u))),
inneighbors;
skip_first=skip_first,
)
if has_vertex(model_spec, vp)
return get_node(model_spec, vp)
end
return nothing
end
export construct_partial_construction_schedule
"""
populate_schedule_build_step!(sched,cb,step_node,model_spec,scene_tree,id_map)
Creates an OpenBuildStep ... CloseBuildStep structure and adds an edge
CloseBuildStep => parent.
Args:
- sched - the schedule
- cb::CustomNode{CloseBuildStep,AbstractID}
- model_spec
- scene_tree
- id_map
"""
function populate_schedule_build_step!(sched, parent::AssemblyComplete, cb, step_node, model_spec, scene_tree, id_map;
connect_to_sub_assemblies=true,
)
# OpenBuildStep
ob = add_node!(sched, OpenBuildStep(node_val(cb)))
for child_id in get_build_step_components(model_spec, id_map, step_node)
cargo = get_node(scene_tree, child_id)
transport_unit = get_node(scene_tree, node_id(TransportUnitNode(cargo)))
@assert isa(cargo, Union{AssemblyNode,ObjectNode})
# LiftIntoPlace
l = add_node!(sched, LiftIntoPlace(cargo))
set_parent!(goal_config(l), start_config(parent))
set_local_transform!(goal_config(l), child_transform(entity(parent), node_id(cargo)))
# ensure that LiftIntoPlace starts in the carry configuration
set_desired_global_transform!(start_config(l),
CoordinateTransformations.Translation(global_transform(start_config(l)).translation)
)
@info "Staging config: setting GOAL config of $(node_id(l)) to $(goal_config(l))"
add_edge!(sched, l, cb) # LiftIntoPlace => CloseBuildStep
# DepositCargo
d = add_node!(sched, DepositCargo(transport_unit))
set_parent!(cargo_deployed_config(d), start_config(l))
add_edge!(sched, d, l) # DepositCargo => LiftIntoPlace
add_edge!(sched, ob, d) # OpenBuildStep => DepositCargo
# TransportUnitGo
tgo = add_node!(sched, TransportUnitGo(transport_unit))
set_parent!(goal_config(tgo), start_config(d))
add_edge!(sched, tgo, d) # TransportUnitGo => DepositCargo
# FormTransportUnit
f = add_node!(sched, FormTransportUnit(transport_unit))
set_parent!(start_config(tgo), goal_config(f))
add_edge!(sched, f, tgo) # FormTransportUnit => TransportUnitGo
# ObjectStart/AssemblyComplete
if isa(cargo, AssemblyNode) # && connect_to_sub_assemblies
cargo_node = get_node(sched, AssemblyComplete(cargo))
else
cargo_node = add_node!(sched, ObjectStart(cargo, TransformNode()))
end
if connect_to_sub_assemblies
set_parent!(goal_config(node_val(cargo_node)), start_config(parent))
end
add_edge!(sched, cargo_node, f) # ObjectStart/AssemblyComplete => FormTransportUnit
set_parent!(cargo_deployed_config(f), start_config(cargo_node))
end
return ob
end
"""
populate_schedule_sub_graph!(
sched,
parent::AssemblyComplete,
model_spec,
scene_tree,
id_map)
Add all building steps to parent, working backward from parents
"""
function populate_schedule_sub_graph!(sched, parent::AssemblyComplete, model_spec, scene_tree, id_map)
parent_assembly = entity(parent)
# sa = add_node!(sched,AssemblyStart(parent_assembly))
sa = add_node!(sched, AssemblyStart(parent)) # shares config with AssemblyComplete (AssemblyComplete.config === AssemblyStart.config)
spec_node = get_node(model_spec, id_map[node_id(parent_assembly)])
step_node = get_previous_build_step(model_spec, spec_node; skip_first=true)
immediate_parent = parent
while !(step_node === nothing)
# CloseBuildStep
cb = add_node!(sched, CloseBuildStep(step_node, scene_tree, model_spec, id_map))
add_edge!(sched, cb, immediate_parent) # CloseBuildStep => AssemblyComplete / OpenBuildStep
set_parent!(node_val(cb).staging_circle, start_config(parent))
set_parent!(node_val(cb).bounding_circle, start_config(parent))
ob = populate_schedule_build_step!(sched, parent, cb, step_node, model_spec, scene_tree, id_map)
immediate_parent = ob
step_node = get_previous_build_step(model_spec, step_node; skip_first=true)
end
add_edge!(sched, sa, immediate_parent) # AssemblyStart => OpenBuildStep
sched
end
"""
construct_partial_construction_schedule(sched,args...)
Constructs a schedule that does not yet reflect the "staging plan" and does not
yet contain any RobotStart or RobotGo nodes.
"""
function construct_partial_construction_schedule(
mpd_model,
model_spec,
scene_tree,
id_map=build_id_map(mpd_model, model_spec)
)
sched = NGraph{DiGraph,ConstructionPredicate,AbstractID}()
parent_map = backup_descendants(model_spec, n -> matches_template(SubModelPlan, n))
# Add assemblies first
for node in node_iterator(model_spec, topological_sort_by_dfs(model_spec))
if matches_template(SubModelPlan, node)
assembly = get_node(scene_tree, id_map[node_id(node)])
# AssemblyComplete
a = add_node!(sched, AssemblyComplete(assembly, TransformNode())) ###############
if is_root_node(scene_tree, assembly)
# ProjectComplete
p = add_node!(sched, ProjectComplete())
add_edge!(sched, a, p)
end
# add build steps
populate_schedule_sub_graph!(sched, node_val(a), model_spec, scene_tree, id_map)
end
end
# Add robots
set_robot_start_configs!(sched, scene_tree)
return sched
end
export validate_schedule_transform_tree
function assert_transform_tree_ancestor(a, b)
@assert has_ancestor(a, b) "a should have ancestor b, for a = $(a), b = $(b)"
end
function transformations_approx_equiv(t1, t2)
a = all(isapprox.(t1.translation, t2.translation))
b = all(isapprox.(t1.linear, t2.linear))
a && b
end
"""
validate_schedule_transform_tree(sched)
Checks if sched and its embedded transform tree are valid.
- The graph itself should be valid
- All chains AssemblyComplete ... LiftIntoPlace -> DepositCargo should be
connected in the embedded transform tree
"""
function validate_schedule_transform_tree(sched; post_staging=false)
try
@assert validate_graph(sched)
for n in get_nodes(sched)
if matches_template(AssemblyComplete, n)
@assert validate_tree(goal_config(n)) "Subtree invalid for $(n)"
end
end
for n in get_nodes(sched)
if matches_template(OpenBuildStep, n)
open_build_step = node_val(n)
assembly = open_build_step.assembly
assembly_complete = get_node(sched, AssemblyComplete(assembly))
for v in outneighbors(sched, n)
deposit_node = get_node(sched, v)
@assert matches_template(DepositCargo, deposit_node)
assert_transform_tree_ancestor(goal_config(deposit_node), start_config(assembly_complete))
for vp in outneighbors(sched, v)
lift_node = get_node(sched, vp)
if matches_template(LiftIntoPlace, lift_node)
@assert has_child(
goal_config(assembly_complete), goal_config(lift_node))
if post_staging
# Show that goal_config(LiftIntoPlace) matches the
# goal config of cargo relative to assembly
@assert transformations_approx_equiv(
local_transform(goal_config(lift_node)),
child_transform(assembly, node_id(entity(lift_node)))
)
# Show that start_config(LiftIntoPlace) matches
# cargo_deployed_config(DepositCargo)
@assert transformations_approx_equiv(
global_transform(start_config(lift_node)),
global_transform(cargo_deployed_config(deposit_node)),
)
end
end
end
end
elseif matches_template(Union{DepositCargo,FormTransportUnit}, n)
transport_unit = entity(n)
@assert has_child(cargo_loaded_config(n), start_config(n))
@assert transformations_approx_equiv(
local_transform(start_config(n)),
inv(child_transform(transport_unit, cargo_id(transport_unit)))
)
if post_staging
@assert isapprox(global_transform(goal_config(n)).translation[3], 0.0; rtol=1e-6, atol=1e-6) "$n, $(global_transform(goal_config(n)).translation)"
end
elseif matches_template(ObjectStart, n)
elseif matches_template(LiftIntoPlace, n)
@assert has_child(goal_config(n), start_config(n))
if post_staging
l = n
cargo = entity(n)
f = get_node(sched, FormTransportUnit(TransportUnitNode(cargo)))
tu = entity(f)
o = cargo
d = get_node(sched, DepositCargo(tu))
l = get_node(sched, LiftIntoPlace(o))
@assert isapprox(global_transform(start_config(f)).translation[3], 0.0; rtol=1e-6, atol=1e-6) "Z translation should be 0 for $(summary(node_id(f)))"
@assert isapprox(global_transform(start_config(d)).translation[3], 0.0; rtol=1e-6, atol=1e-6) "Z translation should be 0 for $(summary(node_id(d)))"
transformations_approx_equiv(
global_transform(start_config(n)),
global_transform(cargo_deployed_config(f))
)
transformations_approx_equiv(
global_transform(cargo_loaded_config(f)),
global_transform(goal_config(f)) ∘ child_transform(tu, node_id(o))
)
transformations_approx_equiv(
global_transform(cargo_loaded_config(d)),
global_transform(goal_config(d)) ∘ child_transform(tu, node_id(o))
)
transformations_approx_equiv(
global_transform(cargo_deployed_config(d)),
global_transform(start_config(l))
)
end
end
end
catch e
if isa(e, AssertionError)
bt = catch_backtrace()
showerror(stderr, e, bt)
return false
else
rethrow(e)
end
end
return true
end
"""
generate_staging_plan(scene_tree,params)
Given a `SceneTree` representing the final configuration of an assembly,
construct a plan for the start config, staging config, and final config of
each subassembly and individual object. The idea is to ensure that no node of
the "plan" graph overlaps with any other. Requires circular bounding geometry
for each component of the assembly.
Start at terminal assembly
work downward through building steps
For each building step, arrange the incoming parts to balance these
objectives:
- minimize "LiftIntoPlace" distance
- maximize distance between components to be placed.
It may be necessary to not completely isolate build steps (i.e., two
consecutive build steps may overlap in the arrival times of subcomponents).
"""
function generate_staging_plan!(scene_tree, sched;
buffer_radius=0.0,
build_step_buffer_radius=0.0,
)
if !all(map(n -> has_vertex(n.geom_hierarchy, HypersphereKey()), get_nodes(scene_tree)))
compute_approximate_geometries!(scene_tree,
HypersphereKey(); ϵ=0.0)
end
# For each build step, determine the radius of the largest transform unit
# that may be active before the build step closes. This radius will be used
# to inform the size of the buffer zones between staging areas
# store growing bounding circle of each assembly
bounding_circles = Dict{AbstractID,LazySets.Ball2}()
staging_circles = Dict{AbstractID,LazySets.Ball2}()
for node in node_iterator(sched, topological_sort_by_dfs(sched))
if matches_template(AssemblyStart, node)
elseif matches_template(OpenBuildStep, node)
# work updward through build steps
# Set staging config of part as start_config(LiftIntoPlace(part))
process_schedule_build_step!(
node,
sched,
scene_tree,
bounding_circles,
staging_circles,
;
build_step_buffer_radius=build_step_buffer_radius,
)
end
end
# Update assembly start points so that none of the staging regions overlap
select_assembly_start_configs_layered!(sched, scene_tree, staging_circles;
buffer_radius=buffer_radius,)
# TODO store a TransformNode in ProjectComplete() (or in the schedule itself,
# once there is a dedicated ConstructionSchedule type) so that an entire
# schedule can be moved anywhere. All would-be root TransormNodes will have
# this root node as their parent, regardless of the edge structure of the
# schedule graph
return staging_circles, bounding_circles
end
"""
select_assembly_start_configs!(sched, scene_tree, staging_radii;
buffer_radius=0.0)
Select the start configs (i.e., the build location) for each assembly. The
location is selected by minimizing distance to the assembly's staging location
while ensuring that no child's staging area overlaps with its parent's staging
area. Uses the same ring optimization approach as for selectig staging
locations.
"""
function select_assembly_start_configs_layered!(sched, scene_tree, staging_circles;
buffer_radius=0.0,
)
# Update assembly start points so that none of the staging regions overlap
for start_node in node_iterator(sched, topological_sort_by_dfs(sched))
if !matches_template(AssemblyComplete, start_node)
continue
end
assembly_complete = node_val(start_node)
node = entity(start_node)
if matches_template(AssemblyNode, node)
# Apply ring solver with child assemblies (not objects)
assembly = node
part_ids = sort(filter(k -> isa(k, AssemblyID),
collect(keys(assembly_components(node)))))
if isempty(part_ids)
continue
end
orig_ball = staging_circles[node_id(assembly)]
new_ball = nothing
@info "Selecting staging location for $(summary(node_id(assembly)))"
@assert haskey(staging_circles, node_id(assembly))
steps_for_parts = find_step_numbers(start_node, part_ids, sched, scene_tree)
steps_dict = Dict(zip(part_ids, steps_for_parts))
@assert length(steps_for_parts) == length(part_ids)
part_ids_left = part_ids
while !isempty(part_ids_left)
# We continue this process until we don't have any more parts to place. At each
# iteration, we determine the maximum number of components that can fit around
# the current staging radius. If there are parts that remain, we increase the
# staging radius from the previous "layer" and keep going.
staging_circle = staging_circles[node_id(assembly)]
staging_radius = staging_circle.radius + buffer_radius
radii_left = [staging_circles[id].radius + buffer_radius for id in part_ids_left]
steps_left = [steps_dict[id] for id in part_ids_left]
unique_step_lengths = sort!(unique(steps_left))
cnt = 1
can_fit_steps = 0
while cnt <= length(unique_step_lengths)
current_step_consideration = unique_step_lengths[cnt]
bool_mask = steps_left .<= current_step_consideration
radii_k = radii_left[bool_mask]
# Determine if the staging radius can fit all components
half_widths = asin.(radii_k ./ (radii_k .+ staging_radius))
can_fit = sum(half_widths) * 2 <= 2.0 * Float64(π)
if can_fit
can_fit_steps = current_step_consideration
cnt += 1
else
break
end
end
part_ids_for_opt = []
radii_for_opt = []
if can_fit_steps == 0
# Current staging radius cannot fit all of the components of the first step
# Let's select the maximum number of components in the first step that can
# fit. Selecting from the smallest radii
current_step_consideration = minimum(unique_step_lengths)
bool_mask = steps_left .<= current_step_consideration
part_ids_at_or_before_step = part_ids_left[bool_mask]
radii = radii_left[bool_mask]
sorted_idxs = sortperm(radii)
sorted_radii = radii[sorted_idxs]
half_widths = asin.(sorted_radii ./ (sorted_radii .+ staging_radius))
last_idx = findlast(cumsum(half_widths) * 2 .<= 2.0 * Float64(π))
idxs_for_opt = sorted_idxs[1:last_idx] # With step mask
part_ids_for_opt = part_ids_at_or_before_step[idxs_for_opt]
radii_for_opt = radii[idxs_for_opt]
elseif can_fit_steps == maximum(unique_step_lengths)
# Current staging radius can fit all of the assemblies
part_ids_for_opt = part_ids_left
radii_for_opt = radii_left
else
# We can fit all components upto can_fit_step, but not all of the remaining components
# We will select the maximum number of components in the next step that can fit in
# addition to all of the components from the can_fit_step
bool_mask = steps_left .<= can_fit_steps
part_ids_for_opt = part_ids_left[bool_mask]
radii_for_opt = radii_left[bool_mask]
arc_dist_used = sum(asin.(radii_for_opt ./ (radii_for_opt .+ staging_radius)))
part_ids_left = setdiff(part_ids_left, part_ids_for_opt)
steps_left = [steps_dict[id] for id in part_ids_left]
radii_left = [staging_circles[id].radius + buffer_radius for id in part_ids_left]
bool_mask = steps_left .<= unique_step_lengths[cnt]
part_ids_at_or_before_step = part_ids_left[bool_mask]
radii = radii_left[bool_mask]
sorted_idxs = sortperm(radii)
sorted_radii = radii[sorted_idxs]
half_widths = asin.(sorted_radii ./ (sorted_radii .+ staging_radius))
last_idx = findlast((arc_dist_used .+ cumsum(half_widths)) * 2 .<= 2.0 * Float64(π))
last_idx = isnothing(last_idx) ? 0 : last_idx
idxs_for_opt = sorted_idxs[1:last_idx] # With step mask
append!(part_ids_for_opt, part_ids_at_or_before_step[idxs_for_opt])
append!(radii_for_opt, radii[idxs_for_opt])
end
part_ids_left = setdiff(part_ids_left, part_ids_for_opt)
steps_left = [steps_dict[id] for id in part_ids_left]
radii_left = [staging_circles[id].radius + buffer_radius for id in part_ids_left]
parts_for_opt = (get_node(scene_tree, part_id) for part_id in part_ids_for_opt)
radii_for_opt = [staging_circles[id].radius + buffer_radius for id in part_ids_for_opt]
ring_radius = -1
θ_star = nothing
# repeat to ensure correct alignment
while staging_radius - ring_radius > 1e-6
θ_des = Vector{Float64}()
ring_radius = staging_radius
for (part_id, part) in zip(part_ids_for_opt, parts_for_opt)
# retrieve staging config from LiftIntoPlace node
lift_node = get_node(sched, LiftIntoPlace(part))
# give coords of the dropoff point (we want `part` to be built as close as possible to here.)
# crucial: must be relative to the assembly origin
tr = relative_transform(
global_transform(goal_config(start_node)),
global_transform(start_config(lift_node))).translation
tform = CoordinateTransformations.Translation(tr...)
geom = staging_circles[part_id]
# the vector from the circle center to the goal location
d_ = project_to_2d(tform.translation) - staging_circle.center
# scale d_ to the appropriate radius, then shift tip vector from part center to geom.center
d = (d_ / norm(d_)) * ring_radius + geom.center
push!(θ_des, atan(d[2], d[1]))
end
# optimize placement and increase staging_radius if necessary
θ_star, staging_radius = solve_ring_placement_problem(θ_des, radii_for_opt, ring_radius)
end
# Compute staging config transforms (relative to parent assembly)
tformed_geoms = Vector{LazySets.Ball2}()
for (i, (θ, r, part_id, part)) in enumerate(zip(θ_star, radii_for_opt, part_ids_for_opt, parts_for_opt))
part_start_node = get_node(sched, AssemblyComplete(part))
geom = staging_circles[part_id]
R = staging_radius + r
# shift by vector from geom.center to part origin
t = CoordinateTransformations.Translation(
R * cos(θ) + staging_circle.center[1] - geom.center[1],
R * sin(θ) + staging_circle.center[2] - geom.center[2],
0.0)
# Get global orientation parent frame ʷRₚ
tform = t ∘ identity_linear_map() # AffineMap transform
# set transform of start node
set_local_transform_in_global_frame!(start_config(part_start_node), tform)
tform2D = CoordinateTransformations.Translation(t.translation[1:2]...)
push!(tformed_geoms, tform2D(geom))
end
old_ball = staging_circles[node_id(assembly)]
new_ball = overapproximate(
vcat(tformed_geoms, staging_circles[node_id(assembly)]),
LazySets.Ball2{Float64,SVector{2,Float64}}
)
@assert new_ball.radius + 1e-5 > old_ball.radius + norm(new_ball.center .- old_ball.center) "$(summary(node_id(assembly))) old ball $(old_ball), new_ball $(new_ball)"
staging_circles[node_id(assembly)] = new_ball
end
# add directly as staging circle of AssemblyComplete node
assembly_complete.inner_staging_circle.base_geom = project_to_3d(orig_ball)
assembly_complete.outer_staging_circle.base_geom = project_to_3d(new_ball)
@info "Updating staging_circle for $(summary(node_id(assembly))) to $(staging_circles[node_id(assembly)])"
end
end
sched
end
"""
select_assembly_start_configs!(sched, scene_tree, staging_radii;
buffer_radius=0.0)
Select the start configs (i.e., the build location) for each assembly. The
location is selected by minimizing distance to the assembly's staging location
while ensuring that no child's staging area overlaps with its parent's staging
area. Uses the same ring optimization approach as for selectig staging
locations.
"""
function select_assembly_start_configs!(sched, scene_tree, staging_circles;
buffer_radius=0.0,
)
# Update assembly start points so that none of the staging regions overlap
for start_node in node_iterator(sched, topological_sort_by_dfs(sched))
if !matches_template(AssemblyComplete, start_node)
continue
end
assembly_complete = node_val(start_node)
node = entity(start_node)
if matches_template(AssemblyNode, node)
# Apply ring solver with child assemblies (not objects)
assembly = node
part_ids = sort(filter(k -> isa(k, AssemblyID),
collect(keys(assembly_components(node)))))
if isempty(part_ids)
continue
end
@info "Selecting staging location for $(summary(node_id(assembly)))"
# start_node = get_node(sched,AssemblyComplete(assembly))
# staging_radii
@assert haskey(staging_circles, node_id(assembly))
staging_circle = staging_circles[node_id(assembly)]
staging_radius = staging_circle.radius + buffer_radius
ring_radius = -1
parts = (get_node(scene_tree, part_id) for part_id in part_ids)
# radii = [staging_circles[id].radius for id in part_ids]
radii = [staging_circles[id].radius + buffer_radius / 2 for id in part_ids]
# θ_des = Vector{Float64}()
θ_star = nothing
# repeat to ensure correct alignment
while staging_radius - ring_radius > 1e-6
θ_des = Vector{Float64}()
ring_radius = staging_radius
for (part_id, part) in zip(part_ids, parts)
# retrieve staging config from LiftIntoPlace node
lift_node = get_node(sched, LiftIntoPlace(part))
# give coords of the dropoff point (we want `part` to be built as close as possible to here.)
# crucial: must be relative to the assembly origin
tr = relative_transform(
global_transform(goal_config(start_node)),
global_transform(start_config(lift_node))).translation
tform = CoordinateTransformations.Translation(tr...)
geom = staging_circles[part_id]
# the vector from the circle center to the goal location
d_ = project_to_2d(tform.translation) - staging_circle.center
# scale d_ to the appropriate radius, then shift tip vector from part center to geom.center
d = (d_ / norm(d_)) * ring_radius + geom.center
push!(θ_des, atan(d[2], d[1]))
end
# optimize placement and increase staging_radius if necessary
θ_star, staging_radius = solve_ring_placement_problem(
θ_des, radii, ring_radius)
end
# Compute staging config transforms (relative to parent assembly)
tformed_geoms = Vector{LazySets.Ball2}()
for (i, (θ, r, part_id, part)) in enumerate(zip(θ_star, radii, part_ids, parts))
part_start_node = get_node(sched, AssemblyComplete(part))
geom = staging_circles[part_id]
R = staging_radius + r
# shift by vector from geom.center to part origin
t = CoordinateTransformations.Translation(
R * cos(θ) + staging_circle.center[1] - geom.center[1],
R * sin(θ) + staging_circle.center[2] - geom.center[2],
0.0)
# Get global orientation parent frame ʷRₚ
tform = t ∘ identity_linear_map() # AffineMap transform
# set transform of start node
# @info "Starting config: setting START config of $(node_id(start_node)) to $(tform)"
set_local_transform_in_global_frame!(start_config(part_start_node), tform)
tform2D = CoordinateTransformations.Translation(t.translation[1:2]...)
push!(tformed_geoms, tform2D(geom))
end
old_ball = staging_circles[node_id(assembly)]
new_ball = overapproximate(
vcat(tformed_geoms, staging_circles[node_id(assembly)]),
LazySets.Ball2{Float64,SVector{2,Float64}}
)
# @assert new_ball.radius > old_ball.radius+norm(new_ball.center .- old_ball.center)
@assert new_ball.radius + 1e-5 > old_ball.radius + norm(new_ball.center .- old_ball.center) "$(summary(node_id(assembly))) old ball $(old_ball), new_ball $(new_ball)"
staging_circles[node_id(assembly)] = new_ball
# add directly as staging circle of AssemblyComplete node
assembly_complete.inner_staging_circle.base_geom = project_to_3d(old_ball)
assembly_complete.outer_staging_circle.base_geom = project_to_3d(new_ball)
@info "Updating staging_circle for $(summary(node_id(assembly))) to $(staging_circles[node_id(assembly)])"
end
end
sched
end
"""
process_schedule_build_step!(node,sched,scene_tree,bounding_circles,staging_circles)
Select the staging configuration for all subcomponents to be added to `assembly`
during `build_step`, where `build_step::OpenBuildStep = node_val(node)`, and
`assembly = build_step.assembly.`
Also updates `bounding_radii[node_id(assembly)]` to reflect the increasing size
of assembly as more parts are added to it.
Updates:
- `staging_circles`
- the relevant `LiftIntoPlace` nodes (start_config and goal_config transforms)
Keyword Args:
- build_step_buffer_radius = 0.0 - amount by which to inflate each transport unit
when layout out the build step.
"""
function process_schedule_build_step!(node, sched, scene_tree, bounding_circles,
staging_circles; build_step_buffer_radius=0.0)
open_build_step = node_val(node)
assembly = open_build_step.assembly
start_node = get_node(sched, AssemblyComplete(assembly))
# optimize staging locations
part_ids = sort(collect(keys(assembly_components(open_build_step))))
parts = (get_node(scene_tree, part_id) for part_id in part_ids)
tforms = (child_transform(assembly, id) for id in part_ids) # working in assembly frame
θ_des = Vector{Float64}()
radii = Vector{Float64}()
geoms = Vector{LazySets.Ball2}()