kos_sim.server

Server and simulation loop for KOS.

  1"""Server and simulation loop for KOS."""
  2
  3import argparse
  4import asyncio
  5import itertools
  6import logging
  7import os
  8import time
  9import traceback
 10from concurrent import futures
 11from dataclasses import dataclass
 12from pathlib import Path
 13
 14import colorlogging
 15import grpc
 16from kos_protos import actuator_pb2_grpc, imu_pb2_grpc, process_manager_pb2_grpc, sim_pb2_grpc
 17from kscale import K
 18from kscale.web.gen.api import RobotURDFMetadataOutput
 19from mujoco_scenes.mjcf import list_scenes
 20
 21from kos_sim import logger
 22from kos_sim.services import ActuatorService, IMUService, ProcessManagerService, SimService
 23from kos_sim.simulator import MujocoSimulator
 24from kos_sim.utils import get_sim_artifacts_path
 25from kos_sim.video_recorder import VideoRecorder
 26
 27
 28@dataclass
 29class SimulationRandomizationConfig:
 30    command_delay_min: float = 0.0
 31    command_delay_max: float = 0.0
 32    joint_pos_delta_noise: float = 0.0
 33    joint_pos_noise: float = 0.0
 34    joint_vel_noise: float = 0.0
 35
 36
 37@dataclass
 38class RenderingConfig:
 39    render: bool = True
 40    render_frequency: float = 1
 41    video_output_dir: str | Path | None = None
 42    frame_width: int = 640
 43    frame_height: int = 480
 44    camera: str | None = None
 45
 46
 47@dataclass
 48class PhysicsConfig:
 49    gravity: bool = True
 50    suspended: bool = False
 51    start_height: float = 1.5
 52    dt: float = 0.0001
 53
 54
 55@dataclass
 56class SimulationServerConfig:
 57    randomization: SimulationRandomizationConfig
 58    rendering: RenderingConfig
 59    physics: PhysicsConfig
 60    model_path: str | Path
 61    model_metadata: RobotURDFMetadataOutput
 62    mujoco_scene: str = "smooth"
 63    host: str = "localhost"
 64    port: int = 50051
 65    sleep_time: float = 1e-6
 66
 67
 68class SimulationServer:
 69    def __init__(
 70        self,
 71        config: SimulationServerConfig,
 72    ) -> None:
 73        self.simulator = MujocoSimulator(
 74            model_path=config.model_path,
 75            model_metadata=config.model_metadata,
 76            dt=config.physics.dt,
 77            gravity=config.physics.gravity,
 78            render_mode="window" if config.rendering.render else "offscreen",
 79            suspended=config.physics.suspended,
 80            start_height=config.physics.start_height,
 81            command_delay_min=config.randomization.command_delay_min,
 82            command_delay_max=config.randomization.command_delay_max,
 83            joint_pos_delta_noise=config.randomization.joint_pos_delta_noise,
 84            joint_pos_noise=config.randomization.joint_pos_noise,
 85            joint_vel_noise=config.randomization.joint_vel_noise,
 86            mujoco_scene=config.mujoco_scene,
 87            camera=config.rendering.camera,
 88            frame_width=config.rendering.frame_width,
 89            frame_height=config.rendering.frame_height,
 90        )
 91        self.host = config.host
 92        self.port = config.port
 93        self._sleep_time = config.sleep_time
 94        self._stop_event = asyncio.Event()
 95        self._server = None
 96        self._step_lock = asyncio.Semaphore(1)
 97        self._render_decimation = int(1.0 / config.rendering.render_frequency)
 98
 99        # Initialize video recorder if needed
