Table-driven attribute reading: rebase the reader on generated rows.

Every mechanical attribute read in MJCF now derives from mjcf.schema.
generate_read_table.py emits typed mjXAttr rows (mjcf_read_table.inc,
doc_test-gated) binding each attribute to its spec struct field; field
offsets are offsetof() expressions, so binding mistakes are compile
errors, and the field's C type -- parsed from the headers -- selects the
row kind, so mjtNum-versus-double is decided by the struct, not the
schema. mjXReader::ReadAttrTable is the generic loop; its static core
also serves the section parsers and records XML-authored fields via
mjs_setAuthored for attach conflict resolution. Row kinds cover
strings, string lists, numeric scalars and vectors (exact and ranged),
enums (int- and byte-width), bitwise flag sets, bools, unbounded typed
vectors, fixed char arrays, and identity constants declared by 'set'.
The rows are inline variables, and carry the writing=custom flag,
because the writer will share them.

The keyword maps the rows reference are generated too: the ~48
hand-written mjMap tables become mjcf_map.h, one map and size constant
per enum as C++17 inline variables, retiring the hand-maintained
extern block in xml_base.h. Map names follow the schema enum names
(fluid->fluidshape, TFAuto->FalseTrueAuto, FAuto->FalseAuto,
joint->jointtype, geom->geomtype, jac->jacobian); all maps are
key-order- and value-identical to the hand tables they replace, and
bool_map is hand-emitted (the bool type is built in, not a schema
enum).

The OneX() parsers reduce to genuine irregulars, schema-marked as
reading=custom: orientation alternatives, file attributes (VFS and
asset-dir context), the actuator shorthand remappings and per-type
input maps, springlength's one-value copy, mesh builtin construction,
hfield elevation, texture cube files, flexcomp seeding, the memory
suffix parse, and the flag bit families. All 41 sensors that are pure
identity-plus-references -- including the frame family and insidesite
-- dispatch through a generated tag table; frame-sensor
objtype/reftype vocabulary tightens from the full mju_str2Type
namespace to the documented body/xbody/geom/site/camera subset, so an
invalid keyword now fails at parse time instead of compile time. The
equality family and both tendon types read shared group rows; the
twelve actuator shorthands share the general rows, with per-tag
legality enforced by the schema check. Sections bind non-mjs structs,
the visual sub-sections reaching their anonymous sub-structs through
member paths declared by an element-level field= facet.

Latent irregularities surfaced by the migration and preserved via
schema declarations or remnants: key's name is set even when absent,
eulerseq and gridlayout are fixed char arrays (chars[n], arity in
characters), gridlayout's length-must-match-gridsize stays a
value-conditional remnant, and constructor-style elements (tendon
wraps, asset model, replicate, attach) are annotated as such -- their
attributes are arguments, not field writes.

Two coherence tests guard the schema against the C sources: every
schema enum constant must be a member of the C enum it claims, and
every C member must be a keyword, a count sentinel, or a documented
exemption; and generate_default_table.py emits one row per defaulted
attribute (mjcf_default_table.inc), compared by SchemaDefaultsTest
against a freshly-constructed spec -- the schema cannot disagree with
the C default-constructors without failing the suite.

