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/.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/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/check-formatting.yml b/.github/workflows/check-formatting.yml index 251ec326f..bdd76a82f 100644 --- a/.github/workflows/check-formatting.yml +++ b/.github/workflows/check-formatting.yml @@ -10,10 +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/on-call-reminder.yml b/.github/workflows/on-call-reminder.yml deleted file mode 100644 index 1798be663..000000000 --- a/.github/workflows/on-call-reminder.yml +++ /dev/null @@ -1,30 +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@v2 - - - name: Set up Python - uses: actions/setup-python@v2 - 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 diff --git a/.github/workflows/run-coverage.yml b/.github/workflows/run-coverage.yml index 7ec93f84f..67472f28d 100644 --- a/.github/workflows/run-coverage.yml +++ b/.github/workflows/run-coverage.yml @@ -22,26 +22,28 @@ jobs: strategy: fail-fast: true matrix: - python-version: ["3.11"] + python-version: ["3.12"] os: [ubuntu-latest] extras: ["test-full"] runs-on: ${{ matrix.os }} 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' @@ -57,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-simulators.yml b/.github/workflows/run-simulators.yml index 885e386a4..89e0819fd 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,46 +71,30 @@ 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 }} 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 && @@ -109,11 +122,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 +141,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 +177,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/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 2a5bb55d1..69336f470 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -33,26 +33,31 @@ jobs: strategy: 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"] + 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.13" + slow: true runs-on: ${{ matrix.os }} 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' @@ -63,8 +68,11 @@ jobs: - name: Install Scenic and dependencies run: | - python -m pip install -e ".[${{ matrix.extras }}]" + 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 || '--no-graphics' }} + pytest ${TEST_OPTIONS} 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/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!' }}" + } + } + ] + } diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 000000000..e65667276 --- /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@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 + with: + persist-credentials: false + + - name: Install the latest version of uv + uses: astral-sh/setup-uv@22695119d769bdb6f7032ad67b9bca0ef8c4a174 + + - 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@1b549b9259bda1cb5ddde3b41741a82a2d15a841 + with: + sarif_file: results.sarif + category: zizmor diff --git a/.gitignore b/.gitignore index d2bf7f6f9..9985ac5d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,143 +1,150 @@ -# 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/ - -# 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 \ No newline at end of file +# 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 +output.txt \ No newline at end of file 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/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. 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/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/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/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 diff --git a/docs/simulators.rst b/docs/simulators.rst index fd0215b81..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 ---------------------------- @@ -94,6 +120,8 @@ We have several interfaces to the `Webots robotics simulator ` 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/__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/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..863b1ff51 100644 --- a/examples/carla/pedestrian.scenic +++ b/examples/carla/pedestrian.scenic @@ -1,6 +1,11 @@ +''' +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' +param address = "10.0.0.122" 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/cosim/readme.md b/examples/cosim/readme.md new file mode 100644 index 000000000..de13c331d --- /dev/null +++ b/examples/cosim/readme.md @@ -0,0 +1,29 @@ +# 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` + +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 +- 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 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``` diff --git a/examples/cosim/test.scenic b/examples/cosim/test.scenic index d0dfa8d48..b4b3b9eb9 100644 --- a/examples/cosim/test.scenic +++ b/examples/cosim/test.scenic @@ -1,4 +1,15 @@ -param carla_host = "walle" - -model scenic.simulators.cosim.model - +# 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 + +ego = new EgoCar with name "ego", with behavior DriveAvoidingCollisions(target_speed=15, avoidance_threshold=12) + +for i in range(100): + 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 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 diff --git a/examples/driving/Carla_Challenge/carlaChallenge2.scenic b/examples/driving/Carla_Challenge/carlaChallenge2.scenic index 61c689f77..cf244683e 100644 --- a/examples/driving/Carla_Challenge/carlaChallenge2.scenic +++ b/examples/driving/Carla_Challenge/carlaChallenge2.scenic @@ -1,8 +1,14 @@ """ 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 """ param map = localPath('../../../assets/maps/CARLA/Town07.xodr') # or other CARLA map that definitely works param carla_map = 'Town07' @@ -51,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 37256b334..275593932 100644 --- a/examples/driving/Carla_Challenge/carlaChallenge3.scenic +++ b/examples/driving/Carla_Challenge/carlaChallenge3.scenic @@ -1,7 +1,13 @@ """ 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 """ 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..bed577db0 100644 --- a/examples/driving/OAS_Scenarios/oas_scenario_03.scenic +++ b/examples/driving/OAS_Scenarios/oas_scenario_03.scenic @@ -1,6 +1,12 @@ """ 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 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 """ param map = localPath('../../../assets/maps/CARLA/Town04.xodr') # or other CARLA map that definitely works @@ -33,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 0b497214c..859fa6c01 100644 --- a/examples/driving/OAS_Scenarios/oas_scenario_04.scenic +++ b/examples/driving/OAS_Scenarios/oas_scenario_04.scenic @@ -1,6 +1,12 @@ """ 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 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 """ param map = localPath('../../../assets/maps/CARLA/Town07.xodr') # or other CARLA map that definitely works @@ -40,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 72fa8dc2f..ea787cc3e 100644 --- a/examples/driving/OAS_Scenarios/oas_scenario_28.scenic +++ b/examples/driving/OAS_Scenarios/oas_scenario_28.scenic @@ -1,8 +1,14 @@ """ 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 """ 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..2ef507ceb 100644 --- a/examples/driving/OAS_Scenarios/oas_scenario_29.scenic +++ b/examples/driving/OAS_Scenarios/oas_scenario_29.scenic @@ -1,8 +1,14 @@ """ 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 """ 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..d94a00693 100644 --- a/examples/driving/OAS_Scenarios/oas_scenario_30.scenic +++ b/examples/driving/OAS_Scenarios/oas_scenario_30.scenic @@ -1,8 +1,14 @@ """ 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 """ param map = localPath('../../../assets/maps/CARLA/Town10HD.xodr') # or other CARLA map that definitely works @@ -55,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 9a63373ab..6ccef865f 100644 --- a/examples/driving/OAS_Scenarios/oas_scenario_32.scenic +++ b/examples/driving/OAS_Scenarios/oas_scenario_32.scenic @@ -2,6 +2,12 @@ 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 """ param map = localPath('../../../assets/maps/CARLA/Town10HD.xodr') # or other CARLA map that definitely works @@ -50,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 dd36ede2b..8e27e3314 100644 --- a/examples/driving/badlyParkedCarPullingIn.scenic +++ b/examples/driving/badlyParkedCarPullingIn.scenic @@ -1,3 +1,11 @@ +''' +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 +''' + 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..49c421125 100644 --- a/examples/driving/car.scenic +++ b/examples/driving/car.scenic @@ -1,6 +1,13 @@ +''' +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 +''' 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 e564107b4..d1c088539 100644 --- a/examples/driving/pedestrian.scenic +++ b/examples/driving/pedestrian.scenic @@ -1,3 +1,10 @@ +''' +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 +''' 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..4cde3a294 100644 --- a/examples/driving/pedestrianAcrossRoad.scenic +++ b/examples/driving/pedestrianAcrossRoad.scenic @@ -1,3 +1,10 @@ +''' +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 +''' param map = localPath('../../assets/maps/CARLA/Town01.xodr') 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/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/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. diff --git a/pyproject.toml b/pyproject.toml index 960bc4724..cc0403738 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,22 +32,22 @@ 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", '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", "scikit-image ~= 0.21", "scipy ~= 1.7", "shapely ~= 2.0", - "trimesh >=4.0.9, <5", + "trimesh >=4.4.8, <5", ] [project.optional-dependencies] @@ -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,21 +67,22 @@ 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", "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 +133,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/dynamics/scenarios.py b/src/scenic/core/dynamics/scenarios.py index 7765eb9ee..889e61d79 100644 --- a/src/scenic/core/dynamics/scenarios.py +++ b/src/scenic/core/dynamics/scenarios.py @@ -1,527 +1,528 @@ -"""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 = [] + 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.""" + preq = PendingRequirement(ty, req, line, prob, name, self._ego) + self._pendingRequirements.append((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: + 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/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): 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 07f4c58b7..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 - - 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) - ) + # 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 - # 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. @@ -3275,7 +3384,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..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 @@ -50,16 +52,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 +75,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 +107,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 +116,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 +151,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: @@ -292,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 @@ -310,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 @@ -317,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 2a57c1f37..0c50a22e4 100644 --- a/src/scenic/core/shapes.py +++ b/src/scenic/core/shapes.py @@ -1,8 +1,9 @@ -""" 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 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/simulators.py b/src/scenic/core/simulators.py index 8c0d51844..3c64256e1 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 @@ -887,6 +895,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/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/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/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/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 ), 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) 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/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..25bd726dc 100644 --- a/src/scenic/simulators/cosim/model.scenic +++ b/src/scenic/simulators/cosim/model.scenic @@ -1,19 +1,138 @@ -from scenic.simulators.cosim.simulator import CosimSimulator - -param metsr_host = "localhost" -param metsr_port = 4000 -param carla_host = "localhost" -param carla_port = 2000 -param metsr_map = "Data.properties.CARLA" -param carla_map = "Town05" -param timestep = 0.1 - -simulator CosimSimulator( - metsr_host = globalParameters.metsr_host, - metsr_port = globalParameters.metsr_port, - carla_host = globalParameters.carla_host, - carla_port = globalParameters.carla_port, - metsr_map = globalParameters.metsr_map, - carla_map = globalParameters.carla_map, - timestep = globalParameters.timestep, - ) +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 .utils.CoSimActions import * +from .utils.scenarios import * + +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() +param bubble_size = 50 + + +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 = globalParameters.bubble_size + ) + +param startTime = 6*60*60 +param verbose=False + +""" +What kind of behaviors: + Sensor Data + Add some functions for accessing simulator data + + Intersection -- Traffic Link / intersection + + Helpers for measuring traffic metrics + +""" +_DAY_MOD = 24*60*60 + +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) + + origin: -1 + destination: -1 + + @property + def isCar(self): + return True + +behavior FollowRandomRoute(): + """ + Object will follow a random route starting from its + spawn position to a random target road. Low level controls will be handled + via the METSR driving module and CARLA autopilot + + NOTE: Random route behaviors are restricted to non-ego vehicles + This is due to the fact that CARLA may remove deadlocked or + non-moving vehicles from the simulation + """ + while True: + take SetAutoPilotAction() + + +class EgoCar(Car): + """ + Car which defines the corresponding bubble location + based on its current position + """ + carla_actor_flag = True + behavior: DriveAvoidingCollisions() + + +class NPCCar(Car, BackgroundDriver): + """ + A Non-Ego vehicle which has no effect on the defined bubble region + + :param carla_actor_flag: Dynamic flag representing vehicles presence in the + Cosimluated region. This flag will be set internally and should + not be modifed + :type: carla_actor_flag: bool + + :param behavior: Actor behavior + :type behavior: Scenic Behavior + """ + carla_actor_flag = False + behavior: FollowRandomRoute() + + +behavior CustomBubbleBehavior(): + """ + Object will follow a random route starting at + the origin zone and ending in the destination zone. + If the object enters the bubble then it will envoke + followLaneBehavior or any general CARLA behavior + """ + while True: + if self.carla_actor_flag: + do FollowLaneBehavior() + else: + do FollowRandomRoute() + + + +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 + +def currentTOD(): + return (simulation().currentTime * simulation().timestep + globalParameters.startTime)%_DAY_MOD + + + + 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}") diff --git a/src/scenic/simulators/cosim/simulator.py b/src/scenic/simulators/cosim/simulator.py index aab42e424..bef2a9191 100644 --- a/src/scenic/simulators/cosim/simulator.py +++ b/src/scenic/simulators/cosim/simulator.py @@ -1,32 +1,1206 @@ -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 - -class CosimSimulator(Simulator): - def __init__(self, map_name, carla_host, carla_port, metsr_host, metsr_port, timestep): - super().__init__() - - breakpoint() - - self.map_name = map_name - self.timestep = timestep - self.sim_timestep = sim_timestep - - self.carla_sim = CarlaSimulator() - self.metsr_sim = METSRSimulator() - - def createSimulation(self, scene, timestep, **kwargs): - assert timestep is None or timestep == self.timestep - - return CosimSimulation( - scene, self.timestep, self.carla_sim, self.metsr_sim, **kwargs - ) - - def destroy(self): - self.carla_sim.destroy() - self.metsr_sim.destroy() - super().destroy() - -class CosimSimulation(Simulation): - pass \ 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 +from scenic.simulators.cosim.utils.utils import * +from scenic.core.regions import CircularRegion +from scenic.core.object_types import Object +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 scenic.simulators.cosim.utils.utils as _utils +import scenic.simulators.carla.utils.utils as utils + + +import scenic.simulators.carla.utils.visuals as visuals +from scenic.simulators.carla.blueprints import oldBlueprintNames +import random + +try: + import carla +except ImportError as e: + raise ModuleNotFoundError('CARLA scenarios require the "carla" Python package') from e + +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=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, + render=True, + record="" + ): + super().__init__() + + + self.metsr_map_name = metsr_map + self.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 + + # 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") + + 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 + 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.") + + # self.scenario_number = 0 + verbosePrint("Carla was initialized correctly proceeding to Metsr") + + # Setting up Metsr simulator + self.metsr_client = METSRClient(host=metsr_host, + #sim_folder="../../../../../METS-R_HPC/output/CARLAT05_20260522_102031_seed_42", + port=metsr_port, + verbose=verbose) + + 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( + "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, + timestep=self.timestep, + sim_ticks_per=self.sim_ticks_per, + 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, 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.timestep = timestep # Timestep for each step + self.sim_ticks_per = sim_ticks_per + + # 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.bubble_spawn_queue = set({}) + self.frozen_scenic_roads = [] + self.xml_to_xodr_intersections = xml_to_xodr_intersections + self.metsr_actors = [] + self.carla_actors = [] + + # For tracking / data collection + self.bubble_sizes = [] + self.total_active_vehicles = [] + + + super().__init__(scene, timestep=timestep, **kwargs) + + + def setup(self) -> None: + """ + Docstring for setup + + Setup the simulation instance + """ + # Updated version takes no arguements + self.metsr_client.reset() + # print(f"{os.getcwd()}") + # self.metsr_client.start_viz(server_port=8080) + valid_metsr_roads = self.metsr_client.query_road() + + 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: + 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() + + 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 + + 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, + } + if origin or destination: + self.metsr_client.generate_trip_between_roads(**call_kwargs) + else: + self.metsr_client.generate_trip(**call_kwargs) + + # Immediately after spawning the object telport the vehicle to the correct spawn location + # TODO !!!Road update in metsr client!!!! TODO + self.metsr_client.teleport_cosim_vehicle(self.getMetsrPrivateVehId(obj), obj.position.x, obj.position.y, bearing=0, private_veh = True, transform_coords = True) + + def createObjectInCarla(self, obj: Object, update_orientation: bool = False, trajectory: list[carla.Transform] = None, metsr_data: dict = 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 + ) + 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) + # 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 + c_str = f"{int(c.r*255)},{int(c.g*255)},{int(c.b*255)}" + blueprint.set_attribute("color", c_str) + try: + carlaActor = self.carla_world.spawn_actor(blueprint, transform) + except Exception as e: + return False + + 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 + + obj.spawn_guard = 2 + obj.carla_actor_flag = True + return True + + + + 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]: # Special handling for ego + self.ego = obj + self.spawn_ego(obj) + self.carla_actors.append(obj) + self.metsr_actors.append(obj) + else: + self.createObjectInMetsr(obj) + self.metsr_actors.append(obj) + # Track route completion for autopilot + obj.finished_route = False + if self.count == 0: + self.metsr_client.tick() # allow obj to enter road if possible + obj.carla_actor_flag = False + obj.spawn_guard = 0 + # Track autopilot behaviors + obj.active_autopilot = False + obj.autopilot_action = False + + 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) Teleport ego to the precise spawn location in MESTR + (3) Collect exact spawn location to define bubbble region + (4) Freeze CoSimulated regions inside METSR + (5) Spawn ego inside CARLA + """ + lane = self._nearest_lane(obj, allow_intersection_links=False) + assert lane, f"Non Valid retrun value for nearest lane" + print(f"Returned Lane : {lane.road.id}_{lane.id}") + origin_str = self.map_scenic_to_metsr_lanes(lane).pop() # Arbitrarily select an element if their are multiple maps? + + self.createObjectInMetsr(obj,origin=origin_str) # Set the METSR vehicle origin to match ego spawn + self.metsr_client.tick() # Allow the vehicle to spawn + obj.final_road = None + + obj.bubble = CircularRegion(center=[obj.position.x, + obj.position.y], + radius=self.bubble_size) + + 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() + + spawn_success = self.createObjectInCarla(obj, update_orientation=True) # 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]: + """ + 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 + """ + raw_data = self.obj_data_cache[obj] + # check vehicle state + 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 + + 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"] + #bearing = math.radians(raw_data["bearing"]) + #globalOrientation = Orientation.fromEuler(bearing,0,0) + yaw, pitch, roll = 0,0,0 #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" + if obj.carla_actor_flag: + properties = self.getCarlaProperties(obj,properties) + else: + properties = self.getMetsrProperties(obj,properties) + return properties + + 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 + """ + 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 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: + """ + 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): Compute new bubble region + """ + + # (1): Update the high fidelity region based on the ego's new locatin + bubble_roads = self._get_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=bubble_roads, bubble_region=self.ego.bubble) + + # (2): Spawn and destroy objects according to region changes + 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): Tick both clients and synchronize states + self.tick_carla() + self.synchronize_clients() + self.tick_metsr() + + if self.render: + self.cameraManager.render(self.display) + pygame.display.flip() + + + 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]}, frozen vehicles {len(self.frozen_vehicles)}") + print(f"Total bubble actors: {len(self.carla_actors) + len(self.bubble_spawn_queue)}") + + # (4): Compute new bubble region and process behavior interrupts + self.ego.bubble = CircularRegion(center=[self.objects[0].x, self.objects[0].y], radius=self.bubble_size) + + + + 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 + (2) Are connected to the bubble via AT LEAST 2 roads + + :return: The set of all intersections meeting the above criteria + :rtype: list[Intersection] + """ + bubble_intersections = [] + for intersection in self.network_helper.network_intersections: + if intersection.intersects(bubble_region): + bubble_intersections.append(intersection) + continue + intersection_roads = intersection.roads + count = 0 + for road in intersection_roads: + if road in bubble_roads: + count += 1 + if count > 1: + bubble_intersections.append(intersection) + break + return bubble_intersections + + def initiate_autopilot(self, obj : Object) -> bool: + """ + docstring for initiate_autopilot + + :param obj: Target vehicle to initiate autopilot for + + Activates autopilot for a simulation vehicle + (i) If the vehicle is in Carla, queries metsr for the target trajectory + then converts that to a corresponding set of Carla Waypoints for Carla autopilot + (2) If the vehicle is in METSR updates the overwrite flag for manually controlling the vehicle + """ + if obj.carla_actor_flag: + # print(f"Setting obj: {obj.name} with no specific trajectory") + obj.carlaActor.set_autopilot(True) # Set the autopilot with no specific trajectory if none is found? + success = True + else: + obj.override_autopilot = False + success = True + + return success + + 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 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 = self.carla_actors + + all_veh_data = self._collect_metsr_vehicle_data(self.carla_actors) + + 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) + self.createObjectInCarla(obj) + continue + + 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) + lane = self._nearest_lane(obj) + veh_data = all_veh_data[obj] + if not hasattr(obj, "previous_road"): + obj.previous_road = None + if lane: # Do not update the vehicle road information until the vehicle is on a METSR recognized roadway + roadIDs = self.map_scenic_to_metsr_lanes(lane) + if roadIDs: + # Update METSR road # TODO FIX THIS FOR HANDLING NON_UNIQUE MAPPINGS + if obj.previous_road not in roadIDs and roadIDs.isdisjoint(self.network_helper.intersection_road_links): # Entering new road within metsr network\ + roadID = roadIDs.pop() + self.metsr_client.enter_next_road(vehID, roadID=roadID, private_veh = True) + obj.previous_road = roadID # update previous road + assert len(roadIDs) == 0, "Unable to identify the correct roadway to enter" + + bearing = get_metsr_rotation(obj.carlaActor.get_transform().rotation.yaw) + # Check if objects are desynchronized + 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) + + + + def classify_bubble_roads(self, bubble_roads : list[Road]) -> tuple[list[Road], list[str], list[str]]: + """ + 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 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 + """ + self.frozen_metsr_roads = bubble_roads + bubble_road_ids = [] + 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 new_roads, old_roads + + + + def update_bubble_objects(self, bubble_roads: list[Road], bubble_intersections: list[Intersection]) -> None: + """ + Docstring for update_bubble_objects + + :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] + + (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 + (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 + + """ + 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 + + 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) # always positive + + # 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) + else: + if obj in self.bubble_spawn_queue: + self.bubble_spawn_queue.remove(obj) + + else: + 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 + else: # spawn the vehicle + spawn_success = self.createObjectInCarla(obj, update_orientation=True) + if spawn_success == False: + self.bubble_spawn_queue.add(obj) + continue + elif obj in self.bubble_spawn_queue: + self.bubble_spawn_queue.remove(obj) + + self.carla_actors.append(obj) + self.metsr_actors.remove(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 + + Trajectories starting point must always correspond to the vehicles current road + """ + way_points = [] + 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 + + + def freeze_roads(self, keys: list[str]) -> None: + """ + Docstring for freeze_roads + + :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" + 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) + + + def release_roads(self,keys: list[str]) -> None: + """ + Docstring for release_roads + + :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" + 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) + + + 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, 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 + obj.trajectory = None + self.carla_actors.remove(obj) + self.metsr_actors.append(obj) + + 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.carla_actors: + 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 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 map_scenic_to_metsr_lanes(self, lane: Lane) -> set[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) + + 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) + + 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: + bubble_region = bubble_region # User specified (for added functionality later) + return self.network_helper._get_bubble_roads(bubble_region) + + + 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: + carla_actions[obj] = allActions[obj] + super().executeActions(carla_actions) + for obj in self.agents: + if obj.carla_actor_flag: # Processing CARLA actors + if not obj.autopilot_action and obj.active_autopilot: # Disable autopilot first to enable smooth transitions + obj.active_autopilot = not(_utils.disable_carla_autopilot(obj)) + elif not obj.autopilot_action: # Autopilot behavior? + ctrl = obj._control + if ctrl is not None: + obj.carlaActor.apply_control(ctrl) + elif obj.autopilot_action and not obj.active_autopilot: # Activate autopilot + obj.active_autopilot = self.initiate_autopilot(obj) + obj._control = None # TODO What does this do? + + else: + if not obj.autopilot_action: # Default is autopilot + target_acc = obj.target_acceleration if hasattr(obj, "target_accleration") else 0 # apply, if no action is taken no movement + self.metsr_client.control_vehicle(self.getMetsrPrivateVehId(obj), target_acc, private_veh=True) + + + def updateObjects(self) -> None: + """ + Docstring for updateObjects + + Update object properties for METSR simulated 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 _save_metsr_state(self, file_name=None) -> None: + """ + docstring for _save_metsr_state + + Saves metsr state to a file to allow for reproducible replay + """ + if file_name == None: + save_file = f"metsr_state_at_{self.count}.bin" + else: + save_file = file_name + + self.metsr_client.save(save_file) + + + """ light helpers for added functionality down the road""" + + 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}" + + + + 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]: + break + else: + assert True, f" Incompatible 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 + + diff --git a/src/scenic/simulators/cosim/utils/CoSimActions.py b/src/scenic/simulators/cosim/utils/CoSimActions.py new file mode 100644 index 000000000..a74b2d6d0 --- /dev/null +++ b/src/scenic/simulators/cosim/utils/CoSimActions.py @@ -0,0 +1,42 @@ +from scenic.core.simulators import Action + + +class BackgroundDriver: + """ + Non ego background driver + Background vehicles are allowed to use autopilot + behaviors, where low level controllers are handled + by the relevent simulator + (1) Carla if applicable + (2) METSR by default + """ + def __init__(self): + carla_actor_flag = False + finished_route_check = False + + def setAutoPilot(self): + self.autopilot_action = True + + def setAccelearation(self, acc=0): + self.target_acceleration = acc + + +class SetAutoPilotAction(Action): + """ Set autopilot flag to be True""" + def canBeTakenBy(self,agent): + return isinstance(agent, BackgroundDriver) + + def applyTo(self, obj, sim): + obj.setAutoPilot() + + +class SetAccelerationAction: + def canBeTakenBy(self,agent): + if hasattr(agent, "carla_actor_flag"): + if agent.carla_actor_flag: + return True + else: + return False + + def applyTo(self, obj, sim): + obj.setAcceleration() diff --git a/src/scenic/simulators/cosim/utils/scenarios.scenic b/src/scenic/simulators/cosim/utils/scenarios.scenic new file mode 100644 index 000000000..dc24b75fa --- /dev/null +++ b/src/scenic/simulators/cosim/utils/scenarios.scenic @@ -0,0 +1,37 @@ + +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): + 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) diff --git a/src/scenic/simulators/cosim/utils/utils.py b/src/scenic/simulators/cosim/utils/utils.py new file mode 100644 index 000000000..1aea99621 --- /dev/null +++ b/src/scenic/simulators/cosim/utils/utils.py @@ -0,0 +1,502 @@ +import xml.etree.ElementTree as ET +import os +import numpy as np +import carla +from scenic.core.regions import CircularRegion +from scenic.domains.driving.roads import LaneSection, 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.laneSections] + 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 = {} + 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_lane,road_lane_map in self.scenic_to_metsr_map_lanes.items(): + scenic_road = road_lane.split("_")[0] + for metsr_map in road_lane_map: + metsr_road = metsr_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] = set() + self.scenic_to_metsr_map_roads[scenic_road].add(metsr_road) + else: + if metsr_road not in self.scenic_to_metsr_map_roads[scenic_road]: + self.scenic_to_metsr_map_roads[scenic_road].add(metsr_road) + else: + self.intersection_road_links.add(metsr_road) + + for road_lane in self.scenic_to_metsr_map_lanes.keys(): + scenic_road = road_lane.split("_")[0] + if scenic_road not in self.scenic_to_metsr_map_roads: + 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 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[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) -> LaneSection | 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: + metsr_roads = self.map_scenic_to_metsr_lanes(nearest_lane) + if metsr_roads: + for road in metsr_roads: + if road not in self.intersection_road_links: + self.obj_lane_cache[obj] = nearest_lane + return nearest_lane + + nearest_lane = obj._laneSection + if nearest_lane is not None: # TODO need to cleanup this case or make a second helped + mapped_roads = self.map_scenic_to_metsr_lanes(nearest_lane) + if mapped_roads: # Check if the returned lane has a valid metsr mapping + continue_search = not self.map_scenic_to_metsr_lanes(nearest_lane).isdisjoint(self.intersection_road_links) and not(allow_intersection_links) + else: + continue_search = True + + 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) + 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: + print(f"Selecting lane based on dist") + for _ in range(len(distances)): + distance, nearest_lane = min(distances, key=lambda t: t[0])[:] # min distance over all lanes + mapped_roads = self.map_scenic_to_metsr_lanes(nearest_lane) + if mapped_roads: + if mapped_roads.isdisjoint(self.intersection_road_links): + break + else: + distances.remove((distance, nearest_lane)) + else: + print(f"Selecting lane based on dist") + 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=True, radius_size: int = 50) -> 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): # try to verify road through the cache + return 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, 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: road + + Checks if the obj is on an intersection based, on its previous logged road + Looks up the intersection directly if no log exists yet + """ + 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]: + """ + 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: LaneSection, route: list[str] ) -> None | list[LaneSection]: + """ + 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_roads = self.map_scenic_to_metsr_lanes(curr_lane) + # if metsr_curr_roads: + # if route[0] not in metsr_curr_roads: + # route.insert(0, metsr_curr_roads.pop()) # Arbritrarily any potential map + + # Collect All valid spawn locations + map_data = self.scenic_to_metsr_map_lanes.items() + # print(f"METSR_MAPS: {list(self.scenic_to_metsr_map_lanes.values())}") + valid_lanes = {} + for road in route: + for scenic_key, metsr_keys in map_data: # Scenic <-> Metsr mappings + for metsr_key in metsr_keys: + 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 = [] + # print(f'Route: {route}') + for i,road in enumerate(route): + # print(f'Processing road: {road}') + 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 : Road) -> list[str] | None: + """ + 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 + + """ + if road in self.scenic_unique_roads: + return None + + 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: LaneSection) -> set[str] | None: + """ + docstring for map_scenic_to_metsr_lanes + + Given a OpenDrive LaneSection + + """ + metsr_keys=None + query_key = f'{lane.road.id}_{lane.id}' + + if query_key in self.scenic_to_metsr_map_lanes: + metsr_keys = self.scenic_to_metsr_map_lanes[query_key] + metsr_keys = set([metsr_key.split("_")[0] for metsr_key in metsr_keys]) + + return metsr_keys + +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: + # type = lane.attrib.get('type') if lane.attrib.get('type') else "" + 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: + if id in lane_mappings: + lane_mappings[id].append(metrs_lane) + else: + lane_mappings[id] = [metrs_lane] + else: + if orig_id in lane_mappings: + lane_mappings[orig_id].append(metrs_lane) + else: + lane_mappings[orig_id] = [metrs_lane] + # else: + # print(f"Skipping lane: {lane.attrib.get('id')}") + + if lane_mappings == {}: + print(f"An occured attempting to process map: {map}") + + # for key, item in lane_mappings.items(): + # print(f"KEY: {key} :: ITEM: {item}") + + 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[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): + """ + 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 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..27dc11952 --- /dev/null +++ b/src/scenic/simulators/metadrive/simulator.py @@ -0,0 +1,289 @@ +"""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, + sumo_map, + timestep=0.1, + render=True, + render3D=False, + 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) + + 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 + + # 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=vehicle_config, + use_mesh_terrain=False, + height_scale=0.0001, + 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=vehicle_config, + ) + 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/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..176eac33f 100644 --- a/src/scenic/simulators/metsr/client.py +++ b/src/scenic/simulators/metsr/client.py @@ -1,630 +1,1115 @@ -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): - 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): - 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 +import networkx as nx + + +""" +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 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": []} + 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 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): + 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 + + # 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 + 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, speed = 0, private_veh = False, transform_coords = False): + msg = { + "TYPE": "CTRL_teleportCoSimVeh", + "DATA": [] + } + if not isinstance(vehID, list): + 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, 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"] + 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 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] diff --git a/src/scenic/simulators/newtonian/car.png b/src/scenic/simulators/newtonian/car.png index 46f39d574..a6f7d014d 100644 Binary files a/src/scenic/simulators/newtonian/car.png and b/src/scenic/simulators/newtonian/car.png differ 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..6ff603509 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,15 @@ 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.screen = None self.frames = [] + self.debug_render = debug_render if timestep is None: timestep = 0.1 @@ -102,10 +108,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 +144,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 +174,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 @@ -197,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() @@ -207,21 +242,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 +260,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) 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/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/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/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 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) 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/core/test_regions.py b/tests/core/test_regions.py index b7293ab3c..ca5a7adb7 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,158 @@ 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) + + +@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)), + 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 af0b6705e..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: @@ -159,7 +160,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/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/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 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..3f638fabc --- /dev/null +++ b/tests/simulators/metadrive/test_metadrive.py @@ -0,0 +1,153 @@ +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 + + +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}" 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..838884b6c 100644 --- a/tests/simulators/webots/test_webots.py +++ b/tests/simulators/webots/test_webots.py @@ -1,7 +1,52 @@ +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") 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/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 ( 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( """ 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() 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 =