100        self.video_recorder = None
101        if config.rendering.video_output_dir is not None:
102            self.video_recorder = VideoRecorder(
103                simulator=self.simulator,
104                output_dir=config.rendering.video_output_dir,
105                fps=int(self.simulator._control_frequency),
106                frame_width=config.rendering.frame_width,
107                frame_height=config.rendering.frame_height,
108            )
109
110    async def _grpc_server_loop(self) -> None:
111        """Run the async gRPC server."""
112        # Create async gRPC server
113        self._server = grpc.aio.server(futures.ThreadPoolExecutor(max_workers=10))
114
115        assert self._server is not None
116
117        # Add our services (these need to be modified to be async as well)
118        actuator_service = ActuatorService(self.simulator, self._step_lock)
119        imu_service = IMUService(self.simulator)
120        sim_service = SimService(self.simulator)
121        process_manager_service = ProcessManagerService(self.simulator, self.video_recorder)
122
123        actuator_pb2_grpc.add_ActuatorServiceServicer_to_server(actuator_service, self._server)
124        imu_pb2_grpc.add_IMUServiceServicer_to_server(imu_service, self._server)
125        sim_pb2_grpc.add_SimulationServiceServicer_to_server(sim_service, self._server)
126        process_manager_pb2_grpc.add_ProcessManagerServiceServicer_to_server(process_manager_service, self._server)
127
128        # Start the server
129        self._server.add_insecure_port(f"{self.host}:{self.port}")
130        await self._server.start()
131        logger.info("Server started on %s:%d", self.host, self.port)
132        await self._server.wait_for_termination()
133
134    async def simulation_loop(self) -> None:
135        """Run the simulation loop asynchronously."""
136        start_time = time.time()
137        num_renders = 0
138        num_steps = 0
139
140        try:
141            while not self._stop_event.is_set():
142                while self.simulator._sim_time < time.time():
143                    # Run one control loop.
144                    async with self._step_lock:
145                        for _ in range(self.simulator._sim_decimation):
146                            await self.simulator.step()
147                    await asyncio.sleep(self._sleep_time)
148
149                if num_steps % self._render_decimation == 0:
150                    await self.simulator.render()
151                    num_renders += 1
152
153                if self.video_recorder is not None and self.video_recorder.is_recording:
154                    await self.video_recorder.capture_frame()
155
156                # Sleep until the next control update.
157                current_time = time.time()
158                if current_time < self.simulator._sim_time:
159                    await asyncio.sleep(self.simulator._sim_time - current_time)
160                num_steps += 1
161                logger.debug(
162                    "Simulation time: %f, rendering frequency: %f",
163                    self.simulator._sim_time,
164                    num_renders / (time.time() - start_time),
165                )
166
167        except Exception as e:
168            logger.error("Simulation loop failed: %s", e)
169            logger.error("Traceback: %s", traceback.format_exc())
170
171        finally:
172            await self.stop()
173
174    async def start(self) -> None:
175        """Start both the gRPC server and simulation loop asynchronously."""
176        grpc_task = asyncio.create_task(self._grpc_server_loop())
177        sim_task = asyncio.create_task(self.simulation_loop())
178
179        try:
180            await asyncio.gather(grpc_task, sim_task)
181        except asyncio.CancelledError:
182            await self.stop()
183
184    async def stop(self) -> None:
185        """Stop the simulation and cleanup resources asynchronously."""
186        logger.info("Shutting down simulation...")
187        self._stop_event.set()
188        if self.video_recorder is not None and self.video_recorder.is_recording:
189            self.video_recorder.stop_recording()
190        if self._server is not None:
191            await self._server.stop(0)
192        await self.simulator.close()
193
194
195async def get_model_metadata(api: K, model_name: str) -> RobotURDFMetadataOutput:
196    model_path = get_sim_artifacts_path() / model_name / "metadata.json"
197    if model_path.exists():
198        return RobotURDFMetadataOutput.model_validate_json(model_path.read_text())
199    model_path.parent.mkdir(parents=True, exist_ok=True)
200    robot_class = await api.get_robot_class(model_name)
201    metadata = robot_class.metadata
202    if metadata is None:
203        raise ValueError(f"No metadata found for model {model_name}")
204    model_path.write_text(metadata.model_dump_json())
205    return metadata
206
207
208async def serve(
209    model_name: str,
210    host: str = "localhost",
211    port: int = 50051,
212    rendering: RenderingConfig = RenderingConfig(),
213    physics: PhysicsConfig = PhysicsConfig(),
214    randomization: SimulationRandomizationConfig = SimulationRandomizationConfig(),
215    mujoco_scene: str = "smooth",
216) -> None:
217    async with K() as api:
218        model_dir, model_metadata = await asyncio.gather(
219            api.download_and_extract_urdf(model_name),
220            get_model_metadata(api, model_name),
221        )
222
223    model_path = next(
224        itertools.chain(
225            model_dir.glob("*.mjcf"),
226            model_dir.glob("*.xml"),
227        )
228    )
229
230    config = SimulationServerConfig(
231        model_path=model_path,
232        model_metadata=model_metadata,
233        physics=physics,
234        rendering=rendering,
235        randomization=randomization,
236    )
237
238    server = SimulationServer(config)
239    await server.start()
240
241
242async def run_server() -> None:
243    parser = argparse.ArgumentParser(description="Start the simulation gRPC server.")
244    parser.add_argument("model_name", type=str, help="Name of the model to simulate")
245    parser.add_argument("--host", type=str, default="localhost", help="Host to listen on")
246    parser.add_argument("--port", type=int, default=50051, help="Port to listen on")
247    parser.add_argument("--dt", type=float, default=0.001, help="Simulation timestep")
248    parser.add_argument("--no-gravity", action="store_true", help="Disable gravity")
249    parser.add_argument("--no-render", action="store_true", help="Disable rendering")
250    parser.add_argument("--render-frequency", type=float, default=1, help="Render frequency (Hz)")
251    parser.add_argument("--suspended", action="store_true", help="Suspended simulation")
252    parser.add_argument("--command-delay-min", type=float, default=0.0, help="Minimum command delay")
253    parser.add_argument("--command-delay-max", type=float, default=0.0, help="Maximum command delay")
254    parser.add_argument("--start-height", type=float, default=1.5, help="Start height")
255    parser.add_argument("--joint-pos-delta-noise", type=float, default=0.0, help="Joint position delta noise (degrees)")
256    parser.add_argument("--joint-pos-noise", type=float, default=0.0, help="Joint position noise (degrees)")
257    parser.add_argument("--joint-vel-noise", type=float, default=0.0, help="Joint velocity noise (degrees/second)")
258    parser.add_argument("--scene", choices=list_scenes(), default="smooth", help="Mujoco scene to use")
259    parser.add_argument("--camera", type=str, default=None, help="Camera to use")
260    parser.add_argument("--debug", action="store_true", help="Enable debug logging")
261    parser.add_argument("--video-output-dir", type=str, default="videos", help="Directory to save videos")
262    parser.add_argument("--frame-width", type=int, default=640, help="Frame width")
263    parser.add_argument("--frame-height", type=int, default=480, help="Frame height")
264
265    args = parser.parse_args()
266
267    colorlogging.configure(level=logging.DEBUG if args.debug else logging.INFO)
268
269    model_name = args.model_name
270    host = args.host
271    port = args.port
272    dt = args.dt
273    gravity = not args.no_gravity
274    render = not args.no_render
275    render_frequency = args.render_frequency
276    suspended = args.suspended
277    start_height = args.start_height
278    command_delay_min = args.command_delay_min
279    command_delay_max = args.command_delay_max
280    joint_pos_delta_noise = args.joint_pos_delta_noise
281    joint_pos_noise = args.joint_pos_noise
282    joint_vel_noise = args.joint_vel_noise
283    mujoco_scene = args.scene
284    camera = args.camera
285    frame_width = args.frame_width
286    frame_height = args.frame_height
287
288    video_output_dir = args.video_output_dir if not render else None
289
290    logger.info("Model name: %s", model_name)
291    logger.info("Port: %d", port)
292    logger.info("DT: %f", dt)
293    logger.info("Gravity: %s", gravity)
294    logger.info("Render: %s", render)
295    logger.info("Render frequency: %f", render_frequency)
296    logger.info("Suspended: %s", suspended)
297    logger.info("Start height: %f", start_height)
298    logger.info("Command delay min: %f", command_delay_min)
299    logger.info("Command delay max: %f", command_delay_max)
300    logger.info("Joint pos delta noise: %f", joint_pos_delta_noise)
301    logger.info("Joint pos noise: %f", joint_pos_noise)
302    logger.info("Joint vel noise: %f", joint_vel_noise)
303    logger.info("Mujoco scene: %s", mujoco_scene)
304    logger.info("Camera: %s", camera)
305    logger.info("Video output dir: %s", video_output_dir)
306    logger.info("Frame width: %d", frame_width)
307    logger.info("Frame height: %d", frame_height)
308
309    if video_output_dir is not None:
310        os.makedirs(video_output_dir, exist_ok=True)
311
312    rendering = RenderingConfig(
313        render=render,
314        render_frequency=render_frequency,
315        video_output_dir=video_output_dir,
316        frame_width=frame_width,
317        frame_height=frame_height,
318        camera=camera,
319    )
320
321    physics = PhysicsConfig(
322        dt=dt,
323        gravity=gravity,
324        suspended=suspended,
325        start_height=start_height,
326    )
327
328    randomization = SimulationRandomizationConfig(
329        command_delay_min=command_delay_min,
330        command_delay_max=command_delay_max,
331        joint_pos_delta_noise=joint_pos_delta_noise,
332        joint_pos_noise=joint_pos_noise,
333        joint_vel_noise=joint_vel_noise,
334    )
335
336    await serve(
337        model_name=model_name,
338        host=host,
339        port=port,
340        rendering=rendering,
341        physics=physics,
342        randomization=randomization,
343        mujoco_scene=mujoco_scene,
344    )
345
346
347def main() -> None:
348    asyncio.run(run_server())
349
350
351if __name__ == "__main__":
352    # python -m kos_sim.server
353    main()
@dataclass
class SimulationRandomizationConfig:
29@dataclass
30class SimulationRandomizationConfig:
31    command_delay_min: float = 0.0
32    command_delay_max: float = 0.0
33    joint_pos_delta_noise: float = 0.0
34    joint_pos_noise: float = 0.0
35    joint_vel_noise: float = 0.0
SimulationRandomizationConfig( command_delay_min: float = 0.0, command_delay_max: float = 0.0, joint_pos_delta_noise: float = 0.0, joint_pos_noise: float = 0.0, joint_vel_noise: float = 0.0)
command_delay_min: float = 0.0
command_delay_max: float = 0.0
joint_pos_delta_noise: float = 0.0
joint_pos_noise: float = 0.0
joint_vel_noise: float = 0.0
@dataclass
class RenderingConfig:
38@dataclass
39class RenderingConfig:
40    render: bool = True
41    render_frequency: float = 1
42    video_output_dir: str | Path | None = None
43    frame_width: int = 640
44    frame_height: int = 480
45    camera: str | None = None
RenderingConfig( render: bool = True, render_frequency: float = 1, video_output_dir: str | pathlib.Path | None = None, frame_width: int = 640, frame_height: int = 480, camera: str | None = None)
render: bool = True
render_frequency: float = 1
video_output_dir: str | pathlib.Path | None = None
frame_width: int = 640
frame_height: int = 480
camera: str | None = None
@dataclass
class PhysicsConfig:
48@dataclass
49class PhysicsConfig:
50    gravity: bool = True
51    suspended: bool = False
52    start_height: float = 1.5
53    dt: float = 0.0001
PhysicsConfig( gravity: bool = True, suspended: bool = False, start_height: float = 1.5, dt: float = 0.0001)
gravity: bool = True
suspended: bool = False
start_height: float = 1.5
dt: float = 0.0001
@dataclass
class SimulationServerConfig:
56@dataclass
57class SimulationServerConfig:
58    randomization: SimulationRandomizationConfig
59    rendering: RenderingConfig
60    physics: PhysicsConfig
61    model_path: str | Path
62    model_metadata: RobotURDFMetadataOutput
63    mujoco_scene: str = "smooth"
64    host: str = "localhost"
65    port: int = 50051
66    sleep_time: float = 1e-6
SimulationServerConfig( randomization: SimulationRandomizationConfig, rendering: RenderingConfig, physics: PhysicsConfig, model_path: str | pathlib.Path, model_metadata: kscale.web.gen.api.RobotURDFMetadataOutput, mujoco_scene: str = 'smooth', host: str = 'localhost', port: int = 50051, sleep_time: float = 1e-06)
rendering: RenderingConfig
physics: PhysicsConfig
model_path: str | pathlib.Path
model_metadata: kscale.web.gen.api.RobotURDFMetadataOutput
mujoco_scene: str = 'smooth'
host: str = 'localhost'
port: int = 50051
sleep_time: float = 1e-06
class SimulationServer:
 69class SimulationServer:
 70    def __init__(
 71        self,
 72        config: SimulationServerConfig,
 73    ) -> None:
 74        self.simulator = MujocoSimulator(
 75            model_path=config.model_path,
 76            model_metadata=config.model_metadata,
 77            dt=config.physics.dt,
 78            gravity=config.physics.gravity,
 79            render_mode="window" if config.rendering.render else "offscreen",
 80            suspended=config.physics.suspended,
 81            start_height=config.physics.start_height,
 82            command_delay_min=config.randomization.command_delay_min,
 83            command_delay_max=config.randomization.command_delay_max,
 84            joint_pos_delta_noise=config.randomization.joint_pos_delta_noise,
 85            joint_pos_noise=config.randomization.joint_pos_noise,
 86            joint_vel_noise=config.randomization.joint_vel_noise,
 87            mujoco_scene=config.mujoco_scene,
 88            camera=config.rendering.camera,
 89            frame_width=config.rendering.frame_width,
 90            frame_height=config.rendering.frame_height,
 91        )
 92        self.host = config.host
 93        self.port = config.port
 94        self._sleep_time = config.sleep_time
 95        self._stop_event = asyncio.Event()
 96        self._server = None
 97        self._step_lock = asyncio.Semaphore(1)
 98        self._render_decimation = int(1.0 / config.rendering.render_frequency)
 99