Verified: doc_test regenerates and diffs every artifact; the full
suite; and an A/B harness compiling the model corpus against the
pre-migration reader -- saved XML and binary models are byte-identical.
PiperOrigin-RevId: 958075724
Change-Id: I9715fe4deeb438eec988fd5084d74ba8b466b10b
This commit is contained in:
Yuval Tassa
2026-08-02 17:24:11 -07:00
committed by Copybara-Service
parent 790f8fac30
commit 4278c7b0cd
14 changed files with 3131 additions and 1668 deletions
+186
View File
@@ -0,0 +1,186 @@
# Copyright 2026 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Generates the default-value check table from src/xml/mjcf.schema.
The schema declares attribute defaults, but the defaults that act live in
the C default-constructors (mjs_default*, mj_defaultOption, ...). This
emits src/xml/mjcf_default_table.inc: one row per defaulted attribute,
binding the declared values to the field they describe, consumed by
SchemaDefaultsTest, which compares every row against a freshly-constructed
spec -- so a schema default that disagrees with the C defaults is a test
failure, not documentation drift. Checked in and gated by
test/doc/doc_test.py.
"""
import os
import sys
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, _SCRIPT_DIR)
import generate_read_table
import mjcf_schema
_REPO_ROOT = os.path.dirname(os.path.dirname(_SCRIPT_DIR))
SCHEMA_PATH = os.path.join(_REPO_ROOT, 'src', 'xml', 'mjcf.schema')
# kind codes shared with the test
KIND_BY_CTYPE = {'double': 0, 'float': 1, 'int': 2,
'mjtByte': 3, 'mjtBool': 3, 'mjtNum': 4}
_HEADER = '''\
// Copyright 2026 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// GENERATED FILE, DO NOT EDIT. Generated from src/xml/mjcf.schema by
// doc/generate/generate_default_table.py; test/doc/doc_test.py checks
// freshness.
//
// One row per schema attribute with a declared default, binding the declared
// values to the bound field. SchemaDefaultsTest compares every row against a
// freshly-constructed spec: the schema's defaults must agree with the C
// default-constructors. Rows are {attr, offset, kind, len, ndecl, values};
// values beyond ndecl are expected to be zero; kind: 0=double 1=float 2=int
// 3=byte 4=mjtNum.
// clang-format off
struct mjXDefaultEntry {
const char* attr;
int offset;
int kind;
int len;
int ndecl;
double value[8];
};
struct mjXDefaultTable {
const char* structname;
const mjXDefaultEntry* entries;
int n;
};
'''
def _values(schema, attr, ctype):
"""(ndecl, [C value expressions]) for an attribute's declared default."""
default = attr.default
if attr.type == 'enum':
constants = dict(schema.enums[attr.target].items)
return 1, [f'(double){constants[default]}']
if attr.type == 'bool':
return 1, ['1' if default == 'true' else '0']
values = default if isinstance(default, tuple) else (default,)
return len(values), [repr(v) for v in values]
def collect(schema, structs):
"""struct key -> list of row tuples, deduplicated across elements."""
tables = {}
seen = {}
for element in schema.elements.values():
if not element.spec:
continue
sub = element.facets.get('field')
key = f'{element.spec}.{sub}' if sub else element.spec
fields = structs.get(key)
if fields is None:
continue
prefix = f'{sub}.' if sub else ''
for attr in schema.expanded_attrs(element):
if attr.default is None or attr.type in ('string', 'file', 'chars',
'ref', 'id', 'flags'):
continue
field = attr.facets.get('field', attr.name)
entry = fields.get(field)
if entry is None:
if 'reading' in attr.facets:
continue # custom lowering with no direct binding
raise ValueError(f'{element.name}.{attr.name}: no field '
f'{element.spec}.{field}')
ctype, dim = entry
if ctype not in KIND_BY_CTYPE and not ctype.startswith('mjt'):
raise ValueError(f'{element.name}.{attr.name}: default bound to '
f'field {field} ({ctype})')
kind = KIND_BY_CTYPE.get(ctype, 2) # other mjt enums are int-sized
length = dim if dim is not None else '1'
ndecl, values = _values(schema, attr, ctype)
if ndecl > 8:
raise ValueError(f'{element.name}.{attr.name}: {ndecl} default '
'values exceed the row capacity')
row = (attr.name, f'(int)offsetof({element.spec}, {prefix}{field})',
kind, str(length), ndecl, values)
prior = seen.get((key, field))
if prior is not None:
if prior != (ndecl, values):
raise ValueError(f'{element.name}.{attr.name}: conflicting '
f'defaults for {key}.{field}')
continue
seen[(key, field)] = (ndecl, values)
tables.setdefault(key, []).append(row)
return tables
def generate() -> str:
schema = mjcf_schema.parse_file(SCHEMA_PATH)
structs = generate_read_table.parse_spec_structs(
generate_read_table.SPEC_H_PATH, generate_read_table.MODEL_H_PATH)
tables = collect(schema, structs)
out = [_HEADER]
for key in sorted(tables):
array = 'kDefaults_' + key.replace('.', '_')
out.append(f'static const mjXDefaultEntry {array}[] = {{')
for attr, offset, kind, length, ndecl, values in tables[key]:
vals = ', '.join(values)
out.append(f' {{"{attr}", {offset}, {kind}, {length}, {ndecl}, '
f'{{{vals}}}}},')
out.append('};')
out.append('')
out.append('static const mjXDefaultTable kDefaultTables[] = {')
for key in sorted(tables):
array = 'kDefaults_' + key.replace('.', '_')
root = key.split('.')[0]
out.append(f' {{"{root}", {array}, '
f'(int)(sizeof({array}) / sizeof({array}[0]))}},')
out.append('};')
out.append('static const int kDefaultTablesN = '
'(int)(sizeof(kDefaultTables) / sizeof(kDefaultTables[0]));')
out.append('// clang-format on')
return '\n'.join(out) + '\n'
def main() -> int:
if len(sys.argv) > 2:
sys.exit('usage: generate_default_table.py [output.inc]')
text = generate()
if len(sys.argv) == 2:
with open(sys.argv[1], 'w', encoding='utf-8') as file:
file.write(text)
else:
sys.stdout.write(text)
return 0
if __name__ == '__main__':
sys.exit(main())
+116
View File
@@ -0,0 +1,116 @@
# Copyright 2026 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Generates the keyword-map header from src/xml/mjcf.schema.
Emits src/xml/mjcf_map.h: one mjMap array and size constant per schema
enum, as C++17 inline variables, so the reader, the writer and the
generated tables share one definition with no extern declarations to
maintain. It is checked in and gated by test/doc/doc_test.py, which
regenerates it from the schema and diffs.
"""
import os
import sys
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, _SCRIPT_DIR)
import mjcf_schema
_REPO_ROOT = os.path.dirname(os.path.dirname(_SCRIPT_DIR))
SCHEMA_PATH = os.path.join(_REPO_ROOT, 'src', 'xml', 'mjcf.schema')
_GUARD = 'MUJOCO_SRC_XML_MJCF_MAP_H_'
_INCLUDES = '''\
#include <mujoco/mjspec.h>
#include <mujoco/mjtype.h>
#include "user/user_composite.h"
#include "user/user_flexcomp.h"
#include "xml/xml_util.h"'''
_HEADER = f'''\
// Copyright 2026 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// GENERATED FILE, DO NOT EDIT. Generated from src/xml/mjcf.schema by
// doc/generate/generate_mjcf_map.py; test/doc/doc_test.py checks freshness.
//
// Keyword maps, one per schema enum, shared by the reader, the writer and
// the generated tables. Inline variables: including this header is all a
// translation unit needs.
#ifndef HEADER_GUARD_PLACEHOLDER
#define HEADER_GUARD_PLACEHOLDER
{_INCLUDES}
// clang-format off
// keywords of the built-in bool type (not a schema enum)
inline constexpr mjMap bool_map[] = {{
{{"false", 0}},
{{"true", 1}},
}};
'''
_FOOTER = '''\
// clang-format on
#endif // HEADER_GUARD_PLACEHOLDER
'''
def generate() -> str:
schema = mjcf_schema.parse_file(SCHEMA_PATH)
out = []
for enum in schema.enums.values():
width = max(len(key) for key in enum.keywords()) + 3
out.append(f'// enum {enum.name}')
out.append(f'inline constexpr mjMap {enum.name}_map[] = {{')
for key, value in enum.items:
padded = f'"{key}",'.ljust(width + 1)
out.append(f' {{{padded} {value}}},')
out.append('};')
out.append(f'inline constexpr int {enum.name}_sz = {len(enum.items)};')
out.append('')
header = _HEADER.replace('HEADER_GUARD_PLACEHOLDER', _GUARD)
footer = _FOOTER.replace('HEADER_GUARD_PLACEHOLDER', _GUARD)
return header + '\n'.join(out) + footer
def main() -> int:
if len(sys.argv) > 2:
sys.exit('usage: generate_mjcf_map.py [output.h]')
text = generate()
if len(sys.argv) == 2:
with open(sys.argv[1], 'w', encoding='utf-8') as file:
file.write(text)
else:
sys.stdout.write(text)
return 0
if __name__ == '__main__':
sys.exit(main())
+451
View File
@@ -0,0 +1,451 @@
# Copyright 2026 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Generates keyword maps and typed attribute-read rows from mjcf.schema.
Emits src/xml/mjcf_read_table.inc: per-element mjXAttr row arrays consumed by
mjXReader::ReadAttrTable, plus the kSensorDispatch tag table and shared group
arrays.
Which elements get rows is determined automatically: every schema element with
a bound spec struct and at least one table-drivable attribute is included,
unless it appears in NOT_TABLE_DRIVEN (elements whose OneX() readers have
custom logic). A coverage check in doc_test verifies that every eligible
element is accounted for.
Field offsets are emitted as offsetof() expressions, so binding mistakes are
compile errors, and the field's C type (parsed from mjspec.h) selects the row
kind, so mjtNum vs double is decided by the struct, not by the schema.
"""
import os
import re
import sys
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, _SCRIPT_DIR)
import mjcf_schema
_REPO_ROOT = os.path.dirname(os.path.dirname(_SCRIPT_DIR))
SCHEMA_PATH = os.path.join(_REPO_ROOT, 'src', 'xml', 'mjcf.schema')
SPEC_H_PATH = os.path.join(_REPO_ROOT, 'include', 'mujoco', 'mjspec.h')
MODEL_H_PATH = os.path.join(_REPO_ROOT, 'include', 'mujoco', 'mjmodel.h')
# elements with a bound spec whose OneX() reader is NOT table-driven;
# their rows are not emitted. These are the genuinely irregular elements
# whose readers have custom logic that can't be factored into ReadAttrTable.
# If you add a new schema element with a bound spec, either migrate its
# reader to ReadAttrTable (it will be auto-included) or add it here.
NOT_TABLE_DRIVEN = {
# actuator shorthands: read shared rows + per-tag remappings
'motor', 'position', 'velocity', 'intvelocity', 'orientation',
'pid', 'damper', 'cylinder', 'muscle', 'adhesion', 'dcmotor',
'actuator_plugin',
# equality subtypes: read shared equality_base + per-type refs
'connect', 'weld', 'equality_joint', 'equality_tendon',
'equality_flex', 'flexvert', 'flexstrain',
# sensors with custom reading logic
'rangefinder', 'distance', 'user', 'normal', 'fromto',
'sensor_contact', 'sensor_plugin', 'tactile',
# other irregulars
'frame', 'plugin', 'numeric', 'text', 'tuple',
}
# sensors whose whole branch derives from the schema (identity constants +
# references); their arrays are also collected into kSensorDispatch
SENSOR_DISPATCH = [
'touch',
'accelerometer',
'velocimeter',
'gyro',
'force',
'torque',
'magnetometer',
'camprojection',
'jointpos',
'jointvel',
'tendonpos',
'tendonvel',
'actuatorpos',
'actuatorvel',
'actuatorfrc',
'jointactuatorfrc',
'tendonactuatorfrc',
'ballquat',
'ballangvel',
'jointlimitpos',
'jointlimitvel',
'jointlimitfrc',
'tendonlimitpos',
'tendonlimitvel',
'tendonlimitfrc',
'framepos',
'framequat',
'framexaxis',
'frameyaxis',
'framezaxis',
'framelinvel',
'frameangvel',
'framelinacc',
'frameangacc',
'insidesite',
'subtreecom',
'subtreelinvel',
'subtreeangmom',
'e_potential',
'e_kinetic',
'clock',
]
# shared groups emitted as standalone row arrays, for shared OneX() readers:
# group name -> (bound struct, emitted array name)
EMIT_GROUPS = {
'equality_base': ('mjsEquality', 'kEqualityBaseAttrs'),
'sensor_base': ('mjsSensor', 'kSensorBaseAttrs'),
}
# groups whose attributes are hand-read in the OneX() remnant;
# their rows are not emitted
HAND_GROUPS = ['orientation', 'transmission',
'sensor_base'] # read via kSensorBaseAttrs before dispatch
# mjspec.h array-dimension expressions equivalent to a numeric schema bound;
# the expression is emitted as the row length so it tracks the header
DIM_EQUIV = {'mjNPOLY+1': '3'}
# schema scalar/C-type -> mjXAttr kind
KIND_BY_CTYPE = {'mjString*': 'kString', 'int': 'kInt', 'double': 'kDouble',
'mjtNum': 'kNum', 'float': 'kFloat'}
_HEADER = '''\
// Copyright 2026 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// GENERATED FILE, DO NOT EDIT. Generated from src/xml/mjcf.schema by
// doc/generate/generate_read_table.py; test/doc/doc_test.py checks freshness.
//
// Typed attribute rows for table-driven reading (mjXReader::ReadAttrTable),
// one array per migrated element. Rows are {attr, kind, len, exact,
// required, nodefault, handwrite, offset}; keyword maps are defined in
// mjcf_map.h.
// clang-format off
'''
def _has_table_attrs(schema, element):
"""Check if the element has at least one non-custom table-drivable attr."""
for member in element.members:
if isinstance(member, mjcf_schema.Attr) and 'reading' not in member.facets:
return True
if isinstance(member, mjcf_schema.Use):
group = schema.groups.get(member.group)
if group:
for gm in group.members:
if (isinstance(gm, mjcf_schema.Attr)
and 'reading' not in gm.facets):
return True
return False
def table_driven_elements(schema):
"""Return the list of element names whose readers are table-driven.
An element is table-driven if it has a bound spec struct, at least one
table-drivable attribute, and is not in NOT_TABLE_DRIVEN.
Args:
schema: Parsed schema object.
"""
result = []
for name, element in schema.elements.items():
if not element.spec:
continue
if not _has_table_attrs(schema, element):
continue
if name in NOT_TABLE_DRIVEN:
continue
result.append(name)
return result
def uncovered_elements(schema):
"""Return element names eligible for table-driving but not accounted for.
An element is "eligible" if it has a bound spec struct and at least one
table-drivable attribute. Every eligible element should be either
auto-included by table_driven_elements() or listed in NOT_TABLE_DRIVEN.
This function returns any that fall through the cracks — used by doc_test.
Args:
schema: Parsed schema object.
"""
eligible = {name for name, el in schema.elements.items()
if el.spec and _has_table_attrs(schema, el)}
auto = set(table_driven_elements(schema))
return eligible - auto - NOT_TABLE_DRIVEN
def parse_spec_structs(*paths):
"""Parse struct layouts from C headers.
Args:
*paths: Paths to C header files to parse.
Returns:
A dict mapping struct name -> {field: (ctype, dim-or-None)}.
Nested anonymous sub-structs (the mjVisual sections) are exposed as
'Outer.subname' pseudo-structs.
"""
structs = {}
field_re = re.compile(
r'^\s*([\w<>*]+(?:\s*\*)?)\s+(\w+)(\[([^\]]+)\])?\s*;', re.M)
for path in paths:
with open(path, encoding='utf-8') as f:
text = f.read()
for m in re.finditer(r'typedef struct (mj\w+)_ \{(.*?)\n\} \1;', text,
re.S):
name, body = m.group(1), m.group(2)
for sm in re.finditer(r'struct \{(.*?)\} (\w+);', body, re.S):
sub_fields = {}
for fm in field_re.finditer(sm.group(1)):
ctype, fname, _, dim = fm.groups()
sub_fields[fname] = (ctype.replace(' ', ''), dim)
structs[f'{name}.{sm.group(2)}'] = sub_fields
body = re.sub(r'struct \{.*?\} \w+;', '', body, flags=re.S)
fields = {}
for fm in field_re.finditer(body):
ctype, fname, _, dim = fm.groups()
fields[fname] = (ctype.replace(' ', ''), dim)
structs[name] = fields
return structs
def _attr_row(spec, fields, attr, ctx, prefix=''):
"""Return a row tuple for one attribute, or None if it has no row."""
required = 'true' if attr.facets.get('required') else 'false'
nodefault = 'true' if attr.facets.get('nodefault') else 'false'
handwrite = 'true' if attr.facets.get('writing') else 'false'
if attr.type == 'id' and attr.name == 'name':
# names are never settable in default classes
return (f'"{attr.name}"', 'mjXAttr::kName', '1', 'true', required,
'true', 'false', '-1')
if attr.type == 'ref' and attr.target == 'default':
return None # class selection is handled by the section parsers
if attr.type == 'file':
return None # files need VFS/asset-dir context: hand-read in the remnant
field = attr.facets.get('field', attr.name)
entry = fields.get(field)
if entry is None:
raise ValueError(f'{ctx}.{attr.name}: no field {spec}.{field}')
ctype, dim = entry
if attr.type in ('string', 'file', 'ref', 'id'):
if ctype == 'mjStringVec*':
kind, length, exact = 'kStringVec', '1', 'true'
elif ctype != 'mjString*':
raise ValueError(f'{ctx}.{attr.name}: bound to non-string '
f'field {field} ({ctype})')
else:
kind, length, exact = 'kString', '1', 'true'
elif attr.type == 'enum':
if not ctype.startswith('mjt') and ctype != 'int':
raise ValueError(f'{ctx}.{attr.name}: enum bound to '
f'field {field} ({ctype})')
kind = 'kEnumByte' if ctype in ('mjtByte', 'mjtBool') else 'kEnum'
length, exact = '1', 'true'
elif attr.type == 'flags':
if ctype != 'int':
raise ValueError(f'{ctx}.{attr.name}: flags bound to '
f'field {field} ({ctype})')
kind, length, exact = 'kFlags', '1', 'true'
elif attr.type == 'bool':
if ctype in ('mjtBool', 'mjtByte'):
kind, length, exact = 'kBool', '1', 'true'
elif ctype == 'int' or ctype.startswith('mjt'):
# int-typed flag: read through the keyword map (values are 0/1)
row = [f'"{attr.name}"', 'mjXAttr::kEnum', '1', 'true', required,
nodefault, handwrite,
f'(int)offsetof({spec}, {prefix}{field})', 'bool_map', '2']
return tuple(row)
else:
raise ValueError(f'{ctx}.{attr.name}: bool bound to '
f'field {field} ({ctype})')
elif attr.type == 'chars':
if ctype != 'char' or dim is None:
raise ValueError(f'{ctx}.{attr.name}: chars bound to '
f'field {field} ({ctype})')
lo, hi = attr.arity.lo, attr.arity.hi
if str(dim) != str(hi):
raise ValueError(f'{ctx}.{attr.name}: chars arity {hi} vs '
f'field dim {dim}')
kind, length = 'kChars', str(hi)
exact = 'true' if lo == hi else 'false'
elif attr.type in ('double', 'float', 'int') and attr.arity.hi is None:
vec_kinds = {'mjDoubleVec*': 'kDoubleVec', 'mjFloatVec*': 'kFloatVec',
'mjIntVec*': 'kIntVec'}
kind = vec_kinds.get(ctype)
if kind is None:
raise ValueError(f'{ctx}.{attr.name}: unbounded vector '
f'bound to field {field} ({ctype})')
length, exact = '1', 'true'
elif attr.type in ('double', 'float', 'int'):
kind = KIND_BY_CTYPE.get(ctype)
if kind is None or (attr.type == 'int') != (kind == 'kInt'):
raise ValueError(f'{ctx}.{attr.name}: schema type '
f'{attr.type} vs field {field} ({ctype})')
lo, hi = attr.arity.lo, attr.arity.hi
length = str(hi)
declared = dim if dim is not None else '1'
if str(declared) != length:
if DIM_EQUIV.get(str(declared)) == length:
length = str(declared) # emit the expression: it tracks the header
else:
raise ValueError(f'{ctx}.{attr.name}: arity {length} vs '
f'field dim {declared}')
exact = 'true' if lo == hi or (lo == 1 and hi == 1) else 'false'
else:
raise ValueError(f'{ctx}.{attr.name}: kind {attr.type} is '
'not table-drivable yet')
offset = f'(int)offsetof({spec}, {prefix}{field})'
row = [f'"{attr.name}"', f'mjXAttr::{kind}', length, exact,
required, nodefault, handwrite, offset]
if kind in ('kEnum', 'kEnumByte', 'kFlags'):
row += [f'{attr.target}_map', f'{attr.target}_sz']
return tuple(row)
def rows_for(schema, structs, element_name):
"""Return (struct name, list of row tuples) for one table-driven element."""
element = schema.elements[element_name]
if not element.spec:
raise ValueError(f'{element_name}: no bound spec struct')
sub = element.facets.get('field')
key = f'{element.spec}.{sub}' if sub else element.spec
fields = structs.get(key)
if fields is None:
raise ValueError(f'{element_name}: struct {key} not found in headers')
prefix = f'{sub}.' if sub else ''
hand_attrs = set()
for gname in HAND_GROUPS:
if any(isinstance(m, mjcf_schema.Use) and m.group == gname
for m in element.members):
hand_attrs |= {m.name for m in schema.groups[gname].members}
rows = []
for const in element.consts():
entry = fields.get(const.field)
if entry is None:
raise ValueError(f'{element_name}: set {const.field}: no field '
f'{element.spec}.{const.field}')
ctype = entry[0]
if not ctype.startswith('mjt') and ctype != 'int':
raise ValueError(f'{element_name}: set {const.field}: field is {ctype}')
rows.append(('nullptr', 'mjXAttr::kConst', '1', 'true', 'false', 'false',
'false',
f'(int)offsetof({element.spec}, {prefix}{const.field})',
'nullptr', '0', const.value))
for attr in schema.expanded_attrs(element):
if attr.name in hand_attrs or 'reading' in attr.facets:
continue # hand-read in the OneX() remnant
row = _attr_row(element.spec, fields, attr, element_name,
prefix=prefix)
if row is not None:
rows.append(row)
return element.spec, rows
def rows_for_group(schema, structs, group_name, spec):
"""Return list of row tuples for a shared group, bound to the given struct."""
fields = structs.get(spec)
if fields is None:
raise ValueError(f'{group_name}: struct {spec} not in mjspec.h')
rows = []
for attr in schema.groups[group_name].members:
if not isinstance(attr, mjcf_schema.Attr) or 'reading' in attr.facets:
continue
row = _attr_row(spec, fields, attr, group_name)
if row is not None:
rows.append(row)
return rows
def generate():
"""Generate the mjcf_read_table.inc content as a string."""
schema = mjcf_schema.parse_file(SCHEMA_PATH)
structs = parse_spec_structs(SPEC_H_PATH, MODEL_H_PATH)
migrated = table_driven_elements(schema)
out = [_HEADER]
for name in migrated:
struct, rows = rows_for(schema, structs, name)
array = f'k{name.capitalize()}Attrs'
out.append(f'// {name} ({struct})')
out.append(f'inline constexpr mjXAttr {array}[] = {{')
for row in rows:
out.append(' {' + ', '.join(row) + '},')
out.append('};')
out.append(f'inline constexpr int {array}N = '
f'sizeof({array}) / sizeof({array}[0]);')
out.append('')
out.append('// sensors fully described by the schema: dispatch by tag')
out.append('struct mjXSensorEntry { const char* tag; const mjXAttr* rows;'
' int n; };')
out.append('inline constexpr mjXSensorEntry kSensorDispatch[] = {')
for name in SENSOR_DISPATCH:
array = f'k{name.capitalize()}Attrs'
tag = schema.elements[name].xml_name()
out.append(f' {{"{tag}", {array}, {array}N}},')
out.append('};')
out.append('inline constexpr int kSensorDispatchN = '
'sizeof(kSensorDispatch) / sizeof(kSensorDispatch[0]);')
out.append('')
for gname, (struct, array) in EMIT_GROUPS.items():
rows = rows_for_group(schema, structs, gname, struct)
out.append(f'// group {gname} ({struct})')
out.append(f'inline constexpr mjXAttr {array}[] = {{')
for row in rows:
out.append(' {' + ', '.join(row) + '},')
out.append('};')
out.append(f'inline constexpr int {array}N = '
f'sizeof({array}) / sizeof({array}[0]);')
out.append('')
out.append('// clang-format on')
return '\n'.join(out) + '\n'
def main():
"""CLI entry point: generate mjcf_read_table.inc to stdout or a file."""
if len(sys.argv) > 2:
sys.exit('usage: generate_read_table.py [output.inc]')
text = generate()
if len(sys.argv) == 2:
with open(sys.argv[1], 'w', encoding='utf-8') as file:
file.write(text)
else:
sys.stdout.write(text)
return 0
if __name__ == '__main__':
sys.exit(main())
+285
View File
@@ -0,0 +1,285 @@
// Copyright 2026 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// GENERATED FILE, DO NOT EDIT. Generated from src/xml/mjcf.schema by
// doc/generate/generate_default_table.py; test/doc/doc_test.py checks
// freshness.
//
// One row per schema attribute with a declared default, binding the declared
// values to the bound field. SchemaDefaultsTest compares every row against a
// freshly-constructed spec: the schema's defaults must agree with the C
// default-constructors. Rows are {attr, offset, kind, len, ndecl, values};
// values beyond ndecl are expected to be zero; kind: 0=double 1=float 2=int
// 3=byte 4=mjtNum.
// clang-format off
struct mjXDefaultEntry {
const char* attr;
int offset;
int kind;
int len;
int ndecl;
double value[8];
};
struct mjXDefaultTable {
const char* structname;
const mjXDefaultEntry* entries;
int n;
};
static const mjXDefaultEntry kDefaults_mjOption[] = {
{"timestep", (int)offsetof(mjOption, timestep), 4, 1, 1, {0.002}},
{"impratio", (int)offsetof(mjOption, impratio), 4, 1, 1, {1.0}},
{"tolerance", (int)offsetof(mjOption, tolerance), 4, 1, 1, {1e-08}},
{"ls_tolerance", (int)offsetof(mjOption, ls_tolerance), 4, 1, 1, {0.01}},
{"noslip_tolerance", (int)offsetof(mjOption, noslip_tolerance), 4, 1, 1, {1e-06}},
{"ccd_tolerance", (int)offsetof(mjOption, ccd_tolerance), 4, 1, 1, {1e-06}},
{"sleep_tolerance", (int)offsetof(mjOption, sleep_tolerance), 4, 1, 1, {0.001}},
{"gravity", (int)offsetof(mjOption, gravity), 4, 3, 3, {0.0, 0.0, -9.81}},
{"wind", (int)offsetof(mjOption, wind), 4, 3, 3, {0.0, 0.0, 0.0}},
{"magnetic", (int)offsetof(mjOption, magnetic), 4, 3, 3, {0.0, -0.5, 0.0}},
{"o_friction", (int)offsetof(mjOption, o_friction), 4, 5, 5, {1.0, 1.0, 0.005, 0.0001, 0.0001}},
{"integrator", (int)offsetof(mjOption, integrator), 2, 1, 1, {(double)mjINT_EULER}},
{"cone", (int)offsetof(mjOption, cone), 2, 1, 1, {(double)mjCONE_PYRAMIDAL}},
{"jacobian", (int)offsetof(mjOption, jacobian), 2, 1, 1, {(double)mjJAC_AUTO}},
{"solver", (int)offsetof(mjOption, solver), 2, 1, 1, {(double)mjSOL_NEWTON}},
{"iterations", (int)offsetof(mjOption, iterations), 2, 1, 1, {100.0}},
{"ls_iterations", (int)offsetof(mjOption, ls_iterations), 2, 1, 1, {50.0}},
{"ccd_iterations", (int)offsetof(mjOption, ccd_iterations), 2, 1, 1, {35.0}},
{"sdf_iterations", (int)offsetof(mjOption, sdf_iterations), 2, 1, 1, {10.0}},
{"sdf_initpoints", (int)offsetof(mjOption, sdf_initpoints), 2, 1, 1, {40.0}},
};
static const mjXDefaultEntry kDefaults_mjVisual_global[] = {
{"cameraid", (int)offsetof(mjVisual, global.cameraid), 2, 1, 1, {-1.0}},
{"orthographic", (int)offsetof(mjVisual, global.orthographic), 2, 1, 1, {0}},
{"fovy", (int)offsetof(mjVisual, global.fovy), 1, 1, 1, {45.0}},
{"ipd", (int)offsetof(mjVisual, global.ipd), 1, 1, 1, {0.068}},
{"azimuth", (int)offsetof(mjVisual, global.azimuth), 1, 1, 1, {90.0}},
{"elevation", (int)offsetof(mjVisual, global.elevation), 1, 1, 1, {-45.0}},
{"linewidth", (int)offsetof(mjVisual, global.linewidth), 1, 1, 1, {1.0}},
{"glow", (int)offsetof(mjVisual, global.glow), 1, 1, 1, {0.3}},
{"offwidth", (int)offsetof(mjVisual, global.offwidth), 2, 1, 1, {640.0}},
{"offheight", (int)offsetof(mjVisual, global.offheight), 2, 1, 1, {480.0}},
{"realtime", (int)offsetof(mjVisual, global.realtime), 1, 1, 1, {1.0}},
{"ellipsoidinertia", (int)offsetof(mjVisual, global.ellipsoidinertia), 2, 1, 1, {0}},
{"bvactive", (int)offsetof(mjVisual, global.bvactive), 2, 1, 1, {1}},
};
static const mjXDefaultEntry kDefaults_mjVisual_headlight[] = {
{"ambient", (int)offsetof(mjVisual, headlight.ambient), 1, 3, 3, {0.1, 0.1, 0.1}},
{"diffuse", (int)offsetof(mjVisual, headlight.diffuse), 1, 3, 3, {0.4, 0.4, 0.4}},
{"specular", (int)offsetof(mjVisual, headlight.specular), 1, 3, 3, {0.5, 0.5, 0.5}},
{"active", (int)offsetof(mjVisual, headlight.active), 2, 1, 1, {1.0}},
};
static const mjXDefaultEntry kDefaults_mjVisual_map[] = {
{"stiffness", (int)offsetof(mjVisual, map.stiffness), 1, 1, 1, {100.0}},
{"stiffnessrot", (int)offsetof(mjVisual, map.stiffnessrot), 1, 1, 1, {500.0}},
{"force", (int)offsetof(mjVisual, map.force), 1, 1, 1, {0.005}},
{"torque", (int)offsetof(mjVisual, map.torque), 1, 1, 1, {0.1}},
{"alpha", (int)offsetof(mjVisual, map.alpha), 1, 1, 1, {0.3}},
{"fogstart", (int)offsetof(mjVisual, map.fogstart), 1, 1, 1, {3.0}},
{"fogend", (int)offsetof(mjVisual, map.fogend), 1, 1, 1, {10.0}},
{"znear", (int)offsetof(mjVisual, map.znear), 1, 1, 1, {0.01}},
{"zfar", (int)offsetof(mjVisual, map.zfar), 1, 1, 1, {50.0}},
{"haze", (int)offsetof(mjVisual, map.haze), 1, 1, 1, {0.3}},
{"shadowclip", (int)offsetof(mjVisual, map.shadowclip), 1, 1, 1, {1.0}},
{"shadowscale", (int)offsetof(mjVisual, map.shadowscale), 1, 1, 1, {0.6}},
{"actuatortendon", (int)offsetof(mjVisual, map.actuatortendon), 1, 1, 1, {2.0}},
};
static const mjXDefaultEntry kDefaults_mjVisual_quality[] = {
{"shadowsize", (int)offsetof(mjVisual, quality.shadowsize), 2, 1, 1, {4096.0}},
{"offsamples", (int)offsetof(mjVisual, quality.offsamples), 2, 1, 1, {4.0}},
{"numslices", (int)offsetof(mjVisual, quality.numslices), 2, 1, 1, {28.0}},
{"numstacks", (int)offsetof(mjVisual, quality.numstacks), 2, 1, 1, {16.0}},
{"numquads", (int)offsetof(mjVisual, quality.numquads), 2, 1, 1, {4.0}},
};
static const mjXDefaultEntry kDefaults_mjVisual_scale[] = {
{"forcewidth", (int)offsetof(mjVisual, scale.forcewidth), 1, 1, 1, {0.1}},
{"contactwidth", (int)offsetof(mjVisual, scale.contactwidth), 1, 1, 1, {0.3}},
{"contactheight", (int)offsetof(mjVisual, scale.contactheight), 1, 1, 1, {0.1}},
{"connect", (int)offsetof(mjVisual, scale.connect), 1, 1, 1, {0.2}},
{"com", (int)offsetof(mjVisual, scale.com), 1, 1, 1, {0.4}},
{"camera", (int)offsetof(mjVisual, scale.camera), 1, 1, 1, {0.3}},
{"light", (int)offsetof(mjVisual, scale.light), 1, 1, 1, {0.3}},
{"selectpoint", (int)offsetof(mjVisual, scale.selectpoint), 1, 1, 1, {0.2}},
{"jointlength", (int)offsetof(mjVisual, scale.jointlength), 1, 1, 1, {1.0}},
{"jointwidth", (int)offsetof(mjVisual, scale.jointwidth), 1, 1, 1, {0.1}},
{"actuatorlength", (int)offsetof(mjVisual, scale.actuatorlength), 1, 1, 1, {0.7}},
{"actuatorwidth", (int)offsetof(mjVisual, scale.actuatorwidth), 1, 1, 1, {0.2}},
{"framelength", (int)offsetof(mjVisual, scale.framelength), 1, 1, 1, {1.0}},
{"framewidth", (int)offsetof(mjVisual, scale.framewidth), 1, 1, 1, {0.1}},
{"constraint", (int)offsetof(mjVisual, scale.constraint), 1, 1, 1, {0.1}},
{"slidercrank", (int)offsetof(mjVisual, scale.slidercrank), 1, 1, 1, {0.2}},
{"frustum", (int)offsetof(mjVisual, scale.frustum), 1, 1, 1, {10.0}},
};
static const mjXDefaultEntry kDefaults_mjsActuator[] = {
{"gear", (int)offsetof(mjsActuator, gear), 0, 6, 6, {1.0, 0.0, 0.0, 0.0, 0.0, 0.0}},
{"actdim", (int)offsetof(mjsActuator, actdim), 2, 1, 1, {-1.0}},
{"dyntype", (int)offsetof(mjsActuator, dyntype), 2, 1, 1, {(double)mjDYN_NONE}},
{"gaintype", (int)offsetof(mjsActuator, gaintype), 2, 1, 1, {(double)mjGAIN_FIXED}},
{"biastype", (int)offsetof(mjsActuator, biastype), 2, 1, 1, {(double)mjBIAS_NONE}},
{"dynprm", (int)offsetof(mjsActuator, dynprm), 0, mjNDYN, 1, {1.0}},
{"gainprm", (int)offsetof(mjsActuator, gainprm), 0, mjNGAIN, 1, {1.0}},
};
static const mjXDefaultEntry kDefaults_mjsBody[] = {
{"pos", (int)offsetof(mjsBody, pos), 0, 3, 3, {0.0, 0.0, 0.0}},
{"quat", (int)offsetof(mjsBody, quat), 0, 4, 4, {1.0, 0.0, 0.0, 0.0}},
{"simple", (int)offsetof(mjsBody, simple), 3, 1, 1, {(double)1}},
};
static const mjXDefaultEntry kDefaults_mjsCamera[] = {
{"fovy", (int)offsetof(mjsCamera, fovy), 0, 1, 1, {45.0}},
{"ipd", (int)offsetof(mjsCamera, ipd), 0, 1, 1, {0.068}},
{"resolution", (int)offsetof(mjsCamera, resolution), 2, 2, 2, {1.0, 1.0}},
{"pos", (int)offsetof(mjsCamera, pos), 0, 3, 3, {0.0, 0.0, 0.0}},
{"quat", (int)offsetof(mjsCamera, quat), 0, 4, 4, {1.0, 0.0, 0.0, 0.0}},
{"mode", (int)offsetof(mjsCamera, mode), 2, 1, 1, {(double)mjCAMLIGHT_FIXED}},
};
static const mjXDefaultEntry kDefaults_mjsEquality[] = {
{"active", (int)offsetof(mjsEquality, active), 3, 1, 1, {1}},
{"solref", (int)offsetof(mjsEquality, solref), 4, mjNREF, 2, {0.02, 1.0}},
{"solimp", (int)offsetof(mjsEquality, solimp), 4, mjNIMP, 5, {0.9, 0.95, 0.001, 0.5, 2.0}},
};
static const mjXDefaultEntry kDefaults_mjsFrame[] = {
{"quat", (int)offsetof(mjsFrame, quat), 0, 4, 4, {1.0, 0.0, 0.0, 0.0}},
};
static const mjXDefaultEntry kDefaults_mjsGeom[] = {
{"type", (int)offsetof(mjsGeom, type), 2, 1, 1, {(double)mjGEOM_SPHERE}},
{"contype", (int)offsetof(mjsGeom, contype), 2, 1, 1, {1.0}},
{"conaffinity", (int)offsetof(mjsGeom, conaffinity), 2, 1, 1, {1.0}},
{"condim", (int)offsetof(mjsGeom, condim), 2, 1, 1, {3.0}},
{"friction", (int)offsetof(mjsGeom, friction), 0, 3, 3, {1.0, 0.005, 0.0001}},
{"density", (int)offsetof(mjsGeom, density), 0, 1, 1, {1000.0}},
{"solmix", (int)offsetof(mjsGeom, solmix), 0, 1, 1, {1.0}},
{"solref", (int)offsetof(mjsGeom, solref), 4, mjNREF, 2, {0.02, 1.0}},
{"solimp", (int)offsetof(mjsGeom, solimp), 4, mjNIMP, 5, {0.9, 0.95, 0.001, 0.5, 2.0}},
{"pos", (int)offsetof(mjsGeom, pos), 0, 3, 3, {0.0, 0.0, 0.0}},
{"quat", (int)offsetof(mjsGeom, quat), 0, 4, 4, {1.0, 0.0, 0.0, 0.0}},
{"fitscale", (int)offsetof(mjsGeom, fitscale), 0, 1, 1, {1.0}},
{"rgba", (int)offsetof(mjsGeom, rgba), 1, 4, 4, {0.5, 0.5, 0.5, 1.0}},
};
static const mjXDefaultEntry kDefaults_mjsJoint[] = {
{"type", (int)offsetof(mjsJoint, type), 2, 1, 1, {(double)mjJNT_HINGE}},
{"axis", (int)offsetof(mjsJoint, axis), 0, 3, 3, {0.0, 0.0, 1.0}},
};
static const mjXDefaultEntry kDefaults_mjsLight[] = {
{"castshadow", (int)offsetof(mjsLight, castshadow), 3, 1, 1, {1}},
{"active", (int)offsetof(mjsLight, active), 3, 1, 1, {1}},
{"pos", (int)offsetof(mjsLight, pos), 0, 3, 3, {0.0, 0.0, 0.0}},
{"dir", (int)offsetof(mjsLight, dir), 0, 3, 3, {0.0, 0.0, -1.0}},
{"bulbradius", (int)offsetof(mjsLight, bulbradius), 1, 1, 1, {0.02}},
{"range", (int)offsetof(mjsLight, range), 1, 1, 1, {10.0}},
{"attenuation", (int)offsetof(mjsLight, attenuation), 1, 3, 3, {1.0, 0.0, 0.0}},
{"cutoff", (int)offsetof(mjsLight, cutoff), 1, 1, 1, {45.0}},
{"exponent", (int)offsetof(mjsLight, exponent), 1, 1, 1, {10.0}},
{"diffuse", (int)offsetof(mjsLight, diffuse), 1, 3, 3, {0.7, 0.7, 0.7}},
{"specular", (int)offsetof(mjsLight, specular), 1, 3, 3, {0.3, 0.3, 0.3}},
{"mode", (int)offsetof(mjsLight, mode), 2, 1, 1, {(double)mjCAMLIGHT_FIXED}},
};
static const mjXDefaultEntry kDefaults_mjsMaterial[] = {
{"texrepeat", (int)offsetof(mjsMaterial, texrepeat), 1, 2, 2, {1.0, 1.0}},
{"specular", (int)offsetof(mjsMaterial, specular), 1, 1, 1, {0.5}},
{"shininess", (int)offsetof(mjsMaterial, shininess), 1, 1, 1, {0.5}},
{"metallic", (int)offsetof(mjsMaterial, metallic), 1, 1, 1, {-1.0}},
{"roughness", (int)offsetof(mjsMaterial, roughness), 1, 1, 1, {-1.0}},
{"rgba", (int)offsetof(mjsMaterial, rgba), 1, 4, 4, {1.0, 1.0, 1.0, 1.0}},
};
static const mjXDefaultEntry kDefaults_mjsMesh[] = {
{"refquat", (int)offsetof(mjsMesh, refquat), 0, 4, 4, {1.0, 0.0, 0.0, 0.0}},
{"scale", (int)offsetof(mjsMesh, scale), 0, 3, 3, {1.0, 1.0, 1.0}},
{"maxhullvert", (int)offsetof(mjsMesh, maxhullvert), 2, 1, 1, {-1.0}},
{"inertia", (int)offsetof(mjsMesh, inertia), 2, 1, 1, {(double)mjMESH_INERTIA_LEGACY}},
};
static const mjXDefaultEntry kDefaults_mjsPair[] = {
{"condim", (int)offsetof(mjsPair, condim), 2, 1, 1, {3.0}},
{"friction", (int)offsetof(mjsPair, friction), 0, 5, 5, {1.0, 1.0, 0.005, 0.0001, 0.0001}},
{"solref", (int)offsetof(mjsPair, solref), 4, mjNREF, 2, {0.02, 1.0}},
{"solimp", (int)offsetof(mjsPair, solimp), 4, mjNIMP, 5, {0.9, 0.95, 0.001, 0.5, 2.0}},
};
static const mjXDefaultEntry kDefaults_mjsSensor[] = {
{"datatype", (int)offsetof(mjsSensor, datatype), 2, 1, 1, {(double)mjDATATYPE_REAL}},
{"needstage", (int)offsetof(mjsSensor, needstage), 2, 1, 1, {(double)mjSTAGE_ACC}},
};
static const mjXDefaultEntry kDefaults_mjsSite[] = {
{"type", (int)offsetof(mjsSite, type), 2, 1, 1, {(double)mjGEOM_SPHERE}},
{"pos", (int)offsetof(mjsSite, pos), 0, 3, 3, {0.0, 0.0, 0.0}},
{"quat", (int)offsetof(mjsSite, quat), 0, 4, 4, {1.0, 0.0, 0.0, 0.0}},
{"size", (int)offsetof(mjsSite, size), 0, 3, 3, {0.005, 0.005, 0.005}},
{"rgba", (int)offsetof(mjsSite, rgba), 1, 4, 4, {0.5, 0.5, 0.5, 1.0}},
};
static const mjXDefaultEntry kDefaults_mjsSkin[] = {
{"rgba", (int)offsetof(mjsSkin, rgba), 1, 4, 4, {0.5, 0.5, 0.5, 1.0}},
};
static const mjXDefaultEntry kDefaults_mjsTendon[] = {
{"springlength", (int)offsetof(mjsTendon, springlength), 0, 2, 2, {-1.0, -1.0}},
{"width", (int)offsetof(mjsTendon, width), 0, 1, 1, {0.003}},
{"rgba", (int)offsetof(mjsTendon, rgba), 1, 4, 4, {0.5, 0.5, 0.5, 1.0}},
};
static const mjXDefaultEntry kDefaults_mjsTexture[] = {
{"type", (int)offsetof(mjsTexture, type), 2, 1, 1, {(double)mjTEXTURE_CUBE}},
{"colorspace", (int)offsetof(mjsTexture, colorspace), 2, 1, 1, {(double)mjCOLORSPACE_AUTO}},
{"gridsize", (int)offsetof(mjsTexture, gridsize), 2, 2, 2, {1.0, 1.0}},
{"rgb1", (int)offsetof(mjsTexture, rgb1), 0, 3, 3, {0.8, 0.8, 0.8}},
{"rgb2", (int)offsetof(mjsTexture, rgb2), 0, 3, 3, {0.5, 0.5, 0.5}},
{"random", (int)offsetof(mjsTexture, random), 0, 1, 1, {0.01}},
{"nchannel", (int)offsetof(mjsTexture, nchannel), 2, 1, 1, {3.0}},
};
static const mjXDefaultTable kDefaultTables[] = {
{"mjOption", kDefaults_mjOption, (int)(sizeof(kDefaults_mjOption) / sizeof(kDefaults_mjOption[0]))},
{"mjVisual", kDefaults_mjVisual_global, (int)(sizeof(kDefaults_mjVisual_global) / sizeof(kDefaults_mjVisual_global[0]))},
{"mjVisual", kDefaults_mjVisual_headlight, (int)(sizeof(kDefaults_mjVisual_headlight) / sizeof(kDefaults_mjVisual_headlight[0]))},
{"mjVisual", kDefaults_mjVisual_map, (int)(sizeof(kDefaults_mjVisual_map) / sizeof(kDefaults_mjVisual_map[0]))},
{"mjVisual", kDefaults_mjVisual_quality, (int)(sizeof(kDefaults_mjVisual_quality) / sizeof(kDefaults_mjVisual_quality[0]))},
{"mjVisual", kDefaults_mjVisual_scale, (int)(sizeof(kDefaults_mjVisual_scale) / sizeof(kDefaults_mjVisual_scale[0]))},
{"mjsActuator", kDefaults_mjsActuator, (int)(sizeof(kDefaults_mjsActuator) / sizeof(kDefaults_mjsActuator[0]))},
{"mjsBody", kDefaults_mjsBody, (int)(sizeof(kDefaults_mjsBody) / sizeof(kDefaults_mjsBody[0]))},
{"mjsCamera", kDefaults_mjsCamera, (int)(sizeof(kDefaults_mjsCamera) / sizeof(kDefaults_mjsCamera[0]))},
{"mjsEquality", kDefaults_mjsEquality, (int)(sizeof(kDefaults_mjsEquality) / sizeof(kDefaults_mjsEquality[0]))},
{"mjsFrame", kDefaults_mjsFrame, (int)(sizeof(kDefaults_mjsFrame) / sizeof(kDefaults_mjsFrame[0]))},
{"mjsGeom", kDefaults_mjsGeom, (int)(sizeof(kDefaults_mjsGeom) / sizeof(kDefaults_mjsGeom[0]))},
{"mjsJoint", kDefaults_mjsJoint, (int)(sizeof(kDefaults_mjsJoint) / sizeof(kDefaults_mjsJoint[0]))},
{"mjsLight", kDefaults_mjsLight, (int)(sizeof(kDefaults_mjsLight) / sizeof(kDefaults_mjsLight[0]))},
{"mjsMaterial", kDefaults_mjsMaterial, (int)(sizeof(kDefaults_mjsMaterial) / sizeof(kDefaults_mjsMaterial[0]))},
{"mjsMesh", kDefaults_mjsMesh, (int)(sizeof(kDefaults_mjsMesh) / sizeof(kDefaults_mjsMesh[0]))},
{"mjsPair", kDefaults_mjsPair, (int)(sizeof(kDefaults_mjsPair) / sizeof(kDefaults_mjsPair[0]))},
{"mjsSensor", kDefaults_mjsSensor, (int)(sizeof(kDefaults_mjsSensor) / sizeof(kDefaults_mjsSensor[0]))},
{"mjsSite", kDefaults_mjsSite, (int)(sizeof(kDefaults_mjsSite) / sizeof(kDefaults_mjsSite[0]))},
{"mjsSkin", kDefaults_mjsSkin, (int)(sizeof(kDefaults_mjsSkin) / sizeof(kDefaults_mjsSkin[0]))},
{"mjsTendon", kDefaults_mjsTendon, (int)(sizeof(kDefaults_mjsTendon) / sizeof(kDefaults_mjsTendon[0]))},
{"mjsTexture", kDefaults_mjsTexture, (int)(sizeof(kDefaults_mjsTexture) / sizeof(kDefaults_mjsTexture[0]))},
};
static const int kDefaultTablesN = (int)(sizeof(kDefaultTables) / sizeof(kDefaultTables[0]));
// clang-format on
+489
View File
@@ -0,0 +1,489 @@
// Copyright 2026 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// GENERATED FILE, DO NOT EDIT. Generated from src/xml/mjcf.schema by
// doc/generate/generate_mjcf_map.py; test/doc/doc_test.py checks freshness.
//
// Keyword maps, one per schema enum, shared by the reader, the writer and
// the generated tables. Inline variables: including this header is all a
// translation unit needs.
#ifndef MUJOCO_SRC_XML_MJCF_MAP_H_
#define MUJOCO_SRC_XML_MJCF_MAP_H_
#include <mujoco/mjspec.h>
#include <mujoco/mjtype.h>
#include "user/user_composite.h"
#include "user/user_flexcomp.h"
#include "xml/xml_util.h"
// clang-format off
// keywords of the built-in bool type (not a schema enum)
inline constexpr mjMap bool_map[] = {
{"false", 0},
{"true", 1},
};
// enum coordinate
inline constexpr mjMap coordinate_map[] = {
{"local", 0},
{"global", 1},
};
inline constexpr int coordinate_sz = 2;
// enum angle
inline constexpr mjMap angle_map[] = {
{"radian", 0},
{"degree", 1},
};
inline constexpr int angle_sz = 2;
// enum fluidshape
inline constexpr mjMap fluidshape_map[] = {
{"none", 0},
{"ellipsoid", 1},
};
inline constexpr int fluidshape_sz = 2;
// enum enable
inline constexpr mjMap enable_map[] = {
{"disable", 0},
{"enable", 1},
};
inline constexpr int enable_sz = 2;
// enum FalseTrueAuto
inline constexpr mjMap FalseTrueAuto_map[] = {
{"false", 0},
{"true", 1},
{"auto", 2},
};
inline constexpr int FalseTrueAuto_sz = 3;
// enum FalseAuto
inline constexpr mjMap FalseAuto_map[] = {
{"false", 0},
{"auto", 1},
};
inline constexpr int FalseAuto_sz = 2;
// enum bodysleep
inline constexpr mjMap bodysleep_map[] = {
{"auto", mjSLEEP_AUTO},
{"never", mjSLEEP_NEVER},
{"allowed", mjSLEEP_ALLOWED},
{"init", mjSLEEP_INIT},
};
inline constexpr int bodysleep_sz = 4;
// enum jointtype
inline constexpr mjMap jointtype_map[] = {
{"free", mjJNT_FREE},
{"ball", mjJNT_BALL},
{"slide", mjJNT_SLIDE},
{"hinge", mjJNT_HINGE},
};
inline constexpr int jointtype_sz = 4;
// enum geomtype
inline constexpr mjMap geomtype_map[] = {
{"plane", mjGEOM_PLANE},
{"hfield", mjGEOM_HFIELD},
{"sphere", mjGEOM_SPHERE},
{"capsule", mjGEOM_CAPSULE},
{"ellipsoid", mjGEOM_ELLIPSOID},
{"cylinder", mjGEOM_CYLINDER},
{"box", mjGEOM_BOX},
{"mesh", mjGEOM_MESH},
{"sdf", mjGEOM_SDF},
};
inline constexpr int geomtype_sz = 9;
// enum projection
inline constexpr mjMap projection_map[] = {
{"perspective", mjPROJ_PERSPECTIVE},
{"orthographic", mjPROJ_ORTHOGRAPHIC},
};
inline constexpr int projection_sz = 2;
// enum camlight
inline constexpr mjMap camlight_map[] = {
{"fixed", mjCAMLIGHT_FIXED},
{"track", mjCAMLIGHT_TRACK},
{"trackcom", mjCAMLIGHT_TRACKCOM},
{"targetbody", mjCAMLIGHT_TARGETBODY},
{"targetbodycom", mjCAMLIGHT_TARGETBODYCOM},
};
inline constexpr int camlight_sz = 5;
// enum lighttype
inline constexpr mjMap lighttype_map[] = {
{"spot", mjLIGHT_SPOT},
{"directional", mjLIGHT_DIRECTIONAL},
{"point", mjLIGHT_POINT},
{"image", mjLIGHT_IMAGE},
};
inline constexpr int lighttype_sz = 4;
// enum texrole
inline constexpr mjMap texrole_map[] = {
{"rgb", mjTEXROLE_RGB},
{"occlusion", mjTEXROLE_OCCLUSION},
{"roughness", mjTEXROLE_ROUGHNESS},
{"metallic", mjTEXROLE_METALLIC},
{"normal", mjTEXROLE_NORMAL},
{"opacity", mjTEXROLE_OPACITY},
{"emissive", mjTEXROLE_EMISSIVE},
{"rgba", mjTEXROLE_RGBA},
{"orm", mjTEXROLE_ORM},
};
inline constexpr int texrole_sz = 9;
// enum integrator
inline constexpr mjMap integrator_map[] = {
{"Euler", mjINT_EULER},
{"RK4", mjINT_RK4},
{"implicit", mjINT_IMPLICIT},
{"implicitfast", mjINT_IMPLICITFAST},
};
inline constexpr int integrator_sz = 4;
// enum cone
inline constexpr mjMap cone_map[] = {
{"pyramidal", mjCONE_PYRAMIDAL},
{"elliptic", mjCONE_ELLIPTIC},
};
inline constexpr int cone_sz = 2;
// enum jacobian
inline constexpr mjMap jacobian_map[] = {
{"dense", mjJAC_DENSE},
{"sparse", mjJAC_SPARSE},
{"auto", mjJAC_AUTO},
};
inline constexpr int jacobian_sz = 3;
// enum solver
inline constexpr mjMap solver_map[] = {
{"PGS", mjSOL_PGS},
{"CG", mjSOL_CG},
{"Newton", mjSOL_NEWTON},
};
inline constexpr int solver_sz = 3;
// enum equality
inline constexpr mjMap equality_map[] = {
{"connect", mjEQ_CONNECT},
{"weld", mjEQ_WELD},
{"joint", mjEQ_JOINT},
{"tendon", mjEQ_TENDON},
{"flex", mjEQ_FLEX},
{"flexvert", mjEQ_FLEXVERT},
{"flexstrain", mjEQ_FLEXSTRAIN},
{"distance", mjEQ_DISTANCE},
};
inline constexpr int equality_sz = 8;
// enum texture
inline constexpr mjMap texture_map[] = {
{"2d", mjTEXTURE_2D},
{"cube", mjTEXTURE_CUBE},
{"skybox", mjTEXTURE_SKYBOX},
};
inline constexpr int texture_sz = 3;
// enum colorspace
inline constexpr mjMap colorspace_map[] = {
{"auto", mjCOLORSPACE_AUTO},
{"linear", mjCOLORSPACE_LINEAR},
{"sRGB", mjCOLORSPACE_SRGB},
};
inline constexpr int colorspace_sz = 3;
// enum builtin
inline constexpr mjMap builtin_map[] = {
{"none", mjBUILTIN_NONE},
{"gradient", mjBUILTIN_GRADIENT},
{"checker", mjBUILTIN_CHECKER},
{"flat", mjBUILTIN_FLAT},
};
inline constexpr int builtin_sz = 4;
// enum mark
inline constexpr mjMap mark_map[] = {
{"none", mjMARK_NONE},
{"edge", mjMARK_EDGE},
{"cross", mjMARK_CROSS},
{"random", mjMARK_RANDOM},
};
inline constexpr int mark_sz = 4;
// enum dyn
inline constexpr mjMap dyn_map[] = {
{"none", mjDYN_NONE},
{"integrator", mjDYN_INTEGRATOR},
{"filter", mjDYN_FILTER},
{"filterexact", mjDYN_FILTEREXACT},
{"muscle", mjDYN_MUSCLE},
{"dcmotor", mjDYN_DCMOTOR},
{"pid", mjDYN_PID},
{"user", mjDYN_USER},
};
inline constexpr int dyn_sz = 8;
// enum dcmotorinput
inline constexpr mjMap dcmotorinput_map[] = {
{"voltage", 0},
{"position", 1},
{"velocity", 2},
};
inline constexpr int dcmotorinput_sz = 3;
// enum gain
inline constexpr mjMap gain_map[] = {
{"fixed", mjGAIN_FIXED},
{"affine", mjGAIN_AFFINE},
{"muscle", mjGAIN_MUSCLE},
{"dcmotor", mjGAIN_DCMOTOR},
{"so3", mjGAIN_SO3},
{"pid", mjGAIN_PID},
{"user", mjGAIN_USER},
};
inline constexpr int gain_sz = 7;
// enum inputchart
inline constexpr mjMap inputchart_map[] = {
{"expmap", mjCHART_EXPMAP},
{"quat", mjCHART_QUAT},
};
inline constexpr int inputchart_sz = 2;
// enum inputbit
inline constexpr mjMap inputbit_map[] = {
{"pos", mjINPUT_POS},
{"vel", mjINPUT_VEL},
{"ff", mjINPUT_FF},
};
inline constexpr int inputbit_sz = 3;
// enum bias
inline constexpr mjMap bias_map[] = {
{"none", mjBIAS_NONE},
{"affine", mjBIAS_AFFINE},
{"muscle", mjBIAS_MUSCLE},
{"dcmotor", mjBIAS_DCMOTOR},
{"so3", mjBIAS_SO3},
{"user", mjBIAS_USER},
};
inline constexpr int bias_sz = 6;
// enum interp
inline constexpr mjMap interp_map[] = {
{"zoh", 0},
{"linear", 1},
{"cubic", 2},
};
inline constexpr int interp_sz = 3;
// enum stage
inline constexpr mjMap stage_map[] = {
{"none", mjSTAGE_NONE},
{"pos", mjSTAGE_POS},
{"vel", mjSTAGE_VEL},
{"acc", mjSTAGE_ACC},
};
inline constexpr int stage_sz = 4;
// enum datatype
inline constexpr mjMap datatype_map[] = {
{"real", mjDATATYPE_REAL},
{"positive", mjDATATYPE_POSITIVE},
{"axis", mjDATATYPE_AXIS},
{"quaternion", mjDATATYPE_QUATERNION},
};
inline constexpr int datatype_sz = 4;
// enum frameobj
inline constexpr mjMap frameobj_map[] = {
{"body", mjOBJ_BODY},
{"xbody", mjOBJ_XBODY},
{"geom", mjOBJ_GEOM},
{"site", mjOBJ_SITE},
{"camera", mjOBJ_CAMERA},
};
inline constexpr int frameobj_sz = 5;
// enum condata
inline constexpr mjMap condata_map[] = {
{"found", mjCONDATA_FOUND},
{"force", mjCONDATA_FORCE},
{"torque", mjCONDATA_TORQUE},
{"dist", mjCONDATA_DIST},
{"pos", mjCONDATA_POS},
{"normal", mjCONDATA_NORMAL},
{"tangent", mjCONDATA_TANGENT},
};
inline constexpr int condata_sz = 7;
// enum raydata
inline constexpr mjMap raydata_map[] = {
{"dist", mjRAYDATA_DIST},
{"dir", mjRAYDATA_DIR},
{"origin", mjRAYDATA_ORIGIN},
{"point", mjRAYDATA_POINT},
{"normal", mjRAYDATA_NORMAL},
{"depth", mjRAYDATA_DEPTH},
};
inline constexpr int raydata_sz = 6;
// enum camout
inline constexpr mjMap camout_map[] = {
{"rgb", mjCAMOUT_RGB},
{"depth", mjCAMOUT_DEPTH},
{"distance", mjCAMOUT_DIST},
{"normal", mjCAMOUT_NORMAL},
{"segmentation", mjCAMOUT_SEG},
};
inline constexpr int camout_sz = 5;
// enum reduce
inline constexpr mjMap reduce_map[] = {
{"none", 0},
{"mindist", 1},
{"maxforce", 2},
{"netforce", 3},
};
inline constexpr int reduce_sz = 4;
// enum conflict
inline constexpr mjMap conflict_map[] = {
{"warning", mjCONFLICT_WARNING},
{"merge", mjCONFLICT_MERGE},
{"error", mjCONFLICT_ERROR},
};
inline constexpr int conflict_sz = 3;
// enum lrmode
inline constexpr mjMap lrmode_map[] = {
{"none", mjLRMODE_NONE},
{"muscle", mjLRMODE_MUSCLE},
{"muscleuser", mjLRMODE_MUSCLEUSER},
{"all", mjLRMODE_ALL},
};
inline constexpr int lrmode_sz = 4;
// enum comp
inline constexpr mjMap comp_map[] = {
{"particle", mjCOMPTYPE_PARTICLE},
{"grid", mjCOMPTYPE_GRID},
{"rope", mjCOMPTYPE_ROPE},
{"loop", mjCOMPTYPE_LOOP},
{"cable", mjCOMPTYPE_CABLE},
{"cloth", mjCOMPTYPE_CLOTH},
};
inline constexpr int comp_sz = 6;
// enum jkind
inline constexpr mjMap jkind_map[] = {
{"main", mjCOMPKIND_JOINT},
};
inline constexpr int jkind_sz = 1;
// enum shape
inline constexpr mjMap shape_map[] = {
{"s", mjCOMPSHAPE_LINE},
{"cos(s)", mjCOMPSHAPE_COS},
{"sin(s)", mjCOMPSHAPE_SIN},
{"0", mjCOMPSHAPE_ZERO},
};
inline constexpr int shape_sz = 4;
// enum meshinertia
inline constexpr mjMap meshinertia_map[] = {
{"convex", mjMESH_INERTIA_CONVEX},
{"legacy", mjMESH_INERTIA_LEGACY},
{"exact", mjMESH_INERTIA_EXACT},
{"shell", mjMESH_INERTIA_SHELL},
};
inline constexpr int meshinertia_sz = 4;
// enum meshbuiltin
inline constexpr mjMap meshbuiltin_map[] = {
{"none", mjMESH_BUILTIN_NONE},
{"sphere", mjMESH_BUILTIN_SPHERE},
{"hemisphere", mjMESH_BUILTIN_HEMISPHERE},
{"cone", mjMESH_BUILTIN_CONE},
{"supertorus", mjMESH_BUILTIN_SUPERTORUS},
{"supersphere", mjMESH_BUILTIN_SUPERSPHERE},
{"wedge", mjMESH_BUILTIN_WEDGE},
{"plate", mjMESH_BUILTIN_PLATE},
};
inline constexpr int meshbuiltin_sz = 8;
// enum fcomp
inline constexpr mjMap fcomp_map[] = {
{"grid", mjFCOMPTYPE_GRID},
{"box", mjFCOMPTYPE_BOX},
{"cylinder", mjFCOMPTYPE_CYLINDER},
{"ellipsoid", mjFCOMPTYPE_ELLIPSOID},
{"square", mjFCOMPTYPE_SQUARE},
{"disc", mjFCOMPTYPE_DISC},
{"circle", mjFCOMPTYPE_CIRCLE},
{"mesh", mjFCOMPTYPE_MESH},
{"gmsh", mjFCOMPTYPE_GMSH},
{"direct", mjFCOMPTYPE_DIRECT},
};
inline constexpr int fcomp_sz = 10;
// enum fdof
inline constexpr mjMap fdof_map[] = {
{"full", mjFCOMPDOF_FULL},
{"radial", mjFCOMPDOF_RADIAL},
{"trilinear", mjFCOMPDOF_TRILINEAR},
{"quadratic", mjFCOMPDOF_QUADRATIC},
{"2d", mjFCOMPDOF_2D},
};
inline constexpr int fdof_sz = 5;
// enum flexself
inline constexpr mjMap flexself_map[] = {
{"none", mjFLEXSELF_NONE},
{"narrow", mjFLEXSELF_NARROW},
{"bvh", mjFLEXSELF_BVH},
{"sap", mjFLEXSELF_SAP},
{"auto", mjFLEXSELF_AUTO},
};
inline constexpr int flexself_sz = 5;
// enum elastic2d
inline constexpr mjMap elastic2d_map[] = {
{"none", 0},
{"bend", 1},
{"stretch", 2},
{"both", 3},
};
inline constexpr int elastic2d_sz = 4;
// enum flexeq
inline constexpr mjMap flexeq_map[] = {
{"false", 0},
{"true", 1},
{"vert", 2},
{"strain", 3},
};
inline constexpr int flexeq_sz = 4;
// clang-format on
#endif // MUJOCO_SRC_XML_MJCF_MAP_H_
File diff suppressed because it is too large Load Diff
+36 -69
View File
@@ -24,75 +24,8 @@
#include "tinyxml2.h"
// keyword maps (defined in implementation files)
extern const int joint_sz;
extern const int bodysleep_sz;
extern const int projection_sz;
extern const int camlight_sz;
extern const int lighttype_sz;
extern const int integrator_sz;
extern const int collision_sz;
extern const int cone_sz;
extern const int jac_sz;
extern const int solver_sz;
extern const int equality_sz;
extern const int texture_sz;
extern const int colorspace_sz;
extern const int builtin_sz;
extern const int mark_sz;
extern const int dyn_sz;
extern const int inputchart_sz;
extern const int inputbit_sz;
extern const int gain_sz;
extern const int bias_sz;
extern const int interp_sz;
extern const int stage_sz;
extern const int datatype_sz;
extern const int camout_sz;
extern const int reduce_sz;
extern const int conflict_sz;
extern const mjMap angle_map[];
extern const mjMap enable_map[];
extern const mjMap bool_map[];
extern const mjMap fluid_map[];
extern const mjMap TFAuto_map[];
extern const mjMap FAuto_map[];
extern const mjMap joint_map[];
extern const mjMap bodysleep_map[];
extern const mjMap geom_map[];
extern const mjMap projection_map[];
extern const mjMap camlight_map[];
extern const mjMap lighttype_map[];
extern const mjMap integrator_map[];
extern const mjMap collision_map[];
extern const mjMap impedance_map[];
extern const mjMap reference_map[];
extern const mjMap cone_map[];
extern const mjMap jac_map[];
extern const mjMap solver_map[];
extern const mjMap equality_map[];
extern const mjMap texture_map[];
extern const mjMap colorspace_map[];
extern const mjMap texrole_map[];
extern const mjMap builtin_map[];
extern const mjMap mark_map[];
extern const mjMap dyn_map[];
extern const mjMap inputchart_map[];
extern const mjMap inputbit_map[];
extern const mjMap gain_map[];
extern const mjMap bias_map[];
extern const mjMap interp_map[];
extern const mjMap stage_map[];
extern const mjMap datatype_map[];
extern const mjMap condata_map[];
extern const mjMap raydata_map[];
extern const mjMap camout_map[];
extern const mjMap reduce_map[];
extern const mjMap meshtype_map[];
extern const mjMap meshinertia_map[];
extern const mjMap flexself_map[];
extern const mjMap elastic2d_map[];
extern const mjMap conflict_map[];
// keyword maps, one per schema enum, generated into mjcf_map.h
#include "xml/mjcf_map.h" // IWYU pragma: export
//---------------------------------- Base XML class ------------------------------------------------
@@ -119,4 +52,38 @@ class mjXBase : public mjXUtil {
mjSpec* spec; // internally-allocated model
};
// typed attribute row for table-driven reading/writing; per-element row arrays are
// generated from mjcf.schema into mjcf_read_table.inc
struct mjXAttr {
enum Kind {
kName, // element name, set via mjs_setName
kString, // mjString* field, set via mjs_setString
kStringVec, // mjStringVec* field: space-separated names
kInt, // int field
kDouble, // double field, scalar or vector
kNum, // mjtNum field, scalar or vector
kFloat, // float field, scalar or vector
kEnum, // int-sized enum field, keyword mapped through `map`
kFlags, // int field ORing several keywords through `map`
kEnumByte, // mjtByte enum field, keyword mapped through `map`
kBool, // mjtBool field, keywords true/false
kConst, // int-sized field set to `value`: what the tag implies
kDoubleVec, // mjDoubleVec* field, set via mjs_setDouble
kFloatVec, // mjFloatVec* field, set via mjs_setFloat
kIntVec, // mjIntVec* field, set via mjs_setInt
kChars, // char[len] field: text copied in place, length-checked
};
const char* attr; // XML attribute name
Kind kind;
int len; // number of values (1 = scalar); numeric kinds only
bool exact; // exactly len values, else up to len
bool required;
bool nodefault; // not read in default classes
bool handwrite; // custom save policy (writing=custom): OneX() remnant
int offset; // byte offset of the bound field; -1 for kName
const mjMap* map; // keyword map; kEnum only
int mapsz; // keyword map size; kEnum only
int value; // constant the field takes; kConst only
};
#endif // MUJOCO_SRC_XML_XML_BASE_H_
+340 -1584
View File
File diff suppressed because it is too large Load Diff
+11
View File
@@ -66,6 +66,17 @@ class mjXReader : public mjXBase {
void Sensor(tinyxml2::XMLElement* section); // sensor section
void Keyframe(tinyxml2::XMLElement* section); // keyframe section
// table-driven attribute reading: the mechanical attributes of an element,
// driven by its generated mjXAttr rows (see mjcf_read_table.inc). The
// static core handles everything except element names; the member wrapper
// adds kName handling and defaults-context awareness.
void ReadAttrTable(tinyxml2::XMLElement* elem, void* obj, mjsElement* el,
const struct mjXAttr* rows, int nrow);
static void ReadAttrTableCore(tinyxml2::XMLElement* elem, void* obj,
const struct mjXAttr* rows, int nrow,
bool skipnodefault,
const void* authored = nullptr);
// single element parsers, used in defaults and main body
void OneFlex(tinyxml2::XMLElement* elem, mjsFlex* pflex);
void OneMesh(tinyxml2::XMLElement* elem, mjsMesh* pmesh, const mjVFS* vfs);
+14 -14
View File
@@ -392,7 +392,7 @@ void mjXWriter::OneJoint(XMLElement* elem, const mjCJoint* joint, mjCDef* def,
// defaults and regular
if (joint->type != def->Joint().type) {
WriteAttrTxt(elem, "type", FindValue(joint_map, joint_sz, joint->type));
WriteAttrTxt(elem, "type", FindValue(jointtype_map, jointtype_sz, joint->type));
}
WriteAttrInt(elem, "group", joint->group, def->Joint().group);
WriteAttr(elem, "ref", 1, &joint->ref, &zero);
@@ -410,11 +410,11 @@ void mjXWriter::OneJoint(XMLElement* elem, const mjCJoint* joint, mjCDef* def,
WriteAttr(elem, "stiffness", nstiff, joint->stiffness, def->Joint().stiffness);
}
if (joint->type != mjJNT_FREE) {
WriteAttrKey(elem, "limited", TFAuto_map, 3, joint->limited, def->Joint().limited);
WriteAttrKey(elem, "limited", FalseTrueAuto_map, 3, joint->limited, def->Joint().limited);
}
WriteAttr(elem, "range", 2, joint->range, def->Joint().range);
if (joint->type != mjJNT_FREE && joint->type != mjJNT_BALL) {
WriteAttrKey(elem, "actuatorfrclimited", TFAuto_map, 3, joint->actfrclimited,
WriteAttrKey(elem, "actuatorfrclimited", FalseTrueAuto_map, 3, joint->actfrclimited,
def->Joint().actfrclimited);
}
WriteAttrKey(elem, "actuatorgravcomp", bool_map, 2, joint->actgravcomp, def->Joint().actgravcomp);
@@ -485,7 +485,7 @@ void mjXWriter::OneGeom(XMLElement* elem, const mjCGeom* geom, mjCDef* def, stri
}
// defaults and regular
WriteAttrKey(elem, "type", geom_map, mjNGEOMTYPES, geom->type, def->Geom().type);
WriteAttrKey(elem, "type", geomtype_map, mjNGEOMTYPES, geom->type, def->Geom().type);
WriteAttrInt(elem, "contype", geom->contype, def->Geom().contype);
WriteAttrInt(elem, "conaffinity", geom->conaffinity, def->Geom().conaffinity);
WriteAttrInt(elem, "condim", geom->condim, def->Geom().condim);
@@ -500,10 +500,10 @@ void mjXWriter::OneGeom(XMLElement* elem, const mjCGeom* geom, mjCDef* def, stri
WriteAttr(elem, "surfacevel", 6, geom->surfacevel, def->Geom().surfacevel, true);
WriteAttr(elem, "adhesion", 1, &geom->adhesion, &def->Geom().adhesion);
WriteAttrKey(elem, "fluidshape",
fluid_map, 2, geom->fluid_ellipsoid, def->Geom().fluid_ellipsoid);
fluidshape_map, 2, geom->fluid_ellipsoid, def->Geom().fluid_ellipsoid);
WriteAttr(elem, "fluidcoef", 5, geom->fluid_coefs, def->Geom().fluid_coefs);
if (geom->type != mjGEOM_MESH) {
WriteAttrKey(elem, "shellinertia", meshtype_map, 2, geom->typeinertia,
WriteAttrKey(elem, "shellinertia", bool_map, 2, geom->typeinertia,
def->Geom().typeinertia);
}
if (mjuu_defined(geom->mass)) {
@@ -558,7 +558,7 @@ void mjXWriter::OneSite(XMLElement* elem, const mjCSite* site, mjCDef* def, stri
// defaults and regular
WriteAttrInt(elem, "group", site->group, def->Site().group);
WriteAttrKey(elem, "type", geom_map, mjNGEOMTYPES, site->type, def->Site().type);
WriteAttrKey(elem, "type", geomtype_map, mjNGEOMTYPES, site->type, def->Site().type);
if (site->get_material() != def->Site().get_material()) {
WriteAttrTxt(elem, "material", site->get_material());
}
@@ -772,8 +772,8 @@ void mjXWriter::OneTendon(XMLElement* elem, const mjCTendon* tendon, mjCDef* def
true);
WriteAttr(elem, "solimpfriction", mjNIMP, tendon->solimp_friction, def->Tendon().solimp_friction,
true);
WriteAttrKey(elem, "limited", TFAuto_map, 3, tendon->limited, def->Tendon().limited);
WriteAttrKey(elem, "actuatorfrclimited", TFAuto_map, 3, tendon->actfrclimited, def->Tendon().actfrclimited);
WriteAttrKey(elem, "limited", FalseTrueAuto_map, 3, tendon->limited, def->Tendon().limited);
WriteAttrKey(elem, "actuatorfrclimited", FalseTrueAuto_map, 3, tendon->actfrclimited, def->Tendon().actfrclimited);
WriteAttr(elem, "range", 2, tendon->range, def->Tendon().range);
WriteAttr(elem, "actuatorfrcrange", 2, tendon->actfrcrange, def->Tendon().actfrcrange);
WriteAttr(elem, "margin", 1, &tendon->margin, &def->Tendon().margin);
@@ -863,11 +863,11 @@ void mjXWriter::OneActuator(XMLElement* elem, const mjCActuator* actuator, mjCDe
WriteAttrInt(elem, "nsample", actuator->nsample, def->Actuator().nsample);
WriteAttrKey(elem, "interp", interp_map, interp_sz, actuator->interp, def->Actuator().interp);
WriteAttr(elem, "delay", 1, &actuator->delay, &def->Actuator().delay);
WriteAttrKey(elem, "ctrllimited", TFAuto_map, 3, actuator->ctrllimited, def->Actuator().ctrllimited);
WriteAttrKey(elem, "ctrllimited", FalseTrueAuto_map, 3, actuator->ctrllimited, def->Actuator().ctrllimited);
WriteAttr(elem, "ctrlrange", 2, actuator->ctrlrange, def->Actuator().ctrlrange);
WriteAttrKey(elem, "forcelimited", TFAuto_map, 3, actuator->forcelimited, def->Actuator().forcelimited);
WriteAttrKey(elem, "forcelimited", FalseTrueAuto_map, 3, actuator->forcelimited, def->Actuator().forcelimited);
WriteAttr(elem, "forcerange", 2, actuator->forcerange, def->Actuator().forcerange);
WriteAttrKey(elem, "actlimited", TFAuto_map, 3, actuator->actlimited, def->Actuator().actlimited);
WriteAttrKey(elem, "actlimited", FalseTrueAuto_map, 3, actuator->actlimited, def->Actuator().actlimited);
WriteAttr(elem, "actrange", 2, actuator->actrange, def->Actuator().actrange);
WriteAttr(elem, "lengthrange", 2, actuator->lengthrange, def->Actuator().lengthrange);
WriteAttr(elem, "gear", 6, actuator->gear, def->Actuator().gear);
@@ -1086,7 +1086,7 @@ void mjXWriter::Option(XMLElement* root) {
model->option.integrator, opt.integrator);
WriteAttrKey(section, "cone", cone_map, cone_sz,
model->option.cone, opt.cone);
WriteAttrKey(section, "jacobian", jac_map, jac_sz,
WriteAttrKey(section, "jacobian", jacobian_map, jacobian_sz,
model->option.jacobian, opt.jacobian);
WriteAttrKey(section, "solver", solver_map, solver_sz,
model->option.solver, opt.solver);
@@ -1743,7 +1743,7 @@ void mjXWriter::Body(XMLElement* elem, mjCBody* body, mjCFrame* frame, string_vi
}
// simple optimization
WriteAttrKey(elem, "simple", FAuto_map, 2, body->simple, 1);
WriteAttrKey(elem, "simple", FalseAuto_map, 2, body->simple, 1);
// userdata
WriteVector(elem, "user", body->get_userdata());
+83
View File
@@ -23,8 +23,11 @@ _SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
_REPO_ROOT = os.path.dirname(os.path.dirname(_SCRIPT_DIR))
sys.path.insert(0, os.path.join(_REPO_ROOT, 'doc', 'generate'))
import generate_api_header
import generate_default_table
import generate_functions
import generate_mjcf_map
import generate_mjcf_table
import generate_read_table
import generate_schema
import mjcf_schema
@@ -91,6 +94,86 @@ class DocTest(googletest.TestCase):
if source != file.read():
self.fail("The file 'mjcf_table.inc' needs to be updated.")
def test_default_table(self):
"""Checks that mjcf_default_table.inc matches the schema-generated output."""
table_file = os.path.join(_REPO_ROOT, 'src', 'xml',
'mjcf_default_table.inc')
source = generate_default_table.generate()
with open(table_file, 'r', encoding='utf-8') as file:
if source != file.read():
self.fail("The file 'mjcf_default_table.inc' needs to be updated.")
def test_mjcf_map(self):
"""Checks that mjcf_map.h matches the schema-generated output."""
map_file = os.path.join(_REPO_ROOT, 'src', 'xml', 'mjcf_map.h')
source = generate_mjcf_map.generate()
with open(map_file, 'r', encoding='utf-8') as file:
if source != file.read():
self.fail("The file 'mjcf_map.h' needs to be updated.")
def test_read_table(self):
"""Checks that mjcf_read_table.inc matches the schema-generated output."""
table_file = os.path.join(_REPO_ROOT, 'src', 'xml', 'mjcf_read_table.inc')
source = generate_read_table.generate()
with open(table_file, 'r', encoding='utf-8') as file:
if source != file.read():
self.fail("The file 'mjcf_read_table.inc' needs to be updated.")
def test_schema_enum_coverage(self):
"""Checks schema enums against the C enums they bind.
Every schema constant must be a member of the bound C enum, and every C
member must be a schema keyword, a count sentinel (mjN*), or a documented
exemption -- so adding a C enum member without updating the schema fails
here.
"""
# C members deliberately not exposed as XML keywords
exempt = {
'bodysleep': { # resolved states of 'auto', not settable
'mjSLEEP_AUTO_ALLOWED', 'mjSLEEP_AUTO_NEVER'},
'geomtype': { # rendering-only types and the missing-geom sentinel
'mjGEOM_ARROW', 'mjGEOM_ARROW1', 'mjGEOM_ARROW2', 'mjGEOM_LINE',
'mjGEOM_LINEBOX', 'mjGEOM_FLEX', 'mjGEOM_SKIN', 'mjGEOM_LABEL',
'mjGEOM_TRIANGLE', 'mjGEOM_NONE'},
'texrole': { # not settable from XML
'mjTEXROLE_USER'},
}
# deliberately partial: keywords are a documented subset of the C enum
partial = {'frameobj'}
enums_c = {}
for name in ('mjtype.h', 'mjspec.h'):
path = os.path.join(_REPO_ROOT, 'include', 'mujoco', name)
with open(path, 'r', encoding='utf-8') as file:
content = file.read()
for m in re.finditer(r'typedef enum (mjt\w+)\s*\{(.*?)\}\s*\1;',
content, re.S):
enums_c[m.group(1)] = re.findall(r'^\s*(mj[A-Z]\w+)', m.group(2),
re.M)
schema_path = os.path.join(_REPO_ROOT, 'src', 'xml', 'mjcf.schema')
schema = mjcf_schema.parse_file(schema_path)
errors = []
for name, enum in schema.enums.items():
if not enum.ctype:
continue
if enum.ctype not in enums_c:
errors.append(f' {name}: C enum {enum.ctype} not found in headers')
continue
members = set(enums_c[enum.ctype])
constants = {value for _, value in enum.items}
for bad in sorted(constants - members):
errors.append(f' {name}: {bad} is not a member of {enum.ctype}')
if name in partial:
continue
uncovered = {m for m in members - constants
if not re.match(r'mjN[A-Z]', m)} - exempt.get(name, set())
for miss in sorted(uncovered):
errors.append(f' {name}: {enum.ctype} member {miss} has no keyword '
'(add it to the schema or to the exemptions here)')
if errors:
self.fail('schema enum coverage:\n' + '\n'.join(errors))
def test_schema(self):
"""Checks that XMLschema.rst matches the generated output."""
schema_file = os.path.join(_REPO_ROOT, 'doc', 'XMLschema.rst')
+1 -1
View File
@@ -554,7 +554,7 @@ TEST_F(RelativeFrameSensorParsingTest, BadRefType) {
)";
std::array<char, 1024> error;
LoadModelFromString(xml, error.data(), error.size());
EXPECT_THAT(error.data(), HasSubstr("reference frame object must be"));
EXPECT_THAT(error.data(), HasSubstr("invalid keyword: 'light'"));
EXPECT_THAT(error.data(), HasSubstr("line 8"));
}
+2
View File
@@ -19,6 +19,8 @@ mujoco_test(
ADDITIONAL_LINK_LIBRARIES compare_model
)
mujoco_test(schema_defaults_test)
mujoco_test(xml_utils_test)
mujoco_test(
+100
View File
@@ -0,0 +1,100 @@
// Copyright 2026 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Checks that the attribute defaults declared in mjcf.schema agree with the
// C default-constructors: every generated row is compared against a
// freshly-constructed spec element.
#include <map>
#include <string>
#include <gtest/gtest.h>
#include <mujoco/mujoco.h>
#include <mujoco/mjspec.h>
#include "test/fixture.h"
#include "src/xml/mjcf_default_table.inc"
namespace mujoco {
namespace {
using SchemaDefaultsTest = MujocoTest;
TEST_F(SchemaDefaultsTest, DeclaredDefaultsMatchConstructors) {
mjSpec* spec = mj_makeSpec();
mjsBody* world = mjs_findBody(spec, "world");
ASSERT_NE(world, nullptr);
mjsBody* body = mjs_addBody(world, nullptr);
std::map<std::string, const void*> objects = {
{"mjOption", &spec->option},
{"mjVisual", &spec->visual},
{"mjsBody", body},
{"mjsFrame", mjs_addFrame(body, nullptr)},
{"mjsJoint", mjs_addJoint(body, nullptr)},
{"mjsGeom", mjs_addGeom(body, nullptr)},
{"mjsSite", mjs_addSite(body, nullptr)},
{"mjsCamera", mjs_addCamera(body, nullptr)},
{"mjsLight", mjs_addLight(body, nullptr)},
{"mjsPair", mjs_addPair(spec, nullptr)},
{"mjsEquality", mjs_addEquality(spec, nullptr)},
{"mjsTendon", mjs_addTendon(spec, nullptr)},
{"mjsActuator", mjs_addActuator(spec, nullptr)},
{"mjsSensor", mjs_addSensor(spec)},
{"mjsMesh", mjs_addMesh(spec, nullptr)},
{"mjsSkin", mjs_addSkin(spec)},
{"mjsMaterial", mjs_addMaterial(spec, nullptr)},
{"mjsTexture", mjs_addTexture(spec)},
};
for (int t = 0; t < kDefaultTablesN; t++) {
const mjXDefaultTable& table = kDefaultTables[t];
auto it = objects.find(table.structname);
ASSERT_NE(it, objects.end()) << "no factory for " << table.structname;
const char* base = static_cast<const char*>(it->second);
for (int i = 0; i < table.n; i++) {
const mjXDefaultEntry& entry = table.entries[i];
const char* field = base + entry.offset;
for (int j = 0; j < entry.len; j++) {
double expected = j < entry.ndecl ? entry.value[j] : 0;
double actual = 0;
switch (entry.kind) {
case 0: // double
actual = reinterpret_cast<const double*>(field)[j];
break;
case 1: // float: compare at float precision
actual = reinterpret_cast<const float*>(field)[j];
expected = static_cast<float>(expected);
break;
case 2: // int-sized, including enums
actual = reinterpret_cast<const int*>(field)[j];
break;
case 3: // byte
actual = reinterpret_cast<const unsigned char*>(field)[j];
break;
case 4: // mjtNum: compare at mjtNum precision
actual = reinterpret_cast<const mjtNum*>(field)[j];
expected = static_cast<mjtNum>(expected);
break;
}
EXPECT_EQ(actual, expected)
<< table.structname << "." << entry.attr << "[" << j << "]";
}
}
}
mj_deleteSpec(spec);
}
} // namespace
} // namespace mujoco