FazBrowse GitHub Viewer
|
Trending
|
URL:
|
Home
Tools:
[Download Repo ZIP]
[View Raw Code]
[Original HTTPS Page]
dm_control/dm_control/mjcf/debugging.py at main · google-deepmind/dm_control · GitHub
Uh oh!
There was an error while loading.
Please reload this page
.
google-deepmind
/
dm_control
Public
Notifications
You must be signed in to change notification settings
Fork
761
Star
4.7k
Code
Issues
113
Pull requests
15
Actions
Security and quality
0
Insights
Additional navigation options
Code
Issues
Pull requests
Actions
Security and quality
Insights
Expand file tree
Breadcrumbs
dm_control
/
dm_control
/
mjcf
/
debugging.py
Copy path
More file actions
More file actions
Latest commit
History
History
History
366 lines (306 loc) · 13 KB
Breadcrumbs
dm_control
/
dm_control
/
mjcf
/
debugging.py
Copy path
File metadata and controls
366 lines (306 loc) · 13 KB
Raw
Copy raw file
Download raw file
Open symbols panel
Edit and raw actions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
# Copyright 2018 The dm_control Authors.
#
# 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.
# ============================================================================
"""Implements PyMJCF debug mode.
PyMJCF debug mode stores a stack trace each time the MJCF object is modified.
If Mujoco raises a compile error on the generated XML model, we would then be
able to find the original source line that created the offending element.
"""
import
collections
import
contextlib
import
copy
import
os
import
re
import
sys
import
traceback
from
absl
import
flags
from
lxml
import
etree
FLAGS
=
flags
.
FLAGS
flags
.
DEFINE_boolean
(
'pymjcf_debug'
,
False
,
'Enables PyMJCF debug mode (SLOW!). In this mode, a stack trace is logged '
'each the MJCF object is modified. This may be helpful in locating the '
'Python source line corresponding to a problematic element in the '
'generated XML.'
)
flags
.
DEFINE_string
(
'pymjcf_debug_full_dump_dir'
,
''
,
'Path to dump full debug info when Mujoco error is encountered.'
)
StackTraceEntry
=
collections
.
namedtuple
(
'StackTraceEntry'
, (
'filename'
,
'line_number'
,
'function_name'
,
'text'
))
ElementDebugInfo
=
collections
.
namedtuple
(
'ElementDebugInfo'
, (
'element'
,
'init_stack'
,
'attribute_stacks'
))
MODULE_PATH
=
os
.
path
.
dirname
(
sys
.
modules
[
__name__
].
__file__
)
DEBUG_METADATA_PREFIX
=
'pymjcfdebug'
_DEBUG_METADATA_TAG_PREFIX
=
'<!--'
+
DEBUG_METADATA_PREFIX
_DEBUG_METADATA_SEARCH_PATTERN
=
re
.
compile
(
r'<!--{}:(\d+)-->'
.
format
(
DEBUG_METADATA_PREFIX
))
# Modified by `freeze_current_stack_trace`.
_CURRENT_FROZEN_STACK
=
None
# These globals will take their default values from the `--pymjcf_debug` and
# `--pymjcf_debug_full_dump_dir` flags respectively. We cannot use `FLAGS` as
# global variables because flag parsing might not have taken place (e.g. when
# running `nosetests`).
_DEBUG_MODE_ENABLED
=
None
_DEBUG_FULL_DUMP_DIR
=
None
def
debug_mode
():
"""Returns a boolean that indicates whether PyMJCF debug mode is enabled."""
global
_DEBUG_MODE_ENABLED
if
_DEBUG_MODE_ENABLED
is
None
:
if
FLAGS
.
is_parsed
():
_DEBUG_MODE_ENABLED
=
FLAGS
.
pymjcf_debug
else
:
_DEBUG_MODE_ENABLED
=
FLAGS
[
'pymjcf_debug'
].
default
return
_DEBUG_MODE_ENABLED
def
enable_debug_mode
():
"""Enables PyMJCF debug mode."""
global
_DEBUG_MODE_ENABLED
_DEBUG_MODE_ENABLED
=
True
def
disable_debug_mode
():
"""Disables PyMJCF debug mode."""
global
_DEBUG_MODE_ENABLED
_DEBUG_MODE_ENABLED
=
False
def
get_full_dump_dir
():
"""Gets the directory to dump full debug info files."""
global
_DEBUG_FULL_DUMP_DIR
if
_DEBUG_FULL_DUMP_DIR
is
None
:
if
FLAGS
.
is_parsed
():
_DEBUG_FULL_DUMP_DIR
=
FLAGS
.
pymjcf_debug_full_dump_dir
else
:
_DEBUG_FULL_DUMP_DIR
=
FLAGS
[
'pymjcf_debug_full_dump_dir'
].
default
return
_DEBUG_FULL_DUMP_DIR
def
set_full_dump_dir
(
dump_path
):
"""Sets the directory to dump full debug info files."""
global
_DEBUG_FULL_DUMP_DIR
_DEBUG_FULL_DUMP_DIR
=
dump_path
def
get_current_stack_trace
():
"""Returns the stack trace of the current execution frame.
Returns:
A list of `StackTraceEntry` named tuples corresponding to the current stack
trace of the process, truncated to immediately before entry into
PyMJCF internal code.
"""
if
_CURRENT_FROZEN_STACK
:
return
copy
.
deepcopy
(
_CURRENT_FROZEN_STACK
)
else
:
return
_get_actual_current_stack_trace
()
def
_get_actual_current_stack_trace
():
"""Returns the stack trace of the current execution frame.
Returns:
A list of `StackTraceEntry` named tuples corresponding to the current stack
trace of the process, truncated to immediately before entry into
PyMJCF internal code.
"""
raw_stack
=
traceback
.
extract_stack
()
processed_stack
=
[]
for
raw_stack_item
in
raw_stack
:
stack_item
=
StackTraceEntry
(
*
raw_stack_item
)
if
(
stack_item
.
filename
.
startswith
(
MODULE_PATH
)
and
not
stack_item
.
filename
.
endswith
(
'_test.py'
)):
break
else
:
processed_stack
.
append
(
stack_item
)
return
processed_stack
@
contextlib
.
contextmanager
def
freeze_current_stack_trace
():
"""A context manager that freezes the stack trace.
AVOID USING THIS CONTEXT MANAGER OUTSIDE OF INTERNAL PYMJCF IMPLEMENTATION,
AS IT REDUCES THE USEFULNESS OF DEBUG MODE.
If PyMJCF debug mode is enabled, calls to `debugging.get_current_stack_trace`
within this context will always return the stack trace from when this context
was entered.
The frozen stack is global to this debugging module. That is, if the context
is entered while another one is still active, then the stack trace of the
outermost one is returned.
This context significantly speeds up bulk operations in debug mode, e.g.
parsing an existing XML string or creating a deeply-nested element, as it
prevents the same stack trace from being repeatedly constructed.
Yields:
`None`
"""
global
_CURRENT_FROZEN_STACK
if
debug_mode
()
and
_CURRENT_FROZEN_STACK
is
None
:
_CURRENT_FROZEN_STACK
=
_get_actual_current_stack_trace
()
yield
_CURRENT_FROZEN_STACK
=
None
else
:
yield
class
DebugContext
:
"""A helper object to store debug information for a generated XML string.
This class is intended for internal use within the PyMJCF implementation.
"""
def
__init__
(
self
):
self
.
_xml_string
=
None
self
.
_debug_info_for_element_ids
=
{}
def
register_element_for_debugging
(
self
,
elem
):
"""Registers an `Element` and returns debugging metadata for the XML.
Args:
elem: An `mjcf.Element`.
Returns:
An `lxml.etree.Comment` that represents debugging metadata in the
generated XML.
"""
if
not
debug_mode
():
return
None
else
:
self
.
_debug_info_for_element_ids
[
id
(
elem
)]
=
ElementDebugInfo
(
elem
,
copy
.
deepcopy
(
elem
.
get_init_stack
()),
copy
.
deepcopy
(
elem
.
get_last_modified_stacks_for_all_attributes
()))
return
etree
.
Comment
(
'{}:{}'
.
format
(
DEBUG_METADATA_PREFIX
,
id
(
elem
)))
def
commit_xml_string
(
self
,
xml_string
):
"""Commits the XML string associated with this debug context.
This function also formats the XML string to make sure that the debugging
metadata appears on the same line as the corresponding XML element.
Args:
xml_string: A pretty-printed XML string.
Returns:
A reformatted XML string where all debugging metadata appears on the same
line as the corresponding XML element.
"""
formatted
=
re
.
sub
(
r'\n\s*'
+
_DEBUG_METADATA_TAG_PREFIX
,
_DEBUG_METADATA_TAG_PREFIX
,
xml_string
)
self
.
_xml_string
=
formatted
return
formatted
def
process_and_raise_last_exception
(
self
):
"""Processes and re-raises the last ValueError caught.
This function will insert the relevant line from the source XML to the error
message. If debug mode is enabled, additional debugging information is
appended to the error message. If debug mode is not enabled, the error
message instructs the user to enable it by rerunning the executable with an
appropriate flag.
"""
err_type
,
err
,
tb
=
sys
.
exc_info
()
line_number_match
=
re
.
search
(
r'[Ll][Ii][Nn][Ee]\s*[:=]?\s*(\d+)'
,
str
(
err
))
if
line_number_match
:
xml_line_number
=
int
(
line_number_match
.
group
(
1
))
xml_line
=
self
.
_xml_string
.
split
(
'
\n
'
)[
xml_line_number
-
1
]
stripped_xml_line
=
xml_line
.
strip
()
comment_match
=
re
.
search
(
_DEBUG_METADATA_TAG_PREFIX
,
stripped_xml_line
)
if
comment_match
:
stripped_xml_line
=
stripped_xml_line
[:
comment_match
.
start
()]
else
:
xml_line
=
''
stripped_xml_line
=
''
message_lines
=
[]
if
debug_mode
():
if
get_full_dump_dir
():
self
.
dump_full_debug_info_to_disk
()
message_lines
.
extend
([
'Compile error raised by Mujoco.'
,
str
(
err
)])
if
xml_line
:
message_lines
.
extend
([
stripped_xml_line
,
self
.
_generate_debug_message_from_xml_line
(
xml_line
)])
else
:
message_lines
.
extend
([
'Compile error raised by Mujoco; '
+
'run again with --pymjcf_debug for additional debug information.'
,
str
(
err
)
])
if
xml_line
:
message_lines
.
append
(
stripped_xml_line
)
raise
err_type
(
'
\n
'
.
join
(
message_lines
)).
with_traceback
(
tb
)
@
property
def
default_dump_dir
(
self
):
return
get_full_dump_dir
()
@
property
def
debug_mode
(
self
):
return
debug_mode
()
def
dump_full_debug_info_to_disk
(
self
,
dump_dir
=
None
):
"""Dumps full debug information to disk.
Full debug information consists of an XML file whose elements are tagged
with a unique ID, and a stack trace file for each element ID. Each stack
trace file consists of a stack trace for when the element was created, and
when each attribute was last modified.
Args:
dump_dir: Full path to the directory in which dump files are created.
Raises:
ValueError: If neither `dump_dir` nor the global dump path is given. The
global dump path can be specified either via the
--pymjcf_debug_full_dump_dir flag or via `debugging.set_full_dump_dir`.
"""
dump_dir
=
dump_dir
or
self
.
default_dump_dir
if
not
dump_dir
:
raise
ValueError
(
'`dump_dir` is not specified'
)
section_separator
=
'
\n
'
+
(
'='
*
80
)
+
'
\n
'
def
dump_stack
(
header
,
stack
,
f
):
indent
=
' '
f
.
write
(
header
+
'
\n
'
)
for
stack_entry
in
stack
:
f
.
write
(
indent
+
'`{}` at {}:{}
\n
'
.
format
(
stack_entry
.
function_name
,
stack_entry
.
filename
,
stack_entry
.
line_number
))
f
.
write
((
indent
*
2
)
+
str
(
stack_entry
.
text
)
+
'
\n
'
)
f
.
write
(
section_separator
)
with
open
(
os
.
path
.
join
(
dump_dir
,
'model.xml'
),
'w'
)
as
f
:
f
.
write
(
self
.
_xml_string
)
for
elem_id
,
debug_info
in
self
.
_debug_info_for_element_ids
.
items
():
with
open
(
os
.
path
.
join
(
dump_dir
,
str
(
elem_id
)
+
'.dump'
),
'w'
)
as
f
:
f
.
write
(
'{}:{}
\n
'
.
format
(
DEBUG_METADATA_PREFIX
,
elem_id
))
f
.
write
(
str
(
debug_info
.
element
)
+
'
\n
'
)
dump_stack
(
'Element creation'
,
debug_info
.
init_stack
,
f
)
for
attrib_name
,
stack
in
debug_info
.
attribute_stacks
.
items
():
attrib_value
=
(
debug_info
.
element
.
get_attribute_xml_string
(
attrib_name
))
if
stack
[
-
1
]
==
debug_info
.
init_stack
[
-
1
]:
if
attrib_value
is
not
None
:
f
.
write
(
'Attribute {}="{}"
\n
'
.
format
(
attrib_name
,
attrib_value
))
f
.
write
(
' was set when the element was created
\n
'
)
f
.
write
(
section_separator
)
else
:
if
attrib_value
is
not
None
:
dump_stack
(
'Attribute {}="{}"'
.
format
(
attrib_name
,
attrib_value
),
stack
,
f
)
else
:
dump_stack
(
'Attribute {} was CLEARED'
.
format
(
attrib_name
),
stack
,
f
)
def
_generate_debug_message_from_xml_line
(
self
,
xml_line
):
"""Generates a debug message by parsing the metadata on an XML line."""
metadata_match
=
_DEBUG_METADATA_SEARCH_PATTERN
.
search
(
xml_line
)
if
metadata_match
:
elem_id
=
int
(
metadata_match
.
group
(
1
))
return
self
.
_generate_debug_message_from_element_id
(
elem_id
)
else
:
return
''
def
_generate_debug_message_from_element_id
(
self
,
elem_id
):
"""Generates a debug message for the specified Element."""
out
=
[]
debug_info
=
self
.
_debug_info_for_element_ids
[
elem_id
]
out
.
append
(
'Debug summary for element:'
)
if
not
get_full_dump_dir
():
out
.
append
(
' * Full debug info can be dumped to disk by setting the '
'flag --pymjcf_debug_full_dump_dir=path/to/dump>'
)
out
.
append
(
' * Element object was created by `{}` at {}:{}'
.
format
(
debug_info
.
init_stack
[
-
1
].
function_name
,
debug_info
.
init_stack
[
-
1
].
filename
,
debug_info
.
init_stack
[
-
1
].
line_number
))
for
attrib_name
,
stack
in
debug_info
.
attribute_stacks
.
items
():
attrib_value
=
debug_info
.
element
.
get_attribute_xml_string
(
attrib_name
)
if
stack
[
-
1
]
==
debug_info
.
init_stack
[
-
1
]:
if
attrib_value
is
not
None
:
out
.
append
(
' * {}="{}" was set when the element was created'
.
format
(
attrib_name
,
attrib_value
))
else
:
if
attrib_value
is
not
None
:
out
.
append
(
' * {}="{}" was set by `{}` at `{}:{}`'
.
format
(
attrib_name
,
attrib_value
,
stack
[
-
1
].
function_name
,
stack
[
-
1
].
filename
,
stack
[
-
1
].
line_number
))
else
:
out
.
append
(
' * {} was CLEARED by `{}` at {}:{}'
.
format
(
attrib_name
,
stack
[
-
1
].
function_name
,
stack
[
-
1
].
filename
,
stack
[
-
1
].
line_number
))
return
'
\n
'
.
join
(
out
)
Back
|
FazBrowse Home
|
New Git URL