100        # Initialize video recorder if needed
101        self.video_recorder = None
102        if config.rendering.video_output_dir is not None:
103            self.video_recorder = VideoRecorder(
104                simulator=self.simulator,
105                output_dir=config.rendering.video_output_dir,
106                fps=int(self.simulator._control_frequency),
107                frame_width=config.rendering.frame_width,
108                frame_height=config.rendering.frame_height,
109            )
110
111    async def _grpc_server_loop(self) -> None:
112        """Run the async gRPC server."""
113        # Create async gRPC server
114        self._server = grpc.aio.server(futures.ThreadPoolExecutor(max_workers=10))
115
116        assert self._server is not None
117
118        # Add our services (these need to be modified to be async as well)
119        actuator_service = ActuatorService(self.simulator, self._step_lock)
120        imu_service = IMUService(self.simulator)
121        sim_service = SimService(self.simulator)
122        process_manager_service = ProcessManagerService(self.simulator, self.video_recorder)
123
124        actuator_pb2_grpc.add_ActuatorServiceServicer_to_server(actuator_service, self._server)
125        imu_pb2_grpc.add_IMUServiceServicer_to_server(imu_service, self._server)
126        sim_pb2_grpc.add_SimulationServiceServicer_to_server(sim_service, self._server)
127        process_manager_pb2_grpc.add_ProcessManagerServiceServicer_to_server(process_manager_service, self._server)
128
129        # Start the server
130        self._server.add_insecure_port(f"{self.host}:{self.port}")
131        await self._server.start()
132        logger.info("Server started on %s:%d", self.host, self.port)
133        await self._server.wait_for_termination()
134
135    async def simulation_loop(self) -> None:
136        """Run the simulation loop asynchronously."""
137        start_time = time.time()
138        num_renders = 0
139        num_steps = 0
140
141        try:
142            while not self._stop_event.is_set():
143                while self.simulator._sim_time < time.time():
144                    # Run one control loop.
145                    async with self._step_lock:
146                        for _ in range(self.simulator._sim_decimation):
147                            await self.simulator.step()
148                    await asyncio.sleep(self._sleep_time)
149
150                if num_steps % self._render_decimation == 0:
151                    await self.simulator.render()
152                    num_renders += 1
153
154                if self.video_recorder is not None and self.video_recorder.is_recording:
155                    await self.video_recorder.capture_frame()
156
157                # Sleep until the next control update.
158                current_time = time.time()
159                if current_time < self.simulator._sim_time:
160                    await asyncio.sleep(self.simulator._sim_time - current_time)
161                num_steps += 1
162                logger.debug(
163                    "Simulation time: %f, rendering frequency: %f",
164                    self.simulator._sim_time,
165                    num_renders / (time.time() - start_time),
166                )
167
168        except Exception as e:
169            logger.error("Simulation loop failed: %s", e)
170            logger.error("Traceback: %s", traceback.format_exc())
171
172        finally:
173            await self.stop()
174
175    async def start(self) -> None:
176        """Start both the gRPC server and simulation loop asynchronously."""
177        grpc_task = asyncio.create_task(self._grpc_server_loop())
178        sim_task = asyncio.create_task(self.simulation_loop())
179
180        try:
181            await asyncio.gather(grpc_task, sim_task)
182        except asyncio.CancelledError:
183            await self.stop()
184
185    async def stop(self) -> None:
186        """Stop the simulation and cleanup resources asynchronously."""
187        logger.info("Shutting down simulation...")
188        self._stop_event.set()
189        if self.video_recorder is not None and self.video_recorder.is_recording:
190            self.video_recorder.stop_recording()
191        if self._server is not None:
192            await self._server.stop(0)
193        await self.simulator.close()
SimulationServer(config: SimulationServerConfig)
 70    def __init__(
 71        self,
 72        config: SimulationServerConfig,
 73    ) -> None:
 74        self.simulator = MujocoSimulator(
 75            model_path=config.model_path,
 76            model_metadata=config.model_metadata,
 77            dt=config.physics.dt,
 78            gravity=config.physics.gravity,
 79            render_mode="window" if config.rendering.render else "offscreen",
 80            suspended=config.physics.suspended,
 81            start_height=config.physics.start_height,
 82            command_delay_min=config.randomization.command_delay_min,
 83            command_delay_max=config.randomization.command_delay_max,
 84            joint_pos_delta_noise=config.randomization.joint_pos_delta_noise,
 85            joint_pos_noise=config.randomization.joint_pos_noise,
 86            joint_vel_noise=config.randomization.joint_vel_noise,
 87            mujoco_scene=config.mujoco_scene,
 88            camera=config.rendering.camera,
 89            frame_width=config.rendering.frame_width,
 90            frame_height=config.rendering.frame_height,
 91        )
 92        self.host = config.host
 93        self.port = config.port
 94        self._sleep_time = config.sleep_time
 95        self._stop_event = asyncio.Event()
 96        self._server = None
 97        self._step_lock = asyncio.Semaphore(1)
 98        self._render_decimation = int(1.0 / config.rendering.render_frequency)
 99
