From dc3be6d884cd6a97e1fc51c9e9f3abfe7b530772 Mon Sep 17 00:00:00 2001 From: Lucy Horowitz <111657652+lucyhorowitz@users.noreply.github.com> Date: Fri, 10 Jul 2026 09:07:52 -0700 Subject: [PATCH 1/4] Minimal needed for airsim --- examples/airsim/patrol.scenic | 9 + src/scenic/simulators/airsim/__init__.py | 10 + src/scenic/simulators/airsim/actions.py | 28 ++ src/scenic/simulators/airsim/behaviors.scenic | 116 +++++++ src/scenic/simulators/airsim/model.scenic | 172 ++++++++++ src/scenic/simulators/airsim/simulator.py | 307 ++++++++++++++++++ src/scenic/simulators/airsim/utils.py | 98 ++++++ 7 files changed, 740 insertions(+) create mode 100644 examples/airsim/patrol.scenic create mode 100644 src/scenic/simulators/airsim/__init__.py create mode 100644 src/scenic/simulators/airsim/actions.py create mode 100644 src/scenic/simulators/airsim/behaviors.scenic create mode 100644 src/scenic/simulators/airsim/model.scenic create mode 100644 src/scenic/simulators/airsim/simulator.py create mode 100644 src/scenic/simulators/airsim/utils.py diff --git a/examples/airsim/patrol.scenic b/examples/airsim/patrol.scenic new file mode 100644 index 000000000..8bc52342d --- /dev/null +++ b/examples/airsim/patrol.scenic @@ -0,0 +1,9 @@ +# NOTE: add your world info path here +# param worldInfoPath = "[YOUR PATH HERE]" +param worldOffset = Vector(0,0,50) # blocks world offset + +model scenic.simulators.airsim.model + + +drone1 = new Drone at (0,0,10), + with behavior Patrol([(-10,20,10),(10,40,10),(-10,40,10),(-10,20,10),(10,20,10),(10,40,10),(-1,40,10)],True) \ No newline at end of file diff --git a/src/scenic/simulators/airsim/__init__.py b/src/scenic/simulators/airsim/__init__.py new file mode 100644 index 000000000..fd2acc4dc --- /dev/null +++ b/src/scenic/simulators/airsim/__init__.py @@ -0,0 +1,10 @@ +# Only import Airsim simulator if the airsim package is installed; otherwise the +# import would raise an exception. +airsim = None +try: + import airsim +except ImportError: + pass +if airsim: + from .simulator import AirSimSimulator +del airsim diff --git a/src/scenic/simulators/airsim/actions.py b/src/scenic/simulators/airsim/actions.py new file mode 100644 index 000000000..144eca555 --- /dev/null +++ b/src/scenic/simulators/airsim/actions.py @@ -0,0 +1,28 @@ +from scenic.core.simulators import Action +from scenic.core.type_support import toVector + +from .utils import ( + VectorToAirsimVec, + airsimToScenicLocation, + airsimToScenicOrientation, + scenicToAirsimLocation, + scenicToAirsimOrientation, + scenicToAirsimScale, +) + + +class SetVelocity(Action): + def __init__(self, velocity): + print("vel=", velocity) + self.newVelocity = airsimToScenicLocation(VectorToAirsimVec(velocity)) + + def applyTo(self, obj, sim): + client = sim.client + client.cancelLastTask(vehicle_name=obj.realObjName) + client.moveByVelocityAsync( + self.newVelocity.x, + self.newVelocity.y, + self.newVelocity.z, + duration=5, + vehicle_name=obj.realObjName, + ) diff --git a/src/scenic/simulators/airsim/behaviors.scenic b/src/scenic/simulators/airsim/behaviors.scenic new file mode 100644 index 000000000..19d125115 --- /dev/null +++ b/src/scenic/simulators/airsim/behaviors.scenic @@ -0,0 +1,116 @@ + + +import airsim +import time +import threading +import sys +import math +from promise import Promise +from scenic.core.type_support import toVector +from .utils import ( + airsimToScenicLocation, + scenicToAirsimOrientation, + airsimToScenicLocation, + airsimToScenicOrientation, + scenicToAirsimScale, +) +from scenic.simulators.airsim.actions import * + + +def magnitude(v): + return math.hypot(v.x, v.y, v.z) + +# creates a promise from an msgpackrpc future (the futures that are used in airsim) +def createPromise(future): + def promFunc(resolve, reject): + + def joinAsync(): + future.join() + resolve(True) + + def waitAsync(): + while not future._set_flag: + time.sleep(.01) + resolve(True) + + if not future._loop: + threading.Thread(target=joinAsync).start() + else: + threading.Thread(target=waitAsync).start() + + prom = Promise(promFunc) + + return prom + +# waits for a promise to be fulfilled +behavior waitForPromise(promise): + while not promise.is_fulfilled: + wait + +# Flies the drone to a position +behavior FlyToPosition(newPos, speed = 5,tolerance = 1,pidMode = True): + # pidMode is true if we want the drone to slow down as it reaches its destination + + client = simulation().client + + + + if pidMode: + newPos = scenicToAirsimLocation(toVector(newPos)) + do waitForPromise(createPromise( + client.moveToPositionAsync( + newPos.x_val, + newPos.y_val, + newPos.z_val, + velocity=speed, + vehicle_name=self.realObjName, + ) + )) + else: + while True: + direction = newPos -self.position + distance = magnitude(direction) + + if distance < tolerance: + break + direction= (direction/distance)*speed + take SetVelocity(direction) + wait + + + return + + + + +behavior Patrol(positions, loop=True, smooth = False, speed = 5,tolerance = 2): + while True: + for pos in positions: + do FlyToPosition(pos,speed=speed,pidMode= not smooth,tolerance=tolerance) + + if not loop: + return + + + + +behavior MoveByVelocity(velocity,seconds): + client = simulation().client + + newVelocity = scenicToAirsimLocation(toVector(velocity)) + + do waitForPromise(createPromise( + client.moveByVelocityAsync( + newVelocity.x_val, + newVelocity.y_val, + newVelocity.z_val, + duration=seconds, + vehicle_name=self.realObjName, + ) + )) + + + + +behavior FlyToStart(): + do FlyToPosition(self._startPos) diff --git a/src/scenic/simulators/airsim/model.scenic b/src/scenic/simulators/airsim/model.scenic new file mode 100644 index 000000000..fbf8d1bbf --- /dev/null +++ b/src/scenic/simulators/airsim/model.scenic @@ -0,0 +1,172 @@ +import trimesh +import json +import os +import numpy as np +import scipy + +from scenic.simulators.airsim.simulator import AirSimSimulator +from scenic.simulators.airsim.actions import * +from scenic.simulators.airsim.behaviors import * +from scenic.simulators.airsim.utils import _addPrexistingObj, getPrexistingObj +from scenic.core.simulators import SimulationCreationError +from scenic.core.distributions import distributionFunction + +import scenic.simulators.airsim.utils as airsimUtils +from scenic.simulators.airsim.utils import ( + airsimToScenicLocation, + airsimToScenicOrientation, + scenicToAirsimOrientation, + scenicToAirsimScale, + scenicToAirsimLocation +) + + + +# ---------- global parameters ---------- + +# setting Default global parameters for any missing parameters +param timestep = 1 +param airsimWorldInfoPth = None +param idleStoragePos = (1000,1000,1000) +param worldInfoPath = "" +param worldOffset = Vector(0,0,0) +worldInfoPath = globalParameters.worldInfoPath +worldOffset = globalParameters.worldOffset + +airsimUtils.worldOffset = globalParameters.worldOffset + +# ---------- helper functions ---------- + + +@distributionFunction +def createMeshShape(subFolder, assetName): + objFile = assetName+".obj" + + tmesh = trimesh.load(worldInfoPath+subFolder+"/"+objFile) + + + center = (tmesh.bounds[0] + tmesh.bounds[1]) / 2 + + + rotation_matrix = trimesh.transformations.rotation_matrix(np.pi / 2, [1, 0, 0]) + tmesh.apply_transform(rotation_matrix) + + + center = (tmesh.bounds[0] + tmesh.bounds[1]) / 2 + center *= 100 #extra multiplication to fix center after coords get divided by 100 in scenic + + dimensions = Vector(tmesh.bounding_box.extents[0],tmesh.bounding_box.extents[1],tmesh.bounding_box.extents[2]) + + return MeshShape(tmesh), center, dimensions + + + + +# ---------- simulator creation ---------- +simulator AirSimSimulator(timestep=globalParameters.timestep,idleStoragePos=globalParameters.idleStoragePos) + + +# ---------- simulator getter funcs ---------- + +def getAssetNames(): + return self.client.simListAssets() + +# ---------- base classes ---------- +class AirSimPreExisting: + name: None + shape: None + allowCollisions: True + blueprint: "AirSimPreExisting" + + + +class AirSimActor: + name: None + realObjName: None + assetName: None + blueprint: None + + + # override + _shapeData: createMeshShape("assets",self.assetName) + shape: self._shapeData[0] + centerOffset: self._shapeData[1] #offset in unreal coords + dims: self._shapeData[2] #save original dims before scenic scales mesh + + def __str__(self): + return self.assetName + + +# ---------- inherited classes ---------- +class Drone(AirSimActor): + blueprint: "Drone" + assetName: "Quadrotor1" + startHovering: True + _startPos: None + +class PX4Drone(Drone): + blueprint: "PX4Drone" + startHovering: True + _startPos: None + + +class StaticObj(AirSimActor): + blueprint: "StaticObj" + physEnabled: False + materialName: None + + + +# ---------- body ---------- + + + +# ensure worldInfoPath is set +if not worldInfoPath: + raise SimulationCreationError('\nworldInfoPath not set, use:\n param worldInfoPath = "[YOUR PATH HERE]" \n\n more information on creating and using worldInfo in docs') +else: + worldInfoPath += "/" + + +# Create prexisiting airsim objs +prexisitingObjs = {} +with open( + worldInfoPath+"worldInfo.json", + "r", +) as inFile: + meshDatas = json.load(inFile) + for meshData in meshDatas: + + actorName = meshData["name"] + meshShape,centerOffset,dims = createMeshShape("objectMeshes",actorName) + + # convert unreal position to airsim position + position = Vector(meshData["position"][0],meshData["position"][1],meshData["position"][2]) #unreal position + + # get orientation + rot = meshData["orientation"] + pitch,roll,yaw = rot[0] , rot[1],rot[2] + angles = (-yaw - 90, pitch,roll) + r = scipy.spatial.transform.Rotation.from_euler( + seq="ZXY", angles=angles, degrees=True + ) + orientation = Orientation(r) + + scenicLoc = airsimToScenicLocation(airsim.Vector3r(position.x, position.y, position.z),centerOffset,fromPose=False) + newObj = new AirSimPreExisting with shape meshShape, with name actorName, + at scenicLoc, + facing orientation # pitch, roll, yaw + + + _addPrexistingObj(newObj) + + +# generate list of assets +assets = [] +for filename in os.listdir(worldInfoPath+"/assets"): + if filename.endswith(".obj"): + # append the obj name without the .obj extension + assets.append(filename[:-4]) + + +param assets = assets \ No newline at end of file diff --git a/src/scenic/simulators/airsim/simulator.py b/src/scenic/simulators/airsim/simulator.py new file mode 100644 index 000000000..7655e8983 --- /dev/null +++ b/src/scenic/simulators/airsim/simulator.py @@ -0,0 +1,307 @@ +# standard libs +import asyncio +from cmath import atan, pi, tan +import math +from math import copysign, degrees, radians, sin +import subprocess +import threading +import time + +# third party libs +import airsim +import numpy as np +import scipy + +# scenic libs +from scenic.core.simulators import ( + Simulation, + SimulationCreationError, + Simulator, + SimulatorInterfaceWarning, +) +from scenic.core.type_support import toVector +from scenic.core.vectors import Orientation, Vector +from scenic.syntax.veneer import verbosePrint + +from .utils import ( + airsimToScenicLocation, + airsimToScenicOrientation, + scenicToAirsimLocation, + scenicToAirsimOrientation, + scenicToAirsimScale, +) + +# Constants +PX4_DRONE = "PX4Drone" + + +class AirSimSimulator(Simulator): + def __init__(self, timestep, idleStoragePos=(0, 0, 0)): + # start airsim client + try: + client = airsim.MultirotorClient() + client.confirmConnection() + client.simPause(True) + except Exception: + raise RuntimeError("Airsim must be running on before executing scenic") + + # init properties + self.client = client + self.idleStoragePos = idleStoragePos + self.timestep = timestep + + verbosePrint( + "\n\nAll Asset Names:\n", + [name for name in self.client.simListAssets()], + level=2, + ) + # call super + super().__init__() + + def createSimulation(self, scene, **kwargs): + return AirSimSimulation(self, scene, self.client, **kwargs) + + def destroy(self): + super().destroy() + + +class AirSimSimulation(Simulation): + # ------------------- Required Methods ------------------- + + def __init__(self, simulator, scene, client, **kwargs): + # init properties + self.simulator = simulator + self.client = client + self.joinables = [] # used in waitforjoinables + self.objTrove = [] # objs to delete on simulation complete + self.objs = {} # obj name to objrealname dict + self.drones = {} # obj name to objrealname dict only for drones + self.PX4Drone = None + self.startDrones = None + self.nextDroneIndex = 0 + + super().__init__(scene, **kwargs) + + def setup(self): + # set up startDrones + self.startDrones = self.client.listVehicles() + + # remove px4 drone from start drones bc it is handled differently + if PX4_DRONE in self.startDrones: + self.startDrones.remove(PX4_DRONE) + + # move all drones (except px4 drone) to offscreen position + self.client.simPause(False) + for i, drone in enumerate(self.startDrones): + newPose = airsim.Pose( + position_val=scenicToAirsimLocation( + toVector(self.simulator.idleStoragePos), Vector(0, 0, 0) + ) + + airsim.Vector3r(i, 0, 0) + ) + self.client.enableApiControl(True, drone) + self.client.simSetVehiclePose( + vehicle_name=drone, pose=newPose, ignore_collision=False + ) + self.client.moveByVelocityAsync( + 0, 0, 0, 1, vehicle_name=drone + ) # make drone hover + + self.client.simPause(True) + + # create objs + super().setup() + + # ensure that drones are in the correct places + self.client.simPause(False) + time.sleep(1) + self.client.simPause(True) + + def createObjectInSimulator(self, obj): + # create AirSimPreExisting + if obj.blueprint == "AirSimPreExisting": + self.objs[obj.name] = obj.name + return + + # set obj name if no name specified + if not obj.name: + obj.name = str(hash(obj)) + + # ensure obj isn't already in world + if obj.name in self.objs: + raise RuntimeError( + "there is already an object of the name " + obj.name + " in the simulator" + ) + + # set default realObjName + realObjName = obj.name + str(hash(obj)) + + # set object airsim pose + pose = airsim.Pose( + position_val=scenicToAirsimLocation(obj.position, obj.centerOffset), + orientation_val=scenicToAirsimOrientation(obj.orientation), + ) + + # create obj in airsim + if obj.blueprint == "Drone": + realObjName = "Drone" + str(self.nextDroneIndex) + obj._startPos = obj.position + + # if there is an avalible drone, take it, else create one + if self.nextDroneIndex < len(self.startDrones): + realObjName = self.startDrones[self.nextDroneIndex] + obj.realObjName = realObjName + else: + self.client.simAddVehicle( + vehicle_name=realObjName, vehicle_type="simpleflight", pose=pose + ) + + self.nextDroneIndex += 1 + + # save the drone name + obj.realObjName = realObjName + self.objs[obj.name] = realObjName + self.drones[obj.name] = realObjName + + # start the drone and place it in the world + self.client.enableApiControl(True, realObjName) + self.client.armDisarm(True, realObjName) + self.client.simSetVehiclePose( + vehicle_name=realObjName, pose=pose, ignore_collision=True + ) + + # set propellers on or off + if obj.startHovering: + self.client.moveByVelocityAsync(0, 0, 0, 1, vehicle_name=realObjName) + else: + # shut off drone propellers + self.client.moveByVelocityAsync(0, 0, 0, -1, vehicle_name=realObjName) + + elif obj.blueprint == "PX4Drone": + realObjName = PX4_DRONE + + if self.PX4Drone: + raise RuntimeError("more than 1 px4 drone is not currently supported") + + self.PX4Drone = PX4_DRONE + self.client.simSetVehiclePose( + vehicle_name=realObjName, pose=pose, ignore_collision=True + ) + + obj.realObjName = realObjName + self.objs[obj.name] = realObjName + + elif obj.blueprint == "StaticObj": + # ensure user is creating an object that uses an existing asset + if not (obj.assetName in self.client.simListAssets()): + raise RuntimeError( + "no asset of name found: " + + obj.assetName + + "\n use one of these assets:\n" + + self.client.simListAssets() + ) + + # create obj in airsim + realObjName = self.client.simSpawnObject( + object_name=realObjName, + asset_name=obj.assetName, + pose=pose, + scale=scenicToAirsimScale( + Vector(obj.width, obj.length, obj.height), obj.dims + ), + physics_enabled=obj.physEnabled, + is_blueprint=False, + ) + + if obj.materialName: + self.client.simSetObjectMaterial(realObjName, obj.materialName) + + # add obj to sim lists + obj.realObjName = realObjName + self.objs[obj.name] = realObjName + self.objTrove.append(realObjName) + + else: + raise RuntimeError("object blueprint does not exist", obj.blueprint) + + def step(self): + self.client.simContinueForTime(self.simulator.timestep) + + # ------------------- Other Simulator methods ------------------- + + def destroy(self): + # reinstantiate client + client = self.client + client.client._loop.stop() # stop any running tasks to prevent errors + + client.simPause(True) + + # destroy all objs + for obj_name in self.objTrove: + client.simDestroyObject(obj_name) + + for droneName, realDroneName in self.drones.items(): + client.cancelLastTask(vehicle_name=realDroneName) + client.moveByVelocityAsync(0, 0, 0, -1, vehicle_name=realDroneName) + + # reset the client + client.reset() + + super().destroy() + print("canceled simulation") + + def getProperties(self, obj, properties): + if obj.blueprint == "AirSimPreExisting": + return dict( + position=obj.position, + velocity=Vector(0, 0, 0), + speed=0, + angularSpeed=0, + angularVelocity=Vector(0, 0, 0), + yaw=0, + pitch=0, + roll=0, + ) + objName = self.objs[obj.name] + + pose = None + velocity, speed, angularSpeed, angularVelocity = None, None, None, None + + # get obj data + if obj.blueprint == "Drone" or obj.blueprint == "PX4Drone": + pose = self.client.simGetVehiclePose(objName) + kinematics = self.client.simGetGroundTruthKinematics(objName) + velocity = airsimToScenicLocation(kinematics.linear_velocity) + + angularVelocity = airsimToScenicLocation(kinematics.angular_velocity) + + elif obj.blueprint == "StaticObj" or obj.blueprint == "AirSimPreExisting": + pose = self.client.simGetObjectPose(objName) + + # static objs don't have velocity + velocity = Vector(0, 0, 0) + angularVelocity = Vector(0, 0, 0) + else: + raise RuntimeError("object blueprint does not exist", obj.blueprint) + + # convert values + globalOrientation = airsimToScenicOrientation(pose.orientation) + yaw, pitch, roll = obj.parentOrientation.localAnglesFor(globalOrientation) + + location = airsimToScenicLocation(pose.position, obj.centerOffset) + + speed = math.hypot(velocity.x, velocity.y, velocity.z) + angularSpeed = math.hypot(angularVelocity.x, angularVelocity.y, angularVelocity.z) + + values = dict( + position=location, + velocity=velocity, + speed=speed, + angularSpeed=angularSpeed, + angularVelocity=angularVelocity, + yaw=yaw, + pitch=pitch, + roll=roll, + ) + + return values diff --git a/src/scenic/simulators/airsim/utils.py b/src/scenic/simulators/airsim/utils.py new file mode 100644 index 000000000..e8c610083 --- /dev/null +++ b/src/scenic/simulators/airsim/utils.py @@ -0,0 +1,98 @@ +import airsim +import scipy + +from scenic.core.type_support import toVector +from scenic.core.vectors import Orientation, Vector + +worldOffset = Vector(0, 0, 0) + + +def tupleToVector3r(tuple): + return airsim.Vector3r(tuple[0], tuple[1], tuple[2]) + + +def VectorToAirsimVec(vec): + return airsim.Vector3r(vec.x, vec.y, vec.z) + + +def AirsimVecToVector(vec): + return Vector(vec.x_val, vec.y_val, vec.z_val) + + +def scenicToAirsimOrientation(orientation): + rad90 = 1.5708 + yaw, pitch, roll = orientation.r.as_euler("ZXY", degrees=False) + yaw = -yaw - rad90 + return airsim.to_quaternion(pitch=pitch, yaw=yaw, roll=roll) + + +def airsimToScenicOrientation(orientation): + pitch, roll, yaw = airsim.to_eularian_angles(orientation) + angles = (pitch, -yaw - 90, roll) + r = scipy.spatial.transform.Rotation.from_euler( + seq="ZXY", angles=angles, degrees=True + ) + orientation = Orientation(r) + return Orientation(r) + + +# forPose - if the output will be fed into airsim's airsim.Pose() function +def scenicToAirsimLocation(position, centerOffset=Vector(0, 0, 0), forPose=True): + # turn position to a vector + position = Vector(position.x, position.y, position.z) + + # convert to unreal + position *= 100 # account for mesh scaling + position = Vector(position.x, -position.y, position.z) # left hand coords + + position -= centerOffset + position -= worldOffset + + if forPose: + # undo airsim's adjustments + position /= 100 # divide by 100 + position = Vector(position.x, position.y, -position.z) # negate z axis + + position = airsim.Vector3r( + position.x, position.y, position.z + ) # multiplies by 100 and negates z axis + + return position + + +# fromPose - if position was gained from simGetObjectPose +def airsimToScenicLocation(position, centerOffset=Vector(0, 0, 0), fromPose=True): + position = Vector( + position.x_val, + position.y_val, + position.z_val, + ) + + if fromPose: + # undo airsim's adjustments + position *= 100 # mult by 100 + position = Vector(position.x, position.y, -position.z) # negate z axis + + position += centerOffset + position += worldOffset + + # convert to scenic + position /= 100 # account for mesh scaling by .01 + position = Vector(position.x, -position.y, position.z) # left hand coords + + return position + + +def scenicToAirsimScale(size, dims): + return airsim.Vector3r(size.x / dims.x, size.y / dims.y, size.z / dims.z) + + +_prexistingObjs = {} + + +def _addPrexistingObj(obj): + _prexistingObjs[obj.name] = obj + + +def getPrexistingObj(objName): + return _prexistingObjs[objName] From 769385127b1e3fd314390c69aaf10dd00e73fbbb Mon Sep 17 00:00:00 2001 From: Lucy Horowitz <111657652+lucyhorowitz@users.noreply.github.com> Date: Fri, 10 Jul 2026 09:23:30 -0700 Subject: [PATCH 2/4] add simple_drone_settings --- .../settings/simple_drone_settings.json | 357 ++++++++++++++++++ 1 file changed, 357 insertions(+) create mode 100644 examples/airsim/settings/simple_drone_settings.json diff --git a/examples/airsim/settings/simple_drone_settings.json b/examples/airsim/settings/simple_drone_settings.json new file mode 100644 index 000000000..dc9a71fe1 --- /dev/null +++ b/examples/airsim/settings/simple_drone_settings.json @@ -0,0 +1,357 @@ +{ + "SettingsVersion": 1.2, + "SimMode": "Multirotor", + "ClockSpeed": 1, + "Vehicles": { + "Drone0": { + "VehicleType": "SimpleFlight", + "AutoCreate": true, + "Sensors": { + "frontDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true + }, + "rightDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true, + "Yaw": 90, + "Pitch": 0, + "Roll": 0 + }, + "leftDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true, + "Yaw": -90, + "Pitch": 0, + "Roll": 0 + }, + "rearDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true, + "Yaw": 180, + "Pitch": 0, + "Roll": 0 + } + } + }, + "Drone1": { + "VehicleType": "SimpleFlight", + "AutoCreate": false, + "Sensors": { + "frontDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true + }, + "rightDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true, + "Yaw": 90, + "Pitch": 0, + "Roll": 0 + }, + "leftDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true, + "Yaw": -90, + "Pitch": 0, + "Roll": 0 + }, + "rearDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true, + "Yaw": 180, + "Pitch": 0, + "Roll": 0 + } + } + }, + "Drone2": { + "VehicleType": "SimpleFlight", + "AutoCreate": false, + "Sensors": { + "frontDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true + }, + "rightDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true, + "Yaw": 90, + "Pitch": 0, + "Roll": 0 + }, + "leftDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true, + "Yaw": -90, + "Pitch": 0, + "Roll": 0 + }, + "rearDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true, + "Yaw": 180, + "Pitch": 0, + "Roll": 0 + } + } + }, + "Drone3": { + "VehicleType": "SimpleFlight", + "AutoCreate": false, + "Sensors": { + "frontDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true + }, + "rightDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true, + "Yaw": 90, + "Pitch": 0, + "Roll": 0 + }, + "leftDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true, + "Yaw": -90, + "Pitch": 0, + "Roll": 0 + }, + "rearDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true, + "Yaw": 180, + "Pitch": 0, + "Roll": 0 + } + } + }, + "Drone4": { + "VehicleType": "SimpleFlight", + "AutoCreate": false, + "Sensors": { + "frontDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true + }, + "rightDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true, + "Yaw": 90, + "Pitch": 0, + "Roll": 0 + }, + "leftDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true, + "Yaw": -90, + "Pitch": 0, + "Roll": 0 + }, + "rearDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true, + "Yaw": 180, + "Pitch": 0, + "Roll": 0 + } + } + }, + "Drone5": { + "VehicleType": "SimpleFlight", + "AutoCreate": false, + "Sensors": { + "frontDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true + }, + "rightDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true, + "Yaw": 90, + "Pitch": 0, + "Roll": 0 + }, + "leftDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true, + "Yaw": -90, + "Pitch": 0, + "Roll": 0 + }, + "rearDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true, + "Yaw": 180, + "Pitch": 0, + "Roll": 0 + } + } + }, + "Drone6": { + "VehicleType": "SimpleFlight", + "AutoCreate": false, + "Sensors": { + "frontDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true + }, + "rightDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true, + "Yaw": 90, + "Pitch": 0, + "Roll": 0 + }, + "leftDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true, + "Yaw": -90, + "Pitch": 0, + "Roll": 0 + }, + "rearDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true, + "Yaw": 180, + "Pitch": 0, + "Roll": 0 + } + } + }, + "Drone7": { + "VehicleType": "SimpleFlight", + "AutoCreate": false, + "Sensors": { + "frontDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true + }, + "rightDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true, + "Yaw": 90, + "Pitch": 0, + "Roll": 0 + }, + "leftDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true, + "Yaw": -90, + "Pitch": 0, + "Roll": 0 + }, + "rearDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true, + "Yaw": 180, + "Pitch": 0, + "Roll": 0 + } + } + }, + "Drone8": { + "VehicleType": "SimpleFlight", + "AutoCreate": false, + "Sensors": { + "frontDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true + }, + "rightDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true, + "Yaw": 90, + "Pitch": 0, + "Roll": 0 + }, + "leftDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true, + "Yaw": -90, + "Pitch": 0, + "Roll": 0 + }, + "rearDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true, + "Yaw": 180, + "Pitch": 0, + "Roll": 0 + } + } + }, + "Drone9": { + "VehicleType": "SimpleFlight", + "AutoCreate": false, + "Sensors": { + "frontDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true + }, + "rightDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true, + "Yaw": 90, + "Pitch": 0, + "Roll": 0 + }, + "leftDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true, + "Yaw": -90, + "Pitch": 0, + "Roll": 0 + }, + "rearDistance": { + "SensorType": 5, + "Enabled": true, + "DrawDebugPoints": true, + "Yaw": 180, + "Pitch": 0, + "Roll": 0 + } + } + } + } +} \ No newline at end of file From a0cff59df9734b32d655be171bcac652ed127d80 Mon Sep 17 00:00:00 2001 From: Lucy Horowitz <111657652+lucyhorowitz@users.noreply.github.com> Date: Fri, 10 Jul 2026 09:36:37 -0700 Subject: [PATCH 3/4] Add AirSim world generation instructions --- examples/airsim/README.md | 102 +++++++++ .../simulators/airsim/generators/__init__.py | 0 .../airsim/generators/generateWorldInfo.py | 199 ++++++++++++++++++ 3 files changed, 301 insertions(+) create mode 100644 examples/airsim/README.md create mode 100644 src/scenic/simulators/airsim/generators/__init__.py create mode 100644 src/scenic/simulators/airsim/generators/generateWorldInfo.py diff --git a/examples/airsim/README.md b/examples/airsim/README.md new file mode 100644 index 000000000..31ab43b91 --- /dev/null +++ b/examples/airsim/README.md @@ -0,0 +1,102 @@ +# AirSim example + +This directory contains a minimal Scenic example for controlling a drone in +[Microsoft AirSim](https://github.com/microsoft/AirSim). + +The example has been tested with AirSim 1.8.1 on Ubuntu 22.04. AirSim requires +a graphical environment and is not supported on macOS. + +## Install the Python dependencies + +Create and activate a virtual environment, then install Scenic from the +repository: + +```bash +python3 -m venv .venv-airsim +source .venv-airsim/bin/activate +python -m pip install --upgrade pip wheel +python -m pip install -e . +``` + +Install the AirSim client and its additional dependencies: + +```bash +python -m pip install msgpack-rpc-python promise +python -m pip install --no-build-isolation airsim==1.8.1 +``` + +The following NumPy and OpenCV versions were used during testing: + +```bash +python -m pip install --force-reinstall \ + numpy==1.26.4 \ + opencv-contrib-python==4.10.0.84 +``` + +Check the resulting environment: + +```bash +python -m pip check +``` + +## Start AirSim + +Download or build an AirSim environment. For example, launch the AirSim 1.8.1 +Blocks environment with the settings included in this directory: + +```bash +/path/to/Blocks.sh \ + -settings="/path/to/Scenic/examples/airsim/settings/simple_drone_settings.json" \ + -windowed \ + -ResX=640 \ + -ResY=480 \ + -NoVSync +``` + +These window settings are conservative defaults that work well on a remote +desktop. They can be adjusted for the available display and hardware. + +Run AirSim from a graphical desktop session and leave it open while Scenic is +running. + +## Generate world information + +The Scenic AirSim model requires information about the objects and assets in +the selected AirSim environment. + +Start AirSim first. Then, from the Scenic repository, run: + +```bash +python src/scenic/simulators/airsim/generators/generateWorldInfo.py \ + --outputDirectory "/path/to/blocks-world-info" +``` + +The output directory must not already exist. Generating the meshes may take +several minutes. + +The resulting directory contains: + +```text +worldInfo.json +objectMeshes/ +assets/ +``` + +Keep this generated directory outside the Scenic repository. Pass its location +to Scenic using the `worldInfoPath` parameter. + +## Run the patrol example + +With AirSim running, open another terminal, activate the Python environment, +and run: + +```bash +scenic examples/airsim/patrol.scenic \ + --simulate \ + --count 1 \ + --time 30 \ + --param worldInfoPath "/path/to/blocks-world-info" +``` + +The example creates one drone and sends it through a fixed sequence of patrol +points. diff --git a/src/scenic/simulators/airsim/generators/__init__.py b/src/scenic/simulators/airsim/generators/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/scenic/simulators/airsim/generators/generateWorldInfo.py b/src/scenic/simulators/airsim/generators/generateWorldInfo.py new file mode 100644 index 000000000..af2cdfc62 --- /dev/null +++ b/src/scenic/simulators/airsim/generators/generateWorldInfo.py @@ -0,0 +1,199 @@ +import argparse +import json +import os +from warnings import warn + +import airsim +import numpy as np +import trimesh + +from scenic.core.utils import repairMesh +from scenic.core.vectors import Vector + +# get output directory +parser = argparse.ArgumentParser() +parser.add_argument( + "-o", + "--outputDirectory", + type=str, + help="the directory where the world info should be dumped. This should be a directory that doesn't exist.", + required=True, +) +args = parser.parse_args() + + +outputDirectory = args.outputDirectory + "/" + + +# start airsim client +client = None +try: + client = airsim.MultirotorClient() + client.confirmConnection() + client.simPause(True) +except Exception: + raise RuntimeError("AirSim must be running before executing this code") + + +try: + os.makedirs(args.outputDirectory, exist_ok=False) +except FileExistsError: + raise RuntimeError("output directory already exists") + +os.makedirs(outputDirectory + "assets", exist_ok=True) +os.makedirs(outputDirectory + "objectMeshes", exist_ok=True) + + +assets = client.simListAssets() + +# create objects of the assets +objNameDict = {} + +for asset in assets: + objName = client.simSpawnObject( + object_name=asset, + asset_name=asset, + pose=airsim.Pose(position_val=airsim.Vector3r(0, 0, 0)), + scale=airsim.Vector3r(1, 1, 1), + ) + objNameDict[asset] = objName.lower() + +print("getting mesh data, this may take a few minutes") +meshes = client.simGetMeshPositionVertexBuffers() + +# cleanup +for mesh in objNameDict.values(): + client.simDestroyObject(mesh) + +print("collected mesh data") +meshDict = {} +for asset in assets: + objName = objNameDict[asset] + for mesh in meshes: + if mesh.name == objName: + meshDict[asset] = mesh + break + + +def makeTrimsh(mesh): + vertex_list = np.array(mesh.vertices, dtype=np.float32) + indices = np.array(mesh.indices, dtype=np.uint32) + + num_vertices = int(len(vertex_list) / 3) + num_indices = len(indices) + + vertices_reshaped = vertex_list.reshape((num_vertices, 3)) + indices_reshaped = indices.reshape((int(num_indices / 3), 3)) + vertices_reshaped = vertices_reshaped.astype(np.float64) + indices_reshaped = indices_reshaped.astype(np.int64) + + tmesh = trimesh.Trimesh( + vertices=vertices_reshaped, faces=indices_reshaped, process=True + ) + + # apply mesh rotation + rotation_matrix = trimesh.transformations.rotation_matrix(-np.pi / 2, [1, 0, 0]) + tmesh.apply_transform(rotation_matrix) + + if tmesh.body_count > 1: + tmesh.fix_normals(multibody=True) + else: + tmesh.fix_normals() + + try: + tmesh = repairMesh(tmesh, verbose=True) + except Exception as e: + warn(e) + print("could not repair mesh:", mesh.name) + return None + + return tmesh + + +# function for creating a default mesh if needed +_defaultMesh = None + + +def defaultMesh(): + global _defaultMesh + if not _defaultMesh: + _defaultMesh = trimesh.creation.box((1, 1, 1)) + return _defaultMesh + + +# save an obj file for each asset +for assetName in assets: + if not (assetName in meshDict): + continue + mesh = meshDict[assetName] + tmesh = makeTrimsh(mesh) + if not tmesh: + tmesh = defaultMesh() + + with open( + outputDirectory + "assets/" + assetName + ".obj", + "w", + ) as outfile: + outfile.write(trimesh.exchange.obj.export_obj(tmesh)) + + +# ----------------- extract world info + + +cleanedMeshes = [] +for mesh in meshes: + found = False + + # check if mesh is in the created meshes + for mesh2 in meshDict.values(): + if mesh.name == mesh2.name: + found = True + break + + # check if mesh is a vehicle + for vehicle in client.listVehicles(): + if mesh.name == vehicle: + found = True + break + + # if mesh was not found in checks, add it to cleanedMeshes + if not found: + cleanedMeshes.append(mesh) + +worldInfo = [] +for mesh in cleanedMeshes: + tmesh = makeTrimsh(mesh) + objectName = mesh.name + + pose = client.simGetObjectPose(objectName) + + position = pose.position + position = Vector( + position.x_val, + position.y_val, + position.z_val, + ) + position *= 100 # divide by 100 + position = Vector(position.x, position.y, -position.z) # negate z axis + + pitch, roll, yaw = airsim.to_eularian_angles(pose.orientation) + + worldInfo.append( + dict( + name=objectName, + position=(position.x, position.y, position.z), + orientation=(pitch, roll, yaw), + ), + ) + + with open( + outputDirectory + "objectMeshes/" + objectName + ".obj", + "w", + ) as outfile: + outfile.write(trimesh.exchange.obj.export_obj(tmesh)) + + +with open(outputDirectory + "worldInfo.json", "w") as outfile: + json.dump(worldInfo, outfile, indent=4) + +print("created world info at:", outputDirectory + "worldInfo.json") From 584d1ff939463279e583e41c99d1827f88b93ba0 Mon Sep 17 00:00:00 2001 From: Lucy Horowitz <111657652+lucyhorowitz@users.noreply.github.com> Date: Fri, 10 Jul 2026 09:49:22 -0700 Subject: [PATCH 4/4] Add fixed AirSim examples --- examples/airsim/flyFromBlockToBlock.scenic | 25 +++++++++++++++++++ examples/airsim/followDrone.scenic | 28 +++++++++++++++++++++ examples/airsim/functionalTerrain.scenic | 29 ++++++++++++++++++++++ examples/airsim/multiDrone.scenic | 8 ++++++ examples/airsim/worldGen.scenic | 28 +++++++++++++++++++++ 5 files changed, 118 insertions(+) create mode 100644 examples/airsim/flyFromBlockToBlock.scenic create mode 100644 examples/airsim/followDrone.scenic create mode 100644 examples/airsim/functionalTerrain.scenic create mode 100644 examples/airsim/multiDrone.scenic create mode 100644 examples/airsim/worldGen.scenic diff --git a/examples/airsim/flyFromBlockToBlock.scenic b/examples/airsim/flyFromBlockToBlock.scenic new file mode 100644 index 000000000..fe9c706f3 --- /dev/null +++ b/examples/airsim/flyFromBlockToBlock.scenic @@ -0,0 +1,25 @@ +param worldOffset = Vector(0, 0, 50) + +model scenic.simulators.airsim.model + + +ground = getPrexistingObj("ground") +centerArea = RectangularRegion(Vector(0, 0, 30), 0, 100, 100) + +platforms = [] +for i in range(10): + platforms.append(new StaticObj on ground, + contained in centerArea, + with assetName "Cone", + with width Range(3, 10), + with length Range(3, 10), + with height 10) + +points = [] +for platform in platforms: + platformRegion = platform.occupiedSpace.boundingPolygon + point = new Point on platformRegion + points.append(new Point at point.position + Vector(0, 0, 15)) + +drone = new Drone at Uniform(*points) + Vector(0, 0, 5), + with behavior Patrol(points, True) diff --git a/examples/airsim/followDrone.scenic b/examples/airsim/followDrone.scenic new file mode 100644 index 000000000..6cc5b40cc --- /dev/null +++ b/examples/airsim/followDrone.scenic @@ -0,0 +1,28 @@ +import math + +param worldOffset = Vector(0, 0, 50) +param timestep = 0.1 + +model scenic.simulators.airsim.model + + +def magnitude(v): + return math.hypot(v.x, v.y, v.z) + + +behavior Follow(target, speed=5, tolerance=2, offset=(0, 0, 1)): + while True: + targetPosition = target.position + offset + velocity = targetPosition - self.position + distance = magnitude(velocity) + if distance > tolerance: + velocity = (velocity / distance) * speed + take SetVelocity(velocity) + wait + + +drone1 = new Drone at (0, 0, 10), + with behavior Patrol([(0, 0, 10), (0, 10, 10), (10, 10, 10), (10, 0, 10)], True, speed=2) + +drone2 = new Drone at (0, 100, 10), + with behavior Follow(drone1, 5, 5, (0, 0, 0)) diff --git a/examples/airsim/functionalTerrain.scenic b/examples/airsim/functionalTerrain.scenic new file mode 100644 index 000000000..7e9c2d6f0 --- /dev/null +++ b/examples/airsim/functionalTerrain.scenic @@ -0,0 +1,29 @@ +import math + +param worldOffset = Vector(0, 0, 50) +param timestep = 0.1 + +model scenic.simulators.airsim.model + + +ground = getPrexistingObj("ground") + +ego = new StaticObj on ground, + with assetName "Cone", + with width 10, + with length 10, + with height 10 + +positions = [] +for i in range(10): + pos = Vector(i * 3, math.cos(i) * Uniform(1, 3), 1) + positions.append(pos + Vector(0, 0, 3)) + + new StaticObj at pos + Vector(0, 0, 1), + with assetName "Cone", + with width 1, + with length 1, + with height 1 + +drone = new Drone on positions[0], + with behavior Patrol(positions, smooth=False, speed=5) diff --git a/examples/airsim/multiDrone.scenic b/examples/airsim/multiDrone.scenic new file mode 100644 index 000000000..730d988d0 --- /dev/null +++ b/examples/airsim/multiDrone.scenic @@ -0,0 +1,8 @@ +param worldOffset = Vector(0, 0, 50) + +model scenic.simulators.airsim.model + + +for i in range(3): + new Drone at (Range(-100, 100), Range(-100, 100), Range(0, 50)), + with behavior FlyToPosition((Range(-100, 100), Range(-100, 100), Range(0, 50))) diff --git a/examples/airsim/worldGen.scenic b/examples/airsim/worldGen.scenic new file mode 100644 index 000000000..7851e57c1 --- /dev/null +++ b/examples/airsim/worldGen.scenic @@ -0,0 +1,28 @@ +param worldOffset = Vector(0, 0, 50) + +model scenic.simulators.airsim.model + + +ground = getPrexistingObj("ground") + +ego = new StaticObj on ground, + with assetName "Cone", + with width 10, + with length 10, + with height 10 + +centerArea = RectangularRegion(Vector(0, 200, 30), 0, 100, 100) + +blocks = [] +for i in range(10): + blocks.append(new StaticObj on ground, + contained in centerArea, + with assetName "Cube", + with width Range(3, 10), + with length Range(3, 10), + with height 10) + +ranBlock = Uniform(*blocks) + +drone = new Drone on ranBlock, + with behavior Patrol([(10, 10, 10), (2, 2, 2)])