xlab-api 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,323 @@
1
+ """Motion-reference and bilateral-augmentation declarations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from collections.abc import Mapping, Sequence
7
+ from dataclasses import dataclass, field, replace
8
+ from typing import Literal
9
+
10
+ from ._names import as_name_tuple
11
+
12
+
13
+ def _left_right_name(name: str) -> str:
14
+ """Return a bilateral counterpart, leaving midline names unchanged."""
15
+ if name.startswith("left_"):
16
+ return "right_" + name[5:]
17
+ if name.startswith("right_"):
18
+ return "left_" + name[6:]
19
+ return name
20
+
21
+
22
+ def _left_right_swaps(names: Sequence[str]) -> dict[str, str]:
23
+ """Build ``destination -> source`` swaps for an ordered collection of names."""
24
+ unique = tuple(names)
25
+ if len(set(unique)) != len(unique):
26
+ raise ValueError(f"Names for a left/right swap must be unique: {unique}.")
27
+ available = set(unique)
28
+ swaps: dict[str, str] = {}
29
+ for name in unique:
30
+ other = _left_right_name(name)
31
+ if other not in available:
32
+ raise ValueError(
33
+ f"{name!r} mirrors to {other!r}, which is not in the name list."
34
+ )
35
+ swaps[name] = other
36
+ return swaps
37
+
38
+
39
+ def _roll_yaw_signs(names: Sequence[str]) -> dict[str, int]:
40
+ """Return the sagittal-plane signs used by both source G1 tables."""
41
+ return {name: -1 if "roll" in name or "yaw" in name else 1 for name in names}
42
+
43
+
44
+ def _require_involution(kind: str, swaps: Mapping[str, str]) -> None:
45
+ if not swaps:
46
+ raise ValueError(
47
+ f"{kind}_swaps is empty. Pass None on MotionReferenceRef.symmetric_augmentation to disable mirroring."
48
+ )
49
+ for name, other in swaps.items():
50
+ if not name or not other:
51
+ raise ValueError(f"{kind} swap has an empty name: {name!r} -> {other!r}.")
52
+ back = swaps.get(other)
53
+ if back != name:
54
+ raise ValueError(
55
+ f"{kind} swap {name!r} -> {other!r} is not an involution (reverse is {back!r}). "
56
+ "An unpaired left/right name is the usual cause."
57
+ )
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class SymmetricAugmentationSpec:
62
+ """Left-right mirror of an AMP reference, declared by name.
63
+
64
+ Indices never live in this declaration. The runtime resolves names against the sensor's
65
+ canonical joint and link order, avoiding accidental reuse of a source repository's integer
66
+ table with a different order. ``joint_swaps[name]`` names the source copied onto ``name``;
67
+ signs are applied at the destination.
68
+ """
69
+
70
+ joint_swaps: Mapping[str, str]
71
+ joint_signs: Mapping[str, int]
72
+ link_swaps: Mapping[str, str]
73
+
74
+ def __post_init__(self) -> None:
75
+ joint_swaps = dict(self.joint_swaps)
76
+ joint_signs = {name: int(sign) for name, sign in self.joint_signs.items()}
77
+ link_swaps = dict(self.link_swaps)
78
+ object.__setattr__(self, "joint_swaps", joint_swaps)
79
+ object.__setattr__(self, "joint_signs", joint_signs)
80
+ object.__setattr__(self, "link_swaps", link_swaps)
81
+ _require_involution("joint", joint_swaps)
82
+ _require_involution("link", link_swaps)
83
+ if set(joint_signs) != set(joint_swaps):
84
+ extra = sorted(set(joint_signs) - set(joint_swaps))
85
+ missing = sorted(set(joint_swaps) - set(joint_signs))
86
+ raise ValueError(
87
+ "joint_signs must cover exactly the joints in joint_swaps. "
88
+ f"Missing signs: {missing}. Extra signs: {extra}."
89
+ )
90
+ bad = sorted(name for name, sign in joint_signs.items() if sign not in (-1, 1))
91
+ if bad:
92
+ raise ValueError(f"joint_signs must be +1 or -1; bad: {bad}.")
93
+
94
+ @classmethod
95
+ def from_left_right(
96
+ cls, joints: Sequence[str], links: Sequence[str]
97
+ ) -> SymmetricAugmentationSpec:
98
+ """Build the name pairing and roll/yaw signs used by both G1 sources."""
99
+ return cls(
100
+ joint_swaps=_left_right_swaps(joints),
101
+ joint_signs=_roll_yaw_signs(joints),
102
+ link_swaps=_left_right_swaps(links),
103
+ )
104
+
105
+
106
+ @dataclass(frozen=True)
107
+ class MotionReferenceRef:
108
+ """A clip-backed motion reference shared by both simulation engines.
109
+
110
+ Quaternions use ``wxyz``. Joints leave the file in its published order and are remapped by
111
+ name onto :attr:`joints`. Exhaustion holds the last frame and raises a runtime flag; it never
112
+ silently restarts. Symmetric augmentation is disabled unless explicitly declared by name.
113
+ """
114
+
115
+ name: str
116
+ clip: str
117
+ joints: str | Sequence[str]
118
+ links: str | Sequence[str]
119
+ scene_objects: tuple[str, ...] = field(
120
+ default=(), metadata={"contract_omit_if_default": True}
121
+ )
122
+ engine_clips: Mapping[str, str] = field(
123
+ default_factory=dict, metadata={"contract_omit_if_default": True}
124
+ )
125
+ engine_overrides: Mapping[str, Mapping[str, object]] = field(
126
+ default_factory=dict, metadata={"contract_omit_if_default": True}
127
+ )
128
+ entity: str = "robot"
129
+ num_frames: int = 1
130
+ frame_interval_s: float = 0.02
131
+ update_period: float = 0.02
132
+ data_start_from: Literal["one_frame_interval", "current_time"] = (
133
+ "one_frame_interval"
134
+ )
135
+ clip_target_fps: float = 50.0
136
+ velocity_method: Literal["frontward", "backward", "frontbackward"] = "frontward"
137
+ start_range: tuple[float, float] = (0.0, 0.0)
138
+ dataset_kind: Literal["retargetted", "terrain", "omomo"] = field(
139
+ default="retargetted", metadata={"contract_omit_if_default": True}
140
+ )
141
+ metadata_yaml: str | None = field(
142
+ default=None, metadata={"contract_omit_if_default": True}
143
+ )
144
+ selected_files: tuple[str, ...] = field(
145
+ default=(), metadata={"contract_omit_if_default": True}
146
+ )
147
+ first_motion_only: bool = field(
148
+ default=False, metadata={"contract_omit_if_default": True}
149
+ )
150
+ supported_file_endings: tuple[str, ...] = field(
151
+ default=("retargetted.npz", "retargeted.npz"),
152
+ metadata={"contract_omit_if_default": True},
153
+ )
154
+ sampling_strategy: Literal["independent", "concat_motion_bins"] = field(
155
+ default="independent", metadata={"contract_omit_if_default": True}
156
+ )
157
+ motion_bin_length_s: float | None = field(
158
+ default=None, metadata={"contract_omit_if_default": True}
159
+ )
160
+ ensure_link_below_zero_ground: bool = field(
161
+ default=False, metadata={"contract_omit_if_default": True}
162
+ )
163
+ motion_start_height_offset: float = field(
164
+ default=0.0, metadata={"contract_omit_if_default": True}
165
+ )
166
+ exhaustion: Literal["freeze_last_and_flag"] = "freeze_last_and_flag"
167
+ quaternion: Literal["wxyz"] = "wxyz"
168
+ symmetric_augmentation: SymmetricAugmentationSpec | None = None
169
+
170
+ def __post_init__(self) -> None:
171
+ object.__setattr__(self, "engine_clips", dict(self.engine_clips or {}))
172
+ object.__setattr__(
173
+ self,
174
+ "engine_overrides",
175
+ {engine: dict(values) for engine, values in self.engine_overrides.items()},
176
+ )
177
+ object.__setattr__(self, "joints", as_name_tuple(self.joints))
178
+ object.__setattr__(self, "links", as_name_tuple(self.links))
179
+ object.__setattr__(self, "scene_objects", as_name_tuple(self.scene_objects))
180
+ if not self.name:
181
+ raise ValueError("Motion reference has no name.")
182
+ if not self.clip:
183
+ raise ValueError(f"Motion reference {self.name!r} has no clip path.")
184
+ if any(not engine or not path for engine, path in self.engine_clips.items()):
185
+ raise ValueError(
186
+ f"Motion reference {self.name!r} has an empty engine clip binding."
187
+ )
188
+ allowed_overrides = {
189
+ "ensure_link_below_zero_ground",
190
+ "motion_start_height_offset",
191
+ }
192
+ for engine, values in self.engine_overrides.items():
193
+ unknown = set(values) - allowed_overrides
194
+ if not engine or unknown:
195
+ raise ValueError(
196
+ f"Motion reference {self.name!r} has invalid overrides for {engine!r}: {sorted(unknown)}."
197
+ )
198
+ if not self.joints:
199
+ raise ValueError(
200
+ f"Motion reference {self.name!r} was given no joint names."
201
+ )
202
+ if not self.links:
203
+ raise ValueError(f"Motion reference {self.name!r} was given no link names.")
204
+ if len(set(self.joints)) != len(self.joints):
205
+ raise ValueError(f"Motion reference {self.name!r} repeats a joint name.")
206
+ if len(set(self.links)) != len(self.links):
207
+ raise ValueError(f"Motion reference {self.name!r} repeats a link name.")
208
+ if len(set(self.scene_objects)) != len(self.scene_objects):
209
+ raise ValueError(
210
+ f"Motion reference {self.name!r} repeats a scene object name."
211
+ )
212
+ if self.num_frames < 1:
213
+ raise ValueError(
214
+ f"Motion reference {self.name!r} has num_frames={self.num_frames}."
215
+ )
216
+ if not math.isfinite(self.frame_interval_s) or self.frame_interval_s < 0.0:
217
+ raise ValueError(
218
+ f"Motion reference {self.name!r} has a negative frame_interval_s."
219
+ )
220
+ if self.frame_interval_s == 0.0 and self.num_frames != 1:
221
+ raise ValueError(
222
+ f"Motion reference {self.name!r} has frame_interval_s=0 with num_frames={self.num_frames}; "
223
+ "zero interval is only meaningful for a current-frame-only reference."
224
+ )
225
+ if not math.isfinite(self.update_period) or self.update_period <= 0.0:
226
+ raise ValueError(
227
+ f"Motion reference {self.name!r} has a non-positive update_period."
228
+ )
229
+ if not math.isfinite(self.clip_target_fps) or self.clip_target_fps <= 0.0:
230
+ raise ValueError(
231
+ f"Motion reference {self.name!r} has a non-positive clip_target_fps."
232
+ )
233
+ lo, hi = self.start_range
234
+ if (
235
+ not math.isfinite(lo)
236
+ or not math.isfinite(hi)
237
+ or lo < 0.0
238
+ or hi < lo
239
+ or hi > 1.0
240
+ ):
241
+ raise ValueError(
242
+ f"Motion reference {self.name!r} has start_range={self.start_range}; "
243
+ "it must satisfy 0 <= lo <= hi <= 1 (a fraction of clip duration)."
244
+ )
245
+ if self.data_start_from not in {"one_frame_interval", "current_time"}:
246
+ raise ValueError(
247
+ f"Motion reference {self.name!r} has data_start_from={self.data_start_from!r}."
248
+ )
249
+ if self.velocity_method not in {"frontward", "backward", "frontbackward"}:
250
+ raise ValueError(
251
+ f"Motion reference {self.name!r} has velocity_method={self.velocity_method!r}."
252
+ )
253
+ if self.dataset_kind not in {"retargetted", "terrain", "omomo"}:
254
+ raise ValueError(
255
+ f"Motion reference {self.name!r} has dataset_kind={self.dataset_kind!r}."
256
+ )
257
+ if self.dataset_kind == "terrain" and not self.metadata_yaml:
258
+ raise ValueError(
259
+ f"Terrain motion reference {self.name!r} requires metadata_yaml."
260
+ )
261
+ if any(not path for path in self.selected_files):
262
+ raise ValueError(
263
+ f"Motion reference {self.name!r} has an empty selected file name."
264
+ )
265
+ if not self.supported_file_endings or any(
266
+ not suffix for suffix in self.supported_file_endings
267
+ ):
268
+ raise ValueError(
269
+ f"Motion reference {self.name!r} must declare non-empty file endings."
270
+ )
271
+ if self.sampling_strategy not in {"independent", "concat_motion_bins"}:
272
+ raise ValueError(
273
+ f"Motion reference {self.name!r} has sampling_strategy={self.sampling_strategy!r}."
274
+ )
275
+ if self.motion_bin_length_s is not None and (
276
+ not math.isfinite(self.motion_bin_length_s)
277
+ or self.motion_bin_length_s <= 0.0
278
+ ):
279
+ raise ValueError(
280
+ f"Motion reference {self.name!r} has invalid motion_bin_length_s."
281
+ )
282
+ if not math.isfinite(self.motion_start_height_offset):
283
+ raise ValueError(
284
+ f"Motion reference {self.name!r} has non-finite motion_start_height_offset."
285
+ )
286
+ if self.exhaustion != "freeze_last_and_flag":
287
+ raise ValueError(
288
+ f"Motion reference {self.name!r} has exhaustion={self.exhaustion!r}; "
289
+ "silent restart is refused because it hides dataset exhaustion."
290
+ )
291
+ if self.quaternion != "wxyz":
292
+ raise ValueError(
293
+ f"Motion reference {self.name!r} has quaternion={self.quaternion!r}; hub convention is wxyz."
294
+ )
295
+ if self.symmetric_augmentation is not None:
296
+ spec_joints = set(self.symmetric_augmentation.joint_swaps)
297
+ have_joints = set(self.joints)
298
+ if spec_joints != have_joints:
299
+ raise ValueError(
300
+ f"Motion reference {self.name!r} symmetric_augmentation joints "
301
+ f"{sorted(spec_joints)} do not match sensor joints {sorted(have_joints)}. "
302
+ "The maps are names; a leftover integer table from another order is the usual cause."
303
+ )
304
+ spec_links = set(self.symmetric_augmentation.link_swaps)
305
+ have_links = set(self.links)
306
+ if spec_links != have_links:
307
+ raise ValueError(
308
+ f"Motion reference {self.name!r} symmetric_augmentation links "
309
+ f"{sorted(spec_links)} do not match sensor links {sorted(have_links)}."
310
+ )
311
+
312
+ def for_engine(self, engine: str) -> MotionReferenceRef:
313
+ """Resolve the reference project's engine-specific dataset root."""
314
+ return replace(
315
+ self,
316
+ clip=self.engine_clips.get(engine, self.clip),
317
+ engine_clips={},
318
+ engine_overrides={},
319
+ **self.engine_overrides.get(engine, {}),
320
+ )
321
+
322
+
323
+ __all__ = ["MotionReferenceRef", "SymmetricAugmentationSpec"]
@@ -0,0 +1,106 @@
1
+ """Report the independent dimensions that make a task portable or native."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from typing import Any
7
+
8
+ from .task import TaskSpec
9
+
10
+
11
+ def _keys_by_engine(values: Mapping[str, Any]) -> dict[str, list[str]]:
12
+ keys: dict[str, list[str]] = {}
13
+ for engine, overrides in sorted(values.items()):
14
+ if isinstance(overrides, Mapping):
15
+ keys[engine] = sorted(str(key) for key in overrides)
16
+ else:
17
+ keys[engine] = []
18
+ return keys
19
+
20
+
21
+ def _semantic_overlays(spec: TaskSpec) -> list[dict[str, Any]]:
22
+ overlays: list[dict[str, Any]] = []
23
+
24
+ def record(path: str, values: Mapping[str, Any]) -> None:
25
+ if values:
26
+ overlays.append(
27
+ {
28
+ "path": path,
29
+ "engines": sorted(values),
30
+ "keys": _keys_by_engine(values),
31
+ }
32
+ )
33
+
34
+ record("sim.profiles", spec.sim.profiles)
35
+ for term_path, term in spec.mdp.terms().items():
36
+ record(f"mdp.{term_path}.engine_params", term.engine_params)
37
+ record("agent.engine_overrides", spec.agent.engine_overrides)
38
+ return sorted(overlays, key=lambda entry: entry["path"])
39
+
40
+
41
+ def _native_extras(spec: TaskSpec) -> list[dict[str, Any]]:
42
+ return [
43
+ {
44
+ "engine": engine,
45
+ "keys": sorted(str(key) for key in extras),
46
+ }
47
+ for engine, extras in sorted(spec.engine_extras.items())
48
+ if extras
49
+ ]
50
+
51
+
52
+ def _clean_resolution(
53
+ resolution_manifest: Mapping[str, Any] | None,
54
+ ) -> dict[str, Any]:
55
+ fields = ("skipped", "emulated", "omitted")
56
+ known = resolution_manifest is not None and all(
57
+ field in resolution_manifest for field in fields
58
+ )
59
+ if not known:
60
+ return {
61
+ "known": False,
62
+ "clean": None,
63
+ "skipped": None,
64
+ "emulated": None,
65
+ "omitted": None,
66
+ }
67
+ counts = {field: len(resolution_manifest.get(field, {})) for field in fields}
68
+ return {
69
+ "known": True,
70
+ "clean": not any(counts.values()),
71
+ **counts,
72
+ }
73
+
74
+
75
+ def portability_report(
76
+ spec: TaskSpec,
77
+ resolution_manifest: Mapping[str, Any] | None = None,
78
+ ) -> dict[str, Any]:
79
+ """Return contract, overlay, native-extra, and resolution dimensions.
80
+
81
+ A task may have a portable multi-engine contract while intentionally
82
+ retaining native solver or randomization values. Conversely, a declaration
83
+ with no overlays can still compile non-cleanly on a backend. Keeping these
84
+ dimensions separate prevents one optimistic boolean from hiding either
85
+ condition.
86
+ """
87
+ semantic_overlays = _semantic_overlays(spec)
88
+ native_extras = _native_extras(spec)
89
+ return {
90
+ "contract_portability": {
91
+ "multi_engine": len(spec.engines) > 1,
92
+ "declared_engines": list(spec.engines),
93
+ },
94
+ "semantic_overlays": {
95
+ "count": len(semantic_overlays),
96
+ "entries": semantic_overlays,
97
+ },
98
+ "native_extras": {
99
+ "count": len(native_extras),
100
+ "entries": native_extras,
101
+ },
102
+ "clean_resolution": _clean_resolution(resolution_manifest),
103
+ }
104
+
105
+
106
+ __all__ = ["portability_report"]
@@ -0,0 +1,29 @@
1
+ """Narrow engine-neutral relations whose physical meaning both backends preserve."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class CollisionExclusionRef:
10
+ """Disable collision generation for one unordered pair of entity bodies."""
11
+
12
+ body_a: str
13
+ body_b: str
14
+ entity: str = "robot"
15
+
16
+ def __post_init__(self) -> None:
17
+ if not self.entity or not self.body_a or not self.body_b:
18
+ raise ValueError(
19
+ "Collision exclusion entity and body names must be non-empty."
20
+ )
21
+ if self.body_a == self.body_b:
22
+ raise ValueError("A collision exclusion must name two different bodies.")
23
+
24
+ @property
25
+ def pair(self) -> tuple[str, str]:
26
+ return tuple(sorted((self.body_a, self.body_b)))
27
+
28
+
29
+ __all__ = ["CollisionExclusionRef"]
@@ -0,0 +1,103 @@
1
+ """Engine-neutral rigid mesh objects used by HOI shadowing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from collections.abc import Mapping
7
+ from dataclasses import dataclass, field, replace
8
+ from pathlib import Path
9
+
10
+ from xlab_api.data import resolve_data_path
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class RigidObjectRef:
15
+ """A mesh object with explicit spawn, reset, collision, and material meaning.
16
+
17
+ ``friction`` is one isotropic Coulomb coefficient. Isaac applies the same
18
+ value as static and dynamic friction; MuJoCo applies it as sliding friction.
19
+ Backend-only contact models do not belong in this shared reference.
20
+ """
21
+
22
+ name: str
23
+ mesh: str
24
+ scale: tuple[float, float, float]
25
+ engine_meshes: Mapping[str, str] = field(default_factory=dict)
26
+ mass: float = 1.0
27
+ kinematic: bool = True
28
+ collision_enabled: bool = True
29
+ friction: float = 1.0
30
+ initial_position: tuple[float, float, float] = (0.0, 0.0, 0.0)
31
+ initial_quaternion_wxyz: tuple[float, float, float, float] = (1.0, 0.0, 0.0, 0.0)
32
+ initial_linear_velocity: tuple[float, float, float] = (0.0, 0.0, 0.0)
33
+ initial_angular_velocity: tuple[float, float, float] = (0.0, 0.0, 0.0)
34
+
35
+ def __post_init__(self):
36
+ object.__setattr__(self, "engine_meshes", dict(self.engine_meshes))
37
+ if not self.name or not self.mesh:
38
+ raise ValueError("Rigid object name and mesh must be non-empty.")
39
+ if len(self.scale) != 3 or not all(
40
+ math.isfinite(value) and value > 0.0 for value in self.scale
41
+ ):
42
+ raise ValueError(
43
+ f"Rigid object {self.name!r} scale must contain three positive values."
44
+ )
45
+ if not math.isfinite(self.mass) or self.mass <= 0.0:
46
+ raise ValueError(f"Rigid object {self.name!r} mass must be positive.")
47
+ if not math.isfinite(self.friction) or self.friction < 0.0:
48
+ raise ValueError(
49
+ f"Rigid object {self.name!r} friction must be non-negative."
50
+ )
51
+ vectors = (
52
+ ("initial_position", self.initial_position, 3),
53
+ ("initial_quaternion_wxyz", self.initial_quaternion_wxyz, 4),
54
+ ("initial_linear_velocity", self.initial_linear_velocity, 3),
55
+ ("initial_angular_velocity", self.initial_angular_velocity, 3),
56
+ )
57
+ for field_name, values, size in vectors:
58
+ if len(values) != size or not all(math.isfinite(value) for value in values):
59
+ raise ValueError(
60
+ f"Rigid object {self.name!r} {field_name} must contain {size} finite values."
61
+ )
62
+ quaternion_norm = math.sqrt(
63
+ sum(value * value for value in self.initial_quaternion_wxyz)
64
+ )
65
+ if not math.isclose(quaternion_norm, 1.0, rel_tol=1.0e-5, abs_tol=1.0e-6):
66
+ raise ValueError(
67
+ f"Rigid object {self.name!r} initial quaternion must be normalized."
68
+ )
69
+
70
+ def for_engine(self, engine: str) -> RigidObjectRef:
71
+ return replace(
72
+ self, mesh=self.engine_meshes.get(engine, self.mesh), engine_meshes={}
73
+ )
74
+
75
+ def resource_path(self, engine: str, *, require_exists: bool = True) -> Path:
76
+ """Resolve this engine's local mesh and optionally require it to exist."""
77
+ path = resolve_data_path(self.engine_meshes.get(engine, self.mesh))
78
+ if require_exists and not path.is_file():
79
+ raise FileNotFoundError(
80
+ f"Rigid object {self.name!r} resource for {engine!r} does not exist: {path}."
81
+ )
82
+ return path
83
+
84
+ def resource_report(self, engine: str) -> dict[str, object]:
85
+ path = self.resource_path(engine, require_exists=False)
86
+ return {
87
+ "name": self.name,
88
+ "declared_resource": self.engine_meshes.get(engine, self.mesh),
89
+ "resource": str(path),
90
+ "exists": path.is_file(),
91
+ "kinematic": self.kinematic,
92
+ "collision_enabled": self.collision_enabled,
93
+ "friction": self.friction,
94
+ "reset": {
95
+ "position": list(self.initial_position),
96
+ "quaternion_wxyz": list(self.initial_quaternion_wxyz),
97
+ "linear_velocity": list(self.initial_linear_velocity),
98
+ "angular_velocity": list(self.initial_angular_velocity),
99
+ },
100
+ }
101
+
102
+
103
+ __all__ = ["RigidObjectRef"]