100        # Initialize video recorder if needed
101        self.video_recorder = None
102        if config.rendering.video_output_dir is not None:
103            self.video_recorder = VideoRecorder(
104                simulator=self.simulator,
105                output_dir=config.rendering.video_output_dir,
106                fps=int(self.simulator._control_frequency),
107                frame_width=config.rendering.frame_width,
108                frame_height=config.rendering.frame_height,
109            )
simulator
host
port
video_recorder
async def simulation_loop(self) -> None:
135    async def simulation_loop(self) -> None:
136        """Run the simulation loop asynchronously."""
137        start_time = time.time()
138        num_renders = 0
139        num_steps = 0
140
141        try:
142            while not self._stop_event.is_set():
143                while self.simulator._sim_time < time.time():
144                    # Run one control loop.
145                    async with self._step_lock:
146                        for _ in range(self.simulator._sim_decimation):
147                            await self.simulator.step()
148                    await asyncio.sleep(self._sleep_time)
149
150                if num_steps % self._render_decimation == 0:
151                    await self.simulator.render()
152                    num_renders += 1
153
154                if self.video_recorder is not None and self.video_recorder.is_recording:
155                    await self.video_recorder.capture_frame()
156
157                # Sleep until the next control update.
158                current_time = time.time()
159                if current_time < self.simulator._sim_time:
160                    await asyncio.sleep(self.simulator._sim_time - current_time)
161                num_steps += 1
162                logger.debug(
163                    "Simulation time: %f, rendering frequency: %f",
164                    self.simulator._sim_time,
165                    num_renders / (time.time() - start_time),
166                )
167
168        except Exception as e:
169            logger.error("Simulation loop failed: %s", e)
170            logger.error("Traceback: %s", traceback.format_exc())
171
172        finally:
173            await self.stop()

