From b0093d930992391eb0703b33aa15632982d1ca5d Mon Sep 17 00:00:00 2001 From: Lola Marrero <110120745+lola831@users.noreply.github.com> Date: Fri, 1 Nov 2024 09:21:04 -0700 Subject: [PATCH 01/73] Enhance CI Workflow for Scenic Simulators: Improved Volume Management, Reliability, and Cost Efficiency (#310) * Create and attach a new gp3 volume from the latest snapshot during EC2 instance setup. * Prevent broken pipe errors during CARLA connection. * Ensure EC2 instance stops even if tests fail, and perform volume cleanup to avoid AWS costs. * Increase CARLA connection timeouts to ensure tests pass reliably. --- .github/workflows/run-simulators.yml | 137 ++++++++++++++----------- tests/simulators/carla/test_actions.py | 14 +-- 2 files changed, 85 insertions(+), 66 deletions(-) diff --git a/.github/workflows/run-simulators.yml b/.github/workflows/run-simulators.yml index 885e386a4..3b04f79df 100644 --- a/.github/workflows/run-simulators.yml +++ b/.github/workflows/run-simulators.yml @@ -10,13 +10,42 @@ jobs: runs-on: ubuntu-latest concurrency: group: sim + outputs: + volume_id: ${{ steps.create_volume_step.outputs.volume_id }} + env: + INSTANCE_ID: ${{ secrets.AWS_EC2_INSTANCE_ID }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }} steps: + - name: Create Volume from Latest Snapshot and Attach to Instance + id: create_volume_step + run: | + # Retrieve the latest snapshot ID + LATEST_SNAPSHOT_ID=$(aws ec2 describe-snapshots --owner-ids self --query 'Snapshots | sort_by(@, &StartTime) | [-1].SnapshotId' --output text) + echo "Checking availability for snapshot: $LATEST_SNAPSHOT_ID" + + # Wait for the snapshot to complete + aws ec2 wait snapshot-completed --snapshot-ids $LATEST_SNAPSHOT_ID + echo "Snapshot is ready." + + # Create a new volume from the latest snapshot + volume_id=$(aws ec2 create-volume --snapshot-id $LATEST_SNAPSHOT_ID --availability-zone us-west-1b --volume-type gp3 --size 400 --throughput 250 --query "VolumeId" --output text) + echo "Created volume with ID: $volume_id" + + # Set volume_id as output + echo "volume_id=$volume_id" >> $GITHUB_OUTPUT + cat $GITHUB_OUTPUT + + # Wait until the volume is available + aws ec2 wait volume-available --volume-ids $volume_id + echo "Volume is now available" + + # Attach the volume to the instance + aws ec2 attach-volume --volume-id $volume_id --instance-id $INSTANCE_ID --device /dev/sda1 + echo "Volume $volume_id attached to instance $INSTANCE_ID as /dev/sda1" + - name: Start EC2 Instance - env: - INSTANCE_ID: ${{ secrets.AWS_EC2_INSTANCE_ID }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }} run: | # Get the instance state instance_state=$(aws ec2 describe-instances --instance-ids $INSTANCE_ID | jq -r '.Reservations[].Instances[].State.Name') @@ -27,7 +56,7 @@ jobs: sleep 10 instance_state=$(aws ec2 describe-instances --instance-ids $INSTANCE_ID | jq -r '.Reservations[].Instances[].State.Name') done - + # Check if instance state is "stopped" if [[ "$instance_state" == "stopped" ]]; then echo "Instance is stopped, starting it..." @@ -42,34 +71,17 @@ jobs: exit 1 fi - # wait for status checks to pass - TIMEOUT=300 # Timeout in seconds - START_TIME=$(date +%s) - END_TIME=$((START_TIME + TIMEOUT)) - while true; do - response=$(aws ec2 describe-instance-status --instance-ids $INSTANCE_ID) - system_status=$(echo "$response" | jq -r '.InstanceStatuses[0].SystemStatus.Status') - instance_status=$(echo "$response" | jq -r '.InstanceStatuses[0].InstanceStatus.Status') - - if [[ "$system_status" == "ok" && "$instance_status" == "ok" ]]; then - echo "Both SystemStatus and InstanceStatus are 'ok'" - exit 0 - fi - - CURRENT_TIME=$(date +%s) - if [[ "$CURRENT_TIME" -ge "$END_TIME" ]]; then - echo "Timeout: Both SystemStatus and InstanceStatus have not reached 'ok' state within $TIMEOUT seconds." - exit 1 - fi - - sleep 10 # Check status every 10 seconds - done + # Wait for instance status checks to pass + echo "Waiting for instance status checks to pass..." + aws ec2 wait instance-status-ok --instance-ids $INSTANCE_ID + echo "Instance is now ready for use." + check_simulator_version_updates: name: check_simulator_version_updates runs-on: ubuntu-latest needs: start_ec2_instance - steps: + steps: - name: Check for Simulator Version Updates env: PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} @@ -109,11 +121,11 @@ jobs: echo "NVIDIA Driver is not set" exit 1 fi - ' + ' - name: NVIDIA Driver is not set if: ${{ failure() }} run: | - echo "NVIDIA SMI is not working, please run the steps here on the instance:" + echo "NVIDIA SMI is not working, please run the steps here on the instance:" echo "https://scenic-lang.atlassian.net/wiki/spaces/KAN/pages/2785287/Setting+Up+AWS+VM?parentProduct=JSW&initialAllowedFeatures=byline-contributors.byline-extensions.page-comments.delete.page-reactions.inline-comments.non-licensed-share&themeState=dark%253Adark%2520light%253Alight%2520spacing%253Aspacing%2520colorMode%253Alight&locale=en-US#Install-NVIDIA-Drivers" run_carla_simulators: @@ -128,17 +140,17 @@ jobs: USER_NAME: ${{secrets.SSH_USERNAME}} run: | echo "$PRIVATE_KEY" > private_key && chmod 600 private_key - ssh -o StrictHostKeyChecking=no -i private_key ${USER_NAME}@${HOSTNAME} ' + ssh -o StrictHostKeyChecking=no -o ServerAliveInterval=60 -o ServerAliveCountMax=3 -i private_key ${USER_NAME}@${HOSTNAME} ' cd /home/ubuntu/actions/Scenic && source venv/bin/activate && carla_versions=($(find /software -maxdepth 1 -type d -name 'carla*')) && for version in "${carla_versions[@]}"; do - echo "============================= CARLA $version =============================" + echo "============================= CARLA $version =============================" export CARLA_ROOT="$version" pytest tests/simulators/carla done ' - + run_webots_simulators: name: run_webots_simulators runs-on: ubuntu-latest @@ -164,39 +176,44 @@ jobs: done kill %1 ' - + stop_ec2_instance: name: stop_ec2_instance runs-on: ubuntu-latest - needs: [run_carla_simulators, run_webots_simulators] - steps: + needs: [start_ec2_instance, check_simulator_version_updates, check_nvidia_smi, run_carla_simulators, run_webots_simulators] + if: always() + env: + VOLUME_ID: ${{ needs.start_ec2_instance.outputs.volume_id }} + INSTANCE_ID: ${{ secrets.AWS_EC2_INSTANCE_ID }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }} + steps: - name: Stop EC2 Instance - env: - INSTANCE_ID: ${{ secrets.AWS_EC2_INSTANCE_ID }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }} run: | - # Get the instance state + # Get the instance state and stop it if running instance_state=$(aws ec2 describe-instances --instance-ids $INSTANCE_ID | jq -r '.Reservations[].Instances[].State.Name') - - # If the machine is pending wait for it to fully start - while [ "$instance_state" == "pending" ]; do - echo "Instance is pending startup, waiting for it to fully start..." - sleep 10 - instance_state=$(aws ec2 describe-instances --instance-ids $INSTANCE_ID | jq -r '.Reservations[].Instances[].State.Name') - done - - # Check if instance state is "stopped" if [[ "$instance_state" == "running" ]]; then - echo "Instance is running, stopping it..." - aws ec2 stop-instances --instance-ids $INSTANCE_ID - elif [[ "$instance_state" == "stopping" ]]; then - echo "Instance is stopping..." + echo "Instance is running, stopping it..." + aws ec2 stop-instances --instance-ids $INSTANCE_ID + aws ec2 wait instance-stopped --instance-ids $INSTANCE_ID + echo "Instance has stopped." elif [[ "$instance_state" == "stopped" ]]; then - echo "Instance is already stopped..." - exit 0 + echo "Instance is already stopped." else - echo "Unknown instance state: $instance_state" - exit 1 + echo "Unexpected instance state: $instance_state" + exit 1 fi + + - name: Detach Volume + run: | + # Detach the volume + aws ec2 detach-volume --volume-id $VOLUME_ID + aws ec2 wait volume-available --volume-ids $VOLUME_ID + echo "Volume $VOLUME_ID detached." + + - name: Delete Volume + run: | + # Delete the volume after snapshot is complete + aws ec2 delete-volume --volume-id $VOLUME_ID + echo "Volume $VOLUME_ID deleted." diff --git a/tests/simulators/carla/test_actions.py b/tests/simulators/carla/test_actions.py index f0aede475..7914ad04a 100644 --- a/tests/simulators/carla/test_actions.py +++ b/tests/simulators/carla/test_actions.py @@ -43,19 +43,21 @@ def getCarlaSimulator(getAssetPath): f"bash {CARLA_ROOT}/CarlaUE4.sh -RenderOffScreen", shell=True ) - for _ in range(30): + for _ in range(180): if isCarlaServerRunning(): break time.sleep(1) + else: + pytest.fail("Unable to connect to CARLA.") # Extra 5 seconds to ensure server startup - time.sleep(5) + time.sleep(10) base = getAssetPath("maps/CARLA") def _getCarlaSimulator(town): path = os.path.join(base, f"{town}.xodr") - simulator = CarlaSimulator(map_path=path, carla_map=town) + simulator = CarlaSimulator(map_path=path, carla_map=town, timeout=180) return simulator, town, path yield _getCarlaSimulator @@ -76,7 +78,7 @@ def test_throttle(getCarlaSimulator): behavior DriveWithThrottle(): while True: take SetThrottleAction(1) - + ego = new Car at (369, -326), with behavior DriveWithThrottle record ego.speed as CarSpeed terminate after 5 steps @@ -109,8 +111,8 @@ def test_brake(getCarlaSimulator): do DriveWithThrottle() for 2 steps do Brake() for 6 steps - ego = new Car at (369, -326), - with blueprint 'vehicle.toyota.prius', + ego = new Car at (369, -326), + with blueprint 'vehicle.toyota.prius', with behavior DriveThenBrake record final ego.speed as CarSpeed terminate after 8 steps From 4ae8ee2f254e82675d2625e081cd157d19356e3d Mon Sep 17 00:00:00 2001 From: Eric Vin <8935814+Eric-Vin@users.noreply.github.com> Date: Tue, 5 Nov 2024 14:18:25 -0800 Subject: [PATCH 02/73] Newtonian Simulator Fixes and Cleanup (#309) * Fixed scaling-warp issues. * Fixed wobble, cleaned up, and added debug render mode. * Added per car width/height scaling. * Moved scenicToScreenVal int cast. * Added clarification comment. --- src/scenic/simulators/newtonian/car.png | Bin 73799 -> 68790 bytes .../simulators/newtonian/driving_model.scenic | 4 +- src/scenic/simulators/newtonian/simulator.py | 83 +++++++++++++----- 3 files changed, 62 insertions(+), 25 deletions(-) diff --git a/src/scenic/simulators/newtonian/car.png b/src/scenic/simulators/newtonian/car.png index 46f39d574c401515262c9cccf5025575a101cc92..a6f7d014d063c02dddab28c13fbeb222713dc621 100644 GIT binary patch literal 68790 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 AxDcKsCq;LkQtqN?FYz;`RWYX)FP-eSHT6;MK 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 D8t+mw literal 73799 zcmd>mbySp5_bxLuNQ#uiP}1EY3_Wy8cO%^)B_S!@AV`a(q_l+6UDDmsB{6UZ)bDry zxZhp(|2u23c;{W`ocEl)pZ)CT?DImnvZ6Ew8ZjCi92|xWL_!q~4j%OIgMtLRGv|n8 z1^Y$hDyikF=3wFKVeD)UCu-(kVh)nAGqyBWH8(c%a_Th~g7tV|t*+&&r64b0>R`uW z{LqHQ)6Nmr8xBrL#M9B()W+NuWMXboU*52i>nP3KE^)z;5WoKb~ zSkk|S3JU-CrgnD!Y3<^wYW_d-{XY)uqVDBr&Z=te;^5|N3UizV)x)D41;m}rja?m_ z)g2sc|5;Jl%E8sa#md1EBrg7FH4vSqwY`~xhYS5<9|Z*g8G9F3V|!C`8Htyauu)j7 zt<413*g1Hlc*Xd+xY%JINgh#AQ7&-_Nq$iYem-$7K8b&QB^*rM?9A<5|MfNdpT461 zweJHQ>>OcFmN0j=b~iVZa(1u-JuX|o`oGVG_rH$!x3AfMp9|lA?aKUPeMx-E;O=3vvLpwA=mt>#@zbxrKiI zZE9>W9O^(g03}MC1U8Z}*gx&4lY{VS6nm;sRMdM>v$wuKe^67rg}fk)GRv)UMWg-T z+6dB*&KM8Ao8CJ}+fkl0F0^!T-@J+2_4W`OwjGvN@V#!)qdg>deE|@pK;Kv?gSz})m?G3E! zi+=hU-&KR}njrR}>vc}Xz`2pp7imA0W~g0}a+ekX5` z-_y-qnl~x-_2lt+!xs7AV-$#uoOJelvrZjv&qe9U#)qo1k9f=}xnI=lFx4cUs7W<5 zPmaq?EJOyO%5x-`CT0 zLasoZ7a)02S;!Z7+_7@k0eRJWap7E)uv3m?oahMK6uTH`(;&H7lTO{CLtUR7+`l2m z)>qp%h*SD6G^@(C`WpwxRcjRMh9Zo#g4$7axvoi7V(EY#b3T^5gCNq9{p}AV1qVCf zJyq?xJg8ZekwyK)pmN5l;M9tYD#>-@DlrNkL9?40+xylTH}-%4I5+?R=CpK4p`Qrl ziTf|R&et}c{2R#Yf(`4nIGoW_6auO7%VUz)&l}5P?0r*mgmfFbZ3$i zzpt4UKlMSh+=}VpDbda`t|cOpnbd>NZL$|g%3X0y|FP~aTZe1#*$WxnTB#TUt-_GG z+Ga_5os6+yUR`HIGA@MdhlLf-tqqixztE^yb(0iknbYLeZ36rUnnB-#uvYheiT-S? z@^t+CN(J*!OXvmZ*q7TDS95B_b}g9OS& zLyxo@Z>p)&e8_M+N(+Y9ogx~c>okW|5d!k9$|db5os773(><28#8Ul>;ULKlyF`21z8(rVSy^PkOHD?w%g!`A)N{*cgv zH$R)PB5{hWNkKMo;u*STZ>BA5-irUn{nXTCCB1$AZHfMeU(mZ7a!rFv&r;Q2C=@lH z@w*;m8%o(^Us0d>aptYL?lUflHrY2ORh?WaoCq&mR1yJ!0PDi7)Owm=bLzQqN!IIm zW(G^n>C&a;M4P+hMyu%?tWjQ8KSt8VdtETa$U< z+werw@3H7*-m%Y=FWT~B{Ea%;Lc%G?hm$f```ppUYm+WYHK^3NkYAf0d;Yjt5<}Nu zby>JPn%Xb)i8|o7l9J<*z3S;AOVva^7or+X)gG~bzl)aT8*3t$|9OtBX8O&g=H7Yl zemBZ|+oc1IGXbTi4J)57-4>BgiTz&7t;7<`uREV7p2R#^EuT3l==l|=(m}Tdy3VsN z8qXR_25UMv)TBdqu(I#4I2ly&ajL$5&pHd(o4ePO?Rmc~^GNNm_*w=9MJJ|vn;9~>IhqajIoM$vEmkK!nQ<$qZLQsXwEH*c0f5(6o@Mhs$=yzwI#r^W#ld)Q zPf9gxvix{jmd6i^HY~G0H{bg%-8o#ZWc!W`Bp6cHnwl?jH61Kr^>+VcCb!8Z-8t&8 zJmgP`OfXhmoUUze>u!o+ryZ8LROAu!bEutj>@uFU=ggXOR5xq660XB1E~TM~Qmg8- zd7J5TT)o|p?Mc8ad`>;RMlw^EaJTjHUSiJwPWRL!`ohV#NBycy;cRG7<2}lMoNs%) zto(z!D(zLh+&Xr4(G+R*cp2Z=adXL{f|BQ;WZw<>&l3=Ix8gp;{kDP|2fO0%ixw5R zB49;y4f6`6$t>gKSAu1q&Wq^*FM|NU^A z;P6^n;QEN=)NMt3b?;l%jfzrM0|)_>_Aad7n`rk5^>j`pxkY*EBEeZkauX>P;xSCVcg6=}(@I*=nd zHZ}_6_r8>^)6bpQxF}|dm?BRVoaFGJy7{AFeR1@ij*`2Gp~l;1<--zA7E|*5*PU&* zf=MTcr^i|mzY4}6cOi8wh3R*ajz)F{8Uh z9}0+>M?Pd!6dC$1b8`9mObqyLed_&aTO0rooOZEISas^jA@wm-oYY}-V8^3md6-&R z{hD{~zFC2Kl1+q0pZn*lMGS&i=d~N>FIPA>-giFDXNLD#?-UKTuER*0FDb5%GejhA z4!$n=yq8t~pf7i#KXXU^TKr!C)g<^FJ<&8wv6CFiD)>xypRoyW5Rg>zKYVspiq*1^ zxzpGGMVhRD+~5viQ()B_kcgZd^HICp)9tLx8O6Pef=tMC)LTtR)xuYr3TYEIzp(nj zS;~%TJ%!mzV}MsK{mSTg6<_X`j8cIZTMycoTWkI@Tl!u^9vqXRZp9M*w*&rT@5%3) ziHKH|o^BZ`Xlc5*Pd(2Mz9oK%i|{C7uwymwvB>uP#p*mx4>!b)aTro9t-MHMfqg(CT~tZNf__i(djSv>WcW}C#~Hv-!gWLf#6`|c&X|aCp&<1e*VWU2!Lz+lBQm(`lmGEAHm zdLJcP8=WP(%Pl$xKk?Y;k{s*ZYN+p19=t-!?UchCyao3`5Bap=*%+G&4bKeQiW%Fa zk_Qp~nE#n6$SdNX^S9<8wz&6AB0RI!z|3}TSx^mOL(%u_2 z9+FSusc#fL5$4=gDa{m(c{g4L>jxAZ0KJHLnUtePH8 zG;MH$hdev`wi?@4DR@x=&QjF<~nGvWNFKw^b2djPx&8M&rmK8H;>s9}ouM zZ2;Gt3qu8c)nqd2ov$)a7M8tnFm!YO#_0>cG?=f$^Lo#4^*ZX<VKeYTx zuv^Am`MZE0?Oe&6^&oas2rP}kOq6zia{Q!fidd^}_1j5Y$|^hkv$vKF-ix$L;!0jy z&l6#uTVUOmc7O_|lV-4u7bVNa_3){dX9gWR@y|CJ9(YWU1s)5K74^(r-V~09z%aF$ z5|HT7eY(CVK9x@CQ>v7_?<)K>T)zbEH2krMM9 z?Sf9dBEeoc7vmBC2l_!evR?qres`Mtq!M~@cxO+67oqZq{3JSG?P*#LXs1)Xb_`y` zkQD71-q_E&jh7XInc^?^r@y}as+Mk8b}_1?+&2nWl?O4`K*Fb z{%iT!q#6LIYVanQ#w>lq{K|x$LUa7{>5!u@QUlAnV2Yj$^5KjHe#@zFz2@bq_RM0- z%n9K?lE|lD&&w`NEZK5uobaZ{_IZ)C5oC(jEZlBWe$V8F!F;xG`E3CUwmxs#_rDft zk%a^KUdK=NbWt#j206$0;PaVcejD3F+dY4h|Df@%C_x)P7*U*kQ9^0?>>@M=iBM*c zGvKAU`5NO6dPe2iPS}?QT&}|DukW&3W9p#4rA}>uhi{(0+7MuOExx(sy9`8XZ%zlE4LuCf4W8An7=Vu(K3EK7VqYz*pWP?heFyUE zt<*4z-|>oixm2F6*SAF*{7$<}fE4&#LKkT7ll-7L!)cMZ$G$iF-dKl5Z~9ocwyfQ~ zSepY@svs&@C@8S4G-AHb^Q`@dJUo&Wy5Yo0nRYh{J+q-v?5V5nmfME2h>H8C=8Xb+ z*~3ddxBaf;(rs3)tt9#@*53Q)Vi&WxRfvxp!-u%Ex$-QZp9krZOS5S7B&Ud{(^szs zZ3o8a$qe7l)n6TG_7QA{I^2O4ZU(b|24!%$2?lrY{}g^*q<6ghxx4&)JA&jq02Yoq zhG5VB(eO9DvK6b>8080AxFFC53R(t25ofv2$=}~{ftIJIU z93~3nVh&bTkqH`AF+Yrdwi-zCX%@3@?OY=@Nb`xa&2;4M?IgP5HMkAI8s&FrnDA)Z zSz&LyNb(nL@^@(>8YIk*VtxO4z`mF?W`q@yMNC^E7x-U4?zaVp2hz+Up`c+lFI(sn_$dkPyTx>~cy@=|UzgiYMm)z_K_M=!69fh74CS#Qo z&zO~4WW9JRjumjXj->4dYJM%?Z|~mc&Z&(~yt}MZmSpxU1M9F#I6DTq!ZK~gC%HE{ z?e}i1feE93i6EByCb<&d`abK|6PE5#WEeHU!M#t6xu@m5e}PO+pNwTtl9%cJ{luxa z%}~-({|;>lifQW=+vQqn9&*;gjolAk`;zv+C}a(Tz=$Qw9I6#b47lY?E?vlQe-Ef*$BQ1?Yobm*4Lk{OHh6#ZfKNRpY`W zo@47##E<1LlpI^*cAjE(f3(GS0xL+Nz=340Jlp1PpL!7X7Z4uh`b+8xf8AUwFodS6 z3fCFjqhC)&E=h4xty$!FwY^A+n7;hFq{rE%^Ud*t6znu(v~9itDPM^D5NcMIPo}=} zjhl=_bS3wcyzRX6EL6#N&<)y%K2)ITw`Vxu%9I>i3MH%y_Gv$$g{>SGDxY+P7b$o( zoh-jhun=#ZzqA;9jgd>>cMWy@@!a5`X-VJkhRpZy{E$0R^UVf!LA{Xt@lonT^nTTG zEp0E9RjM8VY~uL7d#8@c@$K~oi3?gy1y*fNE7Z=9hS0$xn7F$w-Mda;^}}zmJrzk+ zQA)-x=XH`0E02eMPvYBsT>nS1tL4DcVQ1+tKNZ7Q-Gomru-_HUzP#+~_qn`Tv%CxP zyRC~!wN$+scm+dYyQPT-pDbP5W zfH4L4c6#q>$Ixfq4QlOwLg`W{813h*fKp^?S1Q}HrIL#In805r#fZdZR5&wCd?e9X z|Ha)~_KH6&>uhS8Zu;u$ac6dsVe6sVCeeg2r^)J}MJc^Fy2b7@xo#O%VdX;rcx|~6 z+kiYcasxQ2hf4+uZO|^w0o}`VoTvT- zoo{}=*f7;P2(|pG<1iG42xrBW$>NsIjIWnDw4>DtsJASplNPhT@2iEe|B{kav3kQD z5xFPhhr1$HZY8?P0au)9$23lxWBoh13##D`l|K=vvc48>#>|qu@6r=mxna8VRH;zY z_r?&CuPS`?8!7_Tl@zXuI4Cgc?@pZ`HfcC~MaP3|q_{HtoS?9xN+iak-<@yyLi@9n z*a37(X&X7k{~HYjE3~iMYM(RPOtyzw+a`jkV(~n*O2i8oE%1msl=zru&ZnF+_(xn# zq;+KZ11pl@-pNnKZ2W)I^qrq0^hj&G4$%=NOT1TvTrE$Ir%)aMzM-08Xw$3R_O>e3 z1>sOr+sXAjB&SJH2_Er^hC3IoO5@rqqHKziSl>7|Zc88NZA3%@4CF8?FDGLu-n5F= zCF$WUs)YLx76zo+`1(K-YK+b~%6!gxa%6R?8?SiYypK8A-GK{uimux(b1|%IK1bj< z7sdQ+!ij{J;jLKnkFP; zOrg@;Q%m3N{hE~qCAWo@dQD&MT%UF8+2j~MS3x)~j7VG56{Q1l3fy=%NROT29nZqq zD62-fuKVWNPVTVZ$CI?J)iXfV&}+z0^$h{sBT$*jLEc64%@iB4ST^5OxR75wXYpZp zc?$2~wy+gl;g@26+ccae;(^@L-Ki>UvF>Qr&@4&bNCKCRX#ns|V5Y#5j*X?bscXCX zow75<>R0W&pnbo@xC5)UbS1{*`p#f$k3Q{vzRw*3Gg=-jMUbCSH(XiXr4M#5_?;gP^56Fh9YGDpl$MU2bcH!&ua*Hl?t&-n=VULp?cuC+kVtT=UIqp~ znXQL@ZxWH2^Yby?XpAhs)7{l==t{)yY#j_}+3=IP;N4obf`? zfr*nzJSsXR&pUj-b=kNpd@)}YV(&SAGbo89sBooRar&h*87pi5^K*R+I0Y zAh~puVi#B+dF!w1(cWO8Bbhe(5_xA<^A_=0p~XNx3Q$JVNsq4MK9>XsjFCoqffD=hg=3R-*r)R}c0mmJO?H+b`9FJW9X$vYOq zy$$}+eY5Ww!wgt>E7r0S*P{na8^#lYc7;AMda|ahgxBO zx?IJ4lj|#F(v%q})?=VqQC%(1j@Xncep|pu%p)&qy+*8AuTeT zS9}tZ0xTPOng`WMW972+u1Oz#5rS75F$`{k#_7pp?F5$Vd@qbl_Ac*259WPZ+-CY{ zT!%7P-7vVm8t#-Y?x!=BhCWpP;o$5#UR5!_(pM>m^4axzLp7IGi5ZHm(MOQn&RNIw|9vxtt z_u9CDpq%CK%bUcqX=q@p&yRkqdwt!Vd;;N4-k&|M-emFi-)%EBzk#z7;eeej)+G5H zUEXb7-Lz5r%U>bkOdXk=Y_s`VKwSp6k<)@OTQhWo(N1QiH`;vkOB8DvpV$PeKx`%dE$b? zP34~vD<0g;pFd`UIPD{UY!cF{dNGxo8E9@KVDNE5&A7;ji1~)sxhq$Xy;Z!f7$qnY z@0F4(%KPA-J_v`D4`djCzmAr>)j#M(qbe=p8Ng1_6pX{qlp5~H;*lxqDvt*FQT+~5 zf`$@AhoT%;zeG^R1bR}j(e6ezA1Y+)(77S+XMfwF5{H{{`s)t$QK8ikI&Ar)5o<4} z*%z+6sk~FxyZo}Ws8)!P3&h68W&&yOn}7a}7;FpZjeQ1~d3r3&#K74&oLuA&e8eSl z$1@4W_08`{l7cowNJR$6eq(kMA(g>+9N5^)mOLNquC_xH40K7pl9@5B`u^Tt4{|s$ zY0CcBIlGFLnq3;J!dckbq9wAfBn*szK!-@35Kbs)`{Av45%g5?^L_q1-}Nzf5yZ!6 z*9j*-4N-Q8_qXxp9_%7#9wX3c?sHu~1|h-nF{8R-4_JtatfhzBXl`4Y?lRNX0h zECqcN+Cs|}ttwh9yOiHxZ$K^yYk?xI(7%b}PsWDpTi?GFZ$gD=ixFm7)h|AC{*`xG z!&r*=6Yx+)f^`+TE1iB0O-&XLmeB>z#=WKxXjE9nj+F&?-}rL;PKd4BF!tvFb~FhQ z95OsQ>T3AuRknAibf*On|FGz#TC7AbG37I5WFQ`pa+VF)VfJg^3C-|1bzlHn(V9Nu zLpEi*=Wj3otb9j0Z6@}7&e|1P?D;lA`vw}zk~Dikn23xUnt&6Va9j?2(3h$AOea3= z-R}rUU0LX+rhtLO*vp{o*V)|Qnrw_Wg0kn+f!MGA9JuKOE_x%F72-mzPKU;Zj<>X9 zU{v%WU|^=AuR0esZ{ngm=q7dL+G9M_1OIWkx3p@*A87a2%fvFsB`l*wcENB79?e*@ z{J>Re0H8e-{gEROQ%ldIOUXz8^f)R_*-LKd5Tru}Wtp}r1Oy!HyWpenh@ES9&Y*u| zcFXtUb1#f$U7>c5(11R{12(rhDRQk*AT)z6xvdNGD<2+>Ffz3ncvcb-kVpay#A5;< z;4y98y zNh(BkMQ))|^HjjM4+raTR?I&fQE+$J^sTk9W9#r^@d}siuoGZB_0aW{+y$fkt0iGb z9-yhy^;vG-jjav9NeV10NrJ7^rqW$xiZ6}xXskixr{A1}NN}(x0%~{>r-?yf0etPW z@Q)!FHL)m)5*Bvq$wvvI83>-NBiwwVHCdq~0mdcOidsVf2KWKtMu2MtI)`*Zb9IMo zFTelzj@LNiPJI~eSAYX>I)CaW1+M&s{op)s2?~T6H!d*XE3y_%eE=TYQ;j!hVOwrW zNo?rJay^c)GjLd|*{pQ6N(9x)dD6lyd0vZ-*J^-cNiYb6iq=CH4SHw+CxC(ix$;bu zg~hQ>@Cb<`6)=AN8xZ*^+}TqY4Sn2Xb)5+Cj1Hv*s(w>%-lqpG;xS2p^`HnfB#(=l z$Q%nm30qA~tRyrR>_pp@Zx=i3RASCXlP2d3q(XVX`v_%N`auvewqemuWFS~<7WHuN z{dG(iGJdM+lgFoM5OPk=iGsl--TA@Ao^wPF00E0{pW~I$@L|0l(iGw+FJsn;0J`XK zULefBbtY4~0nsR7F#;kukoLzgu}XXEqCko;(di)%vxKhNkwh~`SO7IM=+6!bF||xn z=OaaQ`!m(bSv{~2H>iLTGomQ_lN#6&40tF*>IC$9BjEvos|Q>!>3JpPzt3r`s|1r3 z4}S_`Cz1Ta$;5=29|4j!Zs}Pl5SjcQY(BAlxV9n56@~T=)Hf-yD==$52-#kF-wz0d zs~EW0NY+~Wq6R95L2h7Nf>@zw%0FRe$q=px1m>o|%JOfPnZcnx)U=@JJ9FS)3`u@^ z1saioEtd9>TO$Hb-7)W-9*Metp9rDp_0_fJ8@ATzY>3|Q_=o*ysLL*UdAOeEEwP0*FG027D zpNT$|e0uspf(yXzsHH>f=lccCn-6s&09Js0>^R`Rn?9xb5veQm^)Y|GaV^j1aHKIh zF^8^?Kv?u#!(I;*Kn)D=j6t0OT8l)&29Ep2r1fb003vbPIHnG^0VII6AwZ^gTErZ5 zJVa}Leh8G@{u5pw2AyqfG1F}hsAB^QNUU>Ru2j-F}Ps}LEFaKRKW`Q>n(l#AlHv z&r@vq_U8;6s3aDDyb5RJf%ggi7^6-IekA^z4yY`GV(CiqEnX@ALvTExSL{h%9>lZq zRTm8YVxq^577^I7NGLsof4m8fRJoDYI$vm`5md^1K(9LZx0@!j)J%Dz2qJ1J09f*e zUMO~nnNdH5;^O7jM+SU^dpWiR2!(r$AU;Bc+`NEy!Kf(jjLINnh}$kSAS%9yN6WyG zN`4IZIvkvW5UtLr3I<&3p`vU$!NIMe!GRgy=frQ*A^bolgP}n~*T*w9@L4Z0NAEt& z7Z09>TRV5dgS4uW5tJg1y#NNHK&UAy{0aUA3^yCPMh85L1b9{gJO(>O$ANK5Mr=C; z+();s%Yl}cJ%fh57v3{G$=wKQx(9{jYuz@BtnVhh0mk%P=r{7}tk8Ict`>Lktb_?4G@*J(q@X17y)Dz^($c@?hWC#r%zX6g z#sejRq>`&Jc~+^Dp1XA}0ca%eG=74y8t#6N^q3LSL(o|~FZf~aNx5LhD&7#oqNSK; z`9+XG>=caF5vX^Kau@GJdrZrIwJ{kOUAp{X2yK$CTW~+C7b8%(IvJ)tnh+R*0+CZWYR(D^ z;BsV(lV!YNeLIW?x6KI$H^>b7%o&y_qq&|lwIiwSGg2${S(ZqQ&QUi?WK5j$gB-1JO2&sT$I(#-@<@*)dQQ}y6sfGWo zeWE2cY&dbJazvA|cPx@4;M}3+6s&es&B;q&OknQ)6Ig)4H1`7#2gCuWeL3Y7NL~D0 zKLWl@#(ATSda-xZylvmTCBQz!Yepo&!*XFq|3~({h!Xsf@+_K{t#Mj-BXQS-tIrad zSH2E-prr=#a`Z}ot5Ojkm5Gr78l>?Hy%r%FU38PN@@MtW6aF8uTeuxtqlLz``@TKV zGQfQwV8*$7hYq8thftme#@mk&NjxbITB%;+Oe*$dH+qco+Of} zVlCOSndiq~9aEkFsul(V)Z~`&dP&em;EnA*)&J^E#FF@9@y+ajPeUN$6S)=xYwXw< za3*3OcI?!*XAiuNdw`{yC~ub<3rz(7$$v(7s1P0Y7{B7Ygm?%@rr%f!fR=k8ua5%* zX6?HGb*fM;#Fk_DaP@Ndy#TlXKUhTo3nN(7+6_%3ZdXJu+}Mpx2r^hS4x4>cfK~-n zks-zP7H#h}R<(azHmUi2$nevLMSxG^$0We&UXw6>bNz%3!xu@f7~q-&HSAZBI#d%a zoAwWgrJe%V8wH-?VrL>_2JV3iQO1wz&?Il%B%s|Ss1Qq9%0GJR6hj}O3wz4^-0yvn zQ&^Co`kO*(aWZT1R{Ul#MQXMqUVo(sJ83F_Q5hV+OYrp$9>zcqTc4|U5PHjYS@U=z zN)RGrYN;i|A|C9}8685DZ!|av4$LF}d>6@93=g*rqlH0q%cxIN0Cs@{`F7y!8dV6D zUE;t@!q)&I(qFd)Lh$%)S@R#`{o)ndWj);JZ&=N^9#aVyXCn%vLvp)=#M?;y<1h6{ zNd^m5cL3*5=CC0^^mYM<-_vSlp`x1>dcB8bpd-`7Bd}Qh1T+P_{_Nl2pW42S z&*)L!NaJ|(?Wp!LNwHEm_!@L_gYmvv8_g$U3t z02*DUL$OduUrJmLMIDW?0(L;faFaNSqZMmyzlr?e&KZ#j9Bf%S#Qymtem;0nM^coC zk*%v`omRgOh$xWMfc&RtL3pZ_a8}P_so=nJL}6^hAa7lU96Y9y@oz?;s7#WtxhTGV zoxpGuNTm;(OxE}HnQL_@L18=a<2ek@0aGVfxG+|0SVssR_B7GLeQO zr0D{7wnr{({xULRuv#VYt=;=dYXlk2QZZLHfBK5oF`7=8h!W!-6n{A2V=;yV9TjNv z)eIBS)%7%2B|a6`z`Zgl@koOSpvemZAC##Jz}cy(I}}6tqb1XzLBJu(69OOD79Rz| zRh8Q6=7$bJ1>#DA3@7l)5-*Z+LNc~=1%h{c_39m zCdA-{HUtn-9_U0zWn&X@pb1H{U1+g%nQN`3Ysb*71vlnee2MM`65K{C1#M zo})Mzi4+!_;c-WBfj7*)OCf6*kCfVVhocN1|1sc52Sg*UiUywR3!TKG{^@}@w2Tb@ zcLDIDTPl`~O;7Q#bA^mQAi&LPgX2lS`Av;qF#oXUty}Ce>mGG$ANw;_*a@wKTIKI` zR0;S8%p3XjNz3vk$O|+Chr618u#*3d4sKF;9UEYE!xD!2NLx?QA>UHm3adFq0bjsq z_;CBuus>bMq-3apZT$X(4Tgk2KKqV#C2SMO3<_85eJSlr@L1l1EQ-`v;;5yq3m;f# zCS(%G&)NN`F5?M8bJne_V4O)gJ@o1pZVpn~GH-5-0(o$ij<+&dVyO!5+lAU{-$WLT z+v`VXw_tG5Zs3*L#`_Y$sNh38RyWDxW4#cA&n$8J>9rQyP3(ZGq)~wlAsa^WbxvMq z-Gj`d`KE%_hC?C*TgzASJXUSDWP5RC(~HUen+@~VX1dEiO`HZA3UKJPW)AFqF;Eme z9~~KhhlGG(s!@kCBelWikbhE5L&8L+7H2-OyQ~)RLP+F|?&lMM5`#A)1p)oAx1P$s z&B#V?LR-xG$3HD}Y*$I<&tIGC(#-`xT`(O=3JLy5TsA{wq<5kPxtq-%487Ww;Fs$P zU)Z+4&SPpybj~VD^IcdsowD3#s@@8vL=e7Ge@(QFPb#(wZ7^~u`sL0BR>q&@;4rEe zeUtHh?u!LLrV&YE+QX^suNq5?zs+T_YVDHbX~@y|%z+KVr>G;>TB?v#XPs4I{TjuG zs`>)bvJcU9VZUk2`SSCbUJPmACqg4`F8IyzoAnXcQ4cJM*#fW|FzuFJ1^&?#T&zBY zm7k!vtTY#VbYhoS0sJp_zYJHhIrbyYXop&HLF3-5;s|xaORdD#f<);F zrc>&@Zrs-DbSYkl*F`;Y!zwg+T%luy~>0Bl;g10&aBYS zi|3}Ysy>#MCAwttLjynOtoJNio9EZfO@7Rdty7OqBrdEEkB?8;?PB8{8mgV3pw?@L zrv5tdF|LYYs+-H@Ns50r5fG*x5ReGl%)@Abt1JM< zlmss=F}*NV#d<+oy5GeYj_Nh@JN5i#JIq$~>FL!`(U#|avkAu@c6?PP3AIO=%oIM3 z<(~cVutYH+2w4$F@-MFmqd~$E^FNYtQTyP>S#~hMit54l={UL^(K9%hlU&vO(u@^$ zDg;7cazbjIuOsusOvF&ADΜ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

@F$zXlNSJ?x9zYw1?LYPockbWwYmYB(6adk^I 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 X$aytzuW>YMeYC5iy93+q=L`P^EnoQa diff --git a/src/scenic/simulators/newtonian/driving_model.scenic b/src/scenic/simulators/newtonian/driving_model.scenic index 1c01ccab2..a976dbdd0 100644 --- a/src/scenic/simulators/newtonian/driving_model.scenic +++ b/src/scenic/simulators/newtonian/driving_model.scenic @@ -14,7 +14,9 @@ from scenic.domains.driving.model import * # includes basic actions and behavio from scenic.simulators.utils.colors import Color -simulator NewtonianSimulator(network, render=render) +param debugRender = False + +simulator NewtonianSimulator(network, render=render, debug_render=globalParameters.debugRender) class NewtonianActor(DrivingObject): throttle: 0 diff --git a/src/scenic/simulators/newtonian/simulator.py b/src/scenic/simulators/newtonian/simulator.py index fd38aa427..193103ab7 100644 --- a/src/scenic/simulators/newtonian/simulator.py +++ b/src/scenic/simulators/newtonian/simulator.py @@ -5,6 +5,7 @@ from math import copysign, degrees, radians, sin import os import pathlib +import statistics import time from PIL import Image @@ -58,15 +59,16 @@ class NewtonianSimulator(DrivingSimulator): when not otherwise specified is still 0.1 seconds. """ - def __init__(self, network=None, render=False, export_gif=False): + def __init__(self, network=None, render=False, debug_render=False, export_gif=False): super().__init__() self.export_gif = export_gif self.render = render + self.debug_render = debug_render self.network = network def createSimulation(self, scene, **kwargs): simulation = NewtonianSimulation( - scene, self.network, self.render, self.export_gif, **kwargs + scene, self.network, self.render, self.export_gif, self.debug_render, **kwargs ) if self.export_gif and self.render: simulation.generate_gif("simulation.gif") @@ -76,11 +78,14 @@ def createSimulation(self, scene, **kwargs): class NewtonianSimulation(DrivingSimulation): """Implementation of `Simulation` for the Newtonian simulator.""" - def __init__(self, scene, network, render, export_gif, timestep, **kwargs): + def __init__( + self, scene, network, render, export_gif, debug_render, timestep, **kwargs + ): self.export_gif = export_gif self.render = render self.network = network self.frames = [] + self.debug_render = debug_render if timestep is None: timestep = 0.1 @@ -102,10 +107,31 @@ def setup(self): ) self.screen.fill((255, 255, 255)) x, y, _ = self.objects[0].position - self.min_x, self.max_x = min_x - 50, max_x + 50 - self.min_y, self.max_y = min_y - 50, max_y + 50 + self.min_x, self.max_x = min_x - 40, max_x + 40 + self.min_y, self.max_y = min_y - 40, max_y + 40 self.size_x = self.max_x - self.min_x self.size_y = self.max_y - self.min_y + + # Generate a uniform screen scaling (applied to width and height) + # that includes all of both dimensions. + self.screenScaling = min(WIDTH / self.size_x, HEIGHT / self.size_y) + + # Calculate a screen translation that brings the mean vehicle + # position to the center of the screen. + + # N.B. screenTranslation is initialized to (0, 0) here intentionally. + # so that the actual screenTranslation can be set later based off what + # was computed with this null value. + self.screenTranslation = (0, 0) + + scaled_positions = map( + lambda x: self.scenicToScreenVal(x.position), self.objects + ) + mean_x, mean_y = map(statistics.mean, zip(*scaled_positions)) + + self.screenTranslation = (WIDTH / 2 - mean_x, HEIGHT / 2 - mean_y) + + # Create screen polygon to avoid rendering entirely invisible images self.screen_poly = shapely.geometry.Polygon( ( (self.min_x, self.min_y), @@ -117,9 +143,7 @@ def setup(self): img_path = os.path.join(current_dir, "car.png") self.car = pygame.image.load(img_path) - self.car_width = int(3.5 * WIDTH / self.size_x) - self.car_height = self.car_width - self.car = pygame.transform.scale(self.car, (self.car_width, self.car_height)) + self.parse_network() self.draw_objects() @@ -149,9 +173,14 @@ def addRegion(region, color, width=1): def scenicToScreenVal(self, pos): x, y = pos[:2] - x_prop = (x - self.min_x) / self.size_x - y_prop = (y - self.min_y) / self.size_y - return int(x_prop * WIDTH), HEIGHT - 1 - int(y_prop * HEIGHT) + + screen_x = (x - self.min_x) * self.screenScaling + screen_y = HEIGHT - 1 - (y - self.min_y) * self.screenScaling + + screen_x = screen_x + self.screenTranslation[0] + screen_y = screen_y + self.screenTranslation[1] + + return int(screen_x), int(screen_y) def createObjectInSimulator(self, obj): # Set actor's initial speed @@ -207,21 +236,14 @@ def draw_objects(self): for i, obj in enumerate(self.objects): color = (255, 0, 0) if i == 0 else (0, 0, 255) - h, w = obj.length, obj.width - pos_vec = Vector(-1.75, 1.75) - neg_vec = Vector(w / 2, h / 2) - heading_vec = Vector(0, 10).rotatedBy(obj.heading) - dx, dy = int(heading_vec.x), -int(heading_vec.y) - x, y = self.scenicToScreenVal(obj.position) - rect_x, rect_y = self.scenicToScreenVal(obj.position + pos_vec) + + if self.debug_render: + self.draw_rect(obj, color) + if hasattr(obj, "isCar") and obj.isCar: - self.rotated_car = pygame.transform.rotate( - self.car, math.degrees(obj.heading) - ) - self.screen.blit(self.rotated_car, (rect_x, rect_y)) + self.draw_car(obj) else: - corners = [self.scenicToScreenVal(corner) for corner in obj._corners2D] - pygame.draw.polygon(self.screen, color, corners) + self.draw_rect(obj, color) pygame.display.update() @@ -232,6 +254,19 @@ def draw_objects(self): time.sleep(self.timestep) + def draw_rect(self, obj, color): + corners = [self.scenicToScreenVal(corner) for corner in obj._corners2D] + pygame.draw.polygon(self.screen, color, corners) + + def draw_car(self, obj): + car_width = int(obj.width * self.screenScaling) + car_height = int(obj.height * self.screenScaling) + scaled_car = pygame.transform.scale(self.car, (car_width, car_height)) + rotated_car = pygame.transform.rotate(scaled_car, math.degrees(obj.heading)) + car_rect = rotated_car.get_rect() + car_rect.center = self.scenicToScreenVal(obj.position) + self.screen.blit(rotated_car, car_rect) + def generate_gif(self, filename="simulation.gif"): imgs = [Image.fromarray(frame) for frame in self.frames] imgs[0].save(filename, save_all=True, append_images=imgs[1:], duration=50, loop=0) From fe28e13b52c220a04e46fecc78370560470c4684 Mon Sep 17 00:00:00 2001 From: Daniel Fremont Date: Tue, 26 Nov 2024 11:13:49 -0800 Subject: [PATCH 03/73] Fix requirements inside loops and functions (#316) * fix require statements inside loops * fix require statements inside functions * fix RectangularRegion with random coerced parameter * fix require statements with random closure variables --- src/scenic/core/dynamics/scenarios.py | 7 +-- src/scenic/core/regions.py | 4 +- src/scenic/core/requirements.py | 79 +++++++++++++++++--------- src/scenic/syntax/compiler.py | 9 +-- tests/syntax/test_requirements.py | 81 +++++++++++++++++++++++++++ 5 files changed, 145 insertions(+), 35 deletions(-) diff --git a/src/scenic/core/dynamics/scenarios.py b/src/scenic/core/dynamics/scenarios.py index 77ec470e8..61a55e48c 100644 --- a/src/scenic/core/dynamics/scenarios.py +++ b/src/scenic/core/dynamics/scenarios.py @@ -58,7 +58,7 @@ def __init__(self, *args, **kwargs): self._objects = [] # ordered for reproducibility self._sampledObjects = self._objects self._externalParameters = [] - self._pendingRequirements = defaultdict(list) + self._pendingRequirements = [] self._requirements = [] # things needing to be sampled to evaluate the requirements self._requirementDeps = set() @@ -409,9 +409,8 @@ def _registerObject(self, obj): def _addRequirement(self, ty, reqID, req, line, name, prob): """Save a requirement defined at compile-time for later processing.""" - assert reqID not in self._pendingRequirements preq = PendingRequirement(ty, req, line, prob, name, self._ego) - self._pendingRequirements[reqID] = preq + self._pendingRequirements.append((reqID, preq)) def _addDynamicRequirement(self, ty, req, line, name): """Add a requirement defined during a dynamic simulation.""" @@ -429,7 +428,7 @@ def _compileRequirements(self): namespace = self._dummyNamespace if self._dummyNamespace else self.__dict__ requirementSyntax = self._requirementSyntax assert requirementSyntax is not None - for reqID, requirement in self._pendingRequirements.items(): + for reqID, requirement in self._pendingRequirements: syntax = requirementSyntax[reqID] if requirementSyntax else None # Catch the simple case where someone has most likely forgotten the "monitor" diff --git a/src/scenic/core/regions.py b/src/scenic/core/regions.py index 07f4c58b7..4732fda1c 100644 --- a/src/scenic/core/regions.py +++ b/src/scenic/core/regions.py @@ -3275,7 +3275,9 @@ def __init__(self, position, heading, width, length, name=None): self.circumcircle = (self.position, self.radius) super().__init__( - polygon=self._makePolygons(position, heading, width, length), + polygon=self._makePolygons( + self.position, self.heading, self.width, self.length + ), z=self.position.z, name=name, additionalDeps=deps, diff --git a/src/scenic/core/requirements.py b/src/scenic/core/requirements.py index f143ae552..ab6de91da 100644 --- a/src/scenic/core/requirements.py +++ b/src/scenic/core/requirements.py @@ -50,16 +50,21 @@ def __init__(self, ty, condition, line, prob, name, ego): # condition is an instance of Proposition. Flatten to get a list of atomic propositions. atoms = condition.atomics() - bindings = {} + self.globalBindings = {} # bindings to global/builtin names + self.closureBindings = {} # bindings to top-level closure variables + self.cells = [] # cells used in referenced closures atomGlobals = None for atom in atoms: - bindings.update(getAllGlobals(atom.closure)) + gbindings, cbindings, closures = getNameBindings(atom.closure) + self.globalBindings.update(gbindings) + self.closureBindings.update(cbindings) + for closure in closures: + self.cells.extend(closure.__closure__) globs = atom.closure.__globals__ if atomGlobals is not None: assert globs is atomGlobals else: atomGlobals = globs - self.bindings = bindings self.egoObject = ego def compile(self, namespace, scenario, syntax=None): @@ -68,21 +73,28 @@ def compile(self, namespace, scenario, syntax=None): While we're at it, determine whether the requirement implies any relations we can use for pruning, and gather all of its dependencies. """ - bindings, ego, line = self.bindings, self.egoObject, self.line + globalBindings, closureBindings = self.globalBindings, self.closureBindings + cells, ego, line = self.cells, self.egoObject, self.line condition, ty = self.condition, self.ty # Convert bound values to distributions as needed - for name, value in bindings.items(): - bindings[name] = toDistribution(value) + for name, value in globalBindings.items(): + globalBindings[name] = toDistribution(value) + for name, value in closureBindings.items(): + closureBindings[name] = toDistribution(value) + cells = tuple((cell, toDistribution(cell.cell_contents)) for cell in cells) + allBindings = dict(globalBindings) + allBindings.update(closureBindings) # Check whether requirement implies any relations used for pruning canPrune = condition.check_constrains_sampling() if canPrune: - relations.inferRelationsFrom(syntax, bindings, ego, line) + relations.inferRelationsFrom(syntax, allBindings, ego, line) # Gather dependencies of the requirement deps = set() - for value in bindings.values(): + cellVals = (value for cell, value in cells) + for value in itertools.chain(allBindings.values(), cellVals): if needsSampling(value): deps.add(value) if needsLazyEvaluation(value): @@ -93,7 +105,7 @@ def compile(self, namespace, scenario, syntax=None): # If this requirement contains the CanSee specifier, we will need to sample all objects # to meet the dependencies. - if "CanSee" in bindings: + if "CanSee" in globalBindings: deps.update(scenario.objects) if ego is not None: @@ -102,13 +114,18 @@ def compile(self, namespace, scenario, syntax=None): # Construct closure def closure(values, monitor=None): - # rebind any names referring to sampled objects + # rebind any names referring to sampled objects (for require statements, + # rebind all names, since we want their values at the time the requirement + # was created) # note: need to extract namespace here rather than close over value # from above because of https://github.com/uqfoundation/dill/issues/532 namespace = condition.atomics()[0].closure.__globals__ - for name, value in bindings.items(): - if value in values: + for name, value in globalBindings.items(): + if ty == RequirementType.require or value in values: namespace[name] = values[value] + for cell, value in cells: + cell.cell_contents = values[value] + # rebind ego object, which can be referred to implicitly boundEgo = None if ego is None else values[ego] # evaluate requirement condition, reporting errors on the correct line @@ -132,24 +149,34 @@ def closure(values, monitor=None): return CompiledRequirement(self, closure, deps, condition) -def getAllGlobals(req, restrictTo=None): +def getNameBindings(req, restrictTo=None): """Find all names the given lambda depends on, along with their current bindings.""" namespace = req.__globals__ if restrictTo is not None and restrictTo is not namespace: - return {} + return {}, {}, () externals = inspect.getclosurevars(req) - assert not externals.nonlocals # TODO handle these - globs = dict(externals.builtins) - for name, value in externals.globals.items(): - globs[name] = value - if inspect.isfunction(value): - subglobs = getAllGlobals(value, restrictTo=namespace) - for name, value in subglobs.items(): - if name in globs: - assert value is globs[name] - else: - globs[name] = value - return globs + globalBindings = externals.builtins + + closures = set() + if externals.nonlocals: + closures.add(req) + + def handleFunctions(bindings): + for value in bindings.values(): + if inspect.isfunction(value): + if value.__closure__ is not None: + closures.add(value) + subglobs, _, _ = getNameBindings(value, restrictTo=namespace) + for name, value in subglobs.items(): + if name in globalBindings: + assert value is globalBindings[name] + else: + globalBindings[name] = value + + globalBindings.update(externals.globals) + handleFunctions(externals.globals) + handleFunctions(externals.nonlocals) + return globalBindings, externals.nonlocals, closures class BoundRequirement: diff --git a/src/scenic/syntax/compiler.py b/src/scenic/syntax/compiler.py index 5328c0c6d..70f1b9f24 100644 --- a/src/scenic/syntax/compiler.py +++ b/src/scenic/syntax/compiler.py @@ -1359,11 +1359,12 @@ def createRequirementLike( """Create a call to a function that implements requirement-like features, such as `record` and `terminate when`. Args: - functionName (str): Name of the requirement-like function to call. Its signature must be `(reqId: int, body: () -> bool, lineno: int, name: str | None)` + functionName (str): Name of the requirement-like function to call. Its signature + must be `(reqId: int, body: () -> bool, lineno: int, name: str | None)` body (ast.AST): AST node to evaluate for checking the condition lineno (int): Line number in the source code - name (Optional[str], optional): Optional name for requirements. Defaults to None. - prob (Optional[float], optional): Optional probability for requirements. Defaults to None. + name (Optional[str]): Optional name for requirements. Defaults to None. + prob (Optional[float]): Optional probability for requirements. Defaults to None. """ propTransformer = PropositionTransformer(self.filename) newBody, self.nextSyntaxId = propTransformer.transform(body, self.nextSyntaxId) @@ -1374,7 +1375,7 @@ def createRequirementLike( value=ast.Call( func=ast.Name(functionName, loadCtx), args=[ - ast.Constant(requirementId), # requirement IDre + ast.Constant(requirementId), # requirement ID newBody, # body ast.Constant(lineno), # line number ast.Constant(name), # requirement name diff --git a/tests/syntax/test_requirements.py b/tests/syntax/test_requirements.py index 0c7699fe0..730ac60c4 100644 --- a/tests/syntax/test_requirements.py +++ b/tests/syntax/test_requirements.py @@ -19,6 +19,87 @@ def test_requirement(): assert all(0 <= x <= 10 for x in xs) +def test_requirement_in_loop(): + scenario = compileScenic( + """ + ego = new Object at Range(-10, 10) @ Range(-10, 10) + for i in range(2): + require ego.position[i] >= 0 + """ + ) + poss = [sampleEgo(scenario, maxIterations=150).position for i in range(60)] + assert all(0 <= pos.x <= 10 and 0 <= pos.y <= 10 for pos in poss) + + +def test_requirement_in_function(): + scenario = compileScenic( + """ + ego = new Object at Range(-10, 10) @ Range(-10, 10) + def f(i): + require ego.position[i] >= 0 + for i in range(2): + f(i) + """ + ) + poss = [sampleEgo(scenario, maxIterations=150).position for i in range(60)] + assert all(0 <= pos.x <= 10 and 0 <= pos.y <= 10 for pos in poss) + + +def test_requirement_in_function_helper(): + scenario = compileScenic( + """ + ego = new Object at Range(-10, 10) @ Range(-10, 10) + m = 0 + def f(): + assert m == 0 + return ego.y + m + def g(): + require ego.x < f() + g() + m = -100 + """ + ) + poss = [sampleEgo(scenario, maxIterations=60).position for i in range(60)] + assert all(pos.x < pos.y for pos in poss) + + +def test_requirement_in_function_random_local(): + scenario = compileScenic( + """ + ego = new Object at Range(-10, 10) @ 0 + def f(): + local = Range(0, 1) + require ego.x < local + f() + """ + ) + xs = [sampleEgo(scenario, maxIterations=60).position.x for i in range(60)] + assert all(-10 <= x <= 1 for x in xs) + + +def test_requirement_in_function_random_cell(): + scenario = compileScenic( + """ + ego = new Object at Range(-10, 10) @ 0 + def f(i): + def g(): + return i + return g + g = f(Range(0, 1)) # global function with a cell containing a random value + def h(): + local = Uniform(True, False) + def inner(): # local function likewise + return local + require (g() >= 0) and ((ego.x < -5) if inner() else (ego.x > 5)) + h() + """ + ) + xs = [sampleEgo(scenario, maxIterations=150).position.x for i in range(60)] + assert all(x < -5 or x > 5 for x in xs) + assert any(x < -5 for x in xs) + assert any(x > 5 for x in xs) + + def test_soft_requirement(): scenario = compileScenic( """ From b84d833b84b21ede3bdd04cd96c9e3eb7d58f83f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Ba=C3=B1uelos?= <32311654+abanuelo@users.noreply.github.com> Date: Fri, 13 Dec 2024 14:17:52 -0800 Subject: [PATCH 04/73] chore: include supported version of Webots in docs (#266) * fix: Adding modifications to codecov.yml * chore: Adding note on supported Webots version * fix: update language --------- Co-authored-by: Armando Banuelos Co-authored-by: Armando Banuelos Co-authored-by: Daniel Fremont --- docs/simulators.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/simulators.rst b/docs/simulators.rst index fd0215b81..f03d6d476 100644 --- a/docs/simulators.rst +++ b/docs/simulators.rst @@ -94,6 +94,8 @@ We have several interfaces to the `Webots robotics simulator Date: Fri, 13 Dec 2024 15:56:32 -0800 Subject: [PATCH 05/73] feat: Adding M1/Intel runners to `run-tests.yml` and introducing fast-mode (#265) * fix: Adding modifications to codecov.yml * feat: Adding new M1 runners to run-test.yml workflow * feat: adding intel based mac and fast mode * fix: Adding default fast mode to workflow * fix: adding just test-full dependency installer * fix: no need to double count ubunut-latest tests as being covered as part of coverage * fix: updating coverage to run on 3.12 * fix: adding Pillow reinstallation for run_tests.yml * fix: updating step * fix: adding yes options * fix: updating dependenices for 3.12 Pillow * fix: avoiding ubuntu as its being covered as part of the coverage run * fix: Adding lower bound for pillow dependency * fix fast/slow test selection logic * workflow calls to run-tests should do all tests by default --------- Co-authored-by: Armando Banuelos Co-authored-by: Armando Banuelos Co-authored-by: Daniel Fremont --- .github/workflows/on-call-reminder.yml | 4 ++-- .github/workflows/run-coverage.yml | 2 +- .github/workflows/run-tests.yml | 11 +++++++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/on-call-reminder.yml b/.github/workflows/on-call-reminder.yml index 1798be663..4a7e75882 100644 --- a/.github/workflows/on-call-reminder.yml +++ b/.github/workflows/on-call-reminder.yml @@ -11,10 +11,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: '3.12' diff --git a/.github/workflows/run-coverage.yml b/.github/workflows/run-coverage.yml index 7ec93f84f..34344184c 100644 --- a/.github/workflows/run-coverage.yml +++ b/.github/workflows/run-coverage.yml @@ -22,7 +22,7 @@ jobs: strategy: fail-fast: true matrix: - python-version: ["3.11"] + python-version: ["3.12"] os: [ubuntu-latest] extras: ["test-full"] runs-on: ${{ matrix.os }} diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 2a5bb55d1..f24b21dd8 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -34,8 +34,11 @@ jobs: fail-fast: true matrix: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] - os: [ubuntu-latest, windows-latest] - extras: ["test", "test-full"] + os: [ubuntu-latest, windows-latest, macos-13, macos-latest] + include: + # Only run slow tests on the latest version of Python + - python-version: "3.12" + slow: true runs-on: ${{ matrix.os }} steps: @@ -63,8 +66,8 @@ jobs: - name: Install Scenic and dependencies run: | - python -m pip install -e ".[${{ matrix.extras }}]" + python -m pip install -e ".[test-full]" - name: Run pytest run: | - pytest ${{ inputs.options || '--no-graphics' }} + pytest ${{ inputs.options || (matrix.slow && '--no-graphics' || '--fast --no-graphics') }} From 26d9da7be7630bde48357b8d3d37dba177b9ac4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Ba=C3=B1uelos?= <32311654+abanuelo@users.noreply.github.com> Date: Mon, 16 Dec 2024 17:22:05 -0800 Subject: [PATCH 06/73] feat: enable dynamic scenario termination from closing display (#249) Co-authored-by: Armando Banuelos Co-authored-by: Lola Marrero <110120745+lola831@users.noreply.github.com> --- src/scenic/core/simulators.py | 11 +++++++++++ src/scenic/simulators/newtonian/simulator.py | 6 ++++++ 2 files changed, 17 insertions(+) diff --git a/src/scenic/core/simulators.py b/src/scenic/core/simulators.py index 832b03632..1fa3d1c56 100644 --- a/src/scenic/core/simulators.py +++ b/src/scenic/core/simulators.py @@ -331,6 +331,7 @@ def __init__( continueAfterDivergence=False, verbosity=0, ): + self.screen = None self.result = None self.scene = scene self.objects = [] @@ -432,6 +433,13 @@ def _run(self, dynamicScenario, maxSteps): terminationReason = newReason terminationType = TerminationType.terminatedByMonitor + # Check if users manually closed out display for simulator + if "Dead" in str(self.screen): + return ( + TerminationType.terminatedByUser, + "user manually terminated simulation", + ) + # "Always" and scenario-level requirements have been checked; # now safe to terminate if the top-level scenario has finished, # a monitor requested termination, or we've hit the timeout @@ -886,6 +894,9 @@ class TerminationType(enum.Enum): #: A :term:`dynamic behavior` used :keyword:`terminate simulation` to end the simulation. terminatedByBehavior = "a behavior terminated the simulation" + #: A user manually intervenes and closes display window + terminatedByUser = "manually terminated by user" + class SimulationResult: """Result of running a simulation. diff --git a/src/scenic/simulators/newtonian/simulator.py b/src/scenic/simulators/newtonian/simulator.py index 193103ab7..6ff603509 100644 --- a/src/scenic/simulators/newtonian/simulator.py +++ b/src/scenic/simulators/newtonian/simulator.py @@ -84,6 +84,7 @@ def __init__( self.export_gif = export_gif self.render = render self.network = network + self.screen = None self.frames = [] self.debug_render = debug_render @@ -226,6 +227,11 @@ def step(self): obj.heading += obj.angularSpeed * self.timestep if self.render: + # Handle closing out pygame screen + for event in pygame.event.get(): + if event.type == pygame.QUIT: + self.destroy() + return self.draw_objects() pygame.event.pump() From 9d82edaf4f7733aa172154b9a4d26f21750e28d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Ba=C3=B1uelos?= <32311654+abanuelo@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:39:56 -0800 Subject: [PATCH 07/73] chore: Updating webots examples (#263) Co-authored-by: Armando Banuelos --- examples/webots/city_intersection/README.md | 4 +++- examples/webots/vacuum/README.md | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/examples/webots/city_intersection/README.md b/examples/webots/city_intersection/README.md index e811ff043..3ef601491 100644 --- a/examples/webots/city_intersection/README.md +++ b/examples/webots/city_intersection/README.md @@ -2,6 +2,8 @@ An example showing how to use Scenic to generate training data for an autonomous car. In this example the ego car is approaching an intersection where it has an obligation to yield, with another car crossing the intersection. At regular intervals the ego car's camera output will be saved, and tagged with whether or not the crossing car is visible or not visible. -First navigate to `controllers/create_avoid_obstacles` and run `make` (you may need to first set the Webots environment variable as shown [here](https://cyberbotics.com/doc/guide/compiling-controllers-in-a-terminal)). Then run the scenario using the `worlds/city_intersection.wbt` file in webots and play the simulation by pressing one of the buttons at the top (we recommend "Run the simulation as fast as possible" to maximize speed). After Webots closes (indicating the simulations has completed), look in the `images` directory for the tagged images. +First ensure that you have your `WEBOTS_HOME` environment variable set to the root of your Webots directory by running: `export WEBOTS_HOME=/path/to/webots`. + +Then navigate to `controllers/autonomous_vehicle` and run `make` (you may need to first set the Webots environment variable as shown [here](https://cyberbotics.com/doc/guide/compiling-controllers-in-a-terminal)). Then run the scenario using the `worlds/city_intersection.wbt` file in webots and play the simulation by pressing one of the buttons at the top (we recommend "Run the simulation as fast as possible" to maximize speed). After Webots closes (indicating the simulations has completed), look in the `images` directory for the tagged images. These examples are intended to be run **without** the ``--2d`` flag. diff --git a/examples/webots/vacuum/README.md b/examples/webots/vacuum/README.md index 7ada0e8f0..77d224b3c 100644 --- a/examples/webots/vacuum/README.md +++ b/examples/webots/vacuum/README.md @@ -2,6 +2,8 @@ An example showing how to use Scenic to evaluate the coverage of a robot vacuum. -First navigate to `controllers/create_avoid_obstacles` and run `make` (you may need to first set the Webots environment variable as shown [here](https://cyberbotics.com/doc/guide/compiling-controllers-in-a-terminal)). Then run the scenario using the `worlds/create.wbt` file in webots and play the simulation by pressing one of the buttons at the top (we recommend "Run the simulation as fast as possible" to maximize speed). After Webots closes (indicating all simulations have run), run `python summary.py` to get a summary of the output. +First ensure that you have your `WEBOTS_HOME` environment variable set to the root of your Webots directory by running: `export WEBOTS_HOME=/path/to/webots`. + +Then navigate to `controllers/create_avoid_obstacles` and run `make` (you may need to first set the Webots environment variable as shown [here](https://cyberbotics.com/doc/guide/compiling-controllers-in-a-terminal)). Then run the scenario using the `worlds/create.wbt` file in webots and play the simulation by pressing one of the buttons at the top (we recommend "Run the simulation as fast as possible" to maximize speed). After Webots closes (indicating all simulations have run), run `python summary.py` to get a summary of the output. These examples are intended to be run **without** the ``--2d`` flag. From ca74ae94c052150c8161abe8c5460ae6d624a4bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Ba=C3=B1uelos?= <32311654+abanuelo@users.noreply.github.com> Date: Wed, 18 Dec 2024 11:07:17 -0800 Subject: [PATCH 08/73] chore: Updating examples for CARLA and driving domain (#257) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: Updating examples for CARLA and driving domain * chore: fixing comments to illustrate use of carla simulator --------- Co-authored-by: Armando Bañuelos Co-authored-by: Armando Banuelos --- examples/carla/Carla_Challenge/carlaChallenge1.scenic | 3 +++ examples/carla/Carla_Challenge/carlaChallenge10.scenic | 3 +++ examples/carla/Carla_Challenge/carlaChallenge2.scenic | 3 +++ .../carla/Carla_Challenge/carlaChallenge3_dynamic.scenic | 3 +++ .../carla/Carla_Challenge/carlaChallenge3_static.scenic | 3 +++ examples/carla/Carla_Challenge/carlaChallenge4.scenic | 3 +++ examples/carla/Carla_Challenge/carlaChallenge5.scenic | 3 +++ examples/carla/Carla_Challenge/carlaChallenge6.scenic | 3 +++ examples/carla/Carla_Challenge/carlaChallenge7.scenic | 3 +++ examples/carla/Carla_Challenge/carlaChallenge8.scenic | 3 +++ examples/carla/Carla_Challenge/carlaChallenge9.scenic | 3 +++ .../carla/NHTSA_Scenarios/bypassing/bypassing_01.scenic | 3 +++ .../carla/NHTSA_Scenarios/bypassing/bypassing_02.scenic | 3 +++ .../carla/NHTSA_Scenarios/bypassing/bypassing_03.scenic | 3 +++ .../carla/NHTSA_Scenarios/bypassing/bypassing_04.scenic | 3 +++ .../carla/NHTSA_Scenarios/bypassing/bypassing_05.scenic | 3 +++ .../NHTSA_Scenarios/intersection/intersection_01.scenic | 3 +++ .../NHTSA_Scenarios/intersection/intersection_02.scenic | 3 +++ .../NHTSA_Scenarios/intersection/intersection_03.scenic | 3 +++ .../NHTSA_Scenarios/intersection/intersection_04.scenic | 3 +++ .../NHTSA_Scenarios/intersection/intersection_05.scenic | 3 +++ .../NHTSA_Scenarios/intersection/intersection_06.scenic | 3 +++ .../NHTSA_Scenarios/intersection/intersection_07.scenic | 3 +++ .../NHTSA_Scenarios/intersection/intersection_08.scenic | 3 +++ .../NHTSA_Scenarios/intersection/intersection_09.scenic | 3 +++ .../NHTSA_Scenarios/intersection/intersection_10.scenic | 3 +++ .../NHTSA_Scenarios/pedestrian/pedestrian_01.scenic | 3 +++ .../NHTSA_Scenarios/pedestrian/pedestrian_02.scenic | 3 +++ .../NHTSA_Scenarios/pedestrian/pedestrian_03.scenic | 3 +++ .../NHTSA_Scenarios/pedestrian/pedestrian_04.scenic | 3 +++ .../NHTSA_Scenarios/pedestrian/pedestrian_05.scenic | 3 +++ examples/carla/OAS_Scenarios/oas_scenario_05.scenic | 3 +++ examples/carla/OAS_Scenarios/oas_scenario_06.scenic | 3 +++ examples/carla/adjacentLanes.scenic | 5 +++++ examples/carla/adjacentOpposingPair.scenic | 4 ++++ examples/carla/backgroundActivity.scenic | 9 ++++++++- examples/carla/car.scenic | 4 ++++ examples/carla/manual_control/carlaChallenge1.scenic | 5 ++++- .../carla/manual_control/carlaChallenge3_dynamic.scenic | 5 ++++- examples/carla/manual_control/carlaChallenge4.scenic | 5 ++++- examples/carla/manual_control/carlaChallenge7.scenic | 5 ++++- examples/carla/pedestrian.scenic | 6 +++++- examples/carla/trafficLights.scenic | 3 +++ examples/driving/Carla_Challenge/carlaChallenge2.scenic | 3 +++ examples/driving/Carla_Challenge/carlaChallenge3.scenic | 3 +++ examples/driving/OAS_Scenarios/oas_scenario_03.scenic | 3 +++ examples/driving/OAS_Scenarios/oas_scenario_04.scenic | 3 +++ examples/driving/OAS_Scenarios/oas_scenario_28.scenic | 3 +++ examples/driving/OAS_Scenarios/oas_scenario_29.scenic | 3 +++ examples/driving/OAS_Scenarios/oas_scenario_30.scenic | 3 +++ examples/driving/OAS_Scenarios/oas_scenario_32.scenic | 3 +++ examples/driving/badlyParkedCarPullingIn.scenic | 5 +++++ examples/driving/car.scenic | 4 ++++ examples/driving/pedestrian.scenic | 4 ++++ examples/driving/pedestrianAcrossRoad.scenic | 4 ++++ 55 files changed, 185 insertions(+), 6 deletions(-) diff --git a/examples/carla/Carla_Challenge/carlaChallenge1.scenic b/examples/carla/Carla_Challenge/carlaChallenge1.scenic index df69bfcc1..aba7b40ef 100644 --- a/examples/carla/Carla_Challenge/carlaChallenge1.scenic +++ b/examples/carla/Carla_Challenge/carlaChallenge1.scenic @@ -3,6 +3,9 @@ Traffic Scenario 01. Control loss without previous action. The ego-vehicle loses control due to bad conditions on the road and it must recover, coming back to its original lane. + +To run this file using the Carla simulator: + scenic examples/carla/Carla_Challenge/carlaChallenge1.scenic --2d --model scenic.simulators.carla.model --simulate """ ## SET MAP AND MODEL (i.e. definitions of all referenceable vehicle types, road library, etc) diff --git a/examples/carla/Carla_Challenge/carlaChallenge10.scenic b/examples/carla/Carla_Challenge/carlaChallenge10.scenic index 47c9f0d3e..30e4b3b33 100644 --- a/examples/carla/Carla_Challenge/carlaChallenge10.scenic +++ b/examples/carla/Carla_Challenge/carlaChallenge10.scenic @@ -3,6 +3,9 @@ Traffic Scenario 10. Crossing negotiation at an unsignalized intersection. The ego-vehicle needs to negotiate with other vehicles to cross an unsignalized intersection. In this situation it is assumed that the first to enter the intersection has priority. + +To run this file using the Carla simulator: + scenic examples/carla/Carla_Challenge/carlaChallenge10.scenic --2d --model scenic.simulators.carla.model --simulate """ ## SET MAP AND MODEL (i.e. definitions of all referenceable vehicle types, road library, etc) diff --git a/examples/carla/Carla_Challenge/carlaChallenge2.scenic b/examples/carla/Carla_Challenge/carlaChallenge2.scenic index 0641f8f55..4c423f2b1 100644 --- a/examples/carla/Carla_Challenge/carlaChallenge2.scenic +++ b/examples/carla/Carla_Challenge/carlaChallenge2.scenic @@ -3,6 +3,9 @@ Traffic Scenario 02. Longitudinal control after leading vehicle’s brake. The leading vehicle decelerates suddenly due to an obstacle and the ego-vehicle must perform an emergency brake or an avoidance maneuver. + +To run this file using the Carla simulator: + scenic examples/carla/Carla_Challenge/carlaChallenge2.scenic --2d --model scenic.simulators.carla.model --simulate """ ## SET MAP AND MODEL (i.e. definitions of all referenceable vehicle types, road library, etc) diff --git a/examples/carla/Carla_Challenge/carlaChallenge3_dynamic.scenic b/examples/carla/Carla_Challenge/carlaChallenge3_dynamic.scenic index 7d1d87ad9..9fb37b60c 100644 --- a/examples/carla/Carla_Challenge/carlaChallenge3_dynamic.scenic +++ b/examples/carla/Carla_Challenge/carlaChallenge3_dynamic.scenic @@ -3,6 +3,9 @@ Traffic Scenario 03 (dynamic). Obstacle avoidance without prior action. The ego-vehicle encounters an obstacle / unexpected entity on the road and must perform an emergency brake or an avoidance maneuver. + +To run this file using the Carla simulator: + scenic examples/carla/Carla_Challenge/carlaChallenge3_dynamic.scenic --2d --model scenic.simulators.carla.model --simulate """ # SET MAP AND MODEL (i.e. definitions of all referenceable vehicle types, road library, etc) diff --git a/examples/carla/Carla_Challenge/carlaChallenge3_static.scenic b/examples/carla/Carla_Challenge/carlaChallenge3_static.scenic index f721575b7..d01f5be21 100644 --- a/examples/carla/Carla_Challenge/carlaChallenge3_static.scenic +++ b/examples/carla/Carla_Challenge/carlaChallenge3_static.scenic @@ -3,6 +3,9 @@ Traffic Scenario 03 (static). Obstacle avoidance without prior action. The ego-vehicle encounters an obstacle / unexpected entity on the road and must perform an emergency brake or an avoidance maneuver. + +To run this file using the Carla simulator: + scenic examples/carla/Carla_Challenge/carlaChallenge3_static.scenic --2d --model scenic.simulators.carla.model --simulate """ ## SET MAP AND MODEL (i.e. definitions of all referenceable vehicle types, road library, etc) diff --git a/examples/carla/Carla_Challenge/carlaChallenge4.scenic b/examples/carla/Carla_Challenge/carlaChallenge4.scenic index 82e5576f5..4fe1cf019 100644 --- a/examples/carla/Carla_Challenge/carlaChallenge4.scenic +++ b/examples/carla/Carla_Challenge/carlaChallenge4.scenic @@ -3,6 +3,9 @@ Traffic Scenario 04. Obstacle avoidance without prior action. The ego-vehicle encounters an obstacle / unexpected entity on the road and must perform an emergency brake or an avoidance maneuver. + +To run this file using the Carla simulator: + scenic examples/carla/Carla_Challenge/carlaChallenge4.scenic --2d --model scenic.simulators.carla.model --simulate """ ## SET MAP AND MODEL (i.e. definitions of all referenceable vehicle types, road library, etc) diff --git a/examples/carla/Carla_Challenge/carlaChallenge5.scenic b/examples/carla/Carla_Challenge/carlaChallenge5.scenic index c470b63aa..b97c64655 100644 --- a/examples/carla/Carla_Challenge/carlaChallenge5.scenic +++ b/examples/carla/Carla_Challenge/carlaChallenge5.scenic @@ -1,6 +1,9 @@ """ Scenario Description Based on 2019 Carla Challenge Traffic Scenario 05. Ego-vehicle performs a lane changing to evade a leading vehicle, which is moving too slowly. + +To run this file using the Carla simulator: + scenic examples/carla/Carla_Challenge/carlaChallenge5.scenic --2d --model scenic.simulators.carla.model --simulate """ param map = localPath('../../../assets/maps/CARLA/Town05.xodr') param carla_map = 'Town05' diff --git a/examples/carla/Carla_Challenge/carlaChallenge6.scenic b/examples/carla/Carla_Challenge/carlaChallenge6.scenic index 4ea0a5130..f51b68262 100644 --- a/examples/carla/Carla_Challenge/carlaChallenge6.scenic +++ b/examples/carla/Carla_Challenge/carlaChallenge6.scenic @@ -2,6 +2,9 @@ Based on CARLA Challenge Scenario 6: https://carlachallenge.org/challenge/nhtsa/ Ego-vehicle must go around a blocking object using the opposite lane, yielding to oncoming traffic. + +To run this file using the Carla simulator: + scenic examples/carla/Carla_Challenge/carlaChallenge6.scenic --2d --model scenic.simulators.carla.model --simulate """ # N.B. Town07 is not included with CARLA by default; see installation instructions at diff --git a/examples/carla/Carla_Challenge/carlaChallenge7.scenic b/examples/carla/Carla_Challenge/carlaChallenge7.scenic index 676c90449..92027273b 100644 --- a/examples/carla/Carla_Challenge/carlaChallenge7.scenic +++ b/examples/carla/Carla_Challenge/carlaChallenge7.scenic @@ -3,6 +3,9 @@ Based on 2019 Carla Challenge Traffic Scenario 07. Ego-vehicle is going straight at an intersection but a crossing vehicle runs a red light, forcing the ego-vehicle to perform a collision avoidance maneuver. Note: The traffic light control is not implemented yet, but it will soon be. + +To run this file using the Carla simulator: + scenic examples/carla/Carla_Challenge/carlaChallenge7.scenic --2d --model scenic.simulators.carla.model --simulate """ param map = localPath('../../../assets/maps/CARLA/Town05.xodr') param carla_map = 'Town05' diff --git a/examples/carla/Carla_Challenge/carlaChallenge8.scenic b/examples/carla/Carla_Challenge/carlaChallenge8.scenic index 97837cfcd..e1f8125df 100644 --- a/examples/carla/Carla_Challenge/carlaChallenge8.scenic +++ b/examples/carla/Carla_Challenge/carlaChallenge8.scenic @@ -3,6 +3,9 @@ Traffic Scenario 08. Unprotected left turn at intersection with oncoming traffic. The ego-vehicle is performing an unprotected left turn at an intersection, yielding to oncoming traffic. + +To run this file using the Carla simulator: + scenic examples/carla/Carla_Challenge/carlaChallenge8.scenic --2d --model scenic.simulators.carla.model --simulate """ ## SET MAP AND MODEL (i.e. definitions of all referenceable vehicle types, road library, etc) diff --git a/examples/carla/Carla_Challenge/carlaChallenge9.scenic b/examples/carla/Carla_Challenge/carlaChallenge9.scenic index 0b4e3e9e5..133b4e07c 100644 --- a/examples/carla/Carla_Challenge/carlaChallenge9.scenic +++ b/examples/carla/Carla_Challenge/carlaChallenge9.scenic @@ -1,6 +1,9 @@ """ Scenario Description Based on 2019 Carla Challenge Traffic Scenario 09. Ego-vehicle is performing a right turn at an intersection, yielding to crossing traffic. + +To run this file using the Carla simulator: + scenic examples/carla/Carla_Challenge/carlaChallenge9.scenic --2d --model scenic.simulators.carla.model --simulate """ param map = localPath('../../../assets/maps/CARLA/Town05.xodr') param carla_map = 'Town05' diff --git a/examples/carla/NHTSA_Scenarios/bypassing/bypassing_01.scenic b/examples/carla/NHTSA_Scenarios/bypassing/bypassing_01.scenic index 1541c313b..b041174bf 100644 --- a/examples/carla/NHTSA_Scenarios/bypassing/bypassing_01.scenic +++ b/examples/carla/NHTSA_Scenarios/bypassing/bypassing_01.scenic @@ -4,6 +4,9 @@ AUTHOR: Francis Indaheng, findaheng@berkeley.edu DESCRIPTION: Ego vehicle performs a lane change to bypass a slow adversary vehicle before returning to its original lane. SOURCE: NHSTA, #16 + +To run this file using the Carla simulator: + scenic examples/carla/NHTSA_Scenarios/bypassing/bypassing_01.scenic --2d --model scenic.simulators.carla.model --simulate """ ################################# diff --git a/examples/carla/NHTSA_Scenarios/bypassing/bypassing_02.scenic b/examples/carla/NHTSA_Scenarios/bypassing/bypassing_02.scenic index 5e72aaa04..b95269650 100644 --- a/examples/carla/NHTSA_Scenarios/bypassing/bypassing_02.scenic +++ b/examples/carla/NHTSA_Scenarios/bypassing/bypassing_02.scenic @@ -4,6 +4,9 @@ AUTHOR: Francis Indaheng, findaheng@berkeley.edu DESCRIPTION: Adversary vehicle performs a lane change to bypass the slow ego vehicle before returning to its original lane. SOURCE: NHSTA, #16 + +To run this file using the Carla simulator: + scenic examples/carla/NHTSA_Scenarios/bypassing/bypassing_02.scenic --2d --model scenic.simulators.carla.model --simulate """ ################################# diff --git a/examples/carla/NHTSA_Scenarios/bypassing/bypassing_03.scenic b/examples/carla/NHTSA_Scenarios/bypassing/bypassing_03.scenic index 6493bdc1a..6ec12c61e 100644 --- a/examples/carla/NHTSA_Scenarios/bypassing/bypassing_03.scenic +++ b/examples/carla/NHTSA_Scenarios/bypassing/bypassing_03.scenic @@ -6,6 +6,9 @@ adversary vehicle but cannot return to its original lane because the adversary accelerates. Ego vehicle must then slow down to avoid collision with leading vehicle in new lane. SOURCE: NHSTA, #16 + +To run this file using the Carla simulator: + scenic examples/carla/NHTSA_Scenarios/bypassing/bypassing_03.scenic --2d --model scenic.simulators.carla.model --simulate """ ################################# diff --git a/examples/carla/NHTSA_Scenarios/bypassing/bypassing_04.scenic b/examples/carla/NHTSA_Scenarios/bypassing/bypassing_04.scenic index ff5feda9b..f4c42378c 100644 --- a/examples/carla/NHTSA_Scenarios/bypassing/bypassing_04.scenic +++ b/examples/carla/NHTSA_Scenarios/bypassing/bypassing_04.scenic @@ -4,6 +4,9 @@ AUTHOR: Francis Indaheng, findaheng@berkeley.edu DESCRIPTION: Ego vehicle performs multiple lane changes to bypass two slow adversary vehicles. SOURCE: NHSTA, #16 + +To run this file using the Carla simulator: + scenic examples/carla/NHTSA_Scenarios/bypassing/bypassing_04.scenic --2d --model scenic.simulators.carla.model --simulate """ ################################# diff --git a/examples/carla/NHTSA_Scenarios/bypassing/bypassing_05.scenic b/examples/carla/NHTSA_Scenarios/bypassing/bypassing_05.scenic index 5046e3659..3001bcc92 100644 --- a/examples/carla/NHTSA_Scenarios/bypassing/bypassing_05.scenic +++ b/examples/carla/NHTSA_Scenarios/bypassing/bypassing_05.scenic @@ -4,6 +4,9 @@ AUTHOR: Francis Indaheng, findaheng@berkeley.edu DESCRIPTION: Ego vehicle performs multiple lane changes to bypass three slow adversary vehicles. SOURCE: NHSTA, #16 + +To run this file using the Carla simulator: + scenic examples/carla/NHTSA_Scenarios/bypassing/bypassing_05.scenic --2d --model scenic.simulators.carla.model --simulate """ ################################# diff --git a/examples/carla/NHTSA_Scenarios/intersection/intersection_01.scenic b/examples/carla/NHTSA_Scenarios/intersection/intersection_01.scenic index 571ae9734..d17fdb7c3 100644 --- a/examples/carla/NHTSA_Scenarios/intersection/intersection_01.scenic +++ b/examples/carla/NHTSA_Scenarios/intersection/intersection_01.scenic @@ -5,6 +5,9 @@ DESCRIPTION: Ego vehicle goes straight at 4-way intersection and must suddenly stop to avoid collision when adversary vehicle from opposite lane makes a left turn. SOURCE: NHSTA, #30 + +To run this file using the Carla simulator: + scenic examples/carla/NHTSA_Scenarios/intersection/intersection_01.scenic --2d --model scenic.simulators.carla.model --simulate """ ################################# diff --git a/examples/carla/NHTSA_Scenarios/intersection/intersection_02.scenic b/examples/carla/NHTSA_Scenarios/intersection/intersection_02.scenic index 0d4063ee5..697e302b8 100644 --- a/examples/carla/NHTSA_Scenarios/intersection/intersection_02.scenic +++ b/examples/carla/NHTSA_Scenarios/intersection/intersection_02.scenic @@ -5,6 +5,9 @@ DESCRIPTION: Ego vehicle makes a left turn at 4-way intersection and must suddenly stop to avoid collision when adversary vehicle from opposite lane goes straight. SOURCE: NHSTA, #30 + +To run this file using the Carla simulator: + scenic examples/carla/NHTSA_Scenarios/intersection/intersection_02.scenic --2d --model scenic.simulators.carla.model --simulate """ ################################# diff --git a/examples/carla/NHTSA_Scenarios/intersection/intersection_03.scenic b/examples/carla/NHTSA_Scenarios/intersection/intersection_03.scenic index 6e3af7870..859898bec 100644 --- a/examples/carla/NHTSA_Scenarios/intersection/intersection_03.scenic +++ b/examples/carla/NHTSA_Scenarios/intersection/intersection_03.scenic @@ -5,6 +5,9 @@ DESCRIPTION: Ego vehicle either goes straight or makes a left turn at 4-way intersection and must suddenly stop to avoid collision when adversary vehicle from lateral lane continues straight. SOURCE: NHSTA, #28 #29 + +To run this file using the Carla simulator: + scenic examples/carla/NHTSA_Scenarios/intersection/intersection_03.scenic --2d --model scenic.simulators.carla.model --simulate """ ################################# diff --git a/examples/carla/NHTSA_Scenarios/intersection/intersection_04.scenic b/examples/carla/NHTSA_Scenarios/intersection/intersection_04.scenic index 12820cff1..4ef689d10 100644 --- a/examples/carla/NHTSA_Scenarios/intersection/intersection_04.scenic +++ b/examples/carla/NHTSA_Scenarios/intersection/intersection_04.scenic @@ -5,6 +5,9 @@ DESCRIPTION: Ego vehicle either goes straight or makes a left turn at 4-way intersection and must suddenly stop to avoid collision when adversary vehicle from lateral lane makes a left turn. SOURCE: NHSTA, #28 #29 + +To run this file using the Carla simulator: + scenic examples/carla/NHTSA_Scenarios/intersection/intersection_04.scenic --2d --model scenic.simulators.carla.model --simulate """ ################################# diff --git a/examples/carla/NHTSA_Scenarios/intersection/intersection_05.scenic b/examples/carla/NHTSA_Scenarios/intersection/intersection_05.scenic index bba6bd19c..eff0df4d9 100644 --- a/examples/carla/NHTSA_Scenarios/intersection/intersection_05.scenic +++ b/examples/carla/NHTSA_Scenarios/intersection/intersection_05.scenic @@ -4,6 +4,9 @@ AUTHOR: Francis Indaheng, findaheng@berkeley.edu DESCRIPTION: Ego vehicle makes a right turn at 4-way intersection while adversary vehicle from opposite lane makes a left turn. SOURCE: NHSTA, #25 + +To run this file using the Carla simulator: + scenic examples/carla/NHTSA_Scenarios/intersection/intersection_05.scenic --2d --model scenic.simulators.carla.model --simulate """ ################################# diff --git a/examples/carla/NHTSA_Scenarios/intersection/intersection_06.scenic b/examples/carla/NHTSA_Scenarios/intersection/intersection_06.scenic index cb749407f..5c74c5f34 100644 --- a/examples/carla/NHTSA_Scenarios/intersection/intersection_06.scenic +++ b/examples/carla/NHTSA_Scenarios/intersection/intersection_06.scenic @@ -4,6 +4,9 @@ AUTHOR: Francis Indaheng, findaheng@berkeley.edu DESCRIPTION: Ego vehicle makes a right turn at 4-way intersection while adversary vehicle from lateral lane goes straight. SOURCE: NHSTA, #25 #26 + +To run this file using the Carla simulator: + scenic examples/carla/NHTSA_Scenarios/intersection/intersection_06.scenic --2d --model scenic.simulators.carla.model --simulate """ ################################# diff --git a/examples/carla/NHTSA_Scenarios/intersection/intersection_07.scenic b/examples/carla/NHTSA_Scenarios/intersection/intersection_07.scenic index d7d84f19e..22551f68e 100644 --- a/examples/carla/NHTSA_Scenarios/intersection/intersection_07.scenic +++ b/examples/carla/NHTSA_Scenarios/intersection/intersection_07.scenic @@ -5,6 +5,9 @@ DESCRIPTION: Ego vehicle makes a left turn at 3-way intersection and must suddenly stop to avoid collision when adversary vehicle from lateral lane continues straight. SOURCE: NHSTA, #30 + +To run this file using the Carla simulator: + scenic examples/carla/NHTSA_Scenarios/intersection/intersection_07.scenic --2d --model scenic.simulators.carla.model --simulate """ ################################# diff --git a/examples/carla/NHTSA_Scenarios/intersection/intersection_08.scenic b/examples/carla/NHTSA_Scenarios/intersection/intersection_08.scenic index 5c4e9cdc2..89e1482ec 100644 --- a/examples/carla/NHTSA_Scenarios/intersection/intersection_08.scenic +++ b/examples/carla/NHTSA_Scenarios/intersection/intersection_08.scenic @@ -5,6 +5,9 @@ DESCRIPTION: Ego vehicle goes straight at 3-way intersection and must suddenly stop to avoid collision when adversary vehicle makes a left turn. SOURCE: NHSTA, #30 + +To run this file using the Carla simulator: + scenic examples/carla/NHTSA_Scenarios/intersection/intersection_08.scenic --2d --model scenic.simulators.carla.model --simulate """ ################################# diff --git a/examples/carla/NHTSA_Scenarios/intersection/intersection_09.scenic b/examples/carla/NHTSA_Scenarios/intersection/intersection_09.scenic index 776c70808..e9c81ae4b 100644 --- a/examples/carla/NHTSA_Scenarios/intersection/intersection_09.scenic +++ b/examples/carla/NHTSA_Scenarios/intersection/intersection_09.scenic @@ -4,6 +4,9 @@ AUTHOR: Francis Indaheng, findaheng@berkeley.edu DESCRIPTION: Ego vehicle makes a right turn at 3-way intersection while adversary vehicle from lateral lane goes straight. SOURCE: NHSTA, #28 #29 + +To run this file using the Carla simulator: + scenic examples/carla/NHTSA_Scenarios/intersection/intersection_09.scenic --2d --model scenic.simulators.carla.model --simulate """ ################################# diff --git a/examples/carla/NHTSA_Scenarios/intersection/intersection_10.scenic b/examples/carla/NHTSA_Scenarios/intersection/intersection_10.scenic index 6aa8353a0..50f34b6ec 100644 --- a/examples/carla/NHTSA_Scenarios/intersection/intersection_10.scenic +++ b/examples/carla/NHTSA_Scenarios/intersection/intersection_10.scenic @@ -5,6 +5,9 @@ DESCRIPTION: Ego Vehicle waits at 4-way intersection while adversary vehicle in adjacent lane passes before performing a lane change to bypass a stationary vehicle waiting to make a left turn. SOURCE: NHSTA, #16 + +To run this file using the Carla simulator: + scenic examples/carla/NHTSA_Scenarios/intersection/intersection_10.scenic --2d --model scenic.simulators.carla.model --simulate """ ################################# diff --git a/examples/carla/NHTSA_Scenarios/pedestrian/pedestrian_01.scenic b/examples/carla/NHTSA_Scenarios/pedestrian/pedestrian_01.scenic index 7202f0e3d..c07454ccb 100644 --- a/examples/carla/NHTSA_Scenarios/pedestrian/pedestrian_01.scenic +++ b/examples/carla/NHTSA_Scenarios/pedestrian/pedestrian_01.scenic @@ -4,6 +4,9 @@ AUTHOR: Francis Indaheng, findaheng@berkeley.edu DESCRIPTION: Ego vehicle must suddenly stop to avoid collision when pedestrian crosses the road unexpectedly. SOURCE: Carla Challenge, #03 + +To run this file using the Carla simulator: + scenic examples/carla/NHTSA_Scenarios/pedestrian/pedestrian_01.scenic --2d --model scenic.simulators.carla.model --simulate """ ################################# diff --git a/examples/carla/NHTSA_Scenarios/pedestrian/pedestrian_02.scenic b/examples/carla/NHTSA_Scenarios/pedestrian/pedestrian_02.scenic index c0a9a0109..eb4c0ea40 100644 --- a/examples/carla/NHTSA_Scenarios/pedestrian/pedestrian_02.scenic +++ b/examples/carla/NHTSA_Scenarios/pedestrian/pedestrian_02.scenic @@ -4,6 +4,9 @@ AUTHOR: Francis Indaheng, findaheng@berkeley.edu DESCRIPTION: Both ego and adversary vehicles must suddenly stop to avoid collision when pedestrian crosses the road unexpectedly. SOURCE: Carla Challenge, #03 + +To run this file using the Carla simulator: + scenic examples/carla/NHTSA_Scenarios/pedestrian/pedestrian_02.scenic --2d --model scenic.simulators.carla.model --simulate """ ################################# diff --git a/examples/carla/NHTSA_Scenarios/pedestrian/pedestrian_03.scenic b/examples/carla/NHTSA_Scenarios/pedestrian/pedestrian_03.scenic index 06fadebc1..d07736569 100644 --- a/examples/carla/NHTSA_Scenarios/pedestrian/pedestrian_03.scenic +++ b/examples/carla/NHTSA_Scenarios/pedestrian/pedestrian_03.scenic @@ -4,6 +4,9 @@ AUTHOR: Francis Indaheng, findaheng@berkeley.edu DESCRIPTION: Ego vehicle makes a left turn at an intersection and must suddenly stop to avoid collision when pedestrian crosses the crosswalk. SOURCE: Carla Challenge, #04 + +To run this file using the Carla simulator: + scenic examples/carla/NHTSA_Scenarios/pedestrian/pedestrian_03.scenic --2d --model scenic.simulators.carla.model --simulate """ ################################# diff --git a/examples/carla/NHTSA_Scenarios/pedestrian/pedestrian_04.scenic b/examples/carla/NHTSA_Scenarios/pedestrian/pedestrian_04.scenic index 7d2aa7adb..edc9ff90c 100644 --- a/examples/carla/NHTSA_Scenarios/pedestrian/pedestrian_04.scenic +++ b/examples/carla/NHTSA_Scenarios/pedestrian/pedestrian_04.scenic @@ -4,6 +4,9 @@ AUTHOR: Francis Indaheng, findaheng@berkeley.edu DESCRIPTION: Ego vehicle makes a right turn at an intersection and must yield when pedestrian crosses the crosswalk. SOURCE: Carla Challenge, #04 + +To run this file using the Carla simulator: + scenic examples/carla/NHTSA_Scenarios/pedestrian/pedestrian_04.scenic --2d --model scenic.simulators.carla.model --simulate """ ################################# diff --git a/examples/carla/NHTSA_Scenarios/pedestrian/pedestrian_05.scenic b/examples/carla/NHTSA_Scenarios/pedestrian/pedestrian_05.scenic index 4d1776bb9..b2b6dbb4a 100644 --- a/examples/carla/NHTSA_Scenarios/pedestrian/pedestrian_05.scenic +++ b/examples/carla/NHTSA_Scenarios/pedestrian/pedestrian_05.scenic @@ -4,6 +4,9 @@ AUTHOR: Francis Indaheng, findaheng@berkeley.edu DESCRIPTION: Ego vehicle goes straight at an intersection and must yield when pedestrian crosses the crosswalk. SOURCE: Carla Challenge, #04 + +To run this file using the Carla simulator: + scenic examples/carla/NHTSA_Scenarios/pedestrian/pedestrian_05.scenic --2d --model scenic.simulators.carla.model --simulate """ ################################# diff --git a/examples/carla/OAS_Scenarios/oas_scenario_05.scenic b/examples/carla/OAS_Scenarios/oas_scenario_05.scenic index f3890d5d6..4573b8376 100644 --- a/examples/carla/OAS_Scenarios/oas_scenario_05.scenic +++ b/examples/carla/OAS_Scenarios/oas_scenario_05.scenic @@ -1,6 +1,9 @@ """ Scenario Description Voyage OAS Scenario Unique ID: 2-2-XX-CF-STR-CAR:Pa>E:03 The lead car suddenly stops and then resumes moving forward + +To run this file using the Carla simulator: + scenic examples/carla/NHTSA_Scenarios/OAS_Scenarios/oas_scenario_05.scenic --2d --model scenic.simulators.carla.model --simulate """ param map = localPath('../../../assets/maps/CARLA/Town01.xodr') # or other CARLA map that definitely works diff --git a/examples/carla/OAS_Scenarios/oas_scenario_06.scenic b/examples/carla/OAS_Scenarios/oas_scenario_06.scenic index dfcb3ad8e..f9c0b8aeb 100644 --- a/examples/carla/OAS_Scenarios/oas_scenario_06.scenic +++ b/examples/carla/OAS_Scenarios/oas_scenario_06.scenic @@ -2,6 +2,9 @@ Voyage OAS Scenario Unique ID: 2-2-XX-CF-STR-CAR:Pa>E:03 The car ahead of ego that is badly parked over the sidewalk cuts into ego vehicle's lane. This scenario may fail if there exists any obstacle (e.g. fences) on the sidewalk + +To run this file using the Carla simulator: + scenic examples/carla/NHTSA_Scenarios/OAS_Scenarios/oas_scenario_06.scenic --2d --model scenic.simulators.carla.model --simulate """ diff --git a/examples/carla/adjacentLanes.scenic b/examples/carla/adjacentLanes.scenic index 9015e077a..4bf11661e 100644 --- a/examples/carla/adjacentLanes.scenic +++ b/examples/carla/adjacentLanes.scenic @@ -1,3 +1,8 @@ +''' +To run this file using the Carla simulator: + scenic examples/carla/adjacentLanes.scenic --2d --model scenic.simulators.carla.model +''' + param map = localPath('../../assets/maps/CARLA/Town03.xodr') model scenic.simulators.carla.model diff --git a/examples/carla/adjacentOpposingPair.scenic b/examples/carla/adjacentOpposingPair.scenic index 8f6c0ab80..f28044289 100644 --- a/examples/carla/adjacentOpposingPair.scenic +++ b/examples/carla/adjacentOpposingPair.scenic @@ -1,3 +1,7 @@ +''' +To run this file using the Carla simulator: + scenic examples/carla/adjacentOpposingPair.scenic --2d --model scenic.simulators.carla.model +''' param map = localPath('../../assets/maps/CARLA/Town01.xodr') model scenic.simulators.carla.model diff --git a/examples/carla/backgroundActivity.scenic b/examples/carla/backgroundActivity.scenic index f2cfb906b..8b29e1f01 100644 --- a/examples/carla/backgroundActivity.scenic +++ b/examples/carla/backgroundActivity.scenic @@ -2,6 +2,9 @@ Background Activity The simulation is filled with vehicles that freely roam around the town. This simulates normal driving conditions, without any abnormal behaviors + +To run this file using the Carla simulator: + scenic examples/carla/backgroundActivity.scenic --2d --model scenic.simulators.carla.model --simulate """ # SET MAP AND MODEL (i.e. definitions of all referenceable vehicle types, road library, etc) param map = localPath('../../assets/maps/CARLA/Town05.xodr') # or other CARLA map that definitely works @@ -18,6 +21,10 @@ behavior EgoBehavior(speed=10): interrupt when withinDistanceToObjsInLane(self, 10): take SetBrakeAction(1.0) +# PEDESTRIAN BEHAVIOR: cross the street +behavior PedestrianBehavior(min_speed=1, threshold=10): + do CrossingBehavior(ego, min_speed, threshold) + ## DEFINING SPATIAL RELATIONS # Please refer to scenic/domains/driving/roads.py how to access detailed road infrastructure # 'network' is the 'class Network' object in roads.py @@ -36,7 +43,7 @@ background_walkers = [] for _ in range(10): sideWalk = Uniform(*network.sidewalks) background_walker = new Pedestrian in sideWalk, - with behavior WalkBehavior() + with behavior PedestrianBehavior() background_walkers.append(background_walker) diff --git a/examples/carla/car.scenic b/examples/carla/car.scenic index edf60b5a9..1f4c68cb7 100644 --- a/examples/carla/car.scenic +++ b/examples/carla/car.scenic @@ -1,3 +1,7 @@ +''' +To run this file using the Carla simulator: + scenic examples/carla/car.scenic --2d --model scenic.simulators.carla.model --simulate +''' param map = localPath('../../assets/maps/CARLA/Town01.xodr') model scenic.simulators.carla.model diff --git a/examples/carla/manual_control/carlaChallenge1.scenic b/examples/carla/manual_control/carlaChallenge1.scenic index 763385dfe..bb024a335 100644 --- a/examples/carla/manual_control/carlaChallenge1.scenic +++ b/examples/carla/manual_control/carlaChallenge1.scenic @@ -3,12 +3,15 @@ Traffic Scenario 01. Control loss without previous action. The ego-vehicle loses control due to bad conditions on the road and it must recover, coming back to its original lane. + +To run this file using the Carla simulator: + scenic examples/carla/manual_control/carlaChallenge1.scenic --2d --model scenic.simulators.carla.model --simulate """ ## SET MAP AND MODEL (i.e. definitions of all referenceable vehicle types, road library, etc) param map = localPath('../../../assets/maps/CARLA/Town01.xodr') # or other CARLA map that definitely works param carla_map = 'Town01' -param render = '0' +param render = 0 model scenic.simulators.carla.model ## CONSTANTS diff --git a/examples/carla/manual_control/carlaChallenge3_dynamic.scenic b/examples/carla/manual_control/carlaChallenge3_dynamic.scenic index 3410d5cae..1ae36f3f4 100644 --- a/examples/carla/manual_control/carlaChallenge3_dynamic.scenic +++ b/examples/carla/manual_control/carlaChallenge3_dynamic.scenic @@ -3,12 +3,15 @@ Traffic Scenario 03 (dynamic). Obstacle avoidance without prior action. The ego-vehicle encounters an obstacle / unexpected entity on the road and must perform an emergency brake or an avoidance maneuver. + +To run this file using the Carla simulator: + scenic examples/carla/manual_control/carlaChallenge3_dynamic.scenic --2d --model scenic.simulators.carla.model --simulate """ # SET MAP AND MODEL (i.e. definitions of all referenceable vehicle types, road library, etc) param map = localPath('../../../assets/maps/CARLA/Town05.xodr') # or other CARLA map that definitely works param carla_map = 'Town05' -param render = '0' +param render = 0 model scenic.simulators.carla.model # CONSTANTS diff --git a/examples/carla/manual_control/carlaChallenge4.scenic b/examples/carla/manual_control/carlaChallenge4.scenic index 100d091bf..4b3eb48e5 100644 --- a/examples/carla/manual_control/carlaChallenge4.scenic +++ b/examples/carla/manual_control/carlaChallenge4.scenic @@ -3,12 +3,15 @@ Traffic Scenario 04. Obstacle avoidance without prior action. The ego-vehicle encounters an obstacle / unexpected entity on the road and must perform an emergency brake or an avoidance maneuver. + +To run this file using the Carla simulator: + scenic examples/carla/manual_control/carlaChallenge4.scenic --2d --model scenic.simulators.carla.model --simulate """ ## SET MAP AND MODEL (i.e. definitions of all referenceable vehicle types, road library, etc) param map = localPath('../../../assets/maps/CARLA/Town01.xodr') # or other CARLA map that definitely works param carla_map = 'Town01' -param render = '0' +param render = 0 model scenic.simulators.carla.model ## CONSTANTS diff --git a/examples/carla/manual_control/carlaChallenge7.scenic b/examples/carla/manual_control/carlaChallenge7.scenic index 68f123f67..49477f9c7 100644 --- a/examples/carla/manual_control/carlaChallenge7.scenic +++ b/examples/carla/manual_control/carlaChallenge7.scenic @@ -3,12 +3,15 @@ Traffic Scenario 07. Crossing traffic running a red light at an intersection. The ego-vehicle is going straight at an intersection but a crossing vehicle runs a red light, forcing the ego-vehicle to avoid the collision. + +To run this file using the Carla simulator: + scenic examples/carla/manual_control/carlaChallenge7.scenic --2d --model scenic.simulators.carla.model --simulate """ ## SET MAP AND MODEL (i.e. definitions of all referenceable vehicle types, road library, etc) param map = localPath('../../../assets/maps/CARLA/Town05.xodr') # or other CARLA map that definitely works param carla_map = 'Town05' -param render = '0' +param render = 0 model scenic.simulators.carla.model ## CONSTANTS diff --git a/examples/carla/pedestrian.scenic b/examples/carla/pedestrian.scenic index 9559a1d04..3298d69d6 100644 --- a/examples/carla/pedestrian.scenic +++ b/examples/carla/pedestrian.scenic @@ -1,6 +1,10 @@ +''' +To run this file using the Carla simulator: + scenic examples/carla/pedestrian.scenic --2d --model scenic.simulators.carla.model --simulate +''' param map = localPath('../../assets/maps/CARLA/Town03.xodr') param carla_map = 'Town03' model scenic.simulators.carla.model ego = new Car -new Pedestrian on visible sidewalk +new Pedestrian on visible sidewalk \ No newline at end of file diff --git a/examples/carla/trafficLights.scenic b/examples/carla/trafficLights.scenic index 8bfc12b0f..17013211f 100644 --- a/examples/carla/trafficLights.scenic +++ b/examples/carla/trafficLights.scenic @@ -1,5 +1,8 @@ """ Scenario Description Example scenario of traffic lights management. + +To run this file using the Carla simulator: + scenic examples/carla/trafficLights.scenic --2d --model scenic.simulators.carla.model --simulate """ ## SET MAP AND MODEL (i.e. definitions of all referenceable vehicle types, road library, etc) diff --git a/examples/driving/Carla_Challenge/carlaChallenge2.scenic b/examples/driving/Carla_Challenge/carlaChallenge2.scenic index 61c689f77..f9e95833d 100644 --- a/examples/driving/Carla_Challenge/carlaChallenge2.scenic +++ b/examples/driving/Carla_Challenge/carlaChallenge2.scenic @@ -3,6 +3,9 @@ Based on 2019 Carla Challenge Traffic Scenario 02. Leading vehicle decelerates suddently due to an obstacle and ego-vehicle must react, performing an emergency brake or an avoidance maneuver. Note: The scenario may fail if the leadCar or the ego get past the intersection while following the roadDirection + +To run this file using the Carla simulator: + scenic examples/driving/Carla_Challenge/carlaChallenge2.scenic --2d --model scenic.simulators.carla.model --simulate """ param map = localPath('../../../assets/maps/CARLA/Town07.xodr') # or other CARLA map that definitely works param carla_map = 'Town07' diff --git a/examples/driving/Carla_Challenge/carlaChallenge3.scenic b/examples/driving/Carla_Challenge/carlaChallenge3.scenic index 37256b334..c232683ca 100644 --- a/examples/driving/Carla_Challenge/carlaChallenge3.scenic +++ b/examples/driving/Carla_Challenge/carlaChallenge3.scenic @@ -2,6 +2,9 @@ Based on 2019 Carla Challenge Traffic Scenario 03. Leading vehicle decelerates suddenly due to an obstacle and ego-vehicle must react, performing an emergency brake or an avoidance maneuver. + +To run this file using the Carla simulator: + scenic examples/driving/Carla_Challenge/carlaChallenge3.scenic --2d --model scenic.simulators.carla.model --simulate """ param map = localPath('../../../assets/maps/CARLA/Town01.xodr') # or other CARLA map that definitely works param carla_map = 'Town01' diff --git a/examples/driving/OAS_Scenarios/oas_scenario_03.scenic b/examples/driving/OAS_Scenarios/oas_scenario_03.scenic index 8c103dfcd..79ad88961 100644 --- a/examples/driving/OAS_Scenarios/oas_scenario_03.scenic +++ b/examples/driving/OAS_Scenarios/oas_scenario_03.scenic @@ -1,6 +1,9 @@ """ Scenario Description Voyage OAS Scenario Unique ID: 2-2-XX-CF-STR-CAR The ego vehicle follows the lead car + +To run this file using the Carla simulator: + scenic examples/driving/OAS_Scenarios/oas_scenario_03.scenic --2d --model scenic.simulators.carla.model --simulate """ param map = localPath('../../../assets/maps/CARLA/Town04.xodr') # or other CARLA map that definitely works diff --git a/examples/driving/OAS_Scenarios/oas_scenario_04.scenic b/examples/driving/OAS_Scenarios/oas_scenario_04.scenic index 0b497214c..3dc09a7cf 100644 --- a/examples/driving/OAS_Scenarios/oas_scenario_04.scenic +++ b/examples/driving/OAS_Scenarios/oas_scenario_04.scenic @@ -1,6 +1,9 @@ """ Scenario Description Voyage OAS Scenario Unique ID: 2-2-XX-CF-STR-CAR:01 The ego vehicle follows the lead car which suddenly stops + +To run this file using the Carla simulator: + scenic examples/driving/OAS_Scenarios/oas_scenario_04.scenic --2d --model scenic.simulators.carla.model --simulate """ param map = localPath('../../../assets/maps/CARLA/Town07.xodr') # or other CARLA map that definitely works diff --git a/examples/driving/OAS_Scenarios/oas_scenario_28.scenic b/examples/driving/OAS_Scenarios/oas_scenario_28.scenic index 72fa8dc2f..26ff7a592 100644 --- a/examples/driving/OAS_Scenarios/oas_scenario_28.scenic +++ b/examples/driving/OAS_Scenarios/oas_scenario_28.scenic @@ -3,6 +3,9 @@ Voyage OAS Scenario Unique ID: 3-2-ESW-I-STR-CAR:S>W:02 At three-way intersection. The ego vehicle goes straight. The other car, on the other leg of the intersection, takes a left turn first because it is closer to the intersection. + +To run this file using the Carla simulator: + scenic examples/driving/OAS_Scenarios/oas_scenario_28.scenic --2d --model scenic.simulators.carla.model --simulate """ param map = localPath('../../../assets/maps/CARLA/Town05.xodr') # or other CARLA map that definitely works diff --git a/examples/driving/OAS_Scenarios/oas_scenario_29.scenic b/examples/driving/OAS_Scenarios/oas_scenario_29.scenic index 212b92ac6..2561a70e3 100644 --- a/examples/driving/OAS_Scenarios/oas_scenario_29.scenic +++ b/examples/driving/OAS_Scenarios/oas_scenario_29.scenic @@ -3,6 +3,9 @@ Voyage OAS Scenario Unique ID: 3-2-NSW-I-L-CAR:S>W:02 At 3 way intersection. The ego car turns left. The other car, on a different leg of the intersection, has the right of the way and makes a left turn first because it is closer to the intersection. + +To run this file using the Carla simulator: + scenic examples/driving/OAS_Scenarios/oas_scenario_29.scenic --2d --model scenic.simulators.carla.model --simulate """ param map = localPath('../../../assets/maps/CARLA/Town05.xodr') # or other CARLA map that definitely works param carla_map = 'Town05' diff --git a/examples/driving/OAS_Scenarios/oas_scenario_30.scenic b/examples/driving/OAS_Scenarios/oas_scenario_30.scenic index 4ddb712df..30b4cf30d 100644 --- a/examples/driving/OAS_Scenarios/oas_scenario_30.scenic +++ b/examples/driving/OAS_Scenarios/oas_scenario_30.scenic @@ -3,6 +3,9 @@ Voyage OAS Scenario Unique ID: 3-2-NWS-I-L-CAR:S>W:01 At 3 way intersection. The ego car turns left. The other car approaches from a different leg of the intersection to make a left turn, but ego has the right of the way because it is closer to the intersection. + +To run this file using the Carla simulator: + scenic examples/driving/OAS_Scenarios/oas_scenario_30.scenic --2d --model scenic.simulators.carla.model --simulate """ param map = localPath('../../../assets/maps/CARLA/Town10HD.xodr') # or other CARLA map that definitely works diff --git a/examples/driving/OAS_Scenarios/oas_scenario_32.scenic b/examples/driving/OAS_Scenarios/oas_scenario_32.scenic index 9a63373ab..985325df9 100644 --- a/examples/driving/OAS_Scenarios/oas_scenario_32.scenic +++ b/examples/driving/OAS_Scenarios/oas_scenario_32.scenic @@ -2,6 +2,9 @@ Voyage OAS Scenario Unique ID: 3-2-W-I-L-CAR:N>S At 3-way intersection, ego turns left and the other car on a different leg of the intersection goes straight. There is no requirement on which vehicle has the right of the way. + +To run this file using the Carla simulator: + scenic examples/driving/OAS_Scenarios/oas_scenario_32.scenic --2d --model scenic.simulators.carla.model --simulate """ param map = localPath('../../../assets/maps/CARLA/Town10HD.xodr') # or other CARLA map that definitely works diff --git a/examples/driving/badlyParkedCarPullingIn.scenic b/examples/driving/badlyParkedCarPullingIn.scenic index dd36ede2b..aedfcb667 100644 --- a/examples/driving/badlyParkedCarPullingIn.scenic +++ b/examples/driving/badlyParkedCarPullingIn.scenic @@ -1,3 +1,8 @@ +''' +To run this file using the Carla simulator: + scenic examples/driving/badlyParkedCarPullingIn.scenic --2d --model scenic.simulators.carla.model --simulate +''' + param map = localPath('../../assets/maps/CARLA/Town05.xodr') param carla_map = 'Town05' param time_step = 1.0/10 diff --git a/examples/driving/car.scenic b/examples/driving/car.scenic index 91743ac53..c1d0e3d54 100644 --- a/examples/driving/car.scenic +++ b/examples/driving/car.scenic @@ -1,3 +1,7 @@ +''' +To run this file using the Carla simulator: + scenic examples/driving/car.scenic --2d --model scenic.simulators.carla.model --simulate +''' param map = localPath('../../assets/maps/CARLA/Town01.xodr') diff --git a/examples/driving/pedestrian.scenic b/examples/driving/pedestrian.scenic index e564107b4..fb938284c 100644 --- a/examples/driving/pedestrian.scenic +++ b/examples/driving/pedestrian.scenic @@ -1,3 +1,7 @@ +''' +To run this file using the Carla simulator: + scenic examples/driving/pedestrian.scenic --2d --model scenic.simulators.carla.model --simulate +''' param map = localPath('../../assets/maps/CARLA/Town01.xodr') param carla_map = 'Town01' diff --git a/examples/driving/pedestrianAcrossRoad.scenic b/examples/driving/pedestrianAcrossRoad.scenic index 2bc6f9329..304608d01 100644 --- a/examples/driving/pedestrianAcrossRoad.scenic +++ b/examples/driving/pedestrianAcrossRoad.scenic @@ -1,3 +1,7 @@ +''' +To run this file using the Carla simulator: + scenic examples/driving/pedestrianAcrossRoad.scenic --2d --model scenic.simulators.carla.model --simulate +''' param map = localPath('../../assets/maps/CARLA/Town01.xodr') From 8a2a7e9a607bd9732f69ab80d9db077d9dca810c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Ba=C3=B1uelos?= <32311654+abanuelo@users.noreply.github.com> Date: Fri, 20 Dec 2024 15:31:23 -0800 Subject: [PATCH 09/73] feat: adding webots dynamic scenario tests (#231) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: adding webots dynamic simulation, tbd tested * feat: Adding functional webots tests * fix: reformatting with black and isort and deleting unnecessary files * fix: improving code coverage * fix: black formatting test_webots.py * Adding codecov yml updates from main branch * fix: also including tests directory to ignore for coverage, not relevant * fix: adding checks for environment variables * fix: adjusting fixture call * fix: adding webots extension * fix: adding batch option to avoid popups * Add timeout to Webots dynamic test --------- Co-authored-by: Armando Bañuelos Co-authored-by: Ubuntu Co-authored-by: Armando Banuelos Co-authored-by: Armando Banuelos Co-authored-by: Lola Marrero <110120745+lola831@users.noreply.github.com> --- tests/conftest.py | 11 +++++ .../simulators/webots/dynamic/dynamic.scenic | 27 ++++++++++++ .../scenic_supervisor/scenic_supervisor.py | 30 +++++++++++++ .../webots_data/protos/ScenicObject.proto | 28 ++++++++++++ .../protos/ScenicObjectWithPhysics.proto | 34 +++++++++++++++ .../dynamic/webots_data/worlds/world.wbt | 34 +++++++++++++++ tests/simulators/webots/test_webots.py | 43 +++++++++++++++++++ 7 files changed, 207 insertions(+) create mode 100644 tests/simulators/webots/dynamic/dynamic.scenic create mode 100644 tests/simulators/webots/dynamic/webots_data/controllers/scenic_supervisor/scenic_supervisor.py create mode 100644 tests/simulators/webots/dynamic/webots_data/protos/ScenicObject.proto create mode 100644 tests/simulators/webots/dynamic/webots_data/protos/ScenicObjectWithPhysics.proto create mode 100644 tests/simulators/webots/dynamic/webots_data/worlds/world.wbt diff --git a/tests/conftest.py b/tests/conftest.py index 3fe2f4d92..86493a3f1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -51,6 +51,17 @@ def loader(relpath, **kwargs): return loader +@pytest.fixture +def launchWebots(): + DISPLAY = os.environ.get("DISPLAY") + if not DISPLAY: + pytest.skip("DISPLAY env variable not set.") + WEBOTS_ROOT = os.environ.get("WEBOTS_ROOT") + if not WEBOTS_ROOT: + pytest.skip("WEBOTS_ROOT env variable not set.") + return WEBOTS_ROOT + + ## Command-line options diff --git a/tests/simulators/webots/dynamic/dynamic.scenic b/tests/simulators/webots/dynamic/dynamic.scenic new file mode 100644 index 000000000..96c897102 --- /dev/null +++ b/tests/simulators/webots/dynamic/dynamic.scenic @@ -0,0 +1,27 @@ +""" +Create a Webots cube in air and have it drop +""" + +model scenic.simulators.webots.model + +class Floor(Object): + width: 5 + length: 5 + height: 0.01 + position: (0,0,0) + color: [0.785, 0.785, 0.785] + +class Block(WebotsObject): + webotsAdhoc: {'physics': True} + shape: BoxShape() + width: 0.2 + length: 0.2 + height: 0.2 + density: 100 + color: [1, 0.502, 0] + +floor = new Floor +ego = new Block at (0, 0, 0.5) #above floor by 0.5 + +terminate when ego.z < 0.1 +record (ego.z) as BlockPosition \ No newline at end of file diff --git a/tests/simulators/webots/dynamic/webots_data/controllers/scenic_supervisor/scenic_supervisor.py b/tests/simulators/webots/dynamic/webots_data/controllers/scenic_supervisor/scenic_supervisor.py new file mode 100644 index 000000000..8e875c6a8 --- /dev/null +++ b/tests/simulators/webots/dynamic/webots_data/controllers/scenic_supervisor/scenic_supervisor.py @@ -0,0 +1,30 @@ +import os + +from controller import Supervisor + +import scenic +from scenic.simulators.webots import WebotsSimulator + +WEBOTS_RESULT_FILE_PATH = f"{os.path.dirname(__file__)}/../../../results.txt" + + +def send_results(data): + with open(WEBOTS_RESULT_FILE_PATH, "w") as file: + file.write(data) + + +supervisor = Supervisor() +simulator = WebotsSimulator(supervisor) + +path = supervisor.getCustomData() +print(f"Loading Scenic scenario {path}") +scenario = scenic.scenarioFromFile(path) + +scene, _ = scenario.generate() +simulation = simulator.simulate(scene, verbosity=2) +block_movements = simulation.result.records["BlockPosition"] +first_block_movement = block_movements[0] +last_block_movement = block_movements[-1] +blocks = [first_block_movement, last_block_movement] +supervisor.simulationQuit(status="finished") +send_results(str(blocks)) diff --git a/tests/simulators/webots/dynamic/webots_data/protos/ScenicObject.proto b/tests/simulators/webots/dynamic/webots_data/protos/ScenicObject.proto new file mode 100644 index 000000000..605321782 --- /dev/null +++ b/tests/simulators/webots/dynamic/webots_data/protos/ScenicObject.proto @@ -0,0 +1,28 @@ +#VRML_SIM R2023a utf8 + +PROTO ScenicObject [ + field SFVec3f translation 0 0 0 + field SFRotation rotation 0 0 1 0 + field SFString name "solid" + field SFVec3f angularVelocity 0 0 0 + field MFString url "" +] +{ + Solid { + translation IS translation + rotation IS rotation + name IS name + angularVelocity IS angularVelocity + children [ + CadShape { + url IS url + castShadows FALSE + } + ] + boundingObject Shape { + geometry Mesh { + url IS url + } + } + } +} diff --git a/tests/simulators/webots/dynamic/webots_data/protos/ScenicObjectWithPhysics.proto b/tests/simulators/webots/dynamic/webots_data/protos/ScenicObjectWithPhysics.proto new file mode 100644 index 000000000..cf1d42934 --- /dev/null +++ b/tests/simulators/webots/dynamic/webots_data/protos/ScenicObjectWithPhysics.proto @@ -0,0 +1,34 @@ +#VRML_SIM R2023a utf8 + +PROTO ScenicObjectWithPhysics [ + field SFVec3f translation 0 0 0 + field SFRotation rotation 0 0 1 0 + field SFString name "solid" + field SFVec3f angularVelocity 0 0 0 + field MFString url "" + field SFFloat density 1000 # kg / m^3 +] +{ + Solid { + translation IS translation + rotation IS rotation + name IS name + angularVelocity IS angularVelocity + children [ + CadShape { + url IS url + castShadows FALSE + } + ] + boundingObject Shape { + geometry Mesh { + url IS url + } + } + physics Physics { + # density will be set by the simulator + density IS density + mass -1 + } + } +} diff --git a/tests/simulators/webots/dynamic/webots_data/worlds/world.wbt b/tests/simulators/webots/dynamic/webots_data/worlds/world.wbt new file mode 100644 index 000000000..0ab0be278 --- /dev/null +++ b/tests/simulators/webots/dynamic/webots_data/worlds/world.wbt @@ -0,0 +1,34 @@ +#VRML_SIM R2023a utf8 + +EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/R2023a/projects/objects/backgrounds/protos/TexturedBackground.proto" +EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/R2023a/projects/objects/floors/protos/Floor.proto" +IMPORTABLE EXTERNPROTO "../protos/ScenicObject.proto" +IMPORTABLE EXTERNPROTO "../protos/ScenicObjectWithPhysics.proto" + +WorldInfo { + gravity 3 + basicTimeStep 16 + contactProperties [ + ContactProperties { + coulombFriction [ + 0.8 + ] + } + ] +} +Viewpoint { + orientation -0.19729161510865992 0.07408415124219735 0.977541588446517 2.4380019050245862 + position 6.683615307234937 -5.5006366813466805 4.16419445995153 +} +TexturedBackground { +} +Floor { + name "FLOOR" + size 5 5 +} +Robot { + name "Supervisor" + controller "scenic_supervisor" + customData "../../../dynamic.scenic" + supervisor TRUE +} diff --git a/tests/simulators/webots/test_webots.py b/tests/simulators/webots/test_webots.py index e7904532a..760cebf01 100644 --- a/tests/simulators/webots/test_webots.py +++ b/tests/simulators/webots/test_webots.py @@ -1,7 +1,50 @@ +import os +import subprocess + import pytest from tests.utils import pickle_test, sampleScene, tryPickling +WEBOTS_RESULTS_FILE_PATH = f"{os.path.dirname(__file__)}/dynamic/results.txt" +WEBOTS_WORLD_FILE_PATH = ( + f"{os.path.dirname(__file__)}/dynamic/webots_data/worlds/world.wbt" +) + + +def receive_results(): + with open(WEBOTS_RESULTS_FILE_PATH, "r") as file: + content = file.read() + return content + + +def cleanup_results(): + command = f"rm -f {WEBOTS_RESULTS_FILE_PATH}" + subprocess.run(command, shell=True) + + +def test_dynamics_scenarios(launchWebots): + WEBOTS_ROOT = launchWebots + cleanup_results() + + timeout_seconds = 300 + + command = f"bash {WEBOTS_ROOT}/webots --no-rendering --minimize --batch {WEBOTS_WORLD_FILE_PATH}" + + try: + subprocess.run(command, shell=True, timeout=timeout_seconds) + except subprocess.TimeoutExpired: + pytest.fail(f"Webots test exceeded the timeout of {timeout_seconds} seconds and failed.") + + data = receive_results() + assert data != None + start_z = float(data.split(",")[1].strip(" )]")) + end_z = float(data.split(",")[3].strip(" )]")) + assert start_z == 0.5 + assert start_z > end_z + expected_value = 0.09 + tolerance = 0.01 + assert end_z == pytest.approx(expected_value, abs=tolerance) + def test_basic(loadLocalScenario): scenario = loadLocalScenario("basic.scenic") From dcd0213bf500d26aa559e7946654c9f25440a025 Mon Sep 17 00:00:00 2001 From: Lola Marrero <110120745+lola831@users.noreply.github.com> Date: Tue, 14 Jan 2025 09:21:46 -0800 Subject: [PATCH 10/73] Apply black and isort to test-webots file (#324) --- tests/simulators/webots/test_webots.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/simulators/webots/test_webots.py b/tests/simulators/webots/test_webots.py index 760cebf01..838884b6c 100644 --- a/tests/simulators/webots/test_webots.py +++ b/tests/simulators/webots/test_webots.py @@ -27,14 +27,16 @@ def test_dynamics_scenarios(launchWebots): cleanup_results() timeout_seconds = 300 - + command = f"bash {WEBOTS_ROOT}/webots --no-rendering --minimize --batch {WEBOTS_WORLD_FILE_PATH}" try: subprocess.run(command, shell=True, timeout=timeout_seconds) except subprocess.TimeoutExpired: - pytest.fail(f"Webots test exceeded the timeout of {timeout_seconds} seconds and failed.") - + pytest.fail( + f"Webots test exceeded the timeout of {timeout_seconds} seconds and failed." + ) + data = receive_results() assert data != None start_z = float(data.split(",")[1].strip(" )]")) From 8495cc969a355237fab8c96f48608a541a50d738 Mon Sep 17 00:00:00 2001 From: Daniel Fremont Date: Tue, 21 Jan 2025 17:06:32 -0800 Subject: [PATCH 11/73] Use zizmor to scan for GHA security problems (#320) * apply GHA security improvements suggested by zizmor * run zizmor in CI * Update GH_REF reference in check_simulator_version_updates job --------- Co-authored-by: Lola Marrero <110120745+lola831@users.noreply.github.com> --- .github/workflows/check-formatting.yml | 2 ++ .github/workflows/on-call-reminder.yml | 2 ++ .github/workflows/run-coverage.yml | 2 ++ .github/workflows/run-simulators.yml | 5 ++-- .github/workflows/run-tests.yml | 7 ++++- .github/workflows/zizmor.yml | 37 ++++++++++++++++++++++++++ 6 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/zizmor.yml diff --git a/.github/workflows/check-formatting.yml b/.github/workflows/check-formatting.yml index 251ec326f..98d3a21b1 100644 --- a/.github/workflows/check-formatting.yml +++ b/.github/workflows/check-formatting.yml @@ -11,6 +11,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v3 + with: + persist-credentials: false - name: Run black to check formatting uses: psf/black@stable diff --git a/.github/workflows/on-call-reminder.yml b/.github/workflows/on-call-reminder.yml index 4a7e75882..54a7a6e3e 100644 --- a/.github/workflows/on-call-reminder.yml +++ b/.github/workflows/on-call-reminder.yml @@ -12,6 +12,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v3 + with: + persist-credentials: false - name: Set up Python uses: actions/setup-python@v4 diff --git a/.github/workflows/run-coverage.yml b/.github/workflows/run-coverage.yml index 34344184c..aa41f4e46 100644 --- a/.github/workflows/run-coverage.yml +++ b/.github/workflows/run-coverage.yml @@ -33,12 +33,14 @@ jobs: if: inputs.ref != '' with: ref: ${{ inputs.ref }} + persist-credentials: false - name: Checkout current branch uses: actions/checkout@v3 if: inputs.ref == '' with: ref: ${{ github.ref }} + persist-credentials: false - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 diff --git a/.github/workflows/run-simulators.yml b/.github/workflows/run-simulators.yml index 3b04f79df..89e0819fd 100644 --- a/.github/workflows/run-simulators.yml +++ b/.github/workflows/run-simulators.yml @@ -88,12 +88,13 @@ jobs: HOSTNAME: ${{ secrets.SSH_HOST }} USER_NAME: ${{ secrets.SSH_USERNAME }} GH_ACCESS_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }} + GH_REF: ${{ github.ref }} run: | echo "$PRIVATE_KEY" > private_key && chmod 600 private_key - ssh -o StrictHostKeyChecking=no -i private_key ${USER_NAME}@${HOSTNAME} ' + ssh -o StrictHostKeyChecking=no -o SendEnv=GH_REF -i private_key ${USER_NAME}@${HOSTNAME} ' cd /home/ubuntu/actions/ && rm -rf Scenic && - git clone --branch $(basename "${{ github.ref }}") --single-branch https://$GH_ACCESS_TOKEN@github.com/BerkeleyLearnVerify/Scenic.git && + git clone --branch $(basename "$GH_REF") --single-branch https://$GH_ACCESS_TOKEN@github.com/BerkeleyLearnVerify/Scenic.git && cd Scenic && python3 -m venv venv && source venv/bin/activate && diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index f24b21dd8..025145608 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -47,12 +47,14 @@ jobs: if: inputs.ref != '' with: ref: ${{ inputs.ref }} + persist-credentials: false - name: Checkout current branch uses: actions/checkout@v3 if: inputs.ref == '' with: ref: ${{ github.ref }} + persist-credentials: false - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 @@ -69,5 +71,8 @@ jobs: python -m pip install -e ".[test-full]" - name: Run pytest + env: + TEST_OPTIONS: ${{ inputs.options || (matrix.slow && '--no-graphics' || '--fast --no-graphics') }} + shell: sh run: | - pytest ${{ inputs.options || (matrix.slow && '--no-graphics' || '--fast --no-graphics') }} + pytest ${TEST_OPTIONS} diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 000000000..ecc69380d --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,37 @@ +name: GitHub Actions Security Analysis with zizmor 🌈 +# https://woodruffw.github.io/zizmor + +on: + push: + branches: ["main"] + pull_request: + branches: ["**"] + +jobs: + zizmor: + name: zizmor latest via PyPI + runs-on: ubuntu-latest + permissions: + security-events: write + # required for workflows in private repositories + contents: read + actions: read + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Install the latest version of uv + uses: astral-sh/setup-uv@v4 + + - name: Run zizmor 🌈 + run: uvx zizmor --format sarif . > results.sarif + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload SARIF file + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: results.sarif + category: zizmor From 394f205fa93efd761051a5c825040ee4a0910ca0 Mon Sep 17 00:00:00 2001 From: Lola Marrero <110120745+lola831@users.noreply.github.com> Date: Wed, 12 Feb 2025 20:11:10 -0800 Subject: [PATCH 12/73] Remove OpsGenie and maintenance bot on-call reminder script and workflow (replacing with Slack Round Robin) (#327) --- .github/slack_oncall_reminder.py | 70 -------------------------- .github/workflows/on-call-reminder.yml | 32 ------------ 2 files changed, 102 deletions(-) delete mode 100644 .github/slack_oncall_reminder.py delete mode 100644 .github/workflows/on-call-reminder.yml diff --git a/.github/slack_oncall_reminder.py b/.github/slack_oncall_reminder.py deleted file mode 100644 index f62707077..000000000 --- a/.github/slack_oncall_reminder.py +++ /dev/null @@ -1,70 +0,0 @@ -import argparse - -import requests -from slack_sdk import WebClient -from slack_sdk.errors import SlackApiError - - -def save_users(users_array): - users = {} - for user in users_array: - # NOTE: some apps, slackbots do not have emails to map to - profile = user["profile"] - if "email" in profile.keys(): - user_email = profile["email"] - username = user_email.split("@")[0] - users[username] = user - return users - - -def grab_whos_on_call(OPS_GENIE_API_TOKEN, ROTATION_SCHEDULE_ID): - url = f"https://api.opsgenie.com/v2/schedules/{ROTATION_SCHEDULE_ID}/on-calls" - headers = {"Authorization": f"GenieKey {OPS_GENIE_API_TOKEN}"} - response = requests.get(url, headers=headers) - if response.status_code == 200: - data = response.json() - else: - print(f"Request failed with status code {response.status_code}") - print("Response content:") - print(response.content.decode("utf-8")) - return data["data"]["onCallParticipants"][0]["name"].split("@")[0] - - -def postSlackMessage(client, CHANNEL_ID, OPS_GENIE_API_TOKEN, ROTATION_SCHEDULE_ID): - try: - result = client.users_list() - users = save_users(result["members"]) - on_call = grab_whos_on_call(OPS_GENIE_API_TOKEN, ROTATION_SCHEDULE_ID) - slack_id = users[on_call]["id"] - - result = client.chat_postMessage( - channel=CHANNEL_ID, - text=f"""🛠️Maintenance On-Call: <@{slack_id}>, you will be on-call for the next week. Resources:\n - 📖 - 🔍 - 📊 - 📋 - 🔧 - """, - ) - except SlackApiError as e: - print(f"SlackAPIError: {e}") - - -if __name__ == "__main__": - parser = argparse.ArgumentParser( - description="Script that notifies on-call rotation daily" - ) - parser.add_argument("--slack_api_token", required=True, type=str) - parser.add_argument("--ops_genie_api_token", required=True, type=str) - args = parser.parse_args() - - SLACK_API_TOKEN = args.slack_api_token - OPS_GENIE_API_TOKEN = args.ops_genie_api_token - # NOTE: Feel free to grab the relevant channel ID to post the message to but ensure the App is installed within the channel - CHANNEL_ID = "C06N9KJHN2J" - # NOTE: Rotation schedule is grabbed directly from within the OpsGenie site - ROTATION_SCHEDULE_ID = "904cd122-f269-418d-8c29-3e6751716bae" - - client = WebClient(token=SLACK_API_TOKEN) - postSlackMessage(client, CHANNEL_ID, OPS_GENIE_API_TOKEN, ROTATION_SCHEDULE_ID) diff --git a/.github/workflows/on-call-reminder.yml b/.github/workflows/on-call-reminder.yml deleted file mode 100644 index 54a7a6e3e..000000000 --- a/.github/workflows/on-call-reminder.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: on_call_reminder - -on: - schedule: - - cron: '0 17 * * 3' # Runs every Wednesday at 9am PST (17:00 UTC) - workflow_dispatch: # Allows manual triggering of the workflow - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v3 - with: - persist-credentials: false - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.12' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install requests slack_sdk argparse - - - name: Run Python script - env: - SLACK_API_TOKEN: ${{ secrets.SLACK_API_TOKEN }} - OPS_GENIE_API_TOKEN: ${{ secrets.OPS_GENIE_API_TOKEN }} - run: python .github/slack_oncall_reminder.py --slack_api_token $SLACK_API_TOKEN --ops_genie_api_token $OPS_GENIE_API_TOKEN From f6bb62abb28582752a7f82bc6e5f43a352cd12cd Mon Sep 17 00:00:00 2001 From: Daniel Fremont Date: Fri, 14 Feb 2025 11:12:39 -0800 Subject: [PATCH 13/73] Add support for Python 3.13 (#328) * fixes for Python 3.13 * allow newer manifold3d versions as the API has stabilized * bump version number --- pyproject.toml | 6 +++--- src/scenic/syntax/scenic.gram | 2 +- tests/syntax/test_errors.py | 5 ++++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 960bc4724..1d39d2fdd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "scenic" -version = "3.0.0" +version = "3.1.0a1" description = "The Scenic scenario description language." authors = [ { name = "Daniel Fremont" }, @@ -32,9 +32,9 @@ dependencies = [ "dotmap ~= 1.3", "mapbox_earcut >= 0.12.10", "matplotlib ~= 3.2", - "manifold3d == 2.3.0", + "manifold3d >= 2.5.1", "networkx >= 2.6", - "numpy ~= 1.24", + "numpy >= 1.24", "opencv-python ~= 4.5", "pegen >= 0.3.0", "pillow >= 9.1", diff --git a/src/scenic/syntax/scenic.gram b/src/scenic/syntax/scenic.gram index fa181e190..0033bd59e 100644 --- a/src/scenic/syntax/scenic.gram +++ b/src/scenic/syntax/scenic.gram @@ -2891,7 +2891,7 @@ invalid_while_stmt[NoReturn]: ) } invalid_for_stmt[NoReturn]: - | [ASYNC] 'for' star_targets 'in' star_expressions NEWLINE { self.raise_syntax_error("expected ':'") } + | ['async'] 'for' star_targets 'in' star_expressions NEWLINE { self.raise_syntax_error("expected ':'") } | ['async'] a='for' star_targets 'in' star_expressions ':' NEWLINE !INDENT { self.raise_indentation_error( f"expected an indented block after 'for' statement on line {a.start[0]}" diff --git a/tests/syntax/test_errors.py b/tests/syntax/test_errors.py index f51ad6e3b..da981d583 100644 --- a/tests/syntax/test_errors.py +++ b/tests/syntax/test_errors.py @@ -374,7 +374,10 @@ def checkException(e, lines, program, bug, path, output, topLevel=True): chained = bool(e.__cause__ or (e.__context__ and not e.__suppress_context__)) assert bool(remainingLines) == chained if remainingLines: - mid = loc - 5 if topLevel else loc - 2 + if topLevel: + mid = loc - 6 if sys.version_info >= (3, 13) else loc - 5 + else: + mid = loc - 2 assert len(output) >= -(mid - 1) if e.__cause__: assert ( From 14876931fa0ecd224af44751d9c386c0f86a5eed Mon Sep 17 00:00:00 2001 From: Lola Marrero <110120745+lola831@users.noreply.github.com> Date: Tue, 4 Mar 2025 16:33:01 -0800 Subject: [PATCH 14/73] Update CI for Python 3.13 (#329) * ci: add Python 3.13 to tox.ini and test matrix * Update Sphinx dependency for Python 3.13 compatibility * reformat code using Black 25.1.0 * Align pre-commit, pyproject.toml, and CI configuration to use Black 25.1.0 and isort 5.12.0 for consistent formatting across environments. * Remove setup-python step * Add black formatting commit to .git-blame-ignore-revs --- .git-blame-ignore-revs | 2 ++ .github/workflows/check-formatting.yml | 4 ++++ .github/workflows/run-tests.yml | 4 ++-- .pre-commit-config.yaml | 9 +++++---- examples/__init__.py | 2 +- pyproject.toml | 8 ++++---- src/scenic/core/shapes.py | 2 +- src/scenic/simulators/carla/misc.py | 2 +- src/scenic/syntax/ast.py | 21 +++++++++++++++++++++ tests/core/test_serialization.py | 2 +- tox.ini | 4 ++-- 11 files changed, 44 insertions(+), 16 deletions(-) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index d12f5bf46..afdb18fb5 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -5,3 +5,5 @@ c6c83f95ff370b75c3ee7130dbd8071bfe8b285a # Cleaned up test quote spacing 995cd182924dc9e3dbbc941c5b75454ea0cdaaca +# Ran black on entire codebase +cb51d08fda00df5588a418df42d9d652472f505f diff --git a/.github/workflows/check-formatting.yml b/.github/workflows/check-formatting.yml index 98d3a21b1..5bd2f9128 100644 --- a/.github/workflows/check-formatting.yml +++ b/.github/workflows/check-formatting.yml @@ -16,6 +16,10 @@ jobs: - name: Run black to check formatting uses: psf/black@stable + with: + version: "25.1.0" - name: Run isort to check import order uses: isort/isort-action@v1 + with: + isort-version: "5.12.0" diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 025145608..4386b1542 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -33,11 +33,11 @@ jobs: strategy: fail-fast: true matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] os: [ubuntu-latest, windows-latest, macos-13, macos-latest] include: # Only run slow tests on the latest version of Python - - python-version: "3.12" + - python-version: "3.13" slow: true runs-on: ${{ matrix.os }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 22d93a241..703fabe41 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,11 +1,12 @@ repos: - - repo: https://github.com/psf/black - rev: 23.3.0 + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 25.1.0 hooks: - id: black - language_version: python3.11 + language_version: python3.13 - repo: https://github.com/pycqa/isort rev: 5.12.0 hooks: - - id: isort \ No newline at end of file + - id: isort + language_version: python3.13 diff --git a/examples/__init__.py b/examples/__init__.py index 2a7fb8af9..0bf5a6feb 100644 --- a/examples/__init__.py +++ b/examples/__init__.py @@ -1 +1 @@ -""" Scenic examples""" +"""Scenic examples""" diff --git a/pyproject.toml b/pyproject.toml index 1d39d2fdd..540edd578 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,15 +69,15 @@ test-full = [ # like 'test' but adds dependencies for optional features "exceptiongroup", "inflect ~= 5.5", "pygments ~= 2.11", - "sphinx >= 5.0.0, <6", + "sphinx >= 6.2.0, <7.2.0", "sphinx_rtd_theme >= 0.5.2", "sphinx-tabs ~= 3.4.1", "verifai >= 2.1.0b1", ] dev = [ "scenic[test-full]", - "black ~= 24.0", - "isort ~= 5.11", + "black ~= 25.1.0", + "isort ~= 5.12.0", "pre-commit ~= 3.0", "pytest-cov >= 3.0.0", "tox ~= 4.0", @@ -128,4 +128,4 @@ extend_skip_glob = [ norecursedirs = ["examples"] [tool.coverage.run] -source = ["src"] \ No newline at end of file +source = ["src"] diff --git a/src/scenic/core/shapes.py b/src/scenic/core/shapes.py index 2a57c1f37..b03fe4a1e 100644 --- a/src/scenic/core/shapes.py +++ b/src/scenic/core/shapes.py @@ -1,4 +1,4 @@ -""" Module containing the Shape class and its subclasses, which represent shapes of Objects""" +"""Module containing the Shape class and its subclasses, which represent shapes of Objects""" from abc import ABC, abstractmethod diff --git a/src/scenic/simulators/carla/misc.py b/src/scenic/simulators/carla/misc.py index 116f2cd0a..cd9b97497 100644 --- a/src/scenic/simulators/carla/misc.py +++ b/src/scenic/simulators/carla/misc.py @@ -6,7 +6,7 @@ # This work is licensed under the terms of the MIT license. # For a copy, see . -""" Module with auxiliary functions. """ +"""Module with auxiliary functions.""" import math diff --git a/src/scenic/syntax/ast.py b/src/scenic/syntax/ast.py index e64e3be48..e7781e1a5 100644 --- a/src/scenic/syntax/ast.py +++ b/src/scenic/syntax/ast.py @@ -81,11 +81,13 @@ def __init__( class Ego(AST): "`ego` tracked assign target" + functionName = "ego" class Workspace(AST): ":term:`workspace` tracked assign target" + functionName = "workspace" @@ -247,6 +249,7 @@ def __init__(self, elts: typing.List["parameter"], *args: any, **kwargs: any) -> class parameter(AST): "represents a parameter that is defined with `param` statements" + __match_args__ = ("identifier", "value") def __init__( @@ -1043,6 +1046,7 @@ def __init__( class Front(AST): "Represents position of :scenic:`front of` operator" + functionName = "Front" def __init__(self, *args: any, **kwargs: any) -> None: @@ -1051,6 +1055,7 @@ def __init__(self, *args: any, **kwargs: any) -> None: class Back(AST): "Represents position of :scenic:`back of` operator" + functionName = "Back" def __init__(self, *args: any, **kwargs: any) -> None: @@ -1059,6 +1064,7 @@ def __init__(self, *args: any, **kwargs: any) -> None: class Left(AST): "Represents position of :scenic:`left of` operator" + functionName = "Left" def __init__(self, *args: any, **kwargs: any) -> None: @@ -1067,6 +1073,7 @@ def __init__(self, *args: any, **kwargs: any) -> None: class Right(AST): "Represents position of :scenic:`right of` operator" + functionName = "Right" def __init__(self, *args: any, **kwargs: any) -> None: @@ -1075,6 +1082,7 @@ def __init__(self, *args: any, **kwargs: any) -> None: class Top(AST): "Represents position of :scenic:`top of` operator" + functionName = "Top" def __init__(self, *args: any, **kwargs: any) -> None: @@ -1083,6 +1091,7 @@ def __init__(self, *args: any, **kwargs: any) -> None: class Bottom(AST): "Represents position of :scenic:`bottom of` operator" + functionName = "Bottom" def __init__(self, *args: any, **kwargs: any) -> None: @@ -1091,6 +1100,7 @@ def __init__(self, *args: any, **kwargs: any) -> None: class FrontLeft(AST): "Represents position of :scenic:`front left of` operator" + functionName = "FrontLeft" def __init__(self, *args: any, **kwargs: any) -> None: @@ -1099,6 +1109,7 @@ def __init__(self, *args: any, **kwargs: any) -> None: class FrontRight(AST): "Represents position of :scenic:`front right of` operator" + functionName = "FrontRight" def __init__(self, *args: any, **kwargs: any) -> None: @@ -1107,6 +1118,7 @@ def __init__(self, *args: any, **kwargs: any) -> None: class BackLeft(AST): "Represents position of :scenic:`back left of` operator" + functionName = "BackLeft" def __init__(self, *args: any, **kwargs: any) -> None: @@ -1115,6 +1127,7 @@ def __init__(self, *args: any, **kwargs: any) -> None: class BackRight(AST): "Represents position of :scenic:`back right of` operator" + functionName = "BackRight" def __init__(self, *args: any, **kwargs: any) -> None: @@ -1123,6 +1136,7 @@ def __init__(self, *args: any, **kwargs: any) -> None: class TopFrontLeft(AST): "Represents position of :scenic:`top front left of` operator" + functionName = "TopFrontLeft" def __init__(self, *args: any, **kwargs: any) -> None: @@ -1131,6 +1145,7 @@ def __init__(self, *args: any, **kwargs: any) -> None: class TopFrontRight(AST): "Represents position of :scenic:`top front right of` operator" + functionName = "TopFrontRight" def __init__(self, *args: any, **kwargs: any) -> None: @@ -1139,6 +1154,7 @@ def __init__(self, *args: any, **kwargs: any) -> None: class TopBackLeft(AST): "Represents position of :scenic:`top back left of` operator" + functionName = "TopBackLeft" def __init__(self, *args: any, **kwargs: any) -> None: @@ -1147,6 +1163,7 @@ def __init__(self, *args: any, **kwargs: any) -> None: class TopBackRight(AST): "Represents position of :scenic:`top back right of` operator" + functionName = "TopBackRight" def __init__(self, *args: any, **kwargs: any) -> None: @@ -1155,6 +1172,7 @@ def __init__(self, *args: any, **kwargs: any) -> None: class BottomFrontLeft(AST): "Represents position of :scenic:`bottom front left of` operator" + functionName = "BottomFrontLeft" def __init__(self, *args: any, **kwargs: any) -> None: @@ -1163,6 +1181,7 @@ def __init__(self, *args: any, **kwargs: any) -> None: class BottomFrontRight(AST): "Represents position of :scenic:`bottom front right of` operator" + functionName = "BottomFrontRight" def __init__(self, *args: any, **kwargs: any) -> None: @@ -1171,6 +1190,7 @@ def __init__(self, *args: any, **kwargs: any) -> None: class BottomBackLeft(AST): "Represents position of :scenic:`bottom back left of` operator" + functionName = "BottomBackLeft" def __init__(self, *args: any, **kwargs: any) -> None: @@ -1179,6 +1199,7 @@ def __init__(self, *args: any, **kwargs: any) -> None: class BottomBackRight(AST): "Represents position of :scenic:`bottom back right of` operator" + functionName = "BottomBackRight" def __init__(self, *args: any, **kwargs: any) -> None: diff --git a/tests/core/test_serialization.py b/tests/core/test_serialization.py index af0b6705e..746075c94 100644 --- a/tests/core/test_serialization.py +++ b/tests/core/test_serialization.py @@ -159,7 +159,7 @@ def test_float(self): def test_bytes(self): checkValueEncoding(b"", bytes) checkValueEncoding(b"\x00", bytes) - checkValueEncoding(b"\xFF", bytes) + checkValueEncoding(b"\xff", bytes) checkValueEncoding(b"\x00123456", bytes) def test_str(self): diff --git a/tox.ini b/tox.ini index e08482b81..d4f9392dd 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] -envlist = py{38,39,310,311,312}{,-extras} +envlist = py{38,39,310,311,312,313}{,-extras} labels = - basic = py{38,39,310,311,312} + basic = py{38,39,310,311,312,313} [testenv] extras = From 4b7484846fd5e9d754a559ab8866ff9d9ffa52f1 Mon Sep 17 00:00:00 2001 From: Lola Marrero <110120745+lola831@users.noreply.github.com> Date: Thu, 6 Mar 2025 13:49:51 -0800 Subject: [PATCH 15/73] Feat: Initial MetaDrive Integration (#326) Integration with the MetaDrive simulator. --------- Co-authored-by: Armando Banuelos Co-authored-by: Armando Banuelos Co-authored-by: Armando Banuelos --- .coveragerc | 3 +- assets/maps/CARLA/Town01.net.xml | 1314 ++++++ assets/maps/CARLA/Town04.net.xml | 2980 +++++++++++++ assets/maps/CARLA/Town05.net.xml | 3808 +++++++++++++++++ assets/maps/CARLA/Town07.net.xml | 2090 +++++++++ assets/maps/CARLA/Town10HD.net.xml | 1202 ++++++ codecov.yml | 5 +- docs/conf.py | 4 +- docs/simulators.rst | 26 + docs/tutorials/dynamics.rst | 25 +- .../Carla_Challenge/carlaChallenge2.scenic | 7 +- .../Carla_Challenge/carlaChallenge3.scenic | 5 +- .../OAS_Scenarios/oas_scenario_03.scenic | 4 +- .../OAS_Scenarios/oas_scenario_04.scenic | 5 +- .../OAS_Scenarios/oas_scenario_28.scenic | 7 +- .../OAS_Scenarios/oas_scenario_29.scenic | 7 +- .../OAS_Scenarios/oas_scenario_30.scenic | 6 +- .../OAS_Scenarios/oas_scenario_32.scenic | 4 +- examples/driving/README.md | 2 +- .../driving/badlyParkedCarPullingIn.scenic | 3 + examples/driving/car.scenic | 5 +- examples/driving/pedestrian.scenic | 3 + examples/driving/pedestrianAcrossRoad.scenic | 3 + pyproject.toml | 5 + src/scenic/domains/driving/__init__.py | 9 + src/scenic/domains/driving/behaviors.scenic | 26 +- src/scenic/simulators/metadrive/__init__.py | 22 + src/scenic/simulators/metadrive/model.scenic | 136 + src/scenic/simulators/metadrive/simulator.py | 287 ++ src/scenic/simulators/metadrive/utils.py | 104 + tests/simulators/metadrive/basic.scenic | 13 + tests/simulators/metadrive/test_metadrive.py | 130 + 32 files changed, 12210 insertions(+), 40 deletions(-) create mode 100644 assets/maps/CARLA/Town01.net.xml create mode 100644 assets/maps/CARLA/Town04.net.xml create mode 100644 assets/maps/CARLA/Town05.net.xml create mode 100644 assets/maps/CARLA/Town07.net.xml create mode 100644 assets/maps/CARLA/Town10HD.net.xml create mode 100644 src/scenic/simulators/metadrive/__init__.py create mode 100644 src/scenic/simulators/metadrive/model.scenic create mode 100644 src/scenic/simulators/metadrive/simulator.py create mode 100644 src/scenic/simulators/metadrive/utils.py create mode 100644 tests/simulators/metadrive/basic.scenic create mode 100644 tests/simulators/metadrive/test_metadrive.py diff --git a/.coveragerc b/.coveragerc index b2920e991..f98ea0d97 100644 --- a/.coveragerc +++ b/.coveragerc @@ -5,6 +5,7 @@ omit = src/scenic/simulators/carla/* src/scenic/simulators/gta/* src/scenic/simulators/lgsvl/* + src/scenic/simulators/metadrive/* src/scenic/simulators/webots/* src/scenic/simulators/xplane/* @@ -26,4 +27,4 @@ exclude_lines = ignore_errors = True show_missing = True -precision = 2 \ No newline at end of file +precision = 2 diff --git a/assets/maps/CARLA/Town01.net.xml b/assets/maps/CARLA/Town01.net.xml new file mode 100644 index 000000000..bb3164654 --- /dev/null +++ b/assets/maps/CARLA/Town01.net.xml @@ -0,0 +1,1314 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/maps/CARLA/Town04.net.xml b/assets/maps/CARLA/Town04.net.xml new file mode 100644 index 000000000..3b6c70f62 --- /dev/null +++ b/assets/maps/CARLA/Town04.net.xml @@ -0,0 +1,2980 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/maps/CARLA/Town05.net.xml b/assets/maps/CARLA/Town05.net.xml new file mode 100644 index 000000000..1769ec071 --- /dev/null +++ b/assets/maps/CARLA/Town05.net.xml @@ -0,0 +1,3808 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/maps/CARLA/Town07.net.xml b/assets/maps/CARLA/Town07.net.xml new file mode 100644 index 000000000..5b517d302 --- /dev/null +++ b/assets/maps/CARLA/Town07.net.xml @@ -0,0 +1,2090 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/maps/CARLA/Town10HD.net.xml b/assets/maps/CARLA/Town10HD.net.xml new file mode 100644 index 000000000..bc3942510 --- /dev/null +++ b/assets/maps/CARLA/Town10HD.net.xml @@ -0,0 +1,1202 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/codecov.yml b/codecov.yml index 9b0516a8b..3d6eb52b4 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,4 +1,4 @@ -codecov: +codecov: require_ci_to_pass: true coverage: @@ -21,6 +21,7 @@ ignore: - "src/scenic/simulators/carla/" - "src/scenic/simulators/gta/" - "src/scenic/simulators/lgsvl/" + - "src/scenic/simulators/metadrive/" - "src/scenic/simulators/webots/" - "src/scenic/simulators/xplane/" - "!**/*.py" @@ -30,4 +31,4 @@ comment: cli: plugins: pycoverage: - report_type: "json" \ No newline at end of file + report_type: "json" diff --git a/docs/conf.py b/docs/conf.py index d20aee6ea..02628a3ec 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -46,6 +46,7 @@ paramOverrides=dict( map="../assets/maps/opendrive.org/CulDeSac.xodr", carla_map="blah", + sumo_map="blah", lgsvl_map="blah", ), ) @@ -54,6 +55,7 @@ warnings.simplefilter("ignore", SimulatorInterfaceWarning) import scenic.simulators.carla.model import scenic.simulators.lgsvl.model + import scenic.simulators.metadrive.model veneer.deactivate() # Hack to allow importing models which require 2D compatibility mode @@ -106,7 +108,7 @@ autosummary_generate = True autodoc_inherit_docstrings = False autodoc_member_order = "bysource" -autodoc_mock_imports = ["carla", "lgsvl"] +autodoc_mock_imports = ["carla", "lgsvl", "metadrive"] autodoc_typehints = "description" autodoc_type_aliases = { "Vectorlike": "`scenic.domains.driving.roads.Vectorlike`", diff --git a/docs/simulators.rst b/docs/simulators.rst index f03d6d476..2b1e22df0 100644 --- a/docs/simulators.rst +++ b/docs/simulators.rst @@ -16,6 +16,32 @@ See the individual entries for details on each interface's capabilities and how Currently Supported =================== +MetaDrive +---------------------------- + +Scenic supports integration with the `MetaDrive `_ simulator as an optional dependency, +enabling users to describe dynamic simulations of vehicles, pedestrians, and traffic scenarios. +If your system supports it, you can install it with: + +.. code-block:: console + + python -m pip install scenic[metadrive] + +Scenic supports both 2D and 3D rendering modes for MetaDrive simulations. +2D rendering is available on all systems, providing a top-down view. +However, 3D rendering may not work properly on macOS devices with M-series chips. +Additionally, there is an issue where cars do not fully brake in certain scenarios. +These issues are expected to be addressed in the next version of MetaDrive. + +Scenic uses OpenDRIVE maps, while MetaDrive relies on SUMO maps. Scenic provides corresponding SUMO maps for OpenDRIVE maps under the :file:`assets/maps/CARLA` directory. +Additionally, you can convert your own OpenDRIVE maps to SUMO maps using the `netconvert `_ tool. +To avoid setting the SUMO map manually, name it the same as your OpenDRIVE file and place it in the same directory. +Otherwise, you can specify it explicitly using the ``sumo_map`` global parameter. + +The simulator is compatible with scenarios written using Scenic's :ref:`driving_domain`. +For more information, refer to the documentation of the `scenic.simulators.metadrive` module. + + Built-in Newtonian Simulator ---------------------------- diff --git a/docs/tutorials/dynamics.rst b/docs/tutorials/dynamics.rst index 20738d9fe..734ad257a 100644 --- a/docs/tutorials/dynamics.rst +++ b/docs/tutorials/dynamics.rst @@ -396,10 +396,19 @@ You can see all of the above syntax in action by running some of our examples of scenarios. We have examples written for the CARLA and LGSVL driving simulators, and those in :file:`examples/driving` in particular are designed to use Scenic's abstract :ref:`driving domain ` and so work in either of these simulators, as well -as Scenic's built-in Newtonian physics simulator. The Newtonian simulator is convenient -for testing and simple experiments; you can find details on how to install the more -realistic simulators in our :ref:`simulators` page (they should work on both Linux and -Windows, but not macOS, at the moment). +as Scenic's built-in Newtonian physics simulator and the MetaDrive simulator. While the Newtonian simulator is convenient +for testing simple experiments, we recommend using MetaDrive for more realistic driving scenarios. + +MetaDrive support is **optional**. If your system supports MetaDrive, you can install it separately using: + +.. code-block:: console + + python -m pip install scenic[metadrive] + +If MetaDrive is **not available**, we recommend using the Newtonian simulator instead. + +You can find details on these simulators and how to install them on +our :ref:`simulators` page. Let's try running :file:`examples/driving/badlyParkedCarPullingIn.scenic`, which implements the "a @@ -414,16 +423,16 @@ usual schematic diagram of the generated scenes: To run dynamic simulations, add the :option:`--simulate` option (:option:`-S` for short). Since this scenario is not written for a particular simulator, you'll need to specify which one you want by using the :option:`--model` option (:option:`-m` for short) to -select the corresponding Scenic :term:`world model`: for example, to use the Newtonian simulator we could add -``--model scenic.simulators.newtonian.driving_model``. It's also a good idea to put a time bound on -the simulations, which we can do using the :option:`--time` option. +select the corresponding Scenic :term:`world model`: for example, to use the MetaDrive simulator we could add +``--model scenic.simulators.metadrive.model``. +It's also a good idea to put a time bound on the simulations, which we can do using the :option:`--time` option. .. code-block:: console $ scenic examples/driving/badlyParkedCarPullingIn.scenic \ --2d \ --simulate \ - --model scenic.simulators.newtonian.driving_model \ + --model scenic.simulators.metadrive.model \ --time 200 Running the scenario in CARLA is exactly the same, except we use the diff --git a/examples/driving/Carla_Challenge/carlaChallenge2.scenic b/examples/driving/Carla_Challenge/carlaChallenge2.scenic index f9e95833d..cf244683e 100644 --- a/examples/driving/Carla_Challenge/carlaChallenge2.scenic +++ b/examples/driving/Carla_Challenge/carlaChallenge2.scenic @@ -1,9 +1,12 @@ """ Scenario Description Based on 2019 Carla Challenge Traffic Scenario 02. -Leading vehicle decelerates suddently due to an obstacle and +Leading vehicle decelerates suddently due to an obstacle and ego-vehicle must react, performing an emergency brake or an avoidance maneuver. Note: The scenario may fail if the leadCar or the ego get past the intersection while following the roadDirection +To run this file using the MetaDrive simulator: + scenic examples/driving/Carla_Challenge/carlaChallenge2.scenic --2d --model scenic.simulators.metadrive.model --simulate + To run this file using the Carla simulator: scenic examples/driving/Carla_Challenge/carlaChallenge2.scenic --2d --model scenic.simulators.carla.model --simulate """ @@ -54,5 +57,3 @@ leadCar = new Car following roadDirection from obstacle for LEADCAR_TO_OBSTACLE, ego = new Car following roadDirection from leadCar for EGO_TO_LEADCAR, with behavior EgoBehavior(EGO_SPEED) - - diff --git a/examples/driving/Carla_Challenge/carlaChallenge3.scenic b/examples/driving/Carla_Challenge/carlaChallenge3.scenic index c232683ca..275593932 100644 --- a/examples/driving/Carla_Challenge/carlaChallenge3.scenic +++ b/examples/driving/Carla_Challenge/carlaChallenge3.scenic @@ -1,8 +1,11 @@ """ Scenario Description Based on 2019 Carla Challenge Traffic Scenario 03. -Leading vehicle decelerates suddenly due to an obstacle and +Leading vehicle decelerates suddenly due to an obstacle and ego-vehicle must react, performing an emergency brake or an avoidance maneuver. +To run this file using the MetaDrive simulator: + scenic examples/driving/Carla_Challenge/carlaChallenge3.scenic --2d --model scenic.simulators.metadrive.model --simulate + To run this file using the Carla simulator: scenic examples/driving/Carla_Challenge/carlaChallenge3.scenic --2d --model scenic.simulators.carla.model --simulate """ diff --git a/examples/driving/OAS_Scenarios/oas_scenario_03.scenic b/examples/driving/OAS_Scenarios/oas_scenario_03.scenic index 79ad88961..bed577db0 100644 --- a/examples/driving/OAS_Scenarios/oas_scenario_03.scenic +++ b/examples/driving/OAS_Scenarios/oas_scenario_03.scenic @@ -2,6 +2,9 @@ Voyage OAS Scenario Unique ID: 2-2-XX-CF-STR-CAR The ego vehicle follows the lead car +To run this file using the MetaDrive simulator: + scenic examples/driving/OAS_Scenarios/oas_scenario_03.scenic --2d --model scenic.simulators.metadrive.model --simulate + To run this file using the Carla simulator: scenic examples/driving/OAS_Scenarios/oas_scenario_03.scenic --2d --model scenic.simulators.carla.model --simulate """ @@ -36,4 +39,3 @@ leadCar = new Car on select_lane.centerline, ego = new Car following roadDirection from leadCar for INITIAL_DISTANCE_APART, with behavior FollowLeadCarBehavior() - diff --git a/examples/driving/OAS_Scenarios/oas_scenario_04.scenic b/examples/driving/OAS_Scenarios/oas_scenario_04.scenic index 3dc09a7cf..859fa6c01 100644 --- a/examples/driving/OAS_Scenarios/oas_scenario_04.scenic +++ b/examples/driving/OAS_Scenarios/oas_scenario_04.scenic @@ -2,6 +2,9 @@ Voyage OAS Scenario Unique ID: 2-2-XX-CF-STR-CAR:01 The ego vehicle follows the lead car which suddenly stops +To run this file using the MetaDrive simulator: + scenic examples/driving/OAS_Scenarios/oas_scenario_04.scenic --2d --model scenic.simulators.metadrive.model --simulate + To run this file using the Carla simulator: scenic examples/driving/OAS_Scenarios/oas_scenario_04.scenic --2d --model scenic.simulators.carla.model --simulate """ @@ -43,4 +46,4 @@ other = new Car on select_lane.centerline, with behavior LeadCarBehavior() ego = new Car following roadDirection from other for INITIAL_DISTANCE_APART, - with behavior FollowLeadCarBehavior() \ No newline at end of file + with behavior FollowLeadCarBehavior() diff --git a/examples/driving/OAS_Scenarios/oas_scenario_28.scenic b/examples/driving/OAS_Scenarios/oas_scenario_28.scenic index 26ff7a592..ea787cc3e 100644 --- a/examples/driving/OAS_Scenarios/oas_scenario_28.scenic +++ b/examples/driving/OAS_Scenarios/oas_scenario_28.scenic @@ -1,9 +1,12 @@ """ Scenario Description Voyage OAS Scenario Unique ID: 3-2-ESW-I-STR-CAR:S>W:02 -At three-way intersection. The ego vehicle goes straight. -The other car, on the other leg of the intersection, takes a left turn first +At three-way intersection. The ego vehicle goes straight. +The other car, on the other leg of the intersection, takes a left turn first because it is closer to the intersection. +To run this file using the MetaDrive simulator: + scenic examples/driving/OAS_Scenarios/oas_scenario_28.scenic --2d --model scenic.simulators.metadrive.model --simulate + To run this file using the Carla simulator: scenic examples/driving/OAS_Scenarios/oas_scenario_28.scenic --2d --model scenic.simulators.carla.model --simulate """ diff --git a/examples/driving/OAS_Scenarios/oas_scenario_29.scenic b/examples/driving/OAS_Scenarios/oas_scenario_29.scenic index 2561a70e3..2ef507ceb 100644 --- a/examples/driving/OAS_Scenarios/oas_scenario_29.scenic +++ b/examples/driving/OAS_Scenarios/oas_scenario_29.scenic @@ -1,9 +1,12 @@ """ Scenario Description Voyage OAS Scenario Unique ID: 3-2-NSW-I-L-CAR:S>W:02 -At 3 way intersection. The ego car turns left. -The other car, on a different leg of the intersection, +At 3 way intersection. The ego car turns left. +The other car, on a different leg of the intersection, has the right of the way and makes a left turn first because it is closer to the intersection. +To run this file using the MetaDrive simulator: + scenic examples/driving/OAS_Scenarios/oas_scenario_29.scenic --2d --model scenic.simulators.metadrive.model --simulate + To run this file using the Carla simulator: scenic examples/driving/OAS_Scenarios/oas_scenario_29.scenic --2d --model scenic.simulators.carla.model --simulate """ diff --git a/examples/driving/OAS_Scenarios/oas_scenario_30.scenic b/examples/driving/OAS_Scenarios/oas_scenario_30.scenic index 30b4cf30d..d94a00693 100644 --- a/examples/driving/OAS_Scenarios/oas_scenario_30.scenic +++ b/examples/driving/OAS_Scenarios/oas_scenario_30.scenic @@ -1,9 +1,12 @@ """ Scenario Description Voyage OAS Scenario Unique ID: 3-2-NWS-I-L-CAR:S>W:01 -At 3 way intersection. The ego car turns left. +At 3 way intersection. The ego car turns left. The other car approaches from a different leg of the intersection to make a left turn, but ego has the right of the way because it is closer to the intersection. +To run this file using the MetaDrive simulator: + scenic examples/driving/OAS_Scenarios/oas_scenario_30.scenic --2d --model scenic.simulators.metadrive.model --simulate + To run this file using the Carla simulator: scenic examples/driving/OAS_Scenarios/oas_scenario_30.scenic --2d --model scenic.simulators.carla.model --simulate """ @@ -58,4 +61,3 @@ ego = new Car following roadDirection from egoStart for EGO_OFFSET, other = new Car following roadDirection from actorStart for OTHERCAR_OFFSET, with behavior SafeBehavior(target_speed=SPEED, trajectory=actor_centerlines, \ thresholdDistance = SAFE_DIST) - diff --git a/examples/driving/OAS_Scenarios/oas_scenario_32.scenic b/examples/driving/OAS_Scenarios/oas_scenario_32.scenic index 985325df9..6ccef865f 100644 --- a/examples/driving/OAS_Scenarios/oas_scenario_32.scenic +++ b/examples/driving/OAS_Scenarios/oas_scenario_32.scenic @@ -3,6 +3,9 @@ Voyage OAS Scenario Unique ID: 3-2-W-I-L-CAR:N>S At 3-way intersection, ego turns left and the other car on a different leg of the intersection goes straight. There is no requirement on which vehicle has the right of the way. +To run this file using the MetaDrive simulator: + scenic examples/driving/OAS_Scenarios/oas_scenario_32.scenic --2d --model scenic.simulators.metadrive.model --simulate + To run this file using the Carla simulator: scenic examples/driving/OAS_Scenarios/oas_scenario_32.scenic --2d --model scenic.simulators.carla.model --simulate """ @@ -53,4 +56,3 @@ ego = new Car on ego_L_startLane.centerline, other = new Car on startLane.centerline, with behavior FollowTrafficBehavior(target_speed=10, trajectory=centerlines) - diff --git a/examples/driving/README.md b/examples/driving/README.md index 753169d5a..23979db54 100644 --- a/examples/driving/README.md +++ b/examples/driving/README.md @@ -6,5 +6,5 @@ For example: ``` scenic --2d badlyParkedCarPullingIn.scenic -scenic --2d -S --model scenic.simulators.newtonian.driving_model badlyParkedCarPullingIn.scenic +scenic --2d -S --model scenic.simulators.metadrive.model badlyParkedCarPullingIn.scenic ``` diff --git a/examples/driving/badlyParkedCarPullingIn.scenic b/examples/driving/badlyParkedCarPullingIn.scenic index aedfcb667..8e27e3314 100644 --- a/examples/driving/badlyParkedCarPullingIn.scenic +++ b/examples/driving/badlyParkedCarPullingIn.scenic @@ -1,4 +1,7 @@ ''' +To run this file using the MetaDrive simulator: + scenic examples/driving/badlyParkedCarPullingIn.scenic --2d --model scenic.simulators.metadrive.model --simulate + To run this file using the Carla simulator: scenic examples/driving/badlyParkedCarPullingIn.scenic --2d --model scenic.simulators.carla.model --simulate ''' diff --git a/examples/driving/car.scenic b/examples/driving/car.scenic index c1d0e3d54..49c421125 100644 --- a/examples/driving/car.scenic +++ b/examples/driving/car.scenic @@ -1,4 +1,7 @@ ''' +To run this file using the MetaDrive simulator: + scenic examples/driving/car.scenic --2d --model scenic.simulators.metadrive.model --simulate + To run this file using the Carla simulator: scenic examples/driving/car.scenic --2d --model scenic.simulators.carla.model --simulate ''' @@ -7,4 +10,4 @@ param map = localPath('../../assets/maps/CARLA/Town01.xodr') model scenic.domains.driving.model -ego = new Car \ No newline at end of file +ego = new Car diff --git a/examples/driving/pedestrian.scenic b/examples/driving/pedestrian.scenic index fb938284c..d1c088539 100644 --- a/examples/driving/pedestrian.scenic +++ b/examples/driving/pedestrian.scenic @@ -1,4 +1,7 @@ ''' +To run this file using the MetaDrive simulator: + scenic examples/driving/pedestrian.scenic --2d --model scenic.simulators.metadrive.model --simulate + To run this file using the Carla simulator: scenic examples/driving/pedestrian.scenic --2d --model scenic.simulators.carla.model --simulate ''' diff --git a/examples/driving/pedestrianAcrossRoad.scenic b/examples/driving/pedestrianAcrossRoad.scenic index 304608d01..4cde3a294 100644 --- a/examples/driving/pedestrianAcrossRoad.scenic +++ b/examples/driving/pedestrianAcrossRoad.scenic @@ -1,4 +1,7 @@ ''' +To run this file using the MetaDrive simulator: + scenic examples/driving/pedestrianAcrossRoad.scenic --2d --model scenic.simulators.metadrive.model --simulate + To run this file using the Carla simulator: scenic examples/driving/pedestrianAcrossRoad.scenic --2d --model scenic.simulators.carla.model --simulate ''' diff --git a/pyproject.toml b/pyproject.toml index 540edd578..66ee2b29c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,10 @@ guideways = [ 'pyproj ~= 3.0; python_version < "3.10"', 'pyproj ~= 3.3; python_version >= "3.10"', ] +metadrive = [ + "metadrive-simulator >= 0.4.3", + "sumolib >= 1.21.0", +] test = [ # minimum dependencies for running tests (used for tox virtualenvs) "pytest >= 7.0.0, <8", "pytest-cov >= 3.0.0", @@ -63,6 +67,7 @@ test = [ # minimum dependencies for running tests (used for tox virtualenvs) test-full = [ # like 'test' but adds dependencies for optional features "scenic[test]", # all dependencies from 'test' extra above "scenic[guideways]", # for running guideways modules + 'scenic[metadrive]; python_version <= "3.11"', # MetaDrive only supports Python ≤ 3.11; excluded for newer versions "astor >= 0.8.1", 'carla >= 0.9.12; python_version <= "3.10" and (platform_system == "Linux" or platform_system == "Windows")', "dill", diff --git a/src/scenic/domains/driving/__init__.py b/src/scenic/domains/driving/__init__.py index bff86a5b9..b89c1c54c 100644 --- a/src/scenic/domains/driving/__init__.py +++ b/src/scenic/domains/driving/__init__.py @@ -12,6 +12,7 @@ Scenarios written for the driving domain should work without changes [#f1]_ in any of the following simulators: + * MetaDrive, using the model :doc:`scenic.simulators.metadrive.model` * CARLA, using the model :doc:`scenic.simulators.carla.model` * LGSVL, using the model :doc:`scenic.simulators.lgsvl.model` * the built-in Newtonian simulator, using the model @@ -34,6 +35,14 @@ $ scenic -S --model scenic.simulators.newtonian.driving_model \\ examples/driving/badlyParkedCarPullingIn.scenic + * MetaDrive, using the corresponding SUMO map (.net.xml) from the default map specified in + the scenario: + + .. code-block:: console + + $ scenic -S --2d --model scenic.simulators.metadrive.model \\ + examples/driving/badlyParkedCarPullingIn.scenic + * CARLA, using the default map specified in the scenario: .. code-block:: console diff --git a/src/scenic/domains/driving/behaviors.scenic b/src/scenic/domains/driving/behaviors.scenic index 1abac6f95..173500190 100644 --- a/src/scenic/domains/driving/behaviors.scenic +++ b/src/scenic/domains/driving/behaviors.scenic @@ -40,11 +40,11 @@ behavior ConstantThrottleBehavior(x): take SetThrottleAction(x) behavior FollowLaneBehavior(target_speed = 10, laneToFollow=None, is_oppositeTraffic=False): - """ + """ Follow's the lane on which the vehicle is at, unless the laneToFollow is specified. Once the vehicle reaches an intersection, by default, the vehicle will take the straight route. - If straight route is not available, then any availble turn route will be taken, uniformly randomly. - If turning at the intersection, the vehicle will slow down to make the turn, safely. + If straight route is not available, then any availble turn route will be taken, uniformly randomly. + If turning at the intersection, the vehicle will slow down to make the turn, safely. This behavior does not terminate. A recommended use of the behavior is to accompany it with condition, e.g. do FollowLaneBehavior() until ... @@ -52,7 +52,7 @@ behavior FollowLaneBehavior(target_speed = 10, laneToFollow=None, is_oppositeTra :param target_speed: Its unit is in m/s. By default, it is set to 10 m/s :param laneToFollow: If the lane to follow is different from the lane that the vehicle is on, this parameter can be used to specify that lane. By default, this variable will be set to None, which means that the vehicle will follow the lane that it is currently on. """ - + past_steer_angle = 0 past_speed = 0 # making an assumption here that the agent starts from zero speed if laneToFollow is None: @@ -75,7 +75,7 @@ behavior FollowLaneBehavior(target_speed = 10, laneToFollow=None, is_oppositeTra nearby_intersection = current_lane.centerline[-1] else: nearby_intersection = current_lane.centerline[-1] - + # instantiate longitudinal and lateral controllers _lon_controller, _lat_controller = simulation().getLaneFollowingControllers(self) @@ -127,7 +127,7 @@ behavior FollowLaneBehavior(target_speed = 10, laneToFollow=None, is_oppositeTra if (end_lane is not None) and (self.position in end_lane) and not intersection_passed: intersection_passed = True in_turning_lane = False - entering_intersection = False + entering_intersection = False target_speed = original_target_speed _lon_controller, _lat_controller = simulation().getLaneFollowingControllers(self) @@ -151,11 +151,11 @@ behavior FollowLaneBehavior(target_speed = 10, laneToFollow=None, is_oppositeTra behavior FollowTrajectoryBehavior(target_speed = 10, trajectory = None, turn_speed=None): - """ + """ Follows the given trajectory. The behavior terminates once the end of the trajectory is reached. :param target_speed: Its unit is in m/s. By default, it is set to 10 m/s - :param trajectory: It is a list of sequential lanes to track, from the lane that the vehicle is initially on to the lane it should end up on. + :param trajectory: It is a list of sequential lanes to track, from the lane that the vehicle is initially on to the lane it should end up on. """ assert trajectory is not None @@ -172,7 +172,7 @@ behavior FollowTrajectoryBehavior(target_speed = 10, trajectory = None, turn_spe # instantiate longitudinal and lateral controllers _lon_controller,_lat_controller = simulation().getLaneFollowingControllers(self) past_steer_angle = 0 - + if trajectory[-1].maneuvers: end_intersection = trajectory[-1].maneuvers[0].intersection if end_intersection == None: @@ -209,8 +209,8 @@ behavior FollowTrajectoryBehavior(target_speed = 10, trajectory = None, turn_spe behavior TurnBehavior(trajectory, target_speed=6): """ This behavior uses a controller specifically tuned for turning at an intersection. - This behavior is only operational within an intersection, - it will terminate if the vehicle is outside of an intersection. + This behavior is only operational within an intersection, + it will terminate if the vehicle is outside of an intersection. """ if isinstance(trajectory, PolylineRegion): @@ -276,7 +276,7 @@ behavior LaneChangeBehavior(laneSectionToSwitch, is_oppositeTraffic=False, targe while True: if abs(trajectory_centerline.signedDistanceTo(self.position)) < 0.1: - break + break if (distance from self to nearby_intersection) < distanceToEndpoint: straight_manuevers = filter(lambda i: i.type == ManeuverType.STRAIGHT, current_lane.maneuvers) @@ -304,7 +304,7 @@ behavior LaneChangeBehavior(laneSectionToSwitch, is_oppositeTraffic=False, targe current_speed = 0 cte = trajectory_centerline.signedDistanceTo(self.position) - if is_oppositeTraffic: # [bypass] when crossing over the yellowline to opposite traffic lane + if is_oppositeTraffic: # [bypass] when crossing over the yellowline to opposite traffic lane cte = -cte speed_error = target_speed - current_speed diff --git a/src/scenic/simulators/metadrive/__init__.py b/src/scenic/simulators/metadrive/__init__.py new file mode 100644 index 000000000..d82483150 --- /dev/null +++ b/src/scenic/simulators/metadrive/__init__.py @@ -0,0 +1,22 @@ +"""Interface to the MetaDrive driving simulator. + +This interface must currently be used in `2D compatibility mode`. + +It supports dynamic scenarios involving vehicles and pedestrians. + +The interface implements the :obj:`scenic.domains.driving` abstract domain, so any +object types, behaviors, utility functions, etc. from that domain may be used freely. +For details of additional MetaDrive-specific functionality, see the world model +:obj:`scenic.simulators.metadrive.model`. +""" + +# Only import MetaDriveSimulator if the metadrive package is installed; otherwise the +# import would raise an exception. +metadrive = None +try: + import metadrive +except ImportError: + pass +if metadrive: + from .simulator import MetaDriveSimulator +del metadrive diff --git a/src/scenic/simulators/metadrive/model.scenic b/src/scenic/simulators/metadrive/model.scenic new file mode 100644 index 000000000..6aaab18b0 --- /dev/null +++ b/src/scenic/simulators/metadrive/model.scenic @@ -0,0 +1,136 @@ +"""Scenic world model for traffic scenarios in MetaDrive. + +The model currently supports vehicles and pedestrians. It implements the +basic :obj:`~scenic.domains.driving.model.Car` and `Pedestrian` classes from the :obj:`scenic.domains.driving` domain. +Vehicles and pedestrians support the basic actions and behaviors from the driving domain. + +The model defines several global parameters, whose default values can be overridden +in scenarios using the ``param`` statement or on the command line using the +:option:`--param` option: + +Global Parameters: + sumo_map (str or Path): Path to the SUMO map (``.net.xml`` file) to use in the simulation. + This map should correspond to the **map** file used in the scenario. See the documentation in + :doc:`scenic.domains.driving.model` for details. + timestep (float): The interval (in seconds) between each simulation step. This determines how often Scenic + interrupts MetaDrive to run behaviors, check requirements, and update the simulation state. + The default value is 0.1 seconds. + render (bool): Whether to render the simulation screen. If True (default), it will open a screen and render + the simulation. If False, rendering is disabled. + render3D (bool): Whether to render the simulation in 3D. If True, it will render the simulation in 3D. + If False (default), it will render in 2D. + real_time (bool): If True (default), the simulation will run in real time, ensuring each step takes at least + as long as the specified timestep. If False, the simulation runs as fast as possible. +""" +import pathlib + +from scenic.domains.driving.model import * +from scenic.domains.driving.actions import * +from scenic.domains.driving.behaviors import * + +from scenic.core.errors import InvalidScenarioError + +try: + from scenic.simulators.metadrive.simulator import MetaDriveSimulator + from scenic.simulators.metadrive.utils import scenicToMetaDriveHeading +except ModuleNotFoundError: + # for convenience when testing without the metadrive package + from scenic.core.simulators import SimulatorInterfaceWarning + import warnings + warnings.warn('The "metadrive-simulator" package is not installed; ' + 'will not be able to run dynamic simulations', + SimulatorInterfaceWarning) + + def MetaDriveSimulator(*args, **kwargs): + """Dummy simulator to allow compilation without the 'metadrive-simulator' package. + + :meta private: + """ + raise RuntimeError('the "metadrive-simulator" package is required to run simulations ' + 'from this scenario') + +if "sumo_map" not in globalParameters: + sumo_map_path = str(pathlib.Path(globalParameters.map).with_suffix(".net.xml")) + + if not pathlib.Path(sumo_map_path).exists(): + raise InvalidScenarioError( + f"Missing SUMO map: Expected '{sumo_map_path}' but the file does not exist.\n" + "The SUMO map should have the same base name as the 'map' parameter, with the '.net.xml' extension.\n" + "Ensure that the corresponding '.net.xml' file is located in the same directory as the '.xodr' file." + ) +else: + sumo_map_path = globalParameters.sumo_map + +param sumo_map = sumo_map_path +param timestep = 0.1 +param render = 1 +param render3D = 0 +param real_time = 1 + +simulator MetaDriveSimulator( + sumo_map=globalParameters.sumo_map, + timestep=float(globalParameters.timestep), + render=bool(globalParameters.render), + render3D=bool(globalParameters.render3D), + real_time=bool(globalParameters.real_time), +) + +class MetaDriveActor(DrivingObject): + """Abstract class for MetaDrive objects. + + This class serves as a base for objects in the MetaDrive simulator. It provides essential + functionality for associating Scenic objects with their corresponding MetaDrive simulation objects. + + Properties: + metaDriveActor: A reference to the MetaDrive actor (e.g., vehicle or pedestrian) associated + with this Scenic object. This is set when the object is created in the simulator. + It allows interaction with MetaDrive's simulation environment, such as applying actions + or retrieving simulation data (position, velocity, etc.). + """ + metaDriveActor: None + +class Vehicle(Vehicle, Steers, MetaDriveActor): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._control = {"steering": 0, "throttle": 0, "brake": 0} + + def _reset_control(self): + self._control = {"steering": 0, "throttle": 0, "brake": 0} + + def setThrottle(self, throttle): + self._control["throttle"] = throttle + + def setSteering(self, steering): + self._control["steering"] = steering + + def setBraking(self, braking): + self._control["brake"] = braking + + def _collect_action(self): + steering = -self._control["steering"] # Invert the steering to match MetaDrive's convention + action = [ + steering, + self._control["throttle"] - self._control["brake"], + ] + return action + +class Car(Vehicle): + @property + def isCar(self): + return True + +class Pedestrian(Pedestrian, MetaDriveActor, Walks): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._walking_direction = None + self._walking_speed = None + + @property + def isPedestrian(self): + return True + + def setWalkingDirection(self, heading): + self._walking_direction = scenicToMetaDriveHeading(heading) + + def setWalkingSpeed(self, speed): + self._walking_speed = speed diff --git a/src/scenic/simulators/metadrive/simulator.py b/src/scenic/simulators/metadrive/simulator.py new file mode 100644 index 000000000..f3b101433 --- /dev/null +++ b/src/scenic/simulators/metadrive/simulator.py @@ -0,0 +1,287 @@ +"""Simulator interface for MetaDrive.""" + +try: + from metadrive.component.traffic_participants.pedestrian import Pedestrian + from metadrive.component.vehicle.vehicle_type import DefaultVehicle +except ImportError as e: + raise ModuleNotFoundError( + "Metadrive is required. Please install the 'metadrive-simulator' package (and sumolib) or use scenic[metadrive]." + ) from e + +import logging +import sys +import time + +from scenic.core.simulators import InvalidScenarioError, SimulationCreationError +from scenic.domains.driving.actions import * +from scenic.domains.driving.controllers import ( + PIDLateralController, + PIDLongitudinalController, +) +from scenic.domains.driving.simulators import DrivingSimulation, DrivingSimulator +import scenic.simulators.metadrive.utils as utils + + +class MetaDriveSimulator(DrivingSimulator): + """Implementation of `Simulator` for MetaDrive.""" + + def __init__( + self, + timestep=0.1, + render=True, + render3D=False, + sumo_map=None, + real_time=True, + ): + super().__init__() + self.render = render + self.render3D = render3D if render else False + self.scenario_number = 0 + self.timestep = timestep + self.sumo_map = sumo_map + self.real_time = real_time + self.scenic_offset, self.sumo_map_boundary = utils.getMapParameters(self.sumo_map) + if self.render and not self.render3D: + self.film_size = utils.calculateFilmSize(self.sumo_map_boundary, scaling=5) + else: + self.film_size = None + + def createSimulation(self, scene, *, timestep, **kwargs): + self.scenario_number += 1 + return MetaDriveSimulation( + scene, + render=self.render, + render3D=self.render3D, + scenario_number=self.scenario_number, + timestep=self.timestep, + sumo_map=self.sumo_map, + real_time=self.real_time, + scenic_offset=self.scenic_offset, + sumo_map_boundary=self.sumo_map_boundary, + film_size=self.film_size, + **kwargs, + ) + + +class MetaDriveSimulation(DrivingSimulation): + def __init__( + self, + scene, + render, + render3D, + scenario_number, + timestep, + sumo_map, + real_time, + scenic_offset, + sumo_map_boundary, + film_size, + **kwargs, + ): + if len(scene.objects) == 0: + raise InvalidScenarioError( + "Metadrive requires you to define at least one Scenic object." + ) + if not scene.objects[0].isCar: + raise InvalidScenarioError( + "The first object must be a car to serve as the ego vehicle in Metadrive." + ) + + self.render = render + self.render3D = render3D + self.scenario_number = scenario_number + self.defined_ego = False + self.client = None + self.timestep = timestep + self.sumo_map = sumo_map + self.real_time = real_time + self.scenic_offset = scenic_offset + self.sumo_map_boundary = sumo_map_boundary + self.film_size = film_size + super().__init__(scene, timestep=timestep, **kwargs) + + def createObjectInSimulator(self, obj): + """ + Create an object in the MetaDrive simulator. + + If it's the first object, it initializes the client and sets it up for the ego car. + For additional cars and pedestrians, it spawns objects using the provided position and heading. + """ + converted_position = utils.scenicToMetaDrivePosition( + obj.position, self.scenic_offset + ) + converted_heading = utils.scenicToMetaDriveHeading(obj.heading) + + if not self.defined_ego: + decision_repeat = math.ceil(self.timestep / 0.02) + physics_world_step_size = self.timestep / decision_repeat + + # Initialize the simulator with ego vehicle + self.client = utils.DriveEnv( + dict( + decision_repeat=decision_repeat, + physics_world_step_size=physics_world_step_size, + use_render=self.render3D, + vehicle_config={ + "spawn_position_heading": [ + converted_position, + converted_heading, + ], + }, + use_mesh_terrain=self.render3D, + log_level=logging.CRITICAL, + ) + ) + self.client.config["sumo_map"] = self.sumo_map + self.client.reset() + + # Assign the MetaDrive actor to the ego + metadrive_objects = self.client.engine.get_objects() + obj.metaDriveActor = list(metadrive_objects.values())[0] + self.defined_ego = True + return + + # For additional cars + if obj.isVehicle: + metaDriveActor = self.client.engine.agent_manager.spawn_object( + DefaultVehicle, + vehicle_config=dict(), + position=converted_position, + heading=converted_heading, + ) + obj.metaDriveActor = metaDriveActor + return + + # For pedestrians + if obj.isPedestrian: + metaDriveActor = self.client.engine.agent_manager.spawn_object( + Pedestrian, + position=converted_position, + heading_theta=converted_heading, + ) + obj.metaDriveActor = metaDriveActor + return + + # If the object type is unsupported, raise an error + raise SimulationCreationError( + f"Unsupported object type: {type(obj)} for object {obj}." + ) + + def executeActions(self, allActions): + """Execute actions for all vehicles in the simulation.""" + super().executeActions(allActions) + + # Apply control updates to vehicles and pedestrians + for obj in self.scene.objects[1:]: # Skip ego vehicle (it is handled separately) + if obj.isVehicle: + action = obj._collect_action() + obj.metaDriveActor.before_step(action) + obj._reset_control() + else: + # For Pedestrians + if obj._walking_direction is None: + obj._walking_direction = utils.scenicToMetaDriveHeading(obj.heading) + if obj._walking_speed is None: + obj._walking_speed = obj.speed + direction = [ + math.cos(obj._walking_direction), + math.sin(obj._walking_direction), + ] + obj.metaDriveActor.set_velocity(direction, obj._walking_speed) + + def step(self): + start_time = time.monotonic() + + # Special handling for the ego vehicle + ego_obj = self.scene.objects[0] + action = ego_obj._collect_action() + self.client.step(action) # Apply action in the simulator + ego_obj._reset_control() + + # Render the scene in 2D if needed + if self.render and not self.render3D: + self.client.render( + mode="topdown", semantic_map=True, film_size=self.film_size, scaling=5 + ) + + # If real-time synchronization is enabled, sleep to maintain real-time pace + if self.real_time: + end_time = time.monotonic() + elapsed_time = end_time - start_time + if elapsed_time < self.timestep: + time.sleep(self.timestep - elapsed_time) + + def destroy(self): + if self.client and self.client.engine: + object_ids = list(self.client.engine._spawned_objects.keys()) + if object_ids: + self.client.engine.agent_manager.clear_objects(object_ids) + self.client.close() + + super().destroy() + + def getProperties(self, obj, properties): + metaDriveActor = obj.metaDriveActor + position = utils.metadriveToScenicPosition( + metaDriveActor.position, self.scenic_offset + ) + velocity = Vector(*metaDriveActor.velocity, 0) + speed = metaDriveActor.speed + md_ang_vel = metaDriveActor.body.getAngularVelocity() + angularVelocity = Vector(*md_ang_vel) + angularSpeed = math.hypot(*md_ang_vel) + converted_heading = utils.metaDriveToScenicHeading(metaDriveActor.heading_theta) + yaw, pitch, roll = obj.parentOrientation.globalToLocalAngles( + converted_heading, 0, 0 + ) + elevation = 0 + + values = dict( + position=position, + velocity=velocity, + speed=speed, + angularSpeed=angularSpeed, + angularVelocity=angularVelocity, + yaw=yaw, + pitch=pitch, + roll=roll, + elevation=elevation, + ) + + return values + + def getLaneFollowingControllers(self, agent): + dt = self.timestep + if agent.isCar: + lon_controller = PIDLongitudinalController(K_P=0.5, K_D=0.1, K_I=0.7, dt=dt) + lat_controller = PIDLateralController(K_P=0.13, K_D=0.3, K_I=0.05, dt=dt) + else: + lon_controller = PIDLongitudinalController( + K_P=0.25, K_D=0.025, K_I=0.0, dt=dt + ) + lat_controller = PIDLateralController(K_P=0.2, K_D=0.1, K_I=0.0, dt=dt) + return lon_controller, lat_controller + + def getTurningControllers(self, agent): + dt = self.timestep + if agent.isCar: + lon_controller = PIDLongitudinalController(K_P=0.5, K_D=0.1, K_I=0.7, dt=dt) + lat_controller = PIDLateralController(K_P=0.2, K_D=0.2, K_I=0.2, dt=dt) + else: + lon_controller = PIDLongitudinalController( + K_P=0.25, K_D=0.025, K_I=0.0, dt=dt + ) + lat_controller = PIDLateralController(K_P=0.4, K_D=0.1, K_I=0.0, dt=dt) + return lon_controller, lat_controller + + def getLaneChangingControllers(self, agent): + dt = self.timestep + if agent.isCar: + lon_controller = PIDLongitudinalController(K_P=0.5, K_D=0.1, K_I=0.7, dt=dt) + lat_controller = PIDLateralController(K_P=0.2, K_D=0.2, K_I=0.02, dt=dt) + else: + lon_controller = PIDLongitudinalController( + K_P=0.25, K_D=0.025, K_I=0.0, dt=dt + ) + lat_controller = PIDLateralController(K_P=0.1, K_D=0.3, K_I=0.0, dt=dt) + return lon_controller, lat_controller diff --git a/src/scenic/simulators/metadrive/utils.py b/src/scenic/simulators/metadrive/utils.py new file mode 100644 index 000000000..dba86e93e --- /dev/null +++ b/src/scenic/simulators/metadrive/utils.py @@ -0,0 +1,104 @@ +# NOTE: MetaDrive uses a coordinate system where (0,0) is centered +# around the middle of the SUMO map. To ensure alignment, we shift +# positions using both the computed SUMO map center (center_x, center_y) +# and adjust for SUMO’s netOffset (offset_x, offset_y). + +import math +import xml.etree.ElementTree as ET + +from metadrive.envs import BaseEnv +from metadrive.manager.sumo_map_manager import SumoMapManager +from metadrive.obs.observation_base import DummyObservation + +from scenic.core.vectors import Vector + + +def calculateFilmSize(sumo_map_boundary, scaling=5, margin_factor=1.1): + """Calculates the film size for rendering based on the map's boundary.""" + # Calculate the width and height based on the sumo_map_boundary + xmin, ymin, xmax, ymax = sumo_map_boundary + width = xmax - xmin + height = ymax - ymin + + # Apply margin and convert to pixels + adjusted_width = width * margin_factor + adjusted_height = height * margin_factor + return int(adjusted_width * scaling), int(adjusted_height * scaling) + + +def extractNetOffsetAndBoundary(sumo_map_path): + """Extracts the net offset and boundary from the given SUMO map file.""" + tree = ET.parse(sumo_map_path) + root = tree.getroot() + location_tag = root.find("location") + net_offset = tuple(map(float, location_tag.attrib["netOffset"].split(","))) + sumo_map_boundary = tuple(map(float, location_tag.attrib["convBoundary"].split(","))) + return net_offset, sumo_map_boundary + + +def getMapParameters(sumo_map_path): + """Retrieve the map parameters.""" + net_offset, sumo_map_boundary = extractNetOffsetAndBoundary(sumo_map_path) + xmin, ymin, xmax, ymax = sumo_map_boundary + center_x = (xmin + xmax) / 2 + center_y = (ymin + ymax) / 2 + scenic_offset = (center_x - net_offset[0], center_y - net_offset[1]) + return scenic_offset, sumo_map_boundary + + +def metadriveToScenicPosition(loc, scenic_offset): + """Converts MetaDrive position to Scenic position using map parameters.""" + x_scenic = loc[0] + scenic_offset[0] + y_scenic = loc[1] + scenic_offset[1] + return Vector(x_scenic, y_scenic, 0) + + +def scenicToMetaDrivePosition(vec, scenic_offset): + """Converts Scenic position to MetaDrive position using map parameters.""" + adjusted_x = vec[0] - scenic_offset[0] + adjusted_y = vec[1] - scenic_offset[1] + return adjusted_x, adjusted_y + + +def scenicToMetaDriveHeading(scenicHeading): + """ + Converts Scenic heading to MetaDrive heading by adding π/2 (90 degrees). + + Scenic's coordinate system has 0 radians pointing North, while MetaDrive uses + 0 radians pointing East. This function shifts the heading to align with MetaDrive's system. + """ + metadriveHeading = scenicHeading + (math.pi / 2) + # Normalize to [-π, π] + return (metadriveHeading + math.pi) % (2 * math.pi) - math.pi + + +def metaDriveToScenicHeading(metaDriveHeading): + """Converts MetaDrive heading to Scenic heading by subtracting π/2 (90 degrees).""" + scenicHeading = metaDriveHeading - (math.pi / 2) + # Normalize to [-π, π] + return (scenicHeading + math.pi) % (2 * math.pi) - math.pi + + +class DriveEnv(BaseEnv): + def reward_function(self, agent): + """Dummy reward function.""" + return 0, {} + + def cost_function(self, agent): + """Dummy cost function.""" + return 0, {} + + def done_function(self, agent): + """Dummy done function.""" + return False, {} + + def get_single_observation(self): + """Dummy observation function.""" + return DummyObservation() + + def setup_engine(self): + """Setup the engine for MetaDrive.""" + super().setup_engine() + self.engine.register_manager( + "map_manager", SumoMapManager(self.config["sumo_map"]) + ) diff --git a/tests/simulators/metadrive/basic.scenic b/tests/simulators/metadrive/basic.scenic new file mode 100644 index 000000000..612fd146b --- /dev/null +++ b/tests/simulators/metadrive/basic.scenic @@ -0,0 +1,13 @@ +param map = localPath('../../../assets/maps/CARLA/Town01.xodr') +param sumo_map = localPath('../../../assets/maps/CARLA/Town01.net.xml') + +model scenic.simulators.metadrive.model + +ego = new Car in intersection + +ego = new Car on ego.lane.predecessor + +new Pedestrian on visible sidewalk + +third = new Car on visible ego.road +require abs((apparent heading of third) - 180 deg) <= 30 deg diff --git a/tests/simulators/metadrive/test_metadrive.py b/tests/simulators/metadrive/test_metadrive.py new file mode 100644 index 000000000..60205fb08 --- /dev/null +++ b/tests/simulators/metadrive/test_metadrive.py @@ -0,0 +1,130 @@ +import os + +import pytest + +try: + import metadrive + + from scenic.simulators.metadrive import MetaDriveSimulator +except ModuleNotFoundError: + pytest.skip("MetaDrive package not installed", allow_module_level=True) + +from tests.utils import compileScenic, pickle_test, sampleScene, tryPickling + + +def test_basic(loadLocalScenario): + scenario = loadLocalScenario("basic.scenic", mode2D=True) + scenario.generate(maxIterations=1000) + + +@pickle_test +@pytest.mark.slow +def test_pickle(loadLocalScenario): + scenario = tryPickling(loadLocalScenario("basic.scenic", mode2D=True)) + tryPickling(sampleScene(scenario, maxIterations=1000)) + + +@pytest.fixture(scope="package") +def getMetadriveSimulator(getAssetPath): + base = getAssetPath("maps/CARLA") + + def _getMetadriveSimulator(town): + openDrivePath = os.path.join(base, f"{town}.xodr") + sumoPath = os.path.join(base, f"{town}.net.xml") + simulator = MetaDriveSimulator(sumo_map=sumoPath, render=False) + return simulator, openDrivePath, sumoPath + + yield _getMetadriveSimulator + + +def test_throttle(getMetadriveSimulator): + simulator, openDrivePath, sumoPath = getMetadriveSimulator("Town01") + code = f""" + param map = r'{openDrivePath}' + param sumo_map = r'{sumoPath}' + + model scenic.simulators.metadrive.model + + behavior DriveWithThrottle(): + while True: + take SetThrottleAction(1) + + ego = new Car at (369, -326), with behavior DriveWithThrottle + record ego.speed as CarSpeed + terminate after 5 steps + """ + scenario = compileScenic(code, mode2D=True) + scene = sampleScene(scenario) + simulation = simulator.simulate(scene) + speeds = simulation.result.records["CarSpeed"] + assert speeds[len(speeds) // 2][1] < speeds[-1][1] + + +@pytest.mark.xfail( + reason="Expected failure until MetaDrive uploads the next version on PyPI to fix the issue where cars aren't fully stopping." +) +def test_brake(getMetadriveSimulator): + simulator, openDrivePath, sumoPath = getMetadriveSimulator("Town01") + code = f""" + param map = r'{openDrivePath}' + param sumo_map = r'{sumoPath}' + + model scenic.simulators.metadrive.model + + behavior DriveWithThrottle(): + while True: + take SetThrottleAction(1) + + behavior Brake(): + while True: + take SetThrottleAction(0), SetBrakeAction(1) + + behavior DriveThenBrake(): + do DriveWithThrottle() for 2 steps + do Brake() for 6 steps + + ego = new Car at (369, -326), + with behavior DriveThenBrake + record final ego.speed as CarSpeed + terminate after 8 steps + """ + scenario = compileScenic(code, mode2D=True) + scene = sampleScene(scenario) + simulation = simulator.simulate(scene) + finalSpeed = simulation.result.records["CarSpeed"] + assert finalSpeed == pytest.approx(0.0, abs=1e-1) + + +def test_pedestrian_movement(getMetadriveSimulator): + simulator, openDrivePath, sumoPath = getMetadriveSimulator("Town01") + code = f""" + param map = r'{openDrivePath}' + param sumo_map = r'{sumoPath}' + + model scenic.simulators.metadrive.model + + behavior WalkForward(): + while True: + take SetWalkingDirectionAction(self.heading), SetWalkingSpeedAction(0.5) + + behavior StopWalking(): + while True: + take SetWalkingSpeedAction(0) + + behavior WalkThenStop(): + do WalkForward() for 2 steps + do StopWalking() for 2 steps + + ego = new Car at (30, 2) + pedestrian = new Pedestrian at (50, 6), with behavior WalkThenStop + + record initial pedestrian.position as InitialPos + record final pedestrian.position as FinalPos + terminate after 4 steps + """ + scenario = compileScenic(code, mode2D=True) + scene = sampleScene(scenario) + simulation = simulator.simulate(scene) + initialPos = simulation.result.records["InitialPos"] + finalPos = simulation.result.records["FinalPos"] + assert initialPos != finalPos From 5d36cceaf6574dda343ae30c4d8332d8b140cb36 Mon Sep 17 00:00:00 2001 From: Lola Marrero <110120745+lola831@users.noreply.github.com> Date: Fri, 21 Mar 2025 09:19:32 -0700 Subject: [PATCH 16/73] fix: update pyglet constraint to allow 2.0.18 with Apple Silicon fix (#330) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 66ee2b29c..33f5f3984 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ dependencies = [ "pillow >= 9.1", 'pygame >= 2.1.3.dev8, <3; python_version >= "3.11"', 'pygame ~= 2.0; python_version < "3.11"', - "pyglet ~= 1.5", + "pyglet >= 1.5", "python-fcl >= 0.7", "Rtree ~= 1.0", "rv-ltl ~= 0.1", From 0727a6c6021f36ed202a436f78ddf3b770f95daf Mon Sep 17 00:00:00 2001 From: Lola Marrero <110120745+lola831@users.noreply.github.com> Date: Fri, 4 Apr 2025 09:23:36 -0700 Subject: [PATCH 17/73] Update all workflows to reference fixed commit hashes instead of floating version tags (#333) --- .github/workflows/check-formatting.yml | 6 +++--- .github/workflows/run-coverage.yml | 12 ++++++------ .github/workflows/run-tests.yml | 6 +++--- .github/workflows/sync-issues-with-jira.yml | 8 ++++---- .github/workflows/zizmor.yml | 10 +++++----- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/check-formatting.yml b/.github/workflows/check-formatting.yml index 5bd2f9128..bdd76a82f 100644 --- a/.github/workflows/check-formatting.yml +++ b/.github/workflows/check-formatting.yml @@ -10,16 +10,16 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 with: persist-credentials: false - name: Run black to check formatting - uses: psf/black@stable + uses: psf/black@8a737e727ac5ab2f1d4cf5876720ed276dc8dc4b with: version: "25.1.0" - name: Run isort to check import order - uses: isort/isort-action@v1 + uses: isort/isort-action@24d8a7a51d33ca7f36c3f23598dafa33f7071326 with: isort-version: "5.12.0" diff --git a/.github/workflows/run-coverage.yml b/.github/workflows/run-coverage.yml index aa41f4e46..67472f28d 100644 --- a/.github/workflows/run-coverage.yml +++ b/.github/workflows/run-coverage.yml @@ -29,21 +29,21 @@ jobs: steps: - name: Checkout given ref - uses: actions/checkout@v3 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 if: inputs.ref != '' with: ref: ${{ inputs.ref }} persist-credentials: false - name: Checkout current branch - uses: actions/checkout@v3 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 if: inputs.ref == '' with: ref: ${{ github.ref }} persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 with: python-version: ${{ matrix.python-version }} cache: 'pip' @@ -59,9 +59,9 @@ jobs: - name: Run and report code coverage run: | pytest --cov --cov-report json - + - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v4.0.1 + uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 with: token: ${{ secrets.CODECOV_TOKEN }} - slug: BerkeleyLearnVerify/Scenic \ No newline at end of file + slug: BerkeleyLearnVerify/Scenic diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 4386b1542..69336f470 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -43,21 +43,21 @@ jobs: steps: - name: Checkout given ref - uses: actions/checkout@v3 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 if: inputs.ref != '' with: ref: ${{ inputs.ref }} persist-credentials: false - name: Checkout current branch - uses: actions/checkout@v3 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 if: inputs.ref == '' with: ref: ${{ github.ref }} persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 with: python-version: ${{ matrix.python-version }} cache: 'pip' diff --git a/.github/workflows/sync-issues-with-jira.yml b/.github/workflows/sync-issues-with-jira.yml index c07924afa..80b34b94f 100644 --- a/.github/workflows/sync-issues-with-jira.yml +++ b/.github/workflows/sync-issues-with-jira.yml @@ -9,7 +9,7 @@ jobs: steps: - name: Get issue details id: get_issue_details - uses: actions/github-script@v4 + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea with: github-token: ${{ secrets.GH_ACCESS_TOKEN }} script: | @@ -19,7 +19,7 @@ jobs: const issueLink = `https://github.com/${repoName}/issues/${issueNumber}`; console.log(`::set-output name=issueTitle::${issueTitle}`); console.log(`::set-output name=issueLink::${issueLink}`); - + - name: Create Jira Ticket env: JIRA_DOMAIN: ${{ secrets.JIRA_DOMAIN }} @@ -30,7 +30,7 @@ jobs: run: | echo "Issue Title: $ISSUE_TITLE" echo "Issue Link: $ISSUE_LINK" - + curl --request POST \ --url "https://$JIRA_DOMAIN.atlassian.net/rest/api/3/issue" \ --user "$JIRA_EMAIL:$JIRA_API_TOKEN" \ @@ -61,4 +61,4 @@ jobs: "key": "SCENIC" } } - }' \ No newline at end of file + }' diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index ecc69380d..e65667276 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -18,20 +18,20 @@ jobs: actions: read steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 with: persist-credentials: false - name: Install the latest version of uv - uses: astral-sh/setup-uv@v4 + uses: astral-sh/setup-uv@22695119d769bdb6f7032ad67b9bca0ef8c4a174 - name: Run zizmor 🌈 - run: uvx zizmor --format sarif . > results.sarif + run: uvx zizmor --format sarif . > results.sarif env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload SARIF file - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@1b549b9259bda1cb5ddde3b41741a82a2d15a841 with: sarif_file: results.sarif category: zizmor From b72430ca367ea37dc42932e77fe3aa53998ab5bd Mon Sep 17 00:00:00 2001 From: Lola Marrero <110120745+lola831@users.noreply.github.com> Date: Mon, 7 Apr 2025 12:19:34 -0700 Subject: [PATCH 18/73] Use shapely.points instead of shapely.lib.points (#336) --- src/scenic/core/geometry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scenic/core/geometry.py b/src/scenic/core/geometry.py index dba37887a..b60a68b2d 100644 --- a/src/scenic/core/geometry.py +++ b/src/scenic/core/geometry.py @@ -110,7 +110,7 @@ def distanceToLine(point, a, b): # Fastest known way to make a Shapely Point from a list/tuple/Vector -makeShapelyPoint = shapely.lib.points +makeShapelyPoint = shapely.points def polygonUnion(polys, buf=0, tolerance=0, holeTolerance=0.002): From c8aad620d1ae6b272da36af2d57c80fc2bfb05b1 Mon Sep 17 00:00:00 2001 From: Lola Marrero <110120745+lola831@users.noreply.github.com> Date: Fri, 11 Apr 2025 09:14:39 -0700 Subject: [PATCH 19/73] Raise ValueError for missing sumo_map in MetaDriveSimulator initializer (#335) * Raise ValueError for missing sumo_map in MetaDriveSimulator initializer * Make sumo_map required param in MetaDriveSimulator * Trigger update of PR --- src/scenic/simulators/metadrive/simulator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scenic/simulators/metadrive/simulator.py b/src/scenic/simulators/metadrive/simulator.py index f3b101433..4a3c126e4 100644 --- a/src/scenic/simulators/metadrive/simulator.py +++ b/src/scenic/simulators/metadrive/simulator.py @@ -27,10 +27,10 @@ class MetaDriveSimulator(DrivingSimulator): def __init__( self, + sumo_map, timestep=0.1, render=True, render3D=False, - sumo_map=None, real_time=True, ): super().__init__() From 6afd8e8af38007afc7c0a5742b23d74a7e77e88b Mon Sep 17 00:00:00 2001 From: Lola Marrero <110120745+lola831@users.noreply.github.com> Date: Tue, 15 Apr 2025 14:49:00 -0700 Subject: [PATCH 20/73] Create CODE_OF_CONDUCT.md (#337) --- CODE_OF_CONDUCT.md | 128 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..924ac95d5 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +coc@forum.scenic-lang.org. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. From 74a8fdce655b12fc3c3b3c45f48e582d153d2f85 Mon Sep 17 00:00:00 2001 From: Daniel Fremont Date: Tue, 15 Apr 2025 19:54:37 -0700 Subject: [PATCH 21/73] Add governance and roadmap pages to documentation (#342) * add references to UCB and UCSC in the README for the Open Source Browser * add governance and roadmap to documentation --- README.md | 4 +-- docs/governance.rst | 88 +++++++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 2 ++ docs/roadmap.rst | 64 +++++++++++++++++++++++++++++++++ 4 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 docs/governance.rst create mode 100644 docs/roadmap.rst diff --git a/README.md b/README.md index a6732338e..f804a7d98 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ For an overview of the language and some of its applications, see our [2022 jour The new syntax and features of Scenic 3 are described in our [CAV 2023 paper](https://arxiv.org/abs/2307.03325). Our [Publications](https://docs.scenic-lang.org/en/latest/publications.html) page lists additional relevant publications. -Scenic was initially designed and implemented by Daniel J. Fremont, Tommaso Dreossi, Shromona Ghosh, Xiangyu Yue, Alberto L. Sangiovanni-Vincentelli, and Sanjit A. Seshia. -Additionally, Edward Kim made major contributions to Scenic 2, and Eric Vin, Shun Kashiwa, Matthew Rhea, and Ellen Kalvan to Scenic 3. +Scenic was initially designed and implemented at UC Berkeley by Daniel J. Fremont, Tommaso Dreossi, Shromona Ghosh, Xiangyu Yue, Alberto L. Sangiovanni-Vincentelli, and Sanjit A. Seshia. +Subsequent work has been done primarily at UC Berkeley and UC Santa Cruz: in particular, Edward Kim made major contributions to Scenic 2, and Eric Vin, Shun Kashiwa, Matthew Rhea, and Ellen Kalvan to Scenic 3. Please see our [Credits](https://docs.scenic-lang.org/en/latest/credits.html) page for details and more contributors. If you have any problems using Scenic, please submit an issue to [our GitHub repository](https://github.com/BerkeleyLearnVerify/Scenic) or start a conversation on our [community forum](https://forum.scenic-lang.org/). diff --git a/docs/governance.rst b/docs/governance.rst new file mode 100644 index 000000000..8fef6fab8 --- /dev/null +++ b/docs/governance.rst @@ -0,0 +1,88 @@ +Project Governance +================== + +This document describes the organization and governance of the Scenic project. The key elements are as follows: + + +Steering Committee +------------------ + +The Scenic Steering Committee (SC) has responsibility for the overall governance of the Scenic project. These responsibilities include setting and revising project policies, overseeing the work of the Scenic Core Team and the Working Groups (see below), creating and phasing-out working groups, and being the final authority on changes to the Scenic language and its associated tools. + +The composition of the SC has been initially fixed based on the PIs involved in Scenic's development. In 2025-2026, we plan to develop a democratic process for choosing SC members which is inclusive of the broader Scenic community, including advisors from academia, industry, and government. + +Current SC members: + + * Parasara Sridhar Duggirala (UNC Chapel Hill) + * Daniel Fremont (UC Santa Cruz) + * Necmiye Ozay (U Michigan) + * Sanjit Seshia (UC Berkeley) + + +Core Team +--------- + +The Scenic Core Team (CT) is a trusted core group of researchers, developers, and community members who help manage and develop the Scenic project. Currently, members of the CT are chosen by the SC. + +Current CT members: + + * Kai-Chun Chang + * Parasara Sridhar Duggirala + * Daniel Fremont + * Edward Kim + * Lola Marrero + * Necmiye Ozay + * Sanjit Seshia + * Hazem Torfah + * Eric Vin + * Kai Xu + * Beyazit Yalcinkaya + + +Working Groups +-------------- + +Most of the development of Scenic is governed by specialized working groups, whose procedures are set by the Steering Committee and whose leadership comes from the Core Team. The current working groups are: + + +Language and Infrastructure WG +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This WG governs changes to Scenic's syntax, as well as the project's repository and test infrastructure. + +Current members: + + * Daniel Fremont (co-chair) + * Sanjit Seshia (co-chair) + * Edward Kim + * Hazem Torfah + * Eric Vin + + +Community WG +~~~~~~~~~~~~ + +This WG focuses on matters related to the Scenic user/contributor community. It governs workshops, bootcamps, and other outreach events, as well as development of documentation and other materials. + +Current members: + + * Edward Kim (chair) + * Daniel Fremont + * Sanjit Seshia + + +Autonomous Driving WG +~~~~~~~~~~~~~~~~~~~~~ + +This WG focuses on applications of Scenic in the autonomous driving domain, developing tools for and performing outreach to that community. + +Current members: + + * Eric Vin (chair) + * Necmiye Ozay (co-chair) + * Parasara Sridhar Duggirala (co-chair) + * Kai-Chun Chang + * Ruya Karagulle + * Dejan Ničković + * Hazem Torfah + * Beyazit Yalcinkaya diff --git a/docs/index.rst b/docs/index.rst index cc5fbcd76..d17108c42 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -60,6 +60,8 @@ Table of Contents :maxdepth: 1 :caption: General Information + roadmap + governance publications credits diff --git a/docs/roadmap.rst b/docs/roadmap.rst new file mode 100644 index 000000000..828db89df --- /dev/null +++ b/docs/roadmap.rst @@ -0,0 +1,64 @@ +Project Roadmap +=============== + +This document describes the core areas of development for the Scenic project, as decided by the Steering Committee (see `governance`). + +Our long-term vision is that Scenic becomes a foundational, widely-used, open-source representation and toolkit supporting the entire design lifecycle of autonomous intelligent cyber-physical systems (AI-CPS). Towards that end, we are working in three primary directions: + + 1. Facilitating applications of Scenic in both existing and new domains. + 2. Creating infrastructure to support the use and development of Scenic. + 3. Building a user and developer community through dissemination and outreach activities. + + +Application Development +----------------------- + +This thrust comprises work to facilitate the use of Scenic in specific application domains, both those where Scenic is already being successfully used and new domains that could have high impact. We are currently focusing on three domains: autonomous driving, robotics, and extended (virtual/augmented) reality. + +Autonomous Driving +~~~~~~~~~~~~~~~~~~ + +The Autonomous Driving Working Group is charged with supporting and expanding Scenic's proven use for safety testing/verification of autonomous vehicles. Planned work includes: + + * Test suite generation + * Metrics and visualization + * Improved driver modeling + * Tutorials on testing autonomous vehicles using Scenic + + +Robotics +~~~~~~~~ + +We are working to expand preliminary applications of Scenic to testing and training robotic systems, particularly those which interact with human beings. Planned work includes: + + * Interfaces to simulators including MuJoCo, Gazebo, Habitat, and Isaac Sim + * A Gym-style API to facilitate training RL agents using Scenic + * Tutorials on testing and training robots using Scenic + + +Extended Reality +~~~~~~~~~~~~~~~~ + +Extended (virtual/augmented) reality is a relatively new application domain for Scenic that we have been exploring. Planned work aims to develop personalized training and evaluation methods for sports and healthcare applications. + + +Infrastructure Development +-------------------------- + +This thrust comprises work on computational infrastructure to support Scenic's development. Planned work includes: + + * Enhancing the CI system to test all supported simulators (CARLA and Webots already completed) + * Enhancing the CI system to benchmark scene generation + * Creating a system for managing Scenic Improvement Proposals (SIPs) + * Creating an index for Scenic scenarios and libraries similar to the Python Package Index (PyPI) + + +Governance and Community Engagement +----------------------------------- + +This thrust comprises work to support and grow the community of Scenic users and developers, as well as to develop governance policies ensuring that the evolution of the project reflects the needs of all stakeholders. Planned work includes: + + * Convening working groups for each of the application areas above + * Developing governance policies, e.g. for electing Steering Committee and Core Team members and for evaluating Scenic Improvement Proposals + * Continuing and expanding the annual Scenic Workshop + * Running tutorials at academic and industry conferences From 4ecc20b09ce4efd200ff4d88beb16351ffd0b9bd Mon Sep 17 00:00:00 2001 From: Shreedhar Govil Date: Thu, 17 Apr 2025 00:39:00 +0200 Subject: [PATCH 22/73] Improve Carla Autopilot configuration for Scenic (#332) * add speed to Scenic Autopilot * add autopilot changes * chore: apply Black formatting to Carla autopilot changes --------- Co-authored-by: Shreedhar Govil Co-authored-by: lola --- src/scenic/simulators/carla/actions.py | 35 +++++++++++++++++++- src/scenic/simulators/carla/behaviors.scenic | 4 +-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/scenic/simulators/carla/actions.py b/src/scenic/simulators/carla/actions.py index 8aba75e48..5ca7edbd4 100644 --- a/src/scenic/simulators/carla/actions.py +++ b/src/scenic/simulators/carla/actions.py @@ -108,15 +108,48 @@ def applyTo(self, obj, sim): class SetAutopilotAction(VehicleAction): - def __init__(self, enabled): + def __init__(self, enabled, **kwargs): + """ + :param enabled: Enable or disable autopilot (bool) + :param kwargs: Additional autopilot options such as: + - speed: Speed of the car in m/s (default: None) + - path: Route for the vehicle to follow (default: None) + - ignore_signs_percentage: Percentage of ignored traffic signs (default: 0) + - ignore_lights_percentage: Percentage of ignored traffic lights (default: 0) + - ignore_walkers_percentage: Percentage of ignored pedestrians (default: 0) + - auto_lane_change: Whether to allow automatic lane changes (default: False) + """ if not isinstance(enabled, bool): raise RuntimeError("Enabled must be a boolean.") + self.enabled = enabled + # Default values for optional parameters + self.speed = kwargs.get("speed", None) + self.path = kwargs.get("path", None) + self.ignore_signs_percentage = kwargs.get("ignore_signs_percentage", 0) + self.ignore_lights_percentage = kwargs.get("ignore_lights_percentage", 0) + self.ignore_walkers_percentage = kwargs.get("ignore_walkers_percentage", 0) + self.auto_lane_change = kwargs.get("auto_lane_change", False) # Default: False + def applyTo(self, obj, sim): vehicle = obj.carlaActor vehicle.set_autopilot(self.enabled, sim.tm.get_port()) + # Apply auto lane change setting + sim.tm.auto_lane_change(vehicle, self.auto_lane_change) + + if self.path: + sim.tm.set_route(vehicle, self.path) + if self.speed: + sim.tm.set_desired_speed(vehicle, 3.6 * self.speed) + + # Apply traffic management settings + sim.tm.update_vehicle_lights(vehicle, True) + sim.tm.ignore_signs_percentage(vehicle, self.ignore_signs_percentage) + sim.tm.ignore_lights_percentage(vehicle, self.ignore_lights_percentage) + sim.tm.ignore_walkers_percentage(vehicle, self.ignore_walkers_percentage) + class SetVehicleLightStateAction(VehicleAction): """Set the vehicle lights' states. diff --git a/src/scenic/simulators/carla/behaviors.scenic b/src/scenic/simulators/carla/behaviors.scenic index 9ed39942b..1af819a4a 100644 --- a/src/scenic/simulators/carla/behaviors.scenic +++ b/src/scenic/simulators/carla/behaviors.scenic @@ -7,9 +7,9 @@ try: except ModuleNotFoundError: pass # ignore; error will be caught later if user attempts to run a simulation -behavior AutopilotBehavior(): +behavior AutopilotBehavior(enabled = True, **kwargs): """Behavior causing a vehicle to use CARLA's built-in autopilot.""" - take SetAutopilotAction(True) + take SetAutopilotAction(enabled=enabled, **kwargs) behavior WalkForwardBehavior(speed=0.5): take SetWalkingDirectionAction(self.heading), SetWalkingSpeedAction(speed) From 9d1e52deb0aeb479dab68cb357129fc771c283ce Mon Sep 17 00:00:00 2001 From: Daniel Fremont Date: Thu, 17 Apr 2025 16:36:12 -0700 Subject: [PATCH 23/73] Various optimizations (#319) * disable redundant centering of occupiedSpace * apply MeshRegion transformations simultaneously * reduce redundant checks on occupiedSpace * improve backtrace for errors inside Point @properties * PolygonalFootprintRegion.containsObject checks convex hull before exact bounding polygon * speed up bounding box intersection check * built-in mutators directly modify objects * [WIP] precompute geometric data when shape/dimensions are fixed * precompute geometric data when shape/dimensions are fixed * improve requirement sorting heuristic * fix collision detection benchmarking * precompute body count of MeshVolumeRegion * speed up copying trimeshes when creating MeshRegions * avoid redundant scale computations for fixed shapes * reuse occupiedSpace for fixed objects * compute MeshRegion.mesh lazily * do circumradius check before computing bounding boxes; skip inradius check unless precomputed * skip bounding box check if meshes are lazy * fix transforms of precomputed Shape data; cache MultiPoint of vertices * minor tweaks & extra tests to cover new lines * minor fix; more tests * fix reproducibility issue * robustify MeshRegion boundingPolygon test --- pyproject.toml | 2 +- src/scenic/core/object_types.py | 66 +++- src/scenic/core/regions.py | 343 ++++++++++++------ src/scenic/core/requirements.py | 39 +- src/scenic/core/sample_checking.py | 13 +- src/scenic/core/scenarios.py | 5 +- src/scenic/core/shapes.py | 15 +- src/scenic/core/utils.py | 42 +++ tests/core/test_regions.py | 169 ++++++++- tests/core/test_serialization.py | 1 + tests/core/test_shapes.py | 15 +- tests/syntax/test_distributions.py | 54 ++- .../collisions/benchmark_collisions.py | 3 + .../collisions/city_intersection.scenic | 2 +- .../collisions/narrowGoalNew.scenic | 8 +- tools/benchmarking/collisions/vacuum.scenic | 22 +- 16 files changed, 636 insertions(+), 163 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 33f5f3984..b7928353e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ dependencies = [ "scikit-image ~= 0.21", "scipy ~= 1.7", "shapely ~= 2.0", - "trimesh >=4.0.9, <5", + "trimesh >=4.4.8, <5", ] [project.optional-dependencies] diff --git a/src/scenic/core/object_types.py b/src/scenic/core/object_types.py index 5230f0c83..a49ac062c 100644 --- a/src/scenic/core/object_types.py +++ b/src/scenic/core/object_types.py @@ -16,6 +16,8 @@ from abc import ABC, abstractmethod import collections +import functools +import inspect import math import random import typing @@ -77,7 +79,7 @@ toVector, underlyingType, ) -from scenic.core.utils import DefaultIdentityDict, cached_method, cached_property +from scenic.core.utils import DefaultIdentityDict, cached, cached_method, cached_property from scenic.core.vectors import ( Orientation, Vector, @@ -214,6 +216,7 @@ def __init__(self, properties, constProps=frozenset(), _internal=False): self.properties = tuple(sorted(properties.keys())) self._propertiesSet = set(self.properties) self._constProps = constProps + self._sampleParent = None @classmethod def _withProperties(cls, properties, constProps=None): @@ -544,7 +547,9 @@ def sampleGiven(self, value): if not needsSampling(self): return self props = {prop: value[getattr(self, prop)] for prop in self.properties} - return type(self)(props, constProps=self._constProps, _internal=True) + obj = type(self)(props, constProps=self._constProps, _internal=True) + obj._sampleParent = self + return obj def _allProperties(self): return {prop: getattr(self, prop) for prop in self.properties} @@ -599,6 +604,35 @@ def __repr__(self): return f"{type(self).__name__}({allProps})" +def precomputed_property(func): + """A @property which can be precomputed if its dependencies are not random. + + Converts a function inside a subclass of `Constructible` into a method; the + function's arguments must correspond to the properties of the object needed + to compute this property. If any of those dependencies have random values, + this property will evaluate to `None`; otherwise it will be computed once + the first time it is needed and then reused across samples. + """ + deps = tuple(inspect.signature(func).parameters) + + @cached + @functools.wraps(func) + def method(self): + args = [getattr(self, prop) for prop in deps] + if any(needsSampling(arg) for arg in args): + return None + return func(*args) + + @functools.wraps(func) + def wrapper(self): + parent = self._sampleParent or self + return method(parent) + + wrapper._scenic_cache_clearer = method._scenic_cache_clearer + + return property(wrapper) + + ## Mutators @@ -1297,14 +1331,38 @@ def _corners2D(self): @cached_property def occupiedSpace(self): """A region representing the space this object occupies""" + if self._sampleParent and self._sampleParent._hasStaticBounds: + return self._sampleParent.occupiedSpace + shape = self.shape + scaledShape = self._scaledShape + if scaledShape: + mesh = scaledShape.mesh + dimensions = None # mesh does not need to be scaled + convex = scaledShape.isConvex + else: + mesh = shape.mesh + dimensions = (self.width, self.length, self.height) + convex = shape.isConvex return MeshVolumeRegion( - mesh=shape.mesh, - dimensions=(self.width, self.length, self.height), + mesh=mesh, + dimensions=dimensions, position=self.position, rotation=self.orientation, centerMesh=False, _internal=True, + _isConvex=convex, + _shape=shape, + _scaledShape=scaledShape, + ) + + @precomputed_property + def _scaledShape(shape, width, length, height): + return MeshVolumeRegion( + mesh=shape.mesh, + dimensions=(width, length, height), + centerMesh=False, + _internal=True, _isConvex=shape.isConvex, ) diff --git a/src/scenic/core/regions.py b/src/scenic/core/regions.py index 4732fda1c..6047ec6ab 100644 --- a/src/scenic/core/regions.py +++ b/src/scenic/core/regions.py @@ -13,6 +13,7 @@ import random import warnings +import fcl import numpy import scipy import shapely @@ -56,7 +57,13 @@ ) from scenic.core.lazy_eval import isLazy, valueInContext from scenic.core.type_support import toOrientation, toScalar, toVector -from scenic.core.utils import cached, cached_method, cached_property, unifyMesh +from scenic.core.utils import ( + cached, + cached_method, + cached_property, + findMeshInteriorPoint, + unifyMesh, +) from scenic.core.vectors import ( Orientation, OrientedVector, @@ -806,7 +813,7 @@ def __init__( # Copy parameters self._mesh = mesh self.dimensions = None if dimensions is None else toVector(dimensions) - self.position = None if position is None else toVector(position) + self.position = Vector(0, 0, 0) if position is None else toVector(position) self.rotation = None if rotation is None else toOrientation(rotation) self.orientation = None if orientation is None else toDistribution(orientation) self.tolerance = tolerance @@ -828,31 +835,17 @@ def __init__( if isLazy(self): return - # Convert extract mesh - if isinstance(mesh, trimesh.primitives.Primitive): - self._mesh = mesh.to_mesh() - elif isinstance(mesh, trimesh.base.Trimesh): - self._mesh = mesh.copy() - else: + if not isinstance(mesh, (trimesh.primitives.Primitive, trimesh.base.Trimesh)): raise TypeError( f"Got unexpected mesh parameter of type {type(mesh).__name__}" ) - # Center mesh unless disabled - if centerMesh: - self.mesh.vertices -= self.mesh.bounding_box.center_mass - # Apply scaling, rotation, and translation, if any - if self.dimensions is not None: - scale = numpy.array(self.dimensions) / self.mesh.extents - else: - scale = None if self.rotation is not None: angles = self.rotation._trimeshEulerAngles() else: angles = None - matrix = compose_matrix(scale=scale, angles=angles, translate=self.position) - self.mesh.apply_transform(matrix) + self._rigidTransform = compose_matrix(angles=angles, translate=self.position) self.orientation = orientation @@ -937,10 +930,27 @@ def evaluateInner(self, context): ) ## API Methods ## - @property + @cached_property @distributionFunction def mesh(self): - return self._mesh + mesh = self._mesh + + # Convert/extract mesh + if isinstance(mesh, trimesh.primitives.Primitive): + mesh = mesh.to_mesh() + elif isinstance(mesh, trimesh.base.Trimesh): + mesh = mesh.copy(include_visual=False) + else: + assert False, f"mesh of invalid type {type(mesh).__name__}" + + # Center mesh unless disabled + if self.centerMesh: + mesh.vertices -= mesh.bounding_box.center_mass + + # Apply scaling, rotation, and translation, if any + mesh.apply_transform(self._transform) + + return mesh @distributionFunction def projectVector(self, point, onDirection): @@ -1003,9 +1013,50 @@ def AABB(self): tuple(self.mesh.bounds[1]), ) + @cached_property + def _transform(self): + """Transform from input mesh to final mesh. + + :meta private: + """ + if self.dimensions is not None: + scale = numpy.array(self.dimensions) / self._mesh.extents + else: + scale = None + if self.rotation is not None: + angles = self.rotation._trimeshEulerAngles() + else: + angles = None + transform = compose_matrix(scale=scale, angles=angles, translate=self.position) + return transform + + @cached_property + def _shapeTransform(self): + """Transform from Shape mesh (scaled to unit dimensions) to final mesh. + + :meta private: + """ + if self.dimensions is not None: + scale = numpy.array(self.dimensions) + else: + scale = self._mesh.extents + if self.rotation is not None: + angles = self.rotation._trimeshEulerAngles() + else: + angles = None + transform = compose_matrix(scale=scale, angles=angles, translate=self.position) + return transform + @cached_property def _boundingPolygonHull(self): assert not isLazy(self) + if self._shape: + raw = self._shape._multipoint + tr = self._shapeTransform + matrix = numpy.concatenate((tr[0:3, 0:3].flatten(), tr[0:3, 3])) + transformed = shapely.affinity.affine_transform(raw, matrix) + return transformed.convex_hull + return shapely.multipoints(self.mesh.vertices).convex_hull @cached_property @@ -1075,9 +1126,20 @@ class MeshVolumeRegion(MeshRegion): onDirection: The direction to use if an object being placed on this region doesn't specify one. """ - def __init__(self, *args, _internal=False, _isConvex=None, **kwargs): + def __init__( + self, + *args, + _internal=False, + _isConvex=None, + _shape=None, + _scaledShape=None, + **kwargs, + ): super().__init__(*args, **kwargs) self._isConvex = _isConvex + self._shape = _shape + self._scaledShape = _scaledShape + self._num_samples = None if isLazy(self): return @@ -1095,18 +1157,6 @@ def __init__(self, *args, _internal=False, _isConvex=None, **kwargs): " Consider using scenic.core.utils.repairMesh." ) - # Compute how many samples are necessary to achieve 99% probability - # of success when rejection sampling volume. - p_volume = self._mesh.volume / self._mesh.bounding_box.volume - - if p_volume > 0.99: - self.num_samples = 1 - else: - self.num_samples = min(1e6, max(1, math.ceil(math.log(0.01, 1 - p_volume)))) - - # Always try to take at least 8 samples to avoid surface point total rejections - self.num_samples = max(self.num_samples, 8) - # Property testing methods # @distributionFunction def intersects(self, other, triedReversed=False): @@ -1119,73 +1169,23 @@ def intersects(self, other, triedReversed=False): """ if isinstance(other, MeshVolumeRegion): # PASS 1 - # Check if bounding boxes intersect. If not, volumes cannot intersect. - # For bounding boxes to intersect there must be overlap of the bounds - # in all 3 dimensions. - bounds = self._mesh.bounds - obounds = other._mesh.bounds - range_overlaps = ( - (bounds[0, dim] <= obounds[1, dim]) - and (obounds[0, dim] <= bounds[1, dim]) - for dim in range(3) - ) - bb_overlap = all(range_overlaps) - - if not bb_overlap: + # Check if the centers of the regions are far enough apart that the regions + # cannot overlap. This check only requires the circumradius of each region, + # which we can often precompute without explicitly constructing the mesh. + center_distance = numpy.linalg.norm(self.position - other.position) + if center_distance > self._circumradius + other._circumradius: return False - # PASS 2 - # Compute inradius and circumradius for a candidate point in each region, - # and compute the inradius and circumradius of each point. If the candidate - # points are closer than the sum of the inradius values, they must intersect. - # If the candidate points are farther apart than the sum of the circumradius - # values, they can't intersect. - - # Get a candidate point from each mesh. If the center of the object is in the mesh use that. - # Otherwise try to sample a point as a candidate, skipping this pass if the sample fails. - if self.containsPoint(Vector(*self.mesh.bounding_box.center_mass)): - s_candidate_point = Vector(*self.mesh.bounding_box.center_mass) - elif ( - len(samples := trimesh.sample.volume_mesh(self.mesh, self.num_samples)) - > 0 - ): - s_candidate_point = Vector(*samples[0]) - else: - s_candidate_point = None + # PASS 2A + # If precomputed inradii are available, check if the volumes are close enough + # to ensure a collision. (While we're at it, check circumradii too.) + if self._scaledShape and other._scaledShape: + s_point = self._interiorPoint + s_inradius, s_circumradius = self._interiorPointRadii + o_point = other._interiorPoint + o_inradius, o_circumradius = other._interiorPointRadii - if other.containsPoint(Vector(*other.mesh.bounding_box.center_mass)): - o_candidate_point = Vector(*other.mesh.bounding_box.center_mass) - elif ( - len(samples := trimesh.sample.volume_mesh(other.mesh, other.num_samples)) - > 0 - ): - o_candidate_point = Vector(*samples[0]) - else: - o_candidate_point = None - - if s_candidate_point is not None and o_candidate_point is not None: - # Compute the inradius of each object from its candidate point. - s_inradius = abs( - trimesh.proximity.ProximityQuery(self.mesh).signed_distance( - [s_candidate_point] - )[0] - ) - o_inradius = abs( - trimesh.proximity.ProximityQuery(other.mesh).signed_distance( - [o_candidate_point] - )[0] - ) - - # Compute the circumradius of each object from its candidate point. - s_circumradius = numpy.max( - numpy.linalg.norm(self.mesh.vertices - s_candidate_point, axis=1) - ) - o_circumradius = numpy.max( - numpy.linalg.norm(other.mesh.vertices - o_candidate_point, axis=1) - ) - - # Get the distance between the two points and check for mandatory or impossible collision. - point_distance = s_candidate_point.distanceTo(o_candidate_point) + point_distance = numpy.linalg.norm(s_point - o_point) if point_distance < s_inradius + o_inradius: return True @@ -1193,38 +1193,53 @@ def intersects(self, other, triedReversed=False): if point_distance > s_circumradius + o_circumradius: return False + # PASS 2B + # If precomputed geometry is not available, compute the bounding boxes + # (requiring that we construct the meshes, if they were previously lazy; + # hence we only do this check if we'll be constructing meshes anyway). + # For bounding boxes to intersect there must be overlap of the bounds + # in all 3 dimensions. + else: + bounds = self.mesh.bounds + obounds = other.mesh.bounds + range_overlaps = ( + (bounds[0, dim] <= obounds[1, dim]) + and (obounds[0, dim] <= bounds[1, dim]) + for dim in range(3) + ) + bb_overlap = all(range_overlaps) + + if not bb_overlap: + return False + # PASS 3 - # Use Trimesh's collision manager to check for intersection. + # Use FCL to check for intersection between the surfaces. # If the surfaces collide, that implies a collision of the volumes. # Cheaper than computing volumes immediately. - collision_manager = trimesh.collision.CollisionManager() + # (N.B. Does not require explicitly building the mesh, if we have a + # precomputed _scaledShape available.) - collision_manager.add_object("SelfRegion", self.mesh) - collision_manager.add_object("OtherRegion", other.mesh) - - surface_collision = collision_manager.in_collision_internal() + selfObj = fcl.CollisionObject(*self._fclData) + otherObj = fcl.CollisionObject(*other._fclData) + surface_collision = fcl.collide(selfObj, otherObj) if surface_collision: return True - if self.mesh.is_convex and other.mesh.is_convex: - # For convex shapes, the manager detects containment as well as + if self.isConvex and other.isConvex: + # For convex shapes, FCL detects containment as well as # surface intersections, so we can just return the result return surface_collision # PASS 4 - # If we have 2 candidate points and both regions have only one body, - # we can just check if either region contains the candidate point of the - # other. (This is because we previously ruled out surface intersections) - if ( - s_candidate_point is not None - and o_candidate_point is not None - and self.mesh.body_count == 1 - and other.mesh.body_count == 1 - ): - return self.containsPoint(o_candidate_point) or other.containsPoint( - s_candidate_point - ) + # If both regions have only one body, we can just check if either region + # contains an arbitrary interior point of the other. (This is because we + # previously ruled out surface intersections) + if self._bodyCount == 1 and other._bodyCount == 1: + overlap = self._containsPointExact( + other._interiorPoint + ) or other._containsPointExact(self._interiorPoint) + return overlap # PASS 5 # Compute intersection and check if it's empty. Expensive but guaranteed @@ -1291,6 +1306,9 @@ def containsPoint(self, point): """Check if this region's volume contains a point.""" return self.distanceTo(point) <= self.tolerance + def _containsPointExact(self, point): + return self.mesh.contains([point])[0] + @distributionFunction def containsObject(self, obj): """Check if this region's volume contains an :obj:`~scenic.core.object_types.Object`.""" @@ -1836,6 +1854,97 @@ def getVolumeRegion(self): """Returns this object, as it is already a MeshVolumeRegion""" return self + @property + def num_samples(self): + if self._num_samples is not None: + return self._num_samples + + # Compute how many samples are necessary to achieve 99% probability + # of success when rejection sampling volume. + volume = self._scaledShape.mesh.volume if self._scaledShape else self.mesh.volume + p_volume = volume / self.mesh.bounding_box.volume + + if p_volume > 0.99: + num_samples = 1 + else: + num_samples = math.ceil(min(1e6, max(1, math.log(0.01, 1 - p_volume)))) + + # Always try to take at least 8 samples to avoid surface point total rejections + self._num_samples = max(num_samples, 8) + return self._num_samples + + @cached_property + def _circumradius(self): + if self._scaledShape: + return self._scaledShape._circumradius + if self._shape: + dims = self.dimensions or self._mesh.extents + scale = max(dims) + return scale * self._shape._circumradius + + return numpy.max(numpy.linalg.norm(self.mesh.vertices, axis=1)) + + @cached_property + def _interiorPoint(self): + # Use precomputed point if available (transformed appropriately) + if self._scaledShape: + raw = self._scaledShape._interiorPoint + homog = numpy.append(raw, [1]) + return numpy.dot(self._rigidTransform, homog)[:3] + if self._shape: + raw = self._shape._interiorPoint + homog = numpy.append(raw, [1]) + return numpy.dot(self._shapeTransform, homog)[:3] + + return findMeshInteriorPoint(self.mesh, num_samples=self.num_samples) + + @cached_property + def _interiorPointRadii(self): + # Use precomputed radii if available + if self._scaledShape: + return self._scaledShape._interiorPointRadii + + # Compute inradius and circumradius w.r.t. the point + point = self._interiorPoint + pq = trimesh.proximity.ProximityQuery(self.mesh) + inradius = abs(pq.signed_distance([point])[0]) + circumradius = numpy.max(numpy.linalg.norm(self.mesh.vertices - point, axis=1)) + return inradius, circumradius + + @cached_property + def _bodyCount(self): + # Use precomputed geometry if available + if self._scaledShape: + return self._scaledShape._bodyCount + + return self.mesh.body_count + + @cached_property + def _fclData(self): + # Use precomputed geometry if available + if self._scaledShape: + geom = self._scaledShape._fclData[0] + trans = fcl.Transform(self.rotation.r.as_matrix(), numpy.array(self.position)) + return geom, trans + + mesh = self.mesh + if self.isConvex: + vertCounts = 3 * numpy.ones((len(mesh.faces), 1), dtype=numpy.int64) + faces = numpy.concatenate((vertCounts, mesh.faces), axis=1) + geom = fcl.Convex(mesh.vertices, len(faces), faces.flatten()) + else: + geom = fcl.BVHModel() + geom.beginModel(num_tris_=len(mesh.faces), num_vertices_=len(mesh.vertices)) + geom.addSubModel(mesh.vertices, mesh.faces) + geom.endModel() + trans = fcl.Transform() + return geom, trans + + def __getstate__(self): + state = self.__dict__.copy() + state.pop("_cached__fclData", None) # remove non-picklable FCL objects + return state + class MeshSurfaceRegion(MeshRegion): """A region representing the surface of a mesh. diff --git a/src/scenic/core/requirements.py b/src/scenic/core/requirements.py index ab6de91da..287ec98c2 100644 --- a/src/scenic/core/requirements.py +++ b/src/scenic/core/requirements.py @@ -6,6 +6,8 @@ import inspect import itertools +import fcl +import numpy import rv_ltl import trimesh @@ -319,6 +321,8 @@ def violationMsg(self): class IntersectionRequirement(SamplingRequirement): + """Requirement that a pair of objects do not intersect.""" + def __init__(self, objA, objB, optional=False): super().__init__(optional=optional) self.objA = objA @@ -337,6 +341,16 @@ def violationMsg(self): class BlanketCollisionRequirement(SamplingRequirement): + """Requirement that the surfaces of a given set of objects do not intersect. + + We can check for such intersections more quickly than full objects using FCL, + but since FCL checks for surface intersections rather than volume intersections + (in general), this requirement being satisfied does not imply that the objects + do not intersect (one might still be contained in another). Therefore, this + requirement is intended to quickly check for intersections in the common case + rather than completely determine whether any objects collide. + """ + def __init__(self, objects, optional=True): super().__init__(optional=optional) self.objects = objects @@ -344,23 +358,32 @@ def __init__(self, objects, optional=True): def falsifiedByInner(self, sample): objects = tuple(sample[obj] for obj in self.objects) - cm = trimesh.collision.CollisionManager() + manager = fcl.DynamicAABBTreeCollisionManager() + objForGeom = {} for i, obj in enumerate(objects): - if not obj.allowCollisions: - cm.add_object(str(i), obj.occupiedSpace.mesh) - collision, names = cm.in_collision_internal(return_names=True) + if obj.allowCollisions: + continue + geom, trans = obj.occupiedSpace._fclData + collisionObject = fcl.CollisionObject(geom, trans) + objForGeom[geom] = obj + manager.registerObject(collisionObject) + + manager.setup() + cdata = fcl.CollisionData() + manager.collide(cdata, fcl.defaultCollisionCallback) + collision = cdata.result.is_collision if collision: - self._collidingObjects = tuple(sorted(names)) + contact = cdata.result.contacts[0] + self._collidingObjects = (objForGeom[contact.o1], objForGeom[contact.o2]) return collision @property def violationMsg(self): assert self._collidingObjects is not None - objA_index, objB_index = map(int, self._collidingObjects[0]) - objA, objB = self.objects[objA_index], self.objects[objB_index] - return f"Intersection violation: {objA} intersects {objB}" + objA, objB = self._collidingObjects + return f"Blanket Intersection violation: {objA} intersects {objB}" class ContainmentRequirement(SamplingRequirement): diff --git a/src/scenic/core/sample_checking.py b/src/scenic/core/sample_checking.py index 3a96826df..040d9a23a 100644 --- a/src/scenic/core/sample_checking.py +++ b/src/scenic/core/sample_checking.py @@ -106,7 +106,7 @@ def sortedRequirements(self): """Return the list of requirements in sorted order""" # Extract and sort active requirements reqs = [req for req in self.requirements if req.active] - reqs.sort(key=self.getWeightedAcceptanceProb) + reqs.sort(key=self.getRequirementCost) # Remove any optional requirements at the end of the list, since they're useless while reqs and reqs[-1].optional: @@ -131,6 +131,13 @@ def updateMetrics(self, req, new_metrics): sum_time += new_time - old_time self.bufferSums[req] = (sum_acc, sum_time) - def getWeightedAcceptanceProb(self, req): + def getRequirementCost(self, req): + # Expected cost of a requirement is average runtime divided by rejection probability; + # if estimated rejection probability is zero, break ties using runtime. sum_acc, sum_time = self.bufferSums[req] - return (sum_acc / self.bufferSize) * (sum_time / self.bufferSize) + runtime = sum_time / self.bufferSize + rej_prob = 1 - (sum_acc / self.bufferSize) + if rej_prob > 0: + return (runtime / rej_prob, 0) + else: + return (float("inf"), runtime) diff --git a/src/scenic/core/scenarios.py b/src/scenic/core/scenarios.py index 133911013..fa93d454b 100644 --- a/src/scenic/core/scenarios.py +++ b/src/scenic/core/scenarios.py @@ -495,8 +495,9 @@ def generateDefaultRequirements(self): requirements = [] ## Optional Requirements ## - # Any collision indicates an intersection - requirements.append(BlanketCollisionRequirement(self.objects)) + # Any collision between object surfaces indicates an intersection + if INITIAL_COLLISION_CHECK: + requirements.append(BlanketCollisionRequirement(self.objects)) ## Mandatory Requirements ## # Pairwise object intersection diff --git a/src/scenic/core/shapes.py b/src/scenic/core/shapes.py index b03fe4a1e..0c50a22e4 100644 --- a/src/scenic/core/shapes.py +++ b/src/scenic/core/shapes.py @@ -3,6 +3,7 @@ from abc import ABC, abstractmethod import numpy +import shapely import trimesh from trimesh.transformations import ( concatenate_matrices, @@ -11,7 +12,7 @@ ) from scenic.core.type_support import toOrientation -from scenic.core.utils import cached_property, unifyMesh +from scenic.core.utils import cached_property, findMeshInteriorPoint, unifyMesh from scenic.core.vectors import Orientation ################################################################################################### @@ -64,6 +65,18 @@ def mesh(self): def isConvex(self): pass + @cached_property + def _circumradius(self): + return numpy.max(numpy.linalg.norm(self.mesh.vertices, axis=1)) + + @cached_property + def _interiorPoint(self): + return findMeshInteriorPoint(self.mesh) + + @cached_property + def _multipoint(self): + return shapely.multipoints(self.mesh.vertices) + ################################################################################################### # 3D Shape Classes diff --git a/src/scenic/core/utils.py b/src/scenic/core/utils.py index 4549afdad..9817843f5 100644 --- a/src/scenic/core/utils.py +++ b/src/scenic/core/utils.py @@ -307,6 +307,48 @@ def repairMesh(mesh, pitch=(1 / 2) ** 6, verbose=True): raise ValueError("Mesh could not be repaired.") +def findMeshInteriorPoint(mesh, num_samples=None): + # Use center of mass if it's contained + com = mesh.bounding_box.center_mass + if mesh.contains([com])[0]: + return com + + # Try sampling a point inside the volume + if num_samples is None: + p_volume = mesh.volume / mesh.bounding_box.volume + if p_volume > 0.99: + num_samples = 1 + else: + num_samples = math.ceil(min(1e6, max(1, math.log(0.01, 1 - p_volume)))) + + # Do the "random" number generation ourselves so that it's deterministic + # (this helps debugging and reproducibility) + rng = numpy.random.default_rng(49493130352093220597973654454967996892) + pts = (rng.random((num_samples, 3)) * mesh.extents) + mesh.bounds[0] + samples = pts[mesh.contains(pts)] + if samples.size > 0: + return samples[0] + + # If all else fails, take a point from the surface and move inward + surfacePt, index = list(zip(*mesh.sample(1, return_index=True)))[0] + inward = -mesh.face_normals[index] + startPt = surfacePt + 1e-6 * inward + hits, _, _ = mesh.ray.intersects_location( + ray_origins=[startPt], + ray_directions=[inward], + multiple_hits=False, + ) + if hits.size > 0: + endPt = hits[0] + midPt = (surfacePt + endPt) / 2.0 + if mesh.contains([midPt])[0]: + return midPt + + # Should never get here with reasonable geometry, but we return a surface + # point just in case. + return surfacePt # pragma: no cover + + class DefaultIdentityDict: """Dictionary which is the identity map by default. diff --git a/tests/core/test_regions.py b/tests/core/test_regions.py index b7293ab3c..d97e500c8 100644 --- a/tests/core/test_regions.py +++ b/tests/core/test_regions.py @@ -1,6 +1,7 @@ import math from pathlib import Path +import fcl import pytest import shapely.geometry import trimesh.voxel @@ -8,10 +9,20 @@ from scenic.core.distributions import RandomControlFlowError, Range from scenic.core.object_types import Object, OrientedPoint from scenic.core.regions import * -from scenic.core.vectors import VectorField +from scenic.core.shapes import ConeShape, MeshShape +from scenic.core.vectors import Orientation, VectorField from tests.utils import deprecationTest, sampleSceneFrom +def assertPolygonsEqual(p1, p2, prec=1e-6): + assert p1.difference(p2).area == pytest.approx(0, abs=prec) + assert p2.difference(p1).area == pytest.approx(0, abs=prec) + + +def assertPolygonCovers(p1, p2, prec=1e-6): + assert p2.difference(p1).area == pytest.approx(0, abs=prec) + + def sample_ignoring_rejections(region, num_samples): samples = [] for _ in range(num_samples): @@ -288,6 +299,13 @@ def test_mesh_region_fromFile(getAssetPath): ) +def test_mesh_region_invalid_mesh(): + with pytest.raises(TypeError): + MeshVolumeRegion(42) + with pytest.raises(TypeError): + MeshSurfaceRegion(42) + + def test_mesh_volume_region_zero_dimension(): for dims in ((0, 1, 1), (1, 0, 1), (1, 1, 0)): with pytest.raises(ValueError): @@ -338,6 +356,155 @@ def test_mesh_intersects(): assert not r1.getSurfaceRegion().intersects(r2.getSurfaceRegion()) +def test_mesh_boundingPolygon(getAssetPath, pytestconfig): + r = BoxRegion(dimensions=(8, 6, 2)).difference(BoxRegion(dimensions=(2, 2, 3))) + bp = r.boundingPolygon + poly = shapely.geometry.Polygon( + [(-4, 3), (4, 3), (4, -3), (-4, -3)], [[(-1, 1), (1, 1), (1, -1), (-1, -1)]] + ) + assertPolygonsEqual(bp.polygons, poly) + poly = shapely.geometry.Polygon([(-4, 3), (4, 3), (4, -3), (-4, -3)]) + assertPolygonsEqual(r._boundingPolygonHull, poly) + + shape = MeshShape(BoxRegion(dimensions=(1, 2, 3)).mesh) + r = MeshVolumeRegion(shape.mesh, dimensions=(2, 4, 2), _shape=shape) + bp = r.boundingPolygon + poly = shapely.geometry.Polygon([(-1, 2), (1, 2), (1, -2), (-1, -2)]) + assertPolygonsEqual(bp.polygons, poly) + assertPolygonsEqual(r._boundingPolygonHull, poly) + + o = Orientation.fromEuler(0, 0, math.pi / 4) + r = MeshVolumeRegion(shape.mesh, dimensions=(2, 4, 2), rotation=o, _shape=shape) + bp = r.boundingPolygon + sr2 = math.sqrt(2) + poly = shapely.geometry.Polygon([(-sr2, 2), (sr2, 2), (sr2, -2), (-sr2, -2)]) + assertPolygonsEqual(bp.polygons, poly) + assertPolygonsEqual(r._boundingPolygonHull, poly) + + samples = 50 if pytestconfig.getoption("--fast") else 200 + regions = [] + # Convex + r = BoxRegion(dimensions=(1, 2, 3), position=(4, 5, 6)) + regions.append(r) + # Convex, with scaledShape plus transform + bo = Orientation.fromEuler(math.pi / 4, math.pi / 4, math.pi / 4) + regions.append( + MeshVolumeRegion(r.mesh, position=(15, 20, 5), rotation=bo, _scaledShape=r) + ) + # Convex, with shape and scaledShape plus transform + s = MeshShape(r.mesh) + regions.append( + MeshVolumeRegion( + r.mesh, rotation=bo, position=(4, 5, 6), _shape=s, _scaledShape=r + ) + ) + # Not convex + planePath = getAssetPath("meshes/classic_plane.obj.bz2") + regions.append(MeshVolumeRegion.fromFile(planePath, dimensions=(20, 20, 10))) + # Not convex, with shape plus transform + shape = MeshShape.fromFile(planePath) + regions.append( + MeshVolumeRegion(shape.mesh, dimensions=(0.5, 2, 1.5), rotation=bo, _shape=shape) + ) + for reg in regions: + bp = reg.boundingPolygon + for pt in trimesh.sample.volume_mesh(reg.mesh, samples): + pt[2] = 0 + # exact containment check may fail since polygon is approximate + assert bp.distanceTo(pt) <= 1e-3 + bphull = reg._boundingPolygonHull + assertPolygonCovers(bphull, bp.polygons) + simple = shapely.multipoints(reg.mesh.vertices).convex_hull + assertPolygonsEqual(bphull, simple) + + +def test_mesh_circumradius(getAssetPath): + r1 = BoxRegion(dimensions=(1, 2, 3), position=(4, 5, 6)) + + bo = Orientation.fromEuler(math.pi / 4, math.pi / 4, math.pi / 4) + r2 = MeshVolumeRegion(r1.mesh, position=(15, 20, 5), rotation=bo, _scaledShape=r1) + + planePath = getAssetPath("meshes/classic_plane.obj.bz2") + r3 = MeshVolumeRegion.fromFile(planePath, dimensions=(20, 20, 10)) + + shape = MeshShape.fromFile(planePath) + r4 = MeshVolumeRegion(shape.mesh, dimensions=(0.5, 2, 1.5), rotation=bo, _shape=shape) + + r = BoxRegion(dimensions=(1, 2, 3)).difference(BoxRegion(dimensions=(0.5, 1, 1))) + shape = MeshShape(r.mesh) + scaled = MeshVolumeRegion(shape.mesh, dimensions=(6, 5, 4)).mesh + r5 = MeshVolumeRegion(scaled, position=(-10, -5, 30), rotation=bo, _shape=shape) + + for reg in (r1, r2, r3, r4, r5): + pos = reg.position + d = 2.01 * reg._circumradius + assert SpheroidRegion(dimensions=(d, d, d), position=pos).containsRegion(reg) + + +def test_mesh_interiorPoint(): + regions = [ + BoxRegion(dimensions=(1, 2, 3), position=(4, 5, 6)), + BoxRegion().difference(BoxRegion(dimensions=(0.1, 0.1, 0.1))), + ] + d = 1e6 + r = BoxRegion(dimensions=(d, d, d)).difference( + BoxRegion(dimensions=(d - 1, d - 1, d - 1)) + ) + r._num_samples = 8 # ensure sampling won't yield a good point + regions.append(r) + + bo = Orientation.fromEuler(math.pi / 4, math.pi / 4, math.pi / 4) + r2 = MeshVolumeRegion(r.mesh, position=(15, 20, 5), rotation=bo, _scaledShape=r) + regions.append(r2) + + shape = MeshShape(BoxRegion(dimensions=(1, 2, 3)).mesh) + r3 = MeshVolumeRegion(shape.mesh, position=(-10, -5, 30), rotation=bo, _shape=shape) + regions.append(r3) + + r = BoxRegion(dimensions=(1, 2, 3)).difference(BoxRegion(dimensions=(0.5, 1, 1))) + shape = MeshShape(r.mesh) + scaled = MeshVolumeRegion(shape.mesh, dimensions=(0.1, 0.1, 0.1)).mesh + r4 = MeshVolumeRegion(scaled, position=(-10, -5, 30), rotation=bo, _shape=shape) + regions.append(r4) + + for reg in regions: + cp = reg._interiorPoint + # N.B. _containsPointExact can fail with embreex installed! + assert reg.containsPoint(cp) + inr, circumr = reg._interiorPointRadii + d = 1.99 * inr + assert reg.containsRegion(SpheroidRegion(dimensions=(d, d, d), position=cp)) + d = 2.01 * circumr + assert SpheroidRegion(dimensions=(d, d, d), position=cp).containsRegion(reg) + + +def test_mesh_fcl(): + """Test internal construction of FCL models for MeshVolumeRegions.""" + r1 = BoxRegion(dimensions=(2, 2, 2)).difference(BoxRegion(dimensions=(1, 1, 3))) + + for heading, shouldInt in ((0, False), (math.pi / 4, True), (math.pi / 2, False)): + o = Orientation.fromEuler(heading, 0, 0) + r2 = BoxRegion(dimensions=(1.5, 1.5, 0.5), position=(2, 0, 0), rotation=o) + assert r1.intersects(r2) == shouldInt + + o1 = fcl.CollisionObject(*r1._fclData) + o2 = fcl.CollisionObject(*r2._fclData) + assert fcl.collide(o1, o2) == shouldInt + + bo = Orientation.fromEuler(math.pi / 4, math.pi / 4, math.pi / 4) + r3 = MeshVolumeRegion(r1.mesh, position=(15, 20, 5), rotation=bo, _scaledShape=r1) + o3 = fcl.CollisionObject(*r3._fclData) + r4pos = r3.position.offsetLocally(bo, (0, 2, 0)) + + for heading, shouldInt in ((0, False), (math.pi / 4, True), (math.pi / 2, False)): + o = bo * Orientation.fromEuler(heading, 0, 0) + r4 = BoxRegion(dimensions=(1.5, 1.5, 0.5), position=r4pos, rotation=o) + assert r3.intersects(r4) == shouldInt + + o4 = fcl.CollisionObject(*r4._fclData) + assert fcl.collide(o3, o4) == shouldInt + + def test_mesh_empty_intersection(): r1 = BoxRegion(position=(0, 0, 0)) r2 = BoxRegion(position=(10, 10, 10)) diff --git a/tests/core/test_serialization.py b/tests/core/test_serialization.py index 746075c94..503a869c1 100644 --- a/tests/core/test_serialization.py +++ b/tests/core/test_serialization.py @@ -54,6 +54,7 @@ def assertSceneEquivalence(scene1, scene2, ignoreDynamics=False, ignoreConstProp if ignoreDynamics: del scene1.dynamicScenario, scene2.dynamicScenario for obj in scene1.objects + scene2.objects: + del obj._sampleParent if ignoreConstProps: del obj._constProps if ignoreDynamics: diff --git a/tests/core/test_shapes.py b/tests/core/test_shapes.py index 95e257f8f..a27fd6b3d 100644 --- a/tests/core/test_shapes.py +++ b/tests/core/test_shapes.py @@ -3,7 +3,8 @@ import pytest -from scenic.core.shapes import BoxShape, MeshShape +from scenic.core.regions import BoxRegion +from scenic.core.shapes import BoxShape, CylinderShape, MeshShape def test_shape_fromFile(getAssetPath): @@ -21,3 +22,15 @@ def test_invalid_dimension(badDim): BoxShape(dimensions=dims) with pytest.raises(ValueError): BoxShape(scale=badDim) + + +def test_circumradius(): + s = CylinderShape(dimensions=(3, 1, 17)) # dimensions don't matter + assert s._circumradius == pytest.approx(math.sqrt(2) / 2) + + +def test_interiorPoint(): + s = MeshShape(BoxRegion().difference(BoxRegion(dimensions=(0.1, 0.1, 0.1))).mesh) + pt = s._interiorPoint + assert all(-0.5 <= coord <= 0.5 for coord in pt) + assert not all(-0.05 <= coord <= 0.05 for coord in pt) diff --git a/tests/syntax/test_distributions.py b/tests/syntax/test_distributions.py index c45572988..6b983c5d2 100644 --- a/tests/syntax/test_distributions.py +++ b/tests/syntax/test_distributions.py @@ -668,18 +668,54 @@ def test_reproducibility(): assert iterations == baseIterations +def test_reproducibility_lazy_interior(): + """Regression test for a reproducibility issue involving lazy region computations. + + In this test, an interior point of the objects' shape is computed on demand + during the first sample, then cached. The code for doing so used NumPy's PRNG, + meaning that a second sample with the same random seed could differ. + """ + scenario = compileScenic( + """ + import numpy + from scenic.core.distributions import distributionFunction + @distributionFunction + def gen(arg): + return numpy.random.random() + + region = BoxRegion().difference(BoxRegion(dimensions=(0.1, 0.1, 0.1))) + shape = MeshShape(region.mesh) # Shape which does not contain its center + other = new Object with shape shape + ego = new Object at (Range(0.9, 1.1), 0), with shape shape + param foo = ego intersects other # trigger computation of interior point + param bar = gen(ego) # generate number using NumPy's PRNG + """ + ) + seed = random.randint(0, 1000000000) + random.seed(seed) + numpy.random.seed(seed) + s1 = sampleScene(scenario, maxIterations=60) + random.seed(seed) + numpy.random.seed(seed) + s2 = sampleScene(scenario, maxIterations=60) + assert s1.params["bar"] == s2.params["bar"] + assert s1.egoObject.x == s2.egoObject.x + + @pytest.mark.slow def test_reproducibility_3d(): scenario = compileScenic( - "ego = new Object\n" - "workspace = Workspace(SpheroidRegion(dimensions=(25,15,10)))\n" - "region = BoxRegion(dimensions=(25,15,0.1))\n" - "obj_1 = new Object in workspace, facing Range(0, 360) deg, with width Range(0.5, 1), with length Range(0.5,1)\n" - "obj_2 = new Object in workspace, facing (Range(0, 360) deg, Range(0, 360) deg, Range(0, 360) deg)\n" - "obj_3 = new Object in workspace, on region\n" - "param foo = Uniform(1, 4, 9, 16, 25, 36)\n" - "x = Range(0, 1)\n" - "require x > 0.8" + """ + ego = new Object + workspace = Workspace(SpheroidRegion(dimensions=(5,5,5))) + region = BoxRegion(dimensions=(25,15,0.1)) + #obj_1 = new Object in workspace, facing Range(0, 360) deg, with width Range(0.5, 1), with length Range(0.5,1) + obj_2 = new Object in workspace, facing (Range(0, 360) deg, Range(0, 360) deg, Range(0, 360) deg) + #obj_3 = new Object in workspace, on region + param foo = ego intersects obj_2 + x = Range(0, 1) + require x > 0.8 + """ ) seeds = [random.randint(0, 100) for i in range(10)] for seed in seeds: diff --git a/tools/benchmarking/collisions/benchmark_collisions.py b/tools/benchmarking/collisions/benchmark_collisions.py index bfb36be7f..b68fcc349 100644 --- a/tools/benchmarking/collisions/benchmark_collisions.py +++ b/tools/benchmarking/collisions/benchmark_collisions.py @@ -71,6 +71,9 @@ def run_benchmark(path, params): results[(str((benchmark, benchmark_params)), param)] = results_val + # for setup, subresults in results.items(): + # print(f"{setup}: {subresults[0][1]:.2f} s") + # Plot times import matplotlib.pyplot as plt diff --git a/tools/benchmarking/collisions/city_intersection.scenic b/tools/benchmarking/collisions/city_intersection.scenic index e24170e04..268648e4c 100644 --- a/tools/benchmarking/collisions/city_intersection.scenic +++ b/tools/benchmarking/collisions/city_intersection.scenic @@ -15,7 +15,7 @@ from pathlib import Path class EgoCar(WebotsObject): webotsName: "EGO" - shape: MeshShape.fromFile(Path(localPath(".")).parent.parent.parent / "tools" / "meshes" / "bmwx5_hull.obj.bz2", initial_rotation=(90 deg, 0, 0)) + shape: MeshShape.fromFile(Path(localPath(".")).parent.parent.parent / "assets" / "meshes" / "bmwx5_hull.obj.bz2", initial_rotation=(90 deg, 0, 0)) positionOffset: Vector(-1.43580750, 0, -0.557354985).rotatedBy(Orientation.fromEuler(*self.orientationOffset)) cameraOffset: Vector(-1.43580750, 0, -0.557354985) + Vector(1.72, 0, 1.4) orientationOffset: (90 deg, 0, 0) diff --git a/tools/benchmarking/collisions/narrowGoalNew.scenic b/tools/benchmarking/collisions/narrowGoalNew.scenic index 2d5302098..535a6d443 100644 --- a/tools/benchmarking/collisions/narrowGoalNew.scenic +++ b/tools/benchmarking/collisions/narrowGoalNew.scenic @@ -12,7 +12,7 @@ workspace = Workspace(RectangularRegion(0 @ 0, 0, width, length)) class MarsGround(Ground): width: width length: length - color: (220, 114, 9) + #color: (220, 114, 9) gridSize: 20 class MarsHill(Hill): @@ -28,7 +28,7 @@ class Goal(WebotsObject): width: 0.1 length: 0.1 webotsType: 'GOAL' - color: (9 ,163, 220) + #color: (9 ,163, 220) class Rover(WebotsObject): """Mars rover.""" @@ -45,14 +45,14 @@ class Debris(WebotsObject): class BigRock(Debris): """Large rock.""" - shape: MeshShape.fromFile(Path(localPath(".")).parent.parent.parent / "tools" / "meshes" / "webots_rock_large.obj.bz2") + shape: MeshShape.fromFile(Path(localPath(".")).parent.parent.parent / "assets" / "meshes" / "webots_rock_large.obj.bz2") yaw: Range(0, 360 deg) webotsType: 'ROCK_BIG' positionOffset: Vector(0,0, -self.height/2) class Rock(Debris): """Small rock.""" - shape: MeshShape.fromFile(Path(localPath(".")).parent.parent.parent / "tools" / "meshes" / "webots_rock_small.obj.bz2") + shape: MeshShape.fromFile(Path(localPath(".")).parent.parent.parent / "assets" / "meshes" / "webots_rock_small.obj.bz2") yaw: Range(0, 360 deg) webotsType: 'ROCK_SMALL' positionOffset: Vector(0,0, -self.height/2) diff --git a/tools/benchmarking/collisions/vacuum.scenic b/tools/benchmarking/collisions/vacuum.scenic index e1701c948..e9ef1f157 100644 --- a/tools/benchmarking/collisions/vacuum.scenic +++ b/tools/benchmarking/collisions/vacuum.scenic @@ -30,54 +30,54 @@ class Floor(Object): length: 5 height: 0.01 position: (0,0,-0.005) - color: [200, 200, 200] + #color: [200, 200, 200] class Wall(WebotsObject): webotsAdhoc: {'physics': False} width: 5 length: 0.04 height: 0.5 - color: [160, 160, 160] + #color: [160, 160, 160] class DiningTable(WebotsObject): webotsAdhoc: {'physics': True} - shape: MeshShape.fromFile(Path(localPath(".")).parent.parent.parent / "tools" / "meshes" / "dining_table.obj.bz2") + shape: MeshShape.fromFile(Path(localPath(".")).parent.parent.parent / "assets" / "meshes" / "dining_table.obj.bz2") width: Range(0.7, 1.5) length: Range(0.7, 1.5) height: 0.75 density: 670 # Density of solid birch - color: [103, 71, 54] + #color: [103, 71, 54] class DiningChair(WebotsObject): webotsAdhoc: {'physics': True} - shape: MeshShape.fromFile(Path(localPath(".")).parent.parent.parent / "tools" / "meshes" / "dining_chair.obj.bz2", initial_rotation=(180 deg, 0, 0)) + shape: MeshShape.fromFile(Path(localPath(".")).parent.parent.parent / "assets" / "meshes" / "dining_chair.obj.bz2", initial_rotation=(180 deg, 0, 0)) width: 0.4 length: 0.4 height: 1 density: 670 # Density of solid birch positionStdDev: (0.05, 0.05 ,0) orientationStdDev: (10 deg, 0, 0) - color: [103, 71, 54] + #color: [103, 71, 54] class Couch(WebotsObject): webotsAdhoc: {'physics': False} - shape: MeshShape.fromFile(Path(localPath(".")).parent.parent.parent / "tools" / "meshes" / "couch.obj.bz2", initial_rotation=(-90 deg, 0, 0)) + shape: MeshShape.fromFile(Path(localPath(".")).parent.parent.parent / "assets" / "meshes" / "couch.obj.bz2", initial_rotation=(-90 deg, 0, 0)) width: 2 length: 0.75 height: 0.75 positionStdDev: (0.05, 0.5 ,0) orientationStdDev: (5 deg, 0, 0) - color: [51, 51, 255] + #color: [51, 51, 255] class CoffeeTable(WebotsObject): webotsAdhoc: {'physics': False} - shape: MeshShape.fromFile(Path(localPath(".")).parent.parent.parent / "tools" / "meshes" / "coffee_table.obj.bz2") + shape: MeshShape.fromFile(Path(localPath(".")).parent.parent.parent / "assets" / "meshes" / "coffee_table.obj.bz2") width: 1.5 length: 0.5 height: 0.4 positionStdDev: (0.05, 0.05 ,0) orientationStdDev: (5 deg, 0, 0) - color: [103, 71, 54] + #color: [103, 71, 54] class Toy(WebotsObject): webotsAdhoc: {'physics': True} @@ -86,7 +86,7 @@ class Toy(WebotsObject): length: 0.1 height: 0.1 density: 100 - color: [255, 128, 0] + #color: [255, 128, 0] class BlockToy(Toy): shape: BoxShape() From 56608bacbc8f59ceab0420da439669e69b5221cd Mon Sep 17 00:00:00 2001 From: Lola Marrero <110120745+lola831@users.noreply.github.com> Date: Mon, 28 Apr 2025 14:36:36 -0700 Subject: [PATCH 24/73] Temporarily skip test_mesh_interiorPoint due to inconsistencies (#344) --- tests/core/test_regions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/core/test_regions.py b/tests/core/test_regions.py index d97e500c8..ca5a7adb7 100644 --- a/tests/core/test_regions.py +++ b/tests/core/test_regions.py @@ -441,6 +441,9 @@ def test_mesh_circumradius(getAssetPath): assert SpheroidRegion(dimensions=(d, d, d), position=pos).containsRegion(reg) +@pytest.mark.skip( + reason="Temporarily skipping due to inconsistencies; needs further investigation." +) def test_mesh_interiorPoint(): regions = [ BoxRegion(dimensions=(1, 2, 3), position=(4, 5, 6)), From 17404e0ae2deea0b7b144104a44213eca1173033 Mon Sep 17 00:00:00 2001 From: Lola Marrero <110120745+lola831@users.noreply.github.com> Date: Tue, 29 Apr 2025 15:53:39 -0700 Subject: [PATCH 25/73] Add support for initial velocity in MetaDrive simulations (#340) --- src/scenic/simulators/metadrive/simulator.py | 19 ++++++++-------- tests/simulators/metadrive/test_metadrive.py | 23 ++++++++++++++++++++ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/scenic/simulators/metadrive/simulator.py b/src/scenic/simulators/metadrive/simulator.py index 4a3c126e4..f2e4cd50b 100644 --- a/src/scenic/simulators/metadrive/simulator.py +++ b/src/scenic/simulators/metadrive/simulator.py @@ -112,6 +112,14 @@ def createObjectInSimulator(self, obj): ) converted_heading = utils.scenicToMetaDriveHeading(obj.heading) + vehicle_config = {} + if obj.isVehicle: + vehicle_config["spawn_position_heading"] = [ + converted_position, + converted_heading, + ] + vehicle_config["spawn_velocity"] = [obj.velocity.x, obj.velocity.y] + if not self.defined_ego: decision_repeat = math.ceil(self.timestep / 0.02) physics_world_step_size = self.timestep / decision_repeat @@ -122,12 +130,7 @@ def createObjectInSimulator(self, obj): decision_repeat=decision_repeat, physics_world_step_size=physics_world_step_size, use_render=self.render3D, - vehicle_config={ - "spawn_position_heading": [ - converted_position, - converted_heading, - ], - }, + vehicle_config=vehicle_config, use_mesh_terrain=self.render3D, log_level=logging.CRITICAL, ) @@ -145,9 +148,7 @@ def createObjectInSimulator(self, obj): if obj.isVehicle: metaDriveActor = self.client.engine.agent_manager.spawn_object( DefaultVehicle, - vehicle_config=dict(), - position=converted_position, - heading=converted_heading, + vehicle_config=vehicle_config, ) obj.metaDriveActor = metaDriveActor return diff --git a/tests/simulators/metadrive/test_metadrive.py b/tests/simulators/metadrive/test_metadrive.py index 60205fb08..3f638fabc 100644 --- a/tests/simulators/metadrive/test_metadrive.py +++ b/tests/simulators/metadrive/test_metadrive.py @@ -128,3 +128,26 @@ def test_pedestrian_movement(getMetadriveSimulator): initialPos = simulation.result.records["InitialPos"] finalPos = simulation.result.records["FinalPos"] assert initialPos != finalPos + + +def test_initial_velocity_movement(getMetadriveSimulator): + simulator, openDrivePath, sumoPath = getMetadriveSimulator("Town01") + code = f""" + param map = r'{openDrivePath}' + param sumo_map = r'{sumoPath}' + + model scenic.simulators.metadrive.model + + # Car should move 5 m/s west + ego = new Car at (30, 2), with velocity (-5, 0) + record initial ego.position as InitialPos + record final ego.position as FinalPos + terminate after 1 steps + """ + scenario = compileScenic(code, mode2D=True) + scene = sampleScene(scenario) + simulation = simulator.simulate(scene) + initialPos = simulation.result.records["InitialPos"] + finalPos = simulation.result.records["FinalPos"] + dx = finalPos[0] - initialPos[0] + assert dx < -0.1, f"Expected car to move west (negative dx), but got dx = {dx}" From c1003e9b89c612e81790151640fb78f298a7782d Mon Sep 17 00:00:00 2001 From: Lola Marrero <110120745+lola831@users.noreply.github.com> Date: Mon, 12 May 2025 13:52:56 -0700 Subject: [PATCH 26/73] Add dynamic pedestrian example and helper function; render MetaDrive 3D without added terrain or physics (#343) --- examples/driving/pedestrianJaywalking.scenic | 45 ++++++++++++++++++++ src/scenic/domains/driving/model.scenic | 12 ++++++ src/scenic/simulators/metadrive/simulator.py | 3 +- 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 examples/driving/pedestrianJaywalking.scenic diff --git a/examples/driving/pedestrianJaywalking.scenic b/examples/driving/pedestrianJaywalking.scenic new file mode 100644 index 000000000..73e232afb --- /dev/null +++ b/examples/driving/pedestrianJaywalking.scenic @@ -0,0 +1,45 @@ +""" Scenario Description +A parked car is placed off the curb. When the ego vehicle approaches, a pedestrian steps out from in front of the parked car and crosses the road. +The ego is expected to detect the pedestrian and brake before reaching them. + +To run this file using the MetaDrive simulator: + scenic examples/driving/pedestrianJaywalking.scenic --2d --model scenic.simulators.metadrive.model --simulate +""" +param map = localPath('../../assets/maps/CARLA/Town01.xodr') +model scenic.domains.driving.model + +#CONSTANTS +PEDESTRIAN_TRIGGER_DISTANCE = 15 # Distance at which pedestrian begins to cross +BRAKE_TRIGGER_DISTANCE = 10 # Distance at which ego begins braking +EGO_TO_PARKED_CAR_MIN_DIST = 30 # Ensure ego starts far enough away +PEDESTRIAN_OFFSET = 3 # Offset for pedestrian placement ahead of parked car +PARKED_CAR_OFFSET = 1 # Offset for parked car from the curb + +#EGO BEHAVIOR: Ego drives by following lanes, but brakes if a pedestrian is close +behavior DriveAndBrakeForPedestrians(): + try: + do FollowLaneBehavior() + interrupt when withinDistanceToAnyPedestrians(self, BRAKE_TRIGGER_DISTANCE): + take SetThrottleAction(0), SetBrakeAction(1) + +#PEDESTRIAN BEHAVIOR: Pedestrian crosses road when ego is near +behavior CrossRoad(): + while distance from self to ego > PEDESTRIAN_TRIGGER_DISTANCE: + wait + take SetWalkingDirectionAction(self.heading), SetWalkingSpeedAction(1) + +#SCENE SETUP +ego = new Car with behavior DriveAndBrakeForPedestrians() + +rightCurb = ego.laneGroup.curb +spot = new OrientedPoint on visible rightCurb + +parkedCar = new Car right of spot by PARKED_CAR_OFFSET, with regionContainedIn None + +require distance from ego to parkedCar > EGO_TO_PARKED_CAR_MIN_DIST + +new Pedestrian ahead of parkedCar by PEDESTRIAN_OFFSET, + facing 90 deg relative to parkedCar, + with behavior CrossRoad() + +terminate after 30 seconds diff --git a/src/scenic/domains/driving/model.scenic b/src/scenic/domains/driving/model.scenic index 988d03072..b6d0754af 100644 --- a/src/scenic/domains/driving/model.scenic +++ b/src/scenic/domains/driving/model.scenic @@ -351,6 +351,18 @@ def withinDistanceToAnyObjs(vehicle, thresholdDistance): return True return False +def withinDistanceToAnyPedestrians(vehicle, thresholdDistance): + """Returns True if any visible pedestrian is within thresholdDistance of the given vehicle.""" + objects = simulation().objects + for obj in objects: + if obj is vehicle or not isinstance(obj, Pedestrian): + continue + if not (vehicle can see obj): + continue + if (distance from obj to front of vehicle) < thresholdDistance: + return True + return False + def withinDistanceToObjsInLane(vehicle, thresholdDistance): """ checks whether there exists any obj (1) in front of the vehicle, (2) on the same lane, (3) within thresholdDistance """ diff --git a/src/scenic/simulators/metadrive/simulator.py b/src/scenic/simulators/metadrive/simulator.py index f2e4cd50b..27dc11952 100644 --- a/src/scenic/simulators/metadrive/simulator.py +++ b/src/scenic/simulators/metadrive/simulator.py @@ -131,7 +131,8 @@ def createObjectInSimulator(self, obj): physics_world_step_size=physics_world_step_size, use_render=self.render3D, vehicle_config=vehicle_config, - use_mesh_terrain=self.render3D, + use_mesh_terrain=False, + height_scale=0.0001, log_level=logging.CRITICAL, ) ) From 44a844279f840498053b2909fe8d5bfa700d54cb Mon Sep 17 00:00:00 2001 From: Lola Marrero <110120745+lola831@users.noreply.github.com> Date: Fri, 23 May 2025 16:52:04 +0000 Subject: [PATCH 27/73] Constrain pyglet to >=1.5, <=1.5.26 to avoid trimesh and macOS compatibility issues (#346) * Pin pyglet to ~=1.5 to avoid incompatibility with trimesh * changed to pyglet >= 1.5, <= 1.5.26 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b7928353e..cc0403738 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ dependencies = [ "pillow >= 9.1", 'pygame >= 2.1.3.dev8, <3; python_version >= "3.11"', 'pygame ~= 2.0; python_version < "3.11"', - "pyglet >= 1.5", + "pyglet >= 1.5, <= 1.5.26", "python-fcl >= 0.7", "Rtree ~= 1.0", "rv-ltl ~= 0.1", From 5ebdf46c49a548fe00dba374b0d5d9078fb80464 Mon Sep 17 00:00:00 2001 From: Lola Marrero <110120745+lola831@users.noreply.github.com> Date: Mon, 2 Jun 2025 22:28:03 +0000 Subject: [PATCH 28/73] Run tests weekly + Slack notification (#347) * weekly ci tests * Finish weekly ci tests workflow --- .github/workflows/weekly-ci-tests.yml | 47 +++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/weekly-ci-tests.yml diff --git a/.github/workflows/weekly-ci-tests.yml b/.github/workflows/weekly-ci-tests.yml new file mode 100644 index 000000000..da15e1f93 --- /dev/null +++ b/.github/workflows/weekly-ci-tests.yml @@ -0,0 +1,47 @@ +name: Weekly CI tests + +# No permissions needed +permissions: {} + +# Trigger every Thursday at 9:15 AM Pacific Time (16:15 UTC) +on: + schedule: + - cron: '15 16 * * 4' + +jobs: + run-tests: + uses: ./.github/workflows/run-tests.yml + with: + # Use the default branch" (i.e. main) + ref: '' + + notify: + name: Notify Slack + needs: run-tests + runs-on: ubuntu-latest + if: always() + steps: + - name: Post result to Slack + uses: slackapi/slack-github-action@b0fa283ad8fea605de13dc3f449259339835fc52 + with: + webhook: ${{ secrets.SLACK_WEBHOOK_URL}} + webhook-type: incoming-webhook + payload: | + { + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Weekly CI tests* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|run #${{ github.run_number }}> finished." + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "${{ needs.run-tests.result == 'success' && '✅ All tests passed!' || '🚨 Some tests failed!' }}" + } + } + ] + } From aa67db95a30a7c6db0950f12b1ae7b1da9bba146 Mon Sep 17 00:00:00 2001 From: kay27 Date: Fri, 21 Nov 2025 13:12:56 -0800 Subject: [PATCH 29/73] pre-main merge --- examples/carla/car.scenic | 4 +- examples/carla/pedestrian.scenic | 3 +- examples/cosim/test.scenic | 18 +- examples/metsr/test.scenic | 1 + src/scenic/simulators/carla/simulator.py | 3 +- src/scenic/simulators/cosim/__init__.py | 1 + src/scenic/simulators/cosim/model.scenic | 13 + src/scenic/simulators/cosim/simulator.py | 477 ++++++++++++++++++++++- src/scenic/simulators/cosim/utils.py | 0 src/scenic/simulators/metsr/__init__.py | 2 + src/scenic/simulators/metsr/client.py | 2 +- 11 files changed, 508 insertions(+), 16 deletions(-) create mode 100644 src/scenic/simulators/cosim/__init__.py create mode 100644 src/scenic/simulators/cosim/utils.py create mode 100644 src/scenic/simulators/metsr/__init__.py diff --git a/examples/carla/car.scenic b/examples/carla/car.scenic index edf60b5a9..9d668fe4d 100644 --- a/examples/carla/car.scenic +++ b/examples/carla/car.scenic @@ -1,4 +1,6 @@ -param map = localPath('../../assets/maps/CARLA/Town01.xodr') +param map = localPath('../../../CARLA/Town01.xodr') +param carla_map = 'Town01' +param address = "10.0.0.122" model scenic.simulators.carla.model ego = new Car diff --git a/examples/carla/pedestrian.scenic b/examples/carla/pedestrian.scenic index 9559a1d04..bdcde3152 100644 --- a/examples/carla/pedestrian.scenic +++ b/examples/carla/pedestrian.scenic @@ -1,5 +1,6 @@ -param map = localPath('../../assets/maps/CARLA/Town03.xodr') +param map = localPath('../../../CARLA/Town03.xodr') param carla_map = 'Town03' +param address = "10.0.0.122" model scenic.simulators.carla.model ego = new Car diff --git a/examples/cosim/test.scenic b/examples/cosim/test.scenic index d0dfa8d48..8567d48d8 100644 --- a/examples/cosim/test.scenic +++ b/examples/cosim/test.scenic @@ -1,4 +1,18 @@ -param carla_host = "walle" - +param startTime = 0 +param map = "CARLA_TOWN5" +param map = localPath('../../../CARLA/Town05.xodr') +param carla_map = 'Town01' +param address = "10.0.0.122" +# param verbose = True model scenic.simulators.cosim.model +scenario Test(): + compose: + while True: + do GeneratePrivateTrip(-1, -1) + +scenario Main(): + compose: + foo = Test() + do foo for 500 seconds + diff --git a/examples/metsr/test.scenic b/examples/metsr/test.scenic index 58ba400db..829d2f093 100644 --- a/examples/metsr/test.scenic +++ b/examples/metsr/test.scenic @@ -12,3 +12,4 @@ scenario Main(): compose: foo = Test() do foo for 500 seconds + diff --git a/src/scenic/simulators/carla/simulator.py b/src/scenic/simulators/carla/simulator.py index 6469281d7..dccdc7e73 100644 --- a/src/scenic/simulators/carla/simulator.py +++ b/src/scenic/simulators/carla/simulator.py @@ -41,7 +41,8 @@ def __init__( ): super().__init__() verbosePrint(f"Connecting to CARLA on port {port}") - self.client = carla.Client(address, port) + self.client = carla + carla.Client(address, port) self.client.set_timeout(timeout) # limits networking operations (seconds) if carla_map is not None: try: diff --git a/src/scenic/simulators/cosim/__init__.py b/src/scenic/simulators/cosim/__init__.py new file mode 100644 index 000000000..08253344a --- /dev/null +++ b/src/scenic/simulators/cosim/__init__.py @@ -0,0 +1 @@ +from .simulator import CosimSimulator \ No newline at end of file diff --git a/src/scenic/simulators/cosim/model.scenic b/src/scenic/simulators/cosim/model.scenic index cd76bb741..f6e00aa1e 100644 --- a/src/scenic/simulators/cosim/model.scenic +++ b/src/scenic/simulators/cosim/model.scenic @@ -7,6 +7,7 @@ param carla_port = 2000 param metsr_map = "Data.properties.CARLA" param carla_map = "Town05" param timestep = 0.1 +param map_path = f"../../assets/maps/{globalParameters.carla_map}.xodr" simulator CosimSimulator( metsr_host = globalParameters.metsr_host, @@ -15,5 +16,17 @@ simulator CosimSimulator( carla_port = globalParameters.carla_port, metsr_map = globalParameters.metsr_map, carla_map = globalParameters.carla_map, + map_path = globalParameters.map_path, timestep = globalParameters.timestep, ) + + +class PrivateCar: + carla_actor_flag = False # all vehicles are instantiated assuming they are outside of carla + pass + + + +scenario GeneratePrivateTrip(origin, destination): + new PrivateCar with origin origin, with destination destination + terminate after 1 steps \ No newline at end of file diff --git a/src/scenic/simulators/cosim/simulator.py b/src/scenic/simulators/cosim/simulator.py index aab42e424..e74bb25e8 100644 --- a/src/scenic/simulators/cosim/simulator.py +++ b/src/scenic/simulators/cosim/simulator.py @@ -2,31 +2,488 @@ from scenic.simulators.metsr.simulator import METSRSimulator from scenic.simulators.carla.simulator import CarlaSimulator from scenic.core.vectors import Orientation, Vector +from scenic.syntax.veneer import verbosePrint +from scenic.simulators.metsr.client import METSRClient +import scenic.simulators.carla.utils.utils as utils + + +import pygame +import warnings +import os +import math +import numpy as np +from scenic.core.simulators import SimulationCreationError + + +import scenic.simulators.carla.utils.visuals as visuals +from scenic.simulators.carla.blueprints import oldBlueprintNames +from shapely.geometry import Point + + + +try: + import carla +except ImportError as e: + raise ModuleNotFoundError('CARLA scenarios require the "carla" Python package') from e + +# def initialize_Carla(map_name=None, map_path=None, address="127.0.0.1",port=2000,timeout=10): +# verbosePrint(f"Connecting to CARLA on port {port}") +# client = carla.Client(address,port) + class CosimSimulator(Simulator): - def __init__(self, map_name, carla_host, carla_port, metsr_host, metsr_port, timestep): + def __init__(self, + metsr_map, + carla_map, + map_path, + bubble_size = 5, # random number should constitute the radius out from the ego object + carla_host="127.0.0.1", + carla_port=2000, + metsr_host="localhost", # Not sure what this actually means here + metsr_port=4000, + # timestep=1,# Not entirely sure what the distinction between timestep and sim_timestep is in metsr + timestep=0.1, + traffic_manager_port=None, + timeout=10, + verbose=False + ): super().__init__() breakpoint() - self.map_name = map_name + self.carla_map_name = carla_map + self.metsr_map_name = metsr_map self.timestep = timestep - self.sim_timestep = sim_timestep + self.sim_timestep = timestep + self.map_path = map_path + self.bubble_size = bubble_size - self.carla_sim = CarlaSimulator() - self.metsr_sim = METSRSimulator() + # Setting up the Carla Simulator + verbosePrint(f"Connection to CARLA on port {carla_port}") + self.carla_client = carla + carla.Client(carla_host,carla_port) + self.carla_client.set_timeout(timeout) + if carla_map is not None: + try: + self.world = self.carla_client.load_world(carla_map) + except Exception as e: + raise RuntimeError(f"CARLA could not load world '{carla_map}'") from e + else: + if str(map_path).endswith(".xodr"): + with open(map_path) as odr_file: + self.world = self.carla_client.generate_opendrive_world(odr_file.read()) + else: + raise RuntimeError("CARLA only supports OpenDrive maps") + self.timestep = timestep - def createSimulation(self, scene, timestep, **kwargs): - assert timestep is None or timestep == self.timestep + if traffic_manager_port is None: + traffic_manager_port = carla_port + 6000 + assert traffic_manager_port != metsr_port, f"Specified Traffic manager port {traffic_manager_port} is not available" + self.tm = self.carla_client.get_trafficmanager(traffic_manager_port) + self.tm.set_synchronous_mode(True) + settings = self.world.get_settings() + settings.synchronous_mode = True + assert timestep <= .1 , f"timestep must be less that 0.1" + settings.fixed_delta_seconds = timestep + self.world.apply_settings(settings) + verbosePrint("Map loaded in simulator.") + + # self.render = render + # self.record = record + # self.scenario_numver = 0 + verbosePrint("Carla was initialized correctly proceeding to Metsr") + breakpoint() + + # Setting up Metsr simulator + + self.metsr_client = METSRClient(host=metsr_host, port=metsr_port, verbose=verbose) + + verbosePrint("Clients have successfully been initialized") + + def createSimulation(self,scene,sim_timestep, **kwargs): + assert sim_timestep is not None return CosimSimulation( - scene, self.timestep, self.carla_sim, self.metsr_sim, **kwargs + scene=scene, + carla_client=self.carla_client, + metsr_client=self.metsr_client, + sim_timestep=self.sim_timestep, + tm=self.tm, + bubble=self.bubble_size ) - def destroy(self): self.carla_sim.destroy() self.metsr_sim.destroy() super().destroy() class CosimSimulation(Simulation): - pass \ No newline at end of file + def __init__(self, scene, carla_client, metsr_client, sim_timestep, tm, bubble_size, **kwargs ): + + # Carla and metrs simulators + self.carla_client = carla_client + self.metsr_client = metsr_client + self.sim_timestep = sim_timestep + + # Initializing CARLA params + self.tm = tm # Carla Traffic manager + self.carla_world = self.carla_client.get_world() + self.map = self.carla_world.get_map() + self.blueprintLib = self.carla_world.get_blueprint_library() + self.carla_cameraManager = None + + # Initializing METSR params + self.next_pv_id = 0 + self.pv_id_map = {} + self.frozen_vehicles = set() + + self._client_calls = [] + self.count = 0 + + self.bubble = bubble_size + + super().__init__(scene, timestep=sim_timestep, **kwargs) + + + + def setup(self): + self.metsr_client.reset("Data.properties.CARLA") # Not entirley sure what this is for + + weather = self.scene.params.get("weather") + if weather is not None: + if isinstance(weather, str): + self.carla_world.set_weather(getattr(carla.WeatherParameters, weather)) + elif isinstance(weather, dict): + self.carla_world.set_weather(carla.WeatherParameters(**weather)) + + # Setup HUD + if self.render: + self.displayDim = (1280, 720) + self.displayClock = pygame.time.Clock() + self.camTransform = 0 + pygame.init() + pygame.font.init() + self.hud = visuals.HUD(*self.displayDim) + self.display = pygame.display.set_mode( + self.displayDim, pygame.HWSURFACE | pygame.DOUBLEBUF + ) + self.cameraManager = None + + if self.record: + if not os.path.exists(self.record): + os.mkdir(self.record) + name = "{}/scenario{}.log".format(self.record, self.scenario_number) + # Carla is looking for an absolute path, so convert it if necessary. + name = os.path.abspath(name) + self.carla_client.start_recorder(name) + + # Create objects. + super().setup() + + # Set up camera manager and collision sensor for ego + if self.render: + camIndex = 0 + camPosIndex = 0 + egoActor = self.objects[0].carlaActor + self.cameraManager = visuals.CameraManager(self.carla.world, egoActor, self.hud) + self.cameraManager._transform_index = camPosIndex + self.cameraManager.set_sensor(camIndex) + self.cameraManager.set_transform(self.camTransform) + + self.carla_world.tick() ## allowing manualgearshift to take effect # TODO still need this? + + self.carla_actors = self.get_carla_actors() # initialize carla actors + + for obj in self.carla_actors: + obj.carla_actor_flag = True + if isinstance(obj.carlaActor, carla.Vehicle): + obj.carlaActor.apply_control( + carla.VehicleControl(manual_gear_shift=False) + ) + + self.carla_world.tick() + + for obj in self.carla_objects: + if obj.speed is not None and obj.speed != 0: + raise RuntimeError( + f"object {obj} cannot have a nonzero initial speed " + "(this is not yet possible in CARLA)" + ) + + # Create a polygon buffer around the ego object + self.objects[0].bubble = Point(self.objects[0].x, self.objects[1].y).buffer(self.bubble_size) + + + def createObjectInMetsr(self, obj): + assert obj.origin, "Metsr objects must have a defined origin" + assert obj.destination, "Metsr objects must have a defined destination" + + call_kwargs = { + "vehID": self.getPrivateVehId(obj), + "origin": obj.origin, + "destination": obj.destination, + } + + self.metsr_client.generate_trip(**call_kwargs) + + + def createObjectInCarla(self,obj): + try: + blueprint = self.blueprintLib.find(obj.blueprint) + except IndexError as e: + found = False + if obj.blueprint in oldBlueprintNames: + for oldName in oldBlueprintNames[obj.blueprint]: + try: + blueprint = self.blueprintLib.find(oldName) + found = True + warnings.warn( + f"CARLA blueprint {obj.blueprint} not found;" + f"using older version {oldName}" + ) + obj.blueprint = oldName + break + except IndexError: + continue + if not found: + raise SimulationCreationError( + f"Unable to find blueprint {obj.blueprint}" f" for object {obj}" + ) from e + if obj.rolename is not None: + blueprint.set_attribute("role_name", obj.rolename) + + # set walker as not invincible + if blueprint.has_attribute("is_invincible"): + blueprint.set_attribute("is_invincible", "False") + + loc = utils.scenicToCarlaLocation( + obj.postion, + world=self.carla_world, + blueprint=obj.blueprint, + snapToGround=obj.snapToGround, + ) + rot = utils.scenicToCarlaRotation(obj.orientation) + transform = carla.Transform(loc,rot) + + if blueprint.has_attribute("color") and obj.color is not None: + c = obj.color + c_str = f"{int(c.r*255), {int(c.g*255)},{int(c.b*255)}}" + blueprint.set_attribute("color", c_str) + + carlaActor = self.carla_world.try_spawn_actor(blueprint,transform) + if carlaActor is None: + raise SimulationCreationError(f"Unable to spawn object {obj}") + obj.carlaActor = carlaActor + + carlaActor.set_simulate_physics(obj.physics) + + if isinstance(carlaActor, carla.Vehicle): + extent = carlaActor.bounding_box.extent + ex,ey,ez = extent.x, extent.y, extent.z + + obj.width = ey * 2 if ey > 0 else obj.width + obj.lenth = ex * 2 if ex > 0 else obj.length + obj.height = ez * 2 if ez > 0 else obj.height + carlaActor.apply_control(carla.VehicleControl(manual_gear_shift=True, gear=1)) + + elif isinstance(carlaActor, carla.Walker): + carlaActor.apply_control(carla.WalkerControl()) + #spawn walker controller + controller_bp = self.blueprintLib.find("controller.ai.walker") + controller = self.carla_world.try_spawn_actor( + controller_bp, carla.Transform(), carlaActor + ) + if controller is None: + raise SimulationCreationError( + f"Unable to spawn carla controller for object {obj}" + ) + obj.carlaController = controller + return carlaActor + + + def getCarlaProperties(self, obj, properties): + # Extract Carla properties + carlaActor = obj.carlaActor + currTransform = carlaActor.get_transform() + currLoc = currTransform.location + currRot = currTransform.rotation + currVel = carlaActor.get_velocity() + currAngVel = carlaActor.get_angular_velocity() + + # Prepare Scenic object properties + position = utils.carlaToScenicPosition(currLoc) + velocity = utils.carlaToScenicPosition(currVel) + speed = math.hypot(*velocity) + angularSpeed = utils.carlaToScenicAngularSpeed(currAngVel) + angularVelocity = utils.carlaToScenicAngularVel(currAngVel) + globalOrientation = utils.carlaToScenicOrientation(currRot) + yaw, pitch, roll = obj.parentOrientation.localAnglesFor(globalOrientation) + elevation = utils.carlaToScenicElevation(currLoc) + + values = dict( + position=position, + velocity=velocity, + speed=speed, + angularSpeed=angularSpeed, + angularVelocity=angularVelocity, + yaw=yaw, + pitch=pitch, + roll=roll, + elevation=elevation, + ) + return values + + + def getMetsrProperties(self, obj, properties): + if obj in self.frozen_vehicles: + return None + + raw_data = self.obj_data_cache[obj] + + if "road" not in raw_data and raw_data["state"] <=0: + self.frozen_vehicles.add(obj) + + position = Vector(raw_data["x"], raw_data["y"], 0) + speed = raw_data["speed"] + bearing = math.radians(raw_data["bearing"]) + globalOrientation = Orientation.fromEuler(bearing,0,0) + yaw, pitch, roll = obj.parentOrientation.localAnglesFor(globalOrientation) + velocity = Vector(0, speed, 0).rotatedBy(yaw) + angularSpeed = 0 + angularVelocity = Vector(0,0,0) + + values = dict( + position=position, + velocity=velocity, + speed=speed, + angularSpeed=angularSpeed, + angularVelocity=angularVelocity, + yaw=yaw, + pitch=pitch, + roll=roll + ) + return values + + def getProperties(self, obj, properties): + assert hasattr(obj, carla_actor_flag), f"Object is not assigned properly to a simulator instance" + if obj.carla_actor_flag: + return self.getCarlaProperties(obj) + else: + return self.getMetsrProperties(obj) + + + def getMetsrPrivateVehId(self,obj): + if obj not in self.pv_id_map: + self.pv_id_map[obj] = self.next_pv_id + self.next_pv_id += 1 + return self.pv_id_map[obj] + + + def carla_step(self): + """ + Applying step operations to be seperate for each simulator to allow + for more complex interations (differing frequencies) later on + """ + self.carla_world.tick() + + if self.render: + self.cameraManager.render(self.display) + pygame.display.flip() + + + def metsr_step(self): + """ + Tick the metsr simulation a single timestep + """ + self.count += 1 + if self.count % 100 == 0: + print(".", end="", flush=True) + self.metsr_client.tick() + + + def step(self): + """ + Step both simulators -> Update ego bubble + Update actor locations in either bubble + """ + self.carla_client.tick() + self.metsr_client.tick() + # recreating a new bubble each time? Likely a better way to do this + # Instead of using Shapely here -- use Scenic circle class + # Easier to use Scenic class (already have built in intersection stuff) + self.objects[0].bubble = Point(self.objects[0].x, self.objects[1].y).buffer(self.bubble_size) + # Circle objects region for bubble -- "ground truth" + # Check lanes in 'bubble' + # find the cars in the lanes -- or if the car is the closest lane + + + # lane =self.objects._lane + # if lane: + # min() -- + + + # query new actors that may have entered the bubble + new_actors = self.get_carla_actors() + # Check for new objs to initialize in Carla + for new_actor in new_actors: + if new_actor not in self.carla_actors: + self.createObjectInCarla(new_actor) + new_actor.carla_actor_flag = True + # Remove objects that should no longer be in Carla + for old_actor in self.carla_actors: + if old_actor not in new_actors: + self.destroy_carla_obj(old_actor) + old_actor.carla_actor_flag = False + # release the road segment that the old actors was in originally + # This could potentially delete a segment that SHOULD still be contained + # This is OK because we will update with the relevant actors before ticking again + self.metsr_client.release_cosim_road(self.query_road(old_actor)) + + + self.carla_actors = new_actors + self.query_road() #update the road segments which Metsr controls before ticking metsr + + + + def get_carla_actors(self): + """ + Helper function to compute which objects belong in Carla + """ + bubble = self.objects[0].bubble # Bubble around the ego + actors = [self.objects[0]] + for obj in self.objects[1:]: # check all objects in the sim + mesh = self.objects[0].mesh # grab obj mesh + boundary_points = mesh.vertices[mesh.vertices_on_boundary] # grab boundary points + if np.any([bubble.touches(Point(p[:2])) for p in boundary_points]): #Check if any points are contained in buffer + actors.append(obj) # add to the list of current actors + return actors + + + def query_road(self): + """ + Query carla actors for relevent road segments which metsr will not control + """ + for actor in self.carla_actors: + road_id = self.get_mapping(actor.position)# placeholder idek how to do this + self.metsr_client.set_cosim_road(road_id) + + + def destroy_carla_obj(self,obj): + """ + Special destroy method for updating high-fidelity bubble + """ + if obj.carlaActor is not None: + if isinstance(obj.carlaActor, carla.Vehicle): + obj.carlaActor.set_autopilot(False, self.tm.get_port()) + if isinstance(obj.carlaActor, carla.Walker): + obj.cralaController.stop() + obj.carlaController.destroy() + obj.carlaActor.destroy() + + + def get_mapping(self,obj): + """ + TODO: Map an objects position in xodr world to the metsr equivalent road ID + """ + return 0 + + \ No newline at end of file diff --git a/src/scenic/simulators/cosim/utils.py b/src/scenic/simulators/cosim/utils.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/scenic/simulators/metsr/__init__.py b/src/scenic/simulators/metsr/__init__.py new file mode 100644 index 000000000..0d415299b --- /dev/null +++ b/src/scenic/simulators/metsr/__init__.py @@ -0,0 +1,2 @@ +from .simulator import METSRSimulator + \ No newline at end of file diff --git a/src/scenic/simulators/metsr/client.py b/src/scenic/simulators/metsr/client.py index a3a943f09..33d70cd34 100644 --- a/src/scenic/simulators/metsr/client.py +++ b/src/scenic/simulators/metsr/client.py @@ -281,7 +281,7 @@ def generate_trip_between_roads(self, vehID, origin, destination): # set the road for co-simulation - def set_cosim_road(self, roadID): + def set_cosim_road(self, roadID): # IF a road or METSR road ID is here, the MetsR DOES NOT control it msg = { "TYPE": "CTRL_setCoSimRoad", "DATA": [] From 8a2abb288e009a60f3be3d29f603cc5827a8be64 Mon Sep 17 00:00:00 2001 From: Kay Date: Tue, 3 Feb 2026 14:11:44 -0800 Subject: [PATCH 30/73] fixed typo in 'release_cosim_road' --- src/scenic/simulators/metsr/client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/scenic/simulators/metsr/client.py b/src/scenic/simulators/metsr/client.py index 33d70cd34..2bde3ce32 100644 --- a/src/scenic/simulators/metsr/client.py +++ b/src/scenic/simulators/metsr/client.py @@ -134,6 +134,7 @@ def tick(self, step_num = 1, wait_forever = False): # QUERY: inspect the state of the simulator # By default query public vehicles def query_vehicle(self, id = None, private_veh = False, transform_coords = False): + # transform coords arguments denotes transformation True: METSR -> SUMO , False just METSR msg = {"TYPE": "QUERY_vehicle"} if id is not None: msg["DATA"] = [] @@ -298,7 +299,7 @@ def set_cosim_road(self, roadID): # IF a road or METSR road ID is here, the Mets # release the road for co-simulation def release_cosim_road(self, roadID): msg = { - "TYPE": "CTRL_releaseCoSimRoad", + "TYPE": "CTRL_releaseCosimRoad", "DATA": [] } if not isinstance(roadID, list): @@ -306,7 +307,7 @@ def release_cosim_road(self, roadID): for i in roadID: msg['DATA'].append(i) res = self.send_receive_msg(msg, ignore_heartbeats=True) - assert res["TYPE"] == "CTRL_releaseCoSimRoad", res["TYPE"] + assert res["TYPE"] == "CTRL_releaseCosimRoad", res["TYPE"] assert res["CODE"] == "OK", res["CODE"] return res From 4ce467b4e0d0f0fa394fafbb8c20c83ac5cd0ab2 Mon Sep 17 00:00:00 2001 From: Kay Date: Thu, 5 Feb 2026 10:37:42 -0800 Subject: [PATCH 31/73] generate lane mappings --- src/scenic/simulators/cosim/utils/utils.py | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 src/scenic/simulators/cosim/utils/utils.py diff --git a/src/scenic/simulators/cosim/utils/utils.py b/src/scenic/simulators/cosim/utils/utils.py new file mode 100644 index 000000000..63d29aaa5 --- /dev/null +++ b/src/scenic/simulators/cosim/utils/utils.py @@ -0,0 +1,73 @@ +import xml.etree.ElementTree as ET +import os +import numpy as np + +def generate_map(map): + try: + tree = ET.parse(map) + except FileNotFoundError: + print(f"Could not find map: {map} from {os.getcwd()}") + return {} + + root = tree.getroot() + mappings = {} + edges = root.iterfind("edge") + + for edge in edges: + lanes = edge.findall('lane') + + for lane in lanes: + metrs_lane = lane.attrib.get("id") + params = lane.findall('param') + + for param in params: + if param.get('key') == "origId": + orig_id = param.get('value') + orig_id = orig_id.split() + if isinstance(orig_id, list): + for id in orig_id: + mappings[id] = metrs_lane + else: + mappings[orig_id] = metrs_lane + + if mappings == {}: + print(f"An occured attempting to process map: {map}") + + return mappings + +def test_mapping(map, test_pairs): + mappings = generate_map(map) + for key,value in test_pairs.items(): + if key in mappings: + if value == mappings[key]: + print(f"key value pair {key,value} mapped correctly") + else: + print(f"expected {value} returned {mappings[key]}") + print(f"Value {mappings[key]} for key {key} was incorrect with actual value {value}") + else: + print(f"Failed on test case {key}, {value}") + + +def within_threshold_to(ego, cars, threshold) -> bool: + ego_pos = np.array(ego.position) + for car in cars: + if np.linalg.norm(np.array(car.position) - ego_pos) < threshold: + return True + return False + + +if __name__ == "__main__": + + map = "Town01.net.xml" + + # key value pairs where key == origID and value == lane id + town01_test_pairs = {"4_1": "4_2", "0_2 11_-2 8_2": "0_1", "8_-3 11_3 0_-3": "-8_0" } + + test_mapping(map, town01_test_pairs) + + map = "Town02.net.xml" + + # Randomly selected test cases from the file to check accuracy + town02_test_pairs = {"177_-1": ":132_3_0", "276_-3":":242_2_0", "1_-2 16_-2 12_2 3_-2 15_2": "-1_1"} + + test_mapping(map, town02_test_pairs) \ No newline at end of file From f6c4b4d275dcf8ea528e4ff2081b83fffa8a08e9 Mon Sep 17 00:00:00 2001 From: Kay Date: Thu, 5 Feb 2026 10:39:41 -0800 Subject: [PATCH 32/73] cosim model --- src/scenic/simulators/cosim/model.scenic | 64 +++++++++++++++++++----- 1 file changed, 51 insertions(+), 13 deletions(-) diff --git a/src/scenic/simulators/cosim/model.scenic b/src/scenic/simulators/cosim/model.scenic index f6e00aa1e..f75974d97 100644 --- a/src/scenic/simulators/cosim/model.scenic +++ b/src/scenic/simulators/cosim/model.scenic @@ -1,32 +1,70 @@ -from scenic.simulators.cosim.simulator import CosimSimulator +import pathlib +from scenic.simulators.carla.model import Vehicle, is2DMode + +import scenic.simulators.carla.blueprints as blueprints +from scenic.simulators.carla.behaviors import * +from scenic.simulators.utils.colors import Color + +from scenic.simulators.cosim.simulator import CosimSimulator +map_town = pathlib.Path(globalParameters.map).stem +param xml_path = pathlib.Path(globalParameters.xml_map) +param carla_map = map_town param metsr_host = "localhost" param metsr_port = 4000 -param carla_host = "localhost" +param address = "10.0.0.122" param carla_port = 2000 param metsr_map = "Data.properties.CARLA" -param carla_map = "Town05" param timestep = 0.1 -param map_path = f"../../assets/maps/{globalParameters.carla_map}.xodr" +param snapToGroundDefault = is2DMode() + simulator CosimSimulator( metsr_host = globalParameters.metsr_host, metsr_port = globalParameters.metsr_port, - carla_host = globalParameters.carla_host, + address = globalParameters.address, carla_port = globalParameters.carla_port, metsr_map = globalParameters.metsr_map, - carla_map = globalParameters.carla_map, - map_path = globalParameters.map_path, + carla_map = map_town, + xml_map = globalParameters.xml_path, + map_path = globalParameters.map, timestep = globalParameters.timestep, + bubble_size = 50 ) -class PrivateCar: - carla_actor_flag = False # all vehicles are instantiated assuming they are outside of carla - pass +class Car(Vehicle): + """A car. + + The default ``blueprint`` (see `CarlaActor`) is a uniform distribution over the + blueprints listed in :obj:`scenic.simulators.carla.blueprints.carModels`. + """ + blueprint: Uniform(*blueprints.carModels) + destination: -1 + origin: -1 + + @property + def isCar(self): + return True + + +class EgoCar(Car): + """ + Special class for Ego + """ + carla_actor_flag: True + + + +class NPCCar(Car): + """ + An NPC car + """ + carla_actor_flag: False + # Default Carla Behavior + behavior: DriveAvoidingCollisions(target_speed=10, avoidance_threshold=12) + + -scenario GeneratePrivateTrip(origin, destination): - new PrivateCar with origin origin, with destination destination - terminate after 1 steps \ No newline at end of file From c3ceeef9e411c82298c27709fb571efea447af00 Mon Sep 17 00:00:00 2001 From: Kay Date: Thu, 5 Feb 2026 10:41:02 -0800 Subject: [PATCH 33/73] carla client fix --- src/scenic/simulators/carla/simulator.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/scenic/simulators/carla/simulator.py b/src/scenic/simulators/carla/simulator.py index dccdc7e73..6469281d7 100644 --- a/src/scenic/simulators/carla/simulator.py +++ b/src/scenic/simulators/carla/simulator.py @@ -41,8 +41,7 @@ def __init__( ): super().__init__() verbosePrint(f"Connecting to CARLA on port {port}") - self.client = carla - carla.Client(address, port) + self.client = carla.Client(address, port) self.client.set_timeout(timeout) # limits networking operations (seconds) if carla_map is not None: try: From ae798e4977191fcc75a8e6f45c3e1e58cb38b361 Mon Sep 17 00:00:00 2001 From: Kay Date: Thu, 5 Feb 2026 10:44:00 -0800 Subject: [PATCH 34/73] init --- examples/cosim/test.scenic | 24 +- src/scenic/simulators/cosim/simulator.py | 619 ++++++++++++++++++----- 2 files changed, 492 insertions(+), 151 deletions(-) diff --git a/examples/cosim/test.scenic b/examples/cosim/test.scenic index 8567d48d8..46ef44692 100644 --- a/examples/cosim/test.scenic +++ b/examples/cosim/test.scenic @@ -1,18 +1,16 @@ -param startTime = 0 -param map = "CARLA_TOWN5" -param map = localPath('../../../CARLA/Town05.xodr') -param carla_map = 'Town01' -param address = "10.0.0.122" +# param startTime = 0 +param map = localPath('../../assets/maps/CARLA/Town05.xodr') # OpenDrive file +param xml_map = localPath("../../assets/maps/CARLA/Town05.net.xml") # Sumo file +param address = "10.45.20.114" +# param address = "10.0.0.122" # param verbose = True model scenic.simulators.cosim.model -scenario Test(): - compose: - while True: - do GeneratePrivateTrip(-1, -1) +ego = new EgoCar with behavior DriveAvoidingCollisions(target_speed=15, avoidance_threshold=12), with name "ego", with position Vector(88.80499658384105, 1.5729413763422928, 0) -scenario Main(): - compose: - foo = Test() - do foo for 500 seconds +for i in range(10): + title = f"npccar_{i}" # allow me to debug more easily + new NPCCar with name title + +terminate after 500 steps \ No newline at end of file diff --git a/src/scenic/simulators/cosim/simulator.py b/src/scenic/simulators/cosim/simulator.py index e74bb25e8..3b4cf8208 100644 --- a/src/scenic/simulators/cosim/simulator.py +++ b/src/scenic/simulators/cosim/simulator.py @@ -1,10 +1,12 @@ from scenic.core.simulators import Simulation, Simulator -from scenic.simulators.metsr.simulator import METSRSimulator -from scenic.simulators.carla.simulator import CarlaSimulator from scenic.core.vectors import Orientation, Vector from scenic.syntax.veneer import verbosePrint from scenic.simulators.metsr.client import METSRClient import scenic.simulators.carla.utils.utils as utils +from scenic.simulators.cosim.utils.utils import * +from scenic.core.regions import CircularRegion + +from scenic.domains.driving.roads import Lane, Intersection import pygame @@ -13,12 +15,16 @@ import math import numpy as np from scenic.core.simulators import SimulationCreationError +import scenic.simulators.cosim.utils.utils as _utils + +from scenic.domains.driving.simulators import DrivingSimulation, DrivingSimulator +from scenic.core.distributions import Uniform import scenic.simulators.carla.utils.visuals as visuals from scenic.simulators.carla.blueprints import oldBlueprintNames from shapely.geometry import Point - +import re try: @@ -31,13 +37,14 @@ # client = carla.Client(address,port) -class CosimSimulator(Simulator): +class CosimSimulator(DrivingSimulator): def __init__(self, metsr_map, carla_map, map_path, - bubble_size = 5, # random number should constitute the radius out from the ego object - carla_host="127.0.0.1", + xml_map, + bubble_size = 50, # Might be good to add some logic for what a minimal bubble size is so users cannot make it too small + address="127.0.0.1", carla_port=2000, metsr_host="localhost", # Not sure what this actually means here metsr_port=4000, @@ -45,37 +52,42 @@ def __init__(self, timestep=0.1, traffic_manager_port=None, timeout=10, - verbose=False + verbose=False, + render=True, + record="" ): super().__init__() - breakpoint() - self.carla_map_name = carla_map self.metsr_map_name = metsr_map self.timestep = timestep self.sim_timestep = timestep self.map_path = map_path self.bubble_size = bubble_size + self.render= render + self.record = record # Setting up the Carla Simulator verbosePrint(f"Connection to CARLA on port {carla_port}") - self.carla_client = carla - carla.Client(carla_host,carla_port) + self.carla_client = carla.Client(address,carla_port) self.carla_client.set_timeout(timeout) + """ + Need to figure out how to handle the map paths for this + """ if carla_map is not None: try: self.world = self.carla_client.load_world(carla_map) + self.xml_to_xodr_map = _utils.generate_map(str(xml_map)) #covert pathlib obj to str for XML tree TODO what is best practice? except Exception as e: raise RuntimeError(f"CARLA could not load world '{carla_map}'") from e else: + #TODO figure out how to properly do the map handling here if str(map_path).endswith(".xodr"): with open(map_path) as odr_file: self.world = self.carla_client.generate_opendrive_world(odr_file.read()) else: raise RuntimeError("CARLA only supports OpenDrive maps") self.timestep = timestep - if traffic_manager_port is None: traffic_manager_port = carla_port + 6000 assert traffic_manager_port != metsr_port, f"Specified Traffic manager port {traffic_manager_port} is not available" @@ -93,7 +105,6 @@ def __init__(self, # self.record = record # self.scenario_numver = 0 verbosePrint("Carla was initialized correctly proceeding to Metsr") - breakpoint() # Setting up Metsr simulator @@ -101,23 +112,35 @@ def __init__(self, verbosePrint("Clients have successfully been initialized") - def createSimulation(self,scene,sim_timestep, **kwargs): - assert sim_timestep is not None + def createSimulation(self,scene,*, timestep, **kwargs): #TODO: fix timestep + if timestep is not None and timestep != self.timestep: + raise RuntimeError( + "cannot customize timestep for individual CARLA simulations; " + "set timestep when creating the CarlaSimulator instead" + ) return CosimSimulation( scene=scene, carla_client=self.carla_client, metsr_client=self.metsr_client, sim_timestep=self.sim_timestep, tm=self.tm, - bubble=self.bubble_size + bubble_size=self.bubble_size, + render=self.render, + record=self.record, + mappings=self.xml_to_xodr_map, + **kwargs, ) def destroy(self): - self.carla_sim.destroy() - self.metsr_sim.destroy() + self.metsr_client.close() super().destroy() + settings = self.world.get_settings() + settings.synchronous_mode = False + settings.fixed_delta_seconds = None + self.world.apply_settings(settings) + self.tm.set_synchronous_mode(False) -class CosimSimulation(Simulation): - def __init__(self, scene, carla_client, metsr_client, sim_timestep, tm, bubble_size, **kwargs ): +class CosimSimulation(DrivingSimulation): + def __init__(self, scene, carla_client, metsr_client, sim_timestep, tm,render,record,mappings, bubble_size=100, **kwargs ): # Carla and metrs simulators self.carla_client = carla_client @@ -130,22 +153,33 @@ def __init__(self, scene, carla_client, metsr_client, sim_timestep, tm, bubble_s self.map = self.carla_world.get_map() self.blueprintLib = self.carla_world.get_blueprint_library() self.carla_cameraManager = None + self.render = render + self.record = record # Initializing METSR params self.next_pv_id = 0 self.pv_id_map = {} self.frozen_vehicles = set() + self.xodr_to_xml_map = mappings self._client_calls = [] self.count = 0 - self.bubble = bubble_size + # CoSim related params + self.bubble_size = bubble_size + self.workspace = scene.workspace + self.carla_control_lanes = {} + self.queued_vehicles = {} super().__init__(scene, timestep=sim_timestep, **kwargs) - def setup(self): + def setup(self) -> None: + """ + Setup the simulation instance + Set initial simulator instance for each object + """ self.metsr_client.reset("Data.properties.CARLA") # Not entirley sure what this is for weather = self.scene.params.get("weather") @@ -179,46 +213,59 @@ def setup(self): # Create objects. super().setup() + #TEMP FIX? IDK... + for object in self.objects[1:]: + object.carla_actor_flag = False + object.spawn_guard = 0 + self.objects[0].carla_actor_flag = True + self.objects[0].spawn_guard = 0 + + # print(f"bubble_size {self.bubble_size}") + self.objects[0].bubble = CircularRegion(center=[self.objects[0].x, + self.objects[0].y], + radius=self.bubble_size) + + for obj in self.objects: + if isinstance(obj.carlaActor, carla.Vehicle): + obj.carlaActor.apply_control( + carla.VehicleControl(manual_gear_shift=False) + ) + self.carla_world.tick() + # Set up camera manager and collision sensor for ego if self.render: camIndex = 0 camPosIndex = 0 egoActor = self.objects[0].carlaActor - self.cameraManager = visuals.CameraManager(self.carla.world, egoActor, self.hud) + self.cameraManager = visuals.CameraManager(self.carla_world, egoActor, self.hud) self.cameraManager._transform_index = camPosIndex self.cameraManager.set_sensor(camIndex) self.cameraManager.set_transform(self.camTransform) self.carla_world.tick() ## allowing manualgearshift to take effect # TODO still need this? - self.carla_actors = self.get_carla_actors() # initialize carla actors + for obj in self.scene.objects: + if obj.carla_actor_flag: + if obj.speed is not None and obj.speed != 0: + raise RuntimeError( + f"object {obj} cannot have a nonzero initial speed " + "(this is not yet possible in CARLA)" + ) + + self.synchronize_clients() - for obj in self.carla_actors: - obj.carla_actor_flag = True - if isinstance(obj.carlaActor, carla.Vehicle): - obj.carlaActor.apply_control( - carla.VehicleControl(manual_gear_shift=False) - ) - - self.carla_world.tick() - for obj in self.carla_objects: - if obj.speed is not None and obj.speed != 0: - raise RuntimeError( - f"object {obj} cannot have a nonzero initial speed " - "(this is not yet possible in CARLA)" - ) - - # Create a polygon buffer around the ego object - self.objects[0].bubble = Point(self.objects[0].x, self.objects[1].y).buffer(self.bubble_size) - - - def createObjectInMetsr(self, obj): + def createObjectInMetsr(self, obj) -> None: + """ + Create object in Metsr + """ assert obj.origin, "Metsr objects must have a defined origin" assert obj.destination, "Metsr objects must have a defined destination" + print(f"Creating obj: {obj} with position: {obj.position} in METSR") + call_kwargs = { - "vehID": self.getPrivateVehId(obj), + "vehID": self.getMetsrPrivateVehId(obj), "origin": obj.origin, "destination": obj.destination, } @@ -226,7 +273,9 @@ def createObjectInMetsr(self, obj): self.metsr_client.generate_trip(**call_kwargs) - def createObjectInCarla(self,obj): + def createObjectInCarla(self,obj) -> None: + # Extract blueprint + print(f"Creating obj: {obj} with position: {obj.position} in CARLA") try: blueprint = self.blueprintLib.find(obj.blueprint) except IndexError as e: @@ -237,7 +286,7 @@ def createObjectInCarla(self,obj): blueprint = self.blueprintLib.find(oldName) found = True warnings.warn( - f"CARLA blueprint {obj.blueprint} not found;" + f"CARLA blueprint {obj.blueprint} not found; " f"using older version {oldName}" ) obj.blueprint = oldName @@ -250,44 +299,57 @@ def createObjectInCarla(self,obj): ) from e if obj.rolename is not None: blueprint.set_attribute("role_name", obj.rolename) - + # set walker as not invincible if blueprint.has_attribute("is_invincible"): blueprint.set_attribute("is_invincible", "False") - + + # Set up transform loc = utils.scenicToCarlaLocation( - obj.postion, + obj.position, world=self.carla_world, blueprint=obj.blueprint, snapToGround=obj.snapToGround, ) rot = utils.scenicToCarlaRotation(obj.orientation) - transform = carla.Transform(loc,rot) + transform = carla.Transform(loc, rot) + # Color, cannot be set for Pedestrians if blueprint.has_attribute("color") and obj.color is not None: c = obj.color - c_str = f"{int(c.r*255), {int(c.g*255)},{int(c.b*255)}}" + c_str = f"{int(c.r*255)},{int(c.g*255)},{int(c.b*255)}" blueprint.set_attribute("color", c_str) - carlaActor = self.carla_world.try_spawn_actor(blueprint,transform) - if carlaActor is None: - raise SimulationCreationError(f"Unable to spawn object {obj}") + # Create Carla actor + # print(f"Spawning actor {obj} in location {loc} with original pos: {obj.position}") + try: + carlaActor = self.carla_world.spawn_actor(blueprint, transform) + except Exception as e: + print(f"Error: {e} occured \n displaying object positions") + for obj in self.objects: + car_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True) + simulator = "carla" if obj.carla_actor_flag else "metsr" + print(f"{obj} in {simulator}: [Metsr][Scenic] coords X: {car_data['DATA'][0]['x'], obj.position.x}, y:{car_data['DATA'][0]['y'], obj.position.y}") + print(f"Query Vehicle resuts for obj: :{obj}, is: {car_data['DATA']}") + # raise e(f"Error : {e} occured") + # if carlaActor is None: + # raise SimulationCreationError(f"Unable to spawn object {obj}") obj.carlaActor = carlaActor carlaActor.set_simulate_physics(obj.physics) if isinstance(carlaActor, carla.Vehicle): + # TODO should get dimensions at compile time, not simulation time extent = carlaActor.bounding_box.extent - ex,ey,ez = extent.x, extent.y, extent.z - + ex, ey, ez = extent.x, extent.y, extent.z + # Ensure each extent is positive to work around CARLA issue #5841 obj.width = ey * 2 if ey > 0 else obj.width - obj.lenth = ex * 2 if ex > 0 else obj.length + obj.length = ex * 2 if ex > 0 else obj.length obj.height = ez * 2 if ez > 0 else obj.height carlaActor.apply_control(carla.VehicleControl(manual_gear_shift=True, gear=1)) - elif isinstance(carlaActor, carla.Walker): carlaActor.apply_control(carla.WalkerControl()) - #spawn walker controller + # spawn walker controller controller_bp = self.blueprintLib.find("controller.ai.walker") controller = self.carla_world.try_spawn_actor( controller_bp, carla.Transform(), carlaActor @@ -297,10 +359,39 @@ def createObjectInCarla(self,obj): f"Unable to spawn carla controller for object {obj}" ) obj.carlaController = controller - return carlaActor + + + def createObjectInSimulator(self, obj) -> None: + """ + Create object in the corresponding simulator according to its flag + """ + assert obj.origin, "All objects must have an origin" + assert obj.destination, "All objects must have an destination" + + assert hasattr(obj, "carla_actor_flag"), "All objects must have attribute: carla_actor_flag" - def getCarlaProperties(self, obj, properties): + # try: + # vehID = self.getMetsrPrivateVehId(obj) + # veh_data = self.metsr_client.query_vehicle(vehID, True, True) + # if 'x' in veh_data[0] and 'y' in veh_data[0]: + + + if obj == self.objects[0]: + # Special handling creating EGO in both simulators + # ensures consistency for obje queue spawning in METSR + self.createObjectInCarla(obj) + self.createObjectInMetsr(obj) + elif obj.carla_actor_flag: + self.createObjectInCarla(obj) + else: + self.createObjectInMetsr(obj) + + + def getCarlaProperties(self, obj, properties) -> dict[str, float | Vector | int]: + """ + Return simulator specific properties for object in CARLA simulator + """ # Extract Carla properties carlaActor = obj.carlaActor currTransform = carlaActor.get_transform() @@ -330,10 +421,14 @@ def getCarlaProperties(self, obj, properties): roll=roll, elevation=elevation, ) + # print(f"{obj} properties: {values}") return values - def getMetsrProperties(self, obj, properties): + def getMetsrProperties(self, obj, properties) -> dict[str, float | Vector | int]: + """ + Return simulator specific properties for object in Metsr + """ if obj in self.frozen_vehicles: return None @@ -359,115 +454,263 @@ def getMetsrProperties(self, obj, properties): angularVelocity=angularVelocity, yaw=yaw, pitch=pitch, - roll=roll + roll=roll, + elevation=float(0) ) return values - def getProperties(self, obj, properties): - assert hasattr(obj, carla_actor_flag), f"Object is not assigned properly to a simulator instance" - if obj.carla_actor_flag: - return self.getCarlaProperties(obj) - else: - return self.getMetsrProperties(obj) - - - def getMetsrPrivateVehId(self,obj): + def getProperties(self, obj, properties)-> dict[str, float | Vector | int]: + """ + Return properties for any simulator object + """ + assert hasattr(obj, "carla_actor_flag"), f"Object is not assigned properly to a simulator instance" + """ TODO: Sometimes update objects fails on the first step -- Not clear why this occurs -- adding a check before accessing obj.carlaActor """ + # if obj.carla_actor_flag: + # if obj.carlaActor == None: # TODO added check here to ensure actor is created before polling simulator + # print(f"Something strange has occured for obj {obj} at step: {self.count}") + # properties = self.getCarlaProperties(obj,properties) + # else: + properties = self.getMetsrProperties(obj,properties) + return properties + + def getMetsrPrivateVehId(self,obj) -> int: + """ + Return unique vehicle idea + Generates a new ID if none exists for vehicle + """ if obj not in self.pv_id_map: self.pv_id_map[obj] = self.next_pv_id self.next_pv_id += 1 return self.pv_id_map[obj] - def carla_step(self): + def step(self) -> None: """ - Applying step operations to be seperate for each simulator to allow - for more complex interations (differing frequencies) later on + Step both simulators -> Update ego bubble + Update actor locations in either bubble + + TODO : Verify fixed! - Seems like carla needs to be stepped after creating a new object + TODO : Verify fixed! - If a NON-ego car spawns in the bubble (@ step 0) getproperties fails (carlaActor == None) + TODO : METSR vrs CARLA deviation METSR queue spawning occupied CARLA position + """ - self.carla_world.tick() + + lanes, new_lanes, old_lanes = self.update_carla_lanes() + self.release_lanes(old_lanes) + self.freeze_lanes(new_lanes) + + # Update state of simulator with new bubble region + self.update_bubble_objects(lanes,new_lanes) + + #Carla step + self.carla_world.tick() + self.synchronize_clients() + # Need to ensure carla is ticked a + for obj in self.objects: + if obj.spawn_guard > 0: + obj.spawn_guard -= 1 if self.render: self.cameraManager.render(self.display) pygame.display.flip() - - - def metsr_step(self): - """ - Tick the metsr simulation a single timestep - """ + + # Metsr step self.count += 1 - if self.count % 100 == 0: + if self.count % 100 == 0: # just removing for now print(".", end="", flush=True) self.metsr_client.tick() + # Generate bubble region based on ego objects + # TODO update logic for multiple high-fidelity zones + self.objects[0].bubble = CircularRegion(center=[self.objects[0].x, + self.objects[0].y], + radius=self.bubble_size) - def step(self): - """ - Step both simulators -> Update ego bubble - Update actor locations in either bubble - """ - self.carla_client.tick() - self.metsr_client.tick() - # recreating a new bubble each time? Likely a better way to do this - # Instead of using Shapely here -- use Scenic circle class - # Easier to use Scenic class (already have built in intersection stuff) - self.objects[0].bubble = Point(self.objects[0].x, self.objects[1].y).buffer(self.bubble_size) - # Circle objects region for bubble -- "ground truth" - # Check lanes in 'bubble' - # find the cars in the lanes -- or if the car is the closest lane + self.synchronize_clients() + # for obj in self.objects: + # if obj.carla_actor_flag: + # loc = obj.carlaActor.get_location() + # car_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True) + # if not math.isclose(car_data['DATA'][0]['x'], loc.x) or not math.isclose(car_data['DATA'][0]['y'],-loc.y): + # print(f"{car_data['DATA'][0]['x'], loc.x} -- {car_data['DATA'][0]['y'], -loc.y}") - # lane =self.objects._lane - # if lane: - # min() -- + # Set intersections + intersections = self.get_carla_intersections() + if intersections != []: + self.synchronize_signals(intersections) + - # query new actors that may have entered the bubble - new_actors = self.get_carla_actors() - # Check for new objs to initialize in Carla - for new_actor in new_actors: - if new_actor not in self.carla_actors: - self.createObjectInCarla(new_actor) - new_actor.carla_actor_flag = True - # Remove objects that should no longer be in Carla - for old_actor in self.carla_actors: - if old_actor not in new_actors: - self.destroy_carla_obj(old_actor) - old_actor.carla_actor_flag = False - # release the road segment that the old actors was in originally - # This could potentially delete a segment that SHOULD still be contained - # This is OK because we will update with the relevant actors before ticking again - self.metsr_client.release_cosim_road(self.query_road(old_actor)) + def get_carla_lanes(self) -> list[Lane]: + """ + Collect the current set of lanes intersecting the CoSimulation bubble + """ + # Set ego, lane, bubble + ego = self.objects[0] + ego_lane = ego._lane + bubble_region = ego.bubble + + # TODO what to do if the actor is not currently in a lane + if ego_lane is None: + lanes = [*self.workspace.network.lanes] + distances = [(lane.distanceTo(ego.position),lane) for lane in lanes] + ego_lane = min(distances, key=lambda t: t[0])[1] # min distance over all lanes + + # Collect lanes which intersect bubble + carla_lanes = [] + for lane in self.workspace.network.lanes: + if lane.intersects(bubble_region): + carla_lanes.append(lane) + return carla_lanes + + def get_carla_intersections(self) -> list[Intersection] | None: + """ + TODO: will need update for multiple high-fidelity zones + Collect any intersections currently within the bubble + (1) TODO consider when an intersection should be included + """ + carla_intersections = [] + ego = self.objects[0] + + # check if ego in intersection + if ego._intersection: + carla_intersections.append(ego._intersection) + + # Bubble region + bubble_region = ego.bubble + + # Collect intersections which intersect bubble + for intersection in self.workspace.network.intersections: + if intersection.intersects(bubble_region): + carla_intersections.append(intersection) + + return carla_intersections + + def _set_intersections(self) -> None: + """ + Checks if an intersection is fully inclosed in bubble + (1) If all connecting roads for an intersection are in bubble so is intersection + + """ + pass + + def synchronize_signals(self, intersections: list[Intersection]) -> None: + """ + Ensures consistency for light signals for all intersections contained in the bubble region + """ + pass - self.carla_actors = new_actors - self.query_road() #update the road segments which Metsr controls before ticking metsr + def synchronize_clients(self): + """ + Update Metsr client so object positions are synchronized with the behaviors in Scenic + """ + carla_actors = [obj for obj in self.objects if obj.carla_actor_flag] + for obj in carla_actors: + loc = obj.carlaActor.get_location() + vehID = self.getMetsrPrivateVehId(obj) + assert obj._lane, f"Object {obj} is not on a lane" + roadID = self.get_mapping(obj._lane) + # print(f"Teleporting Vehicle to location {loc.x, -loc.y}") + self.metsr_client.teleport_cosim_vehicle(vehID, roadID, loc.x, -loc.y, private_veh = True, transform_coords = True ) + + + + def update_carla_lanes(self) -> None: #TODO break this out into multiple functions/helpers + """ + TODO : Avoid nested call structure implement both in step + Update the state of lanes + (1): Freeze new lanes inside CoSimulator bubble + (2): Release lanes outside the CoSimulator bubble + (3): Update objects inside Carla with update lanes + """ + # Collect lanes intersecting the bubble + lanes = self.get_carla_lanes() + # Find the corresponding METSR keys + carla_lane_ids = [self.get_mapping(lane) for lane in lanes] + + # Filter for uniquenes + carla_lane_ids = set(carla_lane_ids) + # Lanes which are already set + curr_frozen_ids = list(self.carla_control_lanes.keys()) + + # Collect new and old lanes + new_lanes = [id for id in carla_lane_ids if id not in curr_frozen_ids] + old_lanes = [id for id in list(self.carla_control_lanes.keys()) if id not in carla_lane_ids] + + # Update object existance based on bubble changes + return lanes, new_lanes,old_lanes - def get_carla_actors(self): + + + def update_bubble_objects(self, carla_lanes: list[str], new_lanes: list[str]) -> None: """ - Helper function to compute which objects belong in Carla + Check each objects current lane + (1) If object is in a Carla lane and not created yet + then create object in Carla + (2) Object is NOT in a Carla lane but still in Carla + then delete object in Carla= """ - bubble = self.objects[0].bubble # Bubble around the ego - actors = [self.objects[0]] - for obj in self.objects[1:]: # check all objects in the sim - mesh = self.objects[0].mesh # grab obj mesh - boundary_points = mesh.vertices[mesh.vertices_on_boundary] # grab boundary points - if np.any([bubble.touches(Point(p[:2])) for p in boundary_points]): #Check if any points are contained in buffer - actors.append(obj) # add to the list of current actors - return actors + carla_actors = [obj for obj in self.objects if obj.carla_actor_flag] # TODO might need to consider a more efficient data structure here + # print(f"Updating object for lanes: {carla_lanes}") + for obj in self.objects[1:]: #TODO Need to get a helper for finding the closest lane when no lane + # TODO check for Driving object + lane = obj._lane + if lane: + # If object is in a lane + if lane in carla_lanes and not obj.carla_actor_flag: + if lane in new_lanes: + print(f"Object in new lane intersecting the bubble") + # Object is inside the bubble lanes but not instantiated + veh_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True) + if 'dist' in veh_data["DATA"][0]: + print(f"Creating Object: {obj}, at position: {obj.position} at {self.count}") + self.createObjectInCarla(obj) + carla_actors.append(obj) + obj.carla_actor_flag = True + obj.spawn_guard = 2 + + elif lane not in carla_lanes and obj.carla_actor_flag: + # Object is not in bubble anymore but still in CARLA + if obj.spawn_guard == 0: + obj.carla_actor_flag = False + # print(f"Destroying obj: {obj} at {self.count}") + print(f"Destroying Object: {obj}, Lane: {lane.id, lane.uid}, actor_flag {obj.carla_actor_flag}, postion: {obj.position}") + self.destroy_carla_obj(obj) + carla_actors.remove(obj) + + + + + + - def query_road(self): + def freeze_lanes(self, keys: list[str]) -> None: """ Query carla actors for relevent road segments which metsr will not control """ - for actor in self.carla_actors: - road_id = self.get_mapping(actor.position)# placeholder idek how to do this - self.metsr_client.set_cosim_road(road_id) + for key in keys: + assert key not in self.carla_control_lanes, "Attempted to freeze already frozen lane" + # Keep track of frozen lanes + self.carla_control_lanes[key] = True + self.metsr_client.set_cosim_road(key) + def release_lanes(self,keys: list[str]) -> None: + """ + Release frozen lanes in Carla + """ + for key in keys: + assert key in self.carla_control_lanes, "Attempted to release non frozen lane" + # Remove frozen lane from record + del self.carla_control_lanes[key] + self.metsr_client.release_cosim_road(key) - def destroy_carla_obj(self,obj): + + def destroy_carla_obj(self,obj) -> None: """ Special destroy method for updating high-fidelity bubble """ @@ -478,12 +721,112 @@ def destroy_carla_obj(self,obj): obj.cralaController.stop() obj.carlaController.destroy() obj.carlaActor.destroy() + obj.carlaActor = None # Set this to None to prevent reaccess a previously deleted vehicle? - def get_mapping(self,obj): + def get_mapping(self,lane: Lane) -> str: """ - TODO: Map an objects position in xodr world to the metsr equivalent road ID + TODO: Takes an object and maps the closest lane of the object + in xodr to its corresponding xml key """ - return 0 + metsr_key=None + # Parent road key with associated lane id + query_key = f'{lane.road.id}_{lane.id}' + + # Check if element is present in map between formats + if query_key in self.xodr_to_xml_map: + metsr_key = self.xodr_to_xml_map[query_key] - \ No newline at end of file + # There must be a valid mapping + assert metsr_key is not None, f"Error identifying associated ID for {query_key}" + return metsr_key + + + def metsr_destroy(self) -> None: + """ + Metsr specific destroy + """ + if self.metsr_client.verbose: + print("Client Messages Log:") + print("[") + for call in self.client._messagesLog: + print(f" {call},") + print("]") + + + def carla_destroy(self) -> None: + """ + Carla specific destroy + """ + for obj in self.objects: + if obj.carlaActor is not None: + if isinstance(obj.carlaActor, carla.Vehicle): + obj.carlaActor.set_autopilot(False, self.tm.get_port()) + if isinstance(obj.carlaActor, carla.Walker): + obj.carlaController.stop() + obj.carlaController.destroy() + obj.carlaActor.destroy() + if self.render and self.cameraManager: + self.cameraManager.destroy_sensor() + + self.carla_client.stop_recorder() + # self.carla_world.tick() + + + def destroy(self) -> None: + """ + Destroy both simulator instances + """ + self.metsr_destroy() + self.carla_destroy() + super().destroy() + + + def _nearest_lane(self,obj) -> Lane | None : # TODO :: Update lane logic to consider intersections + """ + Docstring for _nearest_lane + + Return the nearest lane to the object should ensure all objects are cars? + """ + lane = obj._lane + if lane: + return lane + else: + return None + + def executeActions(self, allActions) -> None: + """ + Apply control updates which were accumulated while executing the actions + Filters out actions for Carla only objects + """ + carla_actions = {} + for obj in self.agents: + if obj.carla_actor_flag: + carla_actions[obj] = allActions[obj] + + super().executeActions(carla_actions) + + for obj in self.agents: + if obj.carla_actor_flag: + ctrl = obj._control + if ctrl is not None: + obj.carlaActor.apply_control(ctrl) + obj._control = None + + + def updateObjects(self) -> None: + # metsr_obj = [obj for obj in self.objects if not obj.carla_actor_flag] + # metsr_obj.append(self.objects[0]) + obj_veh_ids = [self.getMetsrPrivateVehId(obj) for obj in self.objects] + raw_veh_data = self.metsr_client.query_vehicle(obj_veh_ids, True, True) + self.obj_data_cache = {obj: raw_veh_data['DATA'][i] for i, obj in enumerate(self.objects)} + + for obj in self.obj_data_cache: + if 'dist' not in self.obj_data_cache[obj]: + self.queued_vehicles[obj] = True + elif 'dist' in self.obj_data_cache[obj] and obj in self.queued_vehicles: + print(f"obj {obj} leaving the spawn queue") + del self.queued_vehicles[obj] + + super().updateObjects() + self.obj_data_cache = None \ No newline at end of file From 714e37b655c20b39e124fb5fb7b9762afbe2044d Mon Sep 17 00:00:00 2001 From: Kay Date: Thu, 5 Feb 2026 10:44:39 -0800 Subject: [PATCH 35/73] CoSim maps --- assets/maps/CARLA/Town01.net.xml | 1958 +++++++ assets/maps/CARLA/Town02.net.xml | 1366 +++++ assets/maps/CARLA/Town03.net.xml | 8360 ++++++++++++++++++++++++++++++ assets/maps/CARLA/Town04.net.xml | 5833 +++++++++++++++++++++ assets/maps/CARLA/Town05.net.xml | 6338 ++++++++++++++++++++++ assets/maps/CARLA/Town06.net.xml | 4853 +++++++++++++++++ assets/maps/CARLA/Town07.net.xml | 4581 ++++++++++++++++ assets/maps/CARLA/Town10.net.xml | 2075 ++++++++ examples/cosim/readme.md | 5 + 9 files changed, 35369 insertions(+) create mode 100644 assets/maps/CARLA/Town01.net.xml create mode 100644 assets/maps/CARLA/Town02.net.xml create mode 100644 assets/maps/CARLA/Town03.net.xml create mode 100644 assets/maps/CARLA/Town04.net.xml create mode 100644 assets/maps/CARLA/Town05.net.xml create mode 100644 assets/maps/CARLA/Town06.net.xml create mode 100644 assets/maps/CARLA/Town07.net.xml create mode 100644 assets/maps/CARLA/Town10.net.xml create mode 100644 examples/cosim/readme.md diff --git a/assets/maps/CARLA/Town01.net.xml b/assets/maps/CARLA/Town01.net.xml new file mode 100644 index 000000000..f4978ab29 --- /dev/null +++ b/assets/maps/CARLA/Town01.net.xml @@ -0,0 +1,1958 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/maps/CARLA/Town02.net.xml b/assets/maps/CARLA/Town02.net.xml new file mode 100644 index 000000000..567ae3ecd --- /dev/null +++ b/assets/maps/CARLA/Town02.net.xml @@ -0,0 +1,1366 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/maps/CARLA/Town03.net.xml b/assets/maps/CARLA/Town03.net.xml new file mode 100644 index 000000000..90651f3d8 --- /dev/null +++ b/assets/maps/CARLA/Town03.net.xml @@ -0,0 +1,8360 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/maps/CARLA/Town04.net.xml b/assets/maps/CARLA/Town04.net.xml new file mode 100644 index 000000000..3c8c5ece1 --- /dev/null +++ b/assets/maps/CARLA/Town04.net.xml @@ -0,0 +1,5833 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/maps/CARLA/Town05.net.xml b/assets/maps/CARLA/Town05.net.xml new file mode 100644 index 000000000..08638a3de --- /dev/null +++ b/assets/maps/CARLA/Town05.net.xml @@ -0,0 +1,6338 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/maps/CARLA/Town06.net.xml b/assets/maps/CARLA/Town06.net.xml new file mode 100644 index 000000000..69d89747a --- /dev/null +++ b/assets/maps/CARLA/Town06.net.xml @@ -0,0 +1,4853 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/maps/CARLA/Town07.net.xml b/assets/maps/CARLA/Town07.net.xml new file mode 100644 index 000000000..ade9bca0d --- /dev/null +++ b/assets/maps/CARLA/Town07.net.xml @@ -0,0 +1,4581 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/maps/CARLA/Town10.net.xml b/assets/maps/CARLA/Town10.net.xml new file mode 100644 index 000000000..19d91eb46 --- /dev/null +++ b/assets/maps/CARLA/Town10.net.xml @@ -0,0 +1,2075 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/cosim/readme.md b/examples/cosim/readme.md new file mode 100644 index 000000000..b71180dfb --- /dev/null +++ b/examples/cosim/readme.md @@ -0,0 +1,5 @@ +In order to run cosimulation using both METSR and Carla the user needs to supply both a SUMO and openDrive file. +To generate the associated SUMO file from an opendrive file one run the following command: + +netconvert --opendrive-files example.xodr --output-file example.net.xml --geometry.min-radius.fix --geometry.remove --opendrive.curve-resolution 1 --opendrive.import-all-lanes --output.original-names --tls.guess-signals --tls.discard-simple --tls.join + From 1d0dcddabd85630811f2519fa83eb52d66485758 Mon Sep 17 00:00:00 2001 From: Kay Date: Thu, 5 Feb 2026 10:49:57 -0800 Subject: [PATCH 36/73] updated ID and UID for Lanes --- src/scenic/formats/opendrive/xodr_parser.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/scenic/formats/opendrive/xodr_parser.py b/src/scenic/formats/opendrive/xodr_parser.py index e01d9c49d..8ca7980b0 100644 --- a/src/scenic/formats/opendrive/xodr_parser.py +++ b/src/scenic/formats/opendrive/xodr_parser.py @@ -702,7 +702,7 @@ def toScenicRoad(self, tolerance): left, center, right = right[::-1], center[::-1], left[::-1] succ, pred = pred, succ section = roadDomain.LaneSection( - id=f"road{self.id_}_sec{len(roadSections)}_lane{id_}", + uid=f"road{self.id_}_sec{len(roadSections)}_lane{id_}", polygon=lane_polys[id_], centerline=PolylineRegion(cleanChain(center)), leftEdge=PolylineRegion(cleanChain(left)), @@ -719,7 +719,7 @@ def toScenicRoad(self, tolerance): laneSections[id_] = section allElements.append(section) section = roadDomain.RoadSection( - id=f"road{self.id_}_sec{len(roadSections)}", + uid=f"road{self.id_}_sec{len(roadSections)}", polygon=sec_poly, centerline=PolylineRegion(cleanChain(pts)), leftEdge=PolylineRegion(cleanChain(sec.left_edge)), @@ -931,7 +931,8 @@ def makeShoulder(laneIDs): rightEdge = PolylineRegion(cleanChain(rightPoints)) centerline = PolylineRegion(cleanChain(centerPoints)) lane = roadDomain.Lane( - id=f"road{self.id_}_lane{nextID}", + uid=f"road{self.id_}_lane{nextID}", + id = laneSection.openDriveID, polygon=ls.parent_lane_poly, centerline=centerline, leftEdge=leftEdge, @@ -986,7 +987,8 @@ def getEdges(forward): if forwardLanes: leftEdge, centerline, rightEdge = getEdges(forward=True) forwardGroup = roadDomain.LaneGroup( - id=f"road{self.id_}_forward", + uid=f"road{self.id_}_forward", + id = self.id_, polygon=buffer_union( (lane.polygon for lane in forwardLanes), tolerance=tolerance ), @@ -1007,7 +1009,8 @@ def getEdges(forward): if backwardLanes: leftEdge, centerline, rightEdge = getEdges(forward=False) backwardGroup = roadDomain.LaneGroup( - id=f"road{self.id_}_backward", + uid=f"road{self.id_}_backward", + id = self.id_, polygon=buffer_union( (lane.polygon for lane in backwardLanes), tolerance=tolerance ), From 321a38330bb66a1098847c1379183b111fb0956a Mon Sep 17 00:00:00 2001 From: Kay Date: Thu, 5 Feb 2026 10:50:12 -0800 Subject: [PATCH 37/73] updated .gitignore --- .gitignore | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index d2bf7f6f9..76c3f2d0d 100644 --- a/.gitignore +++ b/.gitignore @@ -119,6 +119,7 @@ ENV/ env.bak/ venv.bak/ scenic.venv/ +3_10_venv/ # Spyder project settings .spyderproject @@ -140,4 +141,8 @@ dmypy.json # generated parser src/scenic/syntax/parser.py -simulation.gif \ No newline at end of file +simulation.gif + +# Random +test.ipynb +test.sh \ No newline at end of file From e5800e35bf6b4420a0dc328d89db8201e343bf59 Mon Sep 17 00:00:00 2001 From: Kay Date: Thu, 5 Feb 2026 10:50:58 -0800 Subject: [PATCH 38/73] minor --- src/scenic/simulators/cosim/utils.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/scenic/simulators/cosim/utils.py diff --git a/src/scenic/simulators/cosim/utils.py b/src/scenic/simulators/cosim/utils.py deleted file mode 100644 index e69de29bb..000000000 From 69225e49ae02e1a045db248a27301e56e5e06615 Mon Sep 17 00:00:00 2001 From: Kay Date: Wed, 11 Feb 2026 11:26:28 -0800 Subject: [PATCH 39/73] helper for checking spawn conditions --- src/scenic/simulators/cosim/utils/utils.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/scenic/simulators/cosim/utils/utils.py b/src/scenic/simulators/cosim/utils/utils.py index 63d29aaa5..53829b4be 100644 --- a/src/scenic/simulators/cosim/utils/utils.py +++ b/src/scenic/simulators/cosim/utils/utils.py @@ -48,14 +48,22 @@ def test_mapping(map, test_pairs): print(f"Failed on test case {key}, {value}") -def within_threshold_to(ego, cars, threshold) -> bool: - ego_pos = np.array(ego.position) +def within_threshold_to(object, cars) -> bool: + # print(f"checking distance between obj: {object} and cars {[car.name for car in cars]}") + object_pos = np.array(object.position) + obj_distances = [] for car in cars: - if np.linalg.norm(np.array(car.position) - ego_pos) < threshold: + threshold = 1.2 * object.length + dist = np.linalg.norm(np.array(car.position) - object_pos) + if dist < threshold: return True + obj_distances.append(dist) + print(f"Object distances for obj: {object.name}: {obj_distances}") return False + + if __name__ == "__main__": map = "Town01.net.xml" From 4640f9b60471b500b74ef4da6afbfa81844bb3eb Mon Sep 17 00:00:00 2001 From: Kay Date: Wed, 11 Feb 2026 13:06:24 -0800 Subject: [PATCH 40/73] init --- src/scenic/simulators/cosim/simulator.py | 291 ++++++++++++++--------- 1 file changed, 172 insertions(+), 119 deletions(-) diff --git a/src/scenic/simulators/cosim/simulator.py b/src/scenic/simulators/cosim/simulator.py index 3b4cf8208..f3a6731ed 100644 --- a/src/scenic/simulators/cosim/simulator.py +++ b/src/scenic/simulators/cosim/simulator.py @@ -9,6 +9,7 @@ from scenic.domains.driving.roads import Lane, Intersection + import pygame import warnings import os @@ -170,6 +171,7 @@ def __init__(self, scene, carla_client, metsr_client, sim_timestep, tm,render,re self.workspace = scene.workspace self.carla_control_lanes = {} self.queued_vehicles = {} + self.bubble_spawn_queue = set({}) super().__init__(scene, timestep=sim_timestep, **kwargs) @@ -262,7 +264,7 @@ def createObjectInMetsr(self, obj) -> None: assert obj.origin, "Metsr objects must have a defined origin" assert obj.destination, "Metsr objects must have a defined destination" - print(f"Creating obj: {obj} with position: {obj.position} in METSR") + # print(f"Creating obj: {obj} with position: {obj.position} in METSR") call_kwargs = { "vehID": self.getMetsrPrivateVehId(obj), @@ -275,7 +277,6 @@ def createObjectInMetsr(self, obj) -> None: def createObjectInCarla(self,obj) -> None: # Extract blueprint - print(f"Creating obj: {obj} with position: {obj.position} in CARLA") try: blueprint = self.blueprintLib.find(obj.blueprint) except IndexError as e: @@ -329,13 +330,13 @@ def createObjectInCarla(self,obj) -> None: for obj in self.objects: car_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True) simulator = "carla" if obj.carla_actor_flag else "metsr" - print(f"{obj} in {simulator}: [Metsr][Scenic] coords X: {car_data['DATA'][0]['x'], obj.position.x}, y:{car_data['DATA'][0]['y'], obj.position.y}") - print(f"Query Vehicle resuts for obj: :{obj}, is: {car_data['DATA']}") + if obj.carla_actor_flag: + print(f"{obj} in {simulator}: [Metsr][Scenic] coords X: {car_data['DATA'][0]['x'], obj.position.x}, y:{car_data['DATA'][0]['y'], obj.position.y}") + print(f"Query Vehicle resuts for obj: :{obj}, is: {car_data['DATA']}") # raise e(f"Error : {e} occured") - # if carlaActor is None: - # raise SimulationCreationError(f"Unable to spawn object {obj}") + if carlaActor is None: + raise SimulationCreationError(f"Unable to spawn object {obj}") obj.carlaActor = carlaActor - carlaActor.set_simulate_physics(obj.physics) if isinstance(carlaActor, carla.Vehicle): @@ -371,19 +372,13 @@ def createObjectInSimulator(self, obj) -> None: assert hasattr(obj, "carla_actor_flag"), "All objects must have attribute: carla_actor_flag" - # try: - # vehID = self.getMetsrPrivateVehId(obj) - # veh_data = self.metsr_client.query_vehicle(vehID, True, True) - # if 'x' in veh_data[0] and 'y' in veh_data[0]: - - - if obj == self.objects[0]: - # Special handling creating EGO in both simulators - # ensures consistency for obje queue spawning in METSR + if obj == self.objects[0]: # ensures consistency for obje queue spawning in METSR self.createObjectInCarla(obj) self.createObjectInMetsr(obj) + self.synchronize_clients() # First time ego is created synchronize clients elif obj.carla_actor_flag: self.createObjectInCarla(obj) + self.synchronize_clients() else: self.createObjectInMetsr(obj) @@ -421,7 +416,6 @@ def getCarlaProperties(self, obj, properties) -> dict[str, float | Vector | int] roll=roll, elevation=elevation, ) - # print(f"{obj} properties: {values}") return values @@ -465,12 +459,10 @@ def getProperties(self, obj, properties)-> dict[str, float | Vector | int]: """ assert hasattr(obj, "carla_actor_flag"), f"Object is not assigned properly to a simulator instance" """ TODO: Sometimes update objects fails on the first step -- Not clear why this occurs -- adding a check before accessing obj.carlaActor """ - # if obj.carla_actor_flag: - # if obj.carlaActor == None: # TODO added check here to ensure actor is created before polling simulator - # print(f"Something strange has occured for obj {obj} at step: {self.count}") - # properties = self.getCarlaProperties(obj,properties) - # else: - properties = self.getMetsrProperties(obj,properties) + if obj.carla_actor_flag: + properties = self.getCarlaProperties(obj,properties) + else: + properties = self.getMetsrProperties(obj,properties) return properties def getMetsrPrivateVehId(self,obj) -> int: @@ -495,17 +487,42 @@ def step(self) -> None: """ + for obj in self.objects: + if obj.carla_actor_flag: + car_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True) + metsr_x, metsr_y = car_data['DATA'][0]['x'], car_data['DATA'][0]['y'] + loc = obj.carlaActor.get_location() + carla_x, carla_y = loc.x, loc.y + in_metsr_queue = not bool("dist" in car_data['DATA'][0]) + + if not np.isclose(metsr_x, carla_x): + print(f"Checking Object synchronization before Stepping") + print(f"Obj: {obj} in metsr queue? {in_metsr_queue}") + print("=" * 25) + lane = obj._lane + if lane: + print(f"OBJ lane: {self.map_scenic_to_metsr(obj._lane)}") + print(f"OBJ X: {obj}| METSR {metsr_x}: CARLA {carla_x}") + if not np.isclose(metsr_y, -carla_y): + print(f"OBJ Y: {obj}| METSR {metsr_y}: CARLA {-carla_y}") + print("=" * 25) + + # metsr_data = self.metsr_client.query_coSimVehicle() + # print(f"Displaying METSR query data: {metsr_data}") + + lanes, new_lanes, old_lanes = self.update_carla_lanes() self.release_lanes(old_lanes) self.freeze_lanes(new_lanes) - # Update state of simulator with new bubble region self.update_bubble_objects(lanes,new_lanes) #Carla step self.carla_world.tick() self.synchronize_clients() - # Need to ensure carla is ticked a + + + # It takes 2 CARLA ticks to fully instantiate and update properties once spawned in CARLA for obj in self.objects: if obj.spawn_guard > 0: obj.spawn_guard -= 1 @@ -518,27 +535,21 @@ def step(self) -> None: self.count += 1 if self.count % 100 == 0: # just removing for now print(".", end="", flush=True) + + metsr_data = self.metsr_client.query_coSimVehicle() + # print(f"Displaying METSR query data: {metsr_data}") + self.metsr_client.tick() - # Generate bubble region based on ego objects - # TODO update logic for multiple high-fidelity zones + # Generate bubble region based on ego objects TODO update logic for multiple high-fidelity zones self.objects[0].bubble = CircularRegion(center=[self.objects[0].x, self.objects[0].y], radius=self.bubble_size) - - self.synchronize_clients() - # for obj in self.objects: - # if obj.carla_actor_flag: - # loc = obj.carlaActor.get_location() - # car_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True) - # if not math.isclose(car_data['DATA'][0]['x'], loc.x) or not math.isclose(car_data['DATA'][0]['y'],-loc.y): - # print(f"{car_data['DATA'][0]['x'], loc.x} -- {car_data['DATA'][0]['y'], -loc.y}") - - # Set intersections intersections = self.get_carla_intersections() if intersections != []: + # print(f"Intersections were: {intersections}") self.synchronize_signals(intersections) @@ -548,15 +559,9 @@ def get_carla_lanes(self) -> list[Lane]: """ # Set ego, lane, bubble ego = self.objects[0] - ego_lane = ego._lane + lane = self._nearest_lane(ego) bubble_region = ego.bubble - # TODO what to do if the actor is not currently in a lane - if ego_lane is None: - lanes = [*self.workspace.network.lanes] - distances = [(lane.distanceTo(ego.position),lane) for lane in lanes] - ego_lane = min(distances, key=lambda t: t[0])[1] # min distance over all lanes - # Collect lanes which intersect bubble carla_lanes = [] for lane in self.workspace.network.lanes: @@ -610,15 +615,10 @@ def synchronize_clients(self): for obj in carla_actors: loc = obj.carlaActor.get_location() vehID = self.getMetsrPrivateVehId(obj) - assert obj._lane, f"Object {obj} is not on a lane" - roadID = self.get_mapping(obj._lane) - # print(f"Teleporting Vehicle to location {loc.x, -loc.y}") + lane = self._nearest_lane(obj) + roadID = self.map_scenic_to_metsr(lane) self.metsr_client.teleport_cosim_vehicle(vehID, roadID, loc.x, -loc.y, private_veh = True, transform_coords = True ) - - - - def update_carla_lanes(self) -> None: #TODO break this out into multiple functions/helpers """ TODO : Avoid nested call structure implement both in step @@ -630,9 +630,7 @@ def update_carla_lanes(self) -> None: #TODO break this out into multiple functio # Collect lanes intersecting the bubble lanes = self.get_carla_lanes() # Find the corresponding METSR keys - carla_lane_ids = [self.get_mapping(lane) for lane in lanes] - - # Filter for uniquenes + carla_lane_ids = [self.map_scenic_to_metsr(lane) for lane in lanes] carla_lane_ids = set(carla_lane_ids) # Lanes which are already set curr_frozen_ids = list(self.carla_control_lanes.keys()) @@ -642,71 +640,102 @@ def update_carla_lanes(self) -> None: #TODO break this out into multiple functio old_lanes = [id for id in list(self.carla_control_lanes.keys()) if id not in carla_lane_ids] # Update object existance based on bubble changes - return lanes, new_lanes,old_lanes - + return lanes, new_lanes, old_lanes def update_bubble_objects(self, carla_lanes: list[str], new_lanes: list[str]) -> None: """ Check each objects current lane - (1) If object is in a Carla lane and not created yet - then create object in Carla + (1) Check if there are cars in the queue to spawn in the CoSim bubble + prioritize safely spawning these vehicles first (2) Object is NOT in a Carla lane but still in Carla - then delete object in Carla= + then delete object in Carla + (3) If object is in a Carla lane and not created yet + then create object in Carla + + TODO: Slight redundancy for some objects """ + cosim_data = self.metsr_client.query_coSimVehicle() carla_actors = [obj for obj in self.objects if obj.carla_actor_flag] # TODO might need to consider a more efficient data structure here - # print(f"Updating object for lanes: {carla_lanes}") - for obj in self.objects[1:]: #TODO Need to get a helper for finding the closest lane when no lane - # TODO check for Driving object - lane = obj._lane - if lane: - # If object is in a lane - if lane in carla_lanes and not obj.carla_actor_flag: - if lane in new_lanes: - print(f"Object in new lane intersecting the bubble") - # Object is inside the bubble lanes but not instantiated - veh_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True) - if 'dist' in veh_data["DATA"][0]: + # Check if room has opened up to spawn queued vehicles in the bubble + if len(self.bubble_spawn_queue) > 0: + if self.count % 10 == 0: + print(f"{len(self.bubble_spawn_queue)} objects waiting to spawn") + for obj in self.bubble_spawn_queue: + lane = self._nearest_lane(obj) + if lane in carla_lanes: + if not within_threshold_to(obj, carla_actors): + obj.orienation = lane.orientation[obj.position] + self.createObjectInCarla(obj) + carla_actors.add(obj) + obj.carla_actor_flag = True + obj.spawn_guard = 2 + self.bubble_spawn_queue.remove(obj) + # else: + # print(f"Waiting to spawn: {obj}: {self.count}") + + # Remove objects which are no longer inside the bubble + for obj in self.objects[1:]: + lane = self._nearest_lane(obj) + veh_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True) # + + if lane not in carla_lanes and obj.carla_actor_flag: + if obj.spawn_guard == 0: + obj.carla_actor_flag = False + self.destroy_carla_obj(obj) + carla_actors.remove(obj) + # Add objects to the bubble if they are entering through a new lane and their is not enough space to spawn + elif lane in carla_lanes and not obj.carla_actor_flag: + if self.map_scenic_to_metsr(lane) in new_lanes and within_threshold_to(obj, carla_actors): + self.bubble_spawn_queue.add(obj) + + # If their is enough room check that the vehicle is not currently queued and spawn + else: + if 'dist' in veh_data["DATA"][0]: print(f"Creating Object: {obj}, at position: {obj.position} at {self.count}") + print(f"Displaying Cosim data: {cosim_data}") + VehID = self.getMetsrPrivateVehId(obj) + route_data = None + print(f"CoSim Data: {len(cosim_data['DATA'])} : {cosim_data['DATA']}") + for entry in cosim_data["DATA"]: + print(entry) + # assert False, "Terminating early" + for data_entry in cosim_data['DATA']: + # print(f"data entry: {data_entry}") + if data_entry['ID'] == VehID: + route_data = data_entry['route'] + print(f"VehID: {VehID}, Data: {data_entry}") + print(f"Selected route data: {route_data}") + obj.trajectory = self.generate_scenic_trajectory(lane,route_data) + + obj.orienation = lane.orientation[obj.position] self.createObjectInCarla(obj) carla_actors.append(obj) - obj.carla_actor_flag = True - obj.spawn_guard = 2 - - elif lane not in carla_lanes and obj.carla_actor_flag: - # Object is not in bubble anymore but still in CARLA - if obj.spawn_guard == 0: - obj.carla_actor_flag = False - # print(f"Destroying obj: {obj} at {self.count}") - print(f"Destroying Object: {obj}, Lane: {lane.id, lane.uid}, actor_flag {obj.carla_actor_flag}, postion: {obj.position}") - self.destroy_carla_obj(obj) - carla_actors.remove(obj) - - - - - + obj.carla_actor_flag = True + obj.spawn_guard = 2 + # else: + # print(f"Passed spawning obj: {obj} as it is still queue") + def freeze_lanes(self, keys: list[str]) -> None: """ - Query carla actors for relevent road segments which metsr will not control + Query Metsr to freeze simulation and control of given lanes """ for key in keys: assert key not in self.carla_control_lanes, "Attempted to freeze already frozen lane" - # Keep track of frozen lanes - self.carla_control_lanes[key] = True + self.carla_control_lanes[key] = True # Keep track of frozen lanes self.metsr_client.set_cosim_road(key) + def release_lanes(self,keys: list[str]) -> None: """ - Release frozen lanes in Carla + Query Metsr to begin re-simulating and control given lanes """ for key in keys: assert key in self.carla_control_lanes, "Attempted to release non frozen lane" - # Remove frozen lane from record - del self.carla_control_lanes[key] + del self.carla_control_lanes[key] # Remove frozen lane from record self.metsr_client.release_cosim_road(key) @@ -724,7 +753,7 @@ def destroy_carla_obj(self,obj) -> None: obj.carlaActor = None # Set this to None to prevent reaccess a previously deleted vehicle? - def get_mapping(self,lane: Lane) -> str: + def map_scenic_to_metsr(self,lane: Lane) -> str: """ TODO: Takes an object and maps the closest lane of the object in xodr to its corresponding xml key @@ -736,16 +765,47 @@ def get_mapping(self,lane: Lane) -> str: # Check if element is present in map between formats if query_key in self.xodr_to_xml_map: metsr_key = self.xodr_to_xml_map[query_key] + + metsr_key = metsr_key.split("_")[0] # There must be a valid mapping - assert metsr_key is not None, f"Error identifying associated ID for {query_key}" + assert metsr_key is not None, f"Error identifying associated ID for {query_key}" return metsr_key + + def generate_scenic_trajectory(self, curr_lane, route: list[str]) -> list[Lane]: + """ + Docstring for generate_scenic_trajectory + + :param self: CoSim Object + :param route: Metsr route data for a single car + :type route: list[str] + :return: Equivalent trajetory with Scenic Lanes + :rtype: list[Lane] + + """ + assert route is not None, f"Unable to generate trajectory with route data: {route}" + xodr_road_keys = [key for key in self.xodr_to_xml_map.keys()] + print(f'Current lane: {curr_lane.id}, {curr_lane.road.id}') + print(f"Proposed Route: {route} ") + for road in route: + size = len(road) + size2 = len(str(curr_lane.road.id)) + for key in xodr_road_keys: + if key[:size] == road: + pass + # print(key, sep=" ") + if key[:size2] == curr_lane.road.id: + pass + # print(key, sep=" ") + + return None - def metsr_destroy(self) -> None: + def destroy(self) -> None: """ - Metsr specific destroy + Destroy both simulator instances """ + # METSR destroy if self.metsr_client.verbose: print("Client Messages Log:") print("[") @@ -753,11 +813,7 @@ def metsr_destroy(self) -> None: print(f" {call},") print("]") - - def carla_destroy(self) -> None: - """ - Carla specific destroy - """ + # "CARLA destroy" for obj in self.objects: if obj.carlaActor is not None: if isinstance(obj.carlaActor, carla.Vehicle): @@ -770,15 +826,6 @@ def carla_destroy(self) -> None: self.cameraManager.destroy_sensor() self.carla_client.stop_recorder() - # self.carla_world.tick() - - - def destroy(self) -> None: - """ - Destroy both simulator instances - """ - self.metsr_destroy() - self.carla_destroy() super().destroy() @@ -790,9 +837,14 @@ def _nearest_lane(self,obj) -> Lane | None : # TODO :: Update lane logic to cons """ lane = obj._lane if lane: - return lane + nearest_lane = lane else: - return None + lanes = [*self.workspace.network.lanes] + distances = [(lane.distanceTo(obj.position),lane) for lane in lanes] + nearest_lane = min(distances, key=lambda t: t[0])[1] # min distance over all lanes + + return nearest_lane + def executeActions(self, allActions) -> None: """ @@ -821,12 +873,13 @@ def updateObjects(self) -> None: raw_veh_data = self.metsr_client.query_vehicle(obj_veh_ids, True, True) self.obj_data_cache = {obj: raw_veh_data['DATA'][i] for i, obj in enumerate(self.objects)} - for obj in self.obj_data_cache: - if 'dist' not in self.obj_data_cache[obj]: - self.queued_vehicles[obj] = True - elif 'dist' in self.obj_data_cache[obj] and obj in self.queued_vehicles: - print(f"obj {obj} leaving the spawn queue") - del self.queued_vehicles[obj] + #DEBUGGING FOR METSR + # for obj in self.obj_data_cache: + # if 'dist' not in self.obj_data_cache[obj]: + # self.queued_vehicles[obj] = True + # elif 'dist' in self.obj_data_cache[obj] and obj in self.queued_vehicles: + # print(f"obj {obj} leaving the spawn queue") + # del self.queued_vehicles[obj] super().updateObjects() self.obj_data_cache = None \ No newline at end of file From 5b4085ec25f2cf0c3b215d6211ed839aa7fcc831 Mon Sep 17 00:00:00 2001 From: Kay Date: Wed, 11 Feb 2026 13:06:39 -0800 Subject: [PATCH 41/73] init --- examples/cosim/test.scenic | 9 ++++++--- src/scenic/simulators/cosim/model.scenic | 4 +++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/examples/cosim/test.scenic b/examples/cosim/test.scenic index 46ef44692..9a71f7e69 100644 --- a/examples/cosim/test.scenic +++ b/examples/cosim/test.scenic @@ -1,16 +1,19 @@ # param startTime = 0 param map = localPath('../../assets/maps/CARLA/Town05.xodr') # OpenDrive file param xml_map = localPath("../../assets/maps/CARLA/Town05.net.xml") # Sumo file -param address = "10.45.20.114" +param address = "10.29.10.114" # param address = "10.0.0.122" # param verbose = True model scenic.simulators.cosim.model -ego = new EgoCar with behavior DriveAvoidingCollisions(target_speed=15, avoidance_threshold=12), with name "ego", with position Vector(88.80499658384105, 1.5729413763422928, 0) +# with behavior DriveAvoidingCollisions(target_speed=15, avoidance_threshold=12), +ego = new EgoCar with name "ego", with behavior DriveAvoidingCollisions(target_speed=15, avoidance_threshold=12) +#, with position Vector(88.80499658384105, 1.5729413763422928, 0) -for i in range(10): +for i in range(20): title = f"npccar_{i}" # allow me to debug more easily new NPCCar with name title + #, with position Vector(86.43, 1.5500000000000114, 0) terminate after 500 steps \ No newline at end of file diff --git a/src/scenic/simulators/cosim/model.scenic b/src/scenic/simulators/cosim/model.scenic index f75974d97..70b1ae8b4 100644 --- a/src/scenic/simulators/cosim/model.scenic +++ b/src/scenic/simulators/cosim/model.scenic @@ -61,8 +61,10 @@ class NPCCar(Car): An NPC car """ carla_actor_flag: False + trajectory: None + behavior: DriveAvoidingCollisions(target_speed=15, avoidance_threshold=12) # Default Carla Behavior - behavior: DriveAvoidingCollisions(target_speed=10, avoidance_threshold=12) + # behavior: FollowLaneBehavior(self.trajectory) From cd00be131d4b2dbddf2ef21cac2187bde9ec08f8 Mon Sep 17 00:00:00 2001 From: Kay Date: Wed, 11 Feb 2026 13:06:47 -0800 Subject: [PATCH 42/73] init --- src/scenic/simulators/cosim/utils/utils.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/scenic/simulators/cosim/utils/utils.py b/src/scenic/simulators/cosim/utils/utils.py index 53829b4be..f5d30412b 100644 --- a/src/scenic/simulators/cosim/utils/utils.py +++ b/src/scenic/simulators/cosim/utils/utils.py @@ -51,14 +51,13 @@ def test_mapping(map, test_pairs): def within_threshold_to(object, cars) -> bool: # print(f"checking distance between obj: {object} and cars {[car.name for car in cars]}") object_pos = np.array(object.position) - obj_distances = [] + # obj_distances = [] for car in cars: threshold = 1.2 * object.length dist = np.linalg.norm(np.array(car.position) - object_pos) if dist < threshold: return True - obj_distances.append(dist) - print(f"Object distances for obj: {object.name}: {obj_distances}") + # obj_distances.append(dist) return False From 0bcc909439834a237acdac3a4505ae92a2da7264 Mon Sep 17 00:00:00 2001 From: Kay Date: Mon, 9 Mar 2026 16:00:53 -0700 Subject: [PATCH 43/73] updated metsr client --- src/scenic/simulators/metsr/client.py | 1680 +++++++++++++++---------- 1 file changed, 1049 insertions(+), 631 deletions(-) diff --git a/src/scenic/simulators/metsr/client.py b/src/scenic/simulators/metsr/client.py index 2bde3ce32..01a874c3d 100644 --- a/src/scenic/simulators/metsr/client.py +++ b/src/scenic/simulators/metsr/client.py @@ -1,631 +1,1049 @@ -import datetime -import json -import time -import threading - - -from websockets.sync.client import connect - - -class METSRClient: - - def __init__(self, host, port, sim_folder = None, manager = None, - max_connection_attempts = 5, timeout = 30, verbose = False): - super().__init__() - - # Websocket config - self.host = host - self.port = port - self.uri = f"ws://{host}:{port}" - - self.sim_folder = sim_folder # this is required for open the visualization server - self.state = "connecting" - self.timeout = timeout # time out for resending the same message if no response - self.verbose = verbose - self._messagesLog = [] - - # a pointer to the manager, for HPC usage that one manager controls multiple clients - self.manager = manager - - # visualization server and event - self.viz_server = None - self.viz_event = None - - # Track the tick of the corresponding simulator - self.current_tick = None - - # Establish connection - failed_attempts = 0 - while True: - try: - self.ws = connect(self.uri) - self.state = "connected" - if self.verbose: - print(f"Connected to {self.uri}") - break - except ConnectionRefusedError: - print(f"Attempt to connect to {self.uri} failed. " - f"Waiting for 10 seconds before trying again... " - f"({max_connection_attempts - failed_attempts} attempts remaining)") - failed_attempts += 1 - if failed_attempts >= max_connection_attempts: - self.state = "failed" - raise RuntimeError("Could not connect to METS-R Sim") - time.sleep(10) - - # Ensure server is initialized by waiting to receive an initial packet - # (could be ANS_ready or a heartbeat) - self.receive_msg(ignore_heartbeats=False) - - self.lock = threading.Lock() - - def send_msg(self, msg): - if self.verbose: - self._logMessage("SENT", msg) - self.ws.send(json.dumps(msg)) - - def receive_msg(self, ignore_heartbeats, waiting_forever = True): - start_time = time.time() - while True: - raw_msg = self.ws.recv(timeout = self.timeout) - - # Decode the json string - msg = json.loads(str(raw_msg)) - - if self.verbose: - self._logMessage("RECEIVED", msg) - - # EVERY decoded msg must have a TYPE field - assert "TYPE" in msg.keys(), "No type field in received message" - assert msg["TYPE"].split("_")[0] in {"STEP", "ANS", "CTRL", "ATK"}, "Uknown message type: " + str(msg["TYPE"]) - - # Allow tick() - if msg["TYPE"] in {"ANS_ready"}: - self.current_tick = 0 - continue - - # Return decoded message, if it's not an ignored heartbeat - if not ignore_heartbeats or msg["TYPE"] != "STEP": - return msg - - if time.time() - start_time > self.timeout and not waiting_forever: - print("Timeout while waiting for message.") - return None - - def send_receive_msg(self, msg, ignore_heartbeats, max_attempts=5): - with self.lock: - res = None - num_attempts = 0 - try: - while res is None: - num_attempts += 1 - self.send_msg(msg) - if(max_attempts > 0): - res = self.receive_msg(ignore_heartbeats=ignore_heartbeats, waiting_forever=False) - if num_attempts >= max_attempts: - print(f"Failed to receive response after {max_attempts} attempts") - break - else: - res = self.receive_msg(ignore_heartbeats=ignore_heartbeats, waiting_forever=True) - except KeyboardInterrupt: - print("\nKeyboardInterrupt detected. Stopping the current operation but keeping the server active.") - # Reset state or resources if necessary to allow future operations - return None # Return None to indicate the operation was interrupted - except Exception as e: - print(f"An unexpected error occurred: {e}") - # Optional: Handle other types of exceptions if needed - return res - - def tick(self, step_num = 1, wait_forever = False): - assert self.current_tick is not None, "self.current_tick is None. Reset should be called first" - msg = {"TYPE": "STEP", "TICK": self.current_tick, "NUM": step_num} - self.send_msg(msg) - - while True: - # Move through messages until we get to an up to date heartbeat - res = self.receive_msg(ignore_heartbeats=False, waiting_forever=wait_forever) - - assert res["TYPE"] == "STEP", res["TYPE"] - if res["TICK"] == self.current_tick + step_num: - break - - self.current_tick = res["TICK"] - - # QUERY: inspect the state of the simulator - # By default query public vehicles - def query_vehicle(self, id = None, private_veh = False, transform_coords = False): - # transform coords arguments denotes transformation True: METSR -> SUMO , False just METSR - msg = {"TYPE": "QUERY_vehicle"} - if id is not None: - msg["DATA"] = [] - if not isinstance(id, list): - id = [id] - if not isinstance(private_veh, list): - private_veh = [private_veh] * len(id) - if not isinstance(transform_coords, list): - transform_coords = [transform_coords] * len(id) - for veh_id, prv, tran in zip(id, private_veh, transform_coords): - msg["DATA"].append({"vehID": veh_id, "vehType": prv, "transformCoord": tran}) - - res = self.send_receive_msg(msg, ignore_heartbeats=True) - assert res["TYPE"] == "ANS_vehicle", res["TYPE"] - return res - - # query taxi - def query_taxi(self, id = None): - my_msg = {"TYPE": "QUERY_taxi"} - if id is not None: - my_msg['DATA'] = [] - if not isinstance(id, list): - id = [id] - for i in id: - my_msg['DATA'].append(i) - - res = self.send_receive_msg(my_msg, ignore_heartbeats=True) - assert res["TYPE"] == "ANS_taxi", res["TYPE"] - return res - - # query bus - def query_bus(self, id = None): - my_msg = {"TYPE": "QUERY_bus"} - if id is not None: - my_msg['DATA'] = [] - if not isinstance(id, list): - id = [id] - for i in id: - my_msg['DATA'].append(i) - res = self.send_receive_msg(my_msg, ignore_heartbeats=True) - assert res["TYPE"] == "ANS_bus", res["TYPE"] - return res - - - # query road - def query_road(self, id = None): - my_msg = {"TYPE": "QUERY_road"} - if id is not None: - my_msg['DATA'] = [] - if not isinstance(id, list): - id = [id] - for i in id: - my_msg['DATA'].append(i) - res = self.send_receive_msg(my_msg, ignore_heartbeats=True) - assert res["TYPE"] == "ANS_road", res["TYPE"] - return res - - # query zone - def query_zone(self, id = None): - my_msg = {"TYPE": "QUERY_zone"} - if id is not None: - my_msg['DATA'] = [] - if not isinstance(id, list): - id = [id] - for i in id: - my_msg['DATA'].append(i) - res = self.send_receive_msg(my_msg, ignore_heartbeats=True) - assert res["TYPE"] == "ANS_zone", res["TYPE"] - return res - - # query signal - def query_signal(self, id = None): - my_msg = {"TYPE": "QUERY_signal"} - if id is not None: - my_msg['DATA'] = [] - if not isinstance(id, list): - id = [id] - for i in id: - my_msg['DATA'].append(i) - res = self.send_receive_msg(my_msg, ignore_heartbeats=True) - assert res["TYPE"] == "ANS_signal", res["TYPE"] - return res - - # query chargingStation - def query_chargingStation(self, id = None): - my_msg = {"TYPE": "QUERY_chargingStation"} - if id is not None: - my_msg['DATA'] = [] - if not isinstance(id, list): - id = [id] - for i in id: - my_msg['DATA'].append(i) - res = self.send_receive_msg(my_msg, ignore_heartbeats=True) - assert res["TYPE"] == "ANS_chargingStation", res["TYPE"] - return res - - # query vehicleID within the co-sim road - def query_coSimVehicle(self): - my_msg = {"TYPE": "QUERY_coSimVehicle"} - res = self.send_receive_msg(my_msg, ignore_heartbeats=True) - assert res["TYPE"] == "ANS_coSimVehicle", res["TYPE"] - return res - - - # CONTROL: change the state of the simulator - # generate a vehicle trip between origin and destination zones - def generate_trip(self, vehID, origin = -1, destination = -1): - msg = {"TYPE": "CTRL_generateTrip", "DATA": []} - if not isinstance(vehID, list): - vehID = [vehID] - if not isinstance(origin, list): - origin = [origin] * len(vehID) - if not isinstance(destination, list): - destination = [destination] * len(vehID) - - assert len(vehID) == len(origin) == len(destination), "Length of vehID, origin, and destination must be the same" - for vehID, origin, destination in zip(vehID, origin, destination): - msg["DATA"].append({"vehID": vehID, "orig": origin, "dest": destination}) - - res = self.send_receive_msg(msg, ignore_heartbeats=True) - - assert res["TYPE"] == "CTRL_generateTrip", res["TYPE"] - assert res["CODE"] == "OK", res["CODE"] - return res - - # generate a vehicle trip between origin and destination roads - def generate_trip_between_roads(self, vehID, origin, destination): - msg = {"TYPE": "CTRL_genTripBwRoads", "DATA": []} - if not isinstance(vehID, list): - vehID = [vehID] - if not isinstance(origin, list): - origin = [origin] * len(vehID) - if not isinstance(destination, list): - destination = [destination] * len(vehID) - - assert len(vehID) == len(origin) == len(destination), "Length of vehID, origin, and destination must be the same" - for vehID, origin, destination in zip(vehID, origin, destination): - msg["DATA"].append({"vehID": vehID, "orig": origin, "dest": destination}) - - res = self.send_receive_msg(msg, ignore_heartbeats=True) - - assert res["TYPE"] == "CTRL_genTripBwRoads", res["TYPE"] - assert res["CODE"] == "OK", res["CODE"] - return res - - - # set the road for co-simulation - def set_cosim_road(self, roadID): # IF a road or METSR road ID is here, the MetsR DOES NOT control it - msg = { - "TYPE": "CTRL_setCoSimRoad", - "DATA": [] - } - if not isinstance(roadID, list): - roadID = [roadID] - for i in roadID: - msg['DATA'].append(i) - res = self.send_receive_msg(msg, ignore_heartbeats=True) - assert res["TYPE"] == "CTRL_setCoSimRoad", res["TYPE"] - assert res["CODE"] == "OK", res["CODE"] - return res - - # release the road for co-simulation - def release_cosim_road(self, roadID): - msg = { - "TYPE": "CTRL_releaseCosimRoad", - "DATA": [] - } - if not isinstance(roadID, list): - roadID = [roadID] - for i in roadID: - msg['DATA'].append(i) - res = self.send_receive_msg(msg, ignore_heartbeats=True) - assert res["TYPE"] == "CTRL_releaseCosimRoad", res["TYPE"] - assert res["CODE"] == "OK", res["CODE"] - return res - - # teleport vehicle to a target location specified by road and coordiantes, only work when the road is a cosim road - def teleport_cosim_vehicle(self, vehID, roadID, x, y, private_veh = False, transform_coords = False): - msg = { - "TYPE": "CTRL_teleportCoSimVeh", - "DATA": [] - } - if not isinstance(vehID, list): - vehID = [vehID] - roadID = [roadID] - x = [x] - y = [y] - if not isinstance(private_veh, list): - private_veh = [private_veh] * len(vehID) - if not isinstance(transform_coords, list): - transform_coords = [transform_coords] * len(vehID) - for vehID, roadID, x, y, private_veh, transform_coords in zip(vehID, roadID, x, y, private_veh, transform_coords): - msg["DATA"].append({"vehID": vehID, "roadID": roadID, "x": x, "y": y, "vehType": private_veh, "transformCoord": transform_coords}) - res = self.send_receive_msg(msg, ignore_heartbeats=True) - assert res["TYPE"] == "CTRL_teleportCoSimVeh", res["TYPE"] - assert res["CODE"] == "OK", res["CODE"] - return res - - # teleport vehicle to a target location specified by road, lane, and distance to the downstream junction - def teleport_trace_replay_vehicle(self, vehID, roadID, laneID, dist, private_veh = False): - msg = { - "TYPE": "CTRL_teleportTraceReplayVeh", - "DATA": [] - } - if not isinstance(vehID, list): - vehID = [vehID] - roadID = [roadID] - laneID = [laneID] - dist = [dist] - if not isinstance(private_veh, list): - private_veh = [private_veh] * len(vehID) - for vehID, roadID, laneID, dist, private_veh in zip(vehID, roadID, laneID, dist, private_veh): - msg["DATA"].append({"vehID": vehID, "roadID": roadID, "laneID": laneID, "dist": dist, "vehType": private_veh}) - res = self.send_receive_msg(msg, ignore_heartbeats=True) - assert res["TYPE"] == "CTRL_teleportTraceReplayVeh", res["TYPE"] - assert res["CODE"] == "OK", res["CODE"] - return res - - # enter the next road - def enter_next_road(self, vehID, private_veh = False): - msg = { - "TYPE": "CTRL_enterNextRoad", - "DATA": [] - } - if not isinstance(vehID, list): - vehID = [vehID] - if not isinstance(private_veh, list): - private_veh = [private_veh] * len(vehID) - - for vehID, private_veh in zip(vehID, private_veh): - msg["DATA"].append({"vehID": vehID, "vehType": private_veh}) - - res = self.send_receive_msg(msg, ignore_heartbeats=True) - assert res["TYPE"] == "CTRL_enterNextRoad", res["TYPE"] - assert res["CODE"] == "OK", res["CODE"] - return res - - # control vehicle with specified acceleration - def control_vehicle(self, vehID, acc, private_veh = False): - msg = { - "TYPE": "CTRL_controlVeh", - "DATA": [] - } - if not isinstance(vehID, list): - vehID = [vehID] - acc = [acc] - if not isinstance(private_veh, list): - private_veh = [private_veh] * len(vehID) - for vehID, acc, private_veh in zip(vehID, acc, private_veh): - msg["DATA"].append({"vehID": vehID, "vehType": private_veh, "acc": acc}) - res = self.send_receive_msg(msg, ignore_heartbeats=True) - assert res["TYPE"] == "CTRL_controlVeh", res["TYPE"] - assert res["CODE"] == "OK", res["CODE"] - return res - - # update the sensor type of specified vehicle - def update_vehicle_sensor_type(self, vehID, sensorType, private_veh = False): - msg = { - "TYPE": "CTRL_updateVehicleSensorType", - "DATA": [] - } - if not isinstance(vehID, list): - vehID = [vehID] - if not isinstance(private_veh, list): - private_veh = [private_veh] * len(vehID) - if not isinstance(sensorType, list): - sensorType = [sensorType] * len(vehID) - for vehID, sensorType, private_veh in zip(vehID, sensorType, private_veh): - msg["DATA"].append({"vehID": vehID, "sensorType": sensorType, "vehType": private_veh}) - res = self.send_receive_msg(msg, ignore_heartbeats=True) - assert res["TYPE"] == "CTRL_updateVehicleSensorType", res["TYPE"] - assert res["CODE"] == "OK", res["CODE"] - return res - - # dispatch taxi - def dispatch_taxi(self, vehID, orig, dest, num): - msg = { - "TYPE": "CTRL_dispatchTaxi", - "DATA": [] - } - if not isinstance(vehID, list): - vehID = [vehID] - if not isinstance(orig, list): - orig = [orig] * len(vehID) - if not isinstance(dest, list): - dest = [dest] * len(vehID) - if not isinstance(num, list): - num = [num] * len(vehID) - - for vehID, orig, dest, num in zip(vehID, orig, dest, num): - msg["DATA"].append({"vehID": vehID, "orig": orig, "dest": dest, "num": num}) - res = self.send_receive_msg(msg, ignore_heartbeats=True) - assert res["TYPE"] == "CTRL_dispatchTaxi", res["TYPE"] - assert res["CODE"] == "OK", res["CODE"] - return res - - def dispatch_taxi_between_roads(self, vehID, orig, dest, num): - msg = { - "TYPE": "CTRL_dispTaxiBwRoads", - "DATA": [] - } - if not isinstance(vehID, list): - vehID = [vehID] - if not isinstance(orig, list): - orig = [orig] * len(vehID) - if not isinstance(dest, list): - dest = [dest] * len(vehID) - if not isinstance(num, list): - num = [num] * len(vehID) - - for vehID, orig, dest, num in zip(vehID, orig, dest, num): - msg["DATA"].append({"vehID": vehID, "orig": orig, "dest": dest, "num": num}) - res = self.send_receive_msg(msg, ignore_heartbeats=True) - assert res["TYPE"] == "CTRL_dispTaxiBwRoads", res["TYPE"] - assert res["CODE"] == "OK", res["CODE"] - return res - - def add_taxi_requests(self, zoneID, dest, num): - msg = { - "TYPE": "CTRL_addTaxiRequests", - "DATA": [] - } - if not isinstance(zoneID, list): - zoneID = [zoneID] - if not isinstance(dest, list): - dest = [dest] * len(zoneID) - if not isinstance(num, list): - num = [num] * len(zoneID) - - for zoneID, dest, num in zip(zoneID, dest, num): - msg["DATA"].append({"zoneID": zoneID, "dest": dest, "num": num}) - res = self.send_receive_msg(msg, ignore_heartbeats=True) - assert res["TYPE"] == "CTRL_addTaxiRequests", res["TYPE"] - assert res["CODE"] == "OK", res["CODE"] - return res - - def add_taxi_requests_between_roads(self, zoneID, orig, dest, num): - msg = { - "TYPE": "CTRL_addTaxiReqBwRoads", - "DATA": [] - } - if not isinstance(orig, list): - orig = [orig] - if not isinstance(zoneID, list): - zoneID = [zoneID] * len(orig) - if not isinstance(dest, list): - dest = [dest] * len(zoneID) - if not isinstance(num, list): - num = [num] * len(zoneID) - - for zoneID, orig, dest, num in zip(zoneID, orig, dest, num): - msg["DATA"].append({"zoneID": zoneID, "orig": orig, "dest": dest, "num": num}) - res = self.send_receive_msg(msg, ignore_heartbeats=True) - assert res["TYPE"] == "CTRL_addTaxiReqBwRoads", res["TYPE"] - assert res["CODE"] == "OK", res["CODE"] - return res - - - # assign bus - def assign_request_to_bus(self, vehID, orig, dest, num): - msg = { - "TYPE": "CTRL_assignRequestToBus", - "DATA": [] - } - if not isinstance(vehID, list): - vehID = [vehID] - if not isinstance(orig, list): - orig = [orig] * len(vehID) - if not isinstance(dest, list): - dest = [dest] * len(vehID) - if not isinstance(num, list): - num = [num] * len(vehID) - - for vehID, orig, dest, num in zip(vehID, orig, dest, num): - msg["DATA"].append({"vehID": vehID, "orig": orig, "dest": dest, "num": num}) - res = self.send_receive_msg(msg, ignore_heartbeats=True) - assert res["TYPE"] == "CTRL_assignRequestToBus", res["TYPE"] - assert res["CODE"] == "OK", res["CODE"] - return res - - def add_bus_requests(self, zoneID, dest, num): - msg = { - "TYPE": "CTRL_addBusRequests", - "DATA": [] - } - if not isinstance(zoneID, list): - zoneID = [zoneID] - if not isinstance(dest, list): - dest = [dest] * len(zoneID) - if not isinstance(num, list): - num = [num] * len(zoneID) - - for zoneID, dest, num in zip(zoneID, dest, num): - msg["DATA"].append({"zoneID": zoneID, "dest": dest, "num": num}) - res = self.send_receive_msg(msg, ignore_heartbeats=True) - assert res["TYPE"] == "CTRL_addBusRequests", res["TYPE"] - assert res["CODE"] == "OK", res["CODE"] - return res - - - # reset the simulation with a property file - def reset(self, prop_file): - msg = {"TYPE": "CTRL_reset", "propertyFile": prop_file} - res = self.send_receive_msg(msg, ignore_heartbeats=True, max_attempts=-1) - - assert res["TYPE"] == "CTRL_reset", res["TYPE"] - assert res["CODE"] == "OK", res["CODE"] - - self.current_tick = -1 - self.tick() - assert self.current_tick == 0 - - # if viz is running, stop and restart it - if self.viz_server is not None: - self.stop_viz() - - time.sleep(1) # wait for five secs if start viz - - self.start_viz() - - # reset the simulation with a map name - def reset_map(self, map_name): - # find the property file for the map - if map_name == "CARLA": - # copy CARLA data in the sim folder - # source_path = "data/CARLA" - # specify the property file - prop_file = "Data.properties.CARLA" - elif map_name == "NYC": - # copy NYC data in the sim folder - # source_path = "data/NYC" - # specify the property file - prop_file = "Data.properties.NYC" - elif map_name == "UA": - # copy UA data in the sim folder - # source_path = "data/UA" - # specify the property file - prop_file = "Data.properties.UA" - - # docker_cp_command = f"docker cp {source_path} {self.docker_id}:/home/test/data/" - # subprocess.run(docker_cp_command, shell=True, check=True) - - # reset the simulation with the property file - self.reset(prop_file) - - # terminate the simulation - def terminate(self): - msg = {"TYPE": "CTRL_end"} - res = self.send_receive_msg(msg, ignore_heartbeats=True) - assert res["TYPE"] == "CTRL_end", res["TYPE"] - assert res["CODE"] == "OK", res["CODE"] - self.close() - - # close the client but keep the simulator running - def close(self): - if self.ws is not None: - self.ws.close() - self.ws = None - self.state = "closed" - - if self.viz_server is not None: - self.stop_viz() - - - # open visualization server - def start_viz(self): - # obtain the latest directory in the sim_folder/trajectory_output - # get the latest directory - list_of_files = [os.path.join(self.sim_folder + "/trajectory_output", f) for f in os.listdir(self.sim_folder + "/trajectory_output")] - # sort the list of files by creation time - latest_directory = max(list_of_files, key=os.path.getmtime) - # open the visualization server - self.viz_event, self.viz_server = run_visualization_server(latest_directory) - - def stop_viz(self): - if self.viz_server is not None: - stop_visualization_server(self.viz_event, self.viz_server) - self.viz_event = None - self.viz_server = None - - def _logMessage(self, direction, msg): - self._messagesLog.append( - (datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), direction, tuple(msg.items())) - ) - - # override __str__ for logging - def __str__(self): - s = f"-----------\n" \ - f"Client INFO\n" \ - f"-----------\n" \ - f"output folder :\t {self.sim_folder}\n" \ - f"address :\t {self.uri}\n" \ - f"state :\t {self.state}\n" - return s \ No newline at end of file +import datetime +import json +import time +import threading + + +from websockets.sync.client import connect + + +""" +Implementation of the remote data client + +A client directly communicates with a specific METSR-SIM server. + +Acknowledgement: Eric Vin for helping with the revision of the code +""" + +# 2. listerize the query and control function by adding a for loop (is list, go for list, otherwise make it a list with one element) + +class METSRClient: + + def __init__(self, host, port, sim_folder = None, manager = None, max_connection_attempts = 5, timeout = 30, verbose = False): + super().__init__() + + # Websocket config + self.host = host + self.port = port + self.uri = f"ws://{host}:{port}" + + self.sim_folder = sim_folder # this is required for open the visualization server + self.state = "connecting" + self.timeout = timeout # time out for resending the same message if no response + self.verbose = verbose + self._messagesLog = [] + + # a pointer to the manager, for HPC usage that one manager controls multiple clients + self.manager = manager + + # visualization server and event + self.viz_server = None + self.viz_event = None + + # Track the tick of the corresponding simulator + self.current_tick = None + + # Establish connection + failed_attempts = 0 + while True: + try: + time.sleep(10) + self.ws = connect(self.uri, max_size = 10 * 1024 * 1024, ping_interval = None, ping_timeout = None) + self.state = "connected" + if self.verbose: + print(f"Connected to {self.uri}") + break + except ConnectionRefusedError: + print(f"Attempt to connect to {self.uri} failed. " + f"Waiting for 10 seconds before trying again... " + f"({max_connection_attempts - failed_attempts} attempts remaining)") + failed_attempts += 1 + if failed_attempts >= max_connection_attempts: + self.state = "failed" + raise RuntimeError("Could not connect to METS-R SIM") + + + print("Connection established!") + + # Ensure server is initialized by waiting to receive an initial packet + # (could be ANS_ready or a heartbeat) + self.receive_msg(ignore_heartbeats=False) + + self.lock = threading.Lock() + + def send_msg(self, msg): + if self.verbose: + self._logMessage("SENT", msg) + self.ws.send(json.dumps(msg)) + + def receive_msg(self, ignore_heartbeats, waiting_forever = True): + start_time = time.time() + while True: + try: + raw_msg = self.ws.recv(timeout = 30) + + # Decode the json string + msg = json.loads(str(raw_msg)) + + if self.verbose: + self._logMessage("RECEIVED", msg) + + # EVERY decoded msg must have a TYPE field + assert "TYPE" in msg.keys(), "No type field in received message" + assert msg["TYPE"].split("_")[0] in {"STEP", "ANS", "CTRL", "ATK"}, "Uknown message type: " + str(msg["TYPE"]) + + # Allow tick() + if msg["TYPE"] in {"ANS_ready"}: + self.current_tick = 0 + continue + + # Return decoded message, if it's not an ignored heartbeat + if not ignore_heartbeats or msg["TYPE"] != "STEP": + return msg + except: + pass + + if time.time() - start_time > self.timeout and not waiting_forever: + print("Timeout while waiting for message.") + return None + + def send_receive_msg(self, msg, ignore_heartbeats, max_attempts=5): + with self.lock: + res = None + num_attempts = 0 + try: + while res is None: + num_attempts += 1 + self.send_msg(msg) + if(max_attempts > 0): + res = self.receive_msg(ignore_heartbeats=ignore_heartbeats, waiting_forever=False) + if num_attempts >= max_attempts: + print(f"Failed to receive response after {max_attempts} attempts") + break + else: + res = self.receive_msg(ignore_heartbeats=ignore_heartbeats, waiting_forever=True) + except KeyboardInterrupt: + print("\nKeyboardInterrupt detected. Stopping the current operation but keeping the server active.") + # Reset state or resources if necessary to allow future operations + return None # Return None to indicate the operation was interrupted + except Exception as e: + print(f"An unexpected error occurred: {e}") + # Optional: Handle other types of exceptions if needed + return res + + def tick(self, step_num = 1, wait_forever = False): + assert self.current_tick is not None, "self.current_tick is None. Maybe there is another METS-R SIM instance unclosed." + msg = {"TYPE": "STEP", "TICK": self.current_tick, "NUM": step_num} + self.send_msg(msg) + + while True: + # Move through messages until we get to an up to date heartbeat + res = self.receive_msg(ignore_heartbeats=False, waiting_forever=wait_forever) + + assert res["TYPE"] == "STEP", res["TYPE"] + if res["TICK"] == self.current_tick + step_num: + break + + self.current_tick = res["TICK"] + + # QUERY: inspect the state of the simulator + # By default query public vehicles + def query_vehicle(self, id = None, private_veh = False, transform_coords = False): + msg = {"TYPE": "QUERY_vehicle"} + if id is not None: + msg["DATA"] = [] + if not isinstance(id, list): + id = [id] + if not isinstance(private_veh, list): + private_veh = [private_veh] * len(id) + if not isinstance(transform_coords, list): + transform_coords = [transform_coords] * len(id) + for veh_id, prv, tran in zip(id, private_veh, transform_coords): + msg["DATA"].append({"vehID": veh_id, "vehType": prv, "transformCoord": tran}) + + res = self.send_receive_msg(msg, ignore_heartbeats=True) + assert res["TYPE"] == "ANS_vehicle", res["TYPE"] + return res + + # query taxi + def query_taxi(self, id = None): + my_msg = {"TYPE": "QUERY_taxi"} + if id is not None: + my_msg['DATA'] = [] + if not isinstance(id, list): + id = [id] + for i in id: + my_msg['DATA'].append(i) + + res = self.send_receive_msg(my_msg, ignore_heartbeats=True) + assert res["TYPE"] == "ANS_taxi", res["TYPE"] + return res + + # query bus + def query_bus(self, id = None): + my_msg = {"TYPE": "QUERY_bus"} + if id is not None: + my_msg['DATA'] = [] + if not isinstance(id, list): + id = [id] + for i in id: + my_msg['DATA'].append(i) + res = self.send_receive_msg(my_msg, ignore_heartbeats=True) + assert res["TYPE"] == "ANS_bus", res["TYPE"] + return res + + + # query road + def query_road(self, id = None): + my_msg = {"TYPE": "QUERY_road"} + if id is not None: + my_msg['DATA'] = [] + if not isinstance(id, list): + id = [id] + for i in id: + my_msg['DATA'].append(i) + res = self.send_receive_msg(my_msg, ignore_heartbeats=True) + assert res["TYPE"] == "ANS_road", res["TYPE"] + return res + + # query centerline + def query_centerline(self, id, lane_index = -1, transform_coords = False): + my_msg = {"TYPE": "QUERY_centerLine"} + if id is not None: + my_msg['DATA'] = [] + if not isinstance(id, list): + id = [id] + if not isinstance(lane_index, list): + lane_index = [lane_index] * len(id) + if not isinstance(transform_coords, list): + transform_coords = [transform_coords] * len(id) + for i, lane_idx, tran in zip(id, lane_index, transform_coords): + my_msg['DATA'].append({"roadID": i, "laneIndex": lane_idx, "transformCoord": tran}) + else: + raise ValueError("id cannot be None for query_centerLine") + res = self.send_receive_msg(my_msg, ignore_heartbeats=True) + assert res["TYPE"] == "ANS_centerLine", res["TYPE"] + return res + + # query zone + def query_zone(self, id = None): + my_msg = {"TYPE": "QUERY_zone"} + if id is not None: + my_msg['DATA'] = [] + if not isinstance(id, list): + id = [id] + for i in id: + my_msg['DATA'].append(i) + res = self.send_receive_msg(my_msg, ignore_heartbeats=True) + assert res["TYPE"] == "ANS_zone", res["TYPE"] + return res + + # query signal + def query_signal(self, id = None): + my_msg = {"TYPE": "QUERY_signal"} + if id is not None: + my_msg['DATA'] = [] + if not isinstance(id, list): + id = [id] + for i in id: + my_msg['DATA'].append(i) + res = self.send_receive_msg(my_msg, ignore_heartbeats=True) + assert res["TYPE"] == "ANS_signal", res["TYPE"] + return res + + # query signal groups + def query_signal_group(self, id = None): + my_msg = {"TYPE": "QUERY_signalGroup"} + if id is not None: + my_msg['DATA'] = [] + if not isinstance(id, list): + id = [id] + for i in id: + my_msg['DATA'].append(i) + res = self.send_receive_msg(my_msg, ignore_heartbeats=True) + assert res["TYPE"] == "ANS_signalGroup", res["TYPE"] + return res + + # query signal for connection between two consecutive roads + def query_signal_between_roads(self, upstream_road, downstream_road): + msg = {"TYPE": "QUERY_signalForConnection", "DATA": []} + if not isinstance(upstream_road, list): + upstream_road = [upstream_road] + if not isinstance(downstream_road, list): + downstream_road = [downstream_road] * len(upstream_road) + assert len(upstream_road) == len(downstream_road), "Length of upstream_road and downstream_road must be the same" + + for up_road, down_road in zip(upstream_road, downstream_road): + msg["DATA"].append({"upStreamRoad": up_road, "downStreamRoad": down_road}) + + res = self.send_receive_msg(msg, ignore_heartbeats=True) + assert res["TYPE"] == "ANS_signalForConnection", res["TYPE"] + return res + + # query chargingStation + def query_chargingStation(self, id = None): + my_msg = {"TYPE": "QUERY_chargingStation"} + if id is not None: + my_msg['DATA'] = [] + if not isinstance(id, list): + id = [id] + for i in id: + my_msg['DATA'].append(i) + res = self.send_receive_msg(my_msg, ignore_heartbeats=True) + assert res["TYPE"] == "ANS_chargingStation", res["TYPE"] + return res + + # query vehicleID within the co-sim road + def query_coSimVehicle(self): + my_msg = {"TYPE": "QUERY_coSimVehicle"} + res = self.send_receive_msg(my_msg, ignore_heartbeats=True) + assert res["TYPE"] == "ANS_coSimVehicle", res["TYPE"] + return res + + # query route between coordinates + def query_route(self, orig_x, orig_y, dest_x, dest_y, transform_coords = False): + msg = {"TYPE": "QUERY_routesBwCoords", "DATA": []} + if not isinstance(orig_x, list): + orig_x = [orig_x] + orig_y = [orig_y] + dest_x = [dest_x] + dest_y = [dest_y] + + if not isinstance(transform_coords, list): + transform_coords = [transform_coords] * len(orig_x) + + assert len(orig_x) == len(orig_y) == len(dest_x) == len(dest_y), "Length of orig_x, orig_y, dest_x, and dest_y must be the same" + + for orig_x, orig_y, dest_x, dest_y, transform_coord in zip(orig_x, orig_y, dest_x, dest_y, transform_coords): + msg["DATA"].append({"origX": orig_x, "origY": orig_y, "destX": dest_x, "destY": dest_y, "transformCoord": transform_coord}) + + res = self.send_receive_msg(msg, ignore_heartbeats=True) + + assert res["TYPE"] == "ANS_routesBwCoords", res["TYPE"] + return res + + # query route between roads + def query_route_between_roads(self, orig_road, dest_road): + msg = {"TYPE": "QUERY_routesBwRoads", "DATA": []} + if not isinstance(orig_road, list): + orig_road = [orig_road] + + if not isinstance(dest_road, list): + dest_road = [dest_road] * len(orig_road) + assert len(orig_road) == len(dest_road), "Length of orig_road and dest_road must be the same" + + for orig_road, dest_road in zip(orig_road, dest_road): + msg["DATA"].append({"orig": orig_road, "dest": dest_road}) + + res = self.send_receive_msg(msg, ignore_heartbeats=True) + + assert res["TYPE"] == "ANS_routesBwRoads", res["TYPE"] + return res + + # query road weights in the routing map + def query_road_weights(self, roadID = None): + msg = {"TYPE": "QUERY_edgeWeight"} + if roadID is not None: + msg["DATA"] = [] + if not isinstance(roadID, list): + roadID = [roadID] + for i in roadID: + msg["DATA"].append(i) + res = self.send_receive_msg(msg, ignore_heartbeats=True) + assert res["TYPE"] == "ANS_edgeWeight", res["TYPE"] + return res + + # query bus route + def query_bus_route(self, routeID = None): + msg = {"TYPE": "QUERY_busRoute"} + if routeID is not None: + msg["DATA"] = [] + if not isinstance(routeID, list): + routeID = [routeID] + for i in routeID: + msg["DATA"].append(i) + res = self.send_receive_msg(msg, ignore_heartbeats=True) + assert res["TYPE"] == "ANS_busRoute", res["TYPE"] + return res + + # find bus with route + def query_route_bus(self, routeID = None): + msg = {"TYPE": "QUERY_busWithRoute"} + if routeID is not None: + msg["DATA"] = [] + if not isinstance(routeID, list): + routeID = [routeID] + for i in routeID: + msg["DATA"].append(i) + res = self.send_receive_msg(msg, ignore_heartbeats=True) + assert res["TYPE"] == "ANS_busWithRoute", res["TYPE"] + return res + + # CONTROL: change the state of the simulator + # generate a vehicle trip between origin and destination zones + def generate_trip(self, vehID, origin = -1, destination = -1): + msg = {"TYPE": "CTRL_generateTrip", "DATA": []} + if not isinstance(vehID, list): + vehID = [vehID] + if not isinstance(origin, list): + origin = [origin] * len(vehID) + if not isinstance(destination, list): + destination = [destination] * len(vehID) + + assert len(vehID) == len(origin) == len(destination), "Length of vehID, origin, and destination must be the same" + for vehID, origin, destination in zip(vehID, origin, destination): + msg["DATA"].append({"vehID": vehID, "orig": origin, "dest": destination}) + + res = self.send_receive_msg(msg, ignore_heartbeats=True) + + assert res["TYPE"] == "CTRL_generateTrip", res["TYPE"] + assert res["CODE"] == "OK", res["CODE"] + return res + + # generate a vehicle trip between origin and destination roads + def generate_trip_between_roads(self, vehID, origin, destination): + msg = {"TYPE": "CTRL_genTripBwRoads", "DATA": []} + if not isinstance(vehID, list): + vehID = [vehID] + if not isinstance(origin, list): + origin = [origin] * len(vehID) + if not isinstance(destination, list): + destination = [destination] * len(vehID) + + assert len(vehID) == len(origin) == len(destination), "Length of vehID, origin, and destination must be the same" + for vehID, origin, destination in zip(vehID, origin, destination): + msg["DATA"].append({"vehID": vehID, "orig": origin, "dest": destination}) + + res = self.send_receive_msg(msg, ignore_heartbeats=True) + + assert res["TYPE"] == "CTRL_genTripBwRoads", res["TYPE"] + assert res["CODE"] == "OK", res["CODE"] + return res + + + # set the road for co-simulation + def set_cosim_road(self, roadID): + msg = { + "TYPE": "CTRL_setCoSimRoad", + "DATA": [] + } + if not isinstance(roadID, list): + roadID = [roadID] + for i in roadID: + msg['DATA'].append(i) + res = self.send_receive_msg(msg, ignore_heartbeats=True) + assert res["TYPE"] == "CTRL_setCoSimRoad", res["TYPE"] + assert res["CODE"] == "OK", res["CODE"] + return res + + # release the road for co-simulation + def release_cosim_road(self, roadID): + msg = { + "TYPE": "CTRL_releaseCosimRoad", + "DATA": [] + } + if not isinstance(roadID, list): + roadID = [roadID] + for i in roadID: + msg['DATA'].append(i) + res = self.send_receive_msg(msg, ignore_heartbeats=True) + assert res["TYPE"] == "CTRL_releaseCosimRoad", res["TYPE"] + assert res["CODE"] == "OK", res["CODE"] + return res + + # teleport vehicle to a target location specified by road and coordiantes, only work when the road is a cosim road + def teleport_cosim_vehicle(self, vehID, x, y, bearing, private_veh = False, transform_coords = False): + msg = { + "TYPE": "CTRL_teleportCoSimVeh", + "DATA": [] + } + if not isinstance(vehID, list): + vehID = [vehID] + x = [x] + y = [y] + bearing = [bearing] + if not isinstance(bearing, list): + bearing = [bearing] * len(vehID) + if not isinstance(private_veh, list): + private_veh = [private_veh] * len(vehID) + if not isinstance(transform_coords, list): + transform_coords = [transform_coords] * len(vehID) + for vehID, x, y, bearing, private_veh, transform_coords in zip(vehID, x, y, bearing, private_veh, transform_coords): + msg["DATA"].append({"vehID": vehID, "x": x, "y": y, "bearing": bearing, "vehType": private_veh, "transformCoord": transform_coords}) + res = self.send_receive_msg(msg, ignore_heartbeats=True) + assert res["TYPE"] == "CTRL_teleportCoSimVeh", res["TYPE"] + assert res["CODE"] == "OK", res["CODE"] + return res + + # teleport vehicle to a target location specified by road, lane, and distance to the downstream junction + def teleport_trace_replay_vehicle(self, vehID, roadID, laneID, dist, private_veh = False): + msg = { + "TYPE": "CTRL_teleportTraceReplayVeh", + "DATA": [] + } + if not isinstance(vehID, list): + vehID = [vehID] + roadID = [roadID] + laneID = [laneID] + dist = [dist] + if not isinstance(private_veh, list): + private_veh = [private_veh] * len(vehID) + for vehID, roadID, laneID, dist, private_veh in zip(vehID, roadID, laneID, dist, private_veh): + msg["DATA"].append({"vehID": vehID, "roadID": roadID, "laneID": laneID, "dist": dist, "vehType": private_veh}) + res = self.send_receive_msg(msg, ignore_heartbeats=True) + assert res["TYPE"] == "CTRL_teleportTraceReplayVeh", res["TYPE"] + assert res["CODE"] == "OK", res["CODE"] + return res + + # enter the next road + def enter_next_road(self, vehID, roadID="", private_veh = False): + msg = { + "TYPE": "CTRL_enterNextRoad", + "DATA": [] + } + if not isinstance(vehID, list): + vehID = [vehID] + if not isinstance(private_veh, list): + private_veh = [private_veh] * len(vehID) + if not isinstance(roadID, list): + roadID = [roadID] * len(vehID) + + for vehID, private_veh, roadID in zip(vehID, private_veh, roadID): + msg["DATA"].append({"vehID": vehID, "vehType": private_veh, "roadID": roadID}) + + res = self.send_receive_msg(msg, ignore_heartbeats=True) + assert res["TYPE"] == "CTRL_enterNextRoad", res["TYPE"] + assert res["CODE"] == "OK", res["CODE"] + return res + + # reach destination + def reach_dest(self, vehID, private_veh = False): + msg = { + "TYPE": "CTRL_reachDest", + "DATA": [] + } + if not isinstance(vehID, list): + vehID = [vehID] + if not isinstance(private_veh, list): + private_veh = [private_veh] * len(vehID) + + for vehID, private_veh in zip(vehID, private_veh): + msg["DATA"].append({"vehID": vehID, "vehType": private_veh}) + + res = self.send_receive_msg(msg, ignore_heartbeats=True) + assert res["TYPE"] == "CTRL_reachDest", res["TYPE"] + assert res["CODE"] == "OK", res["CODE"] + return res + + # control vehicle with specified acceleration + def control_vehicle(self, vehID, acc, private_veh = False): + msg = { + "TYPE": "CTRL_controlVeh", + "DATA": [] + } + if not isinstance(vehID, list): + vehID = [vehID] + acc = [acc] + if not isinstance(private_veh, list): + private_veh = [private_veh] * len(vehID) + for vehID, acc, private_veh in zip(vehID, acc, private_veh): + msg["DATA"].append({"vehID": vehID, "vehType": private_veh, "acc": acc}) + res = self.send_receive_msg(msg, ignore_heartbeats=True) + assert res["TYPE"] == "CTRL_controlVeh", res["TYPE"] + assert res["CODE"] == "OK", res["CODE"] + return res + + # update the sensor type of specified vehicle + def update_vehicle_sensor_type(self, vehID, sensorType, private_veh = False): + msg = { + "TYPE": "CTRL_updateVehicleSensorType", + "DATA": [] + } + if not isinstance(vehID, list): + vehID = [vehID] + if not isinstance(private_veh, list): + private_veh = [private_veh] * len(vehID) + if not isinstance(sensorType, list): + sensorType = [sensorType] * len(vehID) + for vehID, sensorType, private_veh in zip(vehID, sensorType, private_veh): + msg["DATA"].append({"vehID": vehID, "sensorType": sensorType, "vehType": private_veh}) + res = self.send_receive_msg(msg, ignore_heartbeats=True) + assert res["TYPE"] == "CTRL_updateVehicleSensorType", res["TYPE"] + assert res["CODE"] == "OK", res["CODE"] + return res + + # dispatch taxi + def dispatch_taxi(self, vehID, orig, dest, num): + msg = { + "TYPE": "CTRL_dispatchTaxi", + "DATA": [] + } + if not isinstance(vehID, list): + vehID = [vehID] + if not isinstance(orig, list): + orig = [orig] * len(vehID) + if not isinstance(dest, list): + dest = [dest] * len(vehID) + if not isinstance(num, list): + num = [num] * len(vehID) + + for vehID, orig, dest, num in zip(vehID, orig, dest, num): + msg["DATA"].append({"vehID": vehID, "orig": orig, "dest": dest, "num": num}) + res = self.send_receive_msg(msg, ignore_heartbeats=True) + assert res["TYPE"] == "CTRL_dispatchTaxi", res["TYPE"] + assert res["CODE"] == "OK", res["CODE"] + return res + + def dispatch_taxi_between_roads(self, vehID, orig, dest, num): + msg = { + "TYPE": "CTRL_dispTaxiBwRoads", + "DATA": [] + } + if not isinstance(vehID, list): + vehID = [vehID] + if not isinstance(orig, list): + orig = [orig] * len(vehID) + if not isinstance(dest, list): + dest = [dest] * len(vehID) + if not isinstance(num, list): + num = [num] * len(vehID) + + for vehID, orig, dest, num in zip(vehID, orig, dest, num): + msg["DATA"].append({"vehID": vehID, "orig": orig, "dest": dest, "num": num}) + res = self.send_receive_msg(msg, ignore_heartbeats=True) + assert res["TYPE"] == "CTRL_dispTaxiBwRoads", res["TYPE"] + assert res["CODE"] == "OK", res["CODE"] + return res + + def add_taxi_requests(self, zoneID, dest, num): + msg = { + "TYPE": "CTRL_addTaxiRequests", + "DATA": [] + } + if not isinstance(zoneID, list): + zoneID = [zoneID] + if not isinstance(dest, list): + dest = [dest] * len(zoneID) + if not isinstance(num, list): + num = [num] * len(zoneID) + + for zoneID, dest, num in zip(zoneID, dest, num): + msg["DATA"].append({"zoneID": zoneID, "dest": dest, "num": num}) + res = self.send_receive_msg(msg, ignore_heartbeats=True) + assert res["TYPE"] == "CTRL_addTaxiRequests", res["TYPE"] + assert res["CODE"] == "OK", res["CODE"] + return res + + def add_taxi_requests_between_roads(self, orig, dest, num): + msg = { + "TYPE": "CTRL_addTaxiReqBwRoads", + "DATA": [] + } + if not isinstance(orig, list): + orig = [orig] + if not isinstance(dest, list): + dest = [dest] * len(orig) + if not isinstance(num, list): + num = [num] * len(orig) + + for orig, dest, num in zip(orig, dest, num): + msg["DATA"].append({"orig": orig, "dest": dest, "num": num}) + res = self.send_receive_msg(msg, ignore_heartbeats=True) + assert res["TYPE"] == "CTRL_addTaxiReqBwRoads", res["TYPE"] + assert res["CODE"] == "OK", res["CODE"] + return res + + # assign bus + def add_bus_route(self, routeName, zone, road, paths = None): + if paths is None: + msg = { + "TYPE": "CTRL_addBusRoute", + "DATA": [] + } + else: + msg = { + "TYPE": "CTRL_addBusRouteWithPath", + "DATA": [] + } + if not isinstance(routeName, list): + routeName = [routeName] + zone = [zone] + road = [road] + if paths != None: # TODO -- type 'path' + paths = [paths] + if paths is None: + for routeName, zone, road, paths in zip(routeName, zone, road, paths): + msg["DATA"].append({"routeName": routeName, "zones": zone, "roads": road}) + else: + for routeName, zone, road, paths in zip(routeName, zone, road, paths): + msg["DATA"].append({"routeName": routeName, "zones": zone, "roads": road, "paths": paths}) + res = self.send_receive_msg(msg, ignore_heartbeats=True) + + if paths is None: + assert res["TYPE"] == "CTRL_addBusRoute", res["TYPE"] + else: + assert res["TYPE"] == "CTRL_addBusRouteWithPath", res["TYPE"] + assert res["CODE"] == "OK", res["CODE"] + return res + + def add_bus_run(self, routeName, departTime): + msg = { + "TYPE": "CTRL_addBusRun", + "DATA": [] + } + if not isinstance(routeName, list): + routeName = [routeName] + departTime = [departTime] + + for routeName, departTime in zip(routeName, departTime): + msg["DATA"].append({"routeName": routeName, "departTime": departTime}) + res = self.send_receive_msg(msg, ignore_heartbeats=True) + assert res["TYPE"] == "CTRL_addBusRun", res["TYPE"] + assert res["CODE"] == "OK", res["CODE"] + return res + + def insert_bus_stop(self, busID, routeName, zoneID, roadName, stopIndex): + msg = { + "TYPE": "CTRL_insertStopToRoute", + "DATA": [] + } + if not isinstance(busID, list): + busID = [busID] + routeName = [routeName] * len(busID) + zoneID = [zoneID] * len(busID) + roadName = [roadName] * len(busID) + stopIndex = [stopIndex] * len(busID) + + for busID, routeName, zoneID, roadName, stopIndex in zip(busID, routeName, zoneID, roadName, stopIndex): + msg["DATA"].append({"busID": busID, "routeName": routeName, "zone": zoneID, "road": roadName, "stopIndex": stopIndex}) + + res = self.send_receive_msg(msg, ignore_heartbeats=True) + assert res["TYPE"] == "CTRL_insertStopToRoute", res["TYPE"] + assert res["CODE"] == "OK", res["CODE"] + return res + + def remove_bus_stop(self, busID, routeName, stopIndex): + msg = { + "TYPE": "CTRL_removeStopFromRoute", + "DATA": [] + } + if not isinstance(busID, list): + busID = [busID] + routeName = [routeName] * len(busID) + stopIndex = [stopIndex] * len(busID) + + for busID, routeName, stopIndex in zip(busID, routeName, stopIndex): + msg["DATA"].append({"busID": busID, "routeName": routeName, "stopIndex": stopIndex}) + + res = self.send_receive_msg(msg, ignore_heartbeats=True) + assert res["TYPE"] == "CTRL_removeStopFromRoute", res["TYPE"] + assert res["CODE"] == "OK", res["CODE"] + return res + + + def assign_request_to_bus(self, vehID, orig, dest, num): + msg = { + "TYPE": "CTRL_assignRequestToBus", + "DATA": [] + } + if not isinstance(vehID, list): + vehID = [vehID] + if not isinstance(orig, list): + orig = [orig] * len(vehID) + if not isinstance(dest, list): + dest = [dest] * len(vehID) + if not isinstance(num, list): + num = [num] * len(vehID) + + for vehID, orig, dest, num in zip(vehID, orig, dest, num): + msg["DATA"].append({"vehID": vehID, "orig": orig, "dest": dest, "num": num}) + res = self.send_receive_msg(msg, ignore_heartbeats=True) + assert res["TYPE"] == "CTRL_assignRequestToBus", res["TYPE"] + assert res["CODE"] == "OK", res["CODE"] + return res + + def add_bus_requests(self, zoneID, dest, routeName, num): + msg = { + "TYPE": "CTRL_addBusRequests", + "DATA": [] + } + if not isinstance(zoneID, list): + zoneID = [zoneID] + if not isinstance(dest, list): + dest = [dest] * len(zoneID) + if not isinstance(num, list): + num = [num] * len(zoneID) + if not isinstance(routeName, list): + routeName = [routeName] * len(zoneID) + + for zoneID, dest, num, routeName in zip(zoneID, dest, num, routeName): + msg["DATA"].append({"zoneID": zoneID, "dest": dest, "num": num, "routeName": routeName}) + res = self.send_receive_msg(msg, ignore_heartbeats=True) + assert res["TYPE"] == "CTRL_addBusRequests", res["TYPE"] + assert res["CODE"] == "OK", res["CODE"] + return res + + # update vehicle route + def update_vehicle_route(self, vehID, route, private_veh = False): + msg = { + "TYPE": "CTRL_updateVehicleRoute", + "DATA": [] + } + if not isinstance(vehID, list): + vehID = [vehID] + route = [route] + if not isinstance(private_veh, list): + private_veh = [private_veh] * len(vehID) + + for vehID, route, private_veh in zip(vehID, route, private_veh): + msg["DATA"].append({"vehID": vehID, "route": route, "vehType": private_veh}) + res = self.send_receive_msg(msg, ignore_heartbeats=True) + assert res["TYPE"] == "CTRL_updateVehicleRoute", res["TYPE"] + assert res["CODE"] == "OK", res["CODE"] + return res + + # update road weights in the routing map + def update_road_weights(self, roadID, weight): + msg = {"TYPE": "CTRL_updateEdgeWeight", "DATA": []} + if not isinstance(roadID, list): + roadID = [roadID] + weight = [weight] + if not isinstance(weight, list): + weight = [weight] * len(roadID) + for roadID, weight in zip(roadID, weight): + msg["DATA"].append({"roadID": roadID, "weight": weight}) + res = self.send_receive_msg(msg, ignore_heartbeats=True) + assert res["TYPE"] == "CTRL_updateEdgeWeight", res["TYPE"] + assert res["CODE"] == "OK", res["CODE"] + return res + + # update charging station prices + def update_charging_prices(self, stationID, stationType, price): + msg = {"TYPE": "CTRL_updateChargingPrice", "DATA": []} + if not isinstance(stationID, list): + stationID = [stationID] + stationType = [stationType] + price = [price] + if not isinstance(stationType, list): + stationType = [stationType] * len(stationID) + if not isinstance(price, list): + price = [price] * len(stationID) + for stationID, stationType, price in zip(stationID, stationType, price): + msg["DATA"].append({"chargerID": stationID, "chargerType": stationType, "weight": price}) + res = self.send_receive_msg(msg, ignore_heartbeats=True) + assert res["TYPE"] == "CTRL_updateChargingPrice", res["TYPE"] + assert res["CODE"] == "OK", res["CODE"] + return res + + # Traffic signal phase control + # Update the signal phase given signal ID and target phase (optionally with phase time offset) + # If only phase is provided, starts from the beginning of that phase (phaseTime = 0) + def update_signal(self, signalID, targetPhase, phaseTime = None): + msg = {"TYPE": "CTRL_updateSignal", "DATA": []} + if not isinstance(signalID, list): + signalID = [signalID] + targetPhase = [targetPhase] + if not isinstance(targetPhase, list): + targetPhase = [targetPhase] * len(signalID) + if phaseTime is None: + phaseTime = [None] * len(signalID) + elif not isinstance(phaseTime, list): + phaseTime = [phaseTime] * len(signalID) + else: + # If phaseTime is a list, ensure it matches the length + if len(phaseTime) != len(signalID): + phaseTime = phaseTime * (len(signalID) // len(phaseTime) + 1) + phaseTime = phaseTime[:len(signalID)] + + assert len(signalID) == len(targetPhase) == len(phaseTime), "Length of signalID, targetPhase, and phaseTime must be the same" + + for sig_id, tgt_phase, ph_time in zip(signalID, targetPhase, phaseTime): + signal_data = {"signalID": sig_id, "targetPhase": tgt_phase} + if ph_time is not None: + signal_data["phaseTime"] = ph_time + msg["DATA"].append(signal_data) + + res = self.send_receive_msg(msg, ignore_heartbeats=True) + assert res["TYPE"] == "CTRL_updateSignal", res["TYPE"] + return res + + # Update signal phase timing (green, yellow, red durations) + def update_signal_timing(self, signalID, greenTime, yellowTime, redTime): + msg = {"TYPE": "CTRL_updateSignalTiming", "DATA": []} + if not isinstance(signalID, list): + signalID = [signalID] + greenTime = [greenTime] + yellowTime = [yellowTime] + redTime = [redTime] + if not isinstance(greenTime, list): + greenTime = [greenTime] * len(signalID) + if not isinstance(yellowTime, list): + yellowTime = [yellowTime] * len(signalID) + if not isinstance(redTime, list): + redTime = [redTime] * len(signalID) + + assert len(signalID) == len(greenTime) == len(yellowTime) == len(redTime), "Length of signalID, greenTime, yellowTime, and redTime must be the same" + + for sig_id, green, yellow, red in zip(signalID, greenTime, yellowTime, redTime): + msg["DATA"].append({"signalID": sig_id, "greenTime": green, "yellowTime": yellow, "redTime": red}) + + res = self.send_receive_msg(msg, ignore_heartbeats=True) + assert res["TYPE"] == "CTRL_updateSignalTiming", res["TYPE"] + return res + + # Set a complete new phase plan for a signal (phase timing + starting state + offset) + # Time values are in seconds + def set_signal_phase_plan(self, signalID, greenTime, yellowTime, redTime, startPhase, phaseOffset = None): + msg = {"TYPE": "CTRL_setSignalPhasePlan", "DATA": []} + if not isinstance(signalID, list): + signalID = [signalID] + greenTime = [greenTime] + yellowTime = [yellowTime] + redTime = [redTime] + startPhase = [startPhase] + if not isinstance(greenTime, list): + greenTime = [greenTime] * len(signalID) + if not isinstance(yellowTime, list): + yellowTime = [yellowTime] * len(signalID) + if not isinstance(redTime, list): + redTime = [redTime] * len(signalID) + if not isinstance(startPhase, list): + startPhase = [startPhase] * len(signalID) + if phaseOffset is None: + phaseOffset = [None] * len(signalID) + elif not isinstance(phaseOffset, list): + phaseOffset = [phaseOffset] * len(signalID) + else: + # If phaseOffset is a list, ensure it matches the length + if len(phaseOffset) != len(signalID): + phaseOffset = phaseOffset * (len(signalID) // len(phaseOffset) + 1) + phaseOffset = phaseOffset[:len(signalID)] + + assert len(signalID) == len(greenTime) == len(yellowTime) == len(redTime) == len(startPhase) == len(phaseOffset), "Length of all parameters must match" + + for sig_id, green, yellow, red, start_phase, ph_offset in zip(signalID, greenTime, yellowTime, redTime, startPhase, phaseOffset): + signal_data = {"signalID": sig_id, "greenTime": green, "yellowTime": yellow, "redTime": red, "startPhase": start_phase} + if ph_offset is not None: + signal_data["phaseOffset"] = ph_offset + msg["DATA"].append(signal_data) + + res = self.send_receive_msg(msg, ignore_heartbeats=True) + assert res["TYPE"] == "CTRL_setSignalPhasePlan", res["TYPE"] + return res + + # Set a complete new phase plan with tick-level precision + # Time values are in simulation ticks for more precise control + def set_signal_phase_plan_ticks(self, signalID, greenTicks, yellowTicks, redTicks, startPhase, tickOffset = None): + msg = {"TYPE": "CTRL_setSignalPhasePlanTicks", "DATA": []} + if not isinstance(signalID, list): + signalID = [signalID] + greenTicks = [greenTicks] + yellowTicks = [yellowTicks] + redTicks = [redTicks] + startPhase = [startPhase] + if not isinstance(greenTicks, list): + greenTicks = [greenTicks] * len(signalID) + if not isinstance(yellowTicks, list): + yellowTicks = [yellowTicks] * len(signalID) + if not isinstance(redTicks, list): + redTicks = [redTicks] * len(signalID) + if not isinstance(startPhase, list): + startPhase = [startPhase] * len(signalID) + if tickOffset is None: + tickOffset = [None] * len(signalID) + elif not isinstance(tickOffset, list): + tickOffset = [tickOffset] * len(signalID) + else: + # If tickOffset is a list, ensure it matches the length + if len(tickOffset) != len(signalID): + tickOffset = tickOffset * (len(signalID) // len(tickOffset) + 1) + tickOffset = tickOffset[:len(signalID)] + + assert len(signalID) == len(greenTicks) == len(yellowTicks) == len(redTicks) == len(startPhase) == len(tickOffset), "Length of all parameters must match" + + for sig_id, green, yellow, red, start_phase, tck_offset in zip(signalID, greenTicks, yellowTicks, redTicks, startPhase, tickOffset): + signal_data = {"signalID": sig_id, "greenTicks": green, "yellowTicks": yellow, "redTicks": red, "startPhase": start_phase} + if tck_offset is not None: + signal_data["tickOffset"] = tck_offset + msg["DATA"].append(signal_data) + + res = self.send_receive_msg(msg, ignore_heartbeats=True) + assert res["TYPE"] == "CTRL_setSignalPhasePlanTicks", res["TYPE"] + return res + + + # reset the simulation with a property file + def reset(self): + msg = {"TYPE": "CTRL_reset"} + res = self.send_receive_msg(msg, ignore_heartbeats=True, max_attempts=-1) + + assert res["TYPE"] == "CTRL_reset", res["TYPE"] + assert res["CODE"] == "OK", res["CODE"] + + self.current_tick = -1 + self.tick() + assert self.current_tick == 0 + + # if viz is running, stop and restart it + if self.viz_server is not None: + self.stop_viz() + + time.sleep(1) # wait for five secs if start viz + + self.start_viz() + + # Deprecated: reset the simulation with a property file + # # reset the simulation with a map name + # def reset_map(self, map_name): + # # find the property file for the map + # if map_name == "CARLA": + # # copy CARLA data in the sim folder + # # source_path = "data/CARLA" + # # specify the property file + # prop_file = "Data.properties.CARLA" + # elif map_name == "NYC": + # # copy NYC data in the sim folder + # # source_path = "data/NYC" + # # specify the property file + # prop_file = "Data.properties.NYC" + # elif map_name == "UA": + # # copy UA data in the sim folder + # # source_path = "data/UA" + # # specify the property file + # prop_file = "Data.properties.UA" + + # # docker_cp_command = f"docker cp {source_path} {self.docker_id}:/home/test/data/" + # # subprocess.run(docker_cp_command, shell=True, check=True) + + # # reset the simulation with the property file + # self.reset(prop_file) + + # terminate the simulation + def terminate(self): + msg = {"TYPE": "CTRL_end"} + res = self.send_receive_msg(msg, ignore_heartbeats=True) + assert res["TYPE"] == "CTRL_end", res["TYPE"] + assert res["CODE"] == "OK", res["CODE"] + self.close() + + # close the client but keep the simulator running + def close(self): + if self.ws is not None: + self.ws.close() + self.ws = None + self.state = "closed" + + if self.viz_server is not None: + self.stop_viz() + + + + # override __str__ for logging + def __str__(self): + s = f"-----------\n" \ + f"Client INFO\n" \ + f"-----------\n" \ + f"output folder :\t {self.sim_folder}\n" \ + f"address :\t {self.uri}\n" \ + f"state :\t {self.state}\n" + return s From c4177f672de6f1424dc6926774bde12995f9592c Mon Sep 17 00:00:00 2001 From: Kay Date: Mon, 9 Mar 2026 16:04:28 -0700 Subject: [PATCH 44/73] signal map generation + utilities --- src/scenic/simulators/cosim/utils/utils.py | 253 ++++++++++++++------- 1 file changed, 173 insertions(+), 80 deletions(-) diff --git a/src/scenic/simulators/cosim/utils/utils.py b/src/scenic/simulators/cosim/utils/utils.py index f5d30412b..3c33a53ab 100644 --- a/src/scenic/simulators/cosim/utils/utils.py +++ b/src/scenic/simulators/cosim/utils/utils.py @@ -1,80 +1,173 @@ -import xml.etree.ElementTree as ET -import os -import numpy as np - -def generate_map(map): - try: - tree = ET.parse(map) - except FileNotFoundError: - print(f"Could not find map: {map} from {os.getcwd()}") - return {} - - root = tree.getroot() - mappings = {} - edges = root.iterfind("edge") - - for edge in edges: - lanes = edge.findall('lane') - - for lane in lanes: - metrs_lane = lane.attrib.get("id") - params = lane.findall('param') - - for param in params: - if param.get('key') == "origId": - orig_id = param.get('value') - orig_id = orig_id.split() - if isinstance(orig_id, list): - for id in orig_id: - mappings[id] = metrs_lane - else: - mappings[orig_id] = metrs_lane - - if mappings == {}: - print(f"An occured attempting to process map: {map}") - - return mappings - -def test_mapping(map, test_pairs): - mappings = generate_map(map) - for key,value in test_pairs.items(): - if key in mappings: - if value == mappings[key]: - print(f"key value pair {key,value} mapped correctly") - else: - print(f"expected {value} returned {mappings[key]}") - print(f"Value {mappings[key]} for key {key} was incorrect with actual value {value}") - else: - print(f"Failed on test case {key}, {value}") - - -def within_threshold_to(object, cars) -> bool: - # print(f"checking distance between obj: {object} and cars {[car.name for car in cars]}") - object_pos = np.array(object.position) - # obj_distances = [] - for car in cars: - threshold = 1.2 * object.length - dist = np.linalg.norm(np.array(car.position) - object_pos) - if dist < threshold: - return True - # obj_distances.append(dist) - return False - - - - -if __name__ == "__main__": - - map = "Town01.net.xml" - - # key value pairs where key == origID and value == lane id - town01_test_pairs = {"4_1": "4_2", "0_2 11_-2 8_2": "0_1", "8_-3 11_3 0_-3": "-8_0" } - - test_mapping(map, town01_test_pairs) - - map = "Town02.net.xml" - - # Randomly selected test cases from the file to check accuracy - town02_test_pairs = {"177_-1": ":132_3_0", "276_-3":":242_2_0", "1_-2 16_-2 12_2 3_-2 15_2": "-1_1"} - - test_mapping(map, town02_test_pairs) \ No newline at end of file +import xml.etree.ElementTree as ET +import os +import numpy as np +import carla + + +def generate_map(map): + try: + tree = ET.parse(map) + except FileNotFoundError: + print(f"Could not find map: {map} from {os.getcwd()}") + return {} + + root = tree.getroot() + lane_mappings = {} + edges = root.iterfind("edge") + + for edge in edges: + lanes = edge.findall('lane') + + for lane in lanes: + metrs_lane = lane.attrib.get("id") + params = lane.findall('param') + + for param in params: + if param.get('key') == "origId": + orig_id = param.get('value') + orig_id = orig_id.split() + if isinstance(orig_id, list): + for id in orig_id: + lane_mappings[id] = metrs_lane + else: + lane_mappings[orig_id] = metrs_lane + + if lane_mappings == {}: + print(f"An occured attempting to process map: {map}") + + return lane_mappings + +def generate_signal_map(map): + + try: + tree = ET.parse(map) + except FileNotFoundError: + print(f"Could not find map: {map} from {os.getcwd()}") + return {} + + root = tree.getroot() + signal_mappings = {} + + for tl in root.findall(".//tlLogic"): + tl_id = tl.attrib.get("id") + for param in tl.findall('param'): + + key = param.attrib.get("key","") + + if key.startswith("linkSignalID:"): + metsr_key = f"{tl_id}_{key.split(':')[1]}" + + values = param.attrib.get("value","").split() + if values != "": + signal_mappings[metsr_key] = values + + return signal_mappings + + +def test_mapping(map, test_pairs): + mappings = generate_map(map) + for key,value in test_pairs.items(): + if key in mappings: + if value == mappings[key]: + print(f"key value pair {key,value} mapped correctly") + else: + print(f"expected {value} returned {mappings[key]}") + print(f"Value {mappings[key]} for key {key} was incorrect with actual value {value}") + else: + print(f"Failed on test case {key}, {value}") + + +def within_threshold_to(object, cars, verbose=False) -> bool: + is_close = False + if verbose: + print(f"checking distance between obj: {object} and cars {[(car.name, car.position) for car in cars]}") + object_pos = np.array(object.position) + obj_distances = [] + for car in cars: + if car != object: + threshold = 1.2 * object.length + dist = np.linalg.norm(np.array(car.position) - object_pos) + if dist < threshold: + is_close=True + obj_distances.append(dist) + if verbose: + print(f"Distances were: {obj_distances}") + return is_close + +def get_metsr_rotation(carla_yaw): + """ + Invert carla_yaw = (bearing - 90) % 360 + to recover the original METSR compass bearing. + """ + # ensure 0 ≤ yaw < 360 + carla_yaw = carla_yaw % 360 + # invert the shift of -90° + return (carla_yaw + 90) % 360 + +def get_carla_light_state(light) -> dict: + light_state_dict = {'green_time':light.get_green_time(), + 'red_time': light.get_red_time(), + 'yellow_time':light.get_yellow_time(), + 'state' :light.get_state() } + + return light_state_dict + +def disable_carla_autopilot(self, obj) -> bool: + if hasattr(obj, 'carlaActor'): + if obj.carlaActor != None: + obj.carlaActor.set_autopilot(False) + return True + else: + return False + +def _snapToGround(world, location, blueprint): + """Mutates @location to have the same z-coordinate as the nearest waypoint in @world.""" + waypoint = world.get_map().get_waypoint(location) + # patch to avoid the spawn error issue with vehicles and walkers. + z_offset = 0 + if blueprint is not None and ("vehicle" in blueprint or "walker" in blueprint): + z_offset = 0.5 + + location.z = waypoint.transform.location.z + z_offset + return location + +def scenicToCarlaLocation(pos, world=None, blueprint=None, snapToGround=False): + if snapToGround: + assert world is not None + return _snapToGround(world, carla.Location(pos.x, -pos.y, 0.0), blueprint) + return carla.Location(pos.x, -pos.y, pos.z) + +def scenicToCarlaRotation(orientation): + # CARLA uses intrinsic yaw, pitch, roll rotations (in that order), like Scenic, + # but with yaw being left-handed and with zero yaw being East. + yaw, pitch, roll = orientation.r.as_euler("ZXY", degrees=True) + yaw = -yaw - 90 + return carla.Rotation(pitch=pitch, yaw=yaw, roll=roll) + + + + + + +if __name__ == "__main__": + + map = "Town01.net.xml" + + # key value pairs where key == origID and value == lane id + town01_test_pairs = {"4_1": "4_2", "0_2 11_-2 8_2": "0_1", "8_-3 11_3 0_-3": "-8_0" } + + test_mapping(map, town01_test_pairs) + + map = "Town02.net.xml" + + # Randomly selected test cases from the file to check accuracy + town02_test_pairs = {"177_-1": ":132_3_0", "276_-3":":242_2_0", "1_-2 16_-2 12_2 3_-2 15_2": "-1_1"} + + test_mapping(map, town02_test_pairs) + + map = "Town05.net.xml" + + result = generate_signal_map(map) + + print(f'Result was: {result}') + \ No newline at end of file From bd689a3ad796d7a324fc04555c11609ff4d5709e Mon Sep 17 00:00:00 2001 From: Kay Date: Mon, 9 Mar 2026 16:05:00 -0700 Subject: [PATCH 45/73] added traffic flows --- src/scenic/simulators/cosim/model.scenic | 199 +++++++++++++++-------- 1 file changed, 127 insertions(+), 72 deletions(-) diff --git a/src/scenic/simulators/cosim/model.scenic b/src/scenic/simulators/cosim/model.scenic index 70b1ae8b4..375581fc9 100644 --- a/src/scenic/simulators/cosim/model.scenic +++ b/src/scenic/simulators/cosim/model.scenic @@ -1,72 +1,127 @@ -import pathlib -from scenic.simulators.carla.model import Vehicle, is2DMode - -import scenic.simulators.carla.blueprints as blueprints -from scenic.simulators.carla.behaviors import * -from scenic.simulators.utils.colors import Color - - -from scenic.simulators.cosim.simulator import CosimSimulator -map_town = pathlib.Path(globalParameters.map).stem -param xml_path = pathlib.Path(globalParameters.xml_map) -param carla_map = map_town -param metsr_host = "localhost" -param metsr_port = 4000 -param address = "10.0.0.122" -param carla_port = 2000 -param metsr_map = "Data.properties.CARLA" -param timestep = 0.1 -param snapToGroundDefault = is2DMode() - - -simulator CosimSimulator( - metsr_host = globalParameters.metsr_host, - metsr_port = globalParameters.metsr_port, - address = globalParameters.address, - carla_port = globalParameters.carla_port, - metsr_map = globalParameters.metsr_map, - carla_map = map_town, - xml_map = globalParameters.xml_path, - map_path = globalParameters.map, - timestep = globalParameters.timestep, - bubble_size = 50 - ) - - -class Car(Vehicle): - """A car. - - The default ``blueprint`` (see `CarlaActor`) is a uniform distribution over the - blueprints listed in :obj:`scenic.simulators.carla.blueprints.carModels`. - """ - blueprint: Uniform(*blueprints.carModels) - destination: -1 - origin: -1 - - @property - def isCar(self): - return True - - -class EgoCar(Car): - """ - Special class for Ego - """ - carla_actor_flag: True - - - -class NPCCar(Car): - """ - An NPC car - """ - carla_actor_flag: False - trajectory: None - behavior: DriveAvoidingCollisions(target_speed=15, avoidance_threshold=12) - # Default Carla Behavior - # behavior: FollowLaneBehavior(self.trajectory) - - - - - +import pathlib +from scenic.simulators.carla.model import Vehicle, is2DMode + +import scenic.simulators.carla.blueprints as blueprints +from scenic.simulators.carla.behaviors import * +from scenic.simulators.utils.colors import Color + +from scenic.simulators.metsr.traffic_flows import * + +from scenic.simulators.cosim.simulator import CosimSimulator +map_town = pathlib.Path(globalParameters.map).stem +param xml_path = pathlib.Path(globalParameters.xml_map) +param carla_map = map_town +param metsr_host = "localhost" +param metsr_port = 4000 +param address = "10.0.0.122" +param carla_port = 2000 +param metsr_map = "Data.properties.CARLA" +param timestep = 0.1 +param snapToGroundDefault = is2DMode() + + +simulator CosimSimulator( + metsr_host = globalParameters.metsr_host, + metsr_port = globalParameters.metsr_port, + address = globalParameters.address, + carla_port = globalParameters.carla_port, + metsr_map = globalParameters.metsr_map, + carla_map = map_town, + xml_map = globalParameters.xml_path, + map_path = globalParameters.map, + timestep = globalParameters.timestep, + bubble_size = 100 + ) + +""" +What kind of behaviors: + Sensor Data + Add some functions for accessing simulator data + + Intersection -- Traffic Link / intersection + + Helpers for measuring traffic metrics + +""" + + +class Car(Vehicle): + """A car. + + The default ``blueprint`` (see `CarlaActor`) is a uniform distribution over the + blueprints listed in :obj:`scenic.simulators.carla.blueprints.carModels`. + """ + blueprint: Uniform(*blueprints.carModels) + trajectory: None # Set a trajectory for Carla autopilot + origin: -1 + destination: -1 + + @property + def isCar(self): + return True + + +class EgoCar(Car): + """ + Special class for Ego + """ + carla_actor_flag: True + behavior: DriveAvoidingCollisions(target_speed=15, avoidance_threshold=12) + interrupt: False + +behavior EgoAttack(): + condition = True # overwrite this with some condition to initiate attack + while True: + if condition: + self.interrupt = True + # do --- define attacker bahavior + else: + wait + + +class NPCCar(Car): + """ + An NPC car + """ + finished_route_check: False + destination: -1 + origin: -1 + carla_actor_flag: False + + +scenario GeneratePrivateTrip(origin,destination): + new NPCCar with origin origin, with destination destination + terminate after 1 steps + +scenario TrafficStream(origin, destination, traffic_flow): + compose: + while True: + raw_prob_spawn = traffic_flow.expected_vehs( + currentTOD(), currentTOD()+simulation().timestep) + if raw_prob_spawn < 0 or raw_prob_spawn > 1: + warnings.warn(f"raw_prob_spawn (={raw_prob_spawn}) fell outside [0,1] and will be clamped.") + prob_spawn = min(1, max(raw_prob_spawn, 0)) + if Range(0,1) < prob_spawn: + do GeneratePrivateTrip(origin, destination) + else: + wait + +scenario ConstantTrafficStream(origin, destination, num_vehicles, stime=None, etime=None): + compose: + tf = ConstantTrafficFlow(num_vehicles, stime, etime) + do TrafficStream(origin, destination, tf) + +scenario NormalTrafficStream(origin, destination, num_vehicles, peak_time, stddev): + compose: + tf = NormalTrafficFlow(num_vehicles, peak_time, stddev) + do TrafficStream(origin, destination, tf) + +scenario CommuterTrafficStream(origin, destination, num_vehicles, + peak_time_1, peak_time_2, stddev): + compose: + tf1 = NormalTrafficFlow(num_vehicles, peak_time_1, stddev) + tf2 = NormalTrafficFlow(num_vehicles, peak_time_2, stddev) + do TrafficStream(origin, destination, tf1), TrafficStream(destination, origin, tf2) + + + From 37dd061cc454c715c6f311bd07367c18b6356fe0 Mon Sep 17 00:00:00 2001 From: Kay Date: Mon, 9 Mar 2026 16:05:27 -0700 Subject: [PATCH 46/73] minor --- .gitignore | 295 +++++++++++++++++++------------------ examples/cosim/test.scenic | 32 ++-- 2 files changed, 162 insertions(+), 165 deletions(-) diff --git a/.gitignore b/.gitignore index 76c3f2d0d..b0a3858a3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,148 +1,149 @@ -# Autogenerated documentation -docs/modules - -# Poetry lock file -poetry.lock - -# Scenic cache files -*.snet - -# Webots temporary files -.*.wbproj - -# OS X junk -.DS_Store - -# Sublime Text files -*.sublime-project -*.sublime-workspace - -# VSCode files -*.vscode - -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -.hypothesis/ -.pytest_cache/ -coverage.json - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ -docs/_autosummary/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -.python-version - -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv*/ -ENV/ -env.bak/ -venv.bak/ -scenic.venv/ -3_10_venv/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -*.cproject - -# generated parser -src/scenic/syntax/parser.py - -simulation.gif - -# Random -test.ipynb +# Autogenerated documentation +docs/modules + +# Poetry lock file +poetry.lock + +# Scenic cache files +*.snet + +# Webots temporary files +.*.wbproj + +# OS X junk +.DS_Store + +# Sublime Text files +*.sublime-project +*.sublime-workspace + +# VSCode files +*.vscode + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ +coverage.json + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ +docs/_autosummary/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv*/ +ENV/ +env.bak/ +venv.bak/ +scenic.venv/ +3_10_venv/ +scenic_venv/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +*.cproject + +# generated parser +src/scenic/syntax/parser.py + +simulation.gif + +# Random +test.ipynb test.sh \ No newline at end of file diff --git a/examples/cosim/test.scenic b/examples/cosim/test.scenic index 9a71f7e69..ffd7bc2fc 100644 --- a/examples/cosim/test.scenic +++ b/examples/cosim/test.scenic @@ -1,19 +1,15 @@ -# param startTime = 0 -param map = localPath('../../assets/maps/CARLA/Town05.xodr') # OpenDrive file -param xml_map = localPath("../../assets/maps/CARLA/Town05.net.xml") # Sumo file -param address = "10.29.10.114" -# param address = "10.0.0.122" -# param verbose = True -model scenic.simulators.cosim.model - -# with behavior DriveAvoidingCollisions(target_speed=15, avoidance_threshold=12), -ego = new EgoCar with name "ego", with behavior DriveAvoidingCollisions(target_speed=15, avoidance_threshold=12) -#, with position Vector(88.80499658384105, 1.5729413763422928, 0) - - -for i in range(20): - title = f"npccar_{i}" # allow me to debug more easily - new NPCCar with name title - #, with position Vector(86.43, 1.5500000000000114, 0) - +# param startTime = 0 +param map = localPath('../../assets/maps/CARLA/Town05.xodr') # OpenDrive file +param xml_map = localPath("../../assets/maps/CARLA/Town05.net.xml") # Sumo file +# param address = "172.21.116.114" +param address = "10.0.0.122" +# param verbose = True +model scenic.simulators.cosim.model + +ego = new EgoCar with name "ego", with behavior DriveAvoidingCollisions(target_speed=15, avoidance_threshold=12) + +for i in range(40): + title = f"npccar_{i}" # allow me to debug more easily + vehicle = new NPCCar with name title + terminate after 500 steps \ No newline at end of file From c344c525ce887143ab451011e3d712eb3da4503c Mon Sep 17 00:00:00 2001 From: Kay Date: Mon, 9 Mar 2026 16:09:11 -0700 Subject: [PATCH 47/73] updated docs, light synchronization support added, --- src/scenic/simulators/cosim/simulator.py | 2217 +++++++++++++--------- 1 file changed, 1332 insertions(+), 885 deletions(-) diff --git a/src/scenic/simulators/cosim/simulator.py b/src/scenic/simulators/cosim/simulator.py index f3a6731ed..998ea82fe 100644 --- a/src/scenic/simulators/cosim/simulator.py +++ b/src/scenic/simulators/cosim/simulator.py @@ -1,885 +1,1332 @@ -from scenic.core.simulators import Simulation, Simulator -from scenic.core.vectors import Orientation, Vector -from scenic.syntax.veneer import verbosePrint -from scenic.simulators.metsr.client import METSRClient -import scenic.simulators.carla.utils.utils as utils -from scenic.simulators.cosim.utils.utils import * -from scenic.core.regions import CircularRegion - -from scenic.domains.driving.roads import Lane, Intersection - - - -import pygame -import warnings -import os -import math -import numpy as np -from scenic.core.simulators import SimulationCreationError -import scenic.simulators.cosim.utils.utils as _utils - -from scenic.domains.driving.simulators import DrivingSimulation, DrivingSimulator - -from scenic.core.distributions import Uniform - -import scenic.simulators.carla.utils.visuals as visuals -from scenic.simulators.carla.blueprints import oldBlueprintNames -from shapely.geometry import Point -import re - - -try: - import carla -except ImportError as e: - raise ModuleNotFoundError('CARLA scenarios require the "carla" Python package') from e - -# def initialize_Carla(map_name=None, map_path=None, address="127.0.0.1",port=2000,timeout=10): -# verbosePrint(f"Connecting to CARLA on port {port}") -# client = carla.Client(address,port) - - -class CosimSimulator(DrivingSimulator): - def __init__(self, - metsr_map, - carla_map, - map_path, - xml_map, - bubble_size = 50, # Might be good to add some logic for what a minimal bubble size is so users cannot make it too small - address="127.0.0.1", - carla_port=2000, - metsr_host="localhost", # Not sure what this actually means here - metsr_port=4000, - # timestep=1,# Not entirely sure what the distinction between timestep and sim_timestep is in metsr - timestep=0.1, - traffic_manager_port=None, - timeout=10, - verbose=False, - render=True, - record="" - ): - super().__init__() - - - self.metsr_map_name = metsr_map - self.timestep = timestep - self.sim_timestep = timestep - self.map_path = map_path - self.bubble_size = bubble_size - self.render= render - self.record = record - - # Setting up the Carla Simulator - verbosePrint(f"Connection to CARLA on port {carla_port}") - self.carla_client = carla.Client(address,carla_port) - self.carla_client.set_timeout(timeout) - """ - Need to figure out how to handle the map paths for this - """ - if carla_map is not None: - try: - self.world = self.carla_client.load_world(carla_map) - self.xml_to_xodr_map = _utils.generate_map(str(xml_map)) #covert pathlib obj to str for XML tree TODO what is best practice? - except Exception as e: - raise RuntimeError(f"CARLA could not load world '{carla_map}'") from e - else: - #TODO figure out how to properly do the map handling here - if str(map_path).endswith(".xodr"): - with open(map_path) as odr_file: - self.world = self.carla_client.generate_opendrive_world(odr_file.read()) - else: - raise RuntimeError("CARLA only supports OpenDrive maps") - self.timestep = timestep - if traffic_manager_port is None: - traffic_manager_port = carla_port + 6000 - assert traffic_manager_port != metsr_port, f"Specified Traffic manager port {traffic_manager_port} is not available" - self.tm = self.carla_client.get_trafficmanager(traffic_manager_port) - self.tm.set_synchronous_mode(True) - - settings = self.world.get_settings() - settings.synchronous_mode = True - assert timestep <= .1 , f"timestep must be less that 0.1" - settings.fixed_delta_seconds = timestep - self.world.apply_settings(settings) - verbosePrint("Map loaded in simulator.") - - # self.render = render - # self.record = record - # self.scenario_numver = 0 - verbosePrint("Carla was initialized correctly proceeding to Metsr") - - # Setting up Metsr simulator - - self.metsr_client = METSRClient(host=metsr_host, port=metsr_port, verbose=verbose) - - verbosePrint("Clients have successfully been initialized") - - def createSimulation(self,scene,*, timestep, **kwargs): #TODO: fix timestep - if timestep is not None and timestep != self.timestep: - raise RuntimeError( - "cannot customize timestep for individual CARLA simulations; " - "set timestep when creating the CarlaSimulator instead" - ) - return CosimSimulation( - scene=scene, - carla_client=self.carla_client, - metsr_client=self.metsr_client, - sim_timestep=self.sim_timestep, - tm=self.tm, - bubble_size=self.bubble_size, - render=self.render, - record=self.record, - mappings=self.xml_to_xodr_map, - **kwargs, - ) - def destroy(self): - self.metsr_client.close() - super().destroy() - settings = self.world.get_settings() - settings.synchronous_mode = False - settings.fixed_delta_seconds = None - self.world.apply_settings(settings) - self.tm.set_synchronous_mode(False) - -class CosimSimulation(DrivingSimulation): - def __init__(self, scene, carla_client, metsr_client, sim_timestep, tm,render,record,mappings, bubble_size=100, **kwargs ): - - # Carla and metrs simulators - self.carla_client = carla_client - self.metsr_client = metsr_client - self.sim_timestep = sim_timestep - - # Initializing CARLA params - self.tm = tm # Carla Traffic manager - self.carla_world = self.carla_client.get_world() - self.map = self.carla_world.get_map() - self.blueprintLib = self.carla_world.get_blueprint_library() - self.carla_cameraManager = None - self.render = render - self.record = record - - # Initializing METSR params - self.next_pv_id = 0 - self.pv_id_map = {} - self.frozen_vehicles = set() - self.xodr_to_xml_map = mappings - - self._client_calls = [] - self.count = 0 - - # CoSim related params - self.bubble_size = bubble_size - self.workspace = scene.workspace - self.carla_control_lanes = {} - self.queued_vehicles = {} - self.bubble_spawn_queue = set({}) - - super().__init__(scene, timestep=sim_timestep, **kwargs) - - - - def setup(self) -> None: - """ - Setup the simulation instance - Set initial simulator instance for each object - """ - self.metsr_client.reset("Data.properties.CARLA") # Not entirley sure what this is for - - weather = self.scene.params.get("weather") - if weather is not None: - if isinstance(weather, str): - self.carla_world.set_weather(getattr(carla.WeatherParameters, weather)) - elif isinstance(weather, dict): - self.carla_world.set_weather(carla.WeatherParameters(**weather)) - - # Setup HUD - if self.render: - self.displayDim = (1280, 720) - self.displayClock = pygame.time.Clock() - self.camTransform = 0 - pygame.init() - pygame.font.init() - self.hud = visuals.HUD(*self.displayDim) - self.display = pygame.display.set_mode( - self.displayDim, pygame.HWSURFACE | pygame.DOUBLEBUF - ) - self.cameraManager = None - - if self.record: - if not os.path.exists(self.record): - os.mkdir(self.record) - name = "{}/scenario{}.log".format(self.record, self.scenario_number) - # Carla is looking for an absolute path, so convert it if necessary. - name = os.path.abspath(name) - self.carla_client.start_recorder(name) - - # Create objects. - super().setup() - - #TEMP FIX? IDK... - for object in self.objects[1:]: - object.carla_actor_flag = False - object.spawn_guard = 0 - self.objects[0].carla_actor_flag = True - self.objects[0].spawn_guard = 0 - - # print(f"bubble_size {self.bubble_size}") - self.objects[0].bubble = CircularRegion(center=[self.objects[0].x, - self.objects[0].y], - radius=self.bubble_size) - - for obj in self.objects: - if isinstance(obj.carlaActor, carla.Vehicle): - obj.carlaActor.apply_control( - carla.VehicleControl(manual_gear_shift=False) - ) - self.carla_world.tick() - - # Set up camera manager and collision sensor for ego - if self.render: - camIndex = 0 - camPosIndex = 0 - egoActor = self.objects[0].carlaActor - self.cameraManager = visuals.CameraManager(self.carla_world, egoActor, self.hud) - self.cameraManager._transform_index = camPosIndex - self.cameraManager.set_sensor(camIndex) - self.cameraManager.set_transform(self.camTransform) - - self.carla_world.tick() ## allowing manualgearshift to take effect # TODO still need this? - - for obj in self.scene.objects: - if obj.carla_actor_flag: - if obj.speed is not None and obj.speed != 0: - raise RuntimeError( - f"object {obj} cannot have a nonzero initial speed " - "(this is not yet possible in CARLA)" - ) - - self.synchronize_clients() - - - def createObjectInMetsr(self, obj) -> None: - """ - Create object in Metsr - """ - assert obj.origin, "Metsr objects must have a defined origin" - assert obj.destination, "Metsr objects must have a defined destination" - - # print(f"Creating obj: {obj} with position: {obj.position} in METSR") - - call_kwargs = { - "vehID": self.getMetsrPrivateVehId(obj), - "origin": obj.origin, - "destination": obj.destination, - } - - self.metsr_client.generate_trip(**call_kwargs) - - - def createObjectInCarla(self,obj) -> None: - # Extract blueprint - try: - blueprint = self.blueprintLib.find(obj.blueprint) - except IndexError as e: - found = False - if obj.blueprint in oldBlueprintNames: - for oldName in oldBlueprintNames[obj.blueprint]: - try: - blueprint = self.blueprintLib.find(oldName) - found = True - warnings.warn( - f"CARLA blueprint {obj.blueprint} not found; " - f"using older version {oldName}" - ) - obj.blueprint = oldName - break - except IndexError: - continue - if not found: - raise SimulationCreationError( - f"Unable to find blueprint {obj.blueprint}" f" for object {obj}" - ) from e - if obj.rolename is not None: - blueprint.set_attribute("role_name", obj.rolename) - - # set walker as not invincible - if blueprint.has_attribute("is_invincible"): - blueprint.set_attribute("is_invincible", "False") - - # Set up transform - loc = utils.scenicToCarlaLocation( - obj.position, - world=self.carla_world, - blueprint=obj.blueprint, - snapToGround=obj.snapToGround, - ) - rot = utils.scenicToCarlaRotation(obj.orientation) - transform = carla.Transform(loc, rot) - - # Color, cannot be set for Pedestrians - if blueprint.has_attribute("color") and obj.color is not None: - c = obj.color - c_str = f"{int(c.r*255)},{int(c.g*255)},{int(c.b*255)}" - blueprint.set_attribute("color", c_str) - - # Create Carla actor - # print(f"Spawning actor {obj} in location {loc} with original pos: {obj.position}") - try: - carlaActor = self.carla_world.spawn_actor(blueprint, transform) - except Exception as e: - print(f"Error: {e} occured \n displaying object positions") - for obj in self.objects: - car_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True) - simulator = "carla" if obj.carla_actor_flag else "metsr" - if obj.carla_actor_flag: - print(f"{obj} in {simulator}: [Metsr][Scenic] coords X: {car_data['DATA'][0]['x'], obj.position.x}, y:{car_data['DATA'][0]['y'], obj.position.y}") - print(f"Query Vehicle resuts for obj: :{obj}, is: {car_data['DATA']}") - # raise e(f"Error : {e} occured") - if carlaActor is None: - raise SimulationCreationError(f"Unable to spawn object {obj}") - obj.carlaActor = carlaActor - carlaActor.set_simulate_physics(obj.physics) - - if isinstance(carlaActor, carla.Vehicle): - # TODO should get dimensions at compile time, not simulation time - extent = carlaActor.bounding_box.extent - ex, ey, ez = extent.x, extent.y, extent.z - # Ensure each extent is positive to work around CARLA issue #5841 - obj.width = ey * 2 if ey > 0 else obj.width - obj.length = ex * 2 if ex > 0 else obj.length - obj.height = ez * 2 if ez > 0 else obj.height - carlaActor.apply_control(carla.VehicleControl(manual_gear_shift=True, gear=1)) - elif isinstance(carlaActor, carla.Walker): - carlaActor.apply_control(carla.WalkerControl()) - # spawn walker controller - controller_bp = self.blueprintLib.find("controller.ai.walker") - controller = self.carla_world.try_spawn_actor( - controller_bp, carla.Transform(), carlaActor - ) - if controller is None: - raise SimulationCreationError( - f"Unable to spawn carla controller for object {obj}" - ) - obj.carlaController = controller - - - - def createObjectInSimulator(self, obj) -> None: - """ - Create object in the corresponding simulator according to its flag - """ - assert obj.origin, "All objects must have an origin" - assert obj.destination, "All objects must have an destination" - - assert hasattr(obj, "carla_actor_flag"), "All objects must have attribute: carla_actor_flag" - - if obj == self.objects[0]: # ensures consistency for obje queue spawning in METSR - self.createObjectInCarla(obj) - self.createObjectInMetsr(obj) - self.synchronize_clients() # First time ego is created synchronize clients - elif obj.carla_actor_flag: - self.createObjectInCarla(obj) - self.synchronize_clients() - else: - self.createObjectInMetsr(obj) - - - def getCarlaProperties(self, obj, properties) -> dict[str, float | Vector | int]: - """ - Return simulator specific properties for object in CARLA simulator - """ - # Extract Carla properties - carlaActor = obj.carlaActor - currTransform = carlaActor.get_transform() - currLoc = currTransform.location - currRot = currTransform.rotation - currVel = carlaActor.get_velocity() - currAngVel = carlaActor.get_angular_velocity() - - # Prepare Scenic object properties - position = utils.carlaToScenicPosition(currLoc) - velocity = utils.carlaToScenicPosition(currVel) - speed = math.hypot(*velocity) - angularSpeed = utils.carlaToScenicAngularSpeed(currAngVel) - angularVelocity = utils.carlaToScenicAngularVel(currAngVel) - globalOrientation = utils.carlaToScenicOrientation(currRot) - yaw, pitch, roll = obj.parentOrientation.localAnglesFor(globalOrientation) - elevation = utils.carlaToScenicElevation(currLoc) - - values = dict( - position=position, - velocity=velocity, - speed=speed, - angularSpeed=angularSpeed, - angularVelocity=angularVelocity, - yaw=yaw, - pitch=pitch, - roll=roll, - elevation=elevation, - ) - return values - - - def getMetsrProperties(self, obj, properties) -> dict[str, float | Vector | int]: - """ - Return simulator specific properties for object in Metsr - """ - if obj in self.frozen_vehicles: - return None - - raw_data = self.obj_data_cache[obj] - - if "road" not in raw_data and raw_data["state"] <=0: - self.frozen_vehicles.add(obj) - - position = Vector(raw_data["x"], raw_data["y"], 0) - speed = raw_data["speed"] - bearing = math.radians(raw_data["bearing"]) - globalOrientation = Orientation.fromEuler(bearing,0,0) - yaw, pitch, roll = obj.parentOrientation.localAnglesFor(globalOrientation) - velocity = Vector(0, speed, 0).rotatedBy(yaw) - angularSpeed = 0 - angularVelocity = Vector(0,0,0) - - values = dict( - position=position, - velocity=velocity, - speed=speed, - angularSpeed=angularSpeed, - angularVelocity=angularVelocity, - yaw=yaw, - pitch=pitch, - roll=roll, - elevation=float(0) - ) - return values - - def getProperties(self, obj, properties)-> dict[str, float | Vector | int]: - """ - Return properties for any simulator object - """ - assert hasattr(obj, "carla_actor_flag"), f"Object is not assigned properly to a simulator instance" - """ TODO: Sometimes update objects fails on the first step -- Not clear why this occurs -- adding a check before accessing obj.carlaActor """ - if obj.carla_actor_flag: - properties = self.getCarlaProperties(obj,properties) - else: - properties = self.getMetsrProperties(obj,properties) - return properties - - def getMetsrPrivateVehId(self,obj) -> int: - """ - Return unique vehicle idea - Generates a new ID if none exists for vehicle - """ - if obj not in self.pv_id_map: - self.pv_id_map[obj] = self.next_pv_id - self.next_pv_id += 1 - return self.pv_id_map[obj] - - - def step(self) -> None: - """ - Step both simulators -> Update ego bubble - Update actor locations in either bubble - - TODO : Verify fixed! - Seems like carla needs to be stepped after creating a new object - TODO : Verify fixed! - If a NON-ego car spawns in the bubble (@ step 0) getproperties fails (carlaActor == None) - TODO : METSR vrs CARLA deviation METSR queue spawning occupied CARLA position - - """ - - for obj in self.objects: - if obj.carla_actor_flag: - car_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True) - metsr_x, metsr_y = car_data['DATA'][0]['x'], car_data['DATA'][0]['y'] - loc = obj.carlaActor.get_location() - carla_x, carla_y = loc.x, loc.y - in_metsr_queue = not bool("dist" in car_data['DATA'][0]) - - if not np.isclose(metsr_x, carla_x): - print(f"Checking Object synchronization before Stepping") - print(f"Obj: {obj} in metsr queue? {in_metsr_queue}") - print("=" * 25) - lane = obj._lane - if lane: - print(f"OBJ lane: {self.map_scenic_to_metsr(obj._lane)}") - print(f"OBJ X: {obj}| METSR {metsr_x}: CARLA {carla_x}") - if not np.isclose(metsr_y, -carla_y): - print(f"OBJ Y: {obj}| METSR {metsr_y}: CARLA {-carla_y}") - print("=" * 25) - - # metsr_data = self.metsr_client.query_coSimVehicle() - # print(f"Displaying METSR query data: {metsr_data}") - - - lanes, new_lanes, old_lanes = self.update_carla_lanes() - self.release_lanes(old_lanes) - self.freeze_lanes(new_lanes) - # Update state of simulator with new bubble region - self.update_bubble_objects(lanes,new_lanes) - - #Carla step - self.carla_world.tick() - self.synchronize_clients() - - - # It takes 2 CARLA ticks to fully instantiate and update properties once spawned in CARLA - for obj in self.objects: - if obj.spawn_guard > 0: - obj.spawn_guard -= 1 - - if self.render: - self.cameraManager.render(self.display) - pygame.display.flip() - - # Metsr step - self.count += 1 - if self.count % 100 == 0: # just removing for now - print(".", end="", flush=True) - - metsr_data = self.metsr_client.query_coSimVehicle() - # print(f"Displaying METSR query data: {metsr_data}") - - self.metsr_client.tick() - - # Generate bubble region based on ego objects TODO update logic for multiple high-fidelity zones - self.objects[0].bubble = CircularRegion(center=[self.objects[0].x, - self.objects[0].y], - radius=self.bubble_size) - - # Set intersections - intersections = self.get_carla_intersections() - if intersections != []: - # print(f"Intersections were: {intersections}") - self.synchronize_signals(intersections) - - - def get_carla_lanes(self) -> list[Lane]: - """ - Collect the current set of lanes intersecting the CoSimulation bubble - """ - # Set ego, lane, bubble - ego = self.objects[0] - lane = self._nearest_lane(ego) - bubble_region = ego.bubble - - # Collect lanes which intersect bubble - carla_lanes = [] - for lane in self.workspace.network.lanes: - if lane.intersects(bubble_region): - carla_lanes.append(lane) - - return carla_lanes - - def get_carla_intersections(self) -> list[Intersection] | None: - """ - TODO: will need update for multiple high-fidelity zones - Collect any intersections currently within the bubble - (1) TODO consider when an intersection should be included - """ - carla_intersections = [] - ego = self.objects[0] - - # check if ego in intersection - if ego._intersection: - carla_intersections.append(ego._intersection) - - # Bubble region - bubble_region = ego.bubble - - # Collect intersections which intersect bubble - for intersection in self.workspace.network.intersections: - if intersection.intersects(bubble_region): - carla_intersections.append(intersection) - - return carla_intersections - - def _set_intersections(self) -> None: - """ - Checks if an intersection is fully inclosed in bubble - (1) If all connecting roads for an intersection are in bubble so is intersection - - """ - pass - - def synchronize_signals(self, intersections: list[Intersection]) -> None: - """ - Ensures consistency for light signals for all intersections contained in the bubble region - """ - pass - - def synchronize_clients(self): - """ - Update Metsr client so object positions are synchronized with the behaviors in Scenic - """ - carla_actors = [obj for obj in self.objects if obj.carla_actor_flag] - for obj in carla_actors: - loc = obj.carlaActor.get_location() - vehID = self.getMetsrPrivateVehId(obj) - lane = self._nearest_lane(obj) - roadID = self.map_scenic_to_metsr(lane) - self.metsr_client.teleport_cosim_vehicle(vehID, roadID, loc.x, -loc.y, private_veh = True, transform_coords = True ) - - def update_carla_lanes(self) -> None: #TODO break this out into multiple functions/helpers - """ - TODO : Avoid nested call structure implement both in step - Update the state of lanes - (1): Freeze new lanes inside CoSimulator bubble - (2): Release lanes outside the CoSimulator bubble - (3): Update objects inside Carla with update lanes - """ - # Collect lanes intersecting the bubble - lanes = self.get_carla_lanes() - # Find the corresponding METSR keys - carla_lane_ids = [self.map_scenic_to_metsr(lane) for lane in lanes] - carla_lane_ids = set(carla_lane_ids) - # Lanes which are already set - curr_frozen_ids = list(self.carla_control_lanes.keys()) - - # Collect new and old lanes - new_lanes = [id for id in carla_lane_ids if id not in curr_frozen_ids] - old_lanes = [id for id in list(self.carla_control_lanes.keys()) if id not in carla_lane_ids] - - # Update object existance based on bubble changes - return lanes, new_lanes, old_lanes - - - def update_bubble_objects(self, carla_lanes: list[str], new_lanes: list[str]) -> None: - """ - Check each objects current lane - (1) Check if there are cars in the queue to spawn in the CoSim bubble - prioritize safely spawning these vehicles first - (2) Object is NOT in a Carla lane but still in Carla - then delete object in Carla - (3) If object is in a Carla lane and not created yet - then create object in Carla - - TODO: Slight redundancy for some objects - """ - cosim_data = self.metsr_client.query_coSimVehicle() - carla_actors = [obj for obj in self.objects if obj.carla_actor_flag] # TODO might need to consider a more efficient data structure here - # Check if room has opened up to spawn queued vehicles in the bubble - if len(self.bubble_spawn_queue) > 0: - if self.count % 10 == 0: - print(f"{len(self.bubble_spawn_queue)} objects waiting to spawn") - for obj in self.bubble_spawn_queue: - lane = self._nearest_lane(obj) - if lane in carla_lanes: - if not within_threshold_to(obj, carla_actors): - obj.orienation = lane.orientation[obj.position] - self.createObjectInCarla(obj) - carla_actors.add(obj) - obj.carla_actor_flag = True - obj.spawn_guard = 2 - self.bubble_spawn_queue.remove(obj) - # else: - # print(f"Waiting to spawn: {obj}: {self.count}") - - # Remove objects which are no longer inside the bubble - for obj in self.objects[1:]: - lane = self._nearest_lane(obj) - veh_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True) # - - if lane not in carla_lanes and obj.carla_actor_flag: - if obj.spawn_guard == 0: - obj.carla_actor_flag = False - self.destroy_carla_obj(obj) - carla_actors.remove(obj) - # Add objects to the bubble if they are entering through a new lane and their is not enough space to spawn - elif lane in carla_lanes and not obj.carla_actor_flag: - if self.map_scenic_to_metsr(lane) in new_lanes and within_threshold_to(obj, carla_actors): - self.bubble_spawn_queue.add(obj) - - # If their is enough room check that the vehicle is not currently queued and spawn - else: - if 'dist' in veh_data["DATA"][0]: - print(f"Creating Object: {obj}, at position: {obj.position} at {self.count}") - print(f"Displaying Cosim data: {cosim_data}") - VehID = self.getMetsrPrivateVehId(obj) - route_data = None - print(f"CoSim Data: {len(cosim_data['DATA'])} : {cosim_data['DATA']}") - for entry in cosim_data["DATA"]: - print(entry) - # assert False, "Terminating early" - for data_entry in cosim_data['DATA']: - # print(f"data entry: {data_entry}") - if data_entry['ID'] == VehID: - route_data = data_entry['route'] - print(f"VehID: {VehID}, Data: {data_entry}") - print(f"Selected route data: {route_data}") - obj.trajectory = self.generate_scenic_trajectory(lane,route_data) - - obj.orienation = lane.orientation[obj.position] - self.createObjectInCarla(obj) - carla_actors.append(obj) - obj.carla_actor_flag = True - obj.spawn_guard = 2 - # else: - # print(f"Passed spawning obj: {obj} as it is still queue") - - - - - def freeze_lanes(self, keys: list[str]) -> None: - """ - Query Metsr to freeze simulation and control of given lanes - """ - for key in keys: - assert key not in self.carla_control_lanes, "Attempted to freeze already frozen lane" - self.carla_control_lanes[key] = True # Keep track of frozen lanes - self.metsr_client.set_cosim_road(key) - - - def release_lanes(self,keys: list[str]) -> None: - """ - Query Metsr to begin re-simulating and control given lanes - """ - for key in keys: - assert key in self.carla_control_lanes, "Attempted to release non frozen lane" - del self.carla_control_lanes[key] # Remove frozen lane from record - self.metsr_client.release_cosim_road(key) - - - def destroy_carla_obj(self,obj) -> None: - """ - Special destroy method for updating high-fidelity bubble - """ - if obj.carlaActor is not None: - if isinstance(obj.carlaActor, carla.Vehicle): - obj.carlaActor.set_autopilot(False, self.tm.get_port()) - if isinstance(obj.carlaActor, carla.Walker): - obj.cralaController.stop() - obj.carlaController.destroy() - obj.carlaActor.destroy() - obj.carlaActor = None # Set this to None to prevent reaccess a previously deleted vehicle? - - - def map_scenic_to_metsr(self,lane: Lane) -> str: - """ - TODO: Takes an object and maps the closest lane of the object - in xodr to its corresponding xml key - """ - metsr_key=None - # Parent road key with associated lane id - query_key = f'{lane.road.id}_{lane.id}' - - # Check if element is present in map between formats - if query_key in self.xodr_to_xml_map: - metsr_key = self.xodr_to_xml_map[query_key] - - metsr_key = metsr_key.split("_")[0] - - # There must be a valid mapping - assert metsr_key is not None, f"Error identifying associated ID for {query_key}" - return metsr_key - - def generate_scenic_trajectory(self, curr_lane, route: list[str]) -> list[Lane]: - """ - Docstring for generate_scenic_trajectory - - :param self: CoSim Object - :param route: Metsr route data for a single car - :type route: list[str] - :return: Equivalent trajetory with Scenic Lanes - :rtype: list[Lane] - - """ - assert route is not None, f"Unable to generate trajectory with route data: {route}" - xodr_road_keys = [key for key in self.xodr_to_xml_map.keys()] - print(f'Current lane: {curr_lane.id}, {curr_lane.road.id}') - print(f"Proposed Route: {route} ") - for road in route: - size = len(road) - size2 = len(str(curr_lane.road.id)) - for key in xodr_road_keys: - if key[:size] == road: - pass - # print(key, sep=" ") - if key[:size2] == curr_lane.road.id: - pass - # print(key, sep=" ") - - - return None - - def destroy(self) -> None: - """ - Destroy both simulator instances - """ - # METSR destroy - if self.metsr_client.verbose: - print("Client Messages Log:") - print("[") - for call in self.client._messagesLog: - print(f" {call},") - print("]") - - # "CARLA destroy" - for obj in self.objects: - if obj.carlaActor is not None: - if isinstance(obj.carlaActor, carla.Vehicle): - obj.carlaActor.set_autopilot(False, self.tm.get_port()) - if isinstance(obj.carlaActor, carla.Walker): - obj.carlaController.stop() - obj.carlaController.destroy() - obj.carlaActor.destroy() - if self.render and self.cameraManager: - self.cameraManager.destroy_sensor() - - self.carla_client.stop_recorder() - super().destroy() - - - def _nearest_lane(self,obj) -> Lane | None : # TODO :: Update lane logic to consider intersections - """ - Docstring for _nearest_lane - - Return the nearest lane to the object should ensure all objects are cars? - """ - lane = obj._lane - if lane: - nearest_lane = lane - else: - lanes = [*self.workspace.network.lanes] - distances = [(lane.distanceTo(obj.position),lane) for lane in lanes] - nearest_lane = min(distances, key=lambda t: t[0])[1] # min distance over all lanes - - return nearest_lane - - - def executeActions(self, allActions) -> None: - """ - Apply control updates which were accumulated while executing the actions - Filters out actions for Carla only objects - """ - carla_actions = {} - for obj in self.agents: - if obj.carla_actor_flag: - carla_actions[obj] = allActions[obj] - - super().executeActions(carla_actions) - - for obj in self.agents: - if obj.carla_actor_flag: - ctrl = obj._control - if ctrl is not None: - obj.carlaActor.apply_control(ctrl) - obj._control = None - - - def updateObjects(self) -> None: - # metsr_obj = [obj for obj in self.objects if not obj.carla_actor_flag] - # metsr_obj.append(self.objects[0]) - obj_veh_ids = [self.getMetsrPrivateVehId(obj) for obj in self.objects] - raw_veh_data = self.metsr_client.query_vehicle(obj_veh_ids, True, True) - self.obj_data_cache = {obj: raw_veh_data['DATA'][i] for i, obj in enumerate(self.objects)} - - #DEBUGGING FOR METSR - # for obj in self.obj_data_cache: - # if 'dist' not in self.obj_data_cache[obj]: - # self.queued_vehicles[obj] = True - # elif 'dist' in self.obj_data_cache[obj] and obj in self.queued_vehicles: - # print(f"obj {obj} leaving the spawn queue") - # del self.queued_vehicles[obj] - - super().updateObjects() - self.obj_data_cache = None \ No newline at end of file +from scenic.core.simulators import Simulation, Simulator +from scenic.core.vectors import Orientation, Vector +from scenic.syntax.veneer import verbosePrint +from scenic.simulators.metsr.client import METSRClient +import scenic.simulators.carla.utils.utils as utils +from scenic.simulators.cosim.utils.utils import * +from scenic.core.regions import CircularRegion +from scenic.core.object_types import Object + +from scenic.domains.driving.roads import Lane, Intersection + +from scenic.simulators.carla.behaviors import * + + +import pygame +import warnings +import os +import math +import numpy as np +from scenic.core.simulators import SimulationCreationError +import scenic.simulators.cosim.utils.utils as _utils + +from scenic.domains.driving.simulators import DrivingSimulation, DrivingSimulator + +from scenic.core.distributions import Uniform + +import scenic.simulators.carla.utils.visuals as visuals +from scenic.simulators.carla.blueprints import oldBlueprintNames +from shapely.geometry import Point +import re + + +try: + import carla +except ImportError as e: + raise ModuleNotFoundError('CARLA scenarios require the "carla" Python package') from e + +# def initialize_Carla(map_name=None, map_path=None, address="127.0.0.1",port=2000,timeout=10): +# verbosePrint(f"Connecting to CARLA on port {port}") +# client = carla.Client(address,port) + + +class CosimSimulator(DrivingSimulator): + def __init__(self, + metsr_map, + carla_map, + map_path, + xml_map, + bubble_size = 50, # Might be good to add some logic for what a minimal bubble size is so users cannot make it too small + address="127.0.0.1", + carla_port=2000, + metsr_host="localhost", # Not sure what this actually means here + metsr_port=4000, + # timestep=1,# Not entirely sure what the distinction between timestep and sim_timestep is in metsr + timestep=0.1, + traffic_manager_port=None, + timeout=20, + verbose=False, + render=True, + record="" + ): + super().__init__() + + + self.metsr_map_name = metsr_map + self.timestep = timestep + self.sim_timestep = timestep + self.map_path = map_path + self.bubble_size = bubble_size + self.render= render + self.record = record + + # Setting up the Carla Simulator + verbosePrint(f"Connection to CARLA on port {carla_port}") + self.carla_client = carla.Client(address,carla_port) + self.carla_client.set_timeout(timeout) + """ + Need to figure out how to handle the map paths for this + """ + if carla_map is not None: + try: + self.world = self.carla_client.load_world(carla_map) + self.xml_to_xodr_map = _utils.generate_map(str(xml_map)) #convert pathlib obj to str for XML tree TODO what is best practice? + self.xml_to_xodr_intersections = _utils.generate_signal_map(str(xml_map)) + except Exception as e: + raise RuntimeError(f"CARLA could not load world '{carla_map}'") from e + else: + #TODO figure out how to properly do the map handling here + if str(map_path).endswith(".xodr"): + with open(map_path) as odr_file: + self.world = self.carla_client.generate_opendrive_world(odr_file.read()) + else: + raise RuntimeError("CARLA only supports OpenDrive maps") + self.timestep = timestep + if traffic_manager_port is None: + traffic_manager_port = carla_port + 6000 + assert traffic_manager_port != metsr_port, f"Specified Traffic manager port {traffic_manager_port} is not available" + self.tm = self.carla_client.get_trafficmanager(traffic_manager_port) + self.tm.set_synchronous_mode(True) + + settings = self.world.get_settings() + settings.synchronous_mode = True + assert timestep <= .1 , f"timestep must be less that 0.1" + settings.fixed_delta_seconds = timestep + self.world.apply_settings(settings) + verbosePrint("Map loaded in simulator.") + + # self.scenario_numver = 0 + verbosePrint("Carla was initialized correctly proceeding to Metsr") + + # Setting up Metsr simulator + + self.metsr_client = METSRClient(host=metsr_host, port=metsr_port, verbose=verbose) + + verbosePrint("Clients have successfully been initialized") + + def createSimulation(self,scene,*, timestep, **kwargs): #TODO: fix timestep + if timestep is not None and timestep != self.timestep: + raise RuntimeError( + "cannot customize timestep for individual CARLA simulations; " + "set timestep when creating the CarlaSimulator instead" + ) + return CosimSimulation( + scene=scene, + carla_client=self.carla_client, + metsr_client=self.metsr_client, + sim_timestep=self.sim_timestep, + tm=self.tm, + bubble_size=self.bubble_size, + render=self.render, + record=self.record, + mappings=self.xml_to_xodr_map, + xml_to_xodr_intersections = self.xml_to_xodr_intersections, + **kwargs, + ) + def destroy(self): + self.metsr_client.close() + super().destroy() + settings = self.world.get_settings() + settings.synchronous_mode = False + settings.fixed_delta_seconds = None + self.world.apply_settings(settings) + self.tm.set_synchronous_mode(False) + +class CosimSimulation(DrivingSimulation): + def __init__(self, scene, carla_client, metsr_client, sim_timestep, tm, render ,record, mappings, xml_to_xodr_intersections, bubble_size=100, **kwargs ): + + # Carla and metrs simulators + self.carla_client = carla_client + self.metsr_client = metsr_client + self.sim_timestep = sim_timestep + + # Initializing CARLA params + self.tm = tm # Carla Traffic manager + self.carla_world = self.carla_client.get_world() + self.map = self.carla_world.get_map() + self.blueprintLib = self.carla_world.get_blueprint_library() + self.carla_cameraManager = None + self.render = render + self.record = record + self.cameraManager = None + + # Initializing METSR params + self.next_pv_id = 0 + self.pv_id_map = {} + self.frozen_vehicles = set() + self.scenic_to_metsr_map = mappings + + self._client_calls = [] + self.count = 0 + + # CoSim related params + self.bubble_size = bubble_size + self.workspace = scene.workspace + self.carla_control_roads = {} + self.queued_vehicles = {} + self.bubble_spawn_queue = set({}) + self.frozen_scenic_lanes = [] + self.xml_to_xodr_intersections = xml_to_xodr_intersections + + super().__init__(scene, timestep=sim_timestep, **kwargs) + + + + def setup(self) -> None: + """ + Docstring for setup + + Setup the simulation instance + """ + self.metsr_client.reset() # Updated version takes no arguements + + weather = self.scene.params.get("weather") + if weather is not None: + if isinstance(weather, str): + self.carla_world.set_weather(getattr(carla.WeatherParameters, weather)) + elif isinstance(weather, dict): + self.carla_world.set_weather(carla.WeatherParameters(**weather)) + + # Setup HUD + if self.render: + self.displayDim = (1280, 720) + self.displayClock = pygame.time.Clock() + self.camTransform = 0 + pygame.init() + pygame.font.init() + self.hud = visuals.HUD(*self.displayDim) + self.display = pygame.display.set_mode( + self.displayDim, pygame.HWSURFACE | pygame.DOUBLEBUF + ) + self.cameraManager = None + + if self.record: + if not os.path.exists(self.record): + os.mkdir(self.record) + name = "{}/scenario{}.log".format(self.record, self.scenario_number) + # Carla is looking for an absolute path, so convert it if necessary. + name = os.path.abspath(name) + self.carla_client.start_recorder(name) + + # Create objects. + super().setup() + + #TEMP FIX? IDK... + for object in self.objects[1:]: + object.carla_actor_flag = False + object.spawn_guard = 0 + self.objects[0].carla_actor_flag = True + self.objects[0].spawn_guard = 0 + + # print(f"bubble_size {self.bubble_size}") + self.objects[0].bubble = CircularRegion(center=[self.objects[0].x, + self.objects[0].y], + radius=self.bubble_size) + + _, new_lanes, _ = self.update_carla_lanes() # TODO bad function name + self.freeze_lanes(new_lanes) + self.metsr_client.tick() + + for obj in self.objects: + if isinstance(obj.carlaActor, carla.Vehicle): + obj.carlaActor.apply_control( + carla.VehicleControl(manual_gear_shift=False) + ) + self.carla_world.tick() + + # Set up camera manager and collision sensor for ego + if self.render: + camIndex = 0 + camPosIndex = 0 + egoActor = self.objects[0].carlaActor + self.cameraManager = visuals.CameraManager(self.carla_world, egoActor, self.hud) + self.cameraManager._transform_index = camPosIndex + self.cameraManager.set_sensor(camIndex) + self.cameraManager.set_transform(self.camTransform) + + self.carla_world.tick() ## allowing manualgearshift to take effect # TODO still need this? + + for obj in self.scene.objects: + if obj.carla_actor_flag: + if obj.speed is not None and obj.speed != 0: + raise RuntimeError( + f"object {obj} cannot have a nonzero initial speed " + "(this is not yet possible in CARLA)" + ) + + self.synchronize_clients() + # TODO Waiting for map update + # self._synchronize_signals() + + + def createObjectInMetsr(self, obj: Object, origin: int = None, destination: int = None) -> None: + """ + Docstring for createObjectInMetsr + + :param obj: Cosimulation car object + :type obj: Scenic Object + + Creates vehicle inside the METSR simulator + """ + assert obj.origin, "Metsr objects must have a defined origin" + assert obj.destination, "Metsr objects must have a defined destination" + + obj_origin = origin if origin else obj.origin + obj_destination = destination if destination else obj.destination + + call_kwargs = { + "vehID": self.getMetsrPrivateVehId(obj), + "origin": obj_origin, + "destination": obj_destination, + } + self.metsr_client.generate_trip(**call_kwargs) + + + def createObjectInCarla(self, obj: Object, update_orientation: bool = False, trajectory: list[carla.Transform] = None) -> None: + """ + Docstring for createObjectInCarla + + :param obj: Cosimulation car object + :type obj: Scenic Object + :param update_orientation: Flag to trigger adaptive spawn orientation according to object location + :type update_orientation: bool + + """ + try: + blueprint = self.blueprintLib.find(obj.blueprint) + except IndexError as e: + found = False + if obj.blueprint in oldBlueprintNames: + for oldName in oldBlueprintNames[obj.blueprint]: + try: + blueprint = self.blueprintLib.find(oldName) + found = True + warnings.warn( + f"CARLA blueprint {obj.blueprint} not found; " + f"using older version {oldName}" + ) + obj.blueprint = oldName + break + except IndexError: + continue + if not found: + raise SimulationCreationError( + f"Unable to find blueprint {obj.blueprint}" f" for object {obj}" + ) from e + if obj.rolename is not None: + blueprint.set_attribute("role_name", obj.rolename) + + # set walker as not invincible + if blueprint.has_attribute("is_invincible"): + blueprint.set_attribute("is_invincible", "False") + + # Set up transform + loc = utils.scenicToCarlaLocation( + obj.position, + world=self.carla_world, + blueprint=obj.blueprint, + snapToGround=obj.snapToGround, + ) + lane = self._nearest_lane(obj) + if update_orientation: + lane = self._nearest_lane(obj) + rot = utils.scenicToCarlaRotation(lane.orientation[obj.position]) + else: + rot = utils.scenicToCarlaRotation(obj.orientation) + + transform = carla.Transform(loc, rot) + + # Color, cannot be set for Pedestrians + if blueprint.has_attribute("color") and obj.color is not None: + c = obj.color + c_str = f"{int(c.r*255)},{int(c.g*255)},{int(c.b*255)}" + blueprint.set_attribute("color", c_str) + + # Create Carla actor + # print(f"Spawning actor {obj} in location {loc} with original pos: {obj.position} in CARLA") + try: + carlaActor = self.carla_world.spawn_actor(blueprint, transform) + obj.carla_actor_flag = True + obj.spawn_guard = 2 + except Exception as e: + print(f"Error: {e} occured \n displaying object positions") + for car in self.objects: + car_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(car), True, True) + simulator = "carla" if car.carla_actor_flag else "metsr" + if car.carla_actor_flag: + try: + loc = car.carlaActor.get_location() + except Exception as e: + loc = None + + if loc is not None: + # print(f"{car} in {simulator}: [Metsr][Scenic][CARLA] coords X: {car_data['DATA'][0]['x'], car.position.x, loc.x}, y:{car_data['DATA'][0]['y'], car.position.y, -loc.y}") + print(f"Query Vehicle resuts for obj: :{car}, is: {car_data['DATA']}") + else: + # print(f"{car} in {simulator}: [Metsr][Scenic] coords X: {car_data['DATA'][0]['x'], car.position.x}, y:{car_data['DATA'][0]['y'], car.position.y}") + print(f"Query Vehicle resuts for obj: :{car}, is: {car_data['DATA']}") + + print(f"Checking distance function: {utils.within_threshold_to(obj,[obj for obj in self.objects if obj.carla_actor_flag], verbose=True)}") + print(f"Issue occured at timestop: {self.count}") + # raise e(f"Error : {e} occured") + if carlaActor is None: + raise SimulationCreationError(f"Unable to spawn object {obj}") + obj.carlaActor = carlaActor + carlaActor.set_simulate_physics(obj.physics) + + if isinstance(carlaActor, carla.Vehicle): + # TODO should get dimensions at compile time, not simulation time + extent = carlaActor.bounding_box.extent + ex, ey, ez = extent.x, extent.y, extent.z + # Ensure each extent is positive to work around CARLA issue #5841 + obj.width = ey * 2 if ey > 0 else obj.width + obj.length = ex * 2 if ex > 0 else obj.length + obj.height = ez * 2 if ez > 0 else obj.height + carlaActor.apply_control(carla.VehicleControl(manual_gear_shift=True, gear=1)) + + if trajectory != None: + carlaActor.set_autopilot(True) + self.tm.set_path(carlaActor, trajectory) + + elif isinstance(carlaActor, carla.Walker): + carlaActor.apply_control(carla.WalkerControl()) + # spawn walker controller + controller_bp = self.blueprintLib.find("controller.ai.walker") + controller = self.carla_world.try_spawn_actor( + controller_bp, carla.Transform(), carlaActor + ) + if controller is None: + raise SimulationCreationError( + f"Unable to spawn carla controller for object {obj}" + ) + obj.carlaController = controller + + + + def createObjectInSimulator(self, obj: Object) -> None: + """ + Docstring for createObjectInSimulator + + Spawn the object in the appropriate simulator + (i) Ego is spawned in both simulators + (ii)Ticks metsr to allow the vehicle to enter the road if the simulation has not started + + """ + assert obj.origin, "All objects must have an origin" + assert obj.destination, "All objects must have an destination" + + assert hasattr(obj, "carla_actor_flag"), "All objects must have attribute: carla_actor_flag" + + if obj == self.objects[0]: # ensures consistency for object queue spawning in METSR + trajectory = None + if hasattr(obj, "trajectory"): + if obj.trajectory is not None: + trajectory = self.scenic_trajectory_to_carla(trajectory) + lane = self._nearest_lane(obj) + origin_str = self.map_scenic_to_metsr(lane) + if origin_str[0] == ":": # special handling for ":" suffix on specific roads + origin = int(origin_str[1:]) + else: + origin = int(origin_str) + self.createObjectInCarla(obj, False, trajectory) + self.createObjectInMetsr(obj,origin=origin) # Set the METSR vehicle origin to match ego spawn + obj.final_road = None + self.synchronize_clients(obj) # First time ego is created synchronize clients + if self.count == 0: + self.metsr_client.tick() + + elif obj.carla_actor_flag: + self.createObjectInCarla(obj) + self.synchronize_clients(obj) + obj.finished_route = False + else: + self.createObjectInMetsr(obj) + obj.finished_route = False + if self.count == 0: + self.metsr_client.tick() + + + + def getCarlaProperties(self, obj : Object, properties : dict) -> dict[str, float | Vector | int]: + """ + Docstring for getCarlaProperties + + :param obj: Cosimulation car object + :type obj: Scenic Object + + return objects properties from the Carla simulator + """ + # Extract Carla properties + carlaActor = obj.carlaActor + currTransform = carlaActor.get_transform() + currLoc = currTransform.location + currRot = currTransform.rotation + currVel = carlaActor.get_velocity() + currAngVel = carlaActor.get_angular_velocity() + + # Prepare Scenic object properties + position = utils.carlaToScenicPosition(currLoc) + velocity = utils.carlaToScenicPosition(currVel) + speed = math.hypot(*velocity) + angularSpeed = utils.carlaToScenicAngularSpeed(currAngVel) + angularVelocity = utils.carlaToScenicAngularVel(currAngVel) + globalOrientation = utils.carlaToScenicOrientation(currRot) + yaw, pitch, roll = obj.parentOrientation.localAnglesFor(globalOrientation) + elevation = utils.carlaToScenicElevation(currLoc) + + values = dict( + position=position, + velocity=velocity, + speed=speed, + angularSpeed=angularSpeed, + angularVelocity=angularVelocity, + yaw=yaw, + pitch=pitch, + roll=roll, + elevation=elevation, + ) + return values + + + def getMetsrProperties(self, obj: object, properties : dict) -> dict[str, float | Vector | int]: + """ + Docstring for getMetsrProperties + + :param obj: Cosimulation car object + :type obj: Scenic Object + + return objects properties from the METSR simulator + """ + if obj in self.frozen_vehicles: + return None + + raw_data = self.obj_data_cache[obj] + + if "road" not in raw_data and raw_data["state"] <=0: + self.frozen_vehicles.add(obj) + + position = Vector(raw_data["x"], raw_data["y"], 0) + speed = raw_data["speed"] + bearing = math.radians(raw_data["bearing"]) + globalOrientation = Orientation.fromEuler(bearing,0,0) + yaw, pitch, roll = obj.parentOrientation.localAnglesFor(globalOrientation) + velocity = Vector(0, speed, 0).rotatedBy(yaw) + angularSpeed = 0 + angularVelocity = Vector(0,0,0) + + values = dict( + position=position, + velocity=velocity, + speed=speed, + angularSpeed=angularSpeed, + angularVelocity=angularVelocity, + yaw=yaw, + pitch=pitch, + roll=roll, + elevation=float(0) + ) + return values + + def getProperties(self, obj : Object, properties : dict)-> dict[str, float | Vector | int]: + """ + Docstring for getProperties + + :param obj: Cosimulation car object + :type obj: Scenic Object + + return objects properties for any CoSim object + """ + assert hasattr(obj, "carla_actor_flag"), f"Object is not assigned properly to a simulator instance" + """ TODO: Sometimes update objects fails on the first step -- Not clear why this occurs -- adding a check before accessing obj.carlaActor """ + if obj.carla_actor_flag: + properties = self.getCarlaProperties(obj,properties) + else: + properties = self.getMetsrProperties(obj,properties) + return properties + + def getMetsrPrivateVehId(self, obj: Object) -> int: + """ + Return unique vehicle idea + Generates a new ID if none exists for vehicle + """ + if obj not in self.pv_id_map: + self.pv_id_map[obj] = self.next_pv_id + self.next_pv_id += 1 + return self.pv_id_map[obj] + + + def step(self) -> None: + """ + Step both simulators -> Update ego bubble + Update actor locations in either bubble + + TODO : Verify fixed! - Seems like carla needs to be stepped after creating a new object + TODO : Verify fixed! - If a NON-ego car spawns in the bubble (@ step 0) getproperties fails (carlaActor == None) + TODO : METSR vrs CARLA deviation METSR queue spawning occupied CARLA position + + """ + lanes, new_lanes, old_lanes = self.update_carla_lanes() + self.release_lanes(old_lanes) + self.freeze_lanes(new_lanes) + intersections = self.get_carla_intersections() + # Update state of simulator with new bubble region + self.update_bubble_objects(lanes, new_lanes, intersections) + + #Carla step + self.carla_world.tick() + self.synchronize_clients() + + # It takes 2 CARLA ticks to fully instantiate and update properties once spawned in CARLA + for obj in self.objects: + if obj.spawn_guard > 0: + obj.spawn_guard -= 1 + + if self.render: + self.cameraManager.render(self.display) + pygame.display.flip() + + # Metsr step + self.count += 1 + if self.count % 100 == 0: # just removing for now + print(".", end="", flush=True) + + self.metsr_client.tick() + + # Generate bubble region based on ego objects TODO update logic for multiple high-fidelity zones + self.objects[0].bubble = CircularRegion(center=[self.objects[0].x, + self.objects[0].y], + radius=self.bubble_size) + if self.objects[0].interrupt: + _utils.disable_carla_autopilot(self.objects[0]) + + # if self.count % 10 == 0: + # self._check_traffic_light_consistency() + + + def get_carla_lanes(self) -> list[Lane]: + """ + Collect the current set of lanes intersecting the CoSimulation bubble + """ + # Set ego, lane, bubble + ego = self.objects[0] + lane = self._nearest_lane(ego) + bubble_region = ego.bubble + + # Collect lanes which intersect bubble + carla_lanes = [] + for lane in self.workspace.network.lanes: + if lane.intersects(bubble_region): + carla_lanes.append(lane) + + return carla_lanes + + def get_carla_intersections(self) -> list[Intersection]: + """ + Collect any intersections that are either + (1) Intersecting the CoSim bubble + (2) Are connected to the bubble via AT LEAST 2 roads + + :return: The set of all intersections meeting the above criteria + :rtype: list[Intersection] + """ + + carla_intersections = [] + ego = self.objects[0] + + # check if ego in intersection + if ego._intersection: + carla_intersections.append(ego._intersection) + + # Bubble region + bubble_region = ego.bubble + + # Collect intersections which intersect bubble + for intersection in self.workspace.network.intersections: + if intersection.intersects(bubble_region): + carla_intersections.append(intersection) + continue + intersection_roads = intersection.roads + count = 0 + for road in intersection_roads: + if road in self.frozen_scenic_lanes: + count += 1 + if count > 1: + carla_intersections.append(intersection) + break + + return carla_intersections + + def _synchronize_signals(self) -> None: + """ + docstring for _synchronize_signals + + Synchronize all lights from each map representation to the same timing schedule and state + : has the side effect of resetting all lights to the start of their respective schedules + """ + signals_ids = self.metsr_client.query_signal()['id_list'] + signal_data = self.metsr_client.query_signal(signals_ids) + + carla_world = self.carla_client.get_world() + carla_traffic_lights = carla_world.get_actors().filter('traffic.traffic_light*') + + lights_by_opendrive_id = {light.get_opendrive_id(): light for light in carla_traffic_lights} + updated_ids = {} + + for light_data in signal_data["DATA"]: + light_id = light_data["groupID"] + + if light_id in self.xml_to_xodr_intersections: + light_opendrive_ids = self.xml_to_xodr_intersections[light_id] + + light_config = self.get_light_config(light_data) + + if len(light_opendrive_ids) > 1: + for open_drive_id in light_opendrive_ids: + if open_drive_id not in updated_ids: + if open_drive_id in lights_by_opendrive_id: + light = lights_by_opendrive_id[open_drive_id] + self._update_carla_light_state(light, light_config) + updated_ids[open_drive_id] = True + else: + if open_drive_id in lights_by_opendrive_id: + light = lights_by_opendrive_id[open_drive_id] + # self._check_light_consistency(light, light_config) + self.metsr_client.update_signal(light_data['ID'], targetPhase=light_data['state']) + + else: + open_drive_id = light_opendrive_ids[0] + if open_drive_id not in updated_ids: + if open_drive_id in light_opendrive_ids: + light = lights_by_opendrive_id[open_drive_id] + self._update_carla_light_state(light, light_config) + updated_ids[open_drive_id] = True + else: + if open_drive_id in lights_by_opendrive_id: + light = lights_by_opendrive_id[open_drive_id] + # self._check_light_consistency(light, light_config) + self.metsr_client.update_signal(light_data['ID'], targetPhase=light_data['state']) + + else: + assert True, f"Failed to find corresponding intersection for METSR light: {light_id}" + + self.metsr_client.tick() + self.carla_world.tick() + + + + def _update_carla_light_state(self, light : carla.TrafficLight, light_config : dict[str: float | carla.libcarla.TrafficLightState]) -> None: + """ + Docstring _update_carla_light_state + + :param light: Single Carla light instance + :type light: Carla Light + :param light_config: dictionary containing new ligh configuration + :type light_config: dict + + Update the state of a light with a specified configuration + """ + light.set_green_time(light_config['green_time']) + light.set_yellow_time(light_config['yellow_time']) + light.set_red_time(light_config['red_time']) + light.set_state(light_config['state']) + + + def _check_light_consistency(self, light : carla.TrafficLight, light_config : dict) -> None: + """ + Checks that a light is configured according to a given config + + :param light: Single Carla light instance + :type light: Carla Light + :param light_config: dictionary containing expected light configuration + :type light_config: dict + """ + light_state_dict = _utils.get_carla_light_state(light) + for key in light_config: + if key in light_state_dict: + if light_config[key] != light_state_dict[key]: + print(f"Carla: {light_state_dict}, Metsr: {light_config}") + break + # assert light_config[key] == light_state_dict[key], f" Contradicting light states encountered: current light : {light_state_dict} proposed state {light_config}" + else: + assert True, f" Incomptible light states encountered due to key error for key {key}" + + + def get_light_config(self, metsr_light_data : dict) -> dict[str: float]: + """ + Docstring for get_light_config + + Generates the equivalent configuration for a Carla light given a metsr light instance + + :param metsr_light_data: Metsr query result for a single light + :type metsr_light_data: dict + """ + metsr_to_carla_states = {0 : carla.libcarla.TrafficLightState.Green, 1 : carla.libcarla.TrafficLightState.Yellow , 2 : carla.libcarla.TrafficLightState.Red } + + light_config = {} + # Assuming that State is consistent across a group TODO VERIFIY assumption + light_config["green_time"] = metsr_light_data['phase_ticks'][0] * self.timestep + light_config["yellow_time"] = metsr_light_data['phase_ticks'][1] * self.timestep + light_config["red_time"] = metsr_light_data['phase_ticks'][2] * self.timestep + light_config["state"] = metsr_to_carla_states[metsr_light_data['state']] + + self.metsr_client.update_signal(metsr_light_data['ID'], targetPhase=metsr_light_data['state']) + return light_config + + + def synchronize_clients(self, obj: Object | list[Object] = None): + """ + Docstring for synchronize_clients + + :param obj: Cosimulation car object + :type obj: Scenic Object[s] + + Default : updates all CoSimulated object states in the METSR simulator + (1) Can choose to specify which objects should be updated with obj arguement + """ + if obj != None and not isinstance(obj, list): + carla_actors = [obj] + elif obj != None and isinstance(obj, list): + carla_actors = obj + else: + carla_actors = [obj for obj in self.objects if obj.carla_actor_flag] + carla_actors.append(self.objects[0]) + + for obj in carla_actors: + loc = obj.carlaActor.get_location() + vehID = self.getMetsrPrivateVehId(obj) + lane = self._nearest_lane(obj) + roadID = self.map_scenic_to_metsr(lane) + veh_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True) + # Update METSR road + if hasattr(obj, "previous_road"): + if roadID != obj.previous_road: + print(f"obj: {obj.name} leaving road: {obj.previous_road} moving to : {roadID}") + # Find the corresponding METSR keys + carla_lane_ids = set([self.map_scenic_to_metsr(lane) for lane in self.frozen_scenic_lanes]) + if roadID not in carla_lane_ids: + print(f"Vehicle is leaving the bubble region") + else: + print(f"Vehicle remains in the bubble region") + self.metsr_client.enter_next_road(vehID, roadID=roadID, private_veh = True) + if obj.previous_road == obj.final_road: + obj.finished_route = True + else: + obj.finished_route = False + # Record previous road location + obj.previous_road = roadID + bearing = get_metsr_rotation(obj.carlaActor.get_transform().rotation.yaw) + # Check if objects are desynchronized + if not math.isclose(loc.x, veh_data["DATA"][0]['x']) or not math.isclose(-loc.y, veh_data["DATA"][0]['y']): + self.metsr_client.teleport_cosim_vehicle(vehID, loc.x, -loc.y, bearing=bearing,private_veh = True, transform_coords = True) + + + + def update_carla_lanes(self, bubble_regions : list[Object] | None = None) -> tuple[list[Lane], list[str], list[str]]: #TODO break this out into multiple functions/helpers + """ + Docstring for update_carla_lanes + + :param bubble_regions: List of objects with a designated "bubble region" constituting the CoSim region + :type obj: List[Object] or None + + Collects all lanes which are intersecting the bubble region + (1) Default region is defined by the ego the region can be updated by passing objects with their corresponding regions + """ + if bubble_regions is None: + # Collect lanes intersecting the bubble + lanes = self.get_carla_lanes() + self.frozen_scenic_lanes = lanes + # Find the corresponding METSR keys + carla_lane_ids = [self.map_scenic_to_metsr(lane) for lane in lanes] + # scenic_lane_ids = [lane.road for lane in lanes] + carla_lane_ids = set(carla_lane_ids) + # Lanes which are already set + curr_frozen_ids = list(self.carla_control_roads.keys()) + + # Collect new and old lanes + new_lanes = [id for id in carla_lane_ids if id not in curr_frozen_ids] + old_lanes = [id for id in list(self.carla_control_roads.keys()) if id not in carla_lane_ids] + + # Update object existance based on bubble changes + return lanes, new_lanes, old_lanes + + + + def update_bubble_objects(self, carla_lanes: list[Lane], new_lanes: list[str], intersections: list[Intersection]) -> None: + """ + Docstring for update_bubble_objects + + :param carla_lanes: a list of Scenic lanes which constitute the cosimulation region + :type carla_lanes: list[Lane] + :param intersections: a list of Scenic intersections which are contained or touching the cosimulated region + (A lane must either intersect or have to connecting roads in the cosimulation region) + :type intersections: list[intersection] + + (1) Remove all objects that form CARLA which have either + (i) finished their associated route + (ii) left the region + (2) Spawn new objects in the Cosimulation region if + (i) Their is enough room in the obj's current location to spawn + (ii) The vehicle is not currently waiting to spawn in a metsr queue + (iii) an equivalent Scenic trajectory can be constructed from the metsr proposed route + + """ + carla_actors = [obj for obj in self.objects if obj.carla_actor_flag] # TODO might need to consider a more efficient data structure here + cosim_data = self.metsr_client.query_coSimVehicle() + for obj in self.objects[1:]: + veh_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True) # + lane = self._nearest_lane(obj) + intersection = obj._intersection + + if obj.carla_actor_flag: + if obj.finished_route: + carla_actors.remove(obj) + print(f'removing vehicle: {obj.name} after completing its route') + print(f"Current road_lane (Scenic) is {f'{lane.road.id}_{lane.id}'}") + print(f"{veh_data['DATA'][0]}") + self.remove_bubble_object(obj) + + elif (lane not in carla_lanes) and (intersection not in intersections): + if obj.spawn_guard == 0: + self.remove_bubble_object(obj) + carla_actors.remove(obj) + + # Add objects to the bubble if they are entering through a new lane and their is not enough space to spawn + elif not obj.carla_actor_flag and (lane in carla_lanes or intersection in intersections): + if 'dist' not in veh_data["DATA"][0] or obj.finished_route: # Check that vehicle is not waiting in queue + continue + + not_enough_space = _utils.within_threshold_to(obj, carla_actors) + + if not_enough_space: + # print(f"Not enough space to spawn {obj.name} at {self.count}:") + # for obj in self.objects: + # car_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True) + # simulator = "carla" if obj.carla_actor_flag else "metsr" + # if obj.carla_actor_flag: + # print(f"{obj} in {simulator}: [Metsr][Scenic] coords X: {car_data['DATA'][0]['x'], obj.position.x}, y:{car_data['DATA'][0]['y'], obj.position.y}") + continue + + # If their is enough room check that the vehicle is not currently queued and spawn + else: + carla_trajectory, route_data = None, None + VehID = self.getMetsrPrivateVehId(obj) + for data_entry in cosim_data['DATA']: + if data_entry['ID'] == VehID: + route_data = data_entry['route'] + trajectory = self.generate_scenic_trajectory(lane, route_data, obj._intersection) + if trajectory != None: + obj.final_road = route_data[-1] + carla_trajectory = self.scenic_trajectory_to_carla(trajectory) + break # Once the trajectory is found continue + + if carla_trajectory == None: + if obj not in self.bubble_spawn_queue: + print(f"No valid trajectory found for vehicle data: {[data_entry['route'] for data_entry in cosim_data['DATA'] if data_entry['ID'] == VehID]} ") + print(f"Skipping spawn for obj: {obj.name} at {lane.road.id}_{lane.id} with intersection {obj._intersection}: due to failed trajectory generation") + print(f"Obj location was: :{obj.x, obj.y} default lane was: {obj._lane.id if obj.lane != None else None} selected lane was: {lane}") + self.bubble_spawn_queue.add(obj) + continue # Do not spawn vehicle if no trajectory can be created + + if obj._intersection in intersections: + print(f"Spawning object {obj} in intersection") + + print(f"Spawning obj: {obj.name} in CARLA at : {obj.x, obj.y}") + self.createObjectInCarla(obj,update_orientation=True, trajectory=carla_trajectory) + carla_actors.append(obj) + + + def scenic_trajectory_to_carla(self, trajectory: list[Lane]) -> list: + """ + Docstring for scenic_trajectory_to_carla + + :param trajectory: Scenic trajectory starting at the vehicles location to their goal destinatino + :type trajectory: list[Lane] + + Convert a list of scenic lanes to an equivalent sequence of CARLA waypoint locations + """ + way_points = [] + # world = self.carla_client.get_world() + for lane in trajectory: + points = [lane.centerline.start, lane.centerline.end] + for point in points: + scenic_pos = point + carla_rot = _utils.scenicToCarlaRotation(orientation=scenic_pos.orientation) + carla_loc = _utils.scenicToCarlaLocation(pos=scenic_pos) + way_point = carla.Transform(carla_loc, carla_rot) + way_points.append(way_point.location) + return way_points + + + def freeze_lanes(self, keys: list[str]) -> None: + """ + Docstring for freeze_lanes + + :param keys: RoadIDs for METSR indexed roads + :type keys: list[str] + + Query Metsr to freeze simulation and control of given lanes + + """ + keys = set(keys) + for key in keys: + assert key not in self.carla_control_roads, "Attempted to freeze already frozen lane" + self.carla_control_roads[key] = True # Keep track of frozen lanes + print(f"Freezing METSR key: {key}") + self.metsr_client.set_cosim_road(key) + + + def release_lanes(self,keys: list[str]) -> None: + """ + Docstring for release_lanes + + :param keys: RoadIDs for METSR indexed roads + :type keys: list[str] + + Query Metsr to begin re-simulating and control given lanes + """ + keys = set(keys) + for key in keys: + assert key in self.carla_control_roads, "Attempted to release non frozen lane" + del self.carla_control_roads[key] # Remove frozen lane from record + print(f"Releasing METSR key: {key}") + self.metsr_client.release_cosim_road(key) + + + def destroy_carla_obj(self,obj) -> None: + """ + Docstring for destroy_carla_obj + + Destroys obj from CARLA simulation + + :param obj: Carla object to be destroyed + """ + if obj.carlaActor is not None: + if isinstance(obj.carlaActor, carla.Vehicle): + obj.carlaActor.set_autopilot(False, self.tm.get_port()) + if isinstance(obj.carlaActor, carla.Walker): + obj.cralaController.stop() + obj.carlaController.destroy() + obj.carlaActor.destroy() + obj.carlaActor = None # Set this to None to prevent reaccess a previously deleted vehicle? + + def remove_bubble_object(self,obj) -> None: + """ + Docstring for remove_bubble_object + + :param obj: object to be deleted + :type obj: Car + """ + obj.carla_actor_flag = False + self.destroy_carla_obj(obj) + obj.trajectory = None + + + def map_scenic_to_metsr(self,lane: Lane) -> str: + """ + Docstring for map_scenic_to_metsr + + :param lane: Lane object to be mapped + :type lane: Lane + :return: Takes a Lane object and computes the corresponding METSR road which holds that lane + :rtype: str + """ + metsr_key=None + # Parent road key with associated lane id + query_key = f'{lane.road.id}_{lane.id}' + + # Check if element is present in map between formats + if query_key in self.scenic_to_metsr_map: + metsr_key = self.scenic_to_metsr_map[query_key] + + metsr_key = metsr_key.split("_")[0] + + # There must be a valid mapping + assert metsr_key is not None, f"Error identifying associated ID for {query_key}" + return metsr_key + + def generate_scenic_trajectory(self, curr_lane : Lane , route: list[str], intersection: Intersection = None) -> list[Lane]: + """ + Docstring for generate_scenic_trajectory + + :param curr_lane: Current lane which the target object is placed on + :type curr_lane: Lane + :param route: Metsr route data for a single car + :type route: list[str] + :param intersection: Current intersection object is on if any + :type intersection: Intersection | None + :return: Equivalent trajetory with Scenic Lanes + :rtype: list[Lane] + + + TODO: This is inefficient I think it will be beforehand generate some of these pairings rather than every + time a new car is spawned. Also -- I am unsure how to enforce trajectory feasibility at the lane level? + + """ + target_start = [] + if intersection is not None: + intersection_roads = intersection.roads + for road in intersection_roads: + target_start.append(str(road.id)) + + # For each road find the corresponding {road}_{lane} pair + map_data = self.scenic_to_metsr_map.items() + valid_lanes = {} + for road in route: + for scenic_key, metsr_key in map_data: + key_road = metsr_key.split("_")[0] + if key_road == road: + if road not in valid_lanes: + valid_lanes[road] = [] + valid_lanes[road].append(scenic_key) + + starting_points = [lane.split("_")[0] for lane in valid_lanes[route[0]]] + target_start = [str(curr_lane.road.id)] + is_valid_trajectory = False + + for target in target_start: + if target in starting_points: + is_valid_trajectory = True + break + if not is_valid_trajectory: + return None + + trajectory = [] + lanes = [*self.workspace.network.lanes] + for i,road in enumerate(route): + target_lanes = valid_lanes[road] + assert len(target_lanes) > 1, f"Failed to find target lanes for road: {road}" + for road_lane in target_lanes: + if len(trajectory) == i+1: + break + for lane in lanes: + scenic_road = f'{lane.road.id}' + query_road = road_lane.split("_")[0] + query_lane = road_lane.split("_")[1] + opposite_traffic_flag = bool(query_lane[0] == "-") + if query_road == scenic_road: + if opposite_traffic_flag and str(lane.id)[0] == "-": + trajectory.append(lane) + break + elif not opposite_traffic_flag and not str(lane.id)[0] == "-": + trajectory.append(lane) + break + + if len(trajectory) < 1: + return None + + # print(f"Found trajectory: {[f'{lane.road.id}_{lane.id}' for lane in trajectory]} for route {route}") + + return trajectory + + + def destroy(self) -> None: + """ + Docstring for destroy + + Destroy both simulators instances i.e (METSR, CARLA) + """ + # METSR destroy + if self.metsr_client.verbose: + print("Client Messages Log:") + print("[") + for call in self.client._messagesLog: + print(f" {call},") + print("]") + + # "CARLA destroy" + for obj in self.objects: + if obj.carlaActor is not None: + if isinstance(obj.carlaActor, carla.Vehicle): + obj.carlaActor.set_autopilot(False, self.tm.get_port()) + if isinstance(obj.carlaActor, carla.Walker): + obj.carlaController.stop() + obj.carlaController.destroy() + obj.carlaActor.destroy() + if self.render and self.cameraManager: + self.cameraManager.destroy_sensor() + + self.carla_client.stop_recorder() + super().destroy() + + + def _nearest_lane(self,obj, allow_offlane=True) -> Lane | None : # TODO :: Update lane logic to consider intersections + """ + Docstring for _nearest_lane + + Return the nearest lane to the object + + TODO: Allow a user specified mode where cars are NOT allowed to leave the road? + TODO: Ensure all objects are cars + """ + lane = obj._lane + if lane: + nearest_lane = lane + else: + if not allow_offlane: + assert True, f"Object: {obj.name} is has left the roadway" + lanes = [*self.workspace.network.lanes] + distances = [(lane.distanceTo(obj.position), lane) for lane in lanes] + nearest_lane = min(distances, key=lambda t: t[0])[1] # min distance over all lanes + + return nearest_lane + + + def executeActions(self, allActions) -> None: + """ + Docstring for executeActions + + Apply control updates which were accumulated while executing the actions + Filters out actions for Carla only objects + + :param allActions: ? + """ + carla_actions = {} + for obj in self.agents: + if obj.carla_actor_flag: + carla_actions[obj] = allActions[obj] + + + super().executeActions(carla_actions) + + for obj in self.agents: + if obj.carla_actor_flag: + ctrl = obj._control + if ctrl is not None: + obj.carlaActor.apply_control(ctrl) + obj._control = None + + + def updateObjects(self) -> None: + """ + Docstring for updateObjects + + Update object properties for METSR simulated objects + """ + metsr_obj = [obj for obj in self.objects if not obj.carla_actor_flag] + metsr_obj.append(self.objects[0]) + obj_veh_ids = [self.getMetsrPrivateVehId(obj) for obj in self.objects] + raw_veh_data = self.metsr_client.query_vehicle(obj_veh_ids, True, True) + self.obj_data_cache = {obj: raw_veh_data['DATA'][i] for i, obj in enumerate(self.objects)} + + # #DEBUGGING FOR METSR + # for obj in self.obj_data_cache: + # if 'dist' not in self.obj_data_cache[obj]: + # self.queued_vehicles[obj] = True + # elif 'dist' in self.obj_data_cache[obj] and obj in self.queued_vehicles: + # print(f"obj {obj} leaving the spawn queue") + # del self.queued_vehicles[obj] + + super().updateObjects() + self.obj_data_cache = None + + def check_world_state_consistency(self): + """ + Docstring for check_world_state_consistency + + self: CoSimulation Object + + Compares the state of each vehicle in each simulator + Displays any vehicles which are outside of the tolerance threshold + """ + for obj in self.objects: + if obj.carla_actor_flag: + car_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True) + metsr_x, metsr_y = car_data['DATA'][0]['x'], car_data['DATA'][0]['y'] + loc = obj.carlaActor.get_location() + carla_x, carla_y = loc.x, loc.y + in_metsr_queue = not bool("dist" in car_data['DATA'][0]) + + if not np.isclose(metsr_x, carla_x): + print(f"Checking Object synchronization before Stepping") + print(f"Obj: {obj} in metsr queue? {in_metsr_queue}") + print("=" * 25) + lane = obj._lane + if lane: + print(f"OBJ lane: {self.map_scenic_to_metsr(obj._lane)}") + print(f"OBJ X: {obj}| METSR {metsr_x}: CARLA {carla_x}") + if not np.isclose(metsr_y, -carla_y): + print(f"OBJ Y: {obj}| METSR {metsr_y}: CARLA {-carla_y}") + print("=" * 25) + + metsr_data = self.metsr_client.query_coSimVehicle() + print(f"Displaying METSR query data: {metsr_data}") + + + def _check_traffic_light_consistency(self): + """ + docstring for check_traffic_light_consistency + + """ + signals_ids = self.metsr_client.query_signal()['id_list'] + signal_data = self.metsr_client.query_signal(signals_ids) + + carla_world = self.carla_client.get_world() + carla_traffic_lights = carla_world.get_actors().filter('traffic.traffic_light*') + + lights_by_opendrive_id = {light.get_opendrive_id(): light for light in carla_traffic_lights} + for light_data in signal_data["DATA"]: + light_id = light_data["groupID"] + + if light_id in self.xml_to_xodr_intersections: + light_opendrive_ids = self.xml_to_xodr_intersections[light_id] + light_config = self.get_light_config(light_data) + if len(light_opendrive_ids) > 1: + for open_drive_id in light_opendrive_ids: + if open_drive_id in lights_by_opendrive_id: + light = lights_by_opendrive_id[open_drive_id] + self._check_light_consistency(light, light_config) + else: + print(f'Unable to find associated light for opendrive_id : {open_drive_id}') + + else: + open_drive_id = light_opendrive_ids[0] + if open_drive_id in light_opendrive_ids: + light = lights_by_opendrive_id[open_drive_id] + self._check_light_consistency(light, light_config) + else: + print(f'Unable to find associated light for opendrive_id : {open_drive_id}') + else: + assert True, f"Failed to find corresponding intersection for METSR light: {light_id}" + + + +# Leftovers I am preserving in case they become useful +""" + # metsr_light_group = {} + + # for opendrive_id in list(lights_by_opendrive_id.keys()): + # associated_lights = [] + # for light_data in signal_data["DATA"]: + # key = light_data["groupID"] + # targets= self.xml_to_xodr_intersections[key] + # if len(targets) > 1: + # for target in targets: + # if target == opendrive_id: + # associated_lights.append(light_data["ID"]) + # else: + # target = targets[0] + # if target == opendrive_id: + # associated_lights.append(light_data["ID"]) + + # metsr_light_group[opendrive_id] = associated_lights + + # print(f"Displaying configs for : {opendrive_id}") + # for id in metsr_light_group[opendrive_id]: + # data = self.metsr_client.query_signal(id)["DATA"][0] + # light_config = self.get_light_config(data) + # print(light_config) + # print(f"==========================================") + + # # print(f"finishing consistency check at : {self.count}") + +""" \ No newline at end of file From 71df3654979c40b087e045a018a44f3eec78827f Mon Sep 17 00:00:00 2001 From: Kay Date: Mon, 9 Mar 2026 16:12:59 -0700 Subject: [PATCH 48/73] debug failed trajectory --- src/scenic/simulators/cosim/simulator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scenic/simulators/cosim/simulator.py b/src/scenic/simulators/cosim/simulator.py index 998ea82fe..9fb6e61ea 100644 --- a/src/scenic/simulators/cosim/simulator.py +++ b/src/scenic/simulators/cosim/simulator.py @@ -933,7 +933,7 @@ def update_bubble_objects(self, carla_lanes: list[Lane], new_lanes: list[str], i if obj not in self.bubble_spawn_queue: print(f"No valid trajectory found for vehicle data: {[data_entry['route'] for data_entry in cosim_data['DATA'] if data_entry['ID'] == VehID]} ") print(f"Skipping spawn for obj: {obj.name} at {lane.road.id}_{lane.id} with intersection {obj._intersection}: due to failed trajectory generation") - print(f"Obj location was: :{obj.x, obj.y} default lane was: {obj._lane.id if obj.lane != None else None} selected lane was: {lane}") + print(f"Obj location was: :{obj.x, obj.y} default road was: {obj._lane.road.id if obj.lane != None else None} selected road was: {lane.road.id}") self.bubble_spawn_queue.add(obj) continue # Do not spawn vehicle if no trajectory can be created From 069b3ec9310247671baed36bbaa79e40b51a68f1 Mon Sep 17 00:00:00 2001 From: Kay Date: Mon, 9 Mar 2026 16:15:24 -0700 Subject: [PATCH 49/73] minor --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b0a3858a3..9985ac5d8 100644 --- a/.gitignore +++ b/.gitignore @@ -146,4 +146,5 @@ simulation.gif # Random test.ipynb -test.sh \ No newline at end of file +test.sh +output.txt \ No newline at end of file From 920043cd3ba6f9bf5cee35410fb914e06993f6da Mon Sep 17 00:00:00 2001 From: Kay Date: Mon, 9 Mar 2026 18:30:30 -0700 Subject: [PATCH 50/73] Fixed simulator synchronization --- src/scenic/simulators/cosim/simulator.py | 67 +++++++++++++----------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/src/scenic/simulators/cosim/simulator.py b/src/scenic/simulators/cosim/simulator.py index 9fb6e61ea..0746f4d6e 100644 --- a/src/scenic/simulators/cosim/simulator.py +++ b/src/scenic/simulators/cosim/simulator.py @@ -376,7 +376,7 @@ def createObjectInCarla(self, obj: Object, update_orientation: bool = False, tra # print(f"{car} in {simulator}: [Metsr][Scenic] coords X: {car_data['DATA'][0]['x'], car.position.x}, y:{car_data['DATA'][0]['y'], car.position.y}") print(f"Query Vehicle resuts for obj: :{car}, is: {car_data['DATA']}") - print(f"Checking distance function: {utils.within_threshold_to(obj,[obj for obj in self.objects if obj.carla_actor_flag], verbose=True)}") + print(f"Checking distance function: {_utils.within_threshold_to(obj,[obj for obj in self.objects if obj.carla_actor_flag], verbose=True)}") print(f"Issue occured at timestop: {self.count}") # raise e(f"Error : {e} occured") if carlaActor is None: @@ -567,13 +567,13 @@ def getMetsrPrivateVehId(self, obj: Object) -> int: def step(self) -> None: """ - Step both simulators -> Update ego bubble - Update actor locations in either bubble - - TODO : Verify fixed! - Seems like carla needs to be stepped after creating a new object - TODO : Verify fixed! - If a NON-ego car spawns in the bubble (@ step 0) getproperties fails (carlaActor == None) - TODO : METSR vrs CARLA deviation METSR queue spawning occupied CARLA position + Docstring for step + Step both simulators: + (1): Update the high fidelity region based on the ego's new locatin + (2): Spawn and destroy objects according to region changes + (3): Tick both clients and synchronize states + (4): Update spawn protection counters for newly created objects """ lanes, new_lanes, old_lanes = self.update_carla_lanes() self.release_lanes(old_lanes) @@ -803,6 +803,9 @@ def synchronize_clients(self, obj: Object | list[Object] = None): for obj in carla_actors: loc = obj.carlaActor.get_location() + if loc == carla.Location(0,0,0): # Carla object still in the process of spawning + print(f'Passing sychronization while object spawn is processing') + continue vehID = self.getMetsrPrivateVehId(obj) lane = self._nearest_lane(obj) roadID = self.map_scenic_to_metsr(lane) @@ -886,7 +889,7 @@ def update_bubble_objects(self, carla_lanes: list[Lane], new_lanes: list[str], i veh_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True) # lane = self._nearest_lane(obj) intersection = obj._intersection - + # Remove objects if they have completed their route or left the bubble region if obj.carla_actor_flag: if obj.finished_route: carla_actors.remove(obj) @@ -899,22 +902,22 @@ def update_bubble_objects(self, carla_lanes: list[Lane], new_lanes: list[str], i if obj.spawn_guard == 0: self.remove_bubble_object(obj) carla_actors.remove(obj) - - # Add objects to the bubble if they are entering through a new lane and their is not enough space to spawn + + # Skip spawning objects if there is not enough space or they are currently in a spawn queue elif not obj.carla_actor_flag and (lane in carla_lanes or intersection in intersections): - if 'dist' not in veh_data["DATA"][0] or obj.finished_route: # Check that vehicle is not waiting in queue + if 'dist' not in veh_data["DATA"][0] or obj.finished_route: continue not_enough_space = _utils.within_threshold_to(obj, carla_actors) if not_enough_space: - # print(f"Not enough space to spawn {obj.name} at {self.count}:") - # for obj in self.objects: - # car_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True) - # simulator = "carla" if obj.carla_actor_flag else "metsr" - # if obj.carla_actor_flag: - # print(f"{obj} in {simulator}: [Metsr][Scenic] coords X: {car_data['DATA'][0]['x'], obj.position.x}, y:{car_data['DATA'][0]['y'], obj.position.y}") + if obj not in self.bubble_spawn_queue: + _utils.within_threshold_to(obj,carla_actors, verbose=True) + print(f"Not enough space to spawn {obj.name} at {self.count} in location: {obj.position}:") + self.bubble_spawn_queue.add(obj) + self.check_world_state_consistency() continue + # If their is enough room check that the vehicle is not currently queued and spawn else: @@ -942,6 +945,9 @@ def update_bubble_objects(self, carla_lanes: list[Lane], new_lanes: list[str], i print(f"Spawning obj: {obj.name} in CARLA at : {obj.x, obj.y}") self.createObjectInCarla(obj,update_orientation=True, trajectory=carla_trajectory) + + if obj in self.bubble_spawn_queue: + self.bubble_spawn_queue.remove(obj) carla_actors.append(obj) @@ -981,7 +987,7 @@ def freeze_lanes(self, keys: list[str]) -> None: for key in keys: assert key not in self.carla_control_roads, "Attempted to freeze already frozen lane" self.carla_control_roads[key] = True # Keep track of frozen lanes - print(f"Freezing METSR key: {key}") + # print(f"Freezing METSR key: {key}") self.metsr_client.set_cosim_road(key) @@ -994,11 +1000,11 @@ def release_lanes(self,keys: list[str]) -> None: Query Metsr to begin re-simulating and control given lanes """ - keys = set(keys) + keys = set(keys) for key in keys: assert key in self.carla_control_roads, "Attempted to release non frozen lane" del self.carla_control_roads[key] # Remove frozen lane from record - print(f"Releasing METSR key: {key}") + # print(f"Releasing METSR key: {key}") self.metsr_client.release_cosim_road(key) @@ -1078,7 +1084,7 @@ def generate_scenic_trajectory(self, curr_lane : Lane , route: list[str], inters for road in intersection_roads: target_start.append(str(road.id)) - # For each road find the corresponding {road}_{lane} pair + # Collect All valid spawn locations map_data = self.scenic_to_metsr_map.items() valid_lanes = {} for road in route: @@ -1229,7 +1235,7 @@ def updateObjects(self) -> None: super().updateObjects() self.obj_data_cache = None - def check_world_state_consistency(self): + def check_world_state_consistency(self) -> None: """ Docstring for check_world_state_consistency @@ -1238,8 +1244,9 @@ def check_world_state_consistency(self): Compares the state of each vehicle in each simulator Displays any vehicles which are outside of the tolerance threshold """ - for obj in self.objects: - if obj.carla_actor_flag: + print(f"Displaying in Objects in CARLA with inconsistent state") + for i,obj in enumerate(self.objects): + if obj.carla_actor_flag or i == 0: car_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True) metsr_x, metsr_y = car_data['DATA'][0]['x'], car_data['DATA'][0]['y'] loc = obj.carlaActor.get_location() @@ -1247,19 +1254,19 @@ def check_world_state_consistency(self): in_metsr_queue = not bool("dist" in car_data['DATA'][0]) if not np.isclose(metsr_x, carla_x): - print(f"Checking Object synchronization before Stepping") print(f"Obj: {obj} in metsr queue? {in_metsr_queue}") print("=" * 25) lane = obj._lane if lane: - print(f"OBJ lane: {self.map_scenic_to_metsr(obj._lane)}") - print(f"OBJ X: {obj}| METSR {metsr_x}: CARLA {carla_x}") + print(f"OBJ Road: {self.map_scenic_to_metsr(self._nearest_lane(obj))}") + print(f"OBJ X: {obj}| METSR {metsr_x}: CARLA {carla_x}: SCENIC {obj.position[0]}") if not np.isclose(metsr_y, -carla_y): - print(f"OBJ Y: {obj}| METSR {metsr_y}: CARLA {-carla_y}") + print(f"OBJ Y: {obj}| METSR {metsr_y}: CARLA {-carla_y}: SCENIC {obj.position[1]}") print("=" * 25) + print(f"Finished checking world staet") - metsr_data = self.metsr_client.query_coSimVehicle() - print(f"Displaying METSR query data: {metsr_data}") + # metsr_data = self.metsr_client.query_coSimVehicle() + # print(f"Displaying METSR query data: {metsr_data}") def _check_traffic_light_consistency(self): From 3122915894d62c6246c56fac32103e6160d41737 Mon Sep 17 00:00:00 2001 From: Kay Date: Mon, 16 Mar 2026 12:12:31 -0700 Subject: [PATCH 51/73] merged in globalParam fix --- src/scenic/syntax/veneer.py | 4317 ++++++++++++++++++----------------- 1 file changed, 2160 insertions(+), 2157 deletions(-) diff --git a/src/scenic/syntax/veneer.py b/src/scenic/syntax/veneer.py index 4b550fbdd..5d4f06816 100644 --- a/src/scenic/syntax/veneer.py +++ b/src/scenic/syntax/veneer.py @@ -1,2157 +1,2160 @@ -"""Python implementations of Scenic language constructs. - -This module is automatically imported by all Scenic programs. In addition to -defining the built-in functions, operators, specifiers, etc., it also stores -global state such as the list of all created Scenic objects. - -.. highlight:: scenic-grammar -""" - -__all__ = ( - # Primitive statements and functions - "ego", - "workspace", - "new", - "require", - "resample", - "param", - "globalParameters", - "mutate", - "verbosePrint", - "localPath", - "model", - "simulator", - "simulation", - "require_monitor", - "terminate_when", - "terminate_simulation_when", - "terminate_after", - "in_initial_scenario", - "override", - "record", - "record_initial", - "record_final", - "sin", - "cos", - "hypot", - "max", - "min", - "_toStrScenic", - "_toFloatScenic", - "_toIntScenic", - "filter", - "round", - "len", - "range", - # Prefix operators - "Visible", - "NotVisible", - "Front", - "Back", - "Left", - "Right", - "FrontLeft", - "FrontRight", - "BackLeft", - "BackRight", - "Top", - "Bottom", - "TopFrontLeft", - "TopFrontRight", - "TopBackLeft", - "TopBackRight", - "BottomFrontLeft", - "BottomFrontRight", - "BottomBackLeft", - "BottomBackRight", - "RelativeHeading", - "ApparentHeading", - "RelativePosition", - "DistanceFrom", - "DistancePast", - "Follow", - "AngleTo", - "AngleFrom", - "AltitudeTo", - "AltitudeFrom", - # Infix operators - "FieldAt", - "RelativeTo", - "OffsetAlong", - "CanSee", - "Intersects", - "Until", - "Implies", - "VisibleFromOp", - "NotVisibleFromOp", - # Primitive types - "Vector", - "Orientation", - "VectorField", - "PolygonalVectorField", - "Shape", - "MeshShape", - "BoxShape", - "CylinderShape", - "ConeShape", - "SpheroidShape", - "MeshVolumeRegion", - "MeshSurfaceRegion", - "BoxRegion", - "SpheroidRegion", - "PathRegion", - "Region", - "PointSetRegion", - "RectangularRegion", - "CircularRegion", - "SectorRegion", - "PolygonalRegion", - "PolylineRegion", - "Workspace", - "Mutator", - "Range", - "DiscreteRange", - "Options", - "Uniform", - "Discrete", - "Normal", - "TruncatedNormal", - "VerifaiParameter", - "VerifaiRange", - "VerifaiDiscreteRange", - "VerifaiOptions", - # Constructible types - "Point", - "OrientedPoint", - "Object", - # Specifiers - "With", - "At", - "In", - "ContainedIn", - "On", - "Beyond", - "VisibleFrom", - "NotVisibleFrom", - "VisibleSpec", - "NotVisibleSpec", - "OffsetBy", - "OffsetAlongSpec", - "Facing", - "ApparentlyFacing", - "FacingToward", - "FacingDirectlyToward", - "FacingAwayFrom", - "FacingDirectlyAwayFrom", - "LeftSpec", - "RightSpec", - "Ahead", - "Behind", - "Above", - "Below", - "Following", - # Constants - "everywhere", - "nowhere", - # Exceptions - "GuardViolation", - "PreconditionViolation", - "InvariantViolation", - "RejectionException", - # Internal APIs # TODO remove? - "_scenic_default", - "Behavior", - "Monitor", - "_makeTerminationAction", - "_makeSimulationTerminationAction", - "BlockConclusion", - "runTryInterrupt", - "wrapStarredValue", - "callWithStarArgs", - "Modifier", - "DynamicScenario", - # Proposition Factories - "AtomicProposition", - "PropositionAnd", - "PropositionOr", - "PropositionNot", - "Always", - "Eventually", - "Next", -) - -# various Python types and functions used in the language but defined elsewhere -from scenic.core.distributions import ( - DiscreteRange, - Normal, - Options, - RandomControlFlowError, - Range, - TruncatedNormal, - Uniform, -) -from scenic.core.dynamics.behaviors import Behavior, Monitor -from scenic.core.dynamics.guards import ( - GuardViolation, - InvariantViolation, - PreconditionViolation, -) -from scenic.core.dynamics.invocables import BlockConclusion, runTryInterrupt -from scenic.core.dynamics.scenarios import DynamicScenario -from scenic.core.external_params import ( - VerifaiDiscreteRange, - VerifaiOptions, - VerifaiParameter, - VerifaiRange, -) -from scenic.core.geometry import cos, hypot, max, min, sin -from scenic.core.object_types import Mutator, Object, OrientedPoint, Point -from scenic.core.regions import ( - BoxRegion, - CircularRegion, - MeshSurfaceRegion, - MeshVolumeRegion, - PathRegion, - PointSetRegion, - PolygonalRegion, - PolylineRegion, - RectangularRegion, - Region, - SectorRegion, - SpheroidRegion, - everywhere, - nowhere, -) -from scenic.core.shapes import ( - BoxShape, - ConeShape, - CylinderShape, - MeshShape, - Shape, - SpheroidShape, -) -from scenic.core.specifiers import PropertyDefault as _scenic_default -from scenic.core.vectors import PolygonalVectorField, Vector, VectorField -from scenic.core.workspaces import Workspace - -Discrete = Options - -# isort: split - -# everything that should not be directly accessible from the language is imported here: -import builtins -import collections.abc -from contextlib import contextmanager -import functools -import importlib -import numbers -from pathlib import Path -import sys -import traceback -import typing -import warnings - -from scenic.core.distributions import ( - Distribution, - MultiplexerDistribution, - RejectionException, - StarredDistribution, - TupleDistribution, - canUnpackDistributions, - distributionFunction, - needsSampling, - toDistribution, -) -from scenic.core.dynamics.actions import _EndScenarioAction, _EndSimulationAction -import scenic.core.errors as errors -from scenic.core.errors import InvalidScenarioError, ScenicSyntaxError -from scenic.core.external_params import ExternalParameter -from scenic.core.geometry import apparentHeadingAtPoint, normalizeAngle -from scenic.core.lazy_eval import ( - DelayedArgument, - isLazy, - needsLazyEvaluation, - requiredProperties, - valueInContext, -) -import scenic.core.object_types -from scenic.core.object_types import Constructible, Object2D, OrientedPoint2D, Point2D -import scenic.core.propositions as propositions -from scenic.core.regions import convertToFootprint -import scenic.core.requirements as requirements -from scenic.core.simulators import RejectSimulationException -from scenic.core.specifiers import ModifyingSpecifier, Specifier -from scenic.core.type_support import ( - Heading, - canCoerce, - coerce, - evaluateRequiringEqualTypes, - isA, - toHeading, - toOrientation, - toScalar, - toType, - toTypes, - toVector, - underlyingType, -) -from scenic.core.vectors import Orientation, alwaysGlobalOrientation - -### Internals - -activity = 0 -currentScenario = None -scenarioStack = [] -scenarios = [] -evaluatingRequirement = False -_globalParameters = {} -lockedParameters = set() -lockedModel = None -loadingModel = False -currentSimulation = None -inInitialScenario = True -runningScenarios = [] # in order, oldest first -currentBehavior = None -simulatorFactory = None -evaluatingGuard = False -mode2D = False -_originalConstructibles = (Point, OrientedPoint, Object) -BUFFERING_PITCH = 0.1 - -## APIs used internally by the rest of Scenic - -# Scenic compilation - - -def isActive(): - """Are we in the middle of compiling a Scenic module? - - The 'activity' global can be >1 when Scenic modules in turn import other - Scenic modules. - """ - return activity > 0 - - -def activate(options, namespace=None): - """Activate the veneer when beginning to compile a Scenic module.""" - global activity, _globalParameters, lockedParameters, lockedModel, currentScenario - if options.paramOverrides or options.modelOverride: - assert activity == 0 - _globalParameters.update(options.paramOverrides) - lockedParameters = set(options.paramOverrides) - lockedModel = options.modelOverride - - # If we are in 2D mode, set the global flag and replace all classes - # with their 2D compatibility counterparts. - if options.mode2D: - global mode2D, Point, OrientedPoint, Object - assert mode2D or activity == 0 - mode2D = True - Point = Point2D - OrientedPoint = OrientedPoint2D - Object = Object2D - scenic.core.object_types.Point = Point - scenic.core.object_types.OrientedPoint = OrientedPoint - scenic.core.object_types.Object = Object - - activity += 1 - assert not evaluatingRequirement - assert not evaluatingGuard - assert currentSimulation is None - # placeholder scenario for top-level code - newScenario = DynamicScenario._dummy(namespace) - scenarioStack.append(newScenario) - currentScenario = newScenario - - -def deactivate(): - """Deactivate the veneer after compiling a Scenic module.""" - global activity, _globalParameters, lockedParameters, lockedModel, mode2D - global currentScenario, scenarios, scenarioStack, simulatorFactory - activity -= 1 - assert activity >= 0 - assert not evaluatingRequirement - assert not evaluatingGuard - assert currentSimulation is None - scenarioStack.pop() - assert len(scenarioStack) == activity - scenarios = [] - - if activity == 0: - lockedParameters = set() - lockedModel = None - currentScenario = None - simulatorFactory = None - _globalParameters = {} - - if mode2D: - global Point, OrientedPoint, Object - mode2D = False - Point, OrientedPoint, Object = _originalConstructibles - scenic.core.object_types.Point = Point - scenic.core.object_types.OrientedPoint = OrientedPoint - scenic.core.object_types.Object = Object - else: - currentScenario = scenarioStack[-1] - - -# Instance/Object creation - - -def registerInstance(inst): - """Add a Scenic instance to the global list of created objects. - - This is called by the Point/OrientedPoint constructor. - """ - if currentScenario: - assert isinstance(inst, Constructible) - currentScenario._registerInstance(inst) - - -def registerObject(obj): - """Add a Scenic object to the global list of created objects. - - This is called by the Object constructor. - """ - if evaluatingRequirement: - raise InvalidScenarioError("tried to create an object inside a requirement") - elif currentBehavior is not None: - raise InvalidScenarioError("tried to create an object inside a behavior") - elif activity > 0 or currentScenario: - assert not evaluatingRequirement - assert isinstance(obj, Object) - currentScenario._registerObject(obj) - if currentSimulation: - currentSimulation._createObject(obj) - - -# External parameter creation - - -def registerExternalParameter(value): - """Register a parameter whose value is given by an external sampler.""" - if activity > 0: - assert isinstance(value, ExternalParameter) - currentScenario._externalParameters.append(value) - - -# Function call support - - -def wrapStarredValue(value, lineno): - if isinstance(value, TupleDistribution) or not needsSampling(value): - return value - elif isinstance(value, Distribution): - return [StarredDistribution(value, lineno)] - else: - raise TypeError(f"iterable unpacking cannot be applied to {value}") - - -def callWithStarArgs(_func_to_call, *args, **kwargs): - if not canUnpackDistributions(_func_to_call): - # wrap function to delay evaluation until starred distributions are sampled - _func_to_call = distributionFunction(_func_to_call) - return _func_to_call(*args, **kwargs) - - -# Simulations - - -def instantiateSimulator(factory, params): - global _globalParameters - assert not _globalParameters # TODO improve hack? - _globalParameters = dict(params) - try: - return factory() - finally: - _globalParameters = {} - - -def beginSimulation(sim): - global currentSimulation, currentScenario, inInitialScenario, runningScenarios - global _globalParameters - if isActive(): - raise RuntimeError("tried to start simulation during Scenic compilation!") - assert currentSimulation is None - assert currentScenario is None - assert not scenarioStack - currentSimulation = sim - currentScenario = sim.scene.dynamicScenario - runningScenarios = [] # will be updated by DynamicScenario._start - inInitialScenario = currentScenario._setup is None - currentScenario._bindTo(sim.scene) - _globalParameters = dict(sim.scene.params) - - # rebind globals that could be referenced by behaviors to their sampled values - for modName, ( - namespace, - sampledNS, - originalNS, - ) in sim.scene.behaviorNamespaces.items(): - namespace.clear() - namespace.update(sampledNS) - - -def endSimulation(sim): - global currentSimulation, currentScenario, currentBehavior, runningScenarios - global _globalParameters - currentSimulation = None - currentScenario = None - runningScenarios = [] - currentBehavior = None - _globalParameters = {} - - for modName, ( - namespace, - sampledNS, - originalNS, - ) in sim.scene.behaviorNamespaces.items(): - namespace.clear() - namespace.update(originalNS) - - -def simulationInProgress(): - return currentSimulation is not None - - -# Requirements - - -@contextmanager -def executeInRequirement(scenario, boundEgo, values): - global evaluatingRequirement, currentScenario - assert activity == 0 - assert not evaluatingRequirement - evaluatingRequirement = True - if currentScenario is None: - currentScenario = scenario - clearScenario = True - else: - assert currentScenario is scenario - clearScenario = False - oldEgo = currentScenario._ego - oldObjects = currentScenario._objects - - currentScenario._objects = tuple(values[obj] for obj in currentScenario.objects) - - if boundEgo: - currentScenario._ego = boundEgo - try: - yield - except RandomControlFlowError as e: - # Such errors should not be possible inside a requirement, since all values - # should have already been sampled: something's gone wrong with our rebinding. - raise RuntimeError("internal error: requirement dependency not sampled") from e - finally: - evaluatingRequirement = False - currentScenario._ego = oldEgo - currentScenario._objects = oldObjects - if clearScenario: - currentScenario = None - - -# Dynamic scenarios - - -def registerDynamicScenarioClass(cls): - scenarios.append(cls) - - -@contextmanager -def executeInScenario(scenario, inheritEgo=False): - global currentScenario - oldScenario = currentScenario - if inheritEgo and oldScenario is not None: - scenario._ego = oldScenario._ego # inherit ego from parent - currentScenario = scenario - try: - yield - except AttributeError as e: - # Convert confusing AttributeErrors from trying to access nonexistent scenario - # variables into NameErrors, which is what the user would expect. The information - # needed to do this was made available in Python 3.10, but unfortunately could be - # wrong until 3.10.3: see bpo-46940. - if sys.version_info >= (3, 10, 3) and isinstance(e.obj, DynamicScenario): - newExc = NameError(f"name '{e.name}' is not defined", name=e.name) - raise newExc.with_traceback(e.__traceback__) - else: - raise - finally: - currentScenario = oldScenario - - -def prepareScenario(scenario): - if currentSimulation: - verbosePrint(f"Starting scenario {scenario}", level=3) - - -def finishScenarioSetup(scenario): - global inInitialScenario - inInitialScenario = False - - -def startScenario(scenario): - assert scenario not in runningScenarios - runningScenarios.append(scenario) - - -def endScenario(scenario, reason, quiet=False): - runningScenarios.remove(scenario) - if not quiet: - verbosePrint(f"Stopping scenario {scenario} because: {reason}", level=3) - - -# Dynamic behaviors - - -@contextmanager -def executeInBehavior(behavior): - global currentBehavior - oldBehavior = currentBehavior - currentBehavior = behavior - try: - yield - except AttributeError as e: - # See comment for corresponding code in executeInScenario - if sys.version_info >= (3, 10, 3) and isinstance(e.obj, Behavior): - newExc = NameError(f"name '{e.name}' is not defined", name=e.name) - raise newExc.with_traceback(e.__traceback__) - else: - raise - finally: - currentBehavior = oldBehavior - - -@contextmanager -def executeInGuard(): - global evaluatingGuard - assert not evaluatingGuard - evaluatingGuard = True - try: - yield - finally: - evaluatingGuard = False - - -def _makeTerminationAction(agent, line): - assert activity == 0 - if agent: - scenario = agent._parentScenario() - assert scenario is not None - else: - scenario = None - return _EndScenarioAction(scenario, line) - - -def _makeSimulationTerminationAction(line): - assert activity == 0 - return _EndSimulationAction(line) - - -### Parsing support - - -class Modifier(typing.NamedTuple): - name: str - value: typing.Any - terminator: typing.Optional[str] = None - - -### Primitive statements and functions - - -def new(cls, specifiers): - if not (isinstance(cls, type) and issubclass(cls, Constructible)): - raise TypeError(f'"{cls.__name__}" is not a Scenic class') - return cls._withSpecifiers(specifiers) - - -def ego(obj=None): - """Function implementing loads and stores to the 'ego' pseudo-variable. - - The translator calls this with no arguments for loads, and with the source - value for stores. - """ - egoObject = currentScenario._ego - if obj is None: - if egoObject is None: - raise InvalidScenarioError("referred to ego object not yet assigned") - elif not isinstance(obj, Object): - if isinstance(obj, type) and issubclass(obj, Object): - suffix = " (perhaps you forgot 'new'?)" - else: - suffix = "" - ty = type(obj).__name__ - raise TypeError(f"tried to make non-object (of type {ty}) the ego object{suffix}") - else: - currentScenario._ego = obj - for scenario in runningScenarios: - if scenario._ego is None: - scenario._ego = obj - return egoObject - - -def workspace(workspace=None): - """Function implementing loads and stores to the 'workspace' pseudo-variable. - - See `ego`. - """ - if workspace is None: - if currentScenario._workspace is None: - raise InvalidScenarioError("referred to workspace not yet assigned") - elif not isinstance(workspace, Workspace): - raise TypeError(f"workspace {workspace} is not a Workspace") - elif needsSampling(workspace): - raise InvalidScenarioError("workspace must be a fixed region") - elif needsLazyEvaluation(workspace): - raise InvalidScenarioError( - "workspace uses value undefined " "outside of object definition" - ) - else: - currentScenario._workspace = workspace - return currentScenario._workspace - - -def require(reqID, req, line, name, prob=1): - """Function implementing the require statement.""" - if not name: - name = f"requirement on line {line}" - if evaluatingRequirement: - raise InvalidScenarioError("tried to create a requirement inside a requirement") - if req.has_temporal_operator and prob != 1: - raise InvalidScenarioError( - "requirements with temporal operators must have probability of 1" - ) - if currentSimulation is not None: # requirement being evaluated at runtime - if req.has_temporal_operator: - # support monitors on dynamic requirements and create dynamic requirements - currentScenario._addDynamicRequirement( - requirements.RequirementType.require, req, line, name - ) - else: - if prob >= 1 or Range(0, 1) <= prob: # use Range so value can be recorded - result = req.evaluate() - assert not needsSampling(result) - if needsLazyEvaluation(result): - raise InvalidScenarioError( - f"requirement on line {line} uses value" - " undefined outside of object definition" - ) - if not result: - raise RejectSimulationException(name) - else: # requirement being defined at compile time - currentScenario._addRequirement( - requirements.RequirementType.require, reqID, req, line, name, prob - ) - - -def require_monitor(reqID, value, line, name): - if not name: - name = f"requirement on line {line}" - if currentSimulation is not None: - monitor = value.evaluate() - assert not needsSampling(monitor) - if needsLazyEvaluation(monitor): - raise InvalidScenarioError( - f"requirement on line {line} uses value" - " undefined outside of object definition" - ) - if not isinstance(monitor, Monitor): - raise TypeError(f'"require monitor X" with X not a monitor on line {line}') - currentScenario._addMonitor(monitor) - else: - currentScenario._addRequirement( - requirements.RequirementType.monitor, reqID, value, line, name, 1 - ) - - -def record(reqID, value, line, name): - if not name: - name = f"record{line}" - makeRequirement(requirements.RequirementType.record, reqID, value, line, name) - - -def record_initial(reqID, value, line, name): - if not name: - name = f"record{line}" - makeRequirement(requirements.RequirementType.recordInitial, reqID, value, line, name) - - -def record_final(reqID, value, line, name): - if not name: - name = f"record{line}" - makeRequirement(requirements.RequirementType.recordFinal, reqID, value, line, name) - - -def require_always(reqID, req, line, name): - """Function implementing the 'require always' statement.""" - if not name: - name = f"requirement on line {line}" - makeRequirement(requirements.RequirementType.requireAlways, reqID, req, line, name) - - -def require_eventually(reqID, req, line, name): - """Function implementing the 'require eventually' statement.""" - if not name: - name = f"requirement on line {line}" - makeRequirement( - requirements.RequirementType.requireEventually, reqID, req, line, name - ) - - -def terminate_when(reqID, req, line, name): - """Function implementing the 'terminate when' statement.""" - if not name: - name = f"termination condition on line {line}" - makeRequirement(requirements.RequirementType.terminateWhen, reqID, req, line, name) - - -def terminate_simulation_when(reqID, req, line, name): - """Function implementing the 'terminate simulation when' statement.""" - if not name: - name = f"termination condition on line {line}" - makeRequirement( - requirements.RequirementType.terminateSimulationWhen, reqID, req, line, name - ) - - -def makeRequirement(ty, reqID, req, line, name): - if evaluatingRequirement: - raise InvalidScenarioError(f'tried to use "{ty.value}" inside a requirement') - elif currentBehavior is not None: - raise InvalidScenarioError(f'"{ty.value}" inside a behavior on line {line}') - elif currentSimulation is not None: - currentScenario._addDynamicRequirement(ty, req, line, name) - else: # requirement being defined at compile time - currentScenario._addRequirement(ty, reqID, req, line, name, 1) - - -def terminate_after(timeLimit, terminator=None): - if not isinstance(timeLimit, (builtins.float, builtins.int)): - raise TypeError('"terminate after N" with N not a number') - assert terminator in (None, "seconds", "steps") - inSeconds = terminator != "steps" - currentScenario._setTimeLimit(timeLimit, inSeconds=inSeconds) - - -def resample(dist): - """The built-in resample function.""" - if not isinstance(dist, Distribution): - return dist - try: - return dist.clone() - except NotImplementedError: - raise TypeError("cannot resample non-primitive distribution") from None - - -def verbosePrint( - *objects, level=1, indent=True, sep=" ", end="\n", file=sys.stdout, flush=False -): - """Built-in function printing a message only in verbose mode. - - Scenic's verbosity may be set using the :option:`-v` command-line option. - The simplest way to use this function is with code like - :scenic:`verbosePrint('hello world!')` or :scenic:`verbosePrint('details here', level=3)`; - the other keyword arguments are probably only useful when replacing more complex uses - of the Python `print` function. - - Args: - objects: Object(s) to print (`str` will be called to make them strings). - level (int): Minimum verbosity level at which to print. Default is 1. - indent (bool): Whether to indent the message to align with messages generated by - Scenic (default true). - sep, end, file, flush: As in `print`. - """ - if errors.verbosityLevel >= level: - if indent: - if currentSimulation: - indent = " " if errors.verbosityLevel >= 3 else " " - else: - indent = " " * activity if errors.verbosityLevel >= 2 else " " - print(indent, end="", file=file) - print(*objects, sep=sep, end=end, file=file, flush=flush) - - -def localPath(relpath): - """Convert a path relative to the calling Scenic file into an absolute path. - - For example, :scenic:`localPath('resource.dat')` evaluates to the absolute path - of a file called ``resource.dat`` located in the same directory as the - Scenic file where this expression appears. Note that the path is returned as a - `pathlib.Path` object. - """ - filename = traceback.extract_stack(limit=2)[0].filename - base = Path(filename).parent - return base.joinpath(relpath).resolve() - - -def simulation(): - """Get the currently-running `Simulation`. - - May only be called from code that runs at simulation time, e.g. inside - :term:`dynamic behaviors` and :keyword:`compose` blocks of scenarios. - """ - if isActive(): - raise InvalidScenarioError("used simulation() outside a behavior") - assert currentSimulation is not None - return currentSimulation - - -def simulator(sim): - global simulatorFactory - simulatorFactory = sim - - -def in_initial_scenario(): - return inInitialScenario - - -def override(*args): - if len(args) < 1: - raise TypeError('"override" missing an object') - elif len(args) < 2: - raise TypeError('"override" missing a list of specifiers') - obj = args[0] - if not isinstance(obj, Object): - raise TypeError(f'"override" passed non-Object {obj}') - specs = args[1:] - for spec in specs: - assert isinstance(spec, Specifier), spec - - currentScenario._override(obj, specs) - - -def model(namespace, modelName): - global loadingModel - if loadingModel: - raise InvalidScenarioError('Scenic world model itself uses the "model" statement') - if lockedModel is not None: - modelName = lockedModel - try: - loadingModel = True - module = importlib.import_module(modelName) - except ModuleNotFoundError as e: - if e.name == modelName: - raise InvalidScenarioError( - f"could not import world model {modelName}" - ) from None - else: - raise - finally: - loadingModel = False - names = module.__dict__.get("__all__", None) - if names is not None: - for name in names: - namespace[name] = getattr(module, name) - else: - for name, value in module.__dict__.items(): - if not name.startswith("_"): - namespace[name] = value - - -def param(params): - """Function implementing the param statement.""" - global loadingModel - if evaluatingRequirement: - raise InvalidScenarioError( - "tried to create a global parameter inside a requirement" - ) - elif currentSimulation is not None: - raise InvalidScenarioError( - "tried to create a global parameter during a simulation" - ) - for name, value in params.items(): - if name not in lockedParameters and ( - not loadingModel or name not in _globalParameters - ): - _globalParameters[name] = toDistribution(value) - - -class ParameterTableProxy(collections.abc.Mapping): - def __init__(self, map): - object.__setattr__(self, "_internal_map", map) - - def __getitem__(self, name): - return self._internal_map[name] - - def __iter__(self): - return iter(self._internal_map) - - def __len__(self): - return len(self._internal_map) - - def __getattr__(self, name): - return self.__getitem__(name) # allow namedtuple-like access - - def __setattr__(self, name, value): - raise InvalidScenarioError( - 'cannot modify globalParameters (use "param" statement)' - ) - - def _clone_table(self): - return ParameterTableProxy(self._internal_map.copy()) - - -def globalParameters(): - return ParameterTableProxy(_globalParameters) - - -def mutate(*objects, scale=1): - """Function implementing the mutate statement.""" - if evaluatingRequirement: - raise InvalidScenarioError("used mutate statement inside a requirement") - if len(objects) == 0: - objects = currentScenario._objects - if not isinstance(scale, (builtins.int, builtins.float)): - raise TypeError('"mutate X by Y" with Y not a number') - for obj in objects: - if not isinstance(obj, Object): - raise TypeError('"mutate X" with X not an object') - obj.mutationScale = scale - # Object will now require sampling even if it has no explicit dependencies. - obj._needsSampling = True - obj._isLazy = True - - -### Prefix operators - - -def Visible(region): - """The :grammar:`visible ` operator.""" - region = toType(region, Region, '"visible X" with X not a Region') - return region.intersect(ego().visibleRegion) - - -def NotVisible(region): - """The :grammar:`not visible ` operator.""" - region = toType(region, Region, '"not visible X" with X not a Region') - return region.difference(ego().visibleRegion) - - -# front of , etc. -ops = ( - "front", - "back", - "left", - "right", - "front left", - "front right", - "back left", - "back right", - "top", - "bottom", - "top front left", - "top front right", - "top back left", - "top back right", - "bottom front left", - "bottom front right", - "bottom back left", - "bottom back right", -) -template = '''\ -def {function}(X): - """The :grammar:`{syntax} of ` operator.""" - if not isinstance(X, Object): - raise TypeError('"{syntax} of X" with X not an Object') - return X.{property} -''' -for op in ops: - func = "".join(word.capitalize() for word in op.split(" ")) - prop = func[0].lower() + func[1:] - definition = template.format(function=func, syntax=op, property=prop) - exec(definition) - -### Infix operators - - -def FieldAt(X, Y): - """The :grammar:` at ` operator.""" - if isinstance(X, type) and issubclass(X, Constructible): - raise TypeError('"X at Y" with X not a vector field. (Perhaps you forgot "new"?)') - - if not isA(X, VectorField): - raise TypeError('"X at Y" with X not a vector field') - Y = toVector(Y, '"X at Y" with Y not a vector') - return X[Y] - - -def RelativeTo(X, Y) -> typing.Union[Vector, builtins.float, Orientation]: - """The :scenic:`{X} relative to {Y}` polymorphic operator. - - Allowed forms:: - - relative to # with at least one a field, the other a field or heading - relative to # and vice versa - relative to - relative to - relative to - """ - - # Define lazy RelativeTo helper - @distributionFunction - def lazyRelativeTo(X, Y) -> typing.Union[Vector, builtins.float, Orientation]: - return RelativeTo(X, Y) - - # Define type helpers - def knownOrientation(thing): - return isA(thing, Orientation) or ( - (not isLazy(thing)) - and canCoerce(thing, Orientation) - and (not canCoerce(thing, Vector)) - ) - - def knownHeading(thing): - return isA(thing, numbers.Real) or ( - (not isLazy(thing)) and canCoerce(thing, Heading) - ) - - def knownVector(thing): - return isA(thing, Vector) or ((not isLazy(thing)) and canCoerce(thing, Vector)) - - xf, yf = isA(X, VectorField), isA(Y, VectorField) - if xf or yf: - if xf and yf and X.valueType != Y.valueType: - raise TypeError('"X relative to Y" with X, Y fields of different types') - fieldType = X.valueType if xf else Y.valueType - error = '"X relative to Y" with field and value of different types' - - def helper(context): - pos = context.position.toVector() - xp = X[pos] if xf else toType(X, fieldType, error) - yp = Y[pos] if yf else toType(Y, fieldType, error) - return yp + xp - - return DelayedArgument({"position"}, helper) - - elif isA(X, OrientedPoint) or isA(Y, OrientedPoint): - # Ensure X and Y aren't both oriented points - if isA(X, OrientedPoint) and isA(Y, OrientedPoint): - raise TypeError('"X relative to Y" with X, Y both oriented points') - - # Extract the single oriented point and the other value - if isA(X, OrientedPoint): - op = X - other = Y - else: - op = Y - other = X - - # Check the other value's type - if isA(other, numbers.Real): - return op.heading + toHeading(other) - elif isA(other, Orientation): - return toOrientation(Y) * toOrientation(X) - elif knownVector(other): - other = toVector(other) - return op.relativize(other) - - # This case doesn't match (for now at least). Fall through. - pass - - elif knownOrientation(X) and knownOrientation(Y): - xf = toOrientation(X) - yf = toOrientation(Y) - - return yf * xf - - elif knownHeading(X) and knownHeading(Y): - xf = toHeading(X, f'"X relative to Y" with Y a heading but X a {type(X)}') - yf = toHeading(Y, f'"X relative to Y" with X a heading but Y a {type(Y)}') - - return xf + yf - - elif knownVector(X) or knownVector(Y): - xf = toVector(X, f'"X relative to Y" with Y a vector but X a {type(X)}') - yf = toVector(Y, f'"X relative to Y" with X a vector but Y a {type(Y)}') - - return xf + yf - - if isLazy(X) or isLazy(Y): - # We can't determine what case to use at this point. Try again when things are sampled. - return lazyRelativeTo(X, Y) - - raise TypeError( - f'"X relative to Y" with X and Y incompatible types (X a {type(X)}, Y a {type(Y)})' - ) - - -def OffsetAlong(X, H, Y): - """The :scenic:`{X} offset along {H} by {Y}` polymorphic operator. - - Allowed forms:: - - offset along by - offset along by - """ - X = toVector(X, '"X offset along H by Y" with X not a vector') - Y = toVector(Y, '"X offset along H by Y" with Y not a vector') - if isA(H, VectorField): - H = H[X] - H = toOrientation( - H, '"X offset along H by Y" with H not an orientation or vector field' - ) - return X.offsetLocally(H, Y) - - -def RelativePosition(X, Y=None): - """The :grammar:`relative position of [from ]` operator. - - If the :grammar:`from ` is omitted, the position of ego is used. - """ - X = toVector(X, '"relative position of X from Y" with X not a vector') - if Y is None: - Y = ego() - Y = toVector(Y, '"relative position of X from Y" with Y not a vector') - return X - Y - - -def RelativeHeading(X, Y=None): - """The :grammar:`relative heading of [from ]` operator. - - If the :grammar:`from ` is omitted, the heading of ego is used. - """ - X = toOrientation( - X, '"relative heading of X from Y" with X not a heading or orientation' - ) - if Y is None: - Y = ego().orientation - else: - Y = toOrientation(Y, '"relative heading of X from Y" with Y not a heading') - return normalizeAngle(X.yaw - Y.yaw) - - -def ApparentHeading(X, Y=None): - """The :grammar:`apparent heading of [from ]` operator. - - If the :grammar:`from ` is omitted, the position of ego is used. - """ - if not isA(X, OrientedPoint): - raise TypeError('"apparent heading of X from Y" with X not an OrientedPoint') - if Y is None: - Y = ego() - Y = toVector(Y, '"relative heading of X from Y" with Y not a vector') - return apparentHeadingAtPoint(X.position, X.heading, Y) - - -def DistanceFrom(X, Y=None): - """The :scenic:`distance from {X} to {Y}` polymorphic operator. - - Allowed forms:: - - distance from [to ] - distance from [to ] - distance from to - - If the :grammar:`to ` is omitted, the position of ego is used. - """ - X = toTypes( - X, (Vector, Region), '"distance from X to Y" with X neither a vector nor region' - ) - if Y is None: - Y = ego() - Y = toTypes( - Y, (Vector, Region), '"distance from X to Y" with Y neither a vector nor region' - ) - return X.distanceTo(Y) - - -def DistancePast(X, Y=None): - """The :grammar:`distance past of ` operator. - - If the :grammar:`of {oriented point}` is omitted, the ego object is used. - """ - X = toVector(X, '"distance past X" with X not a vector') - if Y is None: - Y = ego() - Y = toType(Y, OrientedPoint, '"distance past X of Y" with Y not an OrientedPoint') - return Y.distancePast(X) - - -# TODO(shun): Migrate to `AngleFrom` -def AngleTo(X): - """The :grammar:`angle to ` operator (using the position of ego as the reference).""" - X = toVector(X, '"angle to X" with X not a vector') - return ego().angleTo(X) - - -def AngleFrom(X=None, Y=None): - """The :grammar:`angle from to ` operator.""" - assert X is not None or Y is not None - if X is None: - X = ego() - X = toVector(X, '"angle from X to Y" with X not a vector') - if Y is None: - Y = ego() - Y = toVector(Y, '"angle from X to Y" with Y not a vector') - return X.angleTo(Y) - - -def AltitudeTo(X): - """The :grammar:`angle to ` operator (using the position of ego as the reference).""" - X = toVector(X, '"altitude to X" with X not a vector') - return ego().altitudeTo(X) - - -def AltitudeFrom(X=None, Y=None): - """The :grammar:`altitude from to ` operator.""" - assert X is not None or Y is not None - if X is None: - X = ego() - X = toVector(X, '"altitude from X to Y" with X not a vector') - if Y is None: - Y = ego() - Y = toVector(Y, '"altitude from X to Y" with Y not a vector') - return X.altitudeTo(Y) - - -def Follow(F, X, D): - """The :grammar:`follow from for ` operator.""" - if not isA(F, VectorField): - raise TypeError('"follow F from X for D" with F not a vector field') - X = toVector(X, '"follow F from X for D" with X not a vector') - D = toScalar(D, '"follow F from X for D" with D not a number') - pos = F.followFrom(X, D) - orientation = F[pos] - return OrientedPoint._with(position=pos, parentOrientation=orientation) - - -def VisibleFromOp(region, base): - """The :grammar:` visible from ` operator.""" - region = toType(region, Region, '"X visible from Y" with X not a Region') - if not isA(base, Point): - raise TypeError('"X visible from Y" with Y not a Point') - return region.intersect(base.visibleRegion) - - -def NotVisibleFromOp(region, base): - """The :grammar:` not visible from ` operator.""" - region = toType(region, Region, '"X visible from Y" with X not a Region') - if not isA(base, Point): - raise TypeError('"X not visible from Y" with Y not a Point') - - return region.difference(base.visibleRegion) - - -def CanSee(X, Y): - """The :scenic:`{X} can see {Y}` polymorphic operator. - - Allowed forms:: - - can see - can see - """ - if isActive(): - raise InvalidScenarioError( - '"can see" operator prohibited at top level of Scenic programs' - ) - - if not isA(X, Point): - raise TypeError('"X can see Y" with X not a Point, OrientedPoint, or Object') - - if not canCoerce(Y, Vector): - raise TypeError('"X can see Y" with Y not a Vector, Point, or Object') - - objects = toDistribution(currentScenario._objects) - - @distributionFunction - def canSeeHelper(X, Y, objects): - if not isA(Y, Point): - Y = toVector( - Y, '"X can see Y" with X not a Vector, Point, OrientedPoint, or Object' - ) - - occludingObjects = tuple( - obj for obj in objects if obj.occluding and X is not obj and Y is not obj - ) - - return X.canSee(Y, occludingObjects=occludingObjects) - - return canSeeHelper(X, Y, objects) - - -@distributionFunction -def Intersects(X, Y): - """The :scenic:`{X} intersects {Y}` operator.""" - if isA(X, Object): - return X.intersects(Y) - else: - return Y.intersects(X) - - -### Specifiers - - -def With(prop, val): - """The :grammar:`with ` specifier. - - Specifies the given property, with no dependencies. - """ - return Specifier(f"With({prop})", {prop: 1}, {prop: val}) - - -def At(pos): - """The :grammar:`at ` specifier. - - Specifies :prop:`position`, with no dependencies. - """ - pos = toVector(pos, 'specifier "at X" with X not a vector') - return Specifier("At", {"position": 1}, {"position": pos}) - - -def In(region): - """The :grammar:`in ` specifier. - - Specifies :prop:`position`, and optionally, :prop:`parentOrientation` if the given region - has a preferred orientation, with no dependencies. - """ - region = toType(region, Region, 'specifier "in R" with R not a Region') - pos = Region.uniformPointIn(region) - props = {"position": 1} - values = {"position": pos} - if alwaysProvidesOrientation(region): - props["parentOrientation"] = 3 - values["parentOrientation"] = region.orientation[pos] - return Specifier("In", props, values) - - -def ContainedIn(region): - """The :grammar:`contained in ` specifier. - - Specifies :prop:`position`, :prop:`regionContainedIn`, and optionally, :prop:`parentOrientation` - if the given region has a preferred orientation, with no dependencies. - """ - region = toType(region, Region, 'specifier "contained in R" with R not a Region') - pos = Region.uniformPointIn(region) - props = {"position": 1, "regionContainedIn": 1} - values = {"position": pos, "regionContainedIn": region} - if alwaysProvidesOrientation(region): - props["parentOrientation"] = 3 - values["parentOrientation"] = region.orientation[pos] - return Specifier("ContainedIn", props, values) - - -def On(thing): - """The :specifier:`on {X}` specifier. - - Specifies :prop:`position`, and optionally, :prop:`parentOrientation` if the given region - has a preferred orientation. Depends on :prop:`onDirection`, :prop:`baseOffset`, - and :prop:`contactTolerance`. - - Note that while :specifier:`on` can be used with `Region`, `Object` and `Vector`, - it cannot be used with a distribution containing anything other than `Region`. - - May be used to modify an already-specified :prop:`position` property. - - Allowed forms: - on - on - on - """ - if isA(thing, Object): - # Target is an Object: use its onSurface. - target = thing.onSurface - elif canCoerce(thing, Vector, exact=True): - # Target is a vector - target = toVector(thing) - elif canCoerce(thing, Region): - # Target is a region (or could theoretically be coerced to one), - # so we can use it as a target. - target = toType(thing, Region) - else: - raise TypeError('specifier "on R" with R not a Region, Object, or Vector') - - props = {"position": 1} - - if isA(target, Region) and alwaysProvidesOrientation(target): - props["parentOrientation"] = 2 - - def helper(context): - # Pick position based on whether we are specifying or modifying - if hasattr(context, "position"): - if isA(target, Vector): - raise TypeError('Cannot use modifying "on V" with V a vector.') - - pos = projectVectorHelper(target, context.position, context.onDirection) - elif isA(target, Vector): - pos = target - else: - pos = Region.uniformPointIn(target) - - values = {} - - contactOffset = Vector(0, 0, context.contactTolerance / 2) - context.baseOffset - - if "parentOrientation" in props: - values["parentOrientation"] = target.orientation[pos] - contactOffset = contactOffset.rotatedBy(values["parentOrientation"]) - - values["position"] = pos + contactOffset - - return values - - return ModifyingSpecifier( - "On", - props, - DelayedArgument({"onDirection", "baseOffset", "contactTolerance"}, helper), - modifiable_props={"position"}, - ) - - -@distributionFunction -def projectVectorHelper(region, pos, onDirection): - on_pos = region.projectVector(pos, onDirection=onDirection) - - if on_pos is None: - raise RejectionException("Unable to place object on surface.") - else: - return on_pos - - -def alwaysProvidesOrientation(region): - """Whether a Region or distribution over Regions always provides an orientation.""" - if isinstance(region, Region): - return region.orientation is not None - elif isinstance(region, MultiplexerDistribution) and all( - alwaysProvidesOrientation(opt) for opt in region.options - ): - return True - else: # TODO improve somehow! - try: - sample = region.sample() - return sample.orientation is not None or sample is nowhere - except RejectionException: - return False - except Exception as e: - warnings.warn( - f"While sampling internally to determine if a random region provides an orientation, the following exception was raised: {repr(e)}" - ) - return False - - -def OffsetBy(offset): - """The :grammar:`offset by ` specifier. - - Specifies :prop:`position`, and optionally :prop:`parentOrientation`, with no dependencies. - """ - offset = toVector(offset, 'specifier "offset by X" with X not a vector') - value = { - "position": RelativeTo(offset, ego()).toVector(), - "parentOrientation": ego().orientation, - } - return Specifier("OffsetBy", {"position": 1, "parentOrientation": 3}, value) - - -def OffsetAlongSpec(direction, offset): - """The :specifier:`offset along {X} by {Y}` polymorphic specifier. - - Specifies :prop:`position`, and optionally :prop:`parentOrientation`, with no dependencies. - - Allowed forms:: - - offset along by - offset along by - """ - pos = OffsetAlong(ego(), direction, offset) - parentOrientation = ego().orientation - return Specifier( - "OffsetAlong", - {"position": 1, "parentOrientation": 3}, - {"position": pos, "parentOrientation": parentOrientation}, - ) - - -def Beyond(pos, offset, fromPt=None): - """The :specifier:`beyond {X} by {Y} from {Z}` polymorphic specifier. - - Specifies :prop:`position`, and optionally :prop:`parentOrientation`, with no dependencies. - - Allowed forms:: - - beyond by [from ] - beyond by [from ] - - If the :grammar:`from ` is omitted, the position of ego is used. - """ - # Ensure X can be coerced into vector form - pos = toVector(pos, 'specifier "beyond X by Y" with X not a vector') - - # If no from vector is specified, assume ego - if fromPt is None: - fromPt = ego() - - fromPt = toVector(fromPt, 'specifier "beyond X by Y from Z" with Z not a vector') - - dType = underlyingType(offset) - - if dType is builtins.float or dType is builtins.int: - offset = Vector(0, offset, 0) - else: - # offset is not float or int, so try to coerce it into vector form. - offset = toVector( - offset, 'specifier "beyond X by Y" with X not a number or vector' - ) - - # If the from vector is oriented, set that to orientation. Else assume global coords. - if isA(fromPt, OrientedPoint): - orientation = fromPt.orientation - else: - orientation = Orientation.fromEuler(0, 0, 0) - - direction = pos - fromPt - sphericalCoords = direction.sphericalCoordinates() - offsetRotation = Orientation.fromEuler(sphericalCoords[1], sphericalCoords[2], 0) - - new_direction = pos + offset.applyRotation(offsetRotation) - - return Specifier( - "Beyond", - {"position": 1, "parentOrientation": 3}, - {"position": new_direction, "parentOrientation": orientation}, - ) - - -def VisibleFrom(base): - """The :grammar:`visible from ` specifier. - - Specifies :prop:`_observingEntity` and :prop:`position`, with no dependencies. - """ - if not isA(base, Point): - raise TypeError('specifier "visible from O" with O not a Point') - - def helper(self): - if mode2D: - position = Region.uniformPointIn(base.visibleRegion) - else: - containing_region = ( - currentScenario._workspace.region - if self.regionContainedIn is None - and currentScenario._workspace is not None - else self.regionContainedIn - ) - position = ( - Region.uniformPointIn(everywhere, tag="visible") - if containing_region is None - else Region.uniformPointIn(containing_region) - ) - - return {"position": position, "_observingEntity": base} - - return Specifier( - "Visible/VisibleFrom", - {"position": 3, "_observingEntity": 1}, - DelayedArgument({"regionContainedIn"}, helper), - ) - - -def VisibleSpec(): - """The :specifier:`visible` specifier (equivalent to :specifier:`visible from ego`). - - Specifies :prop:`_observingEntity` and :prop:`position`, with no dependencies. - """ - return VisibleFrom(ego()) - - -def NotVisibleFrom(base): - """The :grammar:`not visible from ` specifier. - - Specifies :prop:`_nonObservingEntity` and :prop:`position`, depending on :prop:`regionContainedIn`. - - See `VisibleFrom`. - """ - if not isA(base, Point): - raise TypeError('specifier "not visible from O" with O not a Point') - - def helper(self): - region = self.regionContainedIn - if region is None: - if currentScenario._workspace is None: - raise InvalidScenarioError( - '"not visible" specifier with no workspace or containing region defined' - ) - region = currentScenario._workspace.region - - if mode2D: - position = Region.uniformPointIn(region.difference(base.visibleRegion)) - else: - # We can't limit the available region since any spot could potentially be occluded. - position = Region.uniformPointIn(convertToFootprint(region)) - - return {"position": position, "_nonObservingEntity": base} - - return Specifier( - "NotVisible/NotVisibleFrom", - {"position": 3, "_nonObservingEntity": 1}, - DelayedArgument({"regionContainedIn"}, helper), - ) - - -def NotVisibleSpec(): - """The :specifier:`not visible` specifier (equivalent to :specifier:`not visible from ego`). - - Specifies :prop:`_nonObservingEntity` and :prop:`position`, depending on :prop:`regionContainedIn`. - """ - return NotVisibleFrom(ego()) - - -def LeftSpec(pos, dist=None): - """The :specifier:`left of {X} by {Y}` polymorphic specifier. - - Specifies :prop:`position`, and optionally, :prop:`parentOrientation`, depending on :prop:`width`. - - Allowed forms:: - - left of [by ] - left of [by ] - - If the :grammar:`by ` is omitted, the object's contact tolerance is used. - """ - return directionalSpecHelper( - "Left of", - pos, - dist, - "width", - lambda dist: (dist, 0, 0), - lambda self, dims, tol, dx, dy, dz: Vector( - -self.width / 2 - dx - dims[0] / 2 - tol, dy, dz - ), - ) - - -def RightSpec(pos, dist=None): - """The :specifier:`right of {X} by {Y}` polymorphic specifier. - - Specifies :prop:`position`, and optionally :prop:`parentOrientation`, depending on :prop:`width`. - - Allowed forms:: - - right of [by ] - right of [by ] - - If the :grammar:`by ` is omitted, zero is used. - """ - return directionalSpecHelper( - "Right of", - pos, - dist, - "width", - lambda dist: (dist, 0, 0), - lambda self, dims, tol, dx, dy, dz: Vector( - self.width / 2 + dx + dims[0] / 2 + tol, dy, dz - ), - ) - - -def Ahead(pos, dist=None): - """The :specifier:`ahead of {X} by {Y}` polymorphic specifier. - - Specifies :prop:`position`, and optionally :prop:`parentOrientation`, depending on :prop:`length`. - - Allowed forms:: - - ahead of [by ] - ahead of [by ] - - If the :grammar:`by ` is omitted, the object's contact tolerance is used. - """ - return directionalSpecHelper( - "Ahead of", - pos, - dist, - "length", - lambda dist: (0, dist, 0), - lambda self, dims, tol, dx, dy, dz: Vector( - dx, self.length / 2 + dy + dims[1] / 2 + tol, dz - ), - ) - - -def Behind(pos, dist=None): - """The :specifier:`behind {X} by {Y}` polymorphic specifier. - - Specifies :prop:`position`, and optionally :prop:`parentOrientation`, depending on :prop:`length`. - - Allowed forms:: - - behind [by ] - behind [by ] - - If the :grammar:`by ` is omitted, the object's contact tolerance is used. - """ - return directionalSpecHelper( - "Behind", - pos, - dist, - "length", - lambda dist: (0, dist, 0), - lambda self, dims, tol, dx, dy, dz: Vector( - dx, -self.length / 2 - dy - dims[1] / 2 - tol, dz - ), - ) - - -def Above(pos, dist=None): - """The :specifier:`above {X} by {Y}` polymorphic specifier. - - Specifies :prop:`position`, and optionally :prop:`parentOrientation`, depending on :prop:`height`. - - Allowed forms:: - - above [by ] - above [by ] - - If the :grammar:`by ` is omitted, the object's contact tolerance is used. - """ - return directionalSpecHelper( - "Above", - pos, - dist, - "height", - lambda dist: (0, 0, dist), - lambda self, dims, tol, dx, dy, dz: Vector( - dx, dy, self.height / 2 + dz + dims[2] / 2 + tol - ), - ) - - -def Below(pos, dist=None): - """The :specifier:`below {X} by {Y}` polymorphic specifier. - - Specifies :prop`position`, and optionally :prop:`parentOrientation`, depending on :prop:`height`. - - Allowed forms:: - - below [by ] - below [by ] - - If the :grammar:`by ` is omitted, the object's contact tolerance is used. - """ - return directionalSpecHelper( - "Below", - pos, - dist, - "height", - lambda dist: (0, 0, dist), - lambda self, dims, tol, dx, dy, dz: Vector( - dx, dy, -self.height / 2 - dz - dims[2] / 2 - tol - ), - ) - - -def directionalSpecHelper(syntax, pos, dist, axis, toComponents, makeOffset): - prop = {"position": 1} - if dist is None: - dx = dy = dz = 0 - elif canCoerce(dist, builtins.float): - dx, dy, dz = toComponents(coerce(dist, builtins.float)) - elif canCoerce(dist, Vector): - dx, dy, dz = coerce(dist, Vector) - else: - raise TypeError(f'"{syntax} X by D" with D not a number or vector') - - @distributionFunction - def makeContactOffset(dist, ct): - if dist is None: - return ct / 2 - else: - return 0 - - if isA(pos, Object): - prop["parentOrientation"] = 3 - obj_dims = (pos.width, pos.length, pos.height) - val = lambda self: { - "position": pos.relativePosition( - makeOffset( - self, - obj_dims, - makeContactOffset(dist, self.contactTolerance), - dx, - dy, - dz, - ) - ), - "parentOrientation": pos.orientation, - } - new = DelayedArgument({axis, "contactTolerance"}, val) - elif isA(pos, OrientedPoint): - prop["parentOrientation"] = 3 - val = lambda self: { - "position": pos.relativePosition(makeOffset(self, (0, 0, 0), 0, dx, dy, dz)), - "parentOrientation": pos.orientation, - } - new = DelayedArgument({axis}, val) - else: - pos = toVector(pos, f'specifier "{syntax} X" with X not a vector') - val = lambda self: { - "position": pos.offsetLocally( - self.orientation, makeOffset(self, (0, 0, 0), 0, dx, dy, dz) - ) - } - new = DelayedArgument({axis, "orientation"}, val) - return Specifier(syntax, prop, new) - - -def Following(field, dist, fromPt=None): - """The :specifier:`following {F} from {X} for {D}` specifier. - - Specifies :prop:`position`, and optionally :prop:`parentOrientation`, with no dependencies. - - Allowed forms:: - - following [from ] for - - If the :grammar:`from ` is omitted, the position of ego is used. - """ - if fromPt is None: - fromPt = ego() - field = toType(field, VectorField) - fromPt = toVector(fromPt, '"following F from X for D" with X not a vector') - dist = toScalar(dist, '"following F for D" with D not a number') - pos = field.followFrom(fromPt, dist) - orientation = field[pos] - return Specifier( - "Following", - {"position": 1, "parentOrientation": 3}, - {"position": pos, "parentOrientation": orientation}, - ) - - -def Facing(heading): - """The :specifier:`facing {X}` polymorphic specifier. - - Specifies :prop:`yaw`, :prop:`pitch`, and :prop:`roll`, depending on :prop:`parentOrientation`, - and depending on the form:: - - facing # no further dependencies; - facing # depends on 'position' - """ - if isA(heading, VectorField): - - def helper(context): - headingAtPos = heading[context.position] - if alwaysGlobalOrientation(context.parentOrientation): - orientation = headingAtPos # simplify expr tree in common case - else: - orientation = context.parentOrientation.inverse * headingAtPos - return { - "yaw": orientation.yaw, - "pitch": orientation.pitch, - "roll": orientation.roll, - } - - return Specifier( - "Facing", - {"yaw": 1, "pitch": 1, "roll": 1}, - DelayedArgument({"position", "parentOrientation"}, helper), - ) - else: - orientation = toOrientation( - heading, "facing x with x not a heading or orientation" - ) - orientationDeps = requiredProperties(orientation) - - def helper(context): - target_orientation = valueInContext(orientation, context) - euler = context.parentOrientation.localAnglesFor(target_orientation) - return {"yaw": euler[0], "pitch": euler[1], "roll": euler[2]} - - return Specifier( - "Facing", - {"yaw": 1, "pitch": 1, "roll": 1}, - DelayedArgument({"parentOrientation"} | orientationDeps, helper), - ) - - -def FacingToward(pos): - """The :grammar:`facing toward ` specifier. - - Specifies :prop:`yaw`, depending on :prop:`position` and :prop:`parentOrientation`. - """ - pos = toVector(pos, 'specifier "facing toward X" with X not a vector') - - def helper(context): - direction = pos - context.position - rotated = direction.applyRotation(context.parentOrientation.inverse) - sphericalCoords = ( - rotated.sphericalCoordinates() - ) # Ignore the rho, sphericalCoords[0] - return {"yaw": sphericalCoords[1]} - - return Specifier( - "FacingToward", - {"yaw": 1}, - DelayedArgument({"position", "parentOrientation"}, helper), - ) - - -def FacingDirectlyToward(pos): - """The :grammar:`facing directly toward ` specifier. - - Specifies :prop:`yaw` and :prop:`pitch`, depends on :prop:`position` and :prop:`parentOrientation`. - """ - pos = toVector(pos, 'specifier "facing directly toward X" with X not a vector') - - def helper(context): - """ - Same process as above, except by default also specify the pitch euler angle - """ - direction = pos - context.position - rotated = direction.applyRotation(context.parentOrientation.inverse) - sphericalCoords = rotated.sphericalCoordinates() - return {"yaw": sphericalCoords[1], "pitch": sphericalCoords[2]} - - return Specifier( - "FacingDirectlyToward", - {"yaw": 1, "pitch": 1}, - DelayedArgument({"position", "parentOrientation"}, helper), - ) - - -def FacingAwayFrom(pos): - """The :grammar:`facing away from ` specifier. - - Specifies :prop:`yaw`, depending on :prop:`position` and :prop:`parentOrientation`. - """ - pos = toVector(pos, 'specifier "facing away from X" with X not a vector') - - def helper(context): - """ - As in FacingToward, except invert the resulting rotation axis - """ - direction = context.position - pos - rotated = direction.applyRotation(context.parentOrientation.inverse) - sphericalCoords = rotated.sphericalCoordinates() - return {"yaw": sphericalCoords[1]} - - return Specifier( - "FacingAwayFrom", - {"yaw": 1}, - DelayedArgument({"position", "parentOrientation"}, helper), - ) - - -def FacingDirectlyAwayFrom(pos): - """The :grammar:`facing directly away from ` specifier. - - Specifies :prop:`yaw` and :prop:`pitch`, depending on :prop:`position` and :prop:`parentOrientation`. - """ - pos = toVector(pos, 'specifier "facing away from X" with X not a vector') - - def helper(context): - direction = context.position - pos - rotated = direction.applyRotation(context.parentOrientation.inverse) - sphericalCoords = rotated.sphericalCoordinates() - return {"yaw": sphericalCoords[1], "pitch": sphericalCoords[2]} - - return Specifier( - "FacingDirectlyToward", - {"yaw": 1, "pitch": 1}, - DelayedArgument({"position", "parentOrientation"}, helper), - ) - - -def ApparentlyFacing(heading, fromPt=None): - """The :grammar:`apparently facing [from ]` specifier. - - Specifies :prop:`yaw`, depending on :prop:`position` and :prop:`parentOrientation`. - - If the :grammar:`from ` is omitted, the position of ego is used. - """ - heading = toHeading(heading, 'specifier "apparently facing X" with X not a heading') - if fromPt is None: - fromPt = ego() - fromPt = toVector( - fromPt, 'specifier "apparently facing X from Y" with Y not a vector' - ) - - def helper(context): - return {"yaw": fromPt.angleTo(context.position) + heading} - - return Specifier( - "ApparentlyFacing", - {"yaw": 1}, - DelayedArgument({"position", "parentOrientation"}, helper), - ) - - -### Primitive internal functions, utilized after compiler conversion - - -@distributionFunction -def _toStrScenic(*args, **kwargs) -> str: - return builtins.str(*args, **kwargs) - - -@distributionFunction -def _toFloatScenic(*args, **kwargs) -> float: - return builtins.float(*args, **kwargs) - - -@distributionFunction -def _toIntScenic(*args, **kwargs) -> int: - return builtins.int(*args, **kwargs) - - -### Primitive functions overriding Python builtins - -# N.B. applying functools.wraps to preserve the metadata of the original -# functions seems to break pickling/unpickling - - -@distributionFunction -def filter(function, iterable): - return list(builtins.filter(function, iterable)) - - -@distributionFunction -def round(*args, **kwargs): - return builtins.round(*args, **kwargs) - - -def len(obj): - return obj.__len__() - - -def range(*args): - if any(needsSampling(arg) for arg in args): - raise RandomControlFlowError("cannot construct a range with random parameters") - return builtins.range(*args) - - -### Temporal Operators Factories - - -def AtomicProposition(closure, syntaxId): - return propositions.Atomic(closure, syntaxId) - - -def PropositionAnd(reqs): - return propositions.And(reqs) - - -def PropositionOr(reqs): - return propositions.Or(reqs) - - -def PropositionNot(req): - return propositions.Not(req) - - -def Always(req): - return propositions.Always(req) - - -def Eventually(req): - return propositions.Eventually(req) - - -def Next(req): - return propositions.Next(req) - - -def Until(lhs, rhs): - return propositions.Until(lhs, rhs) - - -def Implies(lhs, rhs): - return propositions.Implies(lhs, rhs) +"""Python implementations of Scenic language constructs. + +This module is automatically imported by all Scenic programs. In addition to +defining the built-in functions, operators, specifiers, etc., it also stores +global state such as the list of all created Scenic objects. + +.. highlight:: scenic-grammar +""" + +__all__ = ( + # Primitive statements and functions + "ego", + "workspace", + "new", + "require", + "resample", + "param", + "globalParameters", + "mutate", + "verbosePrint", + "localPath", + "model", + "simulator", + "simulation", + "require_monitor", + "terminate_when", + "terminate_simulation_when", + "terminate_after", + "in_initial_scenario", + "override", + "record", + "record_initial", + "record_final", + "sin", + "cos", + "hypot", + "max", + "min", + "_toStrScenic", + "_toFloatScenic", + "_toIntScenic", + "filter", + "round", + "len", + "range", + # Prefix operators + "Visible", + "NotVisible", + "Front", + "Back", + "Left", + "Right", + "FrontLeft", + "FrontRight", + "BackLeft", + "BackRight", + "Top", + "Bottom", + "TopFrontLeft", + "TopFrontRight", + "TopBackLeft", + "TopBackRight", + "BottomFrontLeft", + "BottomFrontRight", + "BottomBackLeft", + "BottomBackRight", + "RelativeHeading", + "ApparentHeading", + "RelativePosition", + "DistanceFrom", + "DistancePast", + "Follow", + "AngleTo", + "AngleFrom", + "AltitudeTo", + "AltitudeFrom", + # Infix operators + "FieldAt", + "RelativeTo", + "OffsetAlong", + "CanSee", + "Intersects", + "Until", + "Implies", + "VisibleFromOp", + "NotVisibleFromOp", + # Primitive types + "Vector", + "Orientation", + "VectorField", + "PolygonalVectorField", + "Shape", + "MeshShape", + "BoxShape", + "CylinderShape", + "ConeShape", + "SpheroidShape", + "MeshVolumeRegion", + "MeshSurfaceRegion", + "BoxRegion", + "SpheroidRegion", + "PathRegion", + "Region", + "PointSetRegion", + "RectangularRegion", + "CircularRegion", + "SectorRegion", + "PolygonalRegion", + "PolylineRegion", + "Workspace", + "Mutator", + "Range", + "DiscreteRange", + "Options", + "Uniform", + "Discrete", + "Normal", + "TruncatedNormal", + "VerifaiParameter", + "VerifaiRange", + "VerifaiDiscreteRange", + "VerifaiOptions", + # Constructible types + "Point", + "OrientedPoint", + "Object", + # Specifiers + "With", + "At", + "In", + "ContainedIn", + "On", + "Beyond", + "VisibleFrom", + "NotVisibleFrom", + "VisibleSpec", + "NotVisibleSpec", + "OffsetBy", + "OffsetAlongSpec", + "Facing", + "ApparentlyFacing", + "FacingToward", + "FacingDirectlyToward", + "FacingAwayFrom", + "FacingDirectlyAwayFrom", + "LeftSpec", + "RightSpec", + "Ahead", + "Behind", + "Above", + "Below", + "Following", + # Constants + "everywhere", + "nowhere", + # Exceptions + "GuardViolation", + "PreconditionViolation", + "InvariantViolation", + "RejectionException", + # Internal APIs # TODO remove? + "_scenic_default", + "Behavior", + "Monitor", + "_makeTerminationAction", + "_makeSimulationTerminationAction", + "BlockConclusion", + "runTryInterrupt", + "wrapStarredValue", + "callWithStarArgs", + "Modifier", + "DynamicScenario", + # Proposition Factories + "AtomicProposition", + "PropositionAnd", + "PropositionOr", + "PropositionNot", + "Always", + "Eventually", + "Next", +) + +# various Python types and functions used in the language but defined elsewhere +from scenic.core.distributions import ( + DiscreteRange, + Normal, + Options, + RandomControlFlowError, + Range, + TruncatedNormal, + Uniform, +) +from scenic.core.dynamics.behaviors import Behavior, Monitor +from scenic.core.dynamics.guards import ( + GuardViolation, + InvariantViolation, + PreconditionViolation, +) +from scenic.core.dynamics.invocables import BlockConclusion, runTryInterrupt +from scenic.core.dynamics.scenarios import DynamicScenario +from scenic.core.external_params import ( + VerifaiDiscreteRange, + VerifaiOptions, + VerifaiParameter, + VerifaiRange, +) +from scenic.core.geometry import cos, hypot, max, min, sin +from scenic.core.object_types import Mutator, Object, OrientedPoint, Point +from scenic.core.regions import ( + BoxRegion, + CircularRegion, + MeshSurfaceRegion, + MeshVolumeRegion, + PathRegion, + PointSetRegion, + PolygonalRegion, + PolylineRegion, + RectangularRegion, + Region, + SectorRegion, + SpheroidRegion, + everywhere, + nowhere, +) +from scenic.core.shapes import ( + BoxShape, + ConeShape, + CylinderShape, + MeshShape, + Shape, + SpheroidShape, +) +from scenic.core.specifiers import PropertyDefault as _scenic_default +from scenic.core.vectors import PolygonalVectorField, Vector, VectorField +from scenic.core.workspaces import Workspace + +Discrete = Options + +# isort: split + +# everything that should not be directly accessible from the language is imported here: +import builtins +import collections.abc +from contextlib import contextmanager +import functools +import importlib +import numbers +from pathlib import Path +import sys +import traceback +import typing +import warnings + +from scenic.core.distributions import ( + Distribution, + MultiplexerDistribution, + RejectionException, + StarredDistribution, + TupleDistribution, + canUnpackDistributions, + distributionFunction, + needsSampling, + toDistribution, +) +from scenic.core.dynamics.actions import _EndScenarioAction, _EndSimulationAction +import scenic.core.errors as errors +from scenic.core.errors import InvalidScenarioError, ScenicSyntaxError +from scenic.core.external_params import ExternalParameter +from scenic.core.geometry import apparentHeadingAtPoint, normalizeAngle +from scenic.core.lazy_eval import ( + DelayedArgument, + isLazy, + needsLazyEvaluation, + requiredProperties, + valueInContext, +) +import scenic.core.object_types +from scenic.core.object_types import Constructible, Object2D, OrientedPoint2D, Point2D +import scenic.core.propositions as propositions +from scenic.core.regions import convertToFootprint +import scenic.core.requirements as requirements +from scenic.core.simulators import RejectSimulationException +from scenic.core.specifiers import ModifyingSpecifier, Specifier +from scenic.core.type_support import ( + Heading, + canCoerce, + coerce, + evaluateRequiringEqualTypes, + isA, + toHeading, + toOrientation, + toScalar, + toType, + toTypes, + toVector, + underlyingType, +) +from scenic.core.vectors import Orientation, alwaysGlobalOrientation + +### Internals + +activity = 0 +currentScenario = None +scenarioStack = [] +scenarios = [] +evaluatingRequirement = False +_globalParameters = {} +lockedParameters = set() +lockedModel = None +loadingModel = False +currentSimulation = None +inInitialScenario = True +runningScenarios = [] # in order, oldest first +currentBehavior = None +simulatorFactory = None +evaluatingGuard = False +mode2D = False +_originalConstructibles = (Point, OrientedPoint, Object) +BUFFERING_PITCH = 0.1 + +## APIs used internally by the rest of Scenic + +# Scenic compilation + + +def isActive(): + """Are we in the middle of compiling a Scenic module? + + The 'activity' global can be >1 when Scenic modules in turn import other + Scenic modules. + """ + return activity > 0 + + +def activate(options, namespace=None): + """Activate the veneer when beginning to compile a Scenic module.""" + global activity, _globalParameters, lockedParameters, lockedModel, currentScenario + if options.paramOverrides or options.modelOverride: + assert activity == 0 + _globalParameters.update(options.paramOverrides) + lockedParameters = set(options.paramOverrides) + lockedModel = options.modelOverride + + # If we are in 2D mode, set the global flag and replace all classes + # with their 2D compatibility counterparts. + if options.mode2D: + global mode2D, Point, OrientedPoint, Object + assert mode2D or activity == 0 + mode2D = True + Point = Point2D + OrientedPoint = OrientedPoint2D + Object = Object2D + scenic.core.object_types.Point = Point + scenic.core.object_types.OrientedPoint = OrientedPoint + scenic.core.object_types.Object = Object + + activity += 1 + assert not evaluatingRequirement + assert not evaluatingGuard + assert currentSimulation is None + # placeholder scenario for top-level code + newScenario = DynamicScenario._dummy(namespace) + scenarioStack.append(newScenario) + currentScenario = newScenario + + +def deactivate(): + """Deactivate the veneer after compiling a Scenic module.""" + global activity, _globalParameters, lockedParameters, lockedModel, mode2D + global currentScenario, scenarios, scenarioStack, simulatorFactory + activity -= 1 + assert activity >= 0 + assert not evaluatingRequirement + assert not evaluatingGuard + assert currentSimulation is None + scenarioStack.pop() + assert len(scenarioStack) == activity + scenarios = [] + + if activity == 0: + lockedParameters = set() + lockedModel = None + currentScenario = None + simulatorFactory = None + _globalParameters = {} + + if mode2D: + global Point, OrientedPoint, Object + mode2D = False + Point, OrientedPoint, Object = _originalConstructibles + scenic.core.object_types.Point = Point + scenic.core.object_types.OrientedPoint = OrientedPoint + scenic.core.object_types.Object = Object + else: + currentScenario = scenarioStack[-1] + + +# Instance/Object creation + + +def registerInstance(inst): + """Add a Scenic instance to the global list of created objects. + + This is called by the Point/OrientedPoint constructor. + """ + if currentScenario: + assert isinstance(inst, Constructible) + currentScenario._registerInstance(inst) + + +def registerObject(obj): + """Add a Scenic object to the global list of created objects. + + This is called by the Object constructor. + """ + if evaluatingRequirement: + raise InvalidScenarioError("tried to create an object inside a requirement") + elif currentBehavior is not None: + raise InvalidScenarioError("tried to create an object inside a behavior") + elif activity > 0 or currentScenario: + assert not evaluatingRequirement + assert isinstance(obj, Object) + currentScenario._registerObject(obj) + if currentSimulation: + currentSimulation._createObject(obj) + + +# External parameter creation + + +def registerExternalParameter(value): + """Register a parameter whose value is given by an external sampler.""" + if activity > 0: + assert isinstance(value, ExternalParameter) + currentScenario._externalParameters.append(value) + + +# Function call support + + +def wrapStarredValue(value, lineno): + if isinstance(value, TupleDistribution) or not needsSampling(value): + return value + elif isinstance(value, Distribution): + return [StarredDistribution(value, lineno)] + else: + raise TypeError(f"iterable unpacking cannot be applied to {value}") + + +def callWithStarArgs(_func_to_call, *args, **kwargs): + if not canUnpackDistributions(_func_to_call): + # wrap function to delay evaluation until starred distributions are sampled + _func_to_call = distributionFunction(_func_to_call) + return _func_to_call(*args, **kwargs) + + +# Simulations + + +def instantiateSimulator(factory, params): + global _globalParameters + assert not _globalParameters # TODO improve hack? + _globalParameters = dict(params) + try: + return factory() + finally: + _globalParameters = {} + + +def beginSimulation(sim): + global currentSimulation, currentScenario, inInitialScenario, runningScenarios + global _globalParameters + if isActive(): + raise RuntimeError("tried to start simulation during Scenic compilation!") + assert currentSimulation is None + assert currentScenario is None + assert not scenarioStack + currentSimulation = sim + currentScenario = sim.scene.dynamicScenario + runningScenarios = [] # will be updated by DynamicScenario._start + inInitialScenario = currentScenario._setup is None + currentScenario._bindTo(sim.scene) + _globalParameters = dict(sim.scene.params) + + # rebind globals that could be referenced by behaviors to their sampled values + for modName, ( + namespace, + sampledNS, + originalNS, + ) in sim.scene.behaviorNamespaces.items(): + namespace.clear() + namespace.update(sampledNS) + + +def endSimulation(sim): + global currentSimulation, currentScenario, currentBehavior, runningScenarios + global _globalParameters + currentSimulation = None + currentScenario = None + runningScenarios = [] + currentBehavior = None + _globalParameters = {} + + for modName, ( + namespace, + sampledNS, + originalNS, + ) in sim.scene.behaviorNamespaces.items(): + namespace.clear() + namespace.update(originalNS) + + +def simulationInProgress(): + return currentSimulation is not None + + +# Requirements + + +@contextmanager +def executeInRequirement(scenario, boundEgo, values): + global evaluatingRequirement, currentScenario + assert activity == 0 + assert not evaluatingRequirement + evaluatingRequirement = True + if currentScenario is None: + currentScenario = scenario + clearScenario = True + else: + assert currentScenario is scenario + clearScenario = False + oldEgo = currentScenario._ego + oldObjects = currentScenario._objects + + currentScenario._objects = tuple(values[obj] for obj in currentScenario.objects) + + if boundEgo: + currentScenario._ego = boundEgo + try: + yield + except RandomControlFlowError as e: + # Such errors should not be possible inside a requirement, since all values + # should have already been sampled: something's gone wrong with our rebinding. + raise RuntimeError("internal error: requirement dependency not sampled") from e + finally: + evaluatingRequirement = False + currentScenario._ego = oldEgo + currentScenario._objects = oldObjects + if clearScenario: + currentScenario = None + + +# Dynamic scenarios + + +def registerDynamicScenarioClass(cls): + scenarios.append(cls) + + +@contextmanager +def executeInScenario(scenario, inheritEgo=False): + global currentScenario, _globalParameters + oldScenario = currentScenario + if inheritEgo and oldScenario is not None: + scenario._ego = oldScenario._ego # inherit ego from parent + currentScenario = scenario + oldParams = _globalParameters + _globalParameters = scenario._globalParameters + try: + yield + except AttributeError as e: + # Convert confusing AttributeErrors from trying to access nonexistent scenario + # variables into NameErrors, which is what the user would expect. The information + # needed to do this was made available in Python 3.10, but unfortunately could be + # wrong until 3.10.3: see bpo-46940. + if sys.version_info >= (3, 10, 3) and isinstance(e.obj, DynamicScenario): + newExc = NameError(f"name '{e.name}' is not defined", name=e.name) + raise newExc.with_traceback(e.__traceback__) + else: + raise + finally: + currentScenario = oldScenario + _globalParameters = oldParams + + +def prepareScenario(scenario): + if currentSimulation: + verbosePrint(f"Starting scenario {scenario}", level=3) + + +def finishScenarioSetup(scenario): + global inInitialScenario + inInitialScenario = False + + +def startScenario(scenario): + assert scenario not in runningScenarios + runningScenarios.append(scenario) + + +def endScenario(scenario, reason, quiet=False): + runningScenarios.remove(scenario) + if not quiet: + verbosePrint(f"Stopping scenario {scenario} because: {reason}", level=3) + + +# Dynamic behaviors + + +@contextmanager +def executeInBehavior(behavior): + global currentBehavior + oldBehavior = currentBehavior + currentBehavior = behavior + try: + yield + except AttributeError as e: + # See comment for corresponding code in executeInScenario + if sys.version_info >= (3, 10, 3) and isinstance(e.obj, Behavior): + newExc = NameError(f"name '{e.name}' is not defined", name=e.name) + raise newExc.with_traceback(e.__traceback__) + else: + raise + finally: + currentBehavior = oldBehavior + + +@contextmanager +def executeInGuard(): + global evaluatingGuard + assert not evaluatingGuard + evaluatingGuard = True + try: + yield + finally: + evaluatingGuard = False + + +def _makeTerminationAction(agent, line): + assert activity == 0 + if agent: + scenario = agent._parentScenario() + assert scenario is not None + else: + scenario = None + return _EndScenarioAction(scenario, line) + + +def _makeSimulationTerminationAction(line): + assert activity == 0 + return _EndSimulationAction(line) + + +### Parsing support + + +class Modifier(typing.NamedTuple): + name: str + value: typing.Any + terminator: typing.Optional[str] = None + + +### Primitive statements and functions + + +def new(cls, specifiers): + if not (isinstance(cls, type) and issubclass(cls, Constructible)): + raise TypeError(f'"{cls.__name__}" is not a Scenic class') + return cls._withSpecifiers(specifiers) + + +def ego(obj=None): + """Function implementing loads and stores to the 'ego' pseudo-variable. + + The translator calls this with no arguments for loads, and with the source + value for stores. + """ + egoObject = currentScenario._ego + if obj is None: + if egoObject is None: + raise InvalidScenarioError("referred to ego object not yet assigned") + elif not isinstance(obj, Object): + if isinstance(obj, type) and issubclass(obj, Object): + suffix = " (perhaps you forgot 'new'?)" + else: + suffix = "" + ty = type(obj).__name__ + raise TypeError(f"tried to make non-object (of type {ty}) the ego object{suffix}") + else: + currentScenario._ego = obj + for scenario in runningScenarios: + if scenario._ego is None: + scenario._ego = obj + return egoObject + + +def workspace(workspace=None): + """Function implementing loads and stores to the 'workspace' pseudo-variable. + + See `ego`. + """ + if workspace is None: + if currentScenario._workspace is None: + raise InvalidScenarioError("referred to workspace not yet assigned") + elif not isinstance(workspace, Workspace): + raise TypeError(f"workspace {workspace} is not a Workspace") + elif needsSampling(workspace): + raise InvalidScenarioError("workspace must be a fixed region") + elif needsLazyEvaluation(workspace): + raise InvalidScenarioError( + "workspace uses value undefined " "outside of object definition" + ) + else: + currentScenario._workspace = workspace + return currentScenario._workspace + + +def require(reqID, req, line, name, prob=1): + """Function implementing the require statement.""" + if not name: + name = f"requirement on line {line}" + if evaluatingRequirement: + raise InvalidScenarioError("tried to create a requirement inside a requirement") + if req.has_temporal_operator and prob != 1: + raise InvalidScenarioError( + "requirements with temporal operators must have probability of 1" + ) + if currentSimulation is not None: # requirement being evaluated at runtime + if req.has_temporal_operator: + # support monitors on dynamic requirements and create dynamic requirements + currentScenario._addDynamicRequirement( + requirements.RequirementType.require, req, line, name + ) + else: + if prob >= 1 or Range(0, 1) <= prob: # use Range so value can be recorded + result = req.evaluate() + assert not needsSampling(result) + if needsLazyEvaluation(result): + raise InvalidScenarioError( + f"requirement on line {line} uses value" + " undefined outside of object definition" + ) + if not result: + raise RejectSimulationException(name) + else: # requirement being defined at compile time + currentScenario._addRequirement( + requirements.RequirementType.require, reqID, req, line, name, prob + ) + + +def require_monitor(reqID, value, line, name): + if not name: + name = f"requirement on line {line}" + if currentSimulation is not None: + monitor = value.evaluate() + assert not needsSampling(monitor) + if needsLazyEvaluation(monitor): + raise InvalidScenarioError( + f"requirement on line {line} uses value" + " undefined outside of object definition" + ) + if not isinstance(monitor, Monitor): + raise TypeError(f'"require monitor X" with X not a monitor on line {line}') + currentScenario._addMonitor(monitor) + else: + currentScenario._addRequirement( + requirements.RequirementType.monitor, reqID, value, line, name, 1 + ) + + +def record(reqID, value, line, name): + if not name: + name = f"record{line}" + makeRequirement(requirements.RequirementType.record, reqID, value, line, name) + + +def record_initial(reqID, value, line, name): + if not name: + name = f"record{line}" + makeRequirement(requirements.RequirementType.recordInitial, reqID, value, line, name) + + +def record_final(reqID, value, line, name): + if not name: + name = f"record{line}" + makeRequirement(requirements.RequirementType.recordFinal, reqID, value, line, name) + + +def require_always(reqID, req, line, name): + """Function implementing the 'require always' statement.""" + if not name: + name = f"requirement on line {line}" + makeRequirement(requirements.RequirementType.requireAlways, reqID, req, line, name) + + +def require_eventually(reqID, req, line, name): + """Function implementing the 'require eventually' statement.""" + if not name: + name = f"requirement on line {line}" + makeRequirement( + requirements.RequirementType.requireEventually, reqID, req, line, name + ) + + +def terminate_when(reqID, req, line, name): + """Function implementing the 'terminate when' statement.""" + if not name: + name = f"termination condition on line {line}" + makeRequirement(requirements.RequirementType.terminateWhen, reqID, req, line, name) + + +def terminate_simulation_when(reqID, req, line, name): + """Function implementing the 'terminate simulation when' statement.""" + if not name: + name = f"termination condition on line {line}" + makeRequirement( + requirements.RequirementType.terminateSimulationWhen, reqID, req, line, name + ) + + +def makeRequirement(ty, reqID, req, line, name): + if evaluatingRequirement: + raise InvalidScenarioError(f'tried to use "{ty.value}" inside a requirement') + elif currentBehavior is not None: + raise InvalidScenarioError(f'"{ty.value}" inside a behavior on line {line}') + elif currentSimulation is not None: + currentScenario._addDynamicRequirement(ty, req, line, name) + else: # requirement being defined at compile time + currentScenario._addRequirement(ty, reqID, req, line, name, 1) + + +def terminate_after(timeLimit, terminator=None): + if not isinstance(timeLimit, (builtins.float, builtins.int)): + raise TypeError('"terminate after N" with N not a number') + assert terminator in (None, "seconds", "steps") + inSeconds = terminator != "steps" + currentScenario._setTimeLimit(timeLimit, inSeconds=inSeconds) + + +def resample(dist): + """The built-in resample function.""" + if not isinstance(dist, Distribution): + return dist + try: + return dist.clone() + except NotImplementedError: + raise TypeError("cannot resample non-primitive distribution") from None + + +def verbosePrint( + *objects, level=1, indent=True, sep=" ", end="\n", file=sys.stdout, flush=False +): + """Built-in function printing a message only in verbose mode. + + Scenic's verbosity may be set using the :option:`-v` command-line option. + The simplest way to use this function is with code like + :scenic:`verbosePrint('hello world!')` or :scenic:`verbosePrint('details here', level=3)`; + the other keyword arguments are probably only useful when replacing more complex uses + of the Python `print` function. + + Args: + objects: Object(s) to print (`str` will be called to make them strings). + level (int): Minimum verbosity level at which to print. Default is 1. + indent (bool): Whether to indent the message to align with messages generated by + Scenic (default true). + sep, end, file, flush: As in `print`. + """ + if errors.verbosityLevel >= level: + if indent: + if currentSimulation: + indent = " " if errors.verbosityLevel >= 3 else " " + else: + indent = " " * activity if errors.verbosityLevel >= 2 else " " + print(indent, end="", file=file) + print(*objects, sep=sep, end=end, file=file, flush=flush) + + +def localPath(relpath): + """Convert a path relative to the calling Scenic file into an absolute path. + + For example, :scenic:`localPath('resource.dat')` evaluates to the absolute path + of a file called ``resource.dat`` located in the same directory as the + Scenic file where this expression appears. Note that the path is returned as a + `pathlib.Path` object. + """ + filename = traceback.extract_stack(limit=2)[0].filename + base = Path(filename).parent + return base.joinpath(relpath).resolve() + + +def simulation(): + """Get the currently-running `Simulation`. + + May only be called from code that runs at simulation time, e.g. inside + :term:`dynamic behaviors` and :keyword:`compose` blocks of scenarios. + """ + if isActive(): + raise InvalidScenarioError("used simulation() outside a behavior") + assert currentSimulation is not None + return currentSimulation + + +def simulator(sim): + global simulatorFactory + simulatorFactory = sim + + +def in_initial_scenario(): + return inInitialScenario + + +def override(*args): + if len(args) < 1: + raise TypeError('"override" missing an object') + elif len(args) < 2: + raise TypeError('"override" missing a list of specifiers') + obj = args[0] + if not isinstance(obj, Object): + raise TypeError(f'"override" passed non-Object {obj}') + specs = args[1:] + for spec in specs: + assert isinstance(spec, Specifier), spec + + currentScenario._override(obj, specs) + + +def model(namespace, modelName): + global loadingModel + if loadingModel: + raise InvalidScenarioError('Scenic world model itself uses the "model" statement') + if lockedModel is not None: + modelName = lockedModel + try: + loadingModel = True + module = importlib.import_module(modelName) + except ModuleNotFoundError as e: + if e.name == modelName: + raise InvalidScenarioError( + f"could not import world model {modelName}" + ) from None + else: + raise + finally: + loadingModel = False + names = module.__dict__.get("__all__", None) + if names is not None: + for name in names: + namespace[name] = getattr(module, name) + else: + for name, value in module.__dict__.items(): + if not name.startswith("_"): + namespace[name] = value + + +def param(params): + """Function implementing the param statement.""" + global loadingModel + if evaluatingRequirement: + raise InvalidScenarioError( + "tried to create a global parameter inside a requirement" + ) + elif currentSimulation is not None: + raise InvalidScenarioError( + "tried to create a global parameter during a simulation" + ) + for name, value in params.items(): + if name not in lockedParameters and ( + not loadingModel or name not in _globalParameters + ): + _globalParameters[name] = toDistribution(value) + + +class ParameterTableProxy(collections.abc.Mapping): + def __init__(self, map): + object.__setattr__(self, "_internal_map", map) + + def __getitem__(self, name): + return self._internal_map[name] + + def __iter__(self): + return iter(self._internal_map) + + def __len__(self): + return len(self._internal_map) + + def __getattr__(self, name): + return self.__getitem__(name) # allow namedtuple-like access + + def __setattr__(self, name, value): + raise InvalidScenarioError( + 'cannot modify globalParameters (use "param" statement)' + ) + + def _clone_table(self): + return ParameterTableProxy(self._internal_map.copy()) + + +def globalParameters(): + return ParameterTableProxy(_globalParameters) + + +def mutate(*objects, scale=1): + """Function implementing the mutate statement.""" + if evaluatingRequirement: + raise InvalidScenarioError("used mutate statement inside a requirement") + if len(objects) == 0: + objects = currentScenario._objects + if not isinstance(scale, (builtins.int, builtins.float)): + raise TypeError('"mutate X by Y" with Y not a number') + for obj in objects: + if not isinstance(obj, Object): + raise TypeError('"mutate X" with X not an object') + obj.mutationScale = scale + # Object will now require sampling even if it has no explicit dependencies. + obj._needsSampling = True + obj._isLazy = True + + +### Prefix operators + + +def Visible(region): + """The :grammar:`visible ` operator.""" + region = toType(region, Region, '"visible X" with X not a Region') + return region.intersect(ego().visibleRegion) + + +def NotVisible(region): + """The :grammar:`not visible ` operator.""" + region = toType(region, Region, '"not visible X" with X not a Region') + return region.difference(ego().visibleRegion) + + +# front of , etc. +ops = ( + "front", + "back", + "left", + "right", + "front left", + "front right", + "back left", + "back right", + "top", + "bottom", + "top front left", + "top front right", + "top back left", + "top back right", + "bottom front left", + "bottom front right", + "bottom back left", + "bottom back right", +) +template = '''\ +def {function}(X): + """The :grammar:`{syntax} of ` operator.""" + if not isinstance(X, Object): + raise TypeError('"{syntax} of X" with X not an Object') + return X.{property} +''' +for op in ops: + func = "".join(word.capitalize() for word in op.split(" ")) + prop = func[0].lower() + func[1:] + definition = template.format(function=func, syntax=op, property=prop) + exec(definition) + +### Infix operators + + +def FieldAt(X, Y): + """The :grammar:` at ` operator.""" + if isinstance(X, type) and issubclass(X, Constructible): + raise TypeError('"X at Y" with X not a vector field. (Perhaps you forgot "new"?)') + + if not isA(X, VectorField): + raise TypeError('"X at Y" with X not a vector field') + Y = toVector(Y, '"X at Y" with Y not a vector') + return X[Y] + + +def RelativeTo(X, Y) -> typing.Union[Vector, builtins.float, Orientation]: + """The :scenic:`{X} relative to {Y}` polymorphic operator. + + Allowed forms:: + + relative to # with at least one a field, the other a field or heading + relative to # and vice versa + relative to + relative to + relative to + """ + + # Define lazy RelativeTo helper + @distributionFunction + def lazyRelativeTo(X, Y) -> typing.Union[Vector, builtins.float, Orientation]: + return RelativeTo(X, Y) + + # Define type helpers + def knownOrientation(thing): + return isA(thing, Orientation) or ( + (not isLazy(thing)) + and canCoerce(thing, Orientation) + and (not canCoerce(thing, Vector)) + ) + + def knownHeading(thing): + return isA(thing, numbers.Real) or ( + (not isLazy(thing)) and canCoerce(thing, Heading) + ) + + def knownVector(thing): + return isA(thing, Vector) or ((not isLazy(thing)) and canCoerce(thing, Vector)) + + xf, yf = isA(X, VectorField), isA(Y, VectorField) + if xf or yf: + if xf and yf and X.valueType != Y.valueType: + raise TypeError('"X relative to Y" with X, Y fields of different types') + fieldType = X.valueType if xf else Y.valueType + error = '"X relative to Y" with field and value of different types' + + def helper(context): + pos = context.position.toVector() + xp = X[pos] if xf else toType(X, fieldType, error) + yp = Y[pos] if yf else toType(Y, fieldType, error) + return yp + xp + + return DelayedArgument({"position"}, helper) + + elif isA(X, OrientedPoint) or isA(Y, OrientedPoint): + # Ensure X and Y aren't both oriented points + if isA(X, OrientedPoint) and isA(Y, OrientedPoint): + raise TypeError('"X relative to Y" with X, Y both oriented points') + + # Extract the single oriented point and the other value + if isA(X, OrientedPoint): + op = X + other = Y + else: + op = Y + other = X + + # Check the other value's type + if isA(other, numbers.Real): + return op.heading + toHeading(other) + elif isA(other, Orientation): + return toOrientation(Y) * toOrientation(X) + elif knownVector(other): + other = toVector(other) + return op.relativize(other) + + # This case doesn't match (for now at least). Fall through. + pass + + elif knownOrientation(X) and knownOrientation(Y): + xf = toOrientation(X) + yf = toOrientation(Y) + + return yf * xf + + elif knownHeading(X) and knownHeading(Y): + xf = toHeading(X, f'"X relative to Y" with Y a heading but X a {type(X)}') + yf = toHeading(Y, f'"X relative to Y" with X a heading but Y a {type(Y)}') + + return xf + yf + + elif knownVector(X) or knownVector(Y): + xf = toVector(X, f'"X relative to Y" with Y a vector but X a {type(X)}') + yf = toVector(Y, f'"X relative to Y" with X a vector but Y a {type(Y)}') + + return xf + yf + + if isLazy(X) or isLazy(Y): + # We can't determine what case to use at this point. Try again when things are sampled. + return lazyRelativeTo(X, Y) + + raise TypeError( + f'"X relative to Y" with X and Y incompatible types (X a {type(X)}, Y a {type(Y)})' + ) + + +def OffsetAlong(X, H, Y): + """The :scenic:`{X} offset along {H} by {Y}` polymorphic operator. + + Allowed forms:: + + offset along by + offset along by + """ + X = toVector(X, '"X offset along H by Y" with X not a vector') + Y = toVector(Y, '"X offset along H by Y" with Y not a vector') + if isA(H, VectorField): + H = H[X] + H = toOrientation( + H, '"X offset along H by Y" with H not an orientation or vector field' + ) + return X.offsetLocally(H, Y) + + +def RelativePosition(X, Y=None): + """The :grammar:`relative position of [from ]` operator. + + If the :grammar:`from ` is omitted, the position of ego is used. + """ + X = toVector(X, '"relative position of X from Y" with X not a vector') + if Y is None: + Y = ego() + Y = toVector(Y, '"relative position of X from Y" with Y not a vector') + return X - Y + + +def RelativeHeading(X, Y=None): + """The :grammar:`relative heading of [from ]` operator. + + If the :grammar:`from ` is omitted, the heading of ego is used. + """ + X = toOrientation( + X, '"relative heading of X from Y" with X not a heading or orientation' + ) + if Y is None: + Y = ego().orientation + else: + Y = toOrientation(Y, '"relative heading of X from Y" with Y not a heading') + return normalizeAngle(X.yaw - Y.yaw) + + +def ApparentHeading(X, Y=None): + """The :grammar:`apparent heading of [from ]` operator. + + If the :grammar:`from ` is omitted, the position of ego is used. + """ + if not isA(X, OrientedPoint): + raise TypeError('"apparent heading of X from Y" with X not an OrientedPoint') + if Y is None: + Y = ego() + Y = toVector(Y, '"relative heading of X from Y" with Y not a vector') + return apparentHeadingAtPoint(X.position, X.heading, Y) + + +def DistanceFrom(X, Y=None): + """The :scenic:`distance from {X} to {Y}` polymorphic operator. + + Allowed forms:: + + distance from [to ] + distance from [to ] + distance from to + + If the :grammar:`to ` is omitted, the position of ego is used. + """ + X = toTypes( + X, (Vector, Region), '"distance from X to Y" with X neither a vector nor region' + ) + if Y is None: + Y = ego() + Y = toTypes( + Y, (Vector, Region), '"distance from X to Y" with Y neither a vector nor region' + ) + return X.distanceTo(Y) + + +def DistancePast(X, Y=None): + """The :grammar:`distance past of ` operator. + + If the :grammar:`of {oriented point}` is omitted, the ego object is used. + """ + X = toVector(X, '"distance past X" with X not a vector') + if Y is None: + Y = ego() + Y = toType(Y, OrientedPoint, '"distance past X of Y" with Y not an OrientedPoint') + return Y.distancePast(X) + + +# TODO(shun): Migrate to `AngleFrom` +def AngleTo(X): + """The :grammar:`angle to ` operator (using the position of ego as the reference).""" + X = toVector(X, '"angle to X" with X not a vector') + return ego().angleTo(X) + + +def AngleFrom(X=None, Y=None): + """The :grammar:`angle from to ` operator.""" + assert X is not None or Y is not None + if X is None: + X = ego() + X = toVector(X, '"angle from X to Y" with X not a vector') + if Y is None: + Y = ego() + Y = toVector(Y, '"angle from X to Y" with Y not a vector') + return X.angleTo(Y) + + +def AltitudeTo(X): + """The :grammar:`angle to ` operator (using the position of ego as the reference).""" + X = toVector(X, '"altitude to X" with X not a vector') + return ego().altitudeTo(X) + + +def AltitudeFrom(X=None, Y=None): + """The :grammar:`altitude from to ` operator.""" + assert X is not None or Y is not None + if X is None: + X = ego() + X = toVector(X, '"altitude from X to Y" with X not a vector') + if Y is None: + Y = ego() + Y = toVector(Y, '"altitude from X to Y" with Y not a vector') + return X.altitudeTo(Y) + + +def Follow(F, X, D): + """The :grammar:`follow from for ` operator.""" + if not isA(F, VectorField): + raise TypeError('"follow F from X for D" with F not a vector field') + X = toVector(X, '"follow F from X for D" with X not a vector') + D = toScalar(D, '"follow F from X for D" with D not a number') + pos = F.followFrom(X, D) + orientation = F[pos] + return OrientedPoint._with(position=pos, parentOrientation=orientation) + + +def VisibleFromOp(region, base): + """The :grammar:` visible from ` operator.""" + region = toType(region, Region, '"X visible from Y" with X not a Region') + if not isA(base, Point): + raise TypeError('"X visible from Y" with Y not a Point') + return region.intersect(base.visibleRegion) + + +def NotVisibleFromOp(region, base): + """The :grammar:` not visible from ` operator.""" + region = toType(region, Region, '"X visible from Y" with X not a Region') + if not isA(base, Point): + raise TypeError('"X not visible from Y" with Y not a Point') + + return region.difference(base.visibleRegion) + + +def CanSee(X, Y): + """The :scenic:`{X} can see {Y}` polymorphic operator. + + Allowed forms:: + + can see + can see + """ + if isActive(): + raise InvalidScenarioError( + '"can see" operator prohibited at top level of Scenic programs' + ) + + if not isA(X, Point): + raise TypeError('"X can see Y" with X not a Point, OrientedPoint, or Object') + + if not canCoerce(Y, Vector): + raise TypeError('"X can see Y" with Y not a Vector, Point, or Object') + + objects = toDistribution(currentScenario._objects) + + @distributionFunction + def canSeeHelper(X, Y, objects): + if not isA(Y, Point): + Y = toVector( + Y, '"X can see Y" with X not a Vector, Point, OrientedPoint, or Object' + ) + + occludingObjects = tuple( + obj for obj in objects if obj.occluding and X is not obj and Y is not obj + ) + + return X.canSee(Y, occludingObjects=occludingObjects) + + return canSeeHelper(X, Y, objects) + + +@distributionFunction +def Intersects(X, Y): + """The :scenic:`{X} intersects {Y}` operator.""" + if isA(X, Object): + return X.intersects(Y) + else: + return Y.intersects(X) + + +### Specifiers + + +def With(prop, val): + """The :grammar:`with ` specifier. + + Specifies the given property, with no dependencies. + """ + return Specifier(f"With({prop})", {prop: 1}, {prop: val}) + + +def At(pos): + """The :grammar:`at ` specifier. + + Specifies :prop:`position`, with no dependencies. + """ + pos = toVector(pos, 'specifier "at X" with X not a vector') + return Specifier("At", {"position": 1}, {"position": pos}) + + +def In(region): + """The :grammar:`in ` specifier. + + Specifies :prop:`position`, and optionally, :prop:`parentOrientation` if the given region + has a preferred orientation, with no dependencies. + """ + region = toType(region, Region, 'specifier "in R" with R not a Region') + pos = Region.uniformPointIn(region) + props = {"position": 1} + values = {"position": pos} + if alwaysProvidesOrientation(region): + props["parentOrientation"] = 3 + values["parentOrientation"] = region.orientation[pos] + return Specifier("In", props, values) + + +def ContainedIn(region): + """The :grammar:`contained in ` specifier. + + Specifies :prop:`position`, :prop:`regionContainedIn`, and optionally, :prop:`parentOrientation` + if the given region has a preferred orientation, with no dependencies. + """ + region = toType(region, Region, 'specifier "contained in R" with R not a Region') + pos = Region.uniformPointIn(region) + props = {"position": 1, "regionContainedIn": 1} + values = {"position": pos, "regionContainedIn": region} + if alwaysProvidesOrientation(region): + props["parentOrientation"] = 3 + values["parentOrientation"] = region.orientation[pos] + return Specifier("ContainedIn", props, values) + + +def On(thing): + """The :specifier:`on {X}` specifier. + + Specifies :prop:`position`, and optionally, :prop:`parentOrientation` if the given region + has a preferred orientation. Depends on :prop:`onDirection`, :prop:`baseOffset`, + and :prop:`contactTolerance`. + + Note that while :specifier:`on` can be used with `Region`, `Object` and `Vector`, + it cannot be used with a distribution containing anything other than `Region`. + + May be used to modify an already-specified :prop:`position` property. + + Allowed forms: + on + on + on + """ + if isA(thing, Object): + # Target is an Object: use its onSurface. + target = thing.onSurface + elif canCoerce(thing, Vector, exact=True): + # Target is a vector + target = toVector(thing) + elif canCoerce(thing, Region): + # Target is a region (or could theoretically be coerced to one), + # so we can use it as a target. + target = toType(thing, Region) + else: + raise TypeError('specifier "on R" with R not a Region, Object, or Vector') + + props = {"position": 1} + + if isA(target, Region) and alwaysProvidesOrientation(target): + props["parentOrientation"] = 2 + + def helper(context): + # Pick position based on whether we are specifying or modifying + if hasattr(context, "position"): + if isA(target, Vector): + raise TypeError('Cannot use modifying "on V" with V a vector.') + + pos = projectVectorHelper(target, context.position, context.onDirection) + elif isA(target, Vector): + pos = target + else: + pos = Region.uniformPointIn(target) + + values = {} + + contactOffset = Vector(0, 0, context.contactTolerance / 2) - context.baseOffset + + if "parentOrientation" in props: + values["parentOrientation"] = target.orientation[pos] + contactOffset = contactOffset.rotatedBy(values["parentOrientation"]) + + values["position"] = pos + contactOffset + + return values + + return ModifyingSpecifier( + "On", + props, + DelayedArgument({"onDirection", "baseOffset", "contactTolerance"}, helper), + modifiable_props={"position"}, + ) + + +@distributionFunction +def projectVectorHelper(region, pos, onDirection): + on_pos = region.projectVector(pos, onDirection=onDirection) + + if on_pos is None: + raise RejectionException("Unable to place object on surface.") + else: + return on_pos + + +def alwaysProvidesOrientation(region): + """Whether a Region or distribution over Regions always provides an orientation.""" + if isinstance(region, Region): + return region.orientation is not None + elif isinstance(region, MultiplexerDistribution) and all( + alwaysProvidesOrientation(opt) for opt in region.options + ): + return True + else: # TODO improve somehow! + try: + sample = region.sample() + return sample.orientation is not None or sample is nowhere + except RejectionException: + return False + except Exception as e: + warnings.warn( + f"While sampling internally to determine if a random region provides an orientation, the following exception was raised: {repr(e)}" + ) + return False + + +def OffsetBy(offset): + """The :grammar:`offset by ` specifier. + + Specifies :prop:`position`, and optionally :prop:`parentOrientation`, with no dependencies. + """ + offset = toVector(offset, 'specifier "offset by X" with X not a vector') + value = { + "position": RelativeTo(offset, ego()).toVector(), + "parentOrientation": ego().orientation, + } + return Specifier("OffsetBy", {"position": 1, "parentOrientation": 3}, value) + + +def OffsetAlongSpec(direction, offset): + """The :specifier:`offset along {X} by {Y}` polymorphic specifier. + + Specifies :prop:`position`, and optionally :prop:`parentOrientation`, with no dependencies. + + Allowed forms:: + + offset along by + offset along by + """ + pos = OffsetAlong(ego(), direction, offset) + parentOrientation = ego().orientation + return Specifier( + "OffsetAlong", + {"position": 1, "parentOrientation": 3}, + {"position": pos, "parentOrientation": parentOrientation}, + ) + + +def Beyond(pos, offset, fromPt=None): + """The :specifier:`beyond {X} by {Y} from {Z}` polymorphic specifier. + + Specifies :prop:`position`, and optionally :prop:`parentOrientation`, with no dependencies. + + Allowed forms:: + + beyond by [from ] + beyond by [from ] + + If the :grammar:`from ` is omitted, the position of ego is used. + """ + # Ensure X can be coerced into vector form + pos = toVector(pos, 'specifier "beyond X by Y" with X not a vector') + + # If no from vector is specified, assume ego + if fromPt is None: + fromPt = ego() + + fromPt = toVector(fromPt, 'specifier "beyond X by Y from Z" with Z not a vector') + + dType = underlyingType(offset) + + if dType is builtins.float or dType is builtins.int: + offset = Vector(0, offset, 0) + else: + # offset is not float or int, so try to coerce it into vector form. + offset = toVector( + offset, 'specifier "beyond X by Y" with X not a number or vector' + ) + + # If the from vector is oriented, set that to orientation. Else assume global coords. + if isA(fromPt, OrientedPoint): + orientation = fromPt.orientation + else: + orientation = Orientation.fromEuler(0, 0, 0) + + direction = pos - fromPt + sphericalCoords = direction.sphericalCoordinates() + offsetRotation = Orientation.fromEuler(sphericalCoords[1], sphericalCoords[2], 0) + + new_direction = pos + offset.applyRotation(offsetRotation) + + return Specifier( + "Beyond", + {"position": 1, "parentOrientation": 3}, + {"position": new_direction, "parentOrientation": orientation}, + ) + + +def VisibleFrom(base): + """The :grammar:`visible from ` specifier. + + Specifies :prop:`_observingEntity` and :prop:`position`, with no dependencies. + """ + if not isA(base, Point): + raise TypeError('specifier "visible from O" with O not a Point') + + def helper(self): + if mode2D: + position = Region.uniformPointIn(base.visibleRegion) + else: + containing_region = ( + currentScenario._workspace.region + if self.regionContainedIn is None + and currentScenario._workspace is not None + else self.regionContainedIn + ) + position = ( + Region.uniformPointIn(everywhere, tag="visible") + if containing_region is None + else Region.uniformPointIn(containing_region) + ) + + return {"position": position, "_observingEntity": base} + + return Specifier( + "Visible/VisibleFrom", + {"position": 3, "_observingEntity": 1}, + DelayedArgument({"regionContainedIn"}, helper), + ) + + +def VisibleSpec(): + """The :specifier:`visible` specifier (equivalent to :specifier:`visible from ego`). + + Specifies :prop:`_observingEntity` and :prop:`position`, with no dependencies. + """ + return VisibleFrom(ego()) + + +def NotVisibleFrom(base): + """The :grammar:`not visible from ` specifier. + + Specifies :prop:`_nonObservingEntity` and :prop:`position`, depending on :prop:`regionContainedIn`. + + See `VisibleFrom`. + """ + if not isA(base, Point): + raise TypeError('specifier "not visible from O" with O not a Point') + + def helper(self): + region = self.regionContainedIn + if region is None: + if currentScenario._workspace is None: + raise InvalidScenarioError( + '"not visible" specifier with no workspace or containing region defined' + ) + region = currentScenario._workspace.region + + if mode2D: + position = Region.uniformPointIn(region.difference(base.visibleRegion)) + else: + # We can't limit the available region since any spot could potentially be occluded. + position = Region.uniformPointIn(convertToFootprint(region)) + + return {"position": position, "_nonObservingEntity": base} + + return Specifier( + "NotVisible/NotVisibleFrom", + {"position": 3, "_nonObservingEntity": 1}, + DelayedArgument({"regionContainedIn"}, helper), + ) + + +def NotVisibleSpec(): + """The :specifier:`not visible` specifier (equivalent to :specifier:`not visible from ego`). + + Specifies :prop:`_nonObservingEntity` and :prop:`position`, depending on :prop:`regionContainedIn`. + """ + return NotVisibleFrom(ego()) + + +def LeftSpec(pos, dist=None): + """The :specifier:`left of {X} by {Y}` polymorphic specifier. + + Specifies :prop:`position`, and optionally, :prop:`parentOrientation`, depending on :prop:`width`. + + Allowed forms:: + + left of [by ] + left of [by ] + + If the :grammar:`by ` is omitted, the object's contact tolerance is used. + """ + return directionalSpecHelper( + "Left of", + pos, + dist, + "width", + lambda dist: (dist, 0, 0), + lambda self, dims, tol, dx, dy, dz: Vector( + -self.width / 2 - dx - dims[0] / 2 - tol, dy, dz + ), + ) + + +def RightSpec(pos, dist=None): + """The :specifier:`right of {X} by {Y}` polymorphic specifier. + + Specifies :prop:`position`, and optionally :prop:`parentOrientation`, depending on :prop:`width`. + + Allowed forms:: + + right of [by ] + right of [by ] + + If the :grammar:`by ` is omitted, zero is used. + """ + return directionalSpecHelper( + "Right of", + pos, + dist, + "width", + lambda dist: (dist, 0, 0), + lambda self, dims, tol, dx, dy, dz: Vector( + self.width / 2 + dx + dims[0] / 2 + tol, dy, dz + ), + ) + + +def Ahead(pos, dist=None): + """The :specifier:`ahead of {X} by {Y}` polymorphic specifier. + + Specifies :prop:`position`, and optionally :prop:`parentOrientation`, depending on :prop:`length`. + + Allowed forms:: + + ahead of [by ] + ahead of [by ] + + If the :grammar:`by ` is omitted, the object's contact tolerance is used. + """ + return directionalSpecHelper( + "Ahead of", + pos, + dist, + "length", + lambda dist: (0, dist, 0), + lambda self, dims, tol, dx, dy, dz: Vector( + dx, self.length / 2 + dy + dims[1] / 2 + tol, dz + ), + ) + + +def Behind(pos, dist=None): + """The :specifier:`behind {X} by {Y}` polymorphic specifier. + + Specifies :prop:`position`, and optionally :prop:`parentOrientation`, depending on :prop:`length`. + + Allowed forms:: + + behind [by ] + behind [by ] + + If the :grammar:`by ` is omitted, the object's contact tolerance is used. + """ + return directionalSpecHelper( + "Behind", + pos, + dist, + "length", + lambda dist: (0, dist, 0), + lambda self, dims, tol, dx, dy, dz: Vector( + dx, -self.length / 2 - dy - dims[1] / 2 - tol, dz + ), + ) + + +def Above(pos, dist=None): + """The :specifier:`above {X} by {Y}` polymorphic specifier. + + Specifies :prop:`position`, and optionally :prop:`parentOrientation`, depending on :prop:`height`. + + Allowed forms:: + + above [by ] + above [by ] + + If the :grammar:`by ` is omitted, the object's contact tolerance is used. + """ + return directionalSpecHelper( + "Above", + pos, + dist, + "height", + lambda dist: (0, 0, dist), + lambda self, dims, tol, dx, dy, dz: Vector( + dx, dy, self.height / 2 + dz + dims[2] / 2 + tol + ), + ) + + +def Below(pos, dist=None): + """The :specifier:`below {X} by {Y}` polymorphic specifier. + + Specifies :prop`position`, and optionally :prop:`parentOrientation`, depending on :prop:`height`. + + Allowed forms:: + + below [by ] + below [by ] + + If the :grammar:`by ` is omitted, the object's contact tolerance is used. + """ + return directionalSpecHelper( + "Below", + pos, + dist, + "height", + lambda dist: (0, 0, dist), + lambda self, dims, tol, dx, dy, dz: Vector( + dx, dy, -self.height / 2 - dz - dims[2] / 2 - tol + ), + ) + + +def directionalSpecHelper(syntax, pos, dist, axis, toComponents, makeOffset): + prop = {"position": 1} + if dist is None: + dx = dy = dz = 0 + elif canCoerce(dist, builtins.float): + dx, dy, dz = toComponents(coerce(dist, builtins.float)) + elif canCoerce(dist, Vector): + dx, dy, dz = coerce(dist, Vector) + else: + raise TypeError(f'"{syntax} X by D" with D not a number or vector') + + @distributionFunction + def makeContactOffset(dist, ct): + if dist is None: + return ct / 2 + else: + return 0 + + if isA(pos, Object): + prop["parentOrientation"] = 3 + obj_dims = (pos.width, pos.length, pos.height) + val = lambda self: { + "position": pos.relativePosition( + makeOffset( + self, + obj_dims, + makeContactOffset(dist, self.contactTolerance), + dx, + dy, + dz, + ) + ), + "parentOrientation": pos.orientation, + } + new = DelayedArgument({axis, "contactTolerance"}, val) + elif isA(pos, OrientedPoint): + prop["parentOrientation"] = 3 + val = lambda self: { + "position": pos.relativePosition(makeOffset(self, (0, 0, 0), 0, dx, dy, dz)), + "parentOrientation": pos.orientation, + } + new = DelayedArgument({axis}, val) + else: + pos = toVector(pos, f'specifier "{syntax} X" with X not a vector') + val = lambda self: { + "position": pos.offsetLocally( + self.orientation, makeOffset(self, (0, 0, 0), 0, dx, dy, dz) + ) + } + new = DelayedArgument({axis, "orientation"}, val) + return Specifier(syntax, prop, new) + + +def Following(field, dist, fromPt=None): + """The :specifier:`following {F} from {X} for {D}` specifier. + + Specifies :prop:`position`, and optionally :prop:`parentOrientation`, with no dependencies. + + Allowed forms:: + + following [from ] for + + If the :grammar:`from ` is omitted, the position of ego is used. + """ + if fromPt is None: + fromPt = ego() + field = toType(field, VectorField) + fromPt = toVector(fromPt, '"following F from X for D" with X not a vector') + dist = toScalar(dist, '"following F for D" with D not a number') + pos = field.followFrom(fromPt, dist) + orientation = field[pos] + return Specifier( + "Following", + {"position": 1, "parentOrientation": 3}, + {"position": pos, "parentOrientation": orientation}, + ) + + +def Facing(heading): + """The :specifier:`facing {X}` polymorphic specifier. + + Specifies :prop:`yaw`, :prop:`pitch`, and :prop:`roll`, depending on :prop:`parentOrientation`, + and depending on the form:: + + facing # no further dependencies; + facing # depends on 'position' + """ + if isA(heading, VectorField): + + def helper(context): + headingAtPos = heading[context.position] + if alwaysGlobalOrientation(context.parentOrientation): + orientation = headingAtPos # simplify expr tree in common case + else: + orientation = context.parentOrientation.inverse * headingAtPos + return { + "yaw": orientation.yaw, + "pitch": orientation.pitch, + "roll": orientation.roll, + } + + return Specifier( + "Facing", + {"yaw": 1, "pitch": 1, "roll": 1}, + DelayedArgument({"position", "parentOrientation"}, helper), + ) + else: + orientation = toOrientation( + heading, "facing x with x not a heading or orientation" + ) + orientationDeps = requiredProperties(orientation) + + def helper(context): + target_orientation = valueInContext(orientation, context) + euler = context.parentOrientation.localAnglesFor(target_orientation) + return {"yaw": euler[0], "pitch": euler[1], "roll": euler[2]} + + return Specifier( + "Facing", + {"yaw": 1, "pitch": 1, "roll": 1}, + DelayedArgument({"parentOrientation"} | orientationDeps, helper), + ) + + +def FacingToward(pos): + """The :grammar:`facing toward ` specifier. + + Specifies :prop:`yaw`, depending on :prop:`position` and :prop:`parentOrientation`. + """ + pos = toVector(pos, 'specifier "facing toward X" with X not a vector') + + def helper(context): + direction = pos - context.position + rotated = direction.applyRotation(context.parentOrientation.inverse) + sphericalCoords = ( + rotated.sphericalCoordinates() + ) # Ignore the rho, sphericalCoords[0] + return {"yaw": sphericalCoords[1]} + + return Specifier( + "FacingToward", + {"yaw": 1}, + DelayedArgument({"position", "parentOrientation"}, helper), + ) + + +def FacingDirectlyToward(pos): + """The :grammar:`facing directly toward ` specifier. + + Specifies :prop:`yaw` and :prop:`pitch`, depends on :prop:`position` and :prop:`parentOrientation`. + """ + pos = toVector(pos, 'specifier "facing directly toward X" with X not a vector') + + def helper(context): + """ + Same process as above, except by default also specify the pitch euler angle + """ + direction = pos - context.position + rotated = direction.applyRotation(context.parentOrientation.inverse) + sphericalCoords = rotated.sphericalCoordinates() + return {"yaw": sphericalCoords[1], "pitch": sphericalCoords[2]} + + return Specifier( + "FacingDirectlyToward", + {"yaw": 1, "pitch": 1}, + DelayedArgument({"position", "parentOrientation"}, helper), + ) + + +def FacingAwayFrom(pos): + """The :grammar:`facing away from ` specifier. + + Specifies :prop:`yaw`, depending on :prop:`position` and :prop:`parentOrientation`. + """ + pos = toVector(pos, 'specifier "facing away from X" with X not a vector') + + def helper(context): + """ + As in FacingToward, except invert the resulting rotation axis + """ + direction = context.position - pos + rotated = direction.applyRotation(context.parentOrientation.inverse) + sphericalCoords = rotated.sphericalCoordinates() + return {"yaw": sphericalCoords[1]} + + return Specifier( + "FacingAwayFrom", + {"yaw": 1}, + DelayedArgument({"position", "parentOrientation"}, helper), + ) + + +def FacingDirectlyAwayFrom(pos): + """The :grammar:`facing directly away from ` specifier. + + Specifies :prop:`yaw` and :prop:`pitch`, depending on :prop:`position` and :prop:`parentOrientation`. + """ + pos = toVector(pos, 'specifier "facing away from X" with X not a vector') + + def helper(context): + direction = context.position - pos + rotated = direction.applyRotation(context.parentOrientation.inverse) + sphericalCoords = rotated.sphericalCoordinates() + return {"yaw": sphericalCoords[1], "pitch": sphericalCoords[2]} + + return Specifier( + "FacingDirectlyToward", + {"yaw": 1, "pitch": 1}, + DelayedArgument({"position", "parentOrientation"}, helper), + ) + + +def ApparentlyFacing(heading, fromPt=None): + """The :grammar:`apparently facing [from ]` specifier. + + Specifies :prop:`yaw`, depending on :prop:`position` and :prop:`parentOrientation`. + + If the :grammar:`from ` is omitted, the position of ego is used. + """ + heading = toHeading(heading, 'specifier "apparently facing X" with X not a heading') + if fromPt is None: + fromPt = ego() + fromPt = toVector( + fromPt, 'specifier "apparently facing X from Y" with Y not a vector' + ) + + def helper(context): + return {"yaw": fromPt.angleTo(context.position) + heading} + + return Specifier( + "ApparentlyFacing", + {"yaw": 1}, + DelayedArgument({"position", "parentOrientation"}, helper), + ) + + +### Primitive internal functions, utilized after compiler conversion + + +@distributionFunction +def _toStrScenic(*args, **kwargs) -> str: + return builtins.str(*args, **kwargs) + + +@distributionFunction +def _toFloatScenic(*args, **kwargs) -> float: + return builtins.float(*args, **kwargs) + + +@distributionFunction +def _toIntScenic(*args, **kwargs) -> int: + return builtins.int(*args, **kwargs) + + +### Primitive functions overriding Python builtins + +# N.B. applying functools.wraps to preserve the metadata of the original +# functions seems to break pickling/unpickling + + +@distributionFunction +def filter(function, iterable): + return list(builtins.filter(function, iterable)) + + +@distributionFunction +def round(*args, **kwargs): + return builtins.round(*args, **kwargs) + + +def len(obj): + return obj.__len__() + + +def range(*args): + if any(needsSampling(arg) for arg in args): + raise RandomControlFlowError("cannot construct a range with random parameters") + return builtins.range(*args) + + +### Temporal Operators Factories + + +def AtomicProposition(closure, syntaxId): + return propositions.Atomic(closure, syntaxId) + + +def PropositionAnd(reqs): + return propositions.And(reqs) + + +def PropositionOr(reqs): + return propositions.Or(reqs) + + +def PropositionNot(req): + return propositions.Not(req) + + +def Always(req): + return propositions.Always(req) + + +def Eventually(req): + return propositions.Eventually(req) + + +def Next(req): + return propositions.Next(req) + + +def Until(lhs, rhs): + return propositions.Until(lhs, rhs) + + +def Implies(lhs, rhs): + return propositions.Implies(lhs, rhs) From 3295ef79132512a51cc33552bad759f468698bcf Mon Sep 17 00:00:00 2001 From: Kay Date: Mon, 16 Mar 2026 12:12:42 -0700 Subject: [PATCH 52/73] compositional scenario test case --- examples/cosim/test_flows.scenic | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 examples/cosim/test_flows.scenic diff --git a/examples/cosim/test_flows.scenic b/examples/cosim/test_flows.scenic new file mode 100644 index 000000000..21cdf7003 --- /dev/null +++ b/examples/cosim/test_flows.scenic @@ -0,0 +1,30 @@ +# param startTime = 0 +param map = localPath('../../assets/maps/CARLA/Town05.xodr') # OpenDrive file +param xml_map = localPath("../../assets/maps/CARLA/Town05.net.xml") # Sumo file +param address = "10.139.168.114" +# param address = "10.0.0.122" +# param verbose = True +model scenic.simulators.cosim.model + +scenario CustomCommuterTrafficStream(origin, destination): + setup: + num_commuters = Range(100, 200) + morning_peak_time = 1*60*60 # Normal(9*60*60, 30*60) + evening_peak_time = 2*60*60 # Normal(17*60*60, 30*60) + traffic_stddev = 15*60 # Normal(1*60*60, 10*60) + + compose: + do CommuterTrafficStream(origin, destination, num_commuters, + morning_peak_time, evening_peak_time, traffic_stddev) + +scenario Main(): + setup: + ego = new EgoCar with name "ego", with behavior DriveAvoidingCollisions(target_speed=15, avoidance_threshold=12) + compose: + ts_2_21 = CustomCommuterTrafficStream(2, 21) + ts_3_21 = CustomCommuterTrafficStream(3, 21) + ts_4_21 = CustomCommuterTrafficStream(4, 21) + ts_7_21 = CustomCommuterTrafficStream(7, 21) + ts_11_21 = CustomCommuterTrafficStream(11, 21) + + do ts_2_21, ts_3_21, ts_4_21, ts_7_21, ts_11_21 for 3*60*60 seconds # 16*60*60 seconds From b4f9bc3dfb83d4d074d111ba878ff123013166f5 Mon Sep 17 00:00:00 2001 From: Kay Date: Mon, 16 Mar 2026 12:12:51 -0700 Subject: [PATCH 53/73] minor --- examples/cosim/test.scenic | 6 +++--- src/scenic/simulators/cosim/utils/utils.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/cosim/test.scenic b/examples/cosim/test.scenic index ffd7bc2fc..b4b3b9eb9 100644 --- a/examples/cosim/test.scenic +++ b/examples/cosim/test.scenic @@ -1,14 +1,14 @@ # param startTime = 0 param map = localPath('../../assets/maps/CARLA/Town05.xodr') # OpenDrive file param xml_map = localPath("../../assets/maps/CARLA/Town05.net.xml") # Sumo file -# param address = "172.21.116.114" -param address = "10.0.0.122" +param address = "10.139.168.114" +# param address = "10.0.0.122" # param verbose = True model scenic.simulators.cosim.model ego = new EgoCar with name "ego", with behavior DriveAvoidingCollisions(target_speed=15, avoidance_threshold=12) -for i in range(40): +for i in range(100): title = f"npccar_{i}" # allow me to debug more easily vehicle = new NPCCar with name title diff --git a/src/scenic/simulators/cosim/utils/utils.py b/src/scenic/simulators/cosim/utils/utils.py index 3c33a53ab..4fc58c1c8 100644 --- a/src/scenic/simulators/cosim/utils/utils.py +++ b/src/scenic/simulators/cosim/utils/utils.py @@ -89,7 +89,7 @@ def within_threshold_to(object, cars, verbose=False) -> bool: dist = np.linalg.norm(np.array(car.position) - object_pos) if dist < threshold: is_close=True - obj_distances.append(dist) + obj_distances.append([car.name, dist]) if verbose: print(f"Distances were: {obj_distances}") return is_close From d229dc4854cef5452c6986798356d4338e7ae6a5 Mon Sep 17 00:00:00 2001 From: Kay Date: Mon, 16 Mar 2026 12:49:21 -0700 Subject: [PATCH 54/73] fixed ego spawn logic --- src/scenic/simulators/cosim/simulator.py | 103 ++++++----------------- 1 file changed, 27 insertions(+), 76 deletions(-) diff --git a/src/scenic/simulators/cosim/simulator.py b/src/scenic/simulators/cosim/simulator.py index 0746f4d6e..e47a68d98 100644 --- a/src/scenic/simulators/cosim/simulator.py +++ b/src/scenic/simulators/cosim/simulator.py @@ -377,7 +377,7 @@ def createObjectInCarla(self, obj: Object, update_orientation: bool = False, tra print(f"Query Vehicle resuts for obj: :{car}, is: {car_data['DATA']}") print(f"Checking distance function: {_utils.within_threshold_to(obj,[obj for obj in self.objects if obj.carla_actor_flag], verbose=True)}") - print(f"Issue occured at timestop: {self.count}") + print(f"Issue occured at timestep: {self.count}") # raise e(f"Error : {e} occured") if carlaActor is None: raise SimulationCreationError(f"Unable to spawn object {obj}") @@ -427,24 +427,20 @@ def createObjectInSimulator(self, obj: Object) -> None: assert hasattr(obj, "carla_actor_flag"), "All objects must have attribute: carla_actor_flag" - if obj == self.objects[0]: # ensures consistency for object queue spawning in METSR + if obj == self.objects[0]: # Special handling for ego trajectory = None if hasattr(obj, "trajectory"): if obj.trajectory is not None: trajectory = self.scenic_trajectory_to_carla(trajectory) lane = self._nearest_lane(obj) origin_str = self.map_scenic_to_metsr(lane) - if origin_str[0] == ":": # special handling for ":" suffix on specific roads - origin = int(origin_str[1:]) - else: - origin = int(origin_str) - self.createObjectInCarla(obj, False, trajectory) - self.createObjectInMetsr(obj,origin=origin) # Set the METSR vehicle origin to match ego spawn - obj.final_road = None - self.synchronize_clients(obj) # First time ego is created synchronize clients - if self.count == 0: - self.metsr_client.tick() - + self.createObjectInMetsr(obj,origin=origin_str) # Set the METSR vehicle origin to match ego spawn + self.metsr_client.tick() # tick the simulation to allow the ego to enter the road + car_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True)["DATA"][0] + obj.position = Vector(car_data["x"], car_data["y"], 0) + obj.final_road = None + self.createObjectInCarla(obj, update_orientation=True, trajectory=trajectory) # spawn ego in updated location and update orientation + elif obj.carla_actor_flag: self.createObjectInCarla(obj) self.synchronize_clients(obj) @@ -615,7 +611,10 @@ def step(self) -> None: def get_carla_lanes(self) -> list[Lane]: """ - Collect the current set of lanes intersecting the CoSimulation bubble + docstring for get_carla_lanes + + :return: The current set of lanes intersecting the CoSimulation bubble + :rtype: list[Lane] """ # Set ego, lane, bubble ego = self.objects[0] @@ -754,7 +753,7 @@ def _check_light_consistency(self, light : carla.TrafficLight, light_config : di for key in light_config: if key in light_state_dict: if light_config[key] != light_state_dict[key]: - print(f"Carla: {light_state_dict}, Metsr: {light_config}") + # print(f"Carla: {light_state_dict}, Metsr: {light_config}") break # assert light_config[key] == light_state_dict[key], f" Contradicting light states encountered: current light : {light_state_dict} proposed state {light_config}" else: @@ -804,7 +803,7 @@ def synchronize_clients(self, obj: Object | list[Object] = None): for obj in carla_actors: loc = obj.carlaActor.get_location() if loc == carla.Location(0,0,0): # Carla object still in the process of spawning - print(f'Passing sychronization while object spawn is processing') + # print(f'Passing sychronization while object spawn is processing') continue vehID = self.getMetsrPrivateVehId(obj) lane = self._nearest_lane(obj) @@ -817,9 +816,7 @@ def synchronize_clients(self, obj: Object | list[Object] = None): # Find the corresponding METSR keys carla_lane_ids = set([self.map_scenic_to_metsr(lane) for lane in self.frozen_scenic_lanes]) if roadID not in carla_lane_ids: - print(f"Vehicle is leaving the bubble region") - else: - print(f"Vehicle remains in the bubble region") + print(f"{obj.name} leaving the bubble region") self.metsr_client.enter_next_road(vehID, roadID=roadID, private_veh = True) if obj.previous_road == obj.final_road: obj.finished_route = True @@ -893,9 +890,9 @@ def update_bubble_objects(self, carla_lanes: list[Lane], new_lanes: list[str], i if obj.carla_actor_flag: if obj.finished_route: carla_actors.remove(obj) - print(f'removing vehicle: {obj.name} after completing its route') - print(f"Current road_lane (Scenic) is {f'{lane.road.id}_{lane.id}'}") - print(f"{veh_data['DATA'][0]}") + # print(f'removing vehicle: {obj.name} after completing its route') + # print(f"Current road_lane (Scenic) is {f'{lane.road.id}_{lane.id}'}") + # print(f"{veh_data['DATA'][0]}") self.remove_bubble_object(obj) elif (lane not in carla_lanes) and (intersection not in intersections): @@ -913,7 +910,7 @@ def update_bubble_objects(self, carla_lanes: list[Lane], new_lanes: list[str], i if not_enough_space: if obj not in self.bubble_spawn_queue: _utils.within_threshold_to(obj,carla_actors, verbose=True) - print(f"Not enough space to spawn {obj.name} at {self.count} in location: {obj.position}:") + # print(f"Not enough space to spawn {obj.name} at {self.count} in location: {obj.position}:") self.bubble_spawn_queue.add(obj) self.check_world_state_consistency() continue @@ -934,16 +931,16 @@ def update_bubble_objects(self, carla_lanes: list[Lane], new_lanes: list[str], i if carla_trajectory == None: if obj not in self.bubble_spawn_queue: - print(f"No valid trajectory found for vehicle data: {[data_entry['route'] for data_entry in cosim_data['DATA'] if data_entry['ID'] == VehID]} ") - print(f"Skipping spawn for obj: {obj.name} at {lane.road.id}_{lane.id} with intersection {obj._intersection}: due to failed trajectory generation") - print(f"Obj location was: :{obj.x, obj.y} default road was: {obj._lane.road.id if obj.lane != None else None} selected road was: {lane.road.id}") + # print(f"No valid trajectory found for vehicle data: {[data_entry['route'] for data_entry in cosim_data['DATA'] if data_entry['ID'] == VehID]} ") + # print(f"Skipping spawn for obj: {obj.name} at {lane.road.id}_{lane.id} with intersection {obj._intersection}: due to failed trajectory generation") + # print(f"Obj location was: :{obj.x, obj.y} default road was: {obj._lane.road.id if obj.lane != None else None} selected road was: {lane.road.id}") self.bubble_spawn_queue.add(obj) continue # Do not spawn vehicle if no trajectory can be created - if obj._intersection in intersections: - print(f"Spawning object {obj} in intersection") + # if obj._intersection in intersections: + # print(f"Spawning object {obj} in intersection") - print(f"Spawning obj: {obj.name} in CARLA at : {obj.x, obj.y}") + # print(f"Spawning obj: {obj.name} in CARLA at : {obj.x, obj.y}") self.createObjectInCarla(obj,update_orientation=True, trajectory=carla_trajectory) if obj in self.bubble_spawn_queue: @@ -987,7 +984,6 @@ def freeze_lanes(self, keys: list[str]) -> None: for key in keys: assert key not in self.carla_control_roads, "Attempted to freeze already frozen lane" self.carla_control_roads[key] = True # Keep track of frozen lanes - # print(f"Freezing METSR key: {key}") self.metsr_client.set_cosim_road(key) @@ -1004,7 +1000,6 @@ def release_lanes(self,keys: list[str]) -> None: for key in keys: assert key in self.carla_control_roads, "Attempted to release non frozen lane" del self.carla_control_roads[key] # Remove frozen lane from record - # print(f"Releasing METSR key: {key}") self.metsr_client.release_cosim_road(key) @@ -1224,14 +1219,6 @@ def updateObjects(self) -> None: raw_veh_data = self.metsr_client.query_vehicle(obj_veh_ids, True, True) self.obj_data_cache = {obj: raw_veh_data['DATA'][i] for i, obj in enumerate(self.objects)} - # #DEBUGGING FOR METSR - # for obj in self.obj_data_cache: - # if 'dist' not in self.obj_data_cache[obj]: - # self.queued_vehicles[obj] = True - # elif 'dist' in self.obj_data_cache[obj] and obj in self.queued_vehicles: - # print(f"obj {obj} leaving the spawn queue") - # del self.queued_vehicles[obj] - super().updateObjects() self.obj_data_cache = None @@ -1263,16 +1250,12 @@ def check_world_state_consistency(self) -> None: if not np.isclose(metsr_y, -carla_y): print(f"OBJ Y: {obj}| METSR {metsr_y}: CARLA {-carla_y}: SCENIC {obj.position[1]}") print("=" * 25) - print(f"Finished checking world staet") - - # metsr_data = self.metsr_client.query_coSimVehicle() - # print(f"Displaying METSR query data: {metsr_data}") + print(f"Finished checking world state") def _check_traffic_light_consistency(self): """ docstring for check_traffic_light_consistency - """ signals_ids = self.metsr_client.query_signal()['id_list'] signal_data = self.metsr_client.query_signal(signals_ids) @@ -1305,35 +1288,3 @@ def _check_traffic_light_consistency(self): else: assert True, f"Failed to find corresponding intersection for METSR light: {light_id}" - - -# Leftovers I am preserving in case they become useful -""" - # metsr_light_group = {} - - # for opendrive_id in list(lights_by_opendrive_id.keys()): - # associated_lights = [] - # for light_data in signal_data["DATA"]: - # key = light_data["groupID"] - # targets= self.xml_to_xodr_intersections[key] - # if len(targets) > 1: - # for target in targets: - # if target == opendrive_id: - # associated_lights.append(light_data["ID"]) - # else: - # target = targets[0] - # if target == opendrive_id: - # associated_lights.append(light_data["ID"]) - - # metsr_light_group[opendrive_id] = associated_lights - - # print(f"Displaying configs for : {opendrive_id}") - # for id in metsr_light_group[opendrive_id]: - # data = self.metsr_client.query_signal(id)["DATA"][0] - # light_config = self.get_light_config(data) - # print(light_config) - # print(f"==========================================") - - # # print(f"finishing consistency check at : {self.count}") - -""" \ No newline at end of file From a6517fd07c97fd7fc4d8bd86755419772c094434 Mon Sep 17 00:00:00 2001 From: Kay Date: Mon, 16 Mar 2026 15:05:42 -0700 Subject: [PATCH 55/73] updated readme --- examples/cosim/readme.md | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/examples/cosim/readme.md b/examples/cosim/readme.md index b71180dfb..ba42a28ba 100644 --- a/examples/cosim/readme.md +++ b/examples/cosim/readme.md @@ -1,5 +1,28 @@ -In order to run cosimulation using both METSR and Carla the user needs to supply both a SUMO and openDrive file. -To generate the associated SUMO file from an opendrive file one run the following command: - -netconvert --opendrive-files example.xodr --output-file example.net.xml --geometry.min-radius.fix --geometry.remove --opendrive.curve-resolution 1 --opendrive.import-all-lanes --output.original-names --tls.guess-signals --tls.discard-simple --tls.join - +To run the CoSimulator you first must start by opening and running both CARLA and METS-R + +Setting up METSR: + 1. Create a virtual env `python -m venv metsr_venv` + 2. Run `git clone clone https://github.com/umnilab/METS-R_HPC` + 3. Create an instance of METSR sim + a. An example template for this is provided in `run_blank.py` + +For METSR specific details please review the its documentation: `https://umnilab.github.io/METS-R_doc/` + +Setting up CARLA: + 1. Ensure CARLA is open an running on your desktop + +For CARLA specific details please review the CARLA examples folder or its documentation: `https://carla.readthedocs.io/en/latest/python_api/` + +Finally to run the scenic program first ensure + (i) The `globalParameter address` set at the top of your Scenic program matches your current IP address to allow Scenic to connect to CARLA + (2) The `globalParameter map` set at the top of your Scenic program provides the path to the corresponding `xodr` map you would like to run + (a) Ensure that this parameter matches the configuration file used to spin up METSR + (3) The `globalParameter xml_map` matches provides the path to the corresponding `xml` map. + `xml` map files can be generated from the provided sumo file found in `assets\maps\CARLA` using the command noted at the end fo this file + +Now the simulator can be started running the following command: `scenic [your_file.scenic] --simulate --2d` + +In order to run cosimulation using both METSR and Carla the user needs to supply both a SUMO and openDrive file. +To generate the associated SUMO file from an opendrive file one run the following command: + +netconvert --opendrive-files example.xodr --output-file example.net.xml --geometry.min-radius.fix --geometry.remove --opendrive.curve-resolution 1 --opendrive.import-all-lanes --output.original-names --tls.guess-signals --tls.discard-simple --tls.join \ No newline at end of file From 03858a308bb7b9684735f2c2b8f043071eec654d Mon Sep 17 00:00:00 2001 From: Kay Date: Mon, 16 Mar 2026 15:06:02 -0700 Subject: [PATCH 56/73] template script to spin up metsr --- src/scenic/simulators/cosim/run_blank.py | 42 ++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/scenic/simulators/cosim/run_blank.py diff --git a/src/scenic/simulators/cosim/run_blank.py b/src/scenic/simulators/cosim/run_blank.py new file mode 100644 index 000000000..b74c9036f --- /dev/null +++ b/src/scenic/simulators/cosim/run_blank.py @@ -0,0 +1,42 @@ +import sys +import os +import argparse +import time +from utils.util import read_run_config, prepare_sim_dirs, run_simulations, run_simulations_in_background, run_simulation_in_docker + +# use case: python run_blank.py -r configs/run_cosim_blank.json -v +def get_arguments(argv): + parser = argparse.ArgumentParser(description='METS-R simulation') + parser.add_argument('-r','--run_config', default='configs/run_cosim_CARLAT5.json', + help='the folder that contains all the input data') + parser.add_argument('-v', '--verbose', action='store_true', default=False, + help='verbose mode') + parser.add_argument('-n', '--name', default="random_choice") + args = parser.parse_args(argv) + + config = read_run_config(args.run_config) + config.verbose = args.verbose + + return config + +if __name__ == '__main__': + config = get_arguments(sys.argv[1:]) + os.chdir("docker") + os.system("docker-compose up -d") + os.chdir("..") + + time.sleep(10) # wait 10s for the Kafka servers to be up + + # Prepare simulation directories + dest_data_dirs = prepare_sim_dirs(config) + + try: + # Launch the simulations + container_ids = run_simulation_in_docker(config) + print("Docker Started Successfully") + while True: + time.sleep(1) + finally: + for cid in container_ids: + print(f"Stopping docker cid: {cid}") + os.system(f"docker stop {cid}") From b3ae21f1ceeb3c438ec0dcf989f42c5598421974 Mon Sep 17 00:00:00 2001 From: Kay Vargas <89169913+Kv139@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:18:04 -0700 Subject: [PATCH 57/73] Update readme with improved formatting and clarity --- examples/cosim/readme.md | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/examples/cosim/readme.md b/examples/cosim/readme.md index ba42a28ba..de13c331d 100644 --- a/examples/cosim/readme.md +++ b/examples/cosim/readme.md @@ -1,28 +1,29 @@ -To run the CoSimulator you first must start by opening and running both CARLA and METS-R +# To run the CoSimulator + Start by opening and running both CARLA and METS-R -Setting up METSR: - 1. Create a virtual env `python -m venv metsr_venv` - 2. Run `git clone clone https://github.com/umnilab/METS-R_HPC` - 3. Create an instance of METSR sim - a. An example template for this is provided in `run_blank.py` +# Setting up METSR: +1. Create a virtual env `python -m venv metsr_venv` +2. Run `git clone clone https://github.com/umnilab/METS-R_HPC` +3. Create an instance of METSR sim +a. An example template for this is provided in `run_blank.py` -For METSR specific details please review the its documentation: `https://umnilab.github.io/METS-R_doc/` +For METSR specific details please review the its documentation: https://umnilab.github.io/METS-R_doc/` -Setting up CARLA: - 1. Ensure CARLA is open an running on your desktop +# Setting up CARLA: +1. Ensure CARLA is open an running on your desktop -For CARLA specific details please review the CARLA examples folder or its documentation: `https://carla.readthedocs.io/en/latest/python_api/` +For CARLA specific details please review the CARLA examples folder or its documentation: https://carla.readthedocs.io/en/latest/python_api/ -Finally to run the scenic program first ensure - (i) The `globalParameter address` set at the top of your Scenic program matches your current IP address to allow Scenic to connect to CARLA - (2) The `globalParameter map` set at the top of your Scenic program provides the path to the corresponding `xodr` map you would like to run - (a) Ensure that this parameter matches the configuration file used to spin up METSR - (3) The `globalParameter xml_map` matches provides the path to the corresponding `xml` map. +# Finally to run the scenic program first ensure +- The `globalParameter address` set at the top of your Scenic program matches your current IP address to allow Scenic to connect to CARLA +- The `globalParameter map` set at the top of your Scenic program provides the path to the corresponding `xodr` map you would like to run +- Ensure that this parameter matches the configuration file used to spin up METSR +- The `globalParameter xml_map` matches provides the path to the corresponding `xml` map. `xml` map files can be generated from the provided sumo file found in `assets\maps\CARLA` using the command noted at the end fo this file Now the simulator can be started running the following command: `scenic [your_file.scenic] --simulate --2d` In order to run cosimulation using both METSR and Carla the user needs to supply both a SUMO and openDrive file. -To generate the associated SUMO file from an opendrive file one run the following command: +To generate the associated SUMO file from an opendrive file run the following command: -netconvert --opendrive-files example.xodr --output-file example.net.xml --geometry.min-radius.fix --geometry.remove --opendrive.curve-resolution 1 --opendrive.import-all-lanes --output.original-names --tls.guess-signals --tls.discard-simple --tls.join \ No newline at end of file +```netconvert --opendrive-files example.xodr --output-file example.net.xml --geometry.min-radius.fix --geometry.remove --opendrive.curve-resolution 1 --opendrive.import-all-lanes --output.original-names --tls.guess-signals --tls.discard-simple --tls.join``` From a2db6671ec16a2ac79a8aa9d8958ef82c3cc22e9 Mon Sep 17 00:00:00 2001 From: Kay Date: Tue, 17 Mar 2026 12:32:13 -0700 Subject: [PATCH 58/73] added special handling for connecting lanes between intersections --- src/scenic/simulators/cosim/simulator.py | 52 +++++++++++++++++++++--- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/src/scenic/simulators/cosim/simulator.py b/src/scenic/simulators/cosim/simulator.py index e47a68d98..3409e6b84 100644 --- a/src/scenic/simulators/cosim/simulator.py +++ b/src/scenic/simulators/cosim/simulator.py @@ -177,6 +177,7 @@ def __init__(self, scene, carla_client, metsr_client, sim_timestep, tm, render , self.bubble_spawn_queue = set({}) self.frozen_scenic_lanes = [] self.xml_to_xodr_intersections = xml_to_xodr_intersections + self.intersection_road_links = [] super().__init__(scene, timestep=sim_timestep, **kwargs) @@ -190,6 +191,16 @@ def setup(self) -> None: """ self.metsr_client.reset() # Updated version takes no arguements + valid_metsr_roads = self.metsr_client.query_road() + generated_roads = [value.split('_')[0] for value in self.scenic_to_metsr_map.values()] + + # print(generated_mappings) + # print(valid_roads['orig_id']) + + for mapping in generated_roads: + if mapping not in valid_metsr_roads["orig_id"]: + self.intersection_road_links.append(mapping) + weather = self.scene.params.get("weather") if weather is not None: if isinstance(weather, str): @@ -235,6 +246,7 @@ def setup(self) -> None: _, new_lanes, _ = self.update_carla_lanes() # TODO bad function name self.freeze_lanes(new_lanes) + print(f"Ticking Metsr") self.metsr_client.tick() for obj in self.objects: @@ -278,6 +290,7 @@ def createObjectInMetsr(self, obj: Object, origin: int = None, destination: int Creates vehicle inside the METSR simulator """ + print(f"Creating obj {obj.name if hasattr(obj,'name') else 'NPC'} in metsr") assert obj.origin, "Metsr objects must have a defined origin" assert obj.destination, "Metsr objects must have a defined destination" @@ -302,6 +315,7 @@ def createObjectInCarla(self, obj: Object, update_orientation: bool = False, tra :type update_orientation: bool """ + print(f"Creating obj {obj.name if hasattr(obj,'name') else 'NPC'} in Carla") try: blueprint = self.blueprintLib.find(obj.blueprint) except IndexError as e: @@ -596,8 +610,12 @@ def step(self) -> None: if self.count % 100 == 0: # just removing for now print(".", end="", flush=True) + print(f"Ticking METSR") self.metsr_client.tick() + cosim_data = self.metsr_client.query_coSimVehicle() + print(f"Displaying CoSim data at step: {self.count}: {cosim_data}") + # Generate bubble region based on ego objects TODO update logic for multiple high-fidelity zones self.objects[0].bubble = CircularRegion(center=[self.objects[0].x, self.objects[0].y], @@ -809,14 +827,23 @@ def synchronize_clients(self, obj: Object | list[Object] = None): lane = self._nearest_lane(obj) roadID = self.map_scenic_to_metsr(lane) veh_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True) + # Update METSR road if hasattr(obj, "previous_road"): - if roadID != obj.previous_road: + if roadID != obj.previous_road and roadID not in self.intersection_road_links: # Entering new road within metsr network print(f"obj: {obj.name} leaving road: {obj.previous_road} moving to : {roadID}") + # cosim_data = self.metsr_client.query_coSimVehicle() + # # print(f"Cosim data is: {cosim_data}") + # # for data_entry in cosim_data['DATA']: + # # if data_entry['ID'] == vehID: + # # route_data = data_entry['route'] + # # print(f"Displaying vehicle route data for {obj.name}: {route_data}") # Find the corresponding METSR keys carla_lane_ids = set([self.map_scenic_to_metsr(lane) for lane in self.frozen_scenic_lanes]) if roadID not in carla_lane_ids: print(f"{obj.name} leaving the bubble region") + else: + print(f"{obj.name} entering lane: {roadID}") self.metsr_client.enter_next_road(vehID, roadID=roadID, private_veh = True) if obj.previous_road == obj.final_road: obj.finished_route = True @@ -983,8 +1010,10 @@ def freeze_lanes(self, keys: list[str]) -> None: keys = set(keys) for key in keys: assert key not in self.carla_control_roads, "Attempted to freeze already frozen lane" - self.carla_control_roads[key] = True # Keep track of frozen lanes - self.metsr_client.set_cosim_road(key) + if key not in self.intersection_road_links: # Skip roads not recognized by metsr + self.carla_control_roads[key] = True # Keep track of frozen lanes + self.metsr_client.set_cosim_road(key) + print(f"Freezing: {key}") def release_lanes(self,keys: list[str]) -> None: @@ -999,8 +1028,10 @@ def release_lanes(self,keys: list[str]) -> None: keys = set(keys) for key in keys: assert key in self.carla_control_roads, "Attempted to release non frozen lane" - del self.carla_control_roads[key] # Remove frozen lane from record - self.metsr_client.release_cosim_road(key) + if key not in self.intersection_road_links: # Skip roads not recognized by metsr + del self.carla_control_roads[key] # Remove frozen lane from record + self.metsr_client.release_cosim_road(key) + print(f"Releasing: {key}") def destroy_carla_obj(self,obj) -> None: @@ -1051,6 +1082,10 @@ def map_scenic_to_metsr(self,lane: Lane) -> str: metsr_key = metsr_key.split("_")[0] + # if metsr_key[0] == ":": + # print(f"Original key: {metsr_key}, resultant key: {metsr_key[1:]}") + # metsr_key = metsr_key[1:] + # There must be a valid mapping assert metsr_key is not None, f"Error identifying associated ID for {query_key}" return metsr_key @@ -1288,3 +1323,10 @@ def _check_traffic_light_consistency(self): else: assert True, f"Failed to find corresponding intersection for METSR light: {light_id}" + def _save_metsr_state(self, file_name=None) -> None: + """ + docstring for _save_metsr_state + + Saves metsr state to a file to allow for reproducible replay + """ + pass \ No newline at end of file From 26cc8df573484d1a74f113f9ee0821b8d1c618c1 Mon Sep 17 00:00:00 2001 From: Kay Date: Tue, 17 Mar 2026 17:45:21 -0700 Subject: [PATCH 59/73] merged in modular scenario fixes --- src/scenic/core/dynamics/scenarios.py | 1056 +++++++++---------- src/scenic/syntax/translator.py | 1389 +++++++++++++------------ 2 files changed, 1225 insertions(+), 1220 deletions(-) diff --git a/src/scenic/core/dynamics/scenarios.py b/src/scenic/core/dynamics/scenarios.py index 7765eb9ee..ee477fa6c 100644 --- a/src/scenic/core/dynamics/scenarios.py +++ b/src/scenic/core/dynamics/scenarios.py @@ -1,527 +1,529 @@ -"""Dynamic scenarios.""" - -import ast -from collections import defaultdict -import dataclasses -import functools -import inspect -import sys -import warnings -import weakref - -import rv_ltl - -import scenic -import scenic.core.dynamics as dynamics -from scenic.core.errors import InvalidScenarioError, ScenicSyntaxError -from scenic.core.lazy_eval import DelayedArgument, needsLazyEvaluation -from scenic.core.requirements import ( - DynamicRequirement, - PendingRequirement, - RequirementType, -) -from scenic.core.utils import alarm, argsToString -from scenic.core.workspaces import Workspace - -from .actions import _EndScenarioAction, _EndSimulationAction -from .behaviors import Behavior, Monitor -from .invocables import Invocable -from .utils import RejectSimulationException, StuckBehaviorWarning - - -class DynamicScenario(Invocable): - """Internal class for scenarios which can execute during dynamic simulations. - - Provides additional information complementing `Scenario`, which originally only - supported static scenarios. The two classes should probably eventually be merged. - """ - - def __init_subclass__(cls, *args, **kwargs): - import scenic.syntax.veneer as veneer - - veneer.registerDynamicScenarioClass(cls) - - target = cls._setup or cls._compose or (lambda self, agent: 0) - target = functools.partial(target, 0, 0) # account for Scenic-inserted args - cls.__signature__ = inspect.signature(target) - - _requirementSyntax = None # overridden by subclasses - _simulatorFactory = None - _globalParameters = None - _locals = () - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._ego = None - self._workspace = None - self._instances = [] # ordered for reproducibility - # _objects should contain a reference to the most complete version of - # the objects in this scene (sampled > unsampled) - self._objects = [] # ordered for reproducibility - self._sampledObjects = self._objects - self._externalParameters = [] - self._pendingRequirements = defaultdict(list) - self._requirements = [] - # things needing to be sampled to evaluate the requirements - self._requirementDeps = set() - - self._agents = [] - self._monitors = [] - self._behaviors = [] - self._monitorRequirements = [] - self._temporalRequirements = [] - self._terminationConditions = [] - self._terminateSimulationConditions = [] - self._recordedExprs = [] - self._recordedInitialExprs = [] - self._recordedFinalExprs = [] - - self._subScenarios = [] - self._endWithBehaviors = False - self._timeLimit = None - self._timeLimitIsInSeconds = False - self._prepared = False - self._delayingPreconditionCheck = False - self._dummyNamespace = None - - self._timeLimitInSteps = None # computed at simulation time - self._elapsedTime = 0 - self._eventuallySatisfied = None - self._overrides = {} - - self._requirementMonitors = None - - @classmethod - def _dummy(cls, namespace): - scenario = cls() - scenario._setup = None - scenario._compose = None - scenario._prepared = True - scenario._dummyNamespace = namespace - return scenario - - @classmethod - def _requiresArguments(cls): - """Whether this scenario cannot be instantiated without arguments.""" - if cls._setup: - func = cls._setup - elif cls._compose: - func = cls._compose - else: - return True - sig = inspect.signature(func) - try: - sig.bind(None, None) # first two arguments are added internally by Scenic - return False - except TypeError: - return True - - @property - def ego(self): - if self._ego is None: - return DelayedArgument((), lambda context: self._ego, _internal=True) - return self._ego - - @property - def objects(self): - return tuple(self._objects) - - def _bindTo(self, scene): - """Bind this scenario to a sampled scene when starting a new simulation.""" - self._ego = scene.egoObject - self._workspace = scene.workspace - self._objects = list(scene.objects) - self._agents = [obj for obj in scene.objects if obj.behavior is not None] - self._monitors = list(scene.monitors) - self._temporalRequirements = scene.temporalRequirements - self._terminationConditions = scene.terminationConditions - self._terminateSimulationConditions = scene.terminateSimulationConditions - self._recordedExprs = scene.recordedExprs - self._recordedInitialExprs = scene.recordedInitialExprs - self._recordedFinalExprs = scene.recordedFinalExprs - - def _prepare(self, delayPreconditionCheck=False): - """Prepare the scenario for execution, executing its setup block.""" - import scenic.syntax.veneer as veneer - - assert not self._prepared - self._prepared = True - - self._finalizeArguments() # TODO generalize _prepare for Invocable? - - veneer.prepareScenario(self) - with veneer.executeInScenario(self, inheritEgo=True): - # Check preconditions and invariants - if delayPreconditionCheck: - self._delayingPreconditionCheck = True - else: - self._checkAllPreconditions() - - # Execute setup block - if self._setup is not None: - assert not any(needsLazyEvaluation(arg) for arg in self._args) - assert not any(needsLazyEvaluation(arg) for arg in self._kwargs.values()) - self._setup(None, *self._args, **self._kwargs) - veneer.finishScenarioSetup(self) - - # Extract requirements, scan for relations used for pruning, and create closures - self._compileRequirements() - - @classmethod - def _bindGlobals(cls, globs): - cls._globalParameters = globs - - def _start(self): - """Start the scenario, starting its compose block, behaviors, and monitors.""" - import scenic.syntax.veneer as veneer - - super()._start() - assert self._prepared - - # Check preconditions if they could not be checked earlier - if self._delayingPreconditionCheck: - self._checkAllPreconditions() - - # Compute time limit now that we know the simulation timestep - self._elapsedTime = 0 - self._timeLimitInSteps = self._timeLimit - if self._timeLimitIsInSeconds: - self._timeLimitInSteps /= veneer.currentSimulation.timestep - - # create monitors for each requirement used for this simulation - self._requirementMonitors = [r.toMonitor() for r in self._temporalRequirements] - - veneer.startScenario(self) - with veneer.executeInScenario(self): - # Start compose block - if self._compose is not None: - if not inspect.isgeneratorfunction(self._compose): - from scenic.syntax.translator import composeBlock - - raise InvalidScenarioError( - f'"{composeBlock}" does not invoke any scenarios' - ) - self._runningIterator = self._compose(None, *self._args, **self._kwargs) - - # Initialize behavior coroutines of agents - for agent in self._agents: - behavior = agent.behavior - assert isinstance(behavior, Behavior), behavior - behavior._assignTo(agent) - # Initialize monitor coroutines - for monitor in self._monitors: - monitor._start() - - def _step(self): - """Execute the (already-started) scenario for one time step. - - Returns: - `None` if the scenario will continue executing; otherwise a string describing - why it has terminated. - """ - import scenic.syntax.veneer as veneer - - super()._step() - - # Check temporal requirements - for m in self._requirementMonitors: - result = m.value() - if result == rv_ltl.B4.FALSE: - raise RejectSimulationException(str(m)) - - # Check if we have reached the time limit, if any - if ( - self._timeLimitInSteps is not None - and self._elapsedTime >= self._timeLimitInSteps - ): - return self._stop("reached time limit") - self._elapsedTime += 1 - - # Execute compose block, if any - composeDone = False - if self._runningIterator is None: - composeDone = True # compose block ended in an earlier step - else: - - def alarmHandler(signum, frame): - if sys.gettrace(): - return # skip the warning if we're in the debugger - warnings.warn( - f"the compose block of scenario {self} is taking a long time; " - 'maybe you have an infinite loop with no "wait" statement?', - StuckBehaviorWarning, - ) - - timeout = dynamics.stuckBehaviorWarningTimeout - with veneer.executeInScenario(self), alarm(timeout, alarmHandler): - try: - result = self._runningIterator.send(None) - if isinstance(result, (_EndSimulationAction, _EndScenarioAction)): - return self._stop(result) - except StopIteration: - self._runningIterator = None - composeDone = True - - # If there is a compose block and it has finished, we're done - if self._compose is not None and composeDone: - return self._stop("finished compose block") - - # Optionally end when all our agents' behaviors have ended - if self._endWithBehaviors: - if all(agent.behavior._isFinished for agent in self._agents): - return self._stop("all behaviors finished") - - # Check if any termination conditions apply - for req in self._terminationConditions: - if req.evaluate(): - return self._stop(req) - - # Scenario will not terminate yet - return None - - def _stop(self, reason, quiet=False): - """Stop the scenario's execution, for the given reason.""" - import scenic.syntax.veneer as veneer - - assert self._isRunning - - # Stop monitors and subscenarios. - for monitor in self._monitors: - if monitor._isRunning: - monitor._stop() - self._monitors = [] - for sub in self._subScenarios: - if sub._isRunning: - sub._stop("parent scenario ending", quiet=quiet) - self._runningIterator = None - - # Revert overrides. - for obj, oldVals in self._overrides.items(): - obj._revert(oldVals) - - # Inform the veneer we have stopped, and mark ourselves finished. - veneer.endScenario(self, reason, quiet=quiet) - super()._stop(reason) - - # Reject if a temporal requirement was not satisfied. - if not quiet: - for req in self._requirementMonitors: - if req.lastValue.is_falsy: - raise RejectSimulationException(str(req)) - self._requirementMonitors = None - - return reason - - def _invokeInner(self, agent, subs): - for sub in subs: - if not isinstance(sub, DynamicScenario): - raise TypeError(f"expected a scenario, got {sub}") - sub._prepare() - sub._start() - self._subScenarios = list(subs) - while True: - newSubs = [] - for sub in self._subScenarios: - terminationReason = sub._step() - if isinstance(terminationReason, _EndSimulationAction): - yield terminationReason - assert False, self # should never get here since simulation ends - elif terminationReason is None: - newSubs.append(sub) - self._subScenarios = newSubs - if not newSubs: - return - yield None - # Check if any sub-scenarios stopped during action execution - self._subScenarios = [sub for sub in self._subScenarios if sub._isRunning] - - def _evaluateRecordedExprs(self, ty): - if ty is RequirementType.record: - place = "_recordedExprs" - elif ty is RequirementType.recordInitial: - place = "_recordedInitialExprs" - elif ty is RequirementType.recordFinal: - place = "_recordedFinalExprs" - else: - assert False, "invalid record type requested" - return self._evaluateRecordedExprsAt(place) - - def _evaluateRecordedExprsAt(self, place): - values = {} - for rec in getattr(self, place): - values[rec.name] = rec.evaluate() - for sub in self._subScenarios: - subvals = sub._evaluateRecordedExprsAt(place) - values.update(subvals) - return values - - def _runMonitors(self): - terminationReason = None - endScenario = None - for monitor in self._monitors: - action = monitor._step() - # do not exit early, since subsequent monitors could reject the simulation - if isinstance(action, _EndSimulationAction): - terminationReason = action - elif isinstance(action, _EndScenarioAction): - assert action.scenario is None - endScenario = action - for sub in self._subScenarios: - subreason = sub._runMonitors() - if subreason is not None: - terminationReason = subreason - if endScenario: - self._stop(endScenario) - return terminationReason or endScenario - - def _checkSimulationTerminationConditions(self): - for req in self._terminateSimulationConditions: - if req.isTrue().is_truthy: - return req - return None - - @property - def _allAgents(self): - agents = list(self._agents) - for sub in self._subScenarios: - agents.extend(sub._allAgents) - return agents - - def _inherit(self, other): - if not self._workspace: - self._workspace = other._workspace - self._instances.extend(other._instances) - self._objects.extend(other._objects) - self._agents.extend(other._agents) - self._globalParameters.update(other._globalParameters) - self._externalParameters.extend(other._externalParameters) - self._requirements.extend(other._requirements) - self._behaviors.extend(other._behaviors) - - def _registerInstance(self, inst): - self._instances.append(inst) - - def _registerObject(self, obj): - self._registerInstance(obj) - self._objects.append(obj) - if getattr(obj, "behavior", None) is not None: - self._agents.append(obj) - - obj._parentScenario = weakref.ref(self) - - def _addRequirement(self, ty, reqID, req, line, name, prob): - """Save a requirement defined at compile-time for later processing.""" - assert reqID not in self._pendingRequirements - preq = PendingRequirement(ty, req, line, prob, name, self._ego) - self._pendingRequirements[reqID] = preq - - def _addDynamicRequirement(self, ty, req, line, name): - """Add a requirement defined during a dynamic simulation.""" - dreq = DynamicRequirement(ty, req, line, name) - self._temporalRequirements.append(dreq) - - def _addMonitor(self, monitor): - """Add a monitor during a dynamic simulation.""" - assert isinstance(monitor, Monitor) - self._monitors.append(monitor) - if self._isRunning: - monitor._start() - - def _compileRequirements(self): - namespace = self._dummyNamespace if self._dummyNamespace else self.__dict__ - requirementSyntax = self._requirementSyntax - assert requirementSyntax is not None - for reqID, requirement in self._pendingRequirements.items(): - syntax = requirementSyntax[reqID] if requirementSyntax else None - - # Catch the simple case where someone has most likely forgotten the "monitor" - # keyword. - if ( - (not requirement.ty == RequirementType.monitor) - and isinstance(syntax, ast.Call) - and isinstance(syntax.func, ast.Name) - and syntax.func.id in namespace - and isinstance(namespace[syntax.func.id], type) - and issubclass( - namespace[syntax.func.id], scenic.core.dynamics.behaviors.Monitor - ) - ): - raise ScenicSyntaxError( - f"Missing 'monitor' keyword after 'require' when instantiating '{syntax.func.id}'" - ) - - compiledReq = requirement.compile(namespace, self, syntax) - - self._registerCompiledRequirement(compiledReq) - self._requirementDeps.update(compiledReq.dependencies) - - def _registerCompiledRequirement(self, req): - if req.ty is RequirementType.require: - place = self._requirements - elif req.ty is RequirementType.monitor: - place = self._monitorRequirements - elif req.ty is RequirementType.terminateWhen: - place = self._terminationConditions - elif req.ty is RequirementType.terminateSimulationWhen: - place = self._terminateSimulationConditions - elif req.ty is RequirementType.record: - place = self._recordedExprs - elif req.ty is RequirementType.recordInitial: - place = self._recordedInitialExprs - elif req.ty is RequirementType.recordFinal: - place = self._recordedFinalExprs - else: - raise RuntimeError(f"internal error: requirement {req} has unknown type!") - place.append(req) - - def _setTimeLimit(self, timeLimit, inSeconds=True): - self._timeLimit = timeLimit - self._timeLimitIsInSeconds = inSeconds - - def _override(self, obj, specifiers): - oldVals = obj._override(specifiers) - if obj not in self._overrides: - self._overrides[obj] = oldVals - - def _toScenario(self, namespace): - assert self._prepared - - if not self._workspace: - self._workspace = Workspace() # default empty workspace - astHash = namespace["_astHash"] - name = None if self._dummyNamespace else self.__class__.__name__ - options = dataclasses.replace(namespace["_compileOptions"], scenario=name) - - from scenic.core.scenarios import Scenario - - scenario = Scenario( - self._workspace, - self._simulatorFactory, - self._instances, - self._objects, - self._ego, - self._globalParameters, - self._externalParameters, - self._requirements, - self._requirementDeps, - self._monitorRequirements, - self._behaviorNamespaces, - self, - astHash, - options, - ) # TODO unify these! - return scenario - - def __getattr__(self, name): - if name in self._locals: - return DelayedArgument( - (), lambda context: getattr(self, name), _internal=True - ) - return object.__getattribute__(self, name) - - def __str__(self): - if self._dummyNamespace: - return "top-level scenario" - else: - args = argsToString(self._args, self._kwargs) - return f"{self.__class__.__name__}({args})" +"""Dynamic scenarios.""" + +import ast +from collections import defaultdict +import dataclasses +import functools +import inspect +import sys +import warnings +import weakref + +import rv_ltl + +import scenic +import scenic.core.dynamics as dynamics +from scenic.core.errors import InvalidScenarioError, ScenicSyntaxError +from scenic.core.lazy_eval import DelayedArgument, needsLazyEvaluation +from scenic.core.requirements import ( + DynamicRequirement, + PendingRequirement, + RequirementType, +) +from scenic.core.utils import alarm, argsToString +from scenic.core.workspaces import Workspace + +from .actions import _EndScenarioAction, _EndSimulationAction +from .behaviors import Behavior, Monitor +from .invocables import Invocable +from .utils import RejectSimulationException, StuckBehaviorWarning + + +class DynamicScenario(Invocable): + """Internal class for scenarios which can execute during dynamic simulations. + + Provides additional information complementing `Scenario`, which originally only + supported static scenarios. The two classes should probably eventually be merged. + """ + + def __init_subclass__(cls, *args, **kwargs): + import scenic.syntax.veneer as veneer + + veneer.registerDynamicScenarioClass(cls) + + target = cls._setup or cls._compose or (lambda self, agent: 0) + target = functools.partial(target, 0, 0) # account for Scenic-inserted args + cls.__signature__ = inspect.signature(target) + + _requirementSyntax = None # overridden by subclasses + _simulatorFactory = None + _globalParameters = None + _locals = () + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._ego = None + self._workspace = None + self._instances = [] # ordered for reproducibility + # _objects should contain a reference to the most complete version of + # the objects in this scene (sampled > unsampled) + self._objects = [] # ordered for reproducibility + self._sampledObjects = self._objects + self._externalParameters = [] + self._pendingRequirements = defaultdict(list) + self._requirements = [] + # things needing to be sampled to evaluate the requirements + self._requirementDeps = set() + + self._agents = [] + self._monitors = [] + self._behaviors = [] + self._monitorRequirements = [] + self._temporalRequirements = [] + self._terminationConditions = [] + self._terminateSimulationConditions = [] + self._recordedExprs = [] + self._recordedInitialExprs = [] + self._recordedFinalExprs = [] + + self._subScenarios = [] + self._endWithBehaviors = False + self._timeLimit = None + self._timeLimitIsInSeconds = False + self._prepared = False + self._delayingPreconditionCheck = False + self._dummyNamespace = None + + self._timeLimitInSteps = None # computed at simulation time + self._elapsedTime = 0 + self._eventuallySatisfied = None + self._overrides = {} + + self._requirementMonitors = None + + @classmethod + def _dummy(cls, namespace): + scenario = cls() + scenario._setup = None + scenario._compose = None + scenario._prepared = True + scenario._dummyNamespace = namespace + return scenario + + @classmethod + def _requiresArguments(cls): + """Whether this scenario cannot be instantiated without arguments.""" + if cls._setup: + func = cls._setup + elif cls._compose: + func = cls._compose + else: + return True + sig = inspect.signature(func) + try: + sig.bind(None, None) # first two arguments are added internally by Scenic + return False + except TypeError: + return True + + @property + def ego(self): + if self._ego is None: + return DelayedArgument((), lambda context: self._ego, _internal=True) + return self._ego + + @property + def objects(self): + return tuple(self._objects) + + def _bindTo(self, scene): + """Bind this scenario to a sampled scene when starting a new simulation.""" + self._ego = scene.egoObject + self._workspace = scene.workspace + self._objects = list(scene.objects) + self._agents = [obj for obj in scene.objects if obj.behavior is not None] + self._monitors = list(scene.monitors) + self._temporalRequirements = scene.temporalRequirements + self._terminationConditions = scene.terminationConditions + self._terminateSimulationConditions = scene.terminateSimulationConditions + self._recordedExprs = scene.recordedExprs + self._recordedInitialExprs = scene.recordedInitialExprs + self._recordedFinalExprs = scene.recordedFinalExprs + + def _prepare(self, delayPreconditionCheck=False): + """Prepare the scenario for execution, executing its setup block.""" + import scenic.syntax.veneer as veneer + + assert not self._prepared + self._prepared = True + + self._finalizeArguments() # TODO generalize _prepare for Invocable? + + veneer.prepareScenario(self) + with veneer.executeInScenario(self, inheritEgo=True): + # Check preconditions and invariants + if delayPreconditionCheck: + self._delayingPreconditionCheck = True + else: + self._checkAllPreconditions() + + # Execute setup block + if self._setup is not None: + assert not any(needsLazyEvaluation(arg) for arg in self._args) + assert not any(needsLazyEvaluation(arg) for arg in self._kwargs.values()) + self._setup(None, *self._args, **self._kwargs) + veneer.finishScenarioSetup(self) + + # Extract requirements, scan for relations used for pruning, and create closures + self._compileRequirements() + + @classmethod + def _bindGlobals(cls, globs): + cls._globalParameters = globs + + def _start(self): + """Start the scenario, starting its compose block, behaviors, and monitors.""" + import scenic.syntax.veneer as veneer + + super()._start() + assert self._prepared + + # Check preconditions if they could not be checked earlier + if self._delayingPreconditionCheck: + self._checkAllPreconditions() + + # Compute time limit now that we know the simulation timestep + self._elapsedTime = 0 + self._timeLimitInSteps = self._timeLimit + if self._timeLimitIsInSeconds: + self._timeLimitInSteps /= veneer.currentSimulation.timestep + + # create monitors for each requirement used for this simulation + self._requirementMonitors = [r.toMonitor() for r in self._temporalRequirements] + + veneer.startScenario(self) + with veneer.executeInScenario(self): + # Start compose block + if self._compose is not None: + if not inspect.isgeneratorfunction(self._compose): + from scenic.syntax.translator import composeBlock + + raise InvalidScenarioError( + f'"{composeBlock}" does not invoke any scenarios' + ) + self._runningIterator = self._compose(None, *self._args, **self._kwargs) + + # Initialize behavior coroutines of agents + for agent in self._agents: + behavior = agent.behavior + assert isinstance(behavior, Behavior), behavior + behavior._assignTo(agent) + # Initialize monitor coroutines + for monitor in self._monitors: + monitor._start() + + def _step(self): + """Execute the (already-started) scenario for one time step. + + Returns: + `None` if the scenario will continue executing; otherwise a string describing + why it has terminated. + """ + import scenic.syntax.veneer as veneer + + super()._step() + + # Check temporal requirements + for m in self._requirementMonitors: + result = m.value() + if result == rv_ltl.B4.FALSE: + raise RejectSimulationException(str(m)) + + # Check if we have reached the time limit, if any + if ( + self._timeLimitInSteps is not None + and self._elapsedTime >= self._timeLimitInSteps + ): + return self._stop("reached time limit") + self._elapsedTime += 1 + + # Execute compose block, if any + composeDone = False + if self._runningIterator is None: + composeDone = True # compose block ended in an earlier step + else: + + def alarmHandler(signum, frame): + if sys.gettrace(): + return # skip the warning if we're in the debugger + warnings.warn( + f"the compose block of scenario {self} is taking a long time; " + 'maybe you have an infinite loop with no "wait" statement?', + StuckBehaviorWarning, + ) + + timeout = dynamics.stuckBehaviorWarningTimeout + with veneer.executeInScenario(self), alarm(timeout, alarmHandler): + try: + result = self._runningIterator.send(None) + if isinstance(result, (_EndSimulationAction, _EndScenarioAction)): + return self._stop(result) + except StopIteration: + self._runningIterator = None + composeDone = True + + # If there is a compose block and it has finished, we're done + if self._compose is not None and composeDone: + return self._stop("finished compose block") + + # Optionally end when all our agents' behaviors have ended + if self._endWithBehaviors: + if all(agent.behavior._isFinished for agent in self._agents): + return self._stop("all behaviors finished") + + # Check if any termination conditions apply + for req in self._terminationConditions: + if req.evaluate(): + return self._stop(req) + + # Scenario will not terminate yet + return None + + def _stop(self, reason, quiet=False): + """Stop the scenario's execution, for the given reason.""" + import scenic.syntax.veneer as veneer + + assert self._isRunning + + # Stop monitors and subscenarios. + for monitor in self._monitors: + if monitor._isRunning: + monitor._stop() + self._monitors = [] + for sub in self._subScenarios: + if sub._isRunning: + sub._stop("parent scenario ending", quiet=quiet) + self._runningIterator = None + + # Revert overrides. + for obj, oldVals in self._overrides.items(): + obj._revert(oldVals) + + # Inform the veneer we have stopped, and mark ourselves finished. + veneer.endScenario(self, reason, quiet=quiet) + super()._stop(reason) + + # Reject if a temporal requirement was not satisfied. + if not quiet: + for req in self._requirementMonitors: + if req.lastValue.is_falsy: + raise RejectSimulationException(str(req)) + self._requirementMonitors = None + + return reason + + def _invokeInner(self, agent, subs): + for sub in subs: + if not isinstance(sub, DynamicScenario): + raise TypeError(f"expected a scenario, got {sub}") + sub._prepare() + sub._start() + self._subScenarios = list(subs) + while True: + newSubs = [] + for sub in self._subScenarios: + terminationReason = sub._step() + if isinstance(terminationReason, _EndSimulationAction): + yield terminationReason + assert False, self # should never get here since simulation ends + elif terminationReason is None: + newSubs.append(sub) + self._subScenarios = newSubs + if not newSubs: + return + yield None + # Check if any sub-scenarios stopped during action execution + self._subScenarios = [sub for sub in self._subScenarios if sub._isRunning] + + def _evaluateRecordedExprs(self, ty): + if ty is RequirementType.record: + place = "_recordedExprs" + elif ty is RequirementType.recordInitial: + place = "_recordedInitialExprs" + elif ty is RequirementType.recordFinal: + place = "_recordedFinalExprs" + else: + assert False, "invalid record type requested" + return self._evaluateRecordedExprsAt(place) + + def _evaluateRecordedExprsAt(self, place): + values = {} + for rec in getattr(self, place): + values[rec.name] = rec.evaluate() + for sub in self._subScenarios: + subvals = sub._evaluateRecordedExprsAt(place) + values.update(subvals) + return values + + def _runMonitors(self): + terminationReason = None + endScenario = None + for monitor in self._monitors: + action = monitor._step() + # do not exit early, since subsequent monitors could reject the simulation + if isinstance(action, _EndSimulationAction): + terminationReason = action + elif isinstance(action, _EndScenarioAction): + assert action.scenario is None + endScenario = action + for sub in self._subScenarios: + subreason = sub._runMonitors() + if subreason is not None: + terminationReason = subreason + if endScenario: + self._stop(endScenario) + return terminationReason or endScenario + + def _checkSimulationTerminationConditions(self): + for req in self._terminateSimulationConditions: + if req.isTrue().is_truthy: + return req + return None + + @property + def _allAgents(self): + agents = list(self._agents) + for sub in self._subScenarios: + agents.extend(sub._allAgents) + return agents + + def _inherit(self, other): + if not self._ego: + self._ego = other._ego + if not self._workspace: + self._workspace = other._workspace + self._instances.extend(other._instances) + self._objects.extend(other._objects) + self._agents.extend(other._agents) + self._globalParameters.update(other._globalParameters) + self._externalParameters.extend(other._externalParameters) + self._requirements.extend(other._requirements) + self._behaviors.extend(other._behaviors) + + def _registerInstance(self, inst): + self._instances.append(inst) + + def _registerObject(self, obj): + self._registerInstance(obj) + self._objects.append(obj) + if getattr(obj, "behavior", None) is not None: + self._agents.append(obj) + + obj._parentScenario = weakref.ref(self) + + def _addRequirement(self, ty, reqID, req, line, name, prob): + """Save a requirement defined at compile-time for later processing.""" + assert reqID not in self._pendingRequirements + preq = PendingRequirement(ty, req, line, prob, name, self._ego) + self._pendingRequirements[reqID] = preq + + def _addDynamicRequirement(self, ty, req, line, name): + """Add a requirement defined during a dynamic simulation.""" + dreq = DynamicRequirement(ty, req, line, name) + self._temporalRequirements.append(dreq) + + def _addMonitor(self, monitor): + """Add a monitor during a dynamic simulation.""" + assert isinstance(monitor, Monitor) + self._monitors.append(monitor) + if self._isRunning: + monitor._start() + + def _compileRequirements(self): + namespace = self._dummyNamespace if self._dummyNamespace else self.__dict__ + requirementSyntax = self._requirementSyntax + assert requirementSyntax is not None + for reqID, requirement in self._pendingRequirements.items(): + syntax = requirementSyntax[reqID] if requirementSyntax else None + + # Catch the simple case where someone has most likely forgotten the "monitor" + # keyword. + if ( + (not requirement.ty == RequirementType.monitor) + and isinstance(syntax, ast.Call) + and isinstance(syntax.func, ast.Name) + and syntax.func.id in namespace + and isinstance(namespace[syntax.func.id], type) + and issubclass( + namespace[syntax.func.id], scenic.core.dynamics.behaviors.Monitor + ) + ): + raise ScenicSyntaxError( + f"Missing 'monitor' keyword after 'require' when instantiating '{syntax.func.id}'" + ) + + compiledReq = requirement.compile(namespace, self, syntax) + + self._registerCompiledRequirement(compiledReq) + self._requirementDeps.update(compiledReq.dependencies) + + def _registerCompiledRequirement(self, req): + if req.ty is RequirementType.require: + place = self._requirements + elif req.ty is RequirementType.monitor: + place = self._monitorRequirements + elif req.ty is RequirementType.terminateWhen: + place = self._terminationConditions + elif req.ty is RequirementType.terminateSimulationWhen: + place = self._terminateSimulationConditions + elif req.ty is RequirementType.record: + place = self._recordedExprs + elif req.ty is RequirementType.recordInitial: + place = self._recordedInitialExprs + elif req.ty is RequirementType.recordFinal: + place = self._recordedFinalExprs + else: + raise RuntimeError(f"internal error: requirement {req} has unknown type!") + place.append(req) + + def _setTimeLimit(self, timeLimit, inSeconds=True): + self._timeLimit = timeLimit + self._timeLimitIsInSeconds = inSeconds + + def _override(self, obj, specifiers): + oldVals = obj._override(specifiers) + if obj not in self._overrides: + self._overrides[obj] = oldVals + + def _toScenario(self, namespace): + assert self._prepared + + if not self._workspace: + self._workspace = Workspace() # default empty workspace + astHash = namespace["_astHash"] + name = None if self._dummyNamespace else self.__class__.__name__ + options = dataclasses.replace(namespace["_compileOptions"], scenario=name) + + from scenic.core.scenarios import Scenario + + scenario = Scenario( + self._workspace, + self._simulatorFactory, + self._instances, + self._objects, + self._ego, + self._globalParameters, + self._externalParameters, + self._requirements, + self._requirementDeps, + self._monitorRequirements, + self._behaviorNamespaces, + self, + astHash, + options, + ) # TODO unify these! + return scenario + + def __getattr__(self, name): + if name in self._locals: + return DelayedArgument( + (), lambda context: getattr(self, name), _internal=True + ) + return object.__getattribute__(self, name) + + def __str__(self): + if self._dummyNamespace: + return "top-level scenario" + else: + args = argsToString(self._args, self._kwargs) + return f"{self.__class__.__name__}({args})" diff --git a/src/scenic/syntax/translator.py b/src/scenic/syntax/translator.py index 994e65b8b..e81099185 100644 --- a/src/scenic/syntax/translator.py +++ b/src/scenic/syntax/translator.py @@ -1,693 +1,696 @@ -"""Translator turning Scenic programs into Scenario objects. - -The top-level interface to Scenic is provided by two functions: - -* `scenarioFromString` -- compile a string of Scenic code; -* `scenarioFromFile` -- compile a Scenic file. - -These output a `Scenario` object, from which scenes can be generated. -See the documentation for `Scenario` for details. - -When imported, this module hooks the Python import system in order to implement -the :keyword:`import` statement. This is only for the compiler's own use: it is -not allowed to import a Scenic module from Python, and attempting to do so will -fail with a `ModuleNotFoundError`. - -Scenic is compiled in two main steps: translating the code into Python, and -executing the resulting Python module to generate a Scenario object encoding -the objects, distributions, etc. in the scenario. For details, see the function -`compileStream` below. -""" - -import ast -import builtins -from contextlib import contextmanager -import dataclasses -import hashlib -import importlib -import importlib.abc -import importlib.util -import inspect -import io -import os -import sys -import time -import types -from typing import Optional - -from scenic.core.distributions import RejectionException, toDistribution -from scenic.core.dynamics.scenarios import DynamicScenario -import scenic.core.errors as errors -from scenic.core.errors import InvalidScenarioError, PythonCompileError -from scenic.core.lazy_eval import needsLazyEvaluation -import scenic.core.pruning as pruning -from scenic.core.utils import cached_property -from scenic.syntax.compiler import compileScenicAST -from scenic.syntax.parser import parse_string -import scenic.syntax.veneer as veneer - -### THE TOP LEVEL: compiling a Scenic program - - -@dataclasses.dataclass -class CompileOptions: - """Internal class for capturing options used when compiling a scenario.""" - - # N.B. update `hash` below when adding a new field - - #: Whether or not the scenario uses `2D compatibility mode`. - mode2D: bool = False - #: Overridden world model, if any. - modelOverride: Optional[str] = None - #: Overridden global parameters. - paramOverrides: dict = dataclasses.field(default_factory=dict) - #: Selected modular scenario, if any. - scenario: Optional[str] = None - - @cached_property - def hash(self): - """Deterministic hash saved in serialized scenes to catch option mismatches.""" - stream = io.BytesIO() - stream.write(bytes([self.mode2D])) - if self.modelOverride: - stream.write(self.modelOverride.encode()) - for key in sorted(self.paramOverrides.keys()): - stream.write(key.encode()) - value = self.paramOverrides[key] - if isinstance(value, (int, float, str)): - stream.write(str(value).encode()) - else: - stream.write([0]) - if self.scenario: - stream.write(self.scenario.encode()) - # We can't use `hash` because it is not deterministic - # (e.g. the hashes of strings are randomized) - return hashlib.blake2b(stream.getvalue(), digest_size=4).digest() - - -def scenarioFromString( - string, - params={}, - model=None, - scenario=None, - *, - filename="", - mode2D=False, - **kwargs, -): - """Compile a string of Scenic code into a `Scenario`. - - The optional **filename** is used for error messages. - Other arguments are as in `scenarioFromFile`. - """ - stream = io.BytesIO(string.encode()) - options = CompileOptions(modelOverride=model, paramOverrides=params, mode2D=mode2D) - return _scenarioFromStream(stream, options, filename, scenario=scenario, **kwargs) - - -def scenarioFromFile( - path, params={}, model=None, scenario=None, *, mode2D=False, **kwargs -): - """Compile a Scenic file into a `Scenario`. - - Args: - path (str): Path to a Scenic file. - params (dict): :term:`Global parameters` to override, as a dictionary mapping - parameter names to their desired values. - model (str): Scenic module to use as :term:`world model`. - scenario (str): If there are multiple :term:`modular scenarios` in the - file, which one to compile; if not specified, a scenario called 'Main' - is used if it exists. - mode2D (bool): Whether to compile this scenario in `2D compatibility mode`. - - Returns: - A `Scenario` object representing the Scenic scenario. - - Note for Scenic developers: this function accepts additional keyword - arguments which are intended for internal use and debugging only. - See `_scenarioFromStream` for details. - """ - if not os.path.exists(path): - raise FileNotFoundError(path) - fullpath = os.path.realpath(path) - head, extension = os.path.splitext(fullpath) - if not extension or extension not in scenicExtensions: - ok = ", ".join(scenicExtensions) - err = f"Scenic scenario does not have valid extension ({ok})" - raise RuntimeError(err) - directory, name = os.path.split(head) - - options = CompileOptions(modelOverride=model, paramOverrides=params, mode2D=mode2D) - with open(path, "rb") as stream: - return _scenarioFromStream( - stream, options, fullpath, scenario=scenario, path=path, **kwargs - ) - - -def _scenarioFromStream( - stream, compileOptions, filename, *, scenario=None, path=None, _cacheImports=False -): - """Compile a stream of Scenic code into a `Scenario`. - - This method is not meant to be called directly by users of Scenic. Use the - top-level functions `scenarioFromFile` and `scenarioFromString` instead. - - These functions also accept the following keyword arguments, which are - intended for internal use and debugging only. They should be considered - unstable and are subject to modification or removal at any time. - - Args: - _cacheImports (bool): Whether to cache any imported Scenic modules. - The default behavior is to not do this, so that subsequent attempts - to import such modules will cause them to be recompiled. If it is - safe to cache Scenic modules across multiple compilations, set this - argument to True. Then importing a Scenic module will have the same - behavior as importing a Python module. See `purgeModulesUnsafeToCache` - for a more detailed discussion of the internals behind this. - """ - # Compile the code as if it were a top-level module - oldModules = list(sys.modules.keys()) - try: - with topLevelNamespace(path) as namespace: - compileStream(stream, namespace, compileOptions, filename) - finally: - if not _cacheImports: - purgeModulesUnsafeToCache(oldModules) - # Construct a Scenario from the resulting namespace - return constructScenarioFrom(namespace, scenario) - - -@contextmanager -def topLevelNamespace(path=None): - """Creates an environment like that of a Python script being run directly. - - Specifically, __name__ is '__main__', __file__ is the path used to invoke - the script (not necessarily its absolute path), and the parent directory is - added to the path so that 'import blobbo' will import blobbo from that - directory if it exists there. - """ - directory = os.getcwd() if path is None else os.path.dirname(path) - namespace = {"__name__": "__main__"} - if path is not None: - namespace["__file__"] = path - sys.path.insert(0, directory) - try: - yield namespace - finally: - # Remove directory from sys.path, being a little careful in case the - # Scenic program modified it (unlikely but possible). - try: - sys.path.remove(directory) - except ValueError: - pass - - -def purgeModulesUnsafeToCache(oldModules): - """Uncache loaded modules which should not be kept after compilation. - - Keeping Scenic modules in `sys.modules` after compilation will cause - subsequent attempts at compiling the same module to reuse the compiled - scenario: this is usually not what is desired, since compilation can depend - on external state (in particular overridden global parameters, used e.g. to - specify the map for driving domain scenarios). - - Args: - oldModules: List of names of modules loaded before compilation. These - will be skipped. - """ - toRemove = [] - # copy sys.modules in case it mutates during iteration (actually happens!) - for name, module in sys.modules.copy().items(): - if isinstance(module, ScenicModule) and name not in oldModules: - toRemove.append(name) - for name in toRemove: - parent, _, child = name.rpartition(".") - parent = sys.modules.get(parent) - if parent: - # Remove reference to purged module from parent module. This is necessary - # so that future imports of the purged module will properly refer to the - # newly-loaded version of it. See below for a long disquisition on this. - del parent.__dict__[child] - - # Here are details on why the above line is necessary and the sorry history - # of my attempts to fix this type of bug (hopefully this note will prevent - # further self-sabotage). Suppose we have a Python package 'package' - # with a Scenic submodule 'submodule'. A Scenic program with the line - # from package import submodule - # will import 2 packages, namely package and package.submodule, when first - # compiled. We will then purge package.submodule from sys.modules, but not - # package, since it is an ordinary module. So if the program is compiled a - # second time, the line above will NOT import package.submodule, but simply - # access the attribute 'submodule' of the existing package 'package'. So the - # reference to the old version of package.submodule will leak out. - # (An alternative approach, which I used to use, would be to purge all - # modules containing even indirect references to Scenic modules, but this - # opens a can of worms: the implementation of - # import parent.child - # does not set the 'child' attribute of 'parent' if 'parent.child' is already - # in sys.modules, violating an invariant that Python expects [see - # https://docs.python.org/3/reference/import.html#submodules] and leading to - # confusing errors. So if parent is purged because it has some child which is - # a Scenic module, *all* of its children must then be purged. Since the - # scenic module itself can contain indirect references to Scenic modules (the - # world models), this means we have to purge the entire scenic package. But - # then whoever did 'import scenic' at the top level will be left with a - # reference to the old version of the Scenic module.) - # - # 2023 update: after hitting yet another bug caused by a reference to a - # Scenic module surviving the purge, I've decided to completely ban importing - # Scenic modules from Python (except when building the documentation). Any - # objects needed in both Python and Scenic modules should be defined in a - # Python module and imported from there. - del sys.modules[name] - - -def compileStream(stream, namespace, compileOptions, filename): - """Compile a stream of Scenic code and execute it in a namespace. - - The compilation procedure consists of the following main steps: - - 1. Parse the Scenic code into a Scenic AST using the parser generated - by ``pegen`` from :file:`scenic.gram`. - 2. Compile the Scenic AST into a Python AST with the desired semantics. - This is done by the compiler, `scenic.syntax.compiler`. - 3. Compile and execute the Python AST. - 4. Extract the global state (e.g. objects). - This is done by the `storeScenarioStateIn` function. - """ - if errors.verbosityLevel >= 2: - veneer.verbosePrint(f" Compiling Scenic module from {filename}...") - startTime = time.time() - veneer.activate(compileOptions, namespace) - try: - # Execute preamble - exec(compile(preamble, "", "exec"), namespace) - namespace[namespaceReference] = namespace - - # Parse the source - source = stream.read().decode("utf-8") - scenic_tree = parse_string(source, "exec", filename=filename) - - if dumpScenicAST: - print(f"### Begin Scenic AST of {filename}") - print(dump(scenic_tree, include_attributes=False, indent=4)) - print("### End Scenic AST") - - # Compile the Scenic AST into a Python AST - tree, requirements = compileScenicAST(scenic_tree, filename=filename) - astHasher = hashlib.blake2b(digest_size=4) - astHasher.update(ast.dump(tree).encode()) - - if dumpFinalAST: - print(f"### Begin final AST of {filename}") - print(dump(tree, include_attributes=True, indent=4)) - print("### End final AST") - - pythonSource = astToSource(tree) - if dumpASTPython: - if pythonSource is None: - raise RuntimeError( - "dumping the Python equivalent of the AST" - " requires the astor package" - ) - print(f"### Begin Python equivalent of final AST of {filename}") - print(pythonSource) - print("### End Python equivalent of final AST") - - # Compile the Python AST tree - code = compileTranslatedTree(tree, filename) - - # Execute it - executeCodeIn(code, namespace) - - # Extract scenario state from veneer and store it - astHash = astHasher.digest() - storeScenarioStateIn(namespace, requirements, astHash, compileOptions) - finally: - veneer.deactivate() - if errors.verbosityLevel >= 2: - totalTime = time.time() - startTime - veneer.verbosePrint(f" Compiled Scenic module in {totalTime:.4g} seconds.") - return code, pythonSource - - -def dump( - node: ast.AST, - annotate_fields: bool = True, - include_attributes: bool = False, - *, - indent: int, -): - if sys.version_info >= (3, 9): - print(ast.dump(node, annotate_fields, include_attributes, indent=indent)) - else: - # omit `indent` if not supported - print(ast.dump(node, annotate_fields, include_attributes)) - - -def astToSource(tree: ast.AST): - if sys.version_info >= (3, 9): - return ast.unparse(tree) - try: - import astor - except ModuleNotFoundError: - return None - return astor.to_source(tree) - - -### TRANSLATION PHASE ZERO: definitions of language elements not already in Python - -## Options - -dumpScenicAST = False -dumpFinalAST = False -dumpASTPython = False -usePruning = True - -## Preamble -# (included at the beginning of every module to be translated; -# imports the implementations of the public language features) -preamble = """\ -from scenic.syntax.veneer import * -""" - -## Get Python names of various elements -## (for checking consistency between the translator and the veneer) - -api = set(veneer.__all__) - -namespaceReference = "_Scenic_module_namespace" # used in the implementation of 'model' - -### TRANSLATION PHASE ONE: handling imports - -## Loader for Scenic files, producing ScenicModules - - -class ScenicLoader(importlib.abc.InspectLoader): - def __init__(self, name, filepath): - self.filepath = filepath - - def create_module(self, spec): - return ScenicModule(spec.name) - - def exec_module(self, module): - # Read source file and compile it - with open(self.filepath, "r") as stream: - source = stream.read() - with open(self.filepath, "rb") as stream: - code, pythonSource = compileStream( - stream, module.__dict__, CompileOptions(), self.filepath - ) - # Save code, source, and translated source for later inspection - module._code = code - module._source = source - module._pythonSource = pythonSource - - # If we're in the process of compiling another Scenic module, inherit - # objects, parameters, etc. from this one - if veneer.isActive(): - veneer.currentScenario._inherit(module._scenario) - - def is_package(self, fullname): - return False - - def get_code(self, fullname): - module = importlib.import_module(fullname) - assert isinstance(module, ScenicModule), module - return module._code - - def get_source(self, fullname): - module = importlib.import_module(fullname) - assert isinstance(module, ScenicModule), module - return module._source - - -class ScenicModule(types.ModuleType): - def __getstate__(self): - state = self.__dict__.copy() - del state["__builtins__"] - return (self.__name__, state) - - def __setstate__(self, state): - name, state = state - self.__init__(name) # needed to create __dict__ - self.__dict__.update(state) - self.__builtins__ = builtins.__dict__ - - -# Give instances of ScenicModule a falsy __module__ to prevent Sphinx from -# getting confused. (Autodoc doesn't expect modules to have that attribute, -# and we can't del it.) We only do this during Sphinx runs since it seems to -# sometimes break pickling of the modules. -sphinx = sys.modules.get("sphinx") -buildingDocs = sphinx and getattr(sphinx, "_buildingScenicDocs", False) -if buildingDocs: - ScenicModule.__module__ = None - -## Finder for Scenic (and Python) files - -scenicExtensions = (".scenic", ".sc") - -import importlib.machinery as machinery - -loaders = [ - (machinery.ExtensionFileLoader, machinery.EXTENSION_SUFFIXES), - (machinery.SourceFileLoader, machinery.SOURCE_SUFFIXES), - (machinery.SourcelessFileLoader, machinery.BYTECODE_SUFFIXES), - (ScenicLoader, scenicExtensions), -] - - -class ScenicFileFinder(importlib.abc.PathEntryFinder): - def __init__(self, path): - self._inner = machinery.FileFinder(path, *loaders) - - def find_spec(self, fullname, target=None): - spec = self._inner.find_spec(fullname, target=target) - # Disallow imports of Scenic modules from Python modules, unless we are - # building the documentation (to allow autodoc to introspect them; this - # requires careful setup in `docs/conf.py`). - # See `purgeModulesUnsafeToCache` for the rationale. - if ( - spec - and spec.origin - and not (veneer.isActive() or buildingDocs) - and any(spec.origin.endswith(ext) for ext in scenicExtensions) - ): - return None - return spec - - def invalidate_caches(self): - self._inner.invalidate_caches() - - # Support pkgutil.iter_modules() (used by Sphinx autosummary's recursive mode); - # we need to use a subclass of FileFinder since pkgutil's implementation for - # vanilla FileFinder uses inspect.getmodulename, which doesn't recognize the - # .scenic file extension. - def iter_modules(self, prefix): - # This is mostly copied from pkgutil._iter_file_finder_modules - yielded = {} - try: - filenames = os.listdir(self._inner.path) - except OSError: - return - filenames.sort() - for fn in filenames: - modname = inspect.getmodulename(fn) - if not modname: - # Check for Scenic modules - base = os.path.basename(fn) - for ext in scenicExtensions: - if base.endswith(ext): - modname = base[: -len(ext)] - break - if modname == "__init__" or modname in yielded: - continue - - path = os.path.join(self._inner.path, fn) - ispkg = False - - if not modname and os.path.isdir(path) and "." not in fn: - modname = fn - try: - dircontents = os.listdir(path) - except OSError: - # ignore unreadable directories like import does - dircontents = [] - for fn in dircontents: - subname = inspect.getmodulename(fn) - if subname == "__init__": - ispkg = True - break - else: - continue # not a package - - if modname and "." not in modname: - yielded[modname] = 1 - yield prefix + modname, ispkg - - -# Install path hook using our finder -def scenic_path_hook(path): - if not path: - path = os.getcwd() - if not os.path.isdir(path): - raise ImportError("only directories are supported", path=path) - return ScenicFileFinder(path) - - -sys.path_hooks.insert(0, scenic_path_hook) -sys.path_importer_cache.clear() - -### Translation phase two to four are done by the parser & compiler - -### TRANSLATION PHASE FIVE: AST compilation - - -def compileTranslatedTree(tree, filename): - try: - return compile(tree, filename, "exec") - except SyntaxError as e: - raise PythonCompileError(e) from None - - -### TRANSLATION PHASE SIX: Python execution - - -def executeCodeIn(code, namespace): - """Execute the final translated Python code in the given namespace.""" - try: - exec(code, namespace) - except RejectionException as e: - # Determined statically that the scenario has probability zero. - errors.optionallyDebugRejection(e) - if errors.showInternalBacktrace: - raise InvalidScenarioError(e.args[0]) from e - else: - raise InvalidScenarioError(e.args[0]).with_traceback( - e.__traceback__ - ) from None - - -### TRANSLATION PHASE SEVEN: scenario construction - - -def storeScenarioStateIn(namespace, requirementSyntax, astHash, options): - """Post-process an executed Scenic module, extracting state from the veneer.""" - - # Save requirement syntax and other module-level information - namespace["_astHash"] = astHash - namespace["_compileOptions"] = options - moduleScenario = veneer.currentScenario - factory = veneer.simulatorFactory - bns = gatherBehaviorNamespacesFrom(moduleScenario._behaviors) - - def handle(scenario): - scenario._requirementSyntax = requirementSyntax - if isinstance(scenario, type): - scenario._simulatorFactory = staticmethod(factory) - else: - scenario._simulatorFactory = factory - scenario._behaviorNamespaces = bns - - handle(moduleScenario) - namespace["_scenarios"] = tuple(veneer.scenarios) - for scenarioClass in veneer.scenarios: - handle(scenarioClass) - - # Extract requirements, scan for relations used for pruning, and create closures - # (only for top-level scenario; modular scenarios will be handled when instantiated) - moduleScenario._compileRequirements() - - # Save global parameters - for name, value in veneer._globalParameters.items(): - if needsLazyEvaluation(value): - raise InvalidScenarioError( - f"parameter {name} uses value {value}" - " undefined outside of object definition" - ) - for scenario in veneer.scenarios: - scenario._bindGlobals(veneer._globalParameters) - moduleScenario._bindGlobals(veneer._globalParameters) - - namespace["_scenario"] = moduleScenario - - -def gatherBehaviorNamespacesFrom(behaviors): - """Gather any global namespaces which could be referred to by behaviors. - - We'll need to rebind any sampled values in them at runtime. - """ - behaviorNamespaces = {} - - def registerNamespace(modName, ns): - oldNS = behaviorNamespaces.get(modName) - if oldNS: - # Already registered; just do a consistency check to avoid bizarre - # bugs from having multiple versions of the same module around. - if oldNS is not ns: - raise RuntimeError( - f"scenario refers to multiple versions of module {modName}; " - "perhaps you imported it before you started compilation?" - ) - return - behaviorNamespaces[modName] = ns - for name, value in ns.items(): - if isinstance(value, ScenicModule): - registerNamespace(value.__name__, value.__dict__) - else: - # Convert values requiring sampling to Distributions - dval = toDistribution(value) - if dval is not value: - ns[name] = dval - - for behavior in behaviors: - modName = behavior.__module__ - globalNamespace = behavior.makeGenerator.__globals__ - registerNamespace(modName, globalNamespace) - return behaviorNamespaces - - -def constructScenarioFrom(namespace, scenarioName=None): - """Build a Scenario object from an executed Scenic module.""" - modularScenarios = namespace["_scenarios"] - - def isModularScenario(thing): - return isinstance(thing, type) and issubclass(thing, DynamicScenario) - - if not scenarioName and isModularScenario(namespace.get("Main", None)): - scenarioName = "Main" - if scenarioName: - ty = namespace.get(scenarioName, None) - if not isModularScenario(ty): - raise RuntimeError(f'no scenario "{scenarioName}" found') - if ty._requiresArguments(): - raise RuntimeError( - f'cannot instantiate scenario "{scenarioName}"' " with no arguments" - ) from None - - dynScenario = ty() - elif len(modularScenarios) > 1: - raise RuntimeError( - "multiple choices for scenario to run " - "(specify using the --scenario option)" - ) - elif modularScenarios and not modularScenarios[0]._requiresArguments(): - dynScenario = modularScenarios[0]() - else: - dynScenario = namespace["_scenario"] - - if not dynScenario._prepared: # true for all except top-level scenarios - # Execute setup block (if any) to create objects and requirements; - # extract any requirements and scan for relations used for pruning - dynScenario._prepare(delayPreconditionCheck=True) - scenario = dynScenario._toScenario(namespace) - - # Prune infeasible parts of the space - if usePruning: - pruning.prune(scenario, verbosity=errors.verbosityLevel) - - # Validate scenario - scenario.validate() - - return scenario +"""Translator turning Scenic programs into Scenario objects. + +The top-level interface to Scenic is provided by two functions: + +* `scenarioFromString` -- compile a string of Scenic code; +* `scenarioFromFile` -- compile a Scenic file. + +These output a `Scenario` object, from which scenes can be generated. +See the documentation for `Scenario` for details. + +When imported, this module hooks the Python import system in order to implement +the :keyword:`import` statement. This is only for the compiler's own use: it is +not allowed to import a Scenic module from Python, and attempting to do so will +fail with a `ModuleNotFoundError`. + +Scenic is compiled in two main steps: translating the code into Python, and +executing the resulting Python module to generate a Scenario object encoding +the objects, distributions, etc. in the scenario. For details, see the function +`compileStream` below. +""" + +import ast +import builtins +from contextlib import contextmanager +import dataclasses +import hashlib +import importlib +import importlib.abc +import importlib.util +import inspect +import io +import os +import sys +import time +import types +from typing import Optional + +from scenic.core.distributions import RejectionException, toDistribution +from scenic.core.dynamics.scenarios import DynamicScenario +import scenic.core.errors as errors +from scenic.core.errors import InvalidScenarioError, PythonCompileError +from scenic.core.lazy_eval import needsLazyEvaluation +import scenic.core.pruning as pruning +from scenic.core.utils import cached_property +from scenic.syntax.compiler import compileScenicAST +from scenic.syntax.parser import parse_string +import scenic.syntax.veneer as veneer + +### THE TOP LEVEL: compiling a Scenic program + + +@dataclasses.dataclass +class CompileOptions: + """Internal class for capturing options used when compiling a scenario.""" + + # N.B. update `hash` below when adding a new field + + #: Whether or not the scenario uses `2D compatibility mode`. + mode2D: bool = False + #: Overridden world model, if any. + modelOverride: Optional[str] = None + #: Overridden global parameters. + paramOverrides: dict = dataclasses.field(default_factory=dict) + #: Selected modular scenario, if any. + scenario: Optional[str] = None + + @cached_property + def hash(self): + """Deterministic hash saved in serialized scenes to catch option mismatches.""" + stream = io.BytesIO() + stream.write(bytes([self.mode2D])) + if self.modelOverride: + stream.write(self.modelOverride.encode()) + for key in sorted(self.paramOverrides.keys()): + stream.write(key.encode()) + value = self.paramOverrides[key] + if isinstance(value, (int, float, str)): + stream.write(str(value).encode()) + else: + stream.write([0]) + if self.scenario: + stream.write(self.scenario.encode()) + # We can't use `hash` because it is not deterministic + # (e.g. the hashes of strings are randomized) + return hashlib.blake2b(stream.getvalue(), digest_size=4).digest() + + +def scenarioFromString( + string, + params={}, + model=None, + scenario=None, + *, + filename="", + mode2D=False, + **kwargs, +): + """Compile a string of Scenic code into a `Scenario`. + + The optional **filename** is used for error messages. + Other arguments are as in `scenarioFromFile`. + """ + stream = io.BytesIO(string.encode()) + options = CompileOptions(modelOverride=model, paramOverrides=params, mode2D=mode2D) + return _scenarioFromStream(stream, options, filename, scenario=scenario, **kwargs) + + +def scenarioFromFile( + path, params={}, model=None, scenario=None, *, mode2D=False, **kwargs +): + """Compile a Scenic file into a `Scenario`. + + Args: + path (str): Path to a Scenic file. + params (dict): :term:`Global parameters` to override, as a dictionary mapping + parameter names to their desired values. + model (str): Scenic module to use as :term:`world model`. + scenario (str): If there are multiple :term:`modular scenarios` in the + file, which one to compile; if not specified, a scenario called 'Main' + is used if it exists. + mode2D (bool): Whether to compile this scenario in `2D compatibility mode`. + + Returns: + A `Scenario` object representing the Scenic scenario. + + Note for Scenic developers: this function accepts additional keyword + arguments which are intended for internal use and debugging only. + See `_scenarioFromStream` for details. + """ + if not os.path.exists(path): + raise FileNotFoundError(path) + fullpath = os.path.realpath(path) + head, extension = os.path.splitext(fullpath) + if not extension or extension not in scenicExtensions: + ok = ", ".join(scenicExtensions) + err = f"Scenic scenario does not have valid extension ({ok})" + raise RuntimeError(err) + directory, name = os.path.split(head) + + options = CompileOptions(modelOverride=model, paramOverrides=params, mode2D=mode2D) + with open(path, "rb") as stream: + return _scenarioFromStream( + stream, options, fullpath, scenario=scenario, path=path, **kwargs + ) + + +def _scenarioFromStream( + stream, compileOptions, filename, *, scenario=None, path=None, _cacheImports=False +): + """Compile a stream of Scenic code into a `Scenario`. + + This method is not meant to be called directly by users of Scenic. Use the + top-level functions `scenarioFromFile` and `scenarioFromString` instead. + + These functions also accept the following keyword arguments, which are + intended for internal use and debugging only. They should be considered + unstable and are subject to modification or removal at any time. + + Args: + _cacheImports (bool): Whether to cache any imported Scenic modules. + The default behavior is to not do this, so that subsequent attempts + to import such modules will cause them to be recompiled. If it is + safe to cache Scenic modules across multiple compilations, set this + argument to True. Then importing a Scenic module will have the same + behavior as importing a Python module. See `purgeModulesUnsafeToCache` + for a more detailed discussion of the internals behind this. + """ + # Compile the code as if it were a top-level module + oldModules = list(sys.modules.keys()) + try: + with topLevelNamespace(path) as namespace: + compileStream(stream, namespace, compileOptions, filename) + finally: + if not _cacheImports: + purgeModulesUnsafeToCache(oldModules) + # Construct a Scenario from the resulting namespace + return constructScenarioFrom(namespace, scenario) + + +@contextmanager +def topLevelNamespace(path=None): + """Creates an environment like that of a Python script being run directly. + + Specifically, __name__ is '__main__', __file__ is the path used to invoke + the script (not necessarily its absolute path), and the parent directory is + added to the path so that 'import blobbo' will import blobbo from that + directory if it exists there. + """ + directory = os.getcwd() if path is None else os.path.dirname(path) + namespace = {"__name__": "__main__"} + if path is not None: + namespace["__file__"] = path + sys.path.insert(0, directory) + try: + yield namespace + finally: + # Remove directory from sys.path, being a little careful in case the + # Scenic program modified it (unlikely but possible). + try: + sys.path.remove(directory) + except ValueError: + pass + + +def purgeModulesUnsafeToCache(oldModules): + """Uncache loaded modules which should not be kept after compilation. + + Keeping Scenic modules in `sys.modules` after compilation will cause + subsequent attempts at compiling the same module to reuse the compiled + scenario: this is usually not what is desired, since compilation can depend + on external state (in particular overridden global parameters, used e.g. to + specify the map for driving domain scenarios). + + Args: + oldModules: List of names of modules loaded before compilation. These + will be skipped. + """ + toRemove = [] + # copy sys.modules in case it mutates during iteration (actually happens!) + for name, module in sys.modules.copy().items(): + if isinstance(module, ScenicModule) and name not in oldModules: + toRemove.append(name) + for name in toRemove: + parent, _, child = name.rpartition(".") + parent = sys.modules.get(parent) + if parent: + # Remove reference to purged module from parent module. This is necessary + # so that future imports of the purged module will properly refer to the + # newly-loaded version of it. See below for a long disquisition on this. + del parent.__dict__[child] + + # Here are details on why the above line is necessary and the sorry history + # of my attempts to fix this type of bug (hopefully this note will prevent + # further self-sabotage). Suppose we have a Python package 'package' + # with a Scenic submodule 'submodule'. A Scenic program with the line + # from package import submodule + # will import 2 packages, namely package and package.submodule, when first + # compiled. We will then purge package.submodule from sys.modules, but not + # package, since it is an ordinary module. So if the program is compiled a + # second time, the line above will NOT import package.submodule, but simply + # access the attribute 'submodule' of the existing package 'package'. So the + # reference to the old version of package.submodule will leak out. + # (An alternative approach, which I used to use, would be to purge all + # modules containing even indirect references to Scenic modules, but this + # opens a can of worms: the implementation of + # import parent.child + # does not set the 'child' attribute of 'parent' if 'parent.child' is already + # in sys.modules, violating an invariant that Python expects [see + # https://docs.python.org/3/reference/import.html#submodules] and leading to + # confusing errors. So if parent is purged because it has some child which is + # a Scenic module, *all* of its children must then be purged. Since the + # scenic module itself can contain indirect references to Scenic modules (the + # world models), this means we have to purge the entire scenic package. But + # then whoever did 'import scenic' at the top level will be left with a + # reference to the old version of the Scenic module.) + # + # 2023 update: after hitting yet another bug caused by a reference to a + # Scenic module surviving the purge, I've decided to completely ban importing + # Scenic modules from Python (except when building the documentation). Any + # objects needed in both Python and Scenic modules should be defined in a + # Python module and imported from there. + del sys.modules[name] + + +def compileStream(stream, namespace, compileOptions, filename): + """Compile a stream of Scenic code and execute it in a namespace. + + The compilation procedure consists of the following main steps: + + 1. Parse the Scenic code into a Scenic AST using the parser generated + by ``pegen`` from :file:`scenic.gram`. + 2. Compile the Scenic AST into a Python AST with the desired semantics. + This is done by the compiler, `scenic.syntax.compiler`. + 3. Compile and execute the Python AST. + 4. Extract the global state (e.g. objects). + This is done by the `storeScenarioStateIn` function. + """ + if errors.verbosityLevel >= 2: + veneer.verbosePrint(f" Compiling Scenic module from {filename}...") + startTime = time.time() + veneer.activate(compileOptions, namespace) + try: + # Execute preamble + exec(compile(preamble, "", "exec"), namespace) + namespace[namespaceReference] = namespace + + # Parse the source + source = stream.read().decode("utf-8") + scenic_tree = parse_string(source, "exec", filename=filename) + + if dumpScenicAST: + print(f"### Begin Scenic AST of {filename}") + print(dump(scenic_tree, include_attributes=False, indent=4)) + print("### End Scenic AST") + + # Compile the Scenic AST into a Python AST + tree, requirements = compileScenicAST(scenic_tree, filename=filename) + astHasher = hashlib.blake2b(digest_size=4) + astHasher.update(ast.dump(tree).encode()) + + if dumpFinalAST: + print(f"### Begin final AST of {filename}") + print(dump(tree, include_attributes=True, indent=4)) + print("### End final AST") + + pythonSource = astToSource(tree) + if dumpASTPython: + if pythonSource is None: + raise RuntimeError( + "dumping the Python equivalent of the AST" + " requires the astor package" + ) + print(f"### Begin Python equivalent of final AST of {filename}") + print(pythonSource) + print("### End Python equivalent of final AST") + + # Compile the Python AST tree + code = compileTranslatedTree(tree, filename) + + # Execute it + executeCodeIn(code, namespace) + + # Extract scenario state from veneer and store it + astHash = astHasher.digest() + storeScenarioStateIn(namespace, requirements, astHash, compileOptions) + finally: + veneer.deactivate() + if errors.verbosityLevel >= 2: + totalTime = time.time() - startTime + veneer.verbosePrint(f" Compiled Scenic module in {totalTime:.4g} seconds.") + return code, pythonSource + + +def dump( + node: ast.AST, + annotate_fields: bool = True, + include_attributes: bool = False, + *, + indent: int, +): + if sys.version_info >= (3, 9): + print(ast.dump(node, annotate_fields, include_attributes, indent=indent)) + else: + # omit `indent` if not supported + print(ast.dump(node, annotate_fields, include_attributes)) + + +def astToSource(tree: ast.AST): + if sys.version_info >= (3, 9): + return ast.unparse(tree) + try: + import astor + except ModuleNotFoundError: + return None + return astor.to_source(tree) + + +### TRANSLATION PHASE ZERO: definitions of language elements not already in Python + +## Options + +dumpScenicAST = False +dumpFinalAST = False +dumpASTPython = False +usePruning = True + +## Preamble +# (included at the beginning of every module to be translated; +# imports the implementations of the public language features) +preamble = """\ +from scenic.syntax.veneer import * +""" + +## Get Python names of various elements +## (for checking consistency between the translator and the veneer) + +api = set(veneer.__all__) + +namespaceReference = "_Scenic_module_namespace" # used in the implementation of 'model' + +### TRANSLATION PHASE ONE: handling imports + +## Loader for Scenic files, producing ScenicModules + + +class ScenicLoader(importlib.abc.InspectLoader): + def __init__(self, name, filepath): + self.filepath = filepath + + def create_module(self, spec): + return ScenicModule(spec.name) + + def exec_module(self, module): + # Read source file and compile it + with open(self.filepath, "r") as stream: + source = stream.read() + with open(self.filepath, "rb") as stream: + code, pythonSource = compileStream( + stream, module.__dict__, CompileOptions(), self.filepath + ) + # Save code, source, and translated source for later inspection + module._code = code + module._source = source + module._pythonSource = pythonSource + + # If we're in the process of compiling another Scenic module, inherit + # objects, parameters, etc. from this one + if veneer.isActive(): + veneer.currentScenario._inherit(module._scenario) + + def is_package(self, fullname): + return False + + def get_code(self, fullname): + module = importlib.import_module(fullname) + assert isinstance(module, ScenicModule), module + return module._code + + def get_source(self, fullname): + module = importlib.import_module(fullname) + assert isinstance(module, ScenicModule), module + return module._source + + +class ScenicModule(types.ModuleType): + def __getstate__(self): + state = self.__dict__.copy() + del state["__builtins__"] + return (self.__name__, state) + + def __setstate__(self, state): + name, state = state + self.__init__(name) # needed to create __dict__ + self.__dict__.update(state) + self.__builtins__ = builtins.__dict__ + + +# Give instances of ScenicModule a falsy __module__ to prevent Sphinx from +# getting confused. (Autodoc doesn't expect modules to have that attribute, +# and we can't del it.) We only do this during Sphinx runs since it seems to +# sometimes break pickling of the modules. +sphinx = sys.modules.get("sphinx") +buildingDocs = sphinx and getattr(sphinx, "_buildingScenicDocs", False) +if buildingDocs: + ScenicModule.__module__ = None + +## Finder for Scenic (and Python) files + +scenicExtensions = (".scenic", ".sc") + +import importlib.machinery as machinery + +loaders = [ + (machinery.ExtensionFileLoader, machinery.EXTENSION_SUFFIXES), + (machinery.SourceFileLoader, machinery.SOURCE_SUFFIXES), + (machinery.SourcelessFileLoader, machinery.BYTECODE_SUFFIXES), + (ScenicLoader, scenicExtensions), +] + + +class ScenicFileFinder(importlib.abc.PathEntryFinder): + def __init__(self, path): + self._inner = machinery.FileFinder(path, *loaders) + + def find_spec(self, fullname, target=None): + spec = self._inner.find_spec(fullname, target=target) + # Disallow imports of Scenic modules from Python modules, unless we are + # building the documentation (to allow autodoc to introspect them; this + # requires careful setup in `docs/conf.py`). + # See `purgeModulesUnsafeToCache` for the rationale. + if ( + spec + and spec.origin + and not (veneer.isActive() or buildingDocs) + and any(spec.origin.endswith(ext) for ext in scenicExtensions) + ): + return None + return spec + + def invalidate_caches(self): + self._inner.invalidate_caches() + + # Support pkgutil.iter_modules() (used by Sphinx autosummary's recursive mode); + # we need to use a subclass of FileFinder since pkgutil's implementation for + # vanilla FileFinder uses inspect.getmodulename, which doesn't recognize the + # .scenic file extension. + def iter_modules(self, prefix): + # This is mostly copied from pkgutil._iter_file_finder_modules + yielded = {} + try: + filenames = os.listdir(self._inner.path) + except OSError: + return + filenames.sort() + for fn in filenames: + modname = inspect.getmodulename(fn) + if not modname: + # Check for Scenic modules + base = os.path.basename(fn) + for ext in scenicExtensions: + if base.endswith(ext): + modname = base[: -len(ext)] + break + if modname == "__init__" or modname in yielded: + continue + + path = os.path.join(self._inner.path, fn) + ispkg = False + + if not modname and os.path.isdir(path) and "." not in fn: + modname = fn + try: + dircontents = os.listdir(path) + except OSError: + # ignore unreadable directories like import does + dircontents = [] + for fn in dircontents: + subname = inspect.getmodulename(fn) + if subname == "__init__": + ispkg = True + break + else: + continue # not a package + + if modname and "." not in modname: + yielded[modname] = 1 + yield prefix + modname, ispkg + + +# Install path hook using our finder +def scenic_path_hook(path): + if not path: + path = os.getcwd() + if not os.path.isdir(path): + raise ImportError("only directories are supported", path=path) + return ScenicFileFinder(path) + + +sys.path_hooks.insert(0, scenic_path_hook) +sys.path_importer_cache.clear() + +### Translation phase two to four are done by the parser & compiler + +### TRANSLATION PHASE FIVE: AST compilation + + +def compileTranslatedTree(tree, filename): + try: + return compile(tree, filename, "exec") + except SyntaxError as e: + raise PythonCompileError(e) from None + + +### TRANSLATION PHASE SIX: Python execution + + +def executeCodeIn(code, namespace): + """Execute the final translated Python code in the given namespace.""" + try: + exec(code, namespace) + except RejectionException as e: + # Determined statically that the scenario has probability zero. + errors.optionallyDebugRejection(e) + if errors.showInternalBacktrace: + raise InvalidScenarioError(e.args[0]) from e + else: + raise InvalidScenarioError(e.args[0]).with_traceback( + e.__traceback__ + ) from None + + +### TRANSLATION PHASE SEVEN: scenario construction + + +def storeScenarioStateIn(namespace, requirementSyntax, astHash, options): + """Post-process an executed Scenic module, extracting state from the veneer.""" + + # Save requirement syntax and other module-level information + namespace["_astHash"] = astHash + namespace["_compileOptions"] = options + moduleScenario = veneer.currentScenario + factory = veneer.simulatorFactory + bns = gatherBehaviorNamespacesFrom(moduleScenario._behaviors) + + def handle(scenario): + scenario._requirementSyntax = requirementSyntax + if isinstance(scenario, type): + scenario._simulatorFactory = staticmethod(factory) + else: + scenario._simulatorFactory = factory + scenario._behaviorNamespaces = bns + + handle(moduleScenario) + namespace["_scenarios"] = tuple(veneer.scenarios) + for scenarioClass in veneer.scenarios: + handle(scenarioClass) + + # Extract requirements, scan for relations used for pruning, and create closures + # (only for top-level scenario; modular scenarios will be handled when instantiated) + moduleScenario._compileRequirements() + + # Save global parameters + for name, value in veneer._globalParameters.items(): + if needsLazyEvaluation(value): + raise InvalidScenarioError( + f"parameter {name} uses value {value}" + " undefined outside of object definition" + ) + for scenario in veneer.scenarios: + scenario._bindGlobals(veneer._globalParameters) + moduleScenario._bindGlobals(veneer._globalParameters) + + namespace["_scenario"] = moduleScenario + + +def gatherBehaviorNamespacesFrom(behaviors): + """Gather any global namespaces which could be referred to by behaviors. + + We'll need to rebind any sampled values in them at runtime. + """ + behaviorNamespaces = {} + + def registerNamespace(modName, ns): + oldNS = behaviorNamespaces.get(modName) + if oldNS: + # Already registered; just do a consistency check to avoid bizarre + # bugs from having multiple versions of the same module around. + if oldNS is not ns: + raise RuntimeError( + f"scenario refers to multiple versions of module {modName}; " + "perhaps you imported it before you started compilation?" + ) + return + behaviorNamespaces[modName] = ns + for name, value in ns.items(): + if isinstance(value, ScenicModule): + registerNamespace(value.__name__, value.__dict__) + else: + # Convert values requiring sampling to Distributions + dval = toDistribution(value) + if dval is not value: + ns[name] = dval + + for behavior in behaviors: + modName = behavior.__module__ + globalNamespace = behavior.makeGenerator.__globals__ + registerNamespace(modName, globalNamespace) + return behaviorNamespaces + + +def constructScenarioFrom(namespace, scenarioName=None): + """Build a Scenario object from an executed Scenic module.""" + modularScenarios = namespace["_scenarios"] + topLevelScenario = namespace["_scenario"] + + def isModularScenario(thing): + return isinstance(thing, type) and issubclass(thing, DynamicScenario) + + if not scenarioName and isModularScenario(namespace.get("Main", None)): + scenarioName = "Main" + if scenarioName: + ty = namespace.get(scenarioName, None) + if not isModularScenario(ty): + raise RuntimeError(f'no scenario "{scenarioName}" found') + if ty._requiresArguments(): + raise RuntimeError( + f'cannot instantiate scenario "{scenarioName}"' " with no arguments" + ) from None + + dynScenario = ty() + elif len(modularScenarios) > 1: + raise RuntimeError( + "multiple choices for scenario to run " + "(specify using the --scenario option)" + ) + elif modularScenarios and not modularScenarios[0]._requiresArguments(): + dynScenario = modularScenarios[0]() + else: + dynScenario = topLevelScenario + + if not dynScenario._prepared: # true for all except top-level scenarios + # Execute setup block (if any) to create objects and requirements; + # extract any requirements and scan for relations used for pruning + dynScenario._inherit(topLevelScenario) + dynScenario._prepare(delayPreconditionCheck=True) + + scenario = dynScenario._toScenario(namespace) + + # Prune infeasible parts of the space + if usePruning: + pruning.prune(scenario, verbosity=errors.verbosityLevel) + + # Validate scenario + scenario.validate() + + return scenario From 83929035efadf584418e92aedfa036cc527dfe31 Mon Sep 17 00:00:00 2001 From: Kay Date: Tue, 17 Mar 2026 17:46:21 -0700 Subject: [PATCH 60/73] updated metsr interface with client changes --- src/scenic/simulators/metsr/simulator.py | 250 +++++++++++------------ 1 file changed, 125 insertions(+), 125 deletions(-) diff --git a/src/scenic/simulators/metsr/simulator.py b/src/scenic/simulators/metsr/simulator.py index 827c63650..197d8d28c 100644 --- a/src/scenic/simulators/metsr/simulator.py +++ b/src/scenic/simulators/metsr/simulator.py @@ -1,125 +1,125 @@ -"""Simulator interface for METS-R Sim.""" - -import math - -from scenic.core.simulators import Simulation, Simulator -from scenic.core.vectors import Orientation, Vector -from scenic.simulators.metsr.client import METSRClient - - -class METSRSimulator(Simulator): - def __init__(self, host, port, map_name, timestep, sim_timestep, verbose=False): - super().__init__() - self.client = METSRClient(host=host, port=port, verbose=verbose) - - self.map_name = map_name - self.timestep = timestep - self.sim_timestep = sim_timestep - - def createSimulation(self, scene, timestep, **kwargs): - assert timestep is None or timestep == self.timestep - return METSRSimulation( - scene, self.client, self.map_name, self.timestep, self.sim_timestep, **kwargs - ) - - def destroy(self): - self.client.close() - super().destroy() - - -class METSRSimulation(Simulation): - def __init__(self, scene, client, map_name, timestep, sim_timestep, **kwargs): - self.client = client - self.map_name = map_name - - self.timestep = timestep - self.sim_timestep = sim_timestep - self.sim_ticks_per = int(timestep / sim_timestep) - assert self.sim_ticks_per == timestep / sim_timestep - - self.next_pv_id = 0 - self.pv_id_map = {} - self.frozen_vehicles = set() - - self._client_calls = [] - - self.count = 0 - - super().__init__(scene, timestep=timestep, **kwargs) - - def setup(self): - # Reset map - self.client.reset("Data.properties.CARLA") - - super().setup() # Calls createObjectInSimulator for each object - - def createObjectInSimulator(self, obj): - assert obj.origin - assert obj.destination - - call_kwargs = { - "vehID": self.getPrivateVehId(obj), - "origin": obj.origin, - "destination": obj.destination, - } - - self.client.generate_trip(**call_kwargs) - - def step(self): - self.count += 1 - if self.count % 100 == 0: - print(".", end="", flush=True) - for _ in range(self.sim_ticks_per): - self.client.tick() - - def updateObjects(self): - obj_veh_ids = [self.getPrivateVehId(obj) for obj in self.objects] - raw_veh_data = self.client.query_vehicle(obj_veh_ids, True, True) - self.obj_data_cache = {obj: raw_veh_data['DATA'][i] for i, obj in enumerate(self.objects)} - super().updateObjects() - self.obj_data_cache = None - - def getProperties(self, obj, properties): - if obj in self.frozen_vehicles: - return None - - raw_data = self.obj_data_cache[obj] - - if "road" not in raw_data and raw_data["state"] <= 0: - self.frozen_vehicles.add(obj) - - position = Vector(raw_data["x"], raw_data["y"], 0) - speed = raw_data["speed"] - bearing = math.radians(raw_data["bearing"]) - globalOrientation = Orientation.fromEuler(bearing, 0, 0) - yaw, pitch, roll = obj.parentOrientation.localAnglesFor(globalOrientation) - velocity = Vector(0, speed, 0).rotatedBy(yaw) - angularSpeed = 0 - angularVelocity = Vector(0, 0, 0) - - values = dict( - position=position, - velocity=velocity, - speed=speed, - angularSpeed=angularSpeed, - angularVelocity=angularVelocity, - yaw=yaw, - pitch=pitch, - roll=roll, - ) - return values - - def destroy(self): - if self.client.verbose: - print("Client Messages Log:") - print("[") - for call in self.client._messagesLog: - print(f" {call},") - print("]") - - def getPrivateVehId(self, obj): - if obj not in self.pv_id_map: - self.pv_id_map[obj] = self.next_pv_id - self.next_pv_id += 1 - - return self.pv_id_map[obj] +"""Simulator interface for METS-R Sim.""" + +import math + +from scenic.core.simulators import Simulation, Simulator +from scenic.core.vectors import Orientation, Vector +from scenic.simulators.metsr.client import METSRClient + + +class METSRSimulator(Simulator): + def __init__(self, host, port, map_name, timestep, sim_timestep, verbose=False): + super().__init__() + self.client = METSRClient(host=host, port=port, verbose=verbose) + + self.map_name = map_name + self.timestep = timestep + self.sim_timestep = sim_timestep + + def createSimulation(self, scene, timestep, **kwargs): + assert timestep is None or timestep == self.timestep + return METSRSimulation( + scene, self.client, self.map_name, self.timestep, self.sim_timestep, **kwargs + ) + + def destroy(self): + self.client.close() + super().destroy() + + +class METSRSimulation(Simulation): + def __init__(self, scene, client, map_name, timestep, sim_timestep, **kwargs): + self.client = client + self.map_name = map_name + + self.timestep = timestep + self.sim_timestep = sim_timestep + self.sim_ticks_per = int(timestep / sim_timestep) + assert self.sim_ticks_per == timestep / sim_timestep + + self.next_pv_id = 0 + self.pv_id_map = {} + self.frozen_vehicles = set() + + self._client_calls = [] + + self.count = 0 + + super().__init__(scene, timestep=timestep, **kwargs) + + def setup(self): + # Reset map + self.client.reset() + + super().setup() # Calls createObjectInSimulator for each object + + def createObjectInSimulator(self, obj): + assert obj.origin + assert obj.destination + + call_kwargs = { + "vehID": self.getPrivateVehId(obj), + "origin": obj.origin, + "destination": obj.destination, + } + + self.client.generate_trip(**call_kwargs) + + def step(self): + self.count += 1 + if self.count % 100 == 0: + print(".", end="", flush=True) + for _ in range(self.sim_ticks_per): + self.client.tick() + + def updateObjects(self): + obj_veh_ids = [self.getPrivateVehId(obj) for obj in self.objects] + raw_veh_data = self.client.query_vehicle(obj_veh_ids, True, True) + self.obj_data_cache = {obj: raw_veh_data['DATA'][i] for i, obj in enumerate(self.objects)} + super().updateObjects() + self.obj_data_cache = None + + def getProperties(self, obj, properties): + if obj in self.frozen_vehicles: + return None + + raw_data = self.obj_data_cache[obj] + + if "road" not in raw_data and raw_data["state"] <= 0: + self.frozen_vehicles.add(obj) + + position = Vector(raw_data["x"], raw_data["y"], 0) + speed = raw_data["speed"] + bearing = math.radians(raw_data["bearing"]) + globalOrientation = Orientation.fromEuler(bearing, 0, 0) + yaw, pitch, roll = obj.parentOrientation.localAnglesFor(globalOrientation) + velocity = Vector(0, speed, 0).rotatedBy(yaw) + angularSpeed = 0 + angularVelocity = Vector(0, 0, 0) + + values = dict( + position=position, + velocity=velocity, + speed=speed, + angularSpeed=angularSpeed, + angularVelocity=angularVelocity, + yaw=yaw, + pitch=pitch, + roll=roll, + ) + return values + + def destroy(self): + if self.client.verbose: + print("Client Messages Log:") + print("[") + for call in self.client._messagesLog: + print(f" {call},") + print("]") + + def getPrivateVehId(self, obj): + if obj not in self.pv_id_map: + self.pv_id_map[obj] = self.next_pv_id + self.next_pv_id += 1 + + return self.pv_id_map[obj] From 0d17dfc928d9d9ab17b842b0d9d19edd0cb0619c Mon Sep 17 00:00:00 2001 From: Kay Date: Tue, 17 Mar 2026 17:49:02 -0700 Subject: [PATCH 61/73] updated metsr spawn logic --- src/scenic/simulators/cosim/simulator.py | 116 ++++++++++------------- 1 file changed, 50 insertions(+), 66 deletions(-) diff --git a/src/scenic/simulators/cosim/simulator.py b/src/scenic/simulators/cosim/simulator.py index 3409e6b84..9b9533025 100644 --- a/src/scenic/simulators/cosim/simulator.py +++ b/src/scenic/simulators/cosim/simulator.py @@ -194,9 +194,6 @@ def setup(self) -> None: valid_metsr_roads = self.metsr_client.query_road() generated_roads = [value.split('_')[0] for value in self.scenic_to_metsr_map.values()] - # print(generated_mappings) - # print(valid_roads['orig_id']) - for mapping in generated_roads: if mapping not in valid_metsr_roads["orig_id"]: self.intersection_road_links.append(mapping) @@ -232,21 +229,12 @@ def setup(self) -> None: # Create objects. super().setup() - #TEMP FIX? IDK... - for object in self.objects[1:]: - object.carla_actor_flag = False - object.spawn_guard = 0 - self.objects[0].carla_actor_flag = True - self.objects[0].spawn_guard = 0 - - # print(f"bubble_size {self.bubble_size}") self.objects[0].bubble = CircularRegion(center=[self.objects[0].x, self.objects[0].y], radius=self.bubble_size) _, new_lanes, _ = self.update_carla_lanes() # TODO bad function name self.freeze_lanes(new_lanes) - print(f"Ticking Metsr") self.metsr_client.tick() for obj in self.objects: @@ -290,7 +278,6 @@ def createObjectInMetsr(self, obj: Object, origin: int = None, destination: int Creates vehicle inside the METSR simulator """ - print(f"Creating obj {obj.name if hasattr(obj,'name') else 'NPC'} in metsr") assert obj.origin, "Metsr objects must have a defined origin" assert obj.destination, "Metsr objects must have a defined destination" @@ -302,7 +289,10 @@ def createObjectInMetsr(self, obj: Object, origin: int = None, destination: int "origin": obj_origin, "destination": obj_destination, } - self.metsr_client.generate_trip(**call_kwargs) + if origin or destination: + self.metsr_client.generate_trip_between_roads(**call_kwargs) + else: + self.metsr_client.generate_trip(**call_kwargs) def createObjectInCarla(self, obj: Object, update_orientation: bool = False, trajectory: list[carla.Transform] = None) -> None: @@ -315,7 +305,6 @@ def createObjectInCarla(self, obj: Object, update_orientation: bool = False, tra :type update_orientation: bool """ - print(f"Creating obj {obj.name if hasattr(obj,'name') else 'NPC'} in Carla") try: blueprint = self.blueprintLib.find(obj.blueprint) except IndexError as e: @@ -359,7 +348,7 @@ def createObjectInCarla(self, obj: Object, update_orientation: bool = False, tra rot = utils.scenicToCarlaRotation(obj.orientation) transform = carla.Transform(loc, rot) - + print(f"Attempting to spawn obj {obj.name} in location: {transform.location}") # Color, cannot be set for Pedestrians if blueprint.has_attribute("color") and obj.color is not None: c = obj.color @@ -370,10 +359,12 @@ def createObjectInCarla(self, obj: Object, update_orientation: bool = False, tra # print(f"Spawning actor {obj} in location {loc} with original pos: {obj.position} in CARLA") try: carlaActor = self.carla_world.spawn_actor(blueprint, transform) - obj.carla_actor_flag = True - obj.spawn_guard = 2 except Exception as e: print(f"Error: {e} occured \n displaying object positions") + print(f"Failed to spawn in position: {obj.position}") + car_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True) + print(f"Metsr data for failed spawn: {car_data}") + for car in self.objects: car_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(car), True, True) simulator = "carla" if car.carla_actor_flag else "metsr" @@ -384,15 +375,16 @@ def createObjectInCarla(self, obj: Object, update_orientation: bool = False, tra loc = None if loc is not None: - # print(f"{car} in {simulator}: [Metsr][Scenic][CARLA] coords X: {car_data['DATA'][0]['x'], car.position.x, loc.x}, y:{car_data['DATA'][0]['y'], car.position.y, -loc.y}") - print(f"Query Vehicle resuts for obj: :{car}, is: {car_data['DATA']}") + print(f"{car} in {simulator}: [Metsr][Scenic][CARLA] coords X: {car_data['DATA'][0]['x'], car.position.x, loc.x}, y:{car_data['DATA'][0]['y'], car.position.y, -loc.y}") + print(f"Query Vehicle results for obj: :{car}, is: {car_data['DATA']}") else: - # print(f"{car} in {simulator}: [Metsr][Scenic] coords X: {car_data['DATA'][0]['x'], car.position.x}, y:{car_data['DATA'][0]['y'], car.position.y}") - print(f"Query Vehicle resuts for obj: :{car}, is: {car_data['DATA']}") + print(f"{car} in {simulator}: [Metsr][Scenic] coords X: {car_data['DATA'][0]['x'], car.position.x}, y:{car_data['DATA'][0]['y'], car.position.y}") + print(f"Query Vehicle results for obj: :{car}, is: {car_data['DATA']}") print(f"Checking distance function: {_utils.within_threshold_to(obj,[obj for obj in self.objects if obj.carla_actor_flag], verbose=True)}") print(f"Issue occured at timestep: {self.count}") # raise e(f"Error : {e} occured") + if carlaActor is None: raise SimulationCreationError(f"Unable to spawn object {obj}") obj.carlaActor = carlaActor @@ -424,6 +416,9 @@ def createObjectInCarla(self, obj: Object, update_orientation: bool = False, tra f"Unable to spawn carla controller for object {obj}" ) obj.carlaController = controller + + obj.spawn_guard = 2 + obj.carla_actor_flag = True @@ -440,7 +435,6 @@ def createObjectInSimulator(self, obj: Object) -> None: assert obj.destination, "All objects must have an destination" assert hasattr(obj, "carla_actor_flag"), "All objects must have attribute: carla_actor_flag" - if obj == self.objects[0]: # Special handling for ego trajectory = None if hasattr(obj, "trajectory"): @@ -449,12 +443,11 @@ def createObjectInSimulator(self, obj: Object) -> None: lane = self._nearest_lane(obj) origin_str = self.map_scenic_to_metsr(lane) self.createObjectInMetsr(obj,origin=origin_str) # Set the METSR vehicle origin to match ego spawn - self.metsr_client.tick() # tick the simulation to allow the ego to enter the road car_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True)["DATA"][0] obj.position = Vector(car_data["x"], car_data["y"], 0) obj.final_road = None self.createObjectInCarla(obj, update_orientation=True, trajectory=trajectory) # spawn ego in updated location and update orientation - + elif obj.carla_actor_flag: self.createObjectInCarla(obj) self.synchronize_clients(obj) @@ -464,7 +457,8 @@ def createObjectInSimulator(self, obj: Object) -> None: obj.finished_route = False if self.count == 0: self.metsr_client.tick() - + obj.carla_actor_flag = False + obj.spawn_guard = 0 def getCarlaProperties(self, obj : Object, properties : dict) -> dict[str, float | Vector | int]: @@ -597,7 +591,7 @@ def step(self) -> None: self.synchronize_clients() # It takes 2 CARLA ticks to fully instantiate and update properties once spawned in CARLA - for obj in self.objects: + for obj in self.objects[1:]: # Skip ego if obj.spawn_guard > 0: obj.spawn_guard -= 1 @@ -607,15 +601,12 @@ def step(self) -> None: # Metsr step self.count += 1 - if self.count % 100 == 0: # just removing for now - print(".", end="", flush=True) - - print(f"Ticking METSR") - self.metsr_client.tick() - - cosim_data = self.metsr_client.query_coSimVehicle() - print(f"Displaying CoSim data at step: {self.count}: {cosim_data}") + if self.count % 100 == 0: + print(f"Step: {self.count}") + total_bubble_vehicles = len([1 for obj in self.objects if obj.carla_actor_flag]) + print(f"Total objects in bubble region is: {total_bubble_vehicles}") + self.metsr_client.tick() # Generate bubble region based on ego objects TODO update logic for multiple high-fidelity zones self.objects[0].bubble = CircularRegion(center=[self.objects[0].x, self.objects[0].y], @@ -636,11 +627,11 @@ def get_carla_lanes(self) -> list[Lane]: """ # Set ego, lane, bubble ego = self.objects[0] - lane = self._nearest_lane(ego) + ego_lane = self._nearest_lane(ego) bubble_region = ego.bubble # Collect lanes which intersect bubble - carla_lanes = [] + carla_lanes = [ego_lane] for lane in self.workspace.network.lanes: if lane.intersects(bubble_region): carla_lanes.append(lane) @@ -831,19 +822,18 @@ def synchronize_clients(self, obj: Object | list[Object] = None): # Update METSR road if hasattr(obj, "previous_road"): if roadID != obj.previous_road and roadID not in self.intersection_road_links: # Entering new road within metsr network - print(f"obj: {obj.name} leaving road: {obj.previous_road} moving to : {roadID}") - # cosim_data = self.metsr_client.query_coSimVehicle() - # # print(f"Cosim data is: {cosim_data}") - # # for data_entry in cosim_data['DATA']: - # # if data_entry['ID'] == vehID: - # # route_data = data_entry['route'] - # # print(f"Displaying vehicle route data for {obj.name}: {route_data}") - # Find the corresponding METSR keys + cosim_data = self.metsr_client.query_coSimVehicle() + for data_entry in cosim_data['DATA']: + if data_entry['ID'] == vehID: + route_data = data_entry['route'] + # print(f"obj: {obj.name} leaving road: {obj.previous_road} moving to : {roadID}") + # print(f"route data: {route_data}") + # print(f"vehicle has data: {veh_data}") carla_lane_ids = set([self.map_scenic_to_metsr(lane) for lane in self.frozen_scenic_lanes]) - if roadID not in carla_lane_ids: - print(f"{obj.name} leaving the bubble region") - else: - print(f"{obj.name} entering lane: {roadID}") + # if roadID not in carla_lane_ids: + # print(f"{obj.name} leaving the bubble region") + # else: + # print(f"{obj.name} entering lane: {roadID}") self.metsr_client.enter_next_road(vehID, roadID=roadID, private_veh = True) if obj.previous_road == obj.final_road: obj.finished_route = True @@ -909,6 +899,7 @@ def update_bubble_objects(self, carla_lanes: list[Lane], new_lanes: list[str], i """ carla_actors = [obj for obj in self.objects if obj.carla_actor_flag] # TODO might need to consider a more efficient data structure here cosim_data = self.metsr_client.query_coSimVehicle() + for obj in self.objects[1:]: veh_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True) # lane = self._nearest_lane(obj) @@ -917,9 +908,6 @@ def update_bubble_objects(self, carla_lanes: list[Lane], new_lanes: list[str], i if obj.carla_actor_flag: if obj.finished_route: carla_actors.remove(obj) - # print(f'removing vehicle: {obj.name} after completing its route') - # print(f"Current road_lane (Scenic) is {f'{lane.road.id}_{lane.id}'}") - # print(f"{veh_data['DATA'][0]}") self.remove_bubble_object(obj) elif (lane not in carla_lanes) and (intersection not in intersections): @@ -937,9 +925,8 @@ def update_bubble_objects(self, carla_lanes: list[Lane], new_lanes: list[str], i if not_enough_space: if obj not in self.bubble_spawn_queue: _utils.within_threshold_to(obj,carla_actors, verbose=True) - # print(f"Not enough space to spawn {obj.name} at {self.count} in location: {obj.position}:") self.bubble_spawn_queue.add(obj) - self.check_world_state_consistency() + # self.check_world_state_consistency() continue @@ -958,22 +945,15 @@ def update_bubble_objects(self, carla_lanes: list[Lane], new_lanes: list[str], i if carla_trajectory == None: if obj not in self.bubble_spawn_queue: - # print(f"No valid trajectory found for vehicle data: {[data_entry['route'] for data_entry in cosim_data['DATA'] if data_entry['ID'] == VehID]} ") - # print(f"Skipping spawn for obj: {obj.name} at {lane.road.id}_{lane.id} with intersection {obj._intersection}: due to failed trajectory generation") - # print(f"Obj location was: :{obj.x, obj.y} default road was: {obj._lane.road.id if obj.lane != None else None} selected road was: {lane.road.id}") self.bubble_spawn_queue.add(obj) continue # Do not spawn vehicle if no trajectory can be created - - # if obj._intersection in intersections: - # print(f"Spawning object {obj} in intersection") - - # print(f"Spawning obj: {obj.name} in CARLA at : {obj.x, obj.y}") - self.createObjectInCarla(obj,update_orientation=True, trajectory=carla_trajectory) + self.createObjectInCarla(obj,update_orientation=True, trajectory=carla_trajectory) if obj in self.bubble_spawn_queue: self.bubble_spawn_queue.remove(obj) carla_actors.append(obj) + def scenic_trajectory_to_carla(self, trajectory: list[Lane]) -> list: """ @@ -1012,8 +992,8 @@ def freeze_lanes(self, keys: list[str]) -> None: assert key not in self.carla_control_roads, "Attempted to freeze already frozen lane" if key not in self.intersection_road_links: # Skip roads not recognized by metsr self.carla_control_roads[key] = True # Keep track of frozen lanes + # print(f"Freezing: {key}") self.metsr_client.set_cosim_road(key) - print(f"Freezing: {key}") def release_lanes(self,keys: list[str]) -> None: @@ -1031,7 +1011,7 @@ def release_lanes(self,keys: list[str]) -> None: if key not in self.intersection_road_links: # Skip roads not recognized by metsr del self.carla_control_roads[key] # Remove frozen lane from record self.metsr_client.release_cosim_road(key) - print(f"Releasing: {key}") + # print(f"Releasing: {key}") def destroy_carla_obj(self,obj) -> None: @@ -1329,4 +1309,8 @@ def _save_metsr_state(self, file_name=None) -> None: Saves metsr state to a file to allow for reproducible replay """ - pass \ No newline at end of file + pass + + + # def _update_ego_route(self, ): + # ego = self.objects[0] From 126e11c9611996929adb52c0a03b50da938198da Mon Sep 17 00:00:00 2001 From: Kay Date: Tue, 17 Mar 2026 17:49:12 -0700 Subject: [PATCH 62/73] merged in client updates --- src/scenic/simulators/metsr/client.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/scenic/simulators/metsr/client.py b/src/scenic/simulators/metsr/client.py index 01a874c3d..3ed382993 100644 --- a/src/scenic/simulators/metsr/client.py +++ b/src/scenic/simulators/metsr/client.py @@ -452,7 +452,7 @@ def release_cosim_road(self, roadID): return res # teleport vehicle to a target location specified by road and coordiantes, only work when the road is a cosim road - def teleport_cosim_vehicle(self, vehID, x, y, bearing, private_veh = False, transform_coords = False): + def teleport_cosim_vehicle(self, vehID, x, y, bearing, speed = 0, private_veh = False, transform_coords = False): msg = { "TYPE": "CTRL_teleportCoSimVeh", "DATA": [] @@ -461,15 +461,18 @@ def teleport_cosim_vehicle(self, vehID, x, y, bearing, private_veh = False, tran vehID = [vehID] x = [x] y = [y] + speed = [speed] bearing = [bearing] if not isinstance(bearing, list): bearing = [bearing] * len(vehID) + if not isinstance(speed, list): + speed = [speed] * len(vehID) if not isinstance(private_veh, list): private_veh = [private_veh] * len(vehID) if not isinstance(transform_coords, list): transform_coords = [transform_coords] * len(vehID) - for vehID, x, y, bearing, private_veh, transform_coords in zip(vehID, x, y, bearing, private_veh, transform_coords): - msg["DATA"].append({"vehID": vehID, "x": x, "y": y, "bearing": bearing, "vehType": private_veh, "transformCoord": transform_coords}) + for vehID, x, y, bearing, speed, private_veh, transform_coords in zip(vehID, x, y, bearing, speed, private_veh, transform_coords): + msg["DATA"].append({"vehID": vehID, "x": x, "y": y, "bearing": bearing, "speed": speed, "vehType": private_veh, "transformCoord": transform_coords}) res = self.send_receive_msg(msg, ignore_heartbeats=True) assert res["TYPE"] == "CTRL_teleportCoSimVeh", res["TYPE"] assert res["CODE"] == "OK", res["CODE"] From 83b02b68d371a0598d217580c1d5b0ec267e898d Mon Sep 17 00:00:00 2001 From: Kay Date: Tue, 17 Mar 2026 17:49:35 -0700 Subject: [PATCH 63/73] updated model --- src/scenic/simulators/cosim/model.scenic | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/scenic/simulators/cosim/model.scenic b/src/scenic/simulators/cosim/model.scenic index 375581fc9..486956c5c 100644 --- a/src/scenic/simulators/cosim/model.scenic +++ b/src/scenic/simulators/cosim/model.scenic @@ -1,6 +1,7 @@ import pathlib from scenic.simulators.carla.model import Vehicle, is2DMode + import scenic.simulators.carla.blueprints as blueprints from scenic.simulators.carla.behaviors import * from scenic.simulators.utils.colors import Color @@ -33,6 +34,11 @@ simulator CosimSimulator( bubble_size = 100 ) +param startTime = 6*60*60 +param timestep = 1 +param simTimestep = 0.1 +param verbose=False + """ What kind of behaviors: Sensor Data @@ -43,7 +49,7 @@ What kind of behaviors: Helpers for measuring traffic metrics """ - +_DAY_MOD = 24*60*60 class Car(Vehicle): """A car. @@ -69,6 +75,7 @@ class EgoCar(Car): behavior: DriveAvoidingCollisions(target_speed=15, avoidance_threshold=12) interrupt: False + behavior EgoAttack(): condition = True # overwrite this with some condition to initiate attack while True: @@ -88,9 +95,15 @@ class NPCCar(Car): origin: -1 carla_actor_flag: False +def currentTOD(): + return (simulation().currentTime * simulation().timestep + globalParameters.startTime)%_DAY_MOD + -scenario GeneratePrivateTrip(origin,destination): - new NPCCar with origin origin, with destination destination +scenario GeneratePrivateTrip(origin,destination, name=None): + if name != None: + new NPCCar with origin origin, with destination destination, with name name + else: + new NPCCar with origin origin, with destination destination terminate after 1 steps scenario TrafficStream(origin, destination, traffic_flow): From a3566d86dc0facf202824ab101404cae0a4bc0f0 Mon Sep 17 00:00:00 2001 From: Kay Date: Thu, 2 Apr 2026 13:00:02 -0700 Subject: [PATCH 64/73] Improved overhead for synchronization --- src/scenic/simulators/cosim/simulator.py | 249 ++++++++++++++--------- 1 file changed, 157 insertions(+), 92 deletions(-) diff --git a/src/scenic/simulators/cosim/simulator.py b/src/scenic/simulators/cosim/simulator.py index 9b9533025..d5e5e5e2a 100644 --- a/src/scenic/simulators/cosim/simulator.py +++ b/src/scenic/simulators/cosim/simulator.py @@ -178,6 +178,10 @@ def __init__(self, scene, carla_client, metsr_client, sim_timestep, tm, render , self.frozen_scenic_lanes = [] self.xml_to_xodr_intersections = xml_to_xodr_intersections self.intersection_road_links = [] + self.network_lanes = [] + self.metsr_actors = [] + self.carla_actors = [] + self.bubble_sizes = [] super().__init__(scene, timestep=sim_timestep, **kwargs) @@ -228,14 +232,6 @@ def setup(self) -> None: # Create objects. super().setup() - - self.objects[0].bubble = CircularRegion(center=[self.objects[0].x, - self.objects[0].y], - radius=self.bubble_size) - - _, new_lanes, _ = self.update_carla_lanes() # TODO bad function name - self.freeze_lanes(new_lanes) - self.metsr_client.tick() for obj in self.objects: if isinstance(obj.carlaActor, carla.Vehicle): @@ -265,6 +261,8 @@ def setup(self) -> None: ) self.synchronize_clients() + + self.network_lanes = [*self.workspace.network.lanes] # TODO Waiting for map update # self._synchronize_signals() @@ -340,7 +338,6 @@ def createObjectInCarla(self, obj: Object, update_orientation: bool = False, tra blueprint=obj.blueprint, snapToGround=obj.snapToGround, ) - lane = self._nearest_lane(obj) if update_orientation: lane = self._nearest_lane(obj) rot = utils.scenicToCarlaRotation(lane.orientation[obj.position]) @@ -348,7 +345,7 @@ def createObjectInCarla(self, obj: Object, update_orientation: bool = False, tra rot = utils.scenicToCarlaRotation(obj.orientation) transform = carla.Transform(loc, rot) - print(f"Attempting to spawn obj {obj.name} in location: {transform.location}") + # print(f"Attempting to spawn obj {obj.name} in location: {transform.location}") # Color, cannot be set for Pedestrians if blueprint.has_attribute("color") and obj.color is not None: c = obj.color @@ -436,30 +433,73 @@ def createObjectInSimulator(self, obj: Object) -> None: assert hasattr(obj, "carla_actor_flag"), "All objects must have attribute: carla_actor_flag" if obj == self.objects[0]: # Special handling for ego - trajectory = None - if hasattr(obj, "trajectory"): - if obj.trajectory is not None: - trajectory = self.scenic_trajectory_to_carla(trajectory) - lane = self._nearest_lane(obj) - origin_str = self.map_scenic_to_metsr(lane) - self.createObjectInMetsr(obj,origin=origin_str) # Set the METSR vehicle origin to match ego spawn - car_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True)["DATA"][0] - obj.position = Vector(car_data["x"], car_data["y"], 0) - obj.final_road = None - self.createObjectInCarla(obj, update_orientation=True, trajectory=trajectory) # spawn ego in updated location and update orientation - - elif obj.carla_actor_flag: - self.createObjectInCarla(obj) - self.synchronize_clients(obj) - obj.finished_route = False + self.spawn_ego(obj) + self.carla_actors.append(obj) + self.metsr_actors.append(obj) else: self.createObjectInMetsr(obj) + self.metsr_actors.append(obj) obj.finished_route = False if self.count == 0: self.metsr_client.tick() obj.carla_actor_flag = False obj.spawn_guard = 0 + def spawn_ego(self,obj: Object) -> None: + """ + docstring for spawn_ego + + :param ego: Simulation ego object + :type ego: EgoCar + + Special handling for spawning the Ego vehicle + (1) First spawn ego in METSR on the appropriate lane (set by Scenic) + (2) Collect exact spawn location to define bubbble region + (3) Freeze CoSimulated regions inside METSR + (4) Set the appropriate Ego behavior + (i) Predefined trajectory + (ii)User defined behavior + (iii) Default CARLA autopilot over METSR proposed route + (5) Spawn ego inside CARLA with updated loc and trajectory + """ + trajectory = None + lane = self._nearest_lane(obj) + origin_str = self.map_scenic_to_metsr(lane) + self.createObjectInMetsr(obj,origin=origin_str) # Set the METSR vehicle origin to match ego spawn + self.metsr_client.tick() # Allow the vehicle to spawn + car_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True)["DATA"][0] + obj.position = Vector(car_data["x"], car_data["y"], 0) + obj.final_road = None + + obj.bubble = CircularRegion(center=[car_data["x"], + car_data["y"]], + radius=self.bubble_size) + + _, new_lanes, _ = self.update_carla_lanes(bubble_regions=[obj.bubble]) # TODO bad function name + self.freeze_lanes(new_lanes) # Freeze lanes according to Ego Spawn + for _ in range(1): # Allow ego to enger + self.metsr_client.tick() + + if hasattr(obj, "trajectory"): + if obj.trajectory is not None: + trajectory = self.scenic_trajectory_to_carla(trajectory) + + elif not hasattr(obj, "behavior"): + cosim_data = self.metsr_client.query_coSimVehicle() + # print(f"Attemping to find ego data in : {cosim_data}") + VehID = self.getMetsrPrivateVehId(obj) + for entry in cosim_data["DATA"]: + if entry['ID'] == VehID: + route_data = entry['route'] + trajectory = self.generate_scenic_trajectory(lane, route_data, obj._intersection) + if trajectory != None: + obj.final_road = None + trajectory = self.scenic_trajectory_to_carla(trajectory) + break # Once the trajectory is found continue + + # print(f"Spawning ego with trajectory: {trajectory}") + self.createObjectInCarla(obj, update_orientation=True, trajectory=trajectory) # spawn ego in updated location and update orientation + def getCarlaProperties(self, obj : Object, properties : dict) -> dict[str, float | Vector | int]: """ @@ -600,11 +640,12 @@ def step(self) -> None: pygame.display.flip() # Metsr step + self.bubble_sizes.append(len(self.carla_actors)) self.count += 1 - if self.count % 100 == 0: + if self.count % 50 == 0: print(f"Step: {self.count}") - total_bubble_vehicles = len([1 for obj in self.objects if obj.carla_actor_flag]) - print(f"Total objects in bubble region is: {total_bubble_vehicles}") + print(f"Total actors in simulation: {len(self.objects)}") + print(f"Average bubble actors is: {np.mean(self.bubble_sizes)}") self.metsr_client.tick() # Generate bubble region based on ego objects TODO update logic for multiple high-fidelity zones @@ -618,7 +659,7 @@ def step(self) -> None: # self._check_traffic_light_consistency() - def get_carla_lanes(self) -> list[Lane]: + def get_carla_lanes(self,bubble_region=None) -> list[Lane]: """ docstring for get_carla_lanes @@ -626,12 +667,14 @@ def get_carla_lanes(self) -> list[Lane]: :rtype: list[Lane] """ # Set ego, lane, bubble - ego = self.objects[0] - ego_lane = self._nearest_lane(ego) - bubble_region = ego.bubble + if bubble_region == None: + ego = self.objects[0] + bubble_region = ego.bubble + else: + bubble_region = bubble_region # Collect lanes which intersect bubble - carla_lanes = [ego_lane] + carla_lanes = [] for lane in self.workspace.network.lanes: if lane.intersects(bubble_region): carla_lanes.append(lane) @@ -806,10 +849,14 @@ def synchronize_clients(self, obj: Object | list[Object] = None): elif obj != None and isinstance(obj, list): carla_actors = obj else: - carla_actors = [obj for obj in self.objects if obj.carla_actor_flag] - carla_actors.append(self.objects[0]) + carla_actors = self.carla_actors + all_veh_data = self._collect_metsr_vehicle_data(self.carla_actors) + for obj in carla_actors: + if obj.carlaActor is None: + print(f"Failure identifying carlaActor for obj: {obj.name} at step {self.count}") + continue loc = obj.carlaActor.get_location() if loc == carla.Location(0,0,0): # Carla object still in the process of spawning # print(f'Passing sychronization while object spawn is processing') @@ -817,23 +864,10 @@ def synchronize_clients(self, obj: Object | list[Object] = None): vehID = self.getMetsrPrivateVehId(obj) lane = self._nearest_lane(obj) roadID = self.map_scenic_to_metsr(lane) - veh_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True) - + veh_data = all_veh_data[obj] # Update METSR road if hasattr(obj, "previous_road"): if roadID != obj.previous_road and roadID not in self.intersection_road_links: # Entering new road within metsr network - cosim_data = self.metsr_client.query_coSimVehicle() - for data_entry in cosim_data['DATA']: - if data_entry['ID'] == vehID: - route_data = data_entry['route'] - # print(f"obj: {obj.name} leaving road: {obj.previous_road} moving to : {roadID}") - # print(f"route data: {route_data}") - # print(f"vehicle has data: {veh_data}") - carla_lane_ids = set([self.map_scenic_to_metsr(lane) for lane in self.frozen_scenic_lanes]) - # if roadID not in carla_lane_ids: - # print(f"{obj.name} leaving the bubble region") - # else: - # print(f"{obj.name} entering lane: {roadID}") self.metsr_client.enter_next_road(vehID, roadID=roadID, private_veh = True) if obj.previous_road == obj.final_road: obj.finished_route = True @@ -843,8 +877,8 @@ def synchronize_clients(self, obj: Object | list[Object] = None): obj.previous_road = roadID bearing = get_metsr_rotation(obj.carlaActor.get_transform().rotation.yaw) # Check if objects are desynchronized - if not math.isclose(loc.x, veh_data["DATA"][0]['x']) or not math.isclose(-loc.y, veh_data["DATA"][0]['y']): - self.metsr_client.teleport_cosim_vehicle(vehID, loc.x, -loc.y, bearing=bearing,private_veh = True, transform_coords = True) + if not math.isclose(loc.x, veh_data['x']) or not math.isclose(-loc.y, veh_data['y']): + self.metsr_client.teleport_cosim_vehicle(vehID, loc.x, -loc.y, bearing=bearing, private_veh = True, transform_coords = True) @@ -861,19 +895,26 @@ def update_carla_lanes(self, bubble_regions : list[Object] | None = None) -> tup if bubble_regions is None: # Collect lanes intersecting the bubble lanes = self.get_carla_lanes() - self.frozen_scenic_lanes = lanes - # Find the corresponding METSR keys - carla_lane_ids = [self.map_scenic_to_metsr(lane) for lane in lanes] - # scenic_lane_ids = [lane.road for lane in lanes] - carla_lane_ids = set(carla_lane_ids) - # Lanes which are already set - curr_frozen_ids = list(self.carla_control_roads.keys()) - - # Collect new and old lanes - new_lanes = [id for id in carla_lane_ids if id not in curr_frozen_ids] - old_lanes = [id for id in list(self.carla_control_roads.keys()) if id not in carla_lane_ids] - - # Update object existance based on bubble changes + else: + lanes = [] + for region in bubble_regions: + region_lanes = self.get_carla_lanes(bubble_region=region) + lanes.extend(region_lanes) + + self.frozen_scenic_lanes = lanes + # Find the corresponding METSR keys + carla_lane_ids = [self.map_scenic_to_metsr(lane) for lane in lanes] + # scenic_lane_ids = [lane.road for lane in lanes] + carla_lane_ids = set(carla_lane_ids) + # Lanes which are already set + curr_frozen_ids = list(self.carla_control_roads.keys()) + + # Collect new and old lanes + new_lanes = [id for id in carla_lane_ids if id not in curr_frozen_ids] + old_lanes = [id for id in list(self.carla_control_roads.keys()) if id not in carla_lane_ids] + + + # Update object existance based on bubble changes return lanes, new_lanes, old_lanes @@ -897,36 +938,38 @@ def update_bubble_objects(self, carla_lanes: list[Lane], new_lanes: list[str], i (iii) an equivalent Scenic trajectory can be constructed from the metsr proposed route """ - carla_actors = [obj for obj in self.objects if obj.carla_actor_flag] # TODO might need to consider a more efficient data structure here + cosim_data = self.metsr_client.query_coSimVehicle() + + # Query all vehicles --excluding the ego-- at once + all_veh_data = self._collect_metsr_vehicle_data(self.objects[1:]) for obj in self.objects[1:]: - veh_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True) # + veh_data = all_veh_data[obj] + + if not obj.carla_actor_flag: # Check this before collecting lane data to avoid slowdown + if 'dist' not in veh_data or obj.finished_route: + continue + lane = self._nearest_lane(obj) - intersection = obj._intersection - # Remove objects if they have completed their route or left the bubble region + intersection = obj._intersection + if obj.carla_actor_flag: if obj.finished_route: - carla_actors.remove(obj) self.remove_bubble_object(obj) elif (lane not in carla_lanes) and (intersection not in intersections): if obj.spawn_guard == 0: self.remove_bubble_object(obj) - carla_actors.remove(obj) # Skip spawning objects if there is not enough space or they are currently in a spawn queue - elif not obj.carla_actor_flag and (lane in carla_lanes or intersection in intersections): - if 'dist' not in veh_data["DATA"][0] or obj.finished_route: - continue - - not_enough_space = _utils.within_threshold_to(obj, carla_actors) + elif not obj.carla_actor_flag and (lane in carla_lanes or intersection in intersections): + not_enough_space = _utils.within_threshold_to(obj, self.carla_actors) if not_enough_space: if obj not in self.bubble_spawn_queue: - _utils.within_threshold_to(obj,carla_actors, verbose=True) + _utils.within_threshold_to(obj,self.carla_actors, verbose=True) self.bubble_spawn_queue.add(obj) - # self.check_world_state_consistency() continue @@ -951,7 +994,8 @@ def update_bubble_objects(self, carla_lanes: list[Lane], new_lanes: list[str], i self.createObjectInCarla(obj,update_orientation=True, trajectory=carla_trajectory) if obj in self.bubble_spawn_queue: self.bubble_spawn_queue.remove(obj) - carla_actors.append(obj) + self.carla_actors.append(obj) + self.metsr_actors.remove(obj) @@ -985,14 +1029,12 @@ def freeze_lanes(self, keys: list[str]) -> None: :type keys: list[str] Query Metsr to freeze simulation and control of given lanes - """ keys = set(keys) for key in keys: assert key not in self.carla_control_roads, "Attempted to freeze already frozen lane" if key not in self.intersection_road_links: # Skip roads not recognized by metsr self.carla_control_roads[key] = True # Keep track of frozen lanes - # print(f"Freezing: {key}") self.metsr_client.set_cosim_road(key) @@ -1011,7 +1053,6 @@ def release_lanes(self,keys: list[str]) -> None: if key not in self.intersection_road_links: # Skip roads not recognized by metsr del self.carla_control_roads[key] # Remove frozen lane from record self.metsr_client.release_cosim_road(key) - # print(f"Releasing: {key}") def destroy_carla_obj(self,obj) -> None: @@ -1041,6 +1082,8 @@ def remove_bubble_object(self,obj) -> None: obj.carla_actor_flag = False self.destroy_carla_obj(obj) obj.trajectory = None + self.carla_actors.remove(obj) + self.metsr_actors.append(obj) def map_scenic_to_metsr(self,lane: Lane) -> str: @@ -1175,7 +1218,7 @@ def destroy(self) -> None: super().destroy() - def _nearest_lane(self,obj, allow_offlane=True) -> Lane | None : # TODO :: Update lane logic to consider intersections + def _nearest_lane(self,obj, allow_offlane : bool =True, radius_size : int = 20) -> Lane | None : # TODO :: Update lane logic to consider intersections """ Docstring for _nearest_lane @@ -1190,10 +1233,18 @@ def _nearest_lane(self,obj, allow_offlane=True) -> Lane | None : # TODO :: Updat else: if not allow_offlane: assert True, f"Object: {obj.name} is has left the roadway" - lanes = [*self.workspace.network.lanes] - distances = [(lane.distanceTo(obj.position), lane) for lane in lanes] + + neighborhood = CircularRegion(center=[obj.x, + obj.y], + radius=radius_size) + distances = [] + for lane in self.network_lanes: + if neighborhood.intersects(lane): + distances.append((lane.distanceTo(obj.position), lane)) + + assert len(distances) > 0, f"Car has deviated too far from roadway" nearest_lane = min(distances, key=lambda t: t[0])[1] # min distance over all lanes - + return nearest_lane @@ -1228,15 +1279,29 @@ def updateObjects(self) -> None: Update object properties for METSR simulated objects """ - metsr_obj = [obj for obj in self.objects if not obj.carla_actor_flag] - metsr_obj.append(self.objects[0]) - obj_veh_ids = [self.getMetsrPrivateVehId(obj) for obj in self.objects] - raw_veh_data = self.metsr_client.query_vehicle(obj_veh_ids, True, True) - self.obj_data_cache = {obj: raw_veh_data['DATA'][i] for i, obj in enumerate(self.objects)} - + self.obj_data_cache = self._collect_metsr_vehicle_data(self.metsr_actors) super().updateObjects() self.obj_data_cache = None + def _collect_metsr_vehicle_data(self, objects: list[Object] | None = None): + """ + Docstring for _collect_metsr_vehicle_data + + :param objects: List of objects which data should be queried + :rtype objects: Scenic vehicle object + """ + if objects is None: # all objects by defualt + obj_veh_ids = [self.getMetsrPrivateVehId(obj) for obj in self.objects] + raw_veh_data = self.metsr_client.query_vehicle(obj_veh_ids, True, True) + all_veh_data = {obj: raw_veh_data['DATA'][i] for i, obj in enumerate(self.objects)} + else: + obj_veh_ids = [self.getMetsrPrivateVehId(obj) for obj in objects] + raw_veh_data = self.metsr_client.query_vehicle(obj_veh_ids, True, True) + all_veh_data = {obj: raw_veh_data['DATA'][i] for i, obj in enumerate(objects)} + + return all_veh_data + + def check_world_state_consistency(self) -> None: """ Docstring for check_world_state_consistency From d23d3f3e76a289864cfae743880aeab17be72022 Mon Sep 17 00:00:00 2001 From: Kay Date: Thu, 2 Apr 2026 13:00:19 -0700 Subject: [PATCH 65/73] dynamic bubble region --- src/scenic/simulators/cosim/model.scenic | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/scenic/simulators/cosim/model.scenic b/src/scenic/simulators/cosim/model.scenic index 486956c5c..709fb6d43 100644 --- a/src/scenic/simulators/cosim/model.scenic +++ b/src/scenic/simulators/cosim/model.scenic @@ -19,6 +19,7 @@ param carla_port = 2000 param metsr_map = "Data.properties.CARLA" param timestep = 0.1 param snapToGroundDefault = is2DMode() +param bubble_size = 50 simulator CosimSimulator( @@ -31,7 +32,7 @@ simulator CosimSimulator( xml_map = globalParameters.xml_path, map_path = globalParameters.map, timestep = globalParameters.timestep, - bubble_size = 100 + bubble_size = globalParameters.bubble_size ) param startTime = 6*60*60 @@ -99,7 +100,7 @@ def currentTOD(): return (simulation().currentTime * simulation().timestep + globalParameters.startTime)%_DAY_MOD -scenario GeneratePrivateTrip(origin,destination, name=None): +scenario GeneratePrivateTrip(origin, destination, name=None): if name != None: new NPCCar with origin origin, with destination destination, with name name else: From 908d17d847609e235b91d3480fa9095d394fb98c Mon Sep 17 00:00:00 2001 From: Kay Date: Tue, 7 Apr 2026 17:48:50 -0700 Subject: [PATCH 66/73] merged in client updates --- src/scenic/simulators/metsr/client.py | 63 +++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/src/scenic/simulators/metsr/client.py b/src/scenic/simulators/metsr/client.py index 3ed382993..176eac33f 100644 --- a/src/scenic/simulators/metsr/client.py +++ b/src/scenic/simulators/metsr/client.py @@ -5,6 +5,7 @@ from websockets.sync.client import connect +import networkx as nx """ @@ -322,6 +323,29 @@ def query_route(self, orig_x, orig_y, dest_x, dest_y, transform_coords = False): assert res["TYPE"] == "ANS_routesBwCoords", res["TYPE"] return res + # query K shortest paths between coordinates + def query_k_routes(self, orig_x, orig_y, dest_x, dest_y, k, transform_coords = False): + msg = {"TYPE": "QUERY_multiRoutesBwCoords", "DATA": []} + if not isinstance(orig_x, list): + orig_x = [orig_x] + orig_y = [orig_y] + dest_x = [dest_x] + dest_y = [dest_y] + k = [k] + if not isinstance(transform_coords, list): + transform_coords = [transform_coords] * len(orig_x) + if not isinstance(k, list): + k = [k] * len(orig_x) + + assert len(orig_x) == len(orig_y) == len(dest_x) == len(dest_y), "Length of orig_x, orig_y, dest_x, and dest_y must be the same" + + for orig_x, orig_y, dest_x, dest_y, transform_coord, k in zip(orig_x, orig_y, dest_x, dest_y, transform_coords, k): + msg["DATA"].append({"origX": orig_x, "origY": orig_y, "destX": dest_x, "destY": dest_y, "transformCoord": transform_coord, "K": k}) + + res = self.send_receive_msg(msg, ignore_heartbeats=True) + assert res["TYPE"] == "ANS_kRoutes", res["TYPE"] + return res + # query route between roads def query_route_between_roads(self, orig_road, dest_road): msg = {"TYPE": "QUERY_routesBwRoads", "DATA": []} @@ -339,6 +363,25 @@ def query_route_between_roads(self, orig_road, dest_road): assert res["TYPE"] == "ANS_routesBwRoads", res["TYPE"] return res + + # query K shortest paths between roads + def query_k_routes_between_roads(self, orig_road, dest_road, k): + msg = {"TYPE": "QUERY_multiRoutesBwRoads", "DATA": []} + if not isinstance(orig_road, list): + orig_road = [orig_road] + dest_road = [dest_road] + k = [k] + if not isinstance(k, list): + k = [k] * len(orig_road) + + assert len(orig_road) == len(dest_road), "Length of orig_road and dest_road must be the same" + + for orig_road, dest_road, k in zip(orig_road, dest_road, k): + msg["DATA"].append({"orig": orig_road, "dest": dest_road, "K": k}) + + res = self.send_receive_msg(msg, ignore_heartbeats=True) + assert res["TYPE"] == "ANS_multiRoutesBwRoads", res["TYPE"] + return res # query road weights in the routing map def query_road_weights(self, roadID = None): @@ -378,6 +421,26 @@ def query_route_bus(self, routeID = None): res = self.send_receive_msg(msg, ignore_heartbeats=True) assert res["TYPE"] == "ANS_busWithRoute", res["TYPE"] return res + + # query the entire routing graph, return a networkx graph without edge weights + def query_routing_graph(self): + # Step 1: get all road IDs by querying without arguments + all_roads_res = self.query_road() + road_ids = all_roads_res['orig_id'] + + # Step 2: query road details in batches of 10 and build the graph + graph = nx.DiGraph() + batch_size = 10 + for batch_start in range(0, len(road_ids), batch_size): + batch = road_ids[batch_start : batch_start + batch_size] + res = self.query_road(id=batch) + for road in res['DATA']: + src = road['ID'] + graph.add_node(src, length=road['length'], speed_limit=road['speed_limit'], r_type=road['r_type']) + for dst in road['down_stream_road']: + graph.add_edge(src, dst) + + return graph # CONTROL: change the state of the simulator # generate a vehicle trip between origin and destination zones From 2386eeadce5bb5cbf1dbab2cfcd01fec6e64132d Mon Sep 17 00:00:00 2001 From: Kay Date: Tue, 7 Apr 2026 17:51:59 -0700 Subject: [PATCH 67/73] fixed property updates, added lane/intersection caching, updated bubble spawn logic --- src/scenic/simulators/cosim/simulator.py | 269 +++++++++++++++-------- 1 file changed, 175 insertions(+), 94 deletions(-) diff --git a/src/scenic/simulators/cosim/simulator.py b/src/scenic/simulators/cosim/simulator.py index d5e5e5e2a..9d1a60472 100644 --- a/src/scenic/simulators/cosim/simulator.py +++ b/src/scenic/simulators/cosim/simulator.py @@ -182,6 +182,8 @@ def __init__(self, scene, carla_client, metsr_client, sim_timestep, tm, render , self.metsr_actors = [] self.carla_actors = [] self.bubble_sizes = [] + self.completed_routes = 0 + self.obj_lane_cache = {} super().__init__(scene, timestep=sim_timestep, **kwargs) @@ -195,6 +197,7 @@ def setup(self) -> None: """ self.metsr_client.reset() # Updated version takes no arguements + # Filter out roads from XML which METSR does not use internally valid_metsr_roads = self.metsr_client.query_road() generated_roads = [value.split('_')[0] for value in self.scenic_to_metsr_map.values()] @@ -263,6 +266,23 @@ def setup(self) -> None: self.synchronize_clients() self.network_lanes = [*self.workspace.network.lanes] + + self.connected_lanes_to_intersections = {} + # parse the map to determine which lanes are connected to intersections so speed up lookup + #TODO Should probably make this its own function? + for intersection in [*self.workspace.network.intersections]: + for lane in intersection.incomingLanes: + if lane not in self.connected_lanes_to_intersections: + self.connected_lanes_to_intersections[lane] = [intersection] + else: + self.connected_lanes_to_intersections[lane].append(intersection) + for lane in intersection.outgoingLanes: + if lane not in self.connected_lanes_to_intersections: + self.connected_lanes_to_intersections[lane] = [intersection] + else: + self.connected_lanes_to_intersections[lane].append(intersection) + + # TODO Waiting for map update # self._synchronize_signals() @@ -293,7 +313,7 @@ def createObjectInMetsr(self, obj: Object, origin: int = None, destination: int self.metsr_client.generate_trip(**call_kwargs) - def createObjectInCarla(self, obj: Object, update_orientation: bool = False, trajectory: list[carla.Transform] = None) -> None: + def createObjectInCarla(self, obj: Object, update_orientation: bool = False, trajectory: list[carla.Transform] = None, metsr_data: dict = None) -> None: """ Docstring for createObjectInCarla @@ -358,7 +378,7 @@ def createObjectInCarla(self, obj: Object, update_orientation: bool = False, tra carlaActor = self.carla_world.spawn_actor(blueprint, transform) except Exception as e: print(f"Error: {e} occured \n displaying object positions") - print(f"Failed to spawn in position: {obj.position}") + print(f"Failed to spawn {obj.name} in position: {obj.position}") car_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True) print(f"Metsr data for failed spawn: {car_data}") @@ -380,6 +400,7 @@ def createObjectInCarla(self, obj: Object, update_orientation: bool = False, tra print(f"Checking distance function: {_utils.within_threshold_to(obj,[obj for obj in self.objects if obj.carla_actor_flag], verbose=True)}") print(f"Issue occured at timestep: {self.count}") + return False # raise e(f"Error : {e} occured") if carlaActor is None: @@ -477,7 +498,7 @@ def spawn_ego(self,obj: Object) -> None: _, new_lanes, _ = self.update_carla_lanes(bubble_regions=[obj.bubble]) # TODO bad function name self.freeze_lanes(new_lanes) # Freeze lanes according to Ego Spawn - for _ in range(1): # Allow ego to enger + for _ in range(1): # Allow ego to enter roadway self.metsr_client.tick() if hasattr(obj, "trajectory"): @@ -486,12 +507,12 @@ def spawn_ego(self,obj: Object) -> None: elif not hasattr(obj, "behavior"): cosim_data = self.metsr_client.query_coSimVehicle() - # print(f"Attemping to find ego data in : {cosim_data}") + print(f"Cosim Data : {cosim_data}") VehID = self.getMetsrPrivateVehId(obj) for entry in cosim_data["DATA"]: if entry['ID'] == VehID: route_data = entry['route'] - trajectory = self.generate_scenic_trajectory(lane, route_data, obj._intersection) + trajectory = self.generate_scenic_trajectory(lane, route_data) if trajectory != None: obj.final_road = None trajectory = self.scenic_trajectory_to_carla(trajectory) @@ -551,13 +572,18 @@ def getMetsrProperties(self, obj: object, properties : dict) -> dict[str, float return objects properties from the METSR simulator """ - if obj in self.frozen_vehicles: - return None - raw_data = self.obj_data_cache[obj] + # check vehicle state + is_frozen = bool("road" not in raw_data and raw_data['state'] <= 0) + if obj in self.frozen_vehicles and is_frozen: + # print(f'Obj is frozen: {obj.name}, skipping properties update') + return None # skip update if frozen for more than 1 step - if "road" not in raw_data and raw_data["state"] <=0: + if is_frozen: # update froozen vehicles self.frozen_vehicles.add(obj) + else: + if obj in self.frozen_vehicles: + self.frozen_vehicles.remove(obj) position = Vector(raw_data["x"], raw_data["y"], 0) speed = raw_data["speed"] @@ -642,10 +668,9 @@ def step(self) -> None: # Metsr step self.bubble_sizes.append(len(self.carla_actors)) self.count += 1 - if self.count % 50 == 0: - print(f"Step: {self.count}") - print(f"Total actors in simulation: {len(self.objects)}") - print(f"Average bubble actors is: {np.mean(self.bubble_sizes)}") + # if self.count % 100 == 0: + # print(f"Step: {self.count}. Total actors in simulation: {len(self.objects)}") + # print(f"Vehicles waiting for sufficient space or trajectories: {len(self.bubble_spawn_queue)}") self.metsr_client.tick() # Generate bubble region based on ego objects TODO update logic for multiple high-fidelity zones @@ -655,8 +680,9 @@ def step(self) -> None: if self.objects[0].interrupt: _utils.disable_carla_autopilot(self.objects[0]) - # if self.count % 10 == 0: - # self._check_traffic_light_consistency() + # if self.count % 100 == 0: + # # print(f"Objects waiting to spawn: {len(self.bubble_spawn_queue)}") + # print(f"Objects which are spawned in CARLA: {len(self.carla_actors)} METSR: {len(self.metsr_actors)}") def get_carla_lanes(self,bubble_region=None) -> list[Lane]: @@ -675,9 +701,10 @@ def get_carla_lanes(self,bubble_region=None) -> list[Lane]: # Collect lanes which intersect bubble carla_lanes = [] - for lane in self.workspace.network.lanes: + for lane in self.network_lanes: if lane.intersects(bubble_region): - carla_lanes.append(lane) + carla_lanes.append(lane) + return carla_lanes @@ -854,7 +881,7 @@ def synchronize_clients(self, obj: Object | list[Object] = None): all_veh_data = self._collect_metsr_vehicle_data(self.carla_actors) for obj in carla_actors: - if obj.carlaActor is None: + if not hasattr(obj, "carlaActor"): print(f"Failure identifying carlaActor for obj: {obj.name} at step {self.count}") continue loc = obj.carlaActor.get_location() @@ -943,29 +970,40 @@ def update_bubble_objects(self, carla_lanes: list[Lane], new_lanes: list[str], i # Query all vehicles --excluding the ego-- at once all_veh_data = self._collect_metsr_vehicle_data(self.objects[1:]) - + non_metsr_spawned_vehicles=0 + for obj in self.objects[1:]: veh_data = all_veh_data[obj] - - if not obj.carla_actor_flag: # Check this before collecting lane data to avoid slowdown - if 'dist' not in veh_data or obj.finished_route: - continue - lane = self._nearest_lane(obj) - intersection = obj._intersection + # Check conditions that do not require lane lookup first + if obj.finished_route and obj.carla_actor_flag: # Remove vehicles which have finished route + self.remove_bubble_object(obj) + continue - if obj.carla_actor_flag: - if obj.finished_route: - self.remove_bubble_object(obj) + if ('road' not in veh_data and veh_data['state'] <= 0) or obj.finished_route: # Skip vehicles which are still in queue + continue - elif (lane not in carla_lanes) and (intersection not in intersections): + if not math.isclose(obj.position.x, veh_data['x']) or not math.isclose(obj.position.y, veh_data['y']): # enforce world state consistency + obj.position = Vector(veh_data["x"], veh_data["y"], 0) + + lane = self._nearest_lane(obj) + intersection = self._get_intersection(obj, lane, intersections) + + if (lane not in carla_lanes) and (intersection not in intersections): # Remove objects which have left the bubble + if obj.carla_actor_flag: if obj.spawn_guard == 0: self.remove_bubble_object(obj) + continue + else: + if obj in self.bubble_spawn_queue: # Update spawn queue if region changes + self.bubble_spawn_queue.remove(obj) + continue + # Skip spawning objects if there is not enough space or they are currently in a spawn queue - elif not obj.carla_actor_flag and (lane in carla_lanes or intersection in intersections): - not_enough_space = _utils.within_threshold_to(obj, self.carla_actors) + elif not obj.carla_actor_flag and (lane in carla_lanes or intersection in intersections): + not_enough_space = _utils.within_threshold_to(obj, self.carla_actors,verbose=False) if not_enough_space: if obj not in self.bubble_spawn_queue: _utils.within_threshold_to(obj,self.carla_actors, verbose=True) @@ -973,30 +1011,39 @@ def update_bubble_objects(self, carla_lanes: list[Lane], new_lanes: list[str], i continue - # If their is enough room check that the vehicle is not currently queued and spawn + # If their is enough room generate the vehicles corresponding Scenic trajectory and spawn it else: carla_trajectory, route_data = None, None - VehID = self.getMetsrPrivateVehId(obj) + VehID = self.getMetsrPrivateVehId(obj) for data_entry in cosim_data['DATA']: if data_entry['ID'] == VehID: route_data = data_entry['route'] - trajectory = self.generate_scenic_trajectory(lane, route_data, obj._intersection) + trajectory = self.generate_scenic_trajectory(lane, route_data) if trajectory != None: obj.final_road = route_data[-1] + # Guarenteed that each vehicles trajectory starts at curent lane carla_trajectory = self.scenic_trajectory_to_carla(trajectory) break # Once the trajectory is found continue - + + if carla_trajectory == None: if obj not in self.bubble_spawn_queue: self.bubble_spawn_queue.add(obj) continue # Do not spawn vehicle if no trajectory can be created - self.createObjectInCarla(obj,update_orientation=True, trajectory=carla_trajectory) + spawn_success = self.createObjectInCarla(obj,update_orientation=True, trajectory=carla_trajectory) + if spawn_success == False: + self.bubble_spawn_queue.add(obj) + continue + if obj in self.bubble_spawn_queue: self.bubble_spawn_queue.remove(obj) + self.carla_actors.append(obj) self.metsr_actors.remove(obj) - + + # if self.count % 100 == 0: + # print(f"Total NON spawned metsr vehicles: {non_metsr_spawned_vehicles - self.completed_routes}, Spawned vehicles: {len(self.objects)-non_metsr_spawned_vehicles}") def scenic_trajectory_to_carla(self, trajectory: list[Lane]) -> list: @@ -1007,17 +1054,21 @@ def scenic_trajectory_to_carla(self, trajectory: list[Lane]) -> list: :type trajectory: list[Lane] Convert a list of scenic lanes to an equivalent sequence of CARLA waypoint locations + + Trajectories starting point must alwasy correspond to the vehicles current road """ way_points = [] - # world = self.carla_client.get_world() - for lane in trajectory: - points = [lane.centerline.start, lane.centerline.end] - for point in points: - scenic_pos = point - carla_rot = _utils.scenicToCarlaRotation(orientation=scenic_pos.orientation) - carla_loc = _utils.scenicToCarlaLocation(pos=scenic_pos) - way_point = carla.Transform(carla_loc, carla_rot) - way_points.append(way_point.location) + for i,lane in enumerate(trajectory): + if i == 0: + points = [lane.centerline.end] + else: + points = [lane.centerline.start, lane.centerline.end] + for point in points: + scenic_pos = point + carla_rot = _utils.scenicToCarlaRotation(orientation=scenic_pos.orientation) + carla_loc = _utils.scenicToCarlaLocation(pos=scenic_pos) + way_point = carla.Transform(carla_loc, carla_rot) + way_points.append(way_point.location) return way_points @@ -1105,15 +1156,11 @@ def map_scenic_to_metsr(self,lane: Lane) -> str: metsr_key = metsr_key.split("_")[0] - # if metsr_key[0] == ":": - # print(f"Original key: {metsr_key}, resultant key: {metsr_key[1:]}") - # metsr_key = metsr_key[1:] - # There must be a valid mapping assert metsr_key is not None, f"Error identifying associated ID for {query_key}" return metsr_key - def generate_scenic_trajectory(self, curr_lane : Lane , route: list[str], intersection: Intersection = None) -> list[Lane]: + def generate_scenic_trajectory(self, curr_lane : Lane , route: list[str]) -> list[Lane]: """ Docstring for generate_scenic_trajectory @@ -1131,48 +1178,38 @@ def generate_scenic_trajectory(self, curr_lane : Lane , route: list[str], inters time a new car is spawned. Also -- I am unsure how to enforce trajectory feasibility at the lane level? """ - target_start = [] - if intersection is not None: - intersection_roads = intersection.roads - for road in intersection_roads: - target_start.append(str(road.id)) + # Enforce that the first trajectory target corresponds to current location + metsr_curr_road = self.map_scenic_to_metsr(curr_lane) + if metsr_curr_road != route[0]: + route.insert(0, metsr_curr_road) # Collect All valid spawn locations map_data = self.scenic_to_metsr_map.items() valid_lanes = {} - for road in route: - for scenic_key, metsr_key in map_data: - key_road = metsr_key.split("_")[0] + for road in route: + for scenic_key, metsr_key in map_data: # Scenic <-> Metsr mappings + key_road = metsr_key.split("_")[0] # Road for road_lane pair if key_road == road: if road not in valid_lanes: valid_lanes[road] = [] - valid_lanes[road].append(scenic_key) - - starting_points = [lane.split("_")[0] for lane in valid_lanes[route[0]]] - target_start = [str(curr_lane.road.id)] - is_valid_trajectory = False + valid_lanes[road].append(scenic_key) - for target in target_start: - if target in starting_points: - is_valid_trajectory = True - break - if not is_valid_trajectory: - return None - + # Search for corresponding lane with correct direction/orientation + # Greedily takes the first lane trajectory = [] - lanes = [*self.workspace.network.lanes] for i,road in enumerate(route): target_lanes = valid_lanes[road] assert len(target_lanes) > 1, f"Failed to find target lanes for road: {road}" for road_lane in target_lanes: - if len(trajectory) == i+1: + if len(trajectory) == i+1: # Break once lane is collected break - for lane in lanes: + for lane in self.network_lanes: scenic_road = f'{lane.road.id}' - query_road = road_lane.split("_")[0] - query_lane = road_lane.split("_")[1] + query_road = road_lane.split("_")[0] # Road Key + query_lane = road_lane.split("_")[1] # Lane Key opposite_traffic_flag = bool(query_lane[0] == "-") - if query_road == scenic_road: + if query_road == scenic_road: # Road Match + # Collect the correct lane on road which matches target directoin if opposite_traffic_flag and str(lane.id)[0] == "-": trajectory.append(lane) break @@ -1181,6 +1218,7 @@ def generate_scenic_trajectory(self, curr_lane : Lane , route: list[str], inters break if len(trajectory) < 1: + print(f'No roads found') return None # print(f"Found trajectory: {[f'{lane.road.id}_{lane.id}' for lane in trajectory]} for route {route}") @@ -1218,34 +1256,77 @@ def destroy(self) -> None: super().destroy() - def _nearest_lane(self,obj, allow_offlane : bool =True, radius_size : int = 20) -> Lane | None : # TODO :: Update lane logic to consider intersections + def _nearest_lane(self,obj, allow_offlane : bool =True, radius_size : int = 40) -> Lane | None : # TODO :: Update lane logic to consider intersections """ Docstring for _nearest_lane Return the nearest lane to the object + (1) Checks obj lane cache for previous lane + (2) Checks connection lanes otherwise + (3) Queries Scenic + (4) If lane is none, selects the closest lane in the neighborhood + around the car defined by the radius parameter TODO: Allow a user specified mode where cars are NOT allowed to leave the road? TODO: Ensure all objects are cars """ - lane = obj._lane - if lane: - nearest_lane = lane - else: - if not allow_offlane: - assert True, f"Object: {obj.name} is has left the roadway" + nearest_lane = None + if obj in self.obj_lane_cache: + lane = self.obj_lane_cache[obj] + if lane.containsPoint(obj.position): + nearest_lane = lane + else: + canidate_lanes = lane.adjacentLanes + if canidate_lanes is not None: + for lane in canidate_lanes: + if lane.containsPoint(obj.position): + nearest_lane = lane + + if nearest_lane is None: + nearest_lane = obj._lane + if nearest_lane is None: + if not allow_offlane: + assert True, f"Object: {obj.name} is has left the roadway" + + neighborhood = CircularRegion(center=[obj.x, + obj.y], + radius=radius_size) + distances = [] + for lane in self.network_lanes: + if neighborhood.intersects(lane): + distances.append((lane.distanceTo(obj.position), lane)) + + assert len(distances) > 0, f"Car has deviated too far from roadway" + nearest_lane = min(distances, key=lambda t: t[0])[1] # min distance over all lanes + + assert nearest_lane is not None + self.obj_lane_cache[obj] = nearest_lane + + return nearest_lane + + def _get_intersection(self, obj: Object, lane: Lane, cosim_intersections: list[Lane], ) -> Intersection: + """ + Docstring for _get_intersection - neighborhood = CircularRegion(center=[obj.x, - obj.y], - radius=radius_size) - distances = [] - for lane in self.network_lanes: - if neighborhood.intersects(lane): - distances.append((lane.distanceTo(obj.position), lane)) + :param obj: Object to identify lane for + :type obj: Vehicle Object + :param lane: Objects current lane in map + :type lane: Lane - assert len(distances) > 0, f"Car has deviated too far from roadway" - nearest_lane = min(distances, key=lambda t: t[0])[1] # min distance over all lanes + Collects the nearest lane for an object from the intersection cache based + on outgoing lanes, if none is found queries Scenic + """ + curr_intersection = None + if lane in self.connected_lanes_to_intersections: + for intersection in self.connected_lanes_to_intersections[lane]: + if intersection in cosim_intersections and intersection.containsPoint(obj.position): + curr_intersection = intersection + break + else: + curr_intersection = obj._intersection - return nearest_lane + return curr_intersection + def executeActions(self, allActions) -> None: From 3fd35acb84dda4c81f6459d87acbce8a66235234 Mon Sep 17 00:00:00 2001 From: Kay Date: Tue, 21 Apr 2026 19:16:18 -0700 Subject: [PATCH 68/73] Special handling for deadlocked vehicles and updates to _nearest_lane for enforcing METSR recognized road selection --- src/scenic/simulators/cosim/simulator.py | 267 ++++++++++++++--------- 1 file changed, 167 insertions(+), 100 deletions(-) diff --git a/src/scenic/simulators/cosim/simulator.py b/src/scenic/simulators/cosim/simulator.py index 9d1a60472..3e8b9bad7 100644 --- a/src/scenic/simulators/cosim/simulator.py +++ b/src/scenic/simulators/cosim/simulator.py @@ -28,6 +28,7 @@ from scenic.simulators.carla.blueprints import oldBlueprintNames from shapely.geometry import Point import re +import random try: @@ -51,8 +52,8 @@ def __init__(self, carla_port=2000, metsr_host="localhost", # Not sure what this actually means here metsr_port=4000, - # timestep=1,# Not entirely sure what the distinction between timestep and sim_timestep is in metsr - timestep=0.1, + timestep=0.1, # Not entirely sure what the distinction between timestep and sim_timestep is in metsr + sim_timestep=0.1, traffic_manager_port=None, timeout=20, verbose=False, @@ -64,8 +65,11 @@ def __init__(self, self.metsr_map_name = metsr_map self.timestep = timestep - self.sim_timestep = timestep + self.sim_timestep = sim_timestep # This should represent the timestep recorded in the METSR config self.map_path = map_path + self.sim_ticks_per = int(round((timestep / sim_timestep))) + assert math.isclose(self.sim_ticks_per, timestep / sim_timestep) + self.bubble_size = bubble_size self.render= render self.record = record @@ -91,7 +95,7 @@ def __init__(self, self.world = self.carla_client.generate_opendrive_world(odr_file.read()) else: raise RuntimeError("CARLA only supports OpenDrive maps") - self.timestep = timestep + if traffic_manager_port is None: traffic_manager_port = carla_port + 6000 assert traffic_manager_port != metsr_port, f"Specified Traffic manager port {traffic_manager_port} is not available" @@ -100,8 +104,9 @@ def __init__(self, settings = self.world.get_settings() settings.synchronous_mode = True - assert timestep <= .1 , f"timestep must be less that 0.1" - settings.fixed_delta_seconds = timestep + print(f"Relaxed timestep restriction for testing?") + assert sim_timestep <= .1 , f"timestep must be less that 0.1" + settings.fixed_delta_seconds = sim_timestep self.world.apply_settings(settings) verbosePrint("Map loaded in simulator.") @@ -114,6 +119,8 @@ def __init__(self, verbosePrint("Clients have successfully been initialized") + print(f"Creating CoSimulator with timestep: {self.timestep} and Ticks per step as: {self.sim_ticks_per}") + def createSimulation(self,scene,*, timestep, **kwargs): #TODO: fix timestep if timestep is not None and timestep != self.timestep: raise RuntimeError( @@ -124,7 +131,8 @@ def createSimulation(self,scene,*, timestep, **kwargs): #TODO: fix timestep scene=scene, carla_client=self.carla_client, metsr_client=self.metsr_client, - sim_timestep=self.sim_timestep, + timestep=self.timestep, + sim_ticks_per=self.sim_ticks_per, tm=self.tm, bubble_size=self.bubble_size, render=self.render, @@ -143,12 +151,13 @@ def destroy(self): self.tm.set_synchronous_mode(False) class CosimSimulation(DrivingSimulation): - def __init__(self, scene, carla_client, metsr_client, sim_timestep, tm, render ,record, mappings, xml_to_xodr_intersections, bubble_size=100, **kwargs ): + def __init__(self, scene, carla_client, metsr_client, timestep, sim_ticks_per, tm, render ,record, mappings, xml_to_xodr_intersections, bubble_size=100, **kwargs ): # Carla and metrs simulators self.carla_client = carla_client self.metsr_client = metsr_client - self.sim_timestep = sim_timestep + self.timestep = timestep # Timestep for each step + self.sim_ticks_per = sim_ticks_per # Initializing CARLA params self.tm = tm # Carla Traffic manager @@ -181,11 +190,15 @@ def __init__(self, scene, carla_client, metsr_client, sim_timestep, tm, render , self.network_lanes = [] self.metsr_actors = [] self.carla_actors = [] - self.bubble_sizes = [] - self.completed_routes = 0 self.obj_lane_cache = {} - super().__init__(scene, timestep=sim_timestep, **kwargs) + # For tracking / data collection + self.bubble_sizes = [] + self.total_active_vehicles = [] + self.routes_completed = 0 + + + super().__init__(scene, timestep=timestep, **kwargs) @@ -197,6 +210,9 @@ def setup(self) -> None: """ self.metsr_client.reset() # Updated version takes no arguements + # Cache lanes before object spawns process + self.network_lanes = [*self.workspace.network.lanes] + # Filter out roads from XML which METSR does not use internally valid_metsr_roads = self.metsr_client.query_road() generated_roads = [value.split('_')[0] for value in self.scenic_to_metsr_map.values()] @@ -264,10 +280,9 @@ def setup(self) -> None: ) self.synchronize_clients() - - self.network_lanes = [*self.workspace.network.lanes] self.connected_lanes_to_intersections = {} + # parse the map to determine which lanes are connected to intersections so speed up lookup #TODO Should probably make this its own function? for intersection in [*self.workspace.network.intersections]: @@ -350,13 +365,12 @@ def createObjectInCarla(self, obj: Object, update_orientation: bool = False, tra # set walker as not invincible if blueprint.has_attribute("is_invincible"): blueprint.set_attribute("is_invincible", "False") - # Set up transform loc = utils.scenicToCarlaLocation( obj.position, world=self.carla_world, blueprint=obj.blueprint, - snapToGround=obj.snapToGround, + snapToGround=obj.snapToGround ) if update_orientation: lane = self._nearest_lane(obj) @@ -377,29 +391,33 @@ def createObjectInCarla(self, obj: Object, update_orientation: bool = False, tra try: carlaActor = self.carla_world.spawn_actor(blueprint, transform) except Exception as e: - print(f"Error: {e} occured \n displaying object positions") - print(f"Failed to spawn {obj.name} in position: {obj.position}") - car_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True) - print(f"Metsr data for failed spawn: {car_data}") - - for car in self.objects: - car_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(car), True, True) - simulator = "carla" if car.carla_actor_flag else "metsr" - if car.carla_actor_flag: - try: - loc = car.carlaActor.get_location() - except Exception as e: - loc = None - - if loc is not None: - print(f"{car} in {simulator}: [Metsr][Scenic][CARLA] coords X: {car_data['DATA'][0]['x'], car.position.x, loc.x}, y:{car_data['DATA'][0]['y'], car.position.y, -loc.y}") - print(f"Query Vehicle results for obj: :{car}, is: {car_data['DATA']}") - else: - print(f"{car} in {simulator}: [Metsr][Scenic] coords X: {car_data['DATA'][0]['x'], car.position.x}, y:{car_data['DATA'][0]['y'], car.position.y}") - print(f"Query Vehicle results for obj: :{car}, is: {car_data['DATA']}") - - print(f"Checking distance function: {_utils.within_threshold_to(obj,[obj for obj in self.objects if obj.carla_actor_flag], verbose=True)}") - print(f"Issue occured at timestep: {self.count}") + # print(f"Error: {e} occured \n displaying object positions") + # print(f"Failed to spawn {obj.name} in position: {obj.position}") + # car_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True) + # print(f"Metsr data for failed spawn: {car_data}") + + # for car in self.objects: + # car_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(car), True, True) + # simulator = "carla" if car.carla_actor_flag else "metsr" + # if car.carla_actor_flag: + # try: + # loc = car.carlaActor.get_location() + # except Exception as e: + # loc = None + + # if loc is not None: + # print(f"{car} in {simulator}: [Metsr][Scenic][CARLA] coords X: {car_data['DATA'][0]['x'], car.position.x, loc.x}, y:{car_data['DATA'][0]['y'], car.position.y, -loc.y}") + # print(f"Query Vehicle results for obj: :{car}, is: {car_data['DATA']}") + # else: + # print(f"{car} in {simulator}: [Metsr][Scenic] coords X: {car_data['DATA'][0]['x'], car.position.x}, y:{car_data['DATA'][0]['y'], car.position.y}") + # print(f"Query Vehicle results for obj: :{car}, is: {car_data['DATA']}") + + # print(f"Checking distance function: {_utils.within_threshold_to(obj,[obj for obj in self.objects if obj.carla_actor_flag], verbose=True)}") + # print(f"Issue occured at timestep: {self.count}") + # print(f"Saving METSR State fro reproducibility") + + + # self._save_metsr_state() return False # raise e(f"Error : {e} occured") @@ -484,8 +502,9 @@ def spawn_ego(self,obj: Object) -> None: (5) Spawn ego inside CARLA with updated loc and trajectory """ trajectory = None - lane = self._nearest_lane(obj) + lane = self._nearest_lane(obj, allow_intersection_links = False) origin_str = self.map_scenic_to_metsr(lane) + self.createObjectInMetsr(obj,origin=origin_str) # Set the METSR vehicle origin to match ego spawn self.metsr_client.tick() # Allow the vehicle to spawn car_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True)["DATA"][0] @@ -498,25 +517,28 @@ def spawn_ego(self,obj: Object) -> None: _, new_lanes, _ = self.update_carla_lanes(bubble_regions=[obj.bubble]) # TODO bad function name self.freeze_lanes(new_lanes) # Freeze lanes according to Ego Spawn - for _ in range(1): # Allow ego to enter roadway - self.metsr_client.tick() - - if hasattr(obj, "trajectory"): + self.metsr_client.tick() + car_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True)["DATA"][0] + + if hasattr(obj, "trajectory"): if obj.trajectory is not None: trajectory = self.scenic_trajectory_to_carla(trajectory) - elif not hasattr(obj, "behavior"): - cosim_data = self.metsr_client.query_coSimVehicle() - print(f"Cosim Data : {cosim_data}") - VehID = self.getMetsrPrivateVehId(obj) - for entry in cosim_data["DATA"]: - if entry['ID'] == VehID: - route_data = entry['route'] - trajectory = self.generate_scenic_trajectory(lane, route_data) - if trajectory != None: - obj.final_road = None - trajectory = self.scenic_trajectory_to_carla(trajectory) - break # Once the trajectory is found continue + elif hasattr(obj, "behavior"): # Default to CARLA autopilot behavior from spawn to random target road + if obj.behavior is None: + for _ in range(len(self.network_lanes)): + choice = random.choice(self.network_lanes) + dest_roadID = self.map_scenic_to_metsr(choice) + if dest_roadID not in self.intersection_road_links and dest_roadID != origin_str: + break + + trajectory_query = self.metsr_client.query_route_between_roads(origin_str,dest_roadID) + if "DATA" in trajectory_query: + trajectory = trajectory_query["DATA"][0] + + scenic_trajectory = self.generate_scenic_trajectory(lane, trajectory["road_list"]) + if scenic_trajectory is not None: + trajectory = self.scenic_trajectory_to_carla(scenic_trajectory) # print(f"Spawning ego with trajectory: {trajectory}") self.createObjectInCarla(obj, update_orientation=True, trajectory=trajectory) # spawn ego in updated location and update orientation @@ -574,9 +596,8 @@ def getMetsrProperties(self, obj: object, properties : dict) -> dict[str, float """ raw_data = self.obj_data_cache[obj] # check vehicle state - is_frozen = bool("road" not in raw_data and raw_data['state'] <= 0) + is_frozen = bool("road" not in raw_data and raw_data["state"] < 0) if obj in self.frozen_vehicles and is_frozen: - # print(f'Obj is frozen: {obj.name}, skipping properties update') return None # skip update if frozen for more than 1 step if is_frozen: # update froozen vehicles @@ -626,6 +647,11 @@ def getProperties(self, obj : Object, properties : dict)-> dict[str, float | Vec def getMetsrPrivateVehId(self, obj: Object) -> int: """ + Docstring for getMetsrPrivateVehId + + :param obj: Cosimulation car object + :type obj: Scenic Object + Return unique vehicle idea Generates a new ID if none exists for vehicle """ @@ -634,6 +660,24 @@ def getMetsrPrivateVehId(self, obj: Object) -> int: self.next_pv_id += 1 return self.pv_id_map[obj] + def tick_carla(self) -> None: + """ + Docstring for tick_carla + + Tick Carla client for a single step + """ + for _ in range(self.sim_ticks_per): + self.carla_world.tick() + + + def tick_metsr(self) -> None: + """ + Docstring for tick_metsr + + Tick Metsr client for a single step + """ + for _ in range(self.sim_ticks_per): + self.metsr_client.tick() def step(self) -> None: """ @@ -653,36 +697,28 @@ def step(self) -> None: self.update_bubble_objects(lanes, new_lanes, intersections) #Carla step - self.carla_world.tick() + self.tick_carla() self.synchronize_clients() - # It takes 2 CARLA ticks to fully instantiate and update properties once spawned in CARLA - for obj in self.objects[1:]: # Skip ego - if obj.spawn_guard > 0: - obj.spawn_guard -= 1 - if self.render: self.cameraManager.render(self.display) pygame.display.flip() # Metsr step self.bubble_sizes.append(len(self.carla_actors)) + self.total_active_vehicles.append(len(self.objects) - (len(self.frozen_vehicles) + len(self.bubble_spawn_queue))) self.count += 1 - # if self.count % 100 == 0: - # print(f"Step: {self.count}. Total actors in simulation: {len(self.objects)}") - # print(f"Vehicles waiting for sufficient space or trajectories: {len(self.bubble_spawn_queue)}") + if self.count % 100 == 0: + print(f"Step: {self.count}. Total actors: {len(self.objects)}, bubble queue:{len(self.bubble_spawn_queue)} ") + print(f"Total active vehicles: {self.total_active_vehicles[-1]}, Routes completed: {self.routes_completed}, frozen vehicles {len(self.frozen_vehicles)}") - self.metsr_client.tick() + self.tick_metsr() # Generate bubble region based on ego objects TODO update logic for multiple high-fidelity zones self.objects[0].bubble = CircularRegion(center=[self.objects[0].x, self.objects[0].y], radius=self.bubble_size) - if self.objects[0].interrupt: + if self.objects[0].interrupt: #TODO support behaviors for objects when inside the bubble _utils.disable_carla_autopilot(self.objects[0]) - - # if self.count % 100 == 0: - # # print(f"Objects waiting to spawn: {len(self.bubble_spawn_queue)}") - # print(f"Objects which are spawned in CARLA: {len(self.carla_actors)} METSR: {len(self.metsr_actors)}") def get_carla_lanes(self,bubble_region=None) -> list[Lane]: @@ -880,14 +916,16 @@ def synchronize_clients(self, obj: Object | list[Object] = None): all_veh_data = self._collect_metsr_vehicle_data(self.carla_actors) - for obj in carla_actors: - if not hasattr(obj, "carlaActor"): - print(f"Failure identifying carlaActor for obj: {obj.name} at step {self.count}") + for obj in carla_actors: # TODO clean up all this special handling as some of these cases are unneccesary + try: + loc = obj.carlaActor.get_location() + except RuntimeError: + print(f"Vehicle {obj.name} removed by CARLA likely due to deadlock") + self.remove_bubble_object(obj, destroy=False) continue - loc = obj.carlaActor.get_location() - if loc == carla.Location(0,0,0): # Carla object still in the process of spawning - # print(f'Passing sychronization while object spawn is processing') + if (loc.x,loc.y,loc.z) == (0,0,0): # Carla object still in the process of spawning continue + vehID = self.getMetsrPrivateVehId(obj) lane = self._nearest_lane(obj) roadID = self.map_scenic_to_metsr(lane) @@ -956,7 +994,7 @@ def update_bubble_objects(self, carla_lanes: list[Lane], new_lanes: list[str], i (A lane must either intersect or have to connecting roads in the cosimulation region) :type intersections: list[intersection] - (1) Remove all objects that form CARLA which have either + (1) Remove all objects from CARLA which have either (i) finished their associated route (ii) left the region (2) Spawn new objects in the Cosimulation region if @@ -965,19 +1003,22 @@ def update_bubble_objects(self, carla_lanes: list[Lane], new_lanes: list[str], i (iii) an equivalent Scenic trajectory can be constructed from the metsr proposed route """ - cosim_data = self.metsr_client.query_coSimVehicle() # Query all vehicles --excluding the ego-- at once all_veh_data = self._collect_metsr_vehicle_data(self.objects[1:]) - non_metsr_spawned_vehicles=0 for obj in self.objects[1:]: veh_data = all_veh_data[obj] + # It takes 2 CARLA ticks to fully instantiate and update properties once spawned in CARLA so maintain a counter to ensure full spawn proceesing before deletion + if obj.spawn_guard > 0: + obj.spawn_guard -= 1 + # Check conditions that do not require lane lookup first if obj.finished_route and obj.carla_actor_flag: # Remove vehicles which have finished route self.remove_bubble_object(obj) + self.routes_completed += 1 continue if ('road' not in veh_data and veh_data['state'] <= 0) or obj.finished_route: # Skip vehicles which are still in queue @@ -1006,7 +1047,7 @@ def update_bubble_objects(self, carla_lanes: list[Lane], new_lanes: list[str], i not_enough_space = _utils.within_threshold_to(obj, self.carla_actors,verbose=False) if not_enough_space: if obj not in self.bubble_spawn_queue: - _utils.within_threshold_to(obj,self.carla_actors, verbose=True) + _utils.within_threshold_to(obj,self.carla_actors, verbose=False) self.bubble_spawn_queue.add(obj) continue @@ -1087,6 +1128,7 @@ def freeze_lanes(self, keys: list[str]) -> None: if key not in self.intersection_road_links: # Skip roads not recognized by metsr self.carla_control_roads[key] = True # Keep track of frozen lanes self.metsr_client.set_cosim_road(key) + # print(F"freezing: {key}") def release_lanes(self,keys: list[str]) -> None: @@ -1123,15 +1165,16 @@ def destroy_carla_obj(self,obj) -> None: obj.carlaActor.destroy() obj.carlaActor = None # Set this to None to prevent reaccess a previously deleted vehicle? - def remove_bubble_object(self,obj) -> None: + def remove_bubble_object(self,obj, destroy=True) -> None: """ Docstring for remove_bubble_object :param obj: object to be deleted :type obj: Car """ + if destroy: + self.destroy_carla_obj(obj) obj.carla_actor_flag = False - self.destroy_carla_obj(obj) obj.trajectory = None self.carla_actors.remove(obj) self.metsr_actors.append(obj) @@ -1194,8 +1237,7 @@ def generate_scenic_trajectory(self, curr_lane : Lane , route: list[str]) -> lis valid_lanes[road] = [] valid_lanes[road].append(scenic_key) - # Search for corresponding lane with correct direction/orientation - # Greedily takes the first lane + # Search for corresponding lane with correct direction/orientation (Greedily takes the first lane) trajectory = [] for i,road in enumerate(route): target_lanes = valid_lanes[road] @@ -1220,9 +1262,7 @@ def generate_scenic_trajectory(self, curr_lane : Lane , route: list[str]) -> lis if len(trajectory) < 1: print(f'No roads found') return None - - # print(f"Found trajectory: {[f'{lane.road.id}_{lane.id}' for lane in trajectory]} for route {route}") - + return trajectory @@ -1241,7 +1281,7 @@ def destroy(self) -> None: print("]") # "CARLA destroy" - for obj in self.objects: + for obj in self.carla_actors: if obj.carlaActor is not None: if isinstance(obj.carlaActor, carla.Vehicle): obj.carlaActor.set_autopilot(False, self.tm.get_port()) @@ -1256,7 +1296,7 @@ def destroy(self) -> None: super().destroy() - def _nearest_lane(self,obj, allow_offlane : bool =True, radius_size : int = 40) -> Lane | None : # TODO :: Update lane logic to consider intersections + def _nearest_lane(self,obj : Object, allow_offlane : bool = True, radius_size : int = 50, allow_intersection_links : bool = True) -> Lane | None : # TODO :: Update lane logic to consider intersections """ Docstring for _nearest_lane @@ -1269,6 +1309,15 @@ def _nearest_lane(self,obj, allow_offlane : bool =True, radius_size : int = 40) TODO: Allow a user specified mode where cars are NOT allowed to leave the road? TODO: Ensure all objects are cars + + :param obj: Scenic Vehicle object to find closest lane for + :type obj: Scenic Object + :param allow_offlane: Flag which denotes whether vehicles should be allowed to deviate from the road + :type allow_offlane: bool + :param radius_size: Region around objects position to search for lanes if none is found + :type radis_size: int + :allow_intersection_links: Flag to allows the selection of non-METSR recognized lanes + :type allow_intersection_links: bool """ nearest_lane = None if obj in self.obj_lane_cache: @@ -1281,25 +1330,38 @@ def _nearest_lane(self,obj, allow_offlane : bool =True, radius_size : int = 40) for lane in canidate_lanes: if lane.containsPoint(obj.position): nearest_lane = lane - - if nearest_lane is None: + + if nearest_lane is None or not allow_intersection_links: + if nearest_lane: + if self.map_scenic_to_metsr(nearest_lane) not in self.intersection_road_links: + self.obj_lane_cache[obj] = nearest_lane + return nearest_lane + nearest_lane = obj._lane if nearest_lane is None: if not allow_offlane: assert True, f"Object: {obj.name} is has left the roadway" - + neighborhood = CircularRegion(center=[obj.x, - obj.y], + obj.y], radius=radius_size) distances = [] for lane in self.network_lanes: if neighborhood.intersects(lane): - distances.append((lane.distanceTo(obj.position), lane)) + distances.append((lane.distanceTo(obj.position), lane)) assert len(distances) > 0, f"Car has deviated too far from roadway" - nearest_lane = min(distances, key=lambda t: t[0])[1] # min distance over all lanes + + if not allow_intersection_links: + for _ in range(len(distances)): + distance, nearest_lane = min(distances, key=lambda t: t[0])[:] # min distance over all lanes + if self.map_scenic_to_metsr(nearest_lane) not in self.intersection_road_links: + break + else: + distances.remove((distance, nearest_lane)) + else: + nearest_lane = min(distances, key=lambda t: t[0])[1] # min distance over all lanes - assert nearest_lane is not None self.obj_lane_cache[obj] = nearest_lane return nearest_lane @@ -1455,7 +1517,12 @@ def _save_metsr_state(self, file_name=None) -> None: Saves metsr state to a file to allow for reproducible replay """ - pass + if file_name == None: + save_file = "metsr_state_at_{self.count}.bin" + else: + save_file = file_name + + self.metsr_client.save(save_file) # def _update_ego_route(self, ): From cebae3aaf6ef57ec3f539258467449c58fd20fa3 Mon Sep 17 00:00:00 2001 From: Kay Date: Thu, 23 Apr 2026 14:34:48 -0700 Subject: [PATCH 69/73] Refactored network management, updated metsr property update logic, cleanred up imports, improved naming conventions, added helpers for road level network calls --- src/scenic/simulators/cosim/simulator.py | 758 ++++++--------------- src/scenic/simulators/cosim/utils/utils.py | 295 +++++++- 2 files changed, 501 insertions(+), 552 deletions(-) diff --git a/src/scenic/simulators/cosim/simulator.py b/src/scenic/simulators/cosim/simulator.py index 3e8b9bad7..eca935b89 100644 --- a/src/scenic/simulators/cosim/simulator.py +++ b/src/scenic/simulators/cosim/simulator.py @@ -2,45 +2,30 @@ from scenic.core.vectors import Orientation, Vector from scenic.syntax.veneer import verbosePrint from scenic.simulators.metsr.client import METSRClient -import scenic.simulators.carla.utils.utils as utils from scenic.simulators.cosim.utils.utils import * from scenic.core.regions import CircularRegion from scenic.core.object_types import Object - -from scenic.domains.driving.roads import Lane, Intersection - -from scenic.simulators.carla.behaviors import * - +from scenic.core.simulators import SimulationCreationError +from scenic.domains.driving.roads import Lane, Intersection, Road +from scenic.domains.driving.simulators import DrivingSimulation, DrivingSimulator import pygame import warnings import os import math -import numpy as np -from scenic.core.simulators import SimulationCreationError import scenic.simulators.cosim.utils.utils as _utils +import scenic.simulators.carla.utils.utils as utils -from scenic.domains.driving.simulators import DrivingSimulation, DrivingSimulator - -from scenic.core.distributions import Uniform import scenic.simulators.carla.utils.visuals as visuals from scenic.simulators.carla.blueprints import oldBlueprintNames -from shapely.geometry import Point -import re import random - try: import carla except ImportError as e: raise ModuleNotFoundError('CARLA scenarios require the "carla" Python package') from e -# def initialize_Carla(map_name=None, map_path=None, address="127.0.0.1",port=2000,timeout=10): -# verbosePrint(f"Connecting to CARLA on port {port}") -# client = carla.Client(address,port) - - class CosimSimulator(DrivingSimulator): def __init__(self, metsr_map, @@ -174,7 +159,6 @@ def __init__(self, scene, carla_client, metsr_client, timestep, sim_ticks_per, t self.pv_id_map = {} self.frozen_vehicles = set() self.scenic_to_metsr_map = mappings - self._client_calls = [] self.count = 0 @@ -182,24 +166,19 @@ def __init__(self, scene, carla_client, metsr_client, timestep, sim_ticks_per, t self.bubble_size = bubble_size self.workspace = scene.workspace self.carla_control_roads = {} - self.queued_vehicles = {} self.bubble_spawn_queue = set({}) - self.frozen_scenic_lanes = [] + self.frozen_scenic_roads = [] self.xml_to_xodr_intersections = xml_to_xodr_intersections - self.intersection_road_links = [] - self.network_lanes = [] self.metsr_actors = [] self.carla_actors = [] - self.obj_lane_cache = {} # For tracking / data collection self.bubble_sizes = [] self.total_active_vehicles = [] self.routes_completed = 0 - - super().__init__(scene, timestep=timestep, **kwargs) + super().__init__(scene, timestep=timestep, **kwargs) def setup(self) -> None: @@ -208,18 +187,13 @@ def setup(self) -> None: Setup the simulation instance """ - self.metsr_client.reset() # Updated version takes no arguements - - # Cache lanes before object spawns process - self.network_lanes = [*self.workspace.network.lanes] - - # Filter out roads from XML which METSR does not use internally + # Updated version takes no arguements + self.metsr_client.reset() valid_metsr_roads = self.metsr_client.query_road() - generated_roads = [value.split('_')[0] for value in self.scenic_to_metsr_map.values()] - for mapping in generated_roads: - if mapping not in valid_metsr_roads["orig_id"]: - self.intersection_road_links.append(mapping) + self.network_helper = network_cache(self.workspace, + self.scenic_to_metsr_map, + valid_metsr_roads) weather = self.scene.params.get("weather") if weather is not None: @@ -280,24 +254,7 @@ def setup(self) -> None: ) self.synchronize_clients() - - self.connected_lanes_to_intersections = {} - - # parse the map to determine which lanes are connected to intersections so speed up lookup - #TODO Should probably make this its own function? - for intersection in [*self.workspace.network.intersections]: - for lane in intersection.incomingLanes: - if lane not in self.connected_lanes_to_intersections: - self.connected_lanes_to_intersections[lane] = [intersection] - else: - self.connected_lanes_to_intersections[lane].append(intersection) - for lane in intersection.outgoingLanes: - if lane not in self.connected_lanes_to_intersections: - self.connected_lanes_to_intersections[lane] = [intersection] - else: - self.connected_lanes_to_intersections[lane].append(intersection) - - + # TODO Waiting for map update # self._synchronize_signals() @@ -385,41 +342,10 @@ def createObjectInCarla(self, obj: Object, update_orientation: bool = False, tra c = obj.color c_str = f"{int(c.r*255)},{int(c.g*255)},{int(c.b*255)}" blueprint.set_attribute("color", c_str) - - # Create Carla actor - # print(f"Spawning actor {obj} in location {loc} with original pos: {obj.position} in CARLA") try: carlaActor = self.carla_world.spawn_actor(blueprint, transform) except Exception as e: - # print(f"Error: {e} occured \n displaying object positions") - # print(f"Failed to spawn {obj.name} in position: {obj.position}") - # car_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True) - # print(f"Metsr data for failed spawn: {car_data}") - - # for car in self.objects: - # car_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(car), True, True) - # simulator = "carla" if car.carla_actor_flag else "metsr" - # if car.carla_actor_flag: - # try: - # loc = car.carlaActor.get_location() - # except Exception as e: - # loc = None - - # if loc is not None: - # print(f"{car} in {simulator}: [Metsr][Scenic][CARLA] coords X: {car_data['DATA'][0]['x'], car.position.x, loc.x}, y:{car_data['DATA'][0]['y'], car.position.y, -loc.y}") - # print(f"Query Vehicle results for obj: :{car}, is: {car_data['DATA']}") - # else: - # print(f"{car} in {simulator}: [Metsr][Scenic] coords X: {car_data['DATA'][0]['x'], car.position.x}, y:{car_data['DATA'][0]['y'], car.position.y}") - # print(f"Query Vehicle results for obj: :{car}, is: {car_data['DATA']}") - - # print(f"Checking distance function: {_utils.within_threshold_to(obj,[obj for obj in self.objects if obj.carla_actor_flag], verbose=True)}") - # print(f"Issue occured at timestep: {self.count}") - # print(f"Saving METSR State fro reproducibility") - - - # self._save_metsr_state() return False - # raise e(f"Error : {e} occured") if carlaActor is None: raise SimulationCreationError(f"Unable to spawn object {obj}") @@ -472,6 +398,7 @@ def createObjectInSimulator(self, obj: Object) -> None: assert hasattr(obj, "carla_actor_flag"), "All objects must have attribute: carla_actor_flag" if obj == self.objects[0]: # Special handling for ego + self.ego = obj self.spawn_ego(obj) self.carla_actors.append(obj) self.metsr_actors.append(obj) @@ -503,7 +430,7 @@ def spawn_ego(self,obj: Object) -> None: """ trajectory = None lane = self._nearest_lane(obj, allow_intersection_links = False) - origin_str = self.map_scenic_to_metsr(lane) + origin_str = self.map_scenic_to_metsr_lanes(lane) self.createObjectInMetsr(obj,origin=origin_str) # Set the METSR vehicle origin to match ego spawn self.metsr_client.tick() # Allow the vehicle to spawn @@ -515,8 +442,9 @@ def spawn_ego(self,obj: Object) -> None: car_data["y"]], radius=self.bubble_size) - _, new_lanes, _ = self.update_carla_lanes(bubble_regions=[obj.bubble]) # TODO bad function name - self.freeze_lanes(new_lanes) # Freeze lanes according to Ego Spawn + bubble_roads = self._get_bubble_roads() + _, new_roads, _ = self.classify_bubble_roads(bubble_roads) + self.freeze_roads(new_roads) # Freeze lanes according to Ego Spawn self.metsr_client.tick() car_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True)["DATA"][0] @@ -526,10 +454,10 @@ def spawn_ego(self,obj: Object) -> None: elif hasattr(obj, "behavior"): # Default to CARLA autopilot behavior from spawn to random target road if obj.behavior is None: - for _ in range(len(self.network_lanes)): - choice = random.choice(self.network_lanes) - dest_roadID = self.map_scenic_to_metsr(choice) - if dest_roadID not in self.intersection_road_links and dest_roadID != origin_str: + for _ in range(len(self.network_helper.network_lanes)): + choice = random.choice(self.network_helper.network_lanes) + dest_roadID = self.map_scenic_to_metsr_lanes(choice) + if dest_roadID not in self.network_helper.intersection_road_links and dest_roadID != origin_str: break trajectory_query = self.metsr_client.query_route_between_roads(origin_str,dest_roadID) @@ -540,7 +468,6 @@ def spawn_ego(self,obj: Object) -> None: if scenic_trajectory is not None: trajectory = self.scenic_trajectory_to_carla(scenic_trajectory) - # print(f"Spawning ego with trajectory: {trajectory}") self.createObjectInCarla(obj, update_orientation=True, trajectory=trajectory) # spawn ego in updated location and update orientation @@ -596,7 +523,7 @@ def getMetsrProperties(self, obj: object, properties : dict) -> dict[str, float """ raw_data = self.obj_data_cache[obj] # check vehicle state - is_frozen = bool("road" not in raw_data and raw_data["state"] < 0) + is_frozen = "road" not in raw_data if obj in self.frozen_vehicles and is_frozen: return None # skip update if frozen for more than 1 step @@ -638,7 +565,6 @@ def getProperties(self, obj : Object, properties : dict)-> dict[str, float | Vec return objects properties for any CoSim object """ assert hasattr(obj, "carla_actor_flag"), f"Object is not assigned properly to a simulator instance" - """ TODO: Sometimes update objects fails on the first step -- Not clear why this occurs -- adding a check before accessing obj.carlaActor """ if obj.carla_actor_flag: properties = self.getCarlaProperties(obj,properties) else: @@ -687,16 +613,20 @@ def step(self) -> None: (1): Update the high fidelity region based on the ego's new locatin (2): Spawn and destroy objects according to region changes (3): Tick both clients and synchronize states - (4): Update spawn protection counters for newly created objects + (4): Compute new bubble region and process behavior interrupts TODO """ - lanes, new_lanes, old_lanes = self.update_carla_lanes() - self.release_lanes(old_lanes) - self.freeze_lanes(new_lanes) - intersections = self.get_carla_intersections() - # Update state of simulator with new bubble region - self.update_bubble_objects(lanes, new_lanes, intersections) - - #Carla step + + # (1) + bubble_roads = self._get_bubble_roads() + roads, new_roads, old_roads = self.classify_bubble_roads(bubble_roads) + self.release_roads(old_roads) + self.freeze_roads(new_roads) + intersections = self.get_bubble_intersections(bubble_roads=roads, bubble_region=self.ego.bubble) + + # (2) + self.update_bubble_objects(roads,intersections) + + # (3) self.tick_carla() self.synchronize_clients() @@ -704,47 +634,24 @@ def step(self) -> None: self.cameraManager.render(self.display) pygame.display.flip() - # Metsr step + self.bubble_sizes.append(len(self.carla_actors)) self.total_active_vehicles.append(len(self.objects) - (len(self.frozen_vehicles) + len(self.bubble_spawn_queue))) self.count += 1 if self.count % 100 == 0: print(f"Step: {self.count}. Total actors: {len(self.objects)}, bubble queue:{len(self.bubble_spawn_queue)} ") print(f"Total active vehicles: {self.total_active_vehicles[-1]}, Routes completed: {self.routes_completed}, frozen vehicles {len(self.frozen_vehicles)}") - self.tick_metsr() - # Generate bubble region based on ego objects TODO update logic for multiple high-fidelity zones - self.objects[0].bubble = CircularRegion(center=[self.objects[0].x, + + # (4) + self.ego.bubble = CircularRegion(center=[self.objects[0].x, self.objects[0].y], radius=self.bubble_size) - if self.objects[0].interrupt: #TODO support behaviors for objects when inside the bubble - _utils.disable_carla_autopilot(self.objects[0]) + if self.ego.interrupt: #TODO support behaviors for objects when inside the bubble + _utils.disable_carla_autopilot(self.ego) - - def get_carla_lanes(self,bubble_region=None) -> list[Lane]: - """ - docstring for get_carla_lanes - - :return: The current set of lanes intersecting the CoSimulation bubble - :rtype: list[Lane] - """ - # Set ego, lane, bubble - if bubble_region == None: - ego = self.objects[0] - bubble_region = ego.bubble - else: - bubble_region = bubble_region - - # Collect lanes which intersect bubble - carla_lanes = [] - for lane in self.network_lanes: - if lane.intersects(bubble_region): - carla_lanes.append(lane) - - - return carla_lanes - def get_carla_intersections(self) -> list[Intersection]: + def get_bubble_intersections(self, bubble_roads: list[Road], bubble_region: CircularRegion) -> list[Intersection]: """ Collect any intersections that are either (1) Intersecting the CoSim bubble @@ -753,33 +660,22 @@ def get_carla_intersections(self) -> list[Intersection]: :return: The set of all intersections meeting the above criteria :rtype: list[Intersection] """ - - carla_intersections = [] - ego = self.objects[0] - - # check if ego in intersection - if ego._intersection: - carla_intersections.append(ego._intersection) - - # Bubble region - bubble_region = ego.bubble - - # Collect intersections which intersect bubble - for intersection in self.workspace.network.intersections: + bubble_intersections = [] + for intersection in self.network_helper.network_intersections: if intersection.intersects(bubble_region): - carla_intersections.append(intersection) + bubble_intersections.append(intersection) continue intersection_roads = intersection.roads count = 0 for road in intersection_roads: - if road in self.frozen_scenic_lanes: + if road in bubble_roads: count += 1 if count > 1: - carla_intersections.append(intersection) + bubble_intersections.append(intersection) break - - return carla_intersections - + return bubble_intersections + + def _synchronize_signals(self) -> None: """ docstring for _synchronize_signals @@ -836,67 +732,6 @@ def _synchronize_signals(self) -> None: self.metsr_client.tick() self.carla_world.tick() - - - def _update_carla_light_state(self, light : carla.TrafficLight, light_config : dict[str: float | carla.libcarla.TrafficLightState]) -> None: - """ - Docstring _update_carla_light_state - - :param light: Single Carla light instance - :type light: Carla Light - :param light_config: dictionary containing new ligh configuration - :type light_config: dict - - Update the state of a light with a specified configuration - """ - light.set_green_time(light_config['green_time']) - light.set_yellow_time(light_config['yellow_time']) - light.set_red_time(light_config['red_time']) - light.set_state(light_config['state']) - - - def _check_light_consistency(self, light : carla.TrafficLight, light_config : dict) -> None: - """ - Checks that a light is configured according to a given config - - :param light: Single Carla light instance - :type light: Carla Light - :param light_config: dictionary containing expected light configuration - :type light_config: dict - """ - light_state_dict = _utils.get_carla_light_state(light) - for key in light_config: - if key in light_state_dict: - if light_config[key] != light_state_dict[key]: - # print(f"Carla: {light_state_dict}, Metsr: {light_config}") - break - # assert light_config[key] == light_state_dict[key], f" Contradicting light states encountered: current light : {light_state_dict} proposed state {light_config}" - else: - assert True, f" Incomptible light states encountered due to key error for key {key}" - - - def get_light_config(self, metsr_light_data : dict) -> dict[str: float]: - """ - Docstring for get_light_config - - Generates the equivalent configuration for a Carla light given a metsr light instance - - :param metsr_light_data: Metsr query result for a single light - :type metsr_light_data: dict - """ - metsr_to_carla_states = {0 : carla.libcarla.TrafficLightState.Green, 1 : carla.libcarla.TrafficLightState.Yellow , 2 : carla.libcarla.TrafficLightState.Red } - - light_config = {} - # Assuming that State is consistent across a group TODO VERIFIY assumption - light_config["green_time"] = metsr_light_data['phase_ticks'][0] * self.timestep - light_config["yellow_time"] = metsr_light_data['phase_ticks'][1] * self.timestep - light_config["red_time"] = metsr_light_data['phase_ticks'][2] * self.timestep - light_config["state"] = metsr_to_carla_states[metsr_light_data['state']] - - self.metsr_client.update_signal(metsr_light_data['ID'], targetPhase=metsr_light_data['state']) - return light_config - - def synchronize_clients(self, obj: Object | list[Object] = None): """ Docstring for synchronize_clients @@ -928,11 +763,11 @@ def synchronize_clients(self, obj: Object | list[Object] = None): vehID = self.getMetsrPrivateVehId(obj) lane = self._nearest_lane(obj) - roadID = self.map_scenic_to_metsr(lane) + roadID = self.map_scenic_to_metsr_lanes(lane) veh_data = all_veh_data[obj] # Update METSR road if hasattr(obj, "previous_road"): - if roadID != obj.previous_road and roadID not in self.intersection_road_links: # Entering new road within metsr network + if roadID != obj.previous_road and roadID not in self.network_helper.intersection_road_links: # Entering new road within metsr network self.metsr_client.enter_next_road(vehID, roadID=roadID, private_veh = True) if obj.previous_road == obj.final_road: obj.finished_route = True @@ -947,49 +782,42 @@ def synchronize_clients(self, obj: Object | list[Object] = None): - def update_carla_lanes(self, bubble_regions : list[Object] | None = None) -> tuple[list[Lane], list[str], list[str]]: #TODO break this out into multiple functions/helpers + def classify_bubble_roads(self, bubble_regions : list[Object] | None = None) -> tuple[list[Road], list[str], list[str]]: """ - Docstring for update_carla_lanes + Docstring for update_carla_roads :param bubble_regions: List of objects with a designated "bubble region" constituting the CoSim region :type obj: List[Object] or None - Collects all lanes which are intersecting the bubble region - (1) Default region is defined by the ego the region can be updated by passing objects with their corresponding regions + Collects all roads which are intersecting the bubble region + (1) Default region is defined by the ego the region can be updated by passing objects with their corresponding regions """ if bubble_regions is None: - # Collect lanes intersecting the bubble - lanes = self.get_carla_lanes() + roads = self._get_bubble_roads() # Collect Road intersecting the bubble else: - lanes = [] + roads = [] for region in bubble_regions: - region_lanes = self.get_carla_lanes(bubble_region=region) - lanes.extend(region_lanes) - - self.frozen_scenic_lanes = lanes - # Find the corresponding METSR keys - carla_lane_ids = [self.map_scenic_to_metsr(lane) for lane in lanes] - # scenic_lane_ids = [lane.road for lane in lanes] - carla_lane_ids = set(carla_lane_ids) - # Lanes which are already set - curr_frozen_ids = list(self.carla_control_roads.keys()) - - # Collect new and old lanes - new_lanes = [id for id in carla_lane_ids if id not in curr_frozen_ids] - old_lanes = [id for id in list(self.carla_control_roads.keys()) if id not in carla_lane_ids] - - - # Update object existance based on bubble changes - return lanes, new_lanes, old_lanes + region_lanes = self._get_bubble_roads(bubble_region=region) + roads.extend(region_lanes) + + self.frozen_metsr_roads = roads + bubble_road_ids = [] + for road in roads: + bubble_road_ids + self.map_scenic_to_metsr_road(road) + self.frozen_roads = list(self.carla_control_roads.keys()) + # Collect roads into new and old for freeze/unfreezing + new_roads = [id for id in bubble_road_ids if id not in self.frozen_roads] + old_roads = [id for id in self.frozen_roads if id not in bubble_road_ids] + return roads, new_roads, old_roads - def update_bubble_objects(self, carla_lanes: list[Lane], new_lanes: list[str], intersections: list[Intersection]) -> None: + def update_bubble_objects(self, bubble_roads: list[Road], intersections: list[Intersection]) -> None: """ Docstring for update_bubble_objects - :param carla_lanes: a list of Scenic lanes which constitute the cosimulation region - :type carla_lanes: list[Lane] + :param bubble_roads: a list of Scenic lanes which constitute the cosimulation region + :type bubble_roads: list[Road] :param intersections: a list of Scenic intersections which are contained or touching the cosimulated region (A lane must either intersect or have to connecting roads in the cosimulation region) :type intersections: list[intersection] @@ -1004,88 +832,65 @@ def update_bubble_objects(self, carla_lanes: list[Lane], new_lanes: list[str], i """ cosim_data = self.metsr_client.query_coSimVehicle() - - # Query all vehicles --excluding the ego-- at once all_veh_data = self._collect_metsr_vehicle_data(self.objects[1:]) for obj in self.objects[1:]: veh_data = all_veh_data[obj] - - # It takes 2 CARLA ticks to fully instantiate and update properties once spawned in CARLA so maintain a counter to ensure full spawn proceesing before deletion - if obj.spawn_guard > 0: - obj.spawn_guard -= 1 - - # Check conditions that do not require lane lookup first - if obj.finished_route and obj.carla_actor_flag: # Remove vehicles which have finished route - self.remove_bubble_object(obj) - self.routes_completed += 1 + + # Skip vehicles which have not entered the roadway or have completed their route + if ('road' not in veh_data) or obj.finished_route: continue - - if ('road' not in veh_data and veh_data['state'] <= 0) or obj.finished_route: # Skip vehicles which are still in queue - continue - - if not math.isclose(obj.position.x, veh_data['x']) or not math.isclose(obj.position.y, veh_data['y']): # enforce world state consistency - obj.position = Vector(veh_data["x"], veh_data["y"], 0) - - lane = self._nearest_lane(obj) - intersection = self._get_intersection(obj, lane, intersections) - - if (lane not in carla_lanes) and (intersection not in intersections): # Remove objects which have left the bubble + road = self._nearest_road(obj) + intersection = self._get_intersection(obj, road) + outside_bubble = (road not in bubble_roads and intersection not in intersections) + + # Spawn guard allows the client to process pending object creation """ + if obj.spawn_guard > 0: + obj.spawn_guard = max(obj.spawn_guard - self.sim_ticks_per, 0) + + # Remove vehicles which have left the cosimulation region and spawn vehicles which have entered + if outside_bubble: if obj.carla_actor_flag: - if obj.spawn_guard == 0: - self.remove_bubble_object(obj) - continue + self.remove_bubble_object(obj) else: - if obj in self.bubble_spawn_queue: # Update spawn queue if region changes + if obj in self.bubble_spawn_queue: self.bubble_spawn_queue.remove(obj) - continue - - - # Skip spawning objects if there is not enough space or they are currently in a spawn queue - elif not obj.carla_actor_flag and (lane in carla_lanes or intersection in intersections): + else: + if not math.isclose(obj.position.x, veh_data['x']) or not math.isclose(obj.position.y, veh_data['y']): # enforce world state consistency + obj.position = Vector(veh_data["x"], veh_data["y"], 0) - not_enough_space = _utils.within_threshold_to(obj, self.carla_actors,verbose=False) - if not_enough_space: - if obj not in self.bubble_spawn_queue: - _utils.within_threshold_to(obj,self.carla_actors, verbose=False) - self.bubble_spawn_queue.add(obj) - continue - - - # If their is enough room generate the vehicles corresponding Scenic trajectory and spawn it - else: - carla_trajectory, route_data = None, None - VehID = self.getMetsrPrivateVehId(obj) - for data_entry in cosim_data['DATA']: - if data_entry['ID'] == VehID: - route_data = data_entry['route'] - trajectory = self.generate_scenic_trajectory(lane, route_data) - if trajectory != None: - obj.final_road = route_data[-1] - # Guarenteed that each vehicles trajectory starts at curent lane - carla_trajectory = self.scenic_trajectory_to_carla(trajectory) - break # Once the trajectory is found continue - - - if carla_trajectory == None: + if not obj.carla_actor_flag: # Vehicle needs to be spawned + not_enough_space = _utils.within_threshold_to(obj, self.carla_actors,verbose=False) + if not_enough_space: # ensure there is sufficient room before spawning if obj not in self.bubble_spawn_queue: + _utils.within_threshold_to(obj,self.carla_actors, verbose=False) self.bubble_spawn_queue.add(obj) - continue # Do not spawn vehicle if no trajectory can be created - - spawn_success = self.createObjectInCarla(obj,update_orientation=True, trajectory=carla_trajectory) - if spawn_success == False: - self.bubble_spawn_queue.add(obj) - continue + continue + else: # spawn the vehicle + carla_trajectory, route_data = None, None + VehID = self.getMetsrPrivateVehId(obj) + curr_lane = self._nearest_lane(obj) + for data_entry in cosim_data['DATA']: + if data_entry['ID'] == VehID: + route_data = data_entry['route'] + trajectory = self.generate_scenic_trajectory(curr_lane, route_data) + if trajectory != None: + obj.final_road = route_data[-1] + carla_trajectory = self.scenic_trajectory_to_carla(trajectory) # Guarenteed that each vehicles trajectory starts at curent lane + break # Once the trajectory is found continue + if carla_trajectory == None: + if obj not in self.bubble_spawn_queue: + self.bubble_spawn_queue.add(obj) + continue # Do not spawn vehicle if no trajectory can be created + spawn_success = self.createObjectInCarla(obj,update_orientation=True, trajectory=carla_trajectory) + if spawn_success == False: + self.bubble_spawn_queue.add(obj) + continue + elif obj in self.bubble_spawn_queue: + self.bubble_spawn_queue.remove(obj) - if obj in self.bubble_spawn_queue: - self.bubble_spawn_queue.remove(obj) - - self.carla_actors.append(obj) - self.metsr_actors.remove(obj) - - # if self.count % 100 == 0: - # print(f"Total NON spawned metsr vehicles: {non_metsr_spawned_vehicles - self.completed_routes}, Spawned vehicles: {len(self.objects)-non_metsr_spawned_vehicles}") - + self.carla_actors.append(obj) + self.metsr_actors.remove(obj) def scenic_trajectory_to_carla(self, trajectory: list[Lane]) -> list: """ @@ -1113,9 +918,9 @@ def scenic_trajectory_to_carla(self, trajectory: list[Lane]) -> list: return way_points - def freeze_lanes(self, keys: list[str]) -> None: + def freeze_roads(self, keys: list[str]) -> None: """ - Docstring for freeze_lanes + Docstring for freeze_roads :param keys: RoadIDs for METSR indexed roads :type keys: list[str] @@ -1125,15 +930,14 @@ def freeze_lanes(self, keys: list[str]) -> None: keys = set(keys) for key in keys: assert key not in self.carla_control_roads, "Attempted to freeze already frozen lane" - if key not in self.intersection_road_links: # Skip roads not recognized by metsr + if key not in self.network_helper.intersection_road_links: # Skip roads not recognized by metsr self.carla_control_roads[key] = True # Keep track of frozen lanes self.metsr_client.set_cosim_road(key) - # print(F"freezing: {key}") - def release_lanes(self,keys: list[str]) -> None: + def release_roads(self,keys: list[str]) -> None: """ - Docstring for release_lanes + Docstring for release_roads :param keys: RoadIDs for METSR indexed roads :type keys: list[str] @@ -1143,7 +947,7 @@ def release_lanes(self,keys: list[str]) -> None: keys = set(keys) for key in keys: assert key in self.carla_control_roads, "Attempted to release non frozen lane" - if key not in self.intersection_road_links: # Skip roads not recognized by metsr + if key not in self.network_helper.intersection_road_links: # Skip roads not recognized by metsr del self.carla_control_roads[key] # Remove frozen lane from record self.metsr_client.release_cosim_road(key) @@ -1177,94 +981,7 @@ def remove_bubble_object(self,obj, destroy=True) -> None: obj.carla_actor_flag = False obj.trajectory = None self.carla_actors.remove(obj) - self.metsr_actors.append(obj) - - - def map_scenic_to_metsr(self,lane: Lane) -> str: - """ - Docstring for map_scenic_to_metsr - - :param lane: Lane object to be mapped - :type lane: Lane - :return: Takes a Lane object and computes the corresponding METSR road which holds that lane - :rtype: str - """ - metsr_key=None - # Parent road key with associated lane id - query_key = f'{lane.road.id}_{lane.id}' - - # Check if element is present in map between formats - if query_key in self.scenic_to_metsr_map: - metsr_key = self.scenic_to_metsr_map[query_key] - - metsr_key = metsr_key.split("_")[0] - - # There must be a valid mapping - assert metsr_key is not None, f"Error identifying associated ID for {query_key}" - return metsr_key - - def generate_scenic_trajectory(self, curr_lane : Lane , route: list[str]) -> list[Lane]: - """ - Docstring for generate_scenic_trajectory - - :param curr_lane: Current lane which the target object is placed on - :type curr_lane: Lane - :param route: Metsr route data for a single car - :type route: list[str] - :param intersection: Current intersection object is on if any - :type intersection: Intersection | None - :return: Equivalent trajetory with Scenic Lanes - :rtype: list[Lane] - - - TODO: This is inefficient I think it will be beforehand generate some of these pairings rather than every - time a new car is spawned. Also -- I am unsure how to enforce trajectory feasibility at the lane level? - - """ - # Enforce that the first trajectory target corresponds to current location - metsr_curr_road = self.map_scenic_to_metsr(curr_lane) - if metsr_curr_road != route[0]: - route.insert(0, metsr_curr_road) - - # Collect All valid spawn locations - map_data = self.scenic_to_metsr_map.items() - valid_lanes = {} - for road in route: - for scenic_key, metsr_key in map_data: # Scenic <-> Metsr mappings - key_road = metsr_key.split("_")[0] # Road for road_lane pair - if key_road == road: - if road not in valid_lanes: - valid_lanes[road] = [] - valid_lanes[road].append(scenic_key) - - # Search for corresponding lane with correct direction/orientation (Greedily takes the first lane) - trajectory = [] - for i,road in enumerate(route): - target_lanes = valid_lanes[road] - assert len(target_lanes) > 1, f"Failed to find target lanes for road: {road}" - for road_lane in target_lanes: - if len(trajectory) == i+1: # Break once lane is collected - break - for lane in self.network_lanes: - scenic_road = f'{lane.road.id}' - query_road = road_lane.split("_")[0] # Road Key - query_lane = road_lane.split("_")[1] # Lane Key - opposite_traffic_flag = bool(query_lane[0] == "-") - if query_road == scenic_road: # Road Match - # Collect the correct lane on road which matches target directoin - if opposite_traffic_flag and str(lane.id)[0] == "-": - trajectory.append(lane) - break - elif not opposite_traffic_flag and not str(lane.id)[0] == "-": - trajectory.append(lane) - break - - if len(trajectory) < 1: - print(f'No roads found') - return None - - return trajectory - + self.metsr_actors.append(obj) def destroy(self) -> None: """ @@ -1295,100 +1012,37 @@ def destroy(self) -> None: self.carla_client.stop_recorder() super().destroy() - - def _nearest_lane(self,obj : Object, allow_offlane : bool = True, radius_size : int = 50, allow_intersection_links : bool = True) -> Lane | None : # TODO :: Update lane logic to consider intersections - """ - Docstring for _nearest_lane - - Return the nearest lane to the object - (1) Checks obj lane cache for previous lane - (2) Checks connection lanes otherwise - (3) Queries Scenic - (4) If lane is none, selects the closest lane in the neighborhood - around the car defined by the radius parameter - - TODO: Allow a user specified mode where cars are NOT allowed to leave the road? - TODO: Ensure all objects are cars - - :param obj: Scenic Vehicle object to find closest lane for - :type obj: Scenic Object - :param allow_offlane: Flag which denotes whether vehicles should be allowed to deviate from the road - :type allow_offlane: bool - :param radius_size: Region around objects position to search for lanes if none is found - :type radis_size: int - :allow_intersection_links: Flag to allows the selection of non-METSR recognized lanes - :type allow_intersection_links: bool - """ - nearest_lane = None - if obj in self.obj_lane_cache: - lane = self.obj_lane_cache[obj] - if lane.containsPoint(obj.position): - nearest_lane = lane - else: - canidate_lanes = lane.adjacentLanes - if canidate_lanes is not None: - for lane in canidate_lanes: - if lane.containsPoint(obj.position): - nearest_lane = lane - - if nearest_lane is None or not allow_intersection_links: - if nearest_lane: - if self.map_scenic_to_metsr(nearest_lane) not in self.intersection_road_links: - self.obj_lane_cache[obj] = nearest_lane - return nearest_lane - - nearest_lane = obj._lane - if nearest_lane is None: - if not allow_offlane: - assert True, f"Object: {obj.name} is has left the roadway" - - neighborhood = CircularRegion(center=[obj.x, - obj.y], - radius=radius_size) - distances = [] - for lane in self.network_lanes: - if neighborhood.intersects(lane): - distances.append((lane.distanceTo(obj.position), lane)) - - assert len(distances) > 0, f"Car has deviated too far from roadway" - - if not allow_intersection_links: - for _ in range(len(distances)): - distance, nearest_lane = min(distances, key=lambda t: t[0])[:] # min distance over all lanes - if self.map_scenic_to_metsr(nearest_lane) not in self.intersection_road_links: - break - else: - distances.remove((distance, nearest_lane)) - else: - nearest_lane = min(distances, key=lambda t: t[0])[1] # min distance over all lanes - - self.obj_lane_cache[obj] = nearest_lane - - return nearest_lane + def map_scenic_to_metsr_road(self, road: Road) -> list[str]: + """Maps Scenic road to equvialent METSR roads, 1->M mapping""" + return self.network_helper.map_scenic_to_metsr_road(road) - def _get_intersection(self, obj: Object, lane: Lane, cosim_intersections: list[Lane], ) -> Intersection: - """ - Docstring for _get_intersection + def map_scenic_to_metsr_lanes(self, lane: Lane) -> str: + """Map Scenic lane to equivalent METSR road, guareneteed 1->1 mapping""" + return self.network_helper.map_scenic_to_metsr_lanes(lane) + + def generate_scenic_trajectory(self, curr_lane : Lane , route: list[str]) -> list[Lane] | None: + """Attempt to translate metsr route to equvialent sequence of Scenic lanes""" + return self.network_helper.generate_scenic_trajectory(curr_lane, route) - :param obj: Object to identify lane for - :type obj: Vehicle Object - :param lane: Objects current lane in map - :type lane: Lane + def _nearest_road(self, obj: Object, allow_offroad: bool = True, radius_size: int = 30) -> tuple[Road, str]: + """Collect the nearest road to obj location""" + return self.network_helper._nearest_road(obj, allow_offroad, radius_size) - Collects the nearest lane for an object from the intersection cache based - on outgoing lanes, if none is found queries Scenic - """ - curr_intersection = None - if lane in self.connected_lanes_to_intersections: - for intersection in self.connected_lanes_to_intersections[lane]: - if intersection in cosim_intersections and intersection.containsPoint(obj.position): - curr_intersection = intersection - break + def _nearest_lane(self,obj : Object, allow_offlane : bool = True, radius_size : int = 50, allow_intersection_links : bool = True) -> Lane: + """Collect the nearest lane to obj location""" + return self.network_helper._nearest_lane(obj, allow_offlane, radius_size, allow_intersection_links) + + def _get_intersection(self, obj: Object, road: Road ) -> Intersection | None: + """Returns the intersection the obj is on if any""" + return self.network_helper._get_intersection(obj, road) + + def _get_bubble_roads(self, bubble_region: CircularRegion | None = None) -> list[Lane]: + """Collect all roads which overlap the designated bubble region""" + if bubble_region == None: # Default is attached to ego + bubble_region = self.ego.bubble else: - curr_intersection = obj._intersection - - return curr_intersection - + bubble_region = bubble_region # User specified (for added functionality later) + return self.network_helper._get_bubble_roads(bubble_region) def executeActions(self, allActions) -> None: @@ -1404,10 +1058,7 @@ def executeActions(self, allActions) -> None: for obj in self.agents: if obj.carla_actor_flag: carla_actions[obj] = allActions[obj] - - super().executeActions(carla_actions) - for obj in self.agents: if obj.carla_actor_flag: ctrl = obj._control @@ -1441,40 +1092,24 @@ def _collect_metsr_vehicle_data(self, objects: list[Object] | None = None): obj_veh_ids = [self.getMetsrPrivateVehId(obj) for obj in objects] raw_veh_data = self.metsr_client.query_vehicle(obj_veh_ids, True, True) all_veh_data = {obj: raw_veh_data['DATA'][i] for i, obj in enumerate(objects)} - return all_veh_data + + def _save_metsr_state(self, file_name=None) -> None: + """ + docstring for _save_metsr_state - def check_world_state_consistency(self) -> None: + Saves metsr state to a file to allow for reproducible replay """ - Docstring for check_world_state_consistency + if file_name == None: + save_file = "metsr_state_at_{self.count}.bin" + else: + save_file = file_name - self: CoSimulation Object + self.metsr_client.save(save_file) - Compares the state of each vehicle in each simulator - Displays any vehicles which are outside of the tolerance threshold - """ - print(f"Displaying in Objects in CARLA with inconsistent state") - for i,obj in enumerate(self.objects): - if obj.carla_actor_flag or i == 0: - car_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True) - metsr_x, metsr_y = car_data['DATA'][0]['x'], car_data['DATA'][0]['y'] - loc = obj.carlaActor.get_location() - carla_x, carla_y = loc.x, loc.y - in_metsr_queue = not bool("dist" in car_data['DATA'][0]) - - if not np.isclose(metsr_x, carla_x): - print(f"Obj: {obj} in metsr queue? {in_metsr_queue}") - print("=" * 25) - lane = obj._lane - if lane: - print(f"OBJ Road: {self.map_scenic_to_metsr(self._nearest_lane(obj))}") - print(f"OBJ X: {obj}| METSR {metsr_x}: CARLA {carla_x}: SCENIC {obj.position[0]}") - if not np.isclose(metsr_y, -carla_y): - print(f"OBJ Y: {obj}| METSR {metsr_y}: CARLA {-carla_y}: SCENIC {obj.position[1]}") - print("=" * 25) - print(f"Finished checking world state") + """ light helpers for added functionality down the road""" def _check_traffic_light_consistency(self): """ @@ -1511,19 +1146,62 @@ def _check_traffic_light_consistency(self): else: assert True, f"Failed to find corresponding intersection for METSR light: {light_id}" - def _save_metsr_state(self, file_name=None) -> None: + + + def _update_carla_light_state(self, light : carla.TrafficLight, light_config : dict[str: float | carla.libcarla.TrafficLightState]) -> None: """ - docstring for _save_metsr_state + Docstring _update_carla_light_state - Saves metsr state to a file to allow for reproducible replay + :param light: Single Carla light instance + :type light: Carla Light + :param light_config: dictionary containing new ligh configuration + :type light_config: dict + + Update the state of a light with a specified configuration """ - if file_name == None: - save_file = "metsr_state_at_{self.count}.bin" - else: - save_file = file_name + light.set_green_time(light_config['green_time']) + light.set_yellow_time(light_config['yellow_time']) + light.set_red_time(light_config['red_time']) + light.set_state(light_config['state']) + + + def _check_light_consistency(self, light : carla.TrafficLight, light_config : dict) -> None: + """ + Checks that a light is configured according to a given config + + :param light: Single Carla light instance + :type light: Carla Light + :param light_config: dictionary containing expected light configuration + :type light_config: dict + """ + light_state_dict = _utils.get_carla_light_state(light) + for key in light_config: + if key in light_state_dict: + if light_config[key] != light_state_dict[key]: + break + else: + assert True, f" Incomptible light states encountered due to key error for key {key}" - self.metsr_client.save(save_file) + + def get_light_config(self, metsr_light_data : dict) -> dict[str: float]: + """ + Docstring for get_light_config + + Generates the equivalent configuration for a Carla light given a metsr light instance + + :param metsr_light_data: Metsr query result for a single light + :type metsr_light_data: dict + """ + metsr_to_carla_states = {0 : carla.libcarla.TrafficLightState.Green, 1 : carla.libcarla.TrafficLightState.Yellow , 2 : carla.libcarla.TrafficLightState.Red } + + light_config = {} + # Assuming that State is consistent across a group TODO VERIFIY assumption + light_config["green_time"] = metsr_light_data['phase_ticks'][0] * self.timestep + light_config["yellow_time"] = metsr_light_data['phase_ticks'][1] * self.timestep + light_config["red_time"] = metsr_light_data['phase_ticks'][2] * self.timestep + light_config["state"] = metsr_to_carla_states[metsr_light_data['state']] + + self.metsr_client.update_signal(metsr_light_data['ID'], targetPhase=metsr_light_data['state']) + return light_config - # def _update_ego_route(self, ): - # ego = self.objects[0] diff --git a/src/scenic/simulators/cosim/utils/utils.py b/src/scenic/simulators/cosim/utils/utils.py index 4fc58c1c8..8d2aaae28 100644 --- a/src/scenic/simulators/cosim/utils/utils.py +++ b/src/scenic/simulators/cosim/utils/utils.py @@ -2,7 +2,283 @@ import os import numpy as np import carla +from scenic.core.regions import CircularRegion +from scenic.domains.driving.roads import Lane, Intersection, Road +from scenic.core.object_types import Object + + +class network_cache(): + def __init__(self, + workspace, + scenic_to_metsr_map_lanes, + metsr_represented_roads, + radius_search_size=30): + + self.workspace = workspace + self.metsr_represented_roads = metsr_represented_roads + self.scenic_to_metsr_map_lanes = scenic_to_metsr_map_lanes + self.radius_search_size=radius_search_size + + self.network_lanes = [*self.workspace.network.lanes] + self.network_roads = [*self.workspace.network.roads] + self.network_intersections = [*self.workspace.network.intersections] + + self.scenic_to_metsr_map_roads = {} + self.intersection_road_links = set([]) + self.populate_scenic_to_metsr_roads() + + self.connected_roads_to_intersections = {} + self.populate_roads_to_intersections() + + self.obj_road_cache = {} + self.obj_lane_cache = {} + + # Initialilzation + + def populate_scenic_to_metsr_roads(self) -> None: + """ + Generate scenic -> METSR mappings for roads + """ + for road,road_map in self.scenic_to_metsr_map_lanes.items(): + scenic_road = road.split("_")[0] + metsr_road = road_map.split("_")[0] + if metsr_road in self.metsr_represented_roads["orig_id"]: + if scenic_road not in self.scenic_to_metsr_map_roads: + self.scenic_to_metsr_map_roads[scenic_road] = [] + self.scenic_to_metsr_map_roads[scenic_road].append(metsr_road) + else: + if metsr_road not in self.scenic_to_metsr_map_roads[scenic_road]: + self.scenic_to_metsr_map_roads[scenic_road].append(metsr_road) + else: + self.intersection_road_links.add(metsr_road) + + def populate_roads_to_intersections(self) -> None: + """ + Populate the dictionary + """ + for intersection in [*self.workspace.network.intersections]: + for lane in intersection.incomingLanes: + if lane.road not in self.connected_roads_to_intersections: + self.connected_roads_to_intersections[lane.road] = [intersection] + else: + self.connected_roads_to_intersections[lane.road].append(intersection) + for lane in intersection.outgoingLanes: + if lane not in self.connected_roads_to_intersections: + self.connected_roads_to_intersections[lane.road] = [intersection] + else: + self.connected_roads_to_intersections[lane.road].append(intersection) + + + """Helpers for generating or collecting map data""" + + def _nearest_lane(self,obj : Object, allow_offlane : bool = True, radius_search_size : int = 50, allow_intersection_links : bool = True) -> Lane | None : + """ + Docstring for _nearest_lane + + Return the nearest lane to the object + (1) Checks obj lane cache for previous lane + (2) Checks connection lanes otherwise + (3) Queries Scenic + (4) If lane is none, selects the closest lane in the neighborhood + around the car defined by the radius parameter + + :param obj: Scenic Vehicle object to find closest lane for + :type obj: Scenic Object + :param allow_offlane: Flag which denotes whether vehicles should be allowed to deviate from the road + :type allow_offlane: bool + :param radius_size: Region around objects position to search for lanes if none is found + :type radis_size: int + :allow_intersection_links: Flag to allows the selection of non-METSR recognized lanes + :type allow_intersection_links: bool + """ + + radius_size = radius_search_size if radius_search_size else self.radius_search_size + + nearest_lane = None + if obj in self.obj_lane_cache: + lane = self.obj_lane_cache[obj] + if lane.containsPoint(obj.position): + nearest_lane = lane + else: + canidate_lanes = lane.adjacentLanes + if canidate_lanes is not None: + for lane in canidate_lanes: + if lane.containsPoint(obj.position): + nearest_lane = lane + + if nearest_lane is None or not allow_intersection_links: + if nearest_lane: + if self.map_scenic_to_metsr_lanes(nearest_lane) not in self.intersection_road_links: + self.obj_lane_cache[obj] = nearest_lane + return nearest_lane + + nearest_lane = obj._lane + if nearest_lane is None: + if not allow_offlane: + assert nearest_lane, f"Object: {obj.name} is has left the roadway" + neighborhood = CircularRegion(center=[obj.x,obj.y],radius=radius_size) + distances = [] + for lane in self.network_lanes: + if neighborhood.intersects(lane): + distances.append((lane.distanceTo(obj.position), lane)) + assert len(distances) > 0, f"Object has deviated to far from the roadway : i.e {radius_size/2} meters" + + if not allow_intersection_links: + for _ in range(len(distances)): + distance, nearest_lane = min(distances, key=lambda t: t[0])[:] # min distance over all lanes + if self.map_scenic_to_metsr_lanes(nearest_lane) not in self.intersection_road_links: + break + else: + distances.remove((distance, nearest_lane)) + else: + nearest_lane = min(distances, key=lambda t: t[0])[1] # min distance over all lanes + + self.obj_lane_cache[obj] = nearest_lane + return nearest_lane + + def _nearest_road(self, obj: Object, allow_offroad : bool, radius_size: int = 30) -> Road: + """ + Docstring for _nearest_road + + :param obj: Object to identify road for + :type obj: Vehicle Object + :param allow_offroad: Flag whether offroad vehicles are allowed in this simulation + :type allow_offroad: Bool + :param radius_size: Radius size in meters of the viable search space for offroad vehicles + :type radius_size: Integer + + Return the nearest road on the map for a given object + """ + nearest_road = None + if obj in self.obj_road_cache: + road = self.obj_road_cache[obj] + if road.containsPoint(obj.position): + nearest_road = road + + if not nearest_road and allow_offroad: + distances = [] + neighborhood = CircularRegion(center=[obj.x,obj.y],radius=radius_size) + for road in self.network_roads: + if neighborhood.intersects(road): + if road.containsPoint(obj.position): + nearest_road = road + break + else: + distances.append((road.distanceTo(obj.position), road)) + if not nearest_road: + if len(distances) > 0: + nearest_road = min(distances, key=lambda t: t[0])[1] # min distance over all lanes + else: + assert f"Object has deviated to far from the roadway : i.e {radius_size/2} meters" + + self.obj_road_cache[obj] = road + return nearest_road + + def _get_intersection(self, obj: Object, road: Road ) -> Intersection | None: + """ + Docstring for _get_intersection + + :param obj: Object to identify lane for + :type obj: Vehicle Object + :param road: Objects current road in map + :type road: riad + + Collects the nearest lane for an object from the intersection cache based + on outgoing roads, if none is found queries Scenic + """ + curr_intersection = None + if road in self.connected_roads_to_intersections: + for intersection in self.connected_roads_to_intersections[road]: + if intersection.containsPoint(obj.position): + curr_intersection = intersection + break + + return curr_intersection + + def _get_bubble_roads(self, bubble_region: CircularRegion) -> list[Road]: + """ + docstring for get_bubble_roads + + :return: The current set of roads intersecting the CoSimulation bubble + :rtype: list[road] + """ + bubble_roads = [] + for road in self.network_roads: + if road.intersects(bubble_region): + bubble_roads.append(road) + return bubble_roads + + + def generate_scenic_trajectory(self, curr_lane, route): + # Enforce that the first trajectory target corresponds to current location + metsr_curr_road = self.map_scenic_to_metsr_lanes(curr_lane) + if metsr_curr_road != route[0]: + route.insert(0, metsr_curr_road) + + # Collect All valid spawn locations + map_data = self.scenic_to_metsr_map_lanes.items() + valid_lanes = {} + for road in route: + for scenic_key, metsr_key in map_data: # Scenic <-> Metsr mappings + key_road = metsr_key.split("_")[0] # Road for road_lane pair + if key_road == road: + if road not in valid_lanes: + valid_lanes[road] = [] + valid_lanes[road].append(scenic_key) + + # Search for corresponding lane with correct direction/orientation (Greedily takes the first lane) + trajectory = [] + for i,road in enumerate(route): + target_lanes = valid_lanes[road] + assert len(target_lanes) > 1, f"Failed to find target lanes for road: {road}" + for road_lane in target_lanes: + if len(trajectory) == i+1: # Break once lane is collected + break + for lane in self.network_lanes: + scenic_road = f'{lane.road.id}' + query_road = road_lane.split("_")[0] # Road Key + query_lane = road_lane.split("_")[1] # Lane Key + opposite_traffic_flag = bool(query_lane[0] == "-") + if query_road == scenic_road: # Road Match + # Collect the correct lane on road which matches target directoin + if opposite_traffic_flag and str(lane.id)[0] == "-": + trajectory.append(lane) + break + elif not opposite_traffic_flag and not str(lane.id)[0] == "-": + trajectory.append(lane) + break + + if len(trajectory) < 1: + print(f'No roads found') + return None + + return trajectory + + """ Translating Scenic -> Metsr representations""" + + def map_scenic_to_metsr_road(self, road): + query_key = f'{road.id}' + metsr_keys= None + + if query_key in self.scenic_to_metsr_map_roads: + metsr_keys = self.scenic_to_metsr_map_roads[query_key] + + assert metsr_keys is not None, f"Error identifying associated ID for {query_key}" + return metsr_keys + + def map_scenic_to_metsr_lanes(self, lane): + metsr_key=None + query_key = f'{lane.road.id}_{lane.id}' + + # Check if element is present in map between formats + if query_key in self.scenic_to_metsr_map_lanes: + metsr_key = self.scenic_to_metsr_map_lanes[query_key] + + metsr_key = metsr_key.split("_")[0] + # There must be a valid mapping + assert metsr_key is not None, f"Error identifying associated ID for {query_key}" + return metsr_key def generate_map(map): try: @@ -79,19 +355,19 @@ def test_mapping(map, test_pairs): def within_threshold_to(object, cars, verbose=False) -> bool: is_close = False - if verbose: - print(f"checking distance between obj: {object} and cars {[(car.name, car.position) for car in cars]}") + # if verbose: + # print(f"checking distance between obj: {object} and cars {[(car.name, car.position) for car in cars]}") object_pos = np.array(object.position) - obj_distances = [] + obj_distances = {} for car in cars: if car != object: threshold = 1.2 * object.length dist = np.linalg.norm(np.array(car.position) - object_pos) if dist < threshold: is_close=True - obj_distances.append([car.name, dist]) - if verbose: - print(f"Distances were: {obj_distances}") + obj_distances[car.name] = dist + if verbose and is_close: + print(f"Min Distance for {object} was: {min(obj_distances.values())}") return is_close def get_metsr_rotation(carla_yaw): @@ -143,12 +419,7 @@ def scenicToCarlaRotation(orientation): yaw, pitch, roll = orientation.r.as_euler("ZXY", degrees=True) yaw = -yaw - 90 return carla.Rotation(pitch=pitch, yaw=yaw, roll=roll) - - - - - - + if __name__ == "__main__": map = "Town01.net.xml" From 66e2b97c2357f5ba73420b3eb4380f6b44f3c11f Mon Sep 17 00:00:00 2001 From: Kay Date: Thu, 23 Apr 2026 14:38:09 -0700 Subject: [PATCH 70/73] fixed trajectory translation bug --- src/scenic/simulators/cosim/simulator.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/scenic/simulators/cosim/simulator.py b/src/scenic/simulators/cosim/simulator.py index eca935b89..31ca27770 100644 --- a/src/scenic/simulators/cosim/simulator.py +++ b/src/scenic/simulators/cosim/simulator.py @@ -641,6 +641,7 @@ def step(self) -> None: if self.count % 100 == 0: print(f"Step: {self.count}. Total actors: {len(self.objects)}, bubble queue:{len(self.bubble_spawn_queue)} ") print(f"Total active vehicles: {self.total_active_vehicles[-1]}, Routes completed: {self.routes_completed}, frozen vehicles {len(self.frozen_vehicles)}") + print(f"Total bubble actors: {len(self.carla_actors) - len(self.bubble_spawn_queue)}") self.tick_metsr() # (4) @@ -803,7 +804,7 @@ def classify_bubble_roads(self, bubble_regions : list[Object] | None = None) -> self.frozen_metsr_roads = roads bubble_road_ids = [] for road in roads: - bubble_road_ids + self.map_scenic_to_metsr_road(road) + bubble_road_ids += self.map_scenic_to_metsr_road(road) self.frozen_roads = list(self.carla_control_roads.keys()) # Collect roads into new and old for freeze/unfreezing new_roads = [id for id in bubble_road_ids if id not in self.frozen_roads] @@ -812,7 +813,7 @@ def classify_bubble_roads(self, bubble_regions : list[Object] | None = None) -> - def update_bubble_objects(self, bubble_roads: list[Road], intersections: list[Intersection]) -> None: + def update_bubble_objects(self, bubble_roads: list[Road], bubble_intersections: list[Intersection]) -> None: """ Docstring for update_bubble_objects @@ -840,9 +841,10 @@ def update_bubble_objects(self, bubble_roads: list[Road], intersections: list[In # Skip vehicles which have not entered the roadway or have completed their route if ('road' not in veh_data) or obj.finished_route: continue + road = self._nearest_road(obj) intersection = self._get_intersection(obj, road) - outside_bubble = (road not in bubble_roads and intersection not in intersections) + outside_bubble = (road not in bubble_roads and intersection not in bubble_intersections) # Spawn guard allows the client to process pending object creation """ if obj.spawn_guard > 0: @@ -869,7 +871,7 @@ def update_bubble_objects(self, bubble_roads: list[Road], intersections: list[In else: # spawn the vehicle carla_trajectory, route_data = None, None VehID = self.getMetsrPrivateVehId(obj) - curr_lane = self._nearest_lane(obj) + curr_lane = self._nearest_lane(obj) for data_entry in cosim_data['DATA']: if data_entry['ID'] == VehID: route_data = data_entry['route'] @@ -882,7 +884,7 @@ def update_bubble_objects(self, bubble_roads: list[Road], intersections: list[In if obj not in self.bubble_spawn_queue: self.bubble_spawn_queue.add(obj) continue # Do not spawn vehicle if no trajectory can be created - spawn_success = self.createObjectInCarla(obj,update_orientation=True, trajectory=carla_trajectory) + spawn_success = self.createObjectInCarla(obj, update_orientation=True, trajectory=carla_trajectory) if spawn_success == False: self.bubble_spawn_queue.add(obj) continue From 1d1ac5eb0622a69213bf697028c2f21618b7a978 Mon Sep 17 00:00:00 2001 From: Kay Date: Fri, 24 Apr 2026 11:20:05 -0700 Subject: [PATCH 71/73] Updated road and intersection selection logic --- src/scenic/simulators/cosim/utils/utils.py | 63 +++++++++++++--------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/src/scenic/simulators/cosim/utils/utils.py b/src/scenic/simulators/cosim/utils/utils.py index 8d2aaae28..ff848782d 100644 --- a/src/scenic/simulators/cosim/utils/utils.py +++ b/src/scenic/simulators/cosim/utils/utils.py @@ -136,7 +136,7 @@ def _nearest_lane(self,obj : Object, allow_offlane : bool = True, radius_search_ self.obj_lane_cache[obj] = nearest_lane return nearest_lane - def _nearest_road(self, obj: Object, allow_offroad : bool, radius_size: int = 30) -> Road: + def _nearest_road(self, obj: Object, allow_offroad : bool, radius_size: int = 50) -> Road: """ Docstring for _nearest_road @@ -154,24 +154,25 @@ def _nearest_road(self, obj: Object, allow_offroad : bool, radius_size: int = 30 road = self.obj_road_cache[obj] if road.containsPoint(obj.position): nearest_road = road - - if not nearest_road and allow_offroad: + else: + nearest_road = obj._road + + print(f"For: {obj.name}, nearest road is: {nearest_road}") + if nearest_road is None and allow_offroad: distances = [] neighborhood = CircularRegion(center=[obj.x,obj.y],radius=radius_size) for road in self.network_roads: if neighborhood.intersects(road): - if road.containsPoint(obj.position): - nearest_road = road - break - else: - distances.append((road.distanceTo(obj.position), road)) - if not nearest_road: - if len(distances) > 0: - nearest_road = min(distances, key=lambda t: t[0])[1] # min distance over all lanes - else: - assert f"Object has deviated to far from the roadway : i.e {radius_size/2} meters" + distances.append((road.distanceTo(obj.position), road)) + + if len(distances) > 0: + nearest_road = min(distances, key=lambda t: t[0])[1] # min distance over all lanes + else: + print(f"No road has been found: {obj.name} at {obj.position}") + + assert nearest_road is not None, f"Object has deviated to far from the roadway : i.e {radius_size/2} meters" - self.obj_road_cache[obj] = road + self.obj_road_cache[obj] = nearest_road return nearest_road def _get_intersection(self, obj: Object, road: Road ) -> Intersection | None: @@ -186,14 +187,7 @@ def _get_intersection(self, obj: Object, road: Road ) -> Intersection | None: Collects the nearest lane for an object from the intersection cache based on outgoing roads, if none is found queries Scenic """ - curr_intersection = None - if road in self.connected_roads_to_intersections: - for intersection in self.connected_roads_to_intersections[road]: - if intersection.containsPoint(obj.position): - curr_intersection = intersection - break - - return curr_intersection + return obj._intersection def _get_bubble_roads(self, bubble_region: CircularRegion) -> list[Road]: """ @@ -210,7 +204,20 @@ def _get_bubble_roads(self, bubble_region: CircularRegion) -> list[Road]: - def generate_scenic_trajectory(self, curr_lane, route): + def generate_scenic_trajectory(self, curr_lane: Lane, route: list[str] ) -> None | list[Lane]: + """ + docstring for generate_scenic_trajectory + + :param curr_lane: Current lane the object which that targeted route is being generated for is on + :type curr_lane: Lane + :param route: Proposed route for object queried from METSR with METSR road IDs + :type route: List[str] + + Attempts to generate an eqivalent route of Scenic Lanes from a list of METSR target road IDs. + Enforces that the first road in the trajectory corresponds to the objects current road. If + no route is found returns None. + + """ # Enforce that the first trajectory target corresponds to current location metsr_curr_road = self.map_scenic_to_metsr_lanes(curr_lane) if metsr_curr_road != route[0]: @@ -252,12 +259,18 @@ def generate_scenic_trajectory(self, curr_lane, route): if len(trajectory) < 1: print(f'No roads found') return None - return trajectory """ Translating Scenic -> Metsr representations""" - def map_scenic_to_metsr_road(self, road): + def map_scenic_to_metsr_road(self, road : Road) -> list[str]: + """ + docstring for map_scenic_to_metsr_road + + :param road: Scenic road ID which should be translated to equivalent METSR road ID(s) + :type road: Road + + """ query_key = f'{road.id}' metsr_keys= None From 4ac14ae220b5ca1df316f24241b9b199e1c0eccb Mon Sep 17 00:00:00 2001 From: Kay Date: Thu, 30 Apr 2026 14:29:16 -0700 Subject: [PATCH 72/73] updated ego spawn logic, improved bubble object updates and road lookups --- src/scenic/simulators/cosim/simulator.py | 98 +++++++++++++----------- 1 file changed, 53 insertions(+), 45 deletions(-) diff --git a/src/scenic/simulators/cosim/simulator.py b/src/scenic/simulators/cosim/simulator.py index 31ca27770..e064b2fab 100644 --- a/src/scenic/simulators/cosim/simulator.py +++ b/src/scenic/simulators/cosim/simulator.py @@ -175,7 +175,6 @@ def __init__(self, scene, carla_client, metsr_client, timestep, sim_ticks_per, t # For tracking / data collection self.bubble_sizes = [] self.total_active_vehicles = [] - self.routes_completed = 0 super().__init__(scene, timestep=timestep, **kwargs) @@ -381,6 +380,7 @@ def createObjectInCarla(self, obj: Object, update_orientation: bool = False, tra obj.spawn_guard = 2 obj.carla_actor_flag = True + return True @@ -443,7 +443,7 @@ def spawn_ego(self,obj: Object) -> None: radius=self.bubble_size) bubble_roads = self._get_bubble_roads() - _, new_roads, _ = self.classify_bubble_roads(bubble_roads) + new_roads, _ = self.classify_bubble_roads(bubble_roads) self.freeze_roads(new_roads) # Freeze lanes according to Ego Spawn self.metsr_client.tick() car_data = self.metsr_client.query_vehicle(self.getMetsrPrivateVehId(obj), True, True)["DATA"][0] @@ -454,22 +454,25 @@ def spawn_ego(self,obj: Object) -> None: elif hasattr(obj, "behavior"): # Default to CARLA autopilot behavior from spawn to random target road if obj.behavior is None: - for _ in range(len(self.network_helper.network_lanes)): - choice = random.choice(self.network_helper.network_lanes) - dest_roadID = self.map_scenic_to_metsr_lanes(choice) + choices = [idx for idx in range(len(self.network_helper.network_lanes))] + while len(choices) > 0: + idx = random.choice(choices) + choices.remove(idx) + lane = self.network_helper.network_lanes[idx] + dest_roadID = self.map_scenic_to_metsr_lanes(lane) if dest_roadID not in self.network_helper.intersection_road_links and dest_roadID != origin_str: - break - - trajectory_query = self.metsr_client.query_route_between_roads(origin_str,dest_roadID) - if "DATA" in trajectory_query: - trajectory = trajectory_query["DATA"][0] - + trajectory_query = self.metsr_client.query_route_between_roads(origin_str,dest_roadID) + if trajectory_query["DATA"] != ["KO"]: + trajectory = trajectory_query["DATA"][0] + break + + scenic_trajectory = self.generate_scenic_trajectory(lane, trajectory["road_list"]) if scenic_trajectory is not None: trajectory = self.scenic_trajectory_to_carla(scenic_trajectory) - self.createObjectInCarla(obj, update_orientation=True, trajectory=trajectory) # spawn ego in updated location and update orientation - + spawn_success = self.createObjectInCarla(obj, update_orientation=True, trajectory=trajectory) # spawn ego in updated location and update orientation + assert spawn_success, f"Invalid spawn selection point at : {obj.position}" def getCarlaProperties(self, obj : Object, properties : dict) -> dict[str, float | Vector | int]: """ @@ -618,13 +621,15 @@ def step(self) -> None: # (1) bubble_roads = self._get_bubble_roads() - roads, new_roads, old_roads = self.classify_bubble_roads(bubble_roads) + new_roads, old_roads = self.classify_bubble_roads(bubble_roads) self.release_roads(old_roads) self.freeze_roads(new_roads) - intersections = self.get_bubble_intersections(bubble_roads=roads, bubble_region=self.ego.bubble) + intersections = self.get_bubble_intersections(bubble_roads=bubble_roads, bubble_region=self.ego.bubble) # (2) - self.update_bubble_objects(roads,intersections) + bubble_road_ids = [road.id for road in bubble_roads] + intersection_ids = [intersection.id for intersection in intersections] + self.update_bubble_objects(bubble_road_ids,intersection_ids) # (3) self.tick_carla() @@ -640,14 +645,13 @@ def step(self) -> None: self.count += 1 if self.count % 100 == 0: print(f"Step: {self.count}. Total actors: {len(self.objects)}, bubble queue:{len(self.bubble_spawn_queue)} ") - print(f"Total active vehicles: {self.total_active_vehicles[-1]}, Routes completed: {self.routes_completed}, frozen vehicles {len(self.frozen_vehicles)}") - print(f"Total bubble actors: {len(self.carla_actors) - len(self.bubble_spawn_queue)}") + print(f"Total active vehicles: {self.total_active_vehicles[-1]}, frozen vehicles {len(self.frozen_vehicles)}") + print(f"Total bubble actors: {len(self.carla_actors) + len(self.bubble_spawn_queue)}") + self.tick_metsr() # (4) - self.ego.bubble = CircularRegion(center=[self.objects[0].x, - self.objects[0].y], - radius=self.bubble_size) + self.ego.bubble = CircularRegion(center=[self.objects[0].x, self.objects[0].y], radius=self.bubble_size) if self.ego.interrupt: #TODO support behaviors for objects when inside the bubble _utils.disable_carla_autopilot(self.ego) @@ -759,7 +763,8 @@ def synchronize_clients(self, obj: Object | list[Object] = None): print(f"Vehicle {obj.name} removed by CARLA likely due to deadlock") self.remove_bubble_object(obj, destroy=False) continue - if (loc.x,loc.y,loc.z) == (0,0,0): # Carla object still in the process of spawning + + if (loc.x,loc.y,loc.z) == (0,0,0): # Carla object still in the process of processing obj spawn continue vehID = self.getMetsrPrivateVehId(obj) @@ -783,7 +788,7 @@ def synchronize_clients(self, obj: Object | list[Object] = None): - def classify_bubble_roads(self, bubble_regions : list[Object] | None = None) -> tuple[list[Road], list[str], list[str]]: + def classify_bubble_roads(self, bubble_roads : list[Road]) -> tuple[list[Road], list[str], list[str]]: """ Docstring for update_carla_roads @@ -793,23 +798,16 @@ def classify_bubble_roads(self, bubble_regions : list[Object] | None = None) -> Collects all roads which are intersecting the bubble region (1) Default region is defined by the ego the region can be updated by passing objects with their corresponding regions """ - if bubble_regions is None: - roads = self._get_bubble_roads() # Collect Road intersecting the bubble - else: - roads = [] - for region in bubble_regions: - region_lanes = self._get_bubble_roads(bubble_region=region) - roads.extend(region_lanes) - - self.frozen_metsr_roads = roads + self.frozen_metsr_roads = bubble_roads bubble_road_ids = [] - for road in roads: - bubble_road_ids += self.map_scenic_to_metsr_road(road) + for road in bubble_roads: + if str(road.id) not in self.network_helper.scenic_unique_roads: + bubble_road_ids += self.map_scenic_to_metsr_road(road) self.frozen_roads = list(self.carla_control_roads.keys()) # Collect roads into new and old for freeze/unfreezing new_roads = [id for id in bubble_road_ids if id not in self.frozen_roads] old_roads = [id for id in self.frozen_roads if id not in bubble_road_ids] - return roads, new_roads, old_roads + return new_roads, old_roads @@ -834,29 +832,37 @@ def update_bubble_objects(self, bubble_roads: list[Road], bubble_intersections: """ cosim_data = self.metsr_client.query_coSimVehicle() all_veh_data = self._collect_metsr_vehicle_data(self.objects[1:]) - for obj in self.objects[1:]: veh_data = all_veh_data[obj] - + # Skip vehicles which have not entered the roadway or have completed their route if ('road' not in veh_data) or obj.finished_route: continue - - road = self._nearest_road(obj) - intersection = self._get_intersection(obj, road) - outside_bubble = (road not in bubble_roads and intersection not in bubble_intersections) - - # Spawn guard allows the client to process pending object creation """ + + outside_bubble = False + road = self.network_helper._nearest_road(obj) + id = road.id if road else None + + intersection = None + + if id not in bubble_roads: + intersection = self.network_helper._get_intersection(obj, road) + if intersection not in bubble_intersections: + outside_bubble = True + + # Spawn guard allows the client to process pending object creation if obj.spawn_guard > 0: - obj.spawn_guard = max(obj.spawn_guard - self.sim_ticks_per, 0) + obj.spawn_guard = max(obj.spawn_guard - self.sim_ticks_per, 0) # always positive # Remove vehicles which have left the cosimulation region and spawn vehicles which have entered if outside_bubble: if obj.carla_actor_flag: - self.remove_bubble_object(obj) + if obj.spawn_guard == 0: + self.remove_bubble_object(obj) else: if obj in self.bubble_spawn_queue: self.bubble_spawn_queue.remove(obj) + else: if not math.isclose(obj.position.x, veh_data['x']) or not math.isclose(obj.position.y, veh_data['y']): # enforce world state consistency obj.position = Vector(veh_data["x"], veh_data["y"], 0) @@ -884,7 +890,9 @@ def update_bubble_objects(self, bubble_roads: list[Road], bubble_intersections: if obj not in self.bubble_spawn_queue: self.bubble_spawn_queue.add(obj) continue # Do not spawn vehicle if no trajectory can be created + spawn_success = self.createObjectInCarla(obj, update_orientation=True, trajectory=carla_trajectory) + if spawn_success == False: self.bubble_spawn_queue.add(obj) continue From 615b8e0a6e0dac3e74be2530d80936862eeda000 Mon Sep 17 00:00:00 2001 From: Kay Date: Thu, 30 Apr 2026 14:30:30 -0700 Subject: [PATCH 73/73] fixed lane lookups when metsr recognized road is required, improved intersection/road query logic --- src/scenic/simulators/cosim/utils/utils.py | 92 ++++++++++++---------- 1 file changed, 49 insertions(+), 43 deletions(-) diff --git a/src/scenic/simulators/cosim/utils/utils.py b/src/scenic/simulators/cosim/utils/utils.py index ff848782d..e1d83f125 100644 --- a/src/scenic/simulators/cosim/utils/utils.py +++ b/src/scenic/simulators/cosim/utils/utils.py @@ -20,11 +20,12 @@ def __init__(self, self.radius_search_size=radius_search_size self.network_lanes = [*self.workspace.network.lanes] - self.network_roads = [*self.workspace.network.roads] + self.network_roads = [*self.workspace.network.allRoads] self.network_intersections = [*self.workspace.network.intersections] self.scenic_to_metsr_map_roads = {} self.intersection_road_links = set([]) + self.scenic_unique_roads = set([]) self.populate_scenic_to_metsr_roads() self.connected_roads_to_intersections = {} @@ -39,9 +40,9 @@ def populate_scenic_to_metsr_roads(self) -> None: """ Generate scenic -> METSR mappings for roads """ - for road,road_map in self.scenic_to_metsr_map_lanes.items(): - scenic_road = road.split("_")[0] - metsr_road = road_map.split("_")[0] + for road_lane,road_lane_map in self.scenic_to_metsr_map_lanes.items(): + scenic_road = road_lane.split("_")[0] + metsr_road = road_lane_map.split("_")[0] if metsr_road in self.metsr_represented_roads["orig_id"]: if scenic_road not in self.scenic_to_metsr_map_roads: self.scenic_to_metsr_map_roads[scenic_road] = [] @@ -51,23 +52,18 @@ def populate_scenic_to_metsr_roads(self) -> None: self.scenic_to_metsr_map_roads[scenic_road].append(metsr_road) else: self.intersection_road_links.add(metsr_road) + self.scenic_unique_roads.add(scenic_road) def populate_roads_to_intersections(self) -> None: """ Populate the dictionary """ for intersection in [*self.workspace.network.intersections]: - for lane in intersection.incomingLanes: - if lane.road not in self.connected_roads_to_intersections: - self.connected_roads_to_intersections[lane.road] = [intersection] + for road in intersection.roads: + if road not in self.connected_roads_to_intersections: + self.connected_roads_to_intersections[road] = [intersection] else: - self.connected_roads_to_intersections[lane.road].append(intersection) - for lane in intersection.outgoingLanes: - if lane not in self.connected_roads_to_intersections: - self.connected_roads_to_intersections[lane.road] = [intersection] - else: - self.connected_roads_to_intersections[lane.road].append(intersection) - + self.connected_roads_to_intersections[road].append(intersection) """Helpers for generating or collecting map data""" @@ -113,7 +109,9 @@ def _nearest_lane(self,obj : Object, allow_offlane : bool = True, radius_search_ return nearest_lane nearest_lane = obj._lane - if nearest_lane is None: + if nearest_lane is not None: # TODO need to cleanup this case or make a second helper + continue_search = self.map_scenic_to_metsr_lanes(nearest_lane) in self.intersection_road_links and not(allow_intersection_links) + if nearest_lane is None or continue_search: if not allow_offlane: assert nearest_lane, f"Object: {obj.name} is has left the roadway" neighborhood = CircularRegion(center=[obj.x,obj.y],radius=radius_size) @@ -136,7 +134,7 @@ def _nearest_lane(self,obj : Object, allow_offlane : bool = True, radius_search_ self.obj_lane_cache[obj] = nearest_lane return nearest_lane - def _nearest_road(self, obj: Object, allow_offroad : bool, radius_size: int = 50) -> Road: + def _nearest_road(self, obj: Object, allow_offroad: bool=True, radius_size: int = 50) -> Road: """ Docstring for _nearest_road @@ -152,42 +150,48 @@ def _nearest_road(self, obj: Object, allow_offroad : bool, radius_size: int = 50 nearest_road = None if obj in self.obj_road_cache: road = self.obj_road_cache[obj] - if road.containsPoint(obj.position): - nearest_road = road - else: - nearest_road = obj._road - - print(f"For: {obj.name}, nearest road is: {nearest_road}") - if nearest_road is None and allow_offroad: - distances = [] - neighborhood = CircularRegion(center=[obj.x,obj.y],radius=radius_size) - for road in self.network_roads: - if neighborhood.intersects(road): - distances.append((road.distanceTo(obj.position), road)) - - if len(distances) > 0: - nearest_road = min(distances, key=lambda t: t[0])[1] # min distance over all lanes - else: - print(f"No road has been found: {obj.name} at {obj.position}") + if road.containsPoint(obj.position): # try to verify road through the cache + return road - assert nearest_road is not None, f"Object has deviated to far from the roadway : i.e {radius_size/2} meters" - - self.obj_road_cache[obj] = nearest_road + nearest_road = obj._road # last resort lookup + if nearest_road is not None: # Maintain the previous road + self.obj_road_cache[obj] = nearest_road + return nearest_road - def _get_intersection(self, obj: Object, road: Road ) -> Intersection | None: + def _get_intersection(self, obj: Object, curr_road: Road = None ) -> Intersection | None: """ Docstring for _get_intersection :param obj: Object to identify lane for :type obj: Vehicle Object :param road: Objects current road in map - :type road: riad + :type road: road - Collects the nearest lane for an object from the intersection cache based - on outgoing roads, if none is found queries Scenic + Checks if the obj is on an intersection based, on its previous logged road + Looks up the intersection directly if no log exists yet """ - return obj._intersection + intersection = None + road = None + if curr_road: # If obj is on road it is not in an intersection + return None + + else: + pos = obj.position + if obj in self.obj_road_cache: + road = self.obj_road_cache[obj] # find the most recent road + + if road in self.connected_roads_to_intersections: # check if obj is in a connected intersection + intersections = self.connected_roads_to_intersections[road] + for intersection in intersections: + if intersection.containsPoint(pos): + intersection = intersection + break + + if road is None or intersection is None: # Previous road was not cached, so we need to do a global lookup + intersection = obj._intersection + + return intersection def _get_bubble_roads(self, bubble_region: CircularRegion) -> list[Road]: """ @@ -202,7 +206,6 @@ def _get_bubble_roads(self, bubble_region: CircularRegion) -> list[Road]: bubble_roads.append(road) return bubble_roads - def generate_scenic_trajectory(self, curr_lane: Lane, route: list[str] ) -> None | list[Lane]: """ @@ -263,7 +266,7 @@ def generate_scenic_trajectory(self, curr_lane: Lane, route: list[str] ) -> None """ Translating Scenic -> Metsr representations""" - def map_scenic_to_metsr_road(self, road : Road) -> list[str]: + def map_scenic_to_metsr_road(self, road : Road) -> list[str] | None: """ docstring for map_scenic_to_metsr_road @@ -271,6 +274,9 @@ def map_scenic_to_metsr_road(self, road : Road) -> list[str]: :type road: Road """ + if road in self.scenic_unique_roads: + return None + query_key = f'{road.id}' metsr_keys= None