Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/reference/statements.rst
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ record [initial | final] *value* [as *name*]
----------------------------------------------
Record the value of an expression during each simulation.
The value can be recorded at the start of the scenario (``initial``), at the end of the scenario (``final``), or at every time step during the scenario (if neither ``initial`` nor ``final`` is specified).
The recorded values are available in the ``records`` dictionary of `SimulationResult`: its keys are the given names of the records (or synthesized names if not provided), and the corresponding values are either the value of the recorded expression or a tuple giving its value at each time step as appropriate.
The recorded values are available in the ``records`` dictionary of `SimulationResult`: its keys are the given names, which may be a string or f-string, of the records (or synthesized names if not provided), and the corresponding values are either the value of the recorded expression or a tuple giving its value at each time step as appropriate.
For debugging, the records can also be printed out using the :option:`--show-records` command-line option.

When recording an entire time series (i.e. not using ``initial`` or ``final``), additional options are available, described below.
Expand Down
9 changes: 7 additions & 2 deletions src/scenic/syntax/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -1398,7 +1398,7 @@ def createRequirementLike(
functionName: str,
body: ast.AST,
lineno: int,
name: Optional[str] = None,
name: Optional[ast.AST | str] = None,
kwargs: Dict[str, Union[Tuple[ast.AST, ...], ast.AST]] = {},
):
"""Create a call to a function that implements requirement-like features, such as `record` and `terminate when`.
Expand All @@ -1422,14 +1422,19 @@ def createRequirementLike(
node = ast.Tuple(*node)
keywords.append(ast.keyword(arg=datum, value=node))

if isinstance(name, ast.AST):
name = self.visit(name)
else:
name = ast.Constant(name)

return ast.Expr(
value=ast.Call(
func=ast.Name(functionName, loadCtx),
args=[
ast.Constant(requirementId), # requirement ID
newBody, # body
ast.Constant(lineno), # line number
ast.Constant(name), # requirement name
(name), # requirement name
],
keywords=keywords,
)
Expand Down
2 changes: 1 addition & 1 deletion src/scenic/syntax/scenic.gram
Original file line number Diff line number Diff line change
Expand Up @@ -2121,8 +2121,8 @@ scenic_require_stmt:
s.Require(cond=e, prob=p, name=n, LOCATIONS)
}
scenic_require_stmt_name:
| a=strings {a}
| a=(NAME | NUMBER) { a.string }
| a=STRING { a.string[1:-1] }

scenic_record_stmt:
| "record" e=expression \
Expand Down
17 changes: 17 additions & 0 deletions tests/syntax/test_dynamics.py
Original file line number Diff line number Diff line change
Expand Up @@ -2250,6 +2250,23 @@ def test_record():
)


def test_record_keys():
scenario = compileScenic(
"""
behavior Foo():
for i in range(3):
self.position = self.position + 2@0
wait
ego = new Object with behavior Foo
i=0
record ego.position as f"ego_position_{i}"
terminate when ego.position.x >= 6
"""
)
result = sampleResult(scenario, maxSteps=4)
assert "ego_position_0" in result.records


## lastActions Property
def test_lastActions():
scenario = compileScenic(
Expand Down
123 changes: 122 additions & 1 deletion tests/syntax/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -1138,7 +1138,7 @@ def test_name_quoted(self):
mod = parse_string_helper("require X as 'requirement name'")
stmt = mod.body[0]
match stmt:
case Require(Name("X"), None, "requirement name"):
case Require(Name("X"), None, ast.Constant("requirement name")):
assert True
case _:
assert False
Expand Down Expand Up @@ -1170,6 +1170,17 @@ def test_require_always_with_name(self):
case _:
assert False

def test_require_always_with_name_fstr(self):
mod = parse_string_helper("require always X as f'safety'")
stmt = mod.body[0]
match stmt:
case Require(
Always(Name("X")), None, ast.JoinedStr(values=[ast.Constant("safety")])
):
assert True
case _:
assert False

def test_require_eventually(self):
mod = parse_string_helper("require eventually X")
stmt = mod.body[0]
Expand All @@ -1188,6 +1199,19 @@ def test_require_eventually_with_name(self):
case _:
assert False

def test_require_eventually_with_name_fstr(self):
mod = parse_string_helper("require eventually X as f'liveness'")
stmt = mod.body[0]
match stmt:
case Require(
Eventually(Name("X")),
None,
ast.JoinedStr(values=[ast.Constant("liveness")]),
):
assert True
case _:
assert False


class TestRecord:
def test_record(self):
Expand All @@ -1208,6 +1232,65 @@ def test_record_named(self):
case _:
assert False

def test_record_named_fstr(self):
mod = parse_string_helper("record x as f'name_{3*2}'")
stmt = mod.body[0]
match stmt:
case Record(
Name("x"),
ast.JoinedStr(
values=[
ast.Constant(value="name_"),
ast.FormattedValue(
value=ast.BinOp(
left=ast.Constant(value=3),
op=ast.Mult(),
right=ast.Constant(value=2),
)
),
]
),
):
assert True
case _:
assert False

def test_record_named_fstr_lambda(self):
mod = parse_string_helper("record x as f'name_{(lambda x: 3)(4)}'")
stmt = mod.body[0]
match stmt:
case Record(
Name("x"),
ast.JoinedStr(
values=[
ast.Constant(value="name_"),
ast.FormattedValue(
value=ast.Call(
func=ast.Lambda(
args=ast.arguments(
args=[ast.arg(arg="x")],
),
body=ast.Constant(value=3),
),
args=[ast.Constant(value=4)],
)
),
]
),
):
assert True
case _:
assert False

def test_record_named_str(self):
mod = parse_string_helper("record x as 'name'")
stmt = mod.body[0]
match stmt:
case Record(Name("x"), ast.Constant("name")):
assert True
case _:
assert False

def test_record_recorder(self):
mod = parse_string_helper("record x to file")
stmt = mod.body[0]
Expand Down Expand Up @@ -1283,6 +1366,26 @@ def test_record_initial_named(self):
case _:
assert False

def test_record_initial_named_str(self):
mod = parse_string_helper("record initial x as 'name'")
stmt = mod.body[0]
match stmt:
case RecordInitial(Name("x"), ast.Constant("name")):
assert True
case _:
assert False

def test_record_intial_named_fstr(self):
mod = parse_string_helper("record initial x as f'name'")
stmt = mod.body[0]
match stmt:
case RecordInitial(
Name("x"), ast.JoinedStr(values=[ast.Constant(value="name")])
):
assert True
case _:
assert False

def test_record_final(self):
mod = parse_string_helper("record final x")
stmt = mod.body[0]
Expand All @@ -1301,6 +1404,24 @@ def test_record_final_named(self):
case _:
assert False

def test_record_final_named_str(self):
mod = parse_string_helper("record final x as 'name'")
stmt = mod.body[0]
match stmt:
case RecordFinal(Name("x"), ast.Constant("name")):
assert True
case _:
assert False

def test_record_final_named_fstr(self):
mod = parse_string_helper("record final x as f'name'")
stmt = mod.body[0]
match stmt:
case RecordFinal(Name("x"), ast.JoinedStr(values=[ast.Constant("name")])):
assert True
case _:
assert False


class TestTerminateWhen:
def test_terminate_when(self):
Expand Down