Run the simulation loop asynchronously.

async def start(self) -> None:
175    async def start(self) -> None:
176        """Start both the gRPC server and simulation loop asynchronously."""
177        grpc_task = asyncio.create_task(self._grpc_server_loop())
178        sim_task = asyncio.create_task(self.simulation_loop())
179
180        try:
181            await asyncio.gather(grpc_task, sim_task)
182        except asyncio.CancelledError:
183            await self.stop()

Start both the gRPC server and simulation loop asynchronously.

async def stop(self) -> None:
185    async def stop(self) -> None:
186        """Stop the simulation and cleanup resources asynchronously."""
187        logger.info("Shutting down simulation...")
188        self._stop_event.set()
189        if self.video_recorder is not None and self.video_recorder.is_recording:
190            self.video_recorder.stop_recording()
191        if self._server is not None:
192            await self._server.stop(0)
193        await self.simulator.close()

Stop the simulation and cleanup resources asynchronously.

async def get_model_metadata( api: kscale.web.clients.client.WWWClient, model_name: str) -> kscale.web.gen.api.RobotURDFMetadataOutput:
196async def get_model_metadata(api: K, model_name: str) -> RobotURDFMetadataOutput:
197    model_path = get_sim_artifacts_path() / model_name / "metadata.json"
198    if model_path.exists():
199        return RobotURDFMetadataOutput.model_validate_json(model_path.read_text())
200    model_path.parent.mkdir(parents=True, exist_ok=True)
201    robot_class = await api.get_robot_class(model_name)
202    metadata = robot_class.metadata
203    if metadata is None:
204        raise ValueError(f"No metadata found for model {model_name}")
205    model_path.write_text(metadata.model_dump_json())
206    return metadata
async def serve( model_name: str, host: str = 'localhost', port: int = 50051, rendering: RenderingConfig = RenderingConfig(render=True, render_frequency=1, video_output_dir=None, frame_width=640, frame_height=480, camera=None), physics: PhysicsConfig = PhysicsConfig(gravity=True, suspended=False, start_height=1.5, dt=0.0001), randomization: SimulationRandomizationConfig = SimulationRandomizationConfig(command_delay_min=0.0, command_delay_max=0.0, joint_pos_delta_noise=0.0, joint_pos_noise=0.0, joint_vel_noise=0.0), mujoco_scene: str = 'smooth') -> None:
209async def serve(
210    model_name: str,
211    host: str = "localhost",
212    port: int = 50051,
213    rendering: RenderingConfig = RenderingConfig(),
214    physics: PhysicsConfig = PhysicsConfig(),
215    randomization: SimulationRandomizationConfig = SimulationRandomizationConfig(),
216    mujoco_scene: str = "smooth",
217) -> None:
218    async with K() as api:
219        model_dir, model_metadata = await asyncio.gather(
220            api.download_and_extract_urdf(model_name),
221            get_model_metadata(api, model_name),
222        )
223
224    model_path = next(
225        itertools.chain(
226            model_dir.glob("*.mjcf"),
227            model_dir.glob("*.xml"),
228        )
229    )
230
231    config = SimulationServerConfig(
232        model_path=model_path,
233        model_metadata=model_metadata,
234        physics=physics,
235        rendering=rendering,
236        randomization=randomization,
237    )
238
239    server = SimulationServer(config)
240    await server.start()
async def run_server() -> None:
243async def run_server() -> None:
244    parser = argparse.ArgumentParser(description="Start the simulation gRPC server.")
245    parser.add_argument("model_name", type=str, help="Name of the model to simulate")
246    parser.add_argument("--host", type=str, default="localhost", help="Host to listen on")
247    parser.add_argument("--port", type=int, default=50051, help="Port to listen on")
248    parser.add_argument("--dt", type=float, default=0.001, help="Simulation timestep")
249    parser.add_argument("--no-gravity", action="store_true", help="Disable gravity")
250    parser.add_argument("--no-render", action="store_true", help="Disable rendering")
251    parser.add_argument("--render-frequency", type=float, default=1, help="Render frequency (Hz)")
252    parser.add_argument("--suspended", action="store_true", help="Suspended simulation")
253    parser.add_argument("--command-delay-min", type=float, default=0.0, help="Minimum command delay")
254    parser.add_argument("--command-delay-max", type=float, default=0.0, help="Maximum command delay")
255    parser.add_argument("--start-height", type=float, default=1.5, help="Start height")
256    parser.add_argument("--joint-pos-delta-noise", type=float, default=0.0, help="Joint position delta noise (degrees)")
257    parser.add_argument("--joint-pos-noise", type=float, default=0.0, help="Joint position noise (degrees)")
258    parser.add_argument("--joint-vel-noise", type=float, default=0.0, help="Joint velocity noise (degrees/second)")
259    parser.add_argument("--scene", choices=list_scenes(), default="smooth", help="Mujoco scene to use")
260    parser.add_argument("--camera", type=str, default=None, help="Camera to use")
261    parser.add_argument("--debug", action="store_true", help="Enable debug logging")
262    parser.add_argument("--video-output-dir", type=str, default="videos", help="Directory to save videos")
263    parser.add_argument("--frame-width", type=int, default=640, help="Frame width")
264    parser.add_argument("--frame-height", type=int, default=480, help="Frame height")
265
266    args = parser.parse_args()
267
268    colorlogging.configure(level=logging.DEBUG if args.debug else logging.INFO)
269
270    model_name = args.model_name
271    host = args.host
272    port = args.port
273    dt = args.dt
274    gravity = not args.no_gravity
275    render = not args.no_render
276    render_frequency = args.render_frequency
277    suspended = args.suspended
278    start_height = args.start_height
279    command_delay_min = args.command_delay_min
280    command_delay_max = args.command_delay_max
281    joint_pos_delta_noise = args.joint_pos_delta_noise
282    joint_pos_noise = args.joint_pos_noise
283    joint_vel_noise = args.joint_vel_noise
284    mujoco_scene = args.scene
285    camera = args.camera
286    frame_width = args.frame_width
287    frame_height = args.frame_height
288
289    video_output_dir = args.video_output_dir if not render else None
290
291    logger.info("Model name: %s", model_name)
292    logger.info("Port: %d", port)
293    logger.info("DT: %f", dt)
294    logger.info("Gravity: %s", gravity)
295    logger.info("Render: %s", render)
296    logger.info("Render frequency: %f", render_frequency)
297    logger.info("Suspended: %s", suspended)
298    logger.info("Start height: %f", start_height)
299    logger.info("Command delay min: %f", command_delay_min)
300    logger.info("Command delay max: %f", command_delay_max)
301    logger.info("Joint pos delta noise: %f", joint_pos_delta_noise)
302    logger.info("Joint pos noise: %f", joint_pos_noise)
303    logger.info("Joint vel noise: %f", joint_vel_noise)
304    logger.info("Mujoco scene: %s", mujoco_scene)
305    logger.info("Camera: %s", camera)
306    logger.info("Video output dir: %s", video_output_dir)
307    logger.info("Frame width: %d", frame_width)
308    logger.info("Frame height: %d", frame_height)
309
310    if video_output_dir is not None:
311        os.makedirs(video_output_dir, exist_ok=True)
312
313    rendering = RenderingConfig(
314        render=render,
315        render_frequency=render_frequency,
316        video_output_dir=video_output_dir,
317        frame_width=frame_width,
318        frame_height=frame_height,
319        camera=camera,
320    )
321
322    physics = PhysicsConfig(
323        dt=dt,
324        gravity=gravity,
325        suspended=suspended,
326        start_height=start_height,
327    )
328
329    randomization = SimulationRandomizationConfig(
330        command_delay_min=command_delay_min,
331        command_delay_max=command_delay_max,
332        joint_pos_delta_noise=joint_pos_delta_noise,
333        joint_pos_noise=joint_pos_noise,
334        joint_vel_noise=joint_vel_noise,
335    )
336
337    await serve(
338        model_name=model_name,
339        host=host,
340        port=port,
341        rendering=rendering,
342        physics=physics,
343        randomization=randomization,
344        mujoco_scene=mujoco_scene,
345    )
def main() -> None:
348def main() -> None:
349    asyncio.run(run_server())