Source code for scenic.simulators.webots.model

"""Generic Scenic world model for the Webots simulator.

This model provides a general type of object `WebotsObject` corresponding to a
node in the Webots scene tree, as well as a few more specialized objects.

Scenarios using this model cannot be launched directly from the command line
using the :option:`--simulate` option. Instead, Webots should be started first,
with a ``.wbt`` file that includes nodes for all the objects in the scenario
(see the `WebotsObject` documentation for how to specify which objects
correspond to which nodes). A supervisor node can then invoke Scenic to compile
the scenario and run dynamic simulations: see
:doc:`scenic.simulators.webots.simulator` for details.
"""

import math

from scenic.core.distributions import distributionFunction
from scenic.simulators.webots.actions import *

def _errorMsg():
    raise RuntimeError('scenario must be run from inside Webots')
simulator _errorMsg()

[docs]class WebotsObject: """Abstract class for Webots objects. There are two ways to specify which Webots node this object corresponds to (which must already exist in the world loaded into Webots): most simply, you can set the ``webotsName`` property to the DEF name of the Webots node. For convenience when working with many objects of the same type, you can instead set the ``webotsType`` property to a prefix like 'ROCK': the interface will then search for nodes called 'ROCK_0', 'ROCK_1', etc. Also defines the ``elevation`` property as a standard way to access the "up" component of an object's position, since the Scenic built-in property ``position`` is only 2D. If ``elevation`` is set to :obj:`None`, it will be updated to the object's "up" coordinate in Webots when the simulation starts. Properties: elevation (float or None; dynamic): default ``None`` (see above). requireVisible (bool): Default value ``False`` (overriding the default from `Object`). webotsName (str): 'DEF' name of the Webots node to use for this object. webotsType (str): If ``webotsName`` is not set, the first available node with 'DEF' name consisting of this string followed by '_0', '_1', etc. will be used for this object. webotsObject: Is set at runtime to a handle to the Webots node for the object, for use with the `Supervisor API`_. Primarily for internal use. controller (str or None): name of the Webots controller to use for this object, if any (instead of a Scenic behavior). resetController (bool): Whether to restart the controller for each simulation (default ``True``). positionOffset (`Vector`): Offset to add when computing the object's position in Webots; for objects whose Webots ``translation`` field is not aligned with the center of the object. rotationOffset (float): Offset to add when computing the object's rotation in Webots; for objects whose front is not aligned with the Webots North axis. .. _Supervisor API: https://www.cyberbotics.com/doc/reference/supervisor?tab-language=python """ elevation[dynamic]: None requireVisible: False webotsName: None webotsType: None webotsObject: None controller: None resetController: True positionOffset: (0, 0) rotationOffset: 0
[docs]class Ground(WebotsObject): """Special kind of object representing a (possibly irregular) ground surface. Implemented using an `ElevationGrid`_ node in Webots. Attributes: allowCollisions (bool): default value `True` (overriding default from `Object`). webotsName (str): default value 'Ground' .. _ElevationGrid: https://www.cyberbotics.com/doc/reference/elevationgrid """ allowCollisions: True webotsName: 'Ground' positionOffset: (-self.width/2, -self.length/2) # origin of ElevationGrid is at a corner gridSize: 20 gridSizeX: self.gridSize gridSizeY: self.gridSize terrain: () heights: Ground.heightsFromTerrain(self.terrain, self.gridSizeX, self.gridSizeY, self.width, self.length) @staticmethod @distributionFunction def heightsFromTerrain(terrain, gridSizeX, gridSizeY, width, length): for elem in terrain: if not isinstance(elem, Terrain): raise RuntimeError(f'Ground terrain element {elem} is not a Terrain') heights = [] if gridSizeX < 2 or gridSizeY < 2: raise RuntimeError(f'invalid grid size {gridSizeX} x {gridSizeY} for Ground') dx, dy = width / (gridSizeX - 1), length / (gridSizeY - 1) y = -length / 2 for i in range(gridSizeY): row = [] x = -width / 2 for j in range(gridSizeX): height = sum(elem.heightAt(x @ y) for elem in terrain) row.append(height) x += dx heights.append(tuple(row)) y += dy return tuple(heights) def startDynamicSimulation(self): super().startDynamicSimulation() self.setGeometry() def setGeometry(self): # Set basic properties of grid shape = self.webotsObject.getField('children').getMFNode(0) grid = shape.getField('geometry').getSFNode() # ElevationGrid node grid.getField('xDimension').setSFInt32(self.gridSizeX) grid.getField('xSpacing').setSFFloat(self.width / (self.gridSizeX - 1)) # For backwards compatibility with Webots <= 2019b, we check if we have # zDimension and zSpacing fields. If so we set those. If not, we try to set # yDimension and ySpacing. if grid.getField('zDimension') is not None: grid.getField('zDimension').setSFInt32(self.gridSizeY) grid.getField('zSpacing').setSFFloat(self.length / (self.gridSizeY - 1)) else: grid.getField('yDimension').setSFInt32(self.gridSizeY) grid.getField('ySpacing').setSFFloat(self.length / (self.gridSizeY - 1)) # Adjust length of height field as needed # (this will trigger Webots warnings, unfortunately; there seems to be no way to # update the length simultaneously with xDimension, etc.) heightField = grid.getField('height') count = heightField.getCount() size = self.gridSizeX * self.gridSizeY if count > size: for i in range(count - size): heightField.removeMF(-1) elif count < size: for i in range(size - count): heightField.insertMFFloat(-1, 0) # Set height values i = 0 for row in self.heights: for height in row: heightField.setMFFloat(i, height) i += 1
[docs]class Terrain: """Abstract class for objects added together to make a `Ground`. This is not a `WebotsObject` since it doesn't actually correspond to a Webots node. Only the overall `Ground` has a node. """ allowCollisions: True def heightAt(self, pt): offset = pt - self.position return self.heightAtOffset(offset) def heightAtOffset(self, offset): raise NotImplementedError('should be implemented by subclasses')
[docs]class Hill(Terrain): """`Terrain` shaped like a Gaussian. Attributes: height (float): height of the hill (default 1). spread (float): standard deviation as a fraction of the hill's size (default 3). """ height: 1 spread: 0.25 def heightAtOffset(self, offset): dx, dy = offset if not (-self.hw < dx < self.hw and -self.hl < dy < self.hl): return 0 sx, sy = dx / (self.width * self.spread), dy / (self.length * self.spread) nh = math.exp(-((sx * sx) + (sy * sy)) * 0.5) return self.height * nh