diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..c0084b4 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @NITISH-R-G \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/template.md b/.github/ISSUE_TEMPLATE/template.md new file mode 100644 index 0000000..47506bc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/template.md @@ -0,0 +1,39 @@ +--- +name: Issue Template +about: Create an issue to help us improve +title: '' +labels: '' +assignees: '' +--- + +## Describe the Issue + +A clear and concise description of the issue or feature request. + +## Expected Behavior + +A clear and concise description of what you expected to happen or the feature you want. + +## Current Behavior + +If this is a bug, what is the current behavior? + +## Steps to Reproduce (if applicable) + +1. Step 1 +2. Step 2 +3. Step 3 + +## Possible Solution (optional) + +If you have a solution in mind, describe it here. + +## Context + +How has this issue affected you? What are you trying to accomplish? + +## Environment (if applicable) + +* OS: +* Python version: +* Node version: diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..0f777f8 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,32 @@ +## Description + +Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. + +Fixes # (issue) + +## Type of change + +Please delete options that are not relevant. + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] This change requires a documentation update + +## How Has This Been Tested? + +Please describe the tests that you ran to verify your changes. + +- [ ] Test A +- [ ] Test B + +## Checklist: + +- [ ] My code follows the style guidelines of this project +- [ ] I have performed a self-review of my own code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes +- [ ] Any dependent changes have been merged and published in downstream modules \ No newline at end of file diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..fd3d332 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,26 @@ +backend: + - any: + - 'ev_grid_oracle/**/*' + - 'server/**/*' + - 'models.py' + +frontend: + - any: + - 'web/**/*' + +tests: + - any: + - 'tests/**/*' + +documentation: + - any: + - '**/*.md' + - 'docs/**/*' + +tools: + - any: + - 'tools/**/*' + +ci: + - any: + - '.github/**/*' diff --git a/.github/workflows/ai-insights.yml b/.github/workflows/ai-review.yml similarity index 50% rename from .github/workflows/ai-insights.yml rename to .github/workflows/ai-review.yml index 87f31e0..1de2173 100644 --- a/.github/workflows/ai-insights.yml +++ b/.github/workflows/ai-review.yml @@ -3,25 +3,22 @@ name: AI PR Agent on: pull_request: types: [opened, synchronize, reopened] - issue_comment: + pull_request_review_comment: types: [created] +permissions: + contents: read + pull-requests: write + jobs: pr_agent: runs-on: ubuntu-latest - permissions: - issues: write - pull-requests: write - contents: write name: Run PR Agent if: ${{ github.event.sender.type != 'Bot' }} steps: - name: PR Agent action step id: pragent - uses: Codium-ai/pr-agent@main + uses: coderabbitai/openai-pr-reviewer@latest env: - OPENAI_KEY: ${{ secrets.OPENAI_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - github_action_config.auto_review: "true" - github_action_config.auto_describe: "true" - github_action_config.auto_improve: "true" + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ba6ddf3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,40 @@ +name: CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + lfs: true + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '24' + + - name: Build Frontend + run: | + cd web + npm install + npm run build + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + pip install uv + uv pip install --system -e ".[dev,demo]" + + - name: Run Backend Tests + run: | + uv run pytest tests/ diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..6baeab8 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,40 @@ +name: "CodeQL Analysis" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '0 0 * * 0' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'python', 'javascript-typescript' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml new file mode 100644 index 0000000..5e2563e --- /dev/null +++ b/.github/workflows/greetings.yml @@ -0,0 +1,20 @@ +name: Greetings + +on: + issues: + types: [opened] + pull_request: + types: [opened] + +jobs: + greeting: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/first-interaction@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + issue-message: "Welcome to the repository! Thank you for opening your first issue. We will triage it shortly." + pr-message: "Welcome to the repository! Thank you for opening your first pull request. Please make sure all checks pass and the PR template is filled out." diff --git a/.github/workflows/health-dashboard.yml b/.github/workflows/health-dashboard.yml index fb788a5..94b4472 100644 --- a/.github/workflows/health-dashboard.yml +++ b/.github/workflows/health-dashboard.yml @@ -45,10 +45,3 @@ jobs: with: name: health-dashboard path: dashboard_output/ - - - name: Deploy to GitHub Pages - if: github.ref == 'refs/heads/main' - uses: peaceiris/actions-gh-pages@v4 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./dashboard_output diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000..8fc457a --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,12 @@ +name: "Pull Request Labeler" +on: + - pull_request_target + +jobs: + triage: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v5 diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..c233c5d --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,40 @@ +name: Deploy GitHub Pages + +on: + workflow_run: + workflows: ["Repository Health Dashboard"] + types: + - completed + +permissions: + contents: read + pages: write + id-token: write + +jobs: + deploy: + if: ${{ github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: health-dashboard + path: dashboard_output/ + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Pages + uses: actions/configure-pages@v5 + + - name: Upload pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: dashboard_output/ + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/repo-maintenance.yml b/.github/workflows/repo-maintenance.yml new file mode 100644 index 0000000..631556a --- /dev/null +++ b/.github/workflows/repo-maintenance.yml @@ -0,0 +1,64 @@ +name: Repository Maintenance + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +permissions: + contents: write + +jobs: + maintenance: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref || github.ref }} + lfs: true + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + sudo apt-get update && sudo apt-get install -y xdg-utils + pip install uv + uv pip install --system -e ".[dev,demo]" + uv pip install --system ruff pydeps cyclonedx-py + + - name: Autofix Code (Ruff) + run: | + ruff check --fix . + ruff format . + + - name: Generate Knowledge Graph + run: | + python tools/generate_knowledge_graph.py + + - name: Sync Documentation + run: | + python tools/docs_sync.py + + - name: Generate Architecture Diagrams + run: | + mkdir -p docs/architecture + pydeps ev_grid_oracle --noshow -o docs/architecture/ev_grid_oracle.svg || true + pydeps server --noshow -o docs/architecture/server.svg || true + + - name: Generate SBOM + run: | + cyclonedx-py environment -o bom.json + + - name: Commit changes + if: github.event_name == 'push' || github.event_name == 'schedule' || github.event.pull_request.head.repo.full_name == github.repository + run: | + git config --global user.name 'github-actions[bot]' + git config --global user.email 'github-actions[bot]@users.noreply.github.com' + git add . + git commit -m "chore: automated repository maintenance [skip ci]" || echo "No changes to commit" + git push diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..4590406 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,19 @@ +name: Mark stale issues and pull requests + +on: + schedule: + - cron: '0 0 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@v9 + with: + stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.' + stale-pr-message: 'This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.' + days-before-stale: 60 + days-before-close: 7 diff --git a/.mypy_cache/3.12/cache.0.db b/.mypy_cache/3.12/cache.0.db new file mode 100644 index 0000000..60c02d8 Binary files /dev/null and b/.mypy_cache/3.12/cache.0.db differ diff --git a/.mypy_cache/3.12/cache.1.db b/.mypy_cache/3.12/cache.1.db new file mode 100644 index 0000000..aa4ee6c Binary files /dev/null and b/.mypy_cache/3.12/cache.1.db differ diff --git a/.mypy_cache/3.12/cache.10.db b/.mypy_cache/3.12/cache.10.db new file mode 100644 index 0000000..89a8fc9 Binary files /dev/null and b/.mypy_cache/3.12/cache.10.db differ diff --git a/.mypy_cache/3.12/cache.11.db b/.mypy_cache/3.12/cache.11.db new file mode 100644 index 0000000..13e74ce Binary files /dev/null and b/.mypy_cache/3.12/cache.11.db differ diff --git a/.mypy_cache/3.12/cache.12.db b/.mypy_cache/3.12/cache.12.db new file mode 100644 index 0000000..8caa2f3 Binary files /dev/null and b/.mypy_cache/3.12/cache.12.db differ diff --git a/.mypy_cache/3.12/cache.13.db b/.mypy_cache/3.12/cache.13.db new file mode 100644 index 0000000..a34112a Binary files /dev/null and b/.mypy_cache/3.12/cache.13.db differ diff --git a/.mypy_cache/3.12/cache.14.db b/.mypy_cache/3.12/cache.14.db new file mode 100644 index 0000000..c26376e Binary files /dev/null and b/.mypy_cache/3.12/cache.14.db differ diff --git a/.mypy_cache/3.12/cache.15.db b/.mypy_cache/3.12/cache.15.db new file mode 100644 index 0000000..92b85d8 Binary files /dev/null and b/.mypy_cache/3.12/cache.15.db differ diff --git a/.mypy_cache/3.12/cache.2.db b/.mypy_cache/3.12/cache.2.db new file mode 100644 index 0000000..10ce0b1 Binary files /dev/null and b/.mypy_cache/3.12/cache.2.db differ diff --git a/.mypy_cache/3.12/cache.3.db b/.mypy_cache/3.12/cache.3.db new file mode 100644 index 0000000..ae2c856 Binary files /dev/null and b/.mypy_cache/3.12/cache.3.db differ diff --git a/.mypy_cache/3.12/cache.4.db b/.mypy_cache/3.12/cache.4.db new file mode 100644 index 0000000..3fbe91b Binary files /dev/null and b/.mypy_cache/3.12/cache.4.db differ diff --git a/.mypy_cache/3.12/cache.5.db b/.mypy_cache/3.12/cache.5.db new file mode 100644 index 0000000..a0ab71d Binary files /dev/null and b/.mypy_cache/3.12/cache.5.db differ diff --git a/.mypy_cache/3.12/cache.6.db b/.mypy_cache/3.12/cache.6.db new file mode 100644 index 0000000..1e22feb Binary files /dev/null and b/.mypy_cache/3.12/cache.6.db differ diff --git a/.mypy_cache/3.12/cache.7.db b/.mypy_cache/3.12/cache.7.db new file mode 100644 index 0000000..8482541 Binary files /dev/null and b/.mypy_cache/3.12/cache.7.db differ diff --git a/.mypy_cache/3.12/cache.8.db b/.mypy_cache/3.12/cache.8.db new file mode 100644 index 0000000..77e261d Binary files /dev/null and b/.mypy_cache/3.12/cache.8.db differ diff --git a/.mypy_cache/3.12/cache.9.db b/.mypy_cache/3.12/cache.9.db new file mode 100644 index 0000000..2ddc018 Binary files /dev/null and b/.mypy_cache/3.12/cache.9.db differ diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..f412dd8 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,45 @@ +# 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. 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. + +## Attribution + +This Code of Conduct is adapted from the Contributor Covenant, version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ac57be5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,33 @@ +# Contributing Guidelines + +First off, thank you for considering contributing to our project! It's people like you that make open source such a great community. + +## Where do I go from here? + +If you've noticed a bug or have a feature request, make sure to check our Issues to see if someone else has already created an issue for it. If not, feel free to open a new one. + +## Setting up your environment + +1. Fork the repo and clone it to your local machine. +2. Install `uv` if you haven't already. +3. Run `uv pip install -e ".[dev,demo]"` to install all required development, test, and demo dependencies. +4. For frontend work, `cd web` and run `npm install`. + +## Making Changes + +* Create a new branch from `main`. +* Make your changes. +* Ensure all tests pass by running `uv run pytest tests/`. +* Ensure code quality by running formatting and linting tools (`ruff`, `mypy`, etc.). +* Run `./validate-submission.sh` locally before submitting a PR. +* Commit your changes with clear, descriptive commit messages. + +## Submitting a Pull Request + +* Push your branch to your fork. +* Open a Pull Request against the `main` branch of this repository. +* Provide a detailed description of your changes and why they are necessary. +* Link to any relevant issues. +* Wait for review and address any feedback! + +We appreciate your contributions and look forward to working with you! diff --git a/docs/api/ev_grid_oracle___init__.md b/docs/api/ev_grid_oracle___init__.md new file mode 100644 index 0000000..2bd39bb --- /dev/null +++ b/docs/api/ev_grid_oracle___init__.md @@ -0,0 +1,2 @@ +## Module: __init__.py +EV Grid Oracle package. diff --git a/docs/api/ev_grid_oracle_bescom_feed.md b/docs/api/ev_grid_oracle_bescom_feed.md new file mode 100644 index 0000000..7140eeb --- /dev/null +++ b/docs/api/ev_grid_oracle_bescom_feed.md @@ -0,0 +1,6 @@ +### Class: BESCOMFeedAPI +Deterministic BESCOM feeder "API mock". + +- No network calls (HF Spaces safe). +- Feeder loads are derived from: time-of-day + grid_load_pct + station loads. +- Output is stable under (seed, scenario, tick) so judge replays match. diff --git a/docs/api/ev_grid_oracle_env.md b/docs/api/ev_grid_oracle_env.md new file mode 100644 index 0000000..99c6df9 --- /dev/null +++ b/docs/api/ev_grid_oracle_env.md @@ -0,0 +1,5 @@ +### Class: EVGridCore +Core env logic (no HTTP). Server wraps this. + +v0 slice: deterministic schema, minimal dynamics. +Next slices add demand_sim/grid_sim/reward engine. diff --git a/docs/api/ev_grid_oracle_models.md b/docs/api/ev_grid_oracle_models.md new file mode 100644 index 0000000..92e9475 --- /dev/null +++ b/docs/api/ev_grid_oracle_models.md @@ -0,0 +1,15 @@ +### Class: BESCOMFeederState +Lightweight, judge-friendly feeder snapshot (mocked but deterministic). + +### Class: NegotiationMessage +A short, bounded message used in the explicit multi-agent protocol. + +This is *not* a free-form chat reward. It exists so judges can see +negotiation/constraints explicitly and we can penalize empty spam. + +### Class: GridDirective +GridOperator -> FleetDispatcher constraint signal (verifiable). + +### Class: SimulationPrediction +Aggregated 'dream state' prediction for T+5 ticks. +Kept intentionally small and verifiable for hackathon judging. diff --git a/docs/api/ev_grid_oracle_multi_agent.md b/docs/api/ev_grid_oracle_multi_agent.md new file mode 100644 index 0000000..9d6ca82 --- /dev/null +++ b/docs/api/ev_grid_oracle_multi_agent.md @@ -0,0 +1,9 @@ +### Class: MultiAgentSession +Minimal explicit multi-agent wrapper around EVGridCore. + +- GridOperator emits a directive (constraint signal) + optional message. +- FleetDispatcher emits an action + optional message. +- Resolver applies directive deterministically and steps EVGridCore. + +#### Function: snapshot +Read-only view of the underlying core state. diff --git a/docs/api/ev_grid_oracle_oracle_agent.md b/docs/api/ev_grid_oracle_oracle_agent.md new file mode 100644 index 0000000..e49aeef --- /dev/null +++ b/docs/api/ev_grid_oracle_oracle_agent.md @@ -0,0 +1,10 @@ +### Class: OracleRuntime +Singleton-style loader that prefers CUDA when available. + +This keeps T4 Spaces fast and makes oracle behavior undeniable. + +### Class: OracleAgent +Oracle agent wrapper. + +Default: baseline fallback (always available). +Optional: load a trained LoRA adapter when `lora_repo_id` provided. diff --git a/docs/api/ev_grid_oracle_parsing.md b/docs/api/ev_grid_oracle_parsing.md new file mode 100644 index 0000000..8d9ce80 --- /dev/null +++ b/docs/api/ev_grid_oracle_parsing.md @@ -0,0 +1,2 @@ +#### Function: parse_simulation_and_action +Parse both dream prediction and action (either can be missing). diff --git a/docs/api/ev_grid_oracle_policies.md b/docs/api/ev_grid_oracle_policies.md new file mode 100644 index 0000000..b9ee4b4 --- /dev/null +++ b/docs/api/ev_grid_oracle_policies.md @@ -0,0 +1,14 @@ +#### Function: baseline_policy +Greedy baseline: pick station minimizing (travel_time + wait + stress + price), avoid full. + +Deterministic given state. + +#### Function: always_defer_policy +Collapse baseline: always defer (reward-hack / fairness stressor). + +#### Function: always_load_shift_policy +Collapse baseline: always load_shift on head EV (ignores queues / grid). + +#### Function: nearest_travel_only_policy +Collapse baseline: minimize travel time only (ignores price, wait, stress). +Used to show greedy multi-objective baseline is not trivially dominated. diff --git a/docs/api/ev_grid_oracle_reward.md b/docs/api/ev_grid_oracle_reward.md new file mode 100644 index 0000000..de2bb3c --- /dev/null +++ b/docs/api/ev_grid_oracle_reward.md @@ -0,0 +1,14 @@ +#### Function: _graph_route_km +Approximate driving distance along the city graph using haversine edge weights. +Returns None if no path exists. + +#### Function: compute_reward +Deterministic, verifier-style reward with breakdown. + +Matches hackathon spec: wait, grid_stress, peak, renewable, urgency, anti-hack. + +#### Function: split_role_rewards +Deterministic role-level reward views derived from the same underlying breakdown. + +This is intentionally simple and bounded (judge-friendly): it does not claim +full MARL credit assignment, but it does make incentives explicit. diff --git a/docs/api/ev_grid_oracle_reward_hack.md b/docs/api/ev_grid_oracle_reward_hack.md new file mode 100644 index 0000000..e6f4421 --- /dev/null +++ b/docs/api/ev_grid_oracle_reward_hack.md @@ -0,0 +1,5 @@ +### Class: RewardHackDetector +Stateful, deterministic detector for common reward-hacking patterns. + +Goal: give the existing anti-hack flags "teeth" by detecting multi-step +exploit patterns, not just single-step invalidity. diff --git a/docs/api/ev_grid_oracle_road_models.md b/docs/api/ev_grid_oracle_road_models.md new file mode 100644 index 0000000..b8937ee --- /dev/null +++ b/docs/api/ev_grid_oracle_road_models.md @@ -0,0 +1,3 @@ +### Class: RoadAction +Minimal action space for RL on a real road graph: +choose the next connected node (no teleportation). diff --git a/docs/api/ev_grid_oracle_scenarios.md b/docs/api/ev_grid_oracle_scenarios.md new file mode 100644 index 0000000..333fca0 --- /dev/null +++ b/docs/api/ev_grid_oracle_scenarios.md @@ -0,0 +1,11 @@ +### Class: ScenarioModifiers +Lightweight knobs applied on top of the core simulator. +These are intentionally simple and deterministic for replayable judging. + +#### Function: scenario_schedule +Deterministic, fixed-tick stress tests (OpenOfficeRL-style). + +Note: ticks are env steps (5-minute increments by default). + +#### Function: apply_scenario_events +Returns updated modifiers and the list of events that fired this tick. diff --git a/docs/api/ev_grid_oracle_traffic.md b/docs/api/ev_grid_oracle_traffic.md new file mode 100644 index 0000000..ae17c15 --- /dev/null +++ b/docs/api/ev_grid_oracle_traffic.md @@ -0,0 +1,8 @@ +#### Function: _stable_u01 +Stable pseudo-random in [0,1) from input parts. +Deterministic across processes and Python versions. + +### Class: TrafficModel +Deterministic synthetic traffic for hackathon demos. + +Returns a multiplier m in [0.35, 1.15] to scale base travel_s on an edge. diff --git a/docs/api/ev_grid_oracle_world_model_verifier.md b/docs/api/ev_grid_oracle_world_model_verifier.md new file mode 100644 index 0000000..a68c9f0 --- /dev/null +++ b/docs/api/ev_grid_oracle_world_model_verifier.md @@ -0,0 +1,7 @@ +#### Function: rollout_deterministic_5ticks +Deterministic verifier rollout: apply action once, then advance 5 ticks with *no new arrivals*. +This is intentionally verifier-friendly (stable + reproducible) for RLVR. + +#### Function: score_prediction +Score dream-state prediction accuracy against a deterministic T+5 verifier rollout. +Returns score in [0,1]. diff --git a/docs/api/server___init__.md b/docs/api/server___init__.md new file mode 100644 index 0000000..1fe17d8 --- /dev/null +++ b/docs/api/server___init__.md @@ -0,0 +1,2 @@ +## Module: __init__.py +Server package for OpenEnv runtime. diff --git a/docs/api/server_app.md b/docs/api/server_app.md new file mode 100644 index 0000000..eeb11c9 --- /dev/null +++ b/docs/api/server_app.md @@ -0,0 +1,20 @@ +#### Function: _demo_oracle_act_with_guard +Run oracle policy with CPU-Space-safe guards. + +Returns: action, oracle_text, oracle_llm_active, oracle_timed_out, oracle_skipped_env + +#### Function: healthz +HF Spaces / cold-start friendly health endpoint. +Keep it fast and dependency-safe (no heavy routing work). + +#### Function: _graph_route_polyline +Return a render-friendly polyline (lat/lng pairs) along the station graph. +v0 fallback was a straight line; this produces a multi-point path so the UI reads like navigation. + +#### Function: _spawn_road_point_away_from_stations +Pick a deterministic road-graph node location (lat,lng) that is not within +`min_station_dist_m` of any station. Deterministic for a given seed_key. + +#### Function: demo_spawn_vehicle +Spawn a new EV at a valid road location (away from stations) and immediately compute +an assignment + route event for the frontend. diff --git a/docs/api/server_ev_grid_road_environment.md b/docs/api/server_ev_grid_road_environment.md new file mode 100644 index 0000000..7e9936b --- /dev/null +++ b/docs/api/server_ev_grid_road_environment.md @@ -0,0 +1,3 @@ +### Class: EVGridRoadEnvironment +Separate OpenEnv environment that forces real-road-graph actions. +Mounted as a sub-app under /road/ so it doesn't break the existing env. diff --git a/docs/api/server_role_metrics.md b/docs/api/server_role_metrics.md new file mode 100644 index 0000000..63e0034 --- /dev/null +++ b/docs/api/server_role_metrics.md @@ -0,0 +1,5 @@ +#### Function: compute_role_reward_breakdown +Lightweight, explainable credit assignment for demo storytelling. + +This is NOT a full MARL credit assignment — it allocates the *same* component +values across roles with fixed weights so totals remain easy to interpret. diff --git a/docs/api/tests_test_env_determinism.md b/docs/api/tests_test_env_determinism.md new file mode 100644 index 0000000..d083435 --- /dev/null +++ b/docs/api/tests_test_env_determinism.md @@ -0,0 +1,2 @@ +## Module: test_env_determinism.py +Determinism + strict action validation (core env, no LLM). diff --git a/docs/api/tests_test_policies_collapse.md b/docs/api/tests_test_policies_collapse.md new file mode 100644 index 0000000..b2cf32c --- /dev/null +++ b/docs/api/tests_test_policies_collapse.md @@ -0,0 +1,2 @@ +## Module: test_policies_collapse.py +Smoke tests for collapse / stressor policies (deterministic, no env crashes). diff --git a/docs/api/tools_build_road_graph.md b/docs/api/tools_build_road_graph.md new file mode 100644 index 0000000..26ed695 --- /dev/null +++ b/docs/api/tools_build_road_graph.md @@ -0,0 +1,13 @@ +#### Function: encode_polyline_latlng +Google polyline encoding for [lat,lng] points. +Stored as a compact ASCII string to shrink graph artifacts. + +#### Function: build_adjacency +Pass 1: build point adjacency over snapped coordinates. +Intersections/endpoints are nodes where degree != 2. + +#### Function: contract_edges +Pass 2: for each way, contract degree-2 chains into intersection-to-intersection edges. + +#### Function: filter_largest_component +Pass 3: Keep only the largest connected component (by node count) to satisfy routing coverage. diff --git a/docs/api/tools_export_grpo_tensorboard_plots.md b/docs/api/tools_export_grpo_tensorboard_plots.md new file mode 100644 index 0000000..4a12817 --- /dev/null +++ b/docs/api/tools_export_grpo_tensorboard_plots.md @@ -0,0 +1,10 @@ +## Module: export_grpo_tensorboard_plots.py +Export loss + reward (or closest TRL scalar tags) from a TensorBoard run dir into PNGs. + +Hackathon requirement: committed plots from a *real* GRPO run. After `trainer.train()` in +`training/train_grpo.ipynb`, copy `ev_oracle_grpo_road/` from Colab (or run locally), then: + + pip install tensorboard matplotlib + python tools/export_grpo_tensorboard_plots.py --logdir ev_oracle_grpo_road --out-dir artifacts + +Writes e.g. artifacts/grpo_loss.png and artifacts/grpo_reward.png (filenames depend on tags found). diff --git a/docs/api/tools_sync_space_to_hub.md b/docs/api/tools_sync_space_to_hub.md new file mode 100644 index 0000000..5c51464 --- /dev/null +++ b/docs/api/tools_sync_space_to_hub.md @@ -0,0 +1,12 @@ +## Module: sync_space_to_hub.py +Push this repo to a Hugging Face *Space* without using `git push` (avoids Hub binary rejections). + +Docker Spaces often have **no** “link GitHub repo” in Settings — the Space is its own Hub git repo. +Use this script after `git push origin main`; it uploads sources + a fresh `web/dist` via the Hub API. + +Usage: + cd repo root + npm --prefix web run build # or let this script run it (default) + python tools/sync_space_to_hub.py + +Requires: `pip install huggingface_hub`, token with write access (`HF_TOKEN` or `huggingface-cli login`). diff --git a/docs/api/tools_write_eval_snapshot.md b/docs/api/tools_write_eval_snapshot.md new file mode 100644 index 0000000..1b2e3e3 --- /dev/null +++ b/docs/api/tools_write_eval_snapshot.md @@ -0,0 +1,2 @@ +## Module: write_eval_snapshot.py +Run a tiny paired evaluate.py job and write artifacts/eval_snapshot.json (no LLM). diff --git a/docs/api/training___init__.md b/docs/api/training___init__.md new file mode 100644 index 0000000..0c0d479 --- /dev/null +++ b/docs/api/training___init__.md @@ -0,0 +1,2 @@ +## Module: __init__.py +Training scripts (not imported by server/runtime). diff --git a/docs/api/training_fair_eval.md b/docs/api/training_fair_eval.md new file mode 100644 index 0000000..d3924b7 --- /dev/null +++ b/docs/api/training_fair_eval.md @@ -0,0 +1,19 @@ +#### Function: _binom_two_sided_exact_p +Two-sided exact test for Binomial(n, p); used for McNemar discordant pairs (p=0.5). + +#### Function: mcnemar_discordant +McNemar on paired binary outcomes. +b01 = count(baseline True, oracle False); b10 = count(baseline False, oracle True). + +#### Function: paired_mcnemar_analysis +Paired McNemar for headline binaries (same rows as Wilson chart). + +#### Function: wilson_interval +Wilson score interval for a binomial proportion. +Returns (low, high, p_hat). For n==0 returns (nan, nan, nan). + +#### Function: _paired_improvement_counts +Operational 'wins' where oracle strictly improves a binary bad outcome vs baseline. + +#### Function: plot_fair_eval +Bar chart: select headline baseline vs oracle binary rates with Wilson error bars. diff --git a/docs/api/viz_record.md b/docs/api/viz_record.md new file mode 100644 index 0000000..aef864b --- /dev/null +++ b/docs/api/viz_record.md @@ -0,0 +1,4 @@ +#### Function: record +Record frames as PNGs. + +- `tick_every_frames`: how many frames to show per env.step() (slows animation, looks smoother). diff --git a/docs/knowledge_graph.json b/docs/knowledge_graph.json new file mode 100644 index 0000000..37786f4 --- /dev/null +++ b/docs/knowledge_graph.json @@ -0,0 +1,1542 @@ +{ + "test_script.py": { + "classes": [ + { + "name": "ChargerType", + "docstring": null + }, + { + "name": "StationState", + "docstring": null + } + ], + "functions": [] + }, + "viz/record_two_phase.py": { + "classes": [], + "functions": [ + { + "name": "_step_action", + "docstring": null + }, + { + "name": "record_phase", + "docstring": null + }, + { + "name": "main", + "docstring": null + } + ] + }, + "viz/city_map.py": { + "classes": [ + { + "name": "RenderConfig", + "docstring": null + }, + { + "name": "CityMapRenderer", + "docstring": null + } + ], + "functions": [ + { + "name": "_station_color", + "docstring": null + }, + { + "name": "_norm", + "docstring": null + }, + { + "name": "run_live", + "docstring": null + }, + { + "name": "__init__", + "docstring": null + }, + { + "name": "xy", + "docstring": null + }, + { + "name": "draw_arrow", + "docstring": null + }, + { + "name": "render", + "docstring": null + }, + { + "name": "_draw_background", + "docstring": null + }, + { + "name": "_draw_edges", + "docstring": null + }, + { + "name": "_draw_glow", + "docstring": null + }, + { + "name": "_draw_animated_route", + "docstring": null + }, + { + "name": "blit_line", + "docstring": null + } + ] + }, + "viz/record.py": { + "classes": [], + "functions": [ + { + "name": "record", + "docstring": "Record frames as PNGs.\n\n- `tick_every_frames`: how many frames to show per env.step() (slows animation, looks smoother)." + }, + { + "name": "main", + "docstring": null + } + ] + }, + "viz/gradio_demo.py": { + "classes": [ + { + "name": "Session", + "docstring": null + } + ], + "functions": [ + { + "name": "_norm", + "docstring": null + }, + { + "name": "_station_color", + "docstring": null + }, + { + "name": "render_map", + "docstring": null + }, + { + "name": "new_session", + "docstring": null + }, + { + "name": "step_once", + "docstring": null + }, + { + "name": "compute_kpis", + "docstring": null + }, + { + "name": "xy", + "docstring": null + }, + { + "name": "_start", + "docstring": null + }, + { + "name": "_step", + "docstring": null + }, + { + "name": "_run60", + "docstring": null + }, + { + "name": "_start_and_maybe_autoplay", + "docstring": null + }, + { + "name": "_kpis", + "docstring": null + } + ] + }, + "server/app.py": { + "classes": [ + { + "name": "DemoNewRequest", + "docstring": null + }, + { + "name": "MANewRequest", + "docstring": null + }, + { + "name": "MAAutoStepRequest", + "docstring": null + }, + { + "name": "DemoSpawnVehicleRequest", + "docstring": null + } + ], + "functions": [ + { + "name": "_request_id", + "docstring": null + }, + { + "name": "_oracle_skip_llm_env", + "docstring": null + }, + { + "name": "_rate_limit", + "docstring": null + }, + { + "name": "_demo_oracle_act_with_guard", + "docstring": "Run oracle policy with CPU-Space-safe guards.\n\nReturns: action, oracle_text, oracle_llm_active, oracle_timed_out, oracle_skipped_env" + }, + { + "name": "root", + "docstring": null + }, + { + "name": "healthz", + "docstring": "HF Spaces / cold-start friendly health endpoint.\nKeep it fast and dependency-safe (no heavy routing work)." + }, + { + "name": "_osm_route_polyline", + "docstring": null + }, + { + "name": "_graph_route_polyline", + "docstring": "Return a render-friendly polyline (lat/lng pairs) along the station graph.\nv0 fallback was a straight line; this produces a multi-point path so the UI reads like navigation." + }, + { + "name": "_spawn_road_point_away_from_stations", + "docstring": "Pick a deterministic road-graph node location (lat,lng) that is not within\n`min_station_dist_m` of any station. Deterministic for a given seed_key." + }, + { + "name": "_demo_session_gc", + "docstring": null + }, + { + "name": "_demo_session_get", + "docstring": null + }, + { + "name": "_ma_gc", + "docstring": null + }, + { + "name": "_ma_get", + "docstring": null + }, + { + "name": "ma_new", + "docstring": null + }, + { + "name": "_grid_policy", + "docstring": null + }, + { + "name": "ma_auto_step", + "docstring": null + }, + { + "name": "ma_state", + "docstring": null + }, + { + "name": "ma_step", + "docstring": null + }, + { + "name": "_obs_to_jsonable", + "docstring": null + }, + { + "name": "_station_nodes", + "docstring": null + }, + { + "name": "demo_new", + "docstring": null + }, + { + "name": "demo_state", + "docstring": null + }, + { + "name": "demo_spawn_vehicle", + "docstring": "Spawn a new EV at a valid road location (away from stations) and immediately compute\nan assignment + route event for the frontend." + }, + { + "name": "demo_step", + "docstring": null + }, + { + "name": "main", + "docstring": null + }, + { + "name": "run", + "docstring": null + } + ] + }, + "server/ev_grid_environment.py": { + "classes": [ + { + "name": "EVGridEnvironment", + "docstring": null + } + ], + "functions": [ + { + "name": "__init__", + "docstring": null + }, + { + "name": "reset", + "docstring": null + }, + { + "name": "step", + "docstring": null + }, + { + "name": "state", + "docstring": null + } + ] + }, + "server/ev_grid_road_environment.py": { + "classes": [ + { + "name": "EVGridRoadEnvironment", + "docstring": "Separate OpenEnv environment that forces real-road-graph actions.\nMounted as a sub-app under /road/ so it doesn't break the existing env." + } + ], + "functions": [ + { + "name": "__init__", + "docstring": null + }, + { + "name": "reset", + "docstring": null + }, + { + "name": "step", + "docstring": null + }, + { + "name": "state", + "docstring": null + } + ] + }, + "server/role_metrics.py": { + "classes": [], + "functions": [ + { + "name": "compute_role_kpis", + "docstring": null + }, + { + "name": "compute_role_reward_breakdown", + "docstring": "Lightweight, explainable credit assignment for demo storytelling.\n\nThis is NOT a full MARL credit assignment \u2014 it allocates the *same* component\nvalues across roles with fixed weights so totals remain easy to interpret." + }, + { + "name": "_peak_risk_score", + "docstring": null + }, + { + "name": "summarize_action", + "docstring": null + }, + { + "name": "part", + "docstring": null + } + ] + }, + "server/road_router.py": { + "classes": [ + { + "name": "RoadRouter", + "docstring": null + } + ], + "functions": [ + { + "name": "haversine_m", + "docstring": null + }, + { + "name": "decode_polyline_latlng", + "docstring": null + }, + { + "name": "get_router", + "docstring": null + }, + { + "name": "_next", + "docstring": null + }, + { + "name": "load", + "docstring": null + }, + { + "name": "nearest_node", + "docstring": null + }, + { + "name": "route_polyline", + "docstring": null + }, + { + "name": "_w", + "docstring": null + } + ] + }, + "ev_grid_oracle/bescom_feed.py": { + "classes": [ + { + "name": "BESCOMFeedAPI", + "docstring": "Deterministic BESCOM feeder \"API mock\".\n\n- No network calls (HF Spaces safe).\n- Feeder loads are derived from: time-of-day + grid_load_pct + station loads.\n- Output is stable under (seed, scenario, tick) so judge replays match." + } + ], + "functions": [ + { + "name": "snapshot", + "docstring": null + }, + { + "name": "_stable_seed", + "docstring": null + }, + { + "name": "_zone_for_station", + "docstring": null + } + ] + }, + "ev_grid_oracle/road_models.py": { + "classes": [ + { + "name": "RoadAction", + "docstring": "Minimal action space for RL on a real road graph:\nchoose the next connected node (no teleportation)." + }, + { + "name": "RoadState", + "docstring": null + }, + { + "name": "RoadObservation", + "docstring": null + } + ], + "functions": [ + { + "name": "_non_trivial", + "docstring": null + } + ] + }, + "ev_grid_oracle/road_env.py": { + "classes": [ + { + "name": "RoadCore", + "docstring": null + } + ], + "functions": [ + { + "name": "reset", + "docstring": null + }, + { + "name": "step", + "docstring": null + }, + { + "name": "_obs", + "docstring": null + } + ] + }, + "ev_grid_oracle/multi_agent.py": { + "classes": [ + { + "name": "MultiAgentSession", + "docstring": "Minimal explicit multi-agent wrapper around EVGridCore.\n\n- GridOperator emits a directive (constraint signal) + optional message.\n- FleetDispatcher emits an action + optional message.\n- Resolver applies directive deterministically and steps EVGridCore." + } + ], + "functions": [ + { + "name": "step", + "docstring": null + }, + { + "name": "snapshot", + "docstring": "Read-only view of the underlying core state." + } + ] + }, + "ev_grid_oracle/city_graph.py": { + "classes": [ + { + "name": "StationSpec", + "docstring": null + } + ], + "functions": [ + { + "name": "get_station_by_id", + "docstring": null + }, + { + "name": "get_station_by_slug", + "docstring": null + }, + { + "name": "haversine_km", + "docstring": null + }, + { + "name": "_edge_minutes", + "docstring": null + }, + { + "name": "_add_chain_edges", + "docstring": null + }, + { + "name": "_add_dense_within_cluster", + "docstring": null + }, + { + "name": "build_city_graph", + "docstring": null + }, + { + "name": "travel_time_minutes", + "docstring": null + }, + { + "name": "nearest_stations_by_geo", + "docstring": null + } + ] + }, + "ev_grid_oracle/reward.py": { + "classes": [ + { + "name": "RewardWeights", + "docstring": null + } + ], + "functions": [ + { + "name": "_haversine_km", + "docstring": null + }, + { + "name": "_graph_route_km", + "docstring": "Approximate driving distance along the city graph using haversine edge weights.\nReturns None if no path exists." + }, + { + "name": "compute_reward", + "docstring": "Deterministic, verifier-style reward with breakdown.\n\nMatches hackathon spec: wait, grid_stress, peak, renewable, urgency, anti-hack." + }, + { + "name": "split_role_rewards", + "docstring": "Deterministic role-level reward views derived from the same underlying breakdown.\n\nThis is intentionally simple and bounded (judge-friendly): it does not claim\nfull MARL credit assignment, but it does make incentives explicit." + }, + { + "name": "add_flag", + "docstring": null + }, + { + "name": "f", + "docstring": null + } + ] + }, + "ev_grid_oracle/reward_hack.py": { + "classes": [ + { + "name": "RewardHackDetector", + "docstring": "Stateful, deterministic detector for common reward-hacking patterns.\n\nGoal: give the existing anti-hack flags \"teeth\" by detecting multi-step\nexploit patterns, not just single-step invalidity." + } + ], + "functions": [ + { + "name": "reset", + "docstring": null + }, + { + "name": "step", + "docstring": null + }, + { + "name": "add", + "docstring": null + } + ] + }, + "ev_grid_oracle/personas.py": { + "classes": [ + { + "name": "PersonaParams", + "docstring": null + } + ], + "functions": [ + { + "name": "choose_persona", + "docstring": null + } + ] + }, + "ev_grid_oracle/world_model_verifier.py": { + "classes": [ + { + "name": "PredictionScore", + "docstring": null + } + ], + "functions": [ + { + "name": "_top3", + "docstring": null + }, + { + "name": "rollout_deterministic_5ticks", + "docstring": "Deterministic verifier rollout: apply action once, then advance 5 ticks with *no new arrivals*.\nThis is intentionally verifier-friendly (stable + reproducible) for RLVR." + }, + { + "name": "score_prediction", + "docstring": "Score dream-state prediction accuracy against a deterministic T+5 verifier rollout.\nReturns score in [0,1]." + } + ] + }, + "ev_grid_oracle/grid_sim.py": { + "classes": [ + { + "name": "GridParams", + "docstring": null + } + ], + "functions": [ + { + "name": "_clamp01", + "docstring": null + }, + { + "name": "baseline_grid_load", + "docstring": null + }, + { + "name": "renewable_pct", + "docstring": null + }, + { + "name": "update_grid_load", + "docstring": null + } + ] + }, + "ev_grid_oracle/models.py": { + "classes": [ + { + "name": "ChargerType", + "docstring": null + }, + { + "name": "ChargeRate", + "docstring": null + }, + { + "name": "ActionType", + "docstring": null + }, + { + "name": "DayType", + "docstring": null + }, + { + "name": "PeakRisk", + "docstring": null + }, + { + "name": "StationState", + "docstring": null + }, + { + "name": "EVRequest", + "docstring": null + }, + { + "name": "BESCOMFeederState", + "docstring": "Lightweight, judge-friendly feeder snapshot (mocked but deterministic)." + }, + { + "name": "GridState", + "docstring": null + }, + { + "name": "EVGridAction", + "docstring": null + }, + { + "name": "EVGridObservation", + "docstring": null + }, + { + "name": "NegotiationMessage", + "docstring": "A short, bounded message used in the explicit multi-agent protocol.\n\nThis is *not* a free-form chat reward. It exists so judges can see\nnegotiation/constraints explicitly and we can penalize empty spam." + }, + { + "name": "GridDirective", + "docstring": "GridOperator -> FleetDispatcher constraint signal (verifiable)." + }, + { + "name": "MultiAgentStepRequest", + "docstring": null + }, + { + "name": "MultiAgentStepResponse", + "docstring": null + }, + { + "name": "SimTopStation", + "docstring": null + }, + { + "name": "SimulationPrediction", + "docstring": "Aggregated 'dream state' prediction for T+5 ticks.\nKept intentionally small and verifiable for hackathon judging." + } + ], + "functions": [ + { + "name": "to_jsonable", + "docstring": null + }, + { + "name": "_occupied_le_total", + "docstring": null + }, + { + "name": "_check_consistency", + "docstring": null + } + ] + }, + "ev_grid_oracle/scenarios.py": { + "classes": [ + { + "name": "ScenarioEvent", + "docstring": null + }, + { + "name": "ScenarioModifiers", + "docstring": "Lightweight knobs applied on top of the core simulator.\nThese are intentionally simple and deterministic for replayable judging." + } + ], + "functions": [ + { + "name": "scenario_schedule", + "docstring": "Deterministic, fixed-tick stress tests (OpenOfficeRL-style).\n\nNote: ticks are env steps (5-minute increments by default)." + }, + { + "name": "apply_scenario_events", + "docstring": "Returns updated modifiers and the list of events that fired this tick." + } + ] + }, + "ev_grid_oracle/env.py": { + "classes": [ + { + "name": "EVGridCore", + "docstring": "Core env logic (no HTTP). Server wraps this.\n\nv0 slice: deterministic schema, minimal dynamics.\nNext slices add demand_sim/grid_sim/reward engine." + } + ], + "functions": [ + { + "name": "_peak_risk", + "docstring": null + }, + { + "name": "_make_ev", + "docstring": null + }, + { + "name": "_apply_action", + "docstring": null + }, + { + "name": "_drain_queues_and_charging", + "docstring": null + }, + { + "name": "_update_station_waits", + "docstring": null + }, + { + "name": "_build_prompt", + "docstring": null + }, + { + "name": "reset", + "docstring": null + }, + { + "name": "step", + "docstring": null + }, + { + "name": "_apply_tariff_mult", + "docstring": null + } + ] + }, + "ev_grid_oracle/parsing.py": { + "classes": [], + "functions": [ + { + "name": "parse_simulation", + "docstring": null + }, + { + "name": "parse_action", + "docstring": null + }, + { + "name": "parse_simulation_and_action", + "docstring": "Parse both dream prediction and action (either can be missing)." + } + ] + }, + "ev_grid_oracle/oracle_agent.py": { + "classes": [ + { + "name": "OracleRuntime", + "docstring": "Singleton-style loader that prefers CUDA when available.\n\nThis keeps T4 Spaces fast and makes oracle behavior undeniable." + }, + { + "name": "OracleAgent", + "docstring": "Oracle agent wrapper.\n\nDefault: baseline fallback (always available).\nOptional: load a trained LoRA adapter when `lora_repo_id` provided." + } + ], + "functions": [ + { + "name": "load", + "docstring": null + }, + { + "name": "_ensure_loaded", + "docstring": null + }, + { + "name": "act", + "docstring": null + }, + { + "name": "act_with_text", + "docstring": null + }, + { + "name": "is_active", + "docstring": null + }, + { + "name": "_generate", + "docstring": null + } + ] + }, + "ev_grid_oracle/demand_sim.py": { + "classes": [ + { + "name": "DemandParams", + "docstring": null + } + ], + "functions": [ + { + "name": "_gaussian_bump", + "docstring": null + }, + { + "name": "expected_arrivals_per_step", + "docstring": null + }, + { + "name": "sample_arrivals_per_step", + "docstring": null + } + ] + }, + "ev_grid_oracle/policies.py": { + "classes": [], + "functions": [ + { + "name": "baseline_policy", + "docstring": "Greedy baseline: pick station minimizing (travel_time + wait + stress + price), avoid full.\n\nDeterministic given state." + }, + { + "name": "always_defer_policy", + "docstring": "Collapse baseline: always defer (reward-hack / fairness stressor)." + }, + { + "name": "always_load_shift_policy", + "docstring": "Collapse baseline: always load_shift on head EV (ignores queues / grid)." + }, + { + "name": "nearest_travel_only_policy", + "docstring": "Collapse baseline: minimize travel time only (ignores price, wait, stress).\nUsed to show greedy multi-objective baseline is not trivially dominated." + } + ] + }, + "ev_grid_oracle/traffic.py": { + "classes": [ + { + "name": "TrafficModel", + "docstring": "Deterministic synthetic traffic for hackathon demos.\n\nReturns a multiplier m in [0.35, 1.15] to scale base travel_s on an edge." + } + ], + "functions": [ + { + "name": "_clamp", + "docstring": null + }, + { + "name": "_stable_u01", + "docstring": "Stable pseudo-random in [0,1) from input parts.\nDeterministic across processes and Python versions." + }, + { + "name": "multiplier_for_edge", + "docstring": null + }, + { + "name": "hotspot", + "docstring": null + } + ] + }, + "tools/build_road_graph.py": { + "classes": [ + { + "name": "Node", + "docstring": null + } + ], + "functions": [ + { + "name": "haversine_m", + "docstring": null + }, + { + "name": "_encode_signed", + "docstring": null + }, + { + "name": "encode_polyline_latlng", + "docstring": "Google polyline encoding for [lat,lng] points.\nStored as a compact ASCII string to shrink graph artifacts." + }, + { + "name": "speed_kmh", + "docstring": null + }, + { + "name": "snap", + "docstring": null + }, + { + "name": "_coords_latlng_from_geojson_line", + "docstring": null + }, + { + "name": "parse_args", + "docstring": null + }, + { + "name": "build_adjacency", + "docstring": "Pass 1: build point adjacency over snapped coordinates.\nIntersections/endpoints are nodes where degree != 2." + }, + { + "name": "contract_edges", + "docstring": "Pass 2: for each way, contract degree-2 chains into intersection-to-intersection edges." + }, + { + "name": "filter_largest_component", + "docstring": "Pass 3: Keep only the largest connected component (by node count) to satisfy routing coverage." + }, + { + "name": "main", + "docstring": null + }, + { + "name": "add_neighbor", + "docstring": null + }, + { + "name": "get_node", + "docstring": null + }, + { + "name": "flush", + "docstring": null + } + ] + }, + "tools/sync_space_to_hub.py": { + "classes": [], + "functions": [ + { + "name": "main", + "docstring": null + } + ] + }, + "tools/generate_knowledge_graph.py": { + "classes": [], + "functions": [ + { + "name": "extract_info", + "docstring": null + }, + { + "name": "generate_knowledge_graph", + "docstring": null + } + ] + }, + "tools/fetch_bangalore_roads_overpass.py": { + "classes": [], + "functions": [ + { + "name": "_chunk", + "docstring": null + }, + { + "name": "_overpass_query", + "docstring": null + }, + { + "name": "_tile_bbox", + "docstring": null + }, + { + "name": "_http_post", + "docstring": null + }, + { + "name": "_to_geojson", + "docstring": null + }, + { + "name": "main", + "docstring": null + } + ] + }, + "tools/export_grpo_tensorboard_plots.py": { + "classes": [], + "functions": [ + { + "name": "_pick_tags", + "docstring": null + }, + { + "name": "main", + "docstring": null + }, + { + "name": "plot_tag", + "docstring": null + } + ] + }, + "tools/prune_osm_geojson.py": { + "classes": [], + "functions": [ + { + "name": "_pad_bbox", + "docstring": null + }, + { + "name": "_line_intersects_bbox", + "docstring": null + }, + { + "name": "_simplify_uniform", + "docstring": null + }, + { + "name": "main", + "docstring": null + } + ] + }, + "tools/road_reward_smoke.py": { + "classes": [], + "functions": [ + { + "name": "main", + "docstring": null + }, + { + "name": "parse", + "docstring": null + }, + { + "name": "reward", + "docstring": null + } + ] + }, + "tools/generate_health_dashboard.py": { + "classes": [], + "functions": [ + { + "name": "run_cmd", + "docstring": null + }, + { + "name": "get_git_stats", + "docstring": null + }, + { + "name": "get_leaderboard", + "docstring": null + }, + { + "name": "get_documentation_health", + "docstring": null + }, + { + "name": "fetch_github_stats", + "docstring": null + }, + { + "name": "run_pytest_cov", + "docstring": null + }, + { + "name": "run_radon", + "docstring": null + }, + { + "name": "run_bandit", + "docstring": null + }, + { + "name": "run_ruff", + "docstring": null + }, + { + "name": "calculate_health_scores", + "docstring": null + }, + { + "name": "generate_ai_insights", + "docstring": null + }, + { + "name": "main", + "docstring": null + } + ] + }, + "tools/write_eval_snapshot.py": { + "classes": [], + "functions": [ + { + "name": "main", + "docstring": null + } + ] + }, + "tools/fetch_osm_roads.py": { + "classes": [ + { + "name": "BBox", + "docstring": null + } + ], + "functions": [ + { + "name": "_fetch_overpass", + "docstring": null + }, + { + "name": "_simplify_line", + "docstring": null + }, + { + "name": "_to_feature_collection", + "docstring": null + }, + { + "name": "build_query", + "docstring": null + }, + { + "name": "main", + "docstring": null + } + ] + }, + "tools/build_roads_render.py": { + "classes": [], + "functions": [ + { + "name": "main", + "docstring": null + } + ] + }, + "training/fair_eval.py": { + "classes": [], + "functions": [ + { + "name": "_binom_two_sided_exact_p", + "docstring": "Two-sided exact test for Binomial(n, p); used for McNemar discordant pairs (p=0.5)." + }, + { + "name": "mcnemar_discordant", + "docstring": "McNemar on paired binary outcomes.\nb01 = count(baseline True, oracle False); b10 = count(baseline False, oracle True)." + }, + { + "name": "paired_mcnemar_analysis", + "docstring": "Paired McNemar for headline binaries (same rows as Wilson chart)." + }, + { + "name": "wilson_interval", + "docstring": "Wilson score interval for a binomial proportion.\nReturns (low, high, p_hat). For n==0 returns (nan, nan, nan)." + }, + { + "name": "_binary_keys", + "docstring": null + }, + { + "name": "analyze_per_episode", + "docstring": null + }, + { + "name": "_paired_improvement_counts", + "docstring": "Operational 'wins' where oracle strictly improves a binary bad outcome vs baseline." + }, + { + "name": "plot_fair_eval", + "docstring": "Bar chart: select headline baseline vs oracle binary rates with Wilson error bars." + }, + { + "name": "main", + "docstring": null + }, + { + "name": "pmf", + "docstring": null + }, + { + "name": "pair", + "docstring": null + }, + { + "name": "rate", + "docstring": null + }, + { + "name": "errs", + "docstring": null + } + ] + }, + "training/make_plots.py": { + "classes": [], + "functions": [ + { + "name": "_boxplot_compat", + "docstring": null + }, + { + "name": "_per_episode_rows", + "docstring": null + }, + { + "name": "plot_kpi_bars", + "docstring": null + }, + { + "name": "plot_episode_trajectories", + "docstring": null + }, + { + "name": "plot_delta_histograms", + "docstring": null + }, + { + "name": "plot_reward_breakdown", + "docstring": null + }, + { + "name": "plot_boxplots", + "docstring": null + }, + { + "name": "plot_oracle_win_rates", + "docstring": null + }, + { + "name": "plot_paired_scatter", + "docstring": null + }, + { + "name": "plot_binary_timeline", + "docstring": null + }, + { + "name": "plot_fair_eval_rates", + "docstring": null + }, + { + "name": "plot_mcnemar_summary", + "docstring": null + }, + { + "name": "plot_dashboard_grid", + "docstring": null + }, + { + "name": "main", + "docstring": null + }, + { + "name": "rate", + "docstring": null + } + ] + }, + "training/evaluate.py": { + "classes": [ + { + "name": "EpisodeMetrics", + "docstring": null + } + ], + "functions": [ + { + "name": "_episode_metrics_to_json", + "docstring": null + }, + { + "name": "run_episode", + "docstring": null + }, + { + "name": "summarize", + "docstring": null + }, + { + "name": "summarize_reward_breakdown", + "docstring": null + }, + { + "name": "main", + "docstring": null + } + ] + }, + "tests/test_demo_api.py": { + "classes": [], + "functions": [ + { + "name": "test_demo_new_and_step_roundtrip", + "docstring": null + }, + { + "name": "test_demo_spawn_vehicle_route_event", + "docstring": null + }, + { + "name": "test_demo_step_forced_action_validation_422", + "docstring": null + }, + { + "name": "test_health_shape", + "docstring": null + }, + { + "name": "test_demo_sessions_ttl_eviction", + "docstring": null + }, + { + "name": "test_ma_new_and_step_roundtrip", + "docstring": null + } + ] + }, + "tests/test_env_determinism.py": { + "classes": [], + "functions": [ + { + "name": "test_reset_state_identical_two_cores_same_seed", + "docstring": null + }, + { + "name": "test_step_sequence_identical_two_cores_same_actions", + "docstring": null + }, + { + "name": "test_ev_grid_action_rejects_malformed_payload", + "docstring": null + }, + { + "name": "test_route_action_requires_station", + "docstring": null + } + ] + }, + "tests/test_reward.py": { + "classes": [], + "functions": [ + { + "name": "test_reward_breakdown_has_keys_and_total", + "docstring": null + }, + { + "name": "test_deferring_critical_ev_penalized", + "docstring": null + }, + { + "name": "test_invalid_station_routes_penalized", + "docstring": null + }, + { + "name": "test_split_role_rewards_exception_handling", + "docstring": null + } + ] + }, + "tests/test_world_model_verifier.py": { + "classes": [], + "functions": [ + { + "name": "test_rollout_deterministic_is_stable", + "docstring": null + }, + { + "name": "test_prediction_score_higher_when_close", + "docstring": null + } + ] + }, + "tests/test_evaluate_paired.py": { + "classes": [], + "functions": [ + { + "name": "_chdir_repo_root", + "docstring": null + }, + { + "name": "test_baseline_rollout_identical_for_same_seed_and_scenario", + "docstring": null + }, + { + "name": "test_oracle_matches_baseline_when_skip_llm", + "docstring": null + }, + { + "name": "test_evaluate_cli_paired_json", + "docstring": null + }, + { + "name": "test_fair_eval_cli", + "docstring": null + } + ] + }, + "tests/test_models_and_graph.py": { + "classes": [], + "functions": [ + { + "name": "test_city_graph_connected_and_25_stations", + "docstring": null + }, + { + "name": "test_action_route_requires_station_id_and_zero_defer", + "docstring": null + }, + { + "name": "test_action_defer_requires_positive_defer_minutes", + "docstring": null + }, + { + "name": "test_time_advances_with_5min_steps", + "docstring": null + } + ] + }, + "tests/test_fair_eval_mcnemar.py": { + "classes": [], + "functions": [ + { + "name": "test_mcnemar_no_discordant_is_neutral", + "docstring": null + }, + { + "name": "test_mcnemar_strong_asymmetry_low_p", + "docstring": null + }, + { + "name": "test_paired_mcnemar_analysis_shape", + "docstring": null + } + ] + }, + "tests/test_parsing.py": { + "classes": [], + "functions": [ + { + "name": "test_parse_simulation_valid", + "docstring": null + }, + { + "name": "test_parse_simulation_missing_match", + "docstring": null + }, + { + "name": "test_parse_simulation_exception_handling", + "docstring": null + } + ] + }, + "tests/test_policies_collapse.py": { + "classes": [], + "functions": [ + { + "name": "_run_policy", + "docstring": null + }, + { + "name": "test_collapse_policies_do_not_crash", + "docstring": null + }, + { + "name": "test_collapse_policies_return_valid_actions_when_pending", + "docstring": null + } + ] + } +} \ No newline at end of file diff --git a/ev_grid_oracle/bescom_feed.py b/ev_grid_oracle/bescom_feed.py index 470686a..dc6945e 100644 --- a/ev_grid_oracle/bescom_feed.py +++ b/ev_grid_oracle/bescom_feed.py @@ -85,7 +85,7 @@ def snapshot( return out def _stable_seed(self, *, seed: int, scenario: str, tick: int) -> int: - s = f"{seed}:{scenario}:{tick}".encode("utf-8") + s = f"{seed}:{scenario}:{tick}".encode() h = sha1(s, usedforsecurity=False).hexdigest()[:8] return int(h, 16) diff --git a/ev_grid_oracle/city_graph.py b/ev_grid_oracle/city_graph.py index a03a2a1..cc48984 100644 --- a/ev_grid_oracle/city_graph.py +++ b/ev_grid_oracle/city_graph.py @@ -2,7 +2,6 @@ from dataclasses import dataclass from math import asin, cos, radians, sin, sqrt -from typing import Optional import networkx as nx @@ -266,7 +265,7 @@ def travel_time_minutes( from_station_id: str, to_station_id: str, *, - default_if_missing: Optional[float] = None, + default_if_missing: float | None = None, ) -> float: if from_station_id == to_station_id: return 0.0 diff --git a/ev_grid_oracle/env.py b/ev_grid_oracle/env.py index 149ead4..52c81ad 100644 --- a/ev_grid_oracle/env.py +++ b/ev_grid_oracle/env.py @@ -2,10 +2,10 @@ from dataclasses import dataclass, field from random import Random -from typing import Optional import networkx as nx +from .bescom_feed import BESCOMFeedAPI from .city_graph import STATIONS from .demand_sim import sample_arrivals_per_step from .grid_sim import update_grid_load @@ -19,10 +19,9 @@ PeakRisk, StationState, ) +from .personas import FleetMode, choose_persona from .reward import compute_reward from .reward_hack import RewardHackDetector -from .bescom_feed import BESCOMFeedAPI -from .personas import FleetMode, choose_persona from .scenarios import ( ScenarioEvent, ScenarioModifiers, @@ -46,7 +45,7 @@ class EVGridCore: max_steps: int = 48 step_minutes: int = 5 rng: Random = field(default_factory=Random) - _grid_state: Optional[GridState] = None + _grid_state: GridState | None = None scenario: ScenarioName = "baseline" _scenario_schedule: list[ScenarioEvent] = field(default_factory=list) _scenario_mods: ScenarioModifiers = field(default_factory=ScenarioModifiers) @@ -59,7 +58,7 @@ class EVGridCore: def reset( self, *, - seed: Optional[int] = None, + seed: int | None = None, scenario: ScenarioName = "baseline", fleet_mode: FleetMode = "mixed", ) -> EVGridObservation: @@ -180,8 +179,7 @@ def step(self, action: EVGridAction) -> EVGridObservation: new_total = self._scenario_mods.slot_derate.get(s.station_id) if new_total is not None and new_total < s.total_slots: s.total_slots = int(new_total) - if s.occupied_slots > s.total_slots: - s.occupied_slots = s.total_slots + s.occupied_slots = min(s.occupied_slots, s.total_slots) # 1) apply action (deterministic validation + state mutation) action_effect = _apply_action(prev_state, action) diff --git a/ev_grid_oracle/grid_sim.py b/ev_grid_oracle/grid_sim.py index 150ead2..373c29c 100644 --- a/ev_grid_oracle/grid_sim.py +++ b/ev_grid_oracle/grid_sim.py @@ -15,7 +15,7 @@ class GridParams: def _clamp01(x: float) -> float: - return 0.0 if x < 0.0 else 1.0 if x > 1.0 else x + return 0.0 if x < 0.0 else min(x, 1.0) def baseline_grid_load( diff --git a/ev_grid_oracle/models.py b/ev_grid_oracle/models.py index 0220184..4af8e93 100644 --- a/ev_grid_oracle/models.py +++ b/ev_grid_oracle/models.py @@ -1,10 +1,9 @@ from __future__ import annotations from enum import Enum -from typing import Any, Literal, Optional +from typing import Any, Literal from openenv.core.env_server.types import Action, Observation - from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator @@ -110,12 +109,12 @@ class EVGridAction(Action): action_type: ActionType ev_id: str - station_id: Optional[str] = None + station_id: str | None = None charge_rate: ChargeRate = ChargeRate.fast defer_minutes: int = Field(0, ge=0) @model_validator(mode="after") - def _check_consistency(self) -> "EVGridAction": + def _check_consistency(self) -> EVGridAction: if self.action_type == ActionType.route: if not self.station_id: raise ValueError("station_id required when action_type='route'") diff --git a/ev_grid_oracle/oracle_agent.py b/ev_grid_oracle/oracle_agent.py index cc0503d..2b86eb7 100644 --- a/ev_grid_oracle/oracle_agent.py +++ b/ev_grid_oracle/oracle_agent.py @@ -1,14 +1,13 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any, Optional, Tuple, cast +from typing import Any, cast from ev_grid_oracle.models import EVGridAction, GridState from ev_grid_oracle.parsing import parse_action from ev_grid_oracle.policies import baseline_policy - -_CACHE: dict[tuple[str, Optional[str]], tuple[Any, Any]] = {} +_CACHE: dict[tuple[str, str | None], tuple[Any, Any]] = {} _CACHE_LOCK = None @@ -69,7 +68,7 @@ class OracleAgent: Optional: load a trained LoRA adapter when `lora_repo_id` provided. """ - lora_repo_id: Optional[str] = None + lora_repo_id: str | None = None base_model_id: str = "unsloth/Qwen2.5-3B-Instruct" max_new_tokens: int = 140 @@ -129,7 +128,7 @@ def act(self, state: GridState, prompt: str, graph) -> EVGridAction: def act_with_text( self, state: GridState, prompt: str, graph - ) -> Tuple[EVGridAction, str]: + ) -> tuple[EVGridAction, str]: # choose target ev_id (matches env prompt builder v0) ev_id = state.pending_evs[0].ev_id if state.pending_evs else "EV-000" diff --git a/ev_grid_oracle/parsing.py b/ev_grid_oracle/parsing.py index daeaa81..320ea1e 100644 --- a/ev_grid_oracle/parsing.py +++ b/ev_grid_oracle/parsing.py @@ -1,17 +1,15 @@ from __future__ import annotations import re -from typing import Optional, Tuple from .models import ( ActionType, ChargeRate, EVGridAction, - SimulationPrediction, SimTopStation, + SimulationPrediction, ) - ACTION_RE = re.compile( r"ACTION:\s*(?Proute|defer|load_shift)\s*\n" r"STATION:\s*(?PBLR-\d\d|NONE)\s*\n" @@ -30,7 +28,7 @@ ) -def parse_simulation(text: str) -> Optional[SimulationPrediction]: +def parse_simulation(text: str) -> SimulationPrediction | None: m = SIM_RE.search(text) if not m: return None @@ -58,7 +56,7 @@ def parse_simulation(text: str) -> Optional[SimulationPrediction]: return None -def parse_action(text: str, *, ev_id: str) -> Optional[EVGridAction]: +def parse_action(text: str, *, ev_id: str) -> EVGridAction | None: m = ACTION_RE.search(text.strip()) if not m: return None @@ -84,7 +82,7 @@ def parse_action(text: str, *, ev_id: str) -> Optional[EVGridAction]: def parse_simulation_and_action( text: str, *, ev_id: str -) -> Tuple[Optional[SimulationPrediction], Optional[EVGridAction]]: +) -> tuple[SimulationPrediction | None, EVGridAction | None]: """ Parse both dream prediction and action (either can be missing). """ diff --git a/ev_grid_oracle/personas.py b/ev_grid_oracle/personas.py index eca963b..2385414 100644 --- a/ev_grid_oracle/personas.py +++ b/ev_grid_oracle/personas.py @@ -4,7 +4,6 @@ from random import Random from typing import Literal - FleetPersona = Literal[ "TaxiFleet", "CorporateShuttle", "DeliveryFleet", "PrivateOwner", "Emergency" ] diff --git a/ev_grid_oracle/road_models.py b/ev_grid_oracle/road_models.py index 1995308..4ab8013 100644 --- a/ev_grid_oracle/road_models.py +++ b/ev_grid_oracle/road_models.py @@ -1,6 +1,5 @@ from __future__ import annotations - from openenv.core.env_server.types import Action, Observation from pydantic import BaseModel, ConfigDict, Field, model_validator @@ -17,7 +16,7 @@ class RoadAction(Action): next_node: int = Field(..., ge=0) @model_validator(mode="after") - def _non_trivial(self) -> "RoadAction": + def _non_trivial(self) -> RoadAction: if self.current_node == self.next_node: raise ValueError("next_node must differ from current_node") return self diff --git a/ev_grid_oracle/scenarios.py b/ev_grid_oracle/scenarios.py index 2283489..ece3bb7 100644 --- a/ev_grid_oracle/scenarios.py +++ b/ev_grid_oracle/scenarios.py @@ -3,7 +3,6 @@ from dataclasses import dataclass from typing import Any, Literal, TypedDict - ScenarioName = Literal[ "baseline", "heatwave_peak", @@ -188,7 +187,7 @@ def apply_scenario_events( # Stable ids for bookmarks / UI (deterministic). for e in fired: - e.setdefault("id", f"{name}:{int(e['tick'])}:{str(e.get('type', ''))}") + e.setdefault("id", f"{name}:{int(e['tick'])}:{e.get('type', '')!s}") # Modifiers are "sticky": once an event changes a knob, it persists. for e in fired: diff --git a/ev_grid_oracle/traffic.py b/ev_grid_oracle/traffic.py index b8b74a1..327ee4a 100644 --- a/ev_grid_oracle/traffic.py +++ b/ev_grid_oracle/traffic.py @@ -6,7 +6,7 @@ def _clamp(x: float, lo: float, hi: float) -> float: - return lo if x < lo else hi if x > hi else x + return lo if x < lo else min(x, hi) def _stable_u01(*parts: object) -> float: diff --git a/ev_grid_oracle/world_model_verifier.py b/ev_grid_oracle/world_model_verifier.py index e0cb589..af56642 100644 --- a/ev_grid_oracle/world_model_verifier.py +++ b/ev_grid_oracle/world_model_verifier.py @@ -95,7 +95,7 @@ def score_prediction( overlap_score = max(0.0, 1.0 - overlap_err) score = 0.40 * grid_score + 0.25 * ren_score + 0.20 * jacc + 0.15 * overlap_score - score = 0.0 if score < 0.0 else 1.0 if score > 1.0 else score + score = 0.0 if score < 0.0 else min(score, 1.0) return PredictionScore( score_0_1=float(score), breakdown={ diff --git a/server/app.py b/server/app.py index 3e14b3f..993d271 100644 --- a/server/app.py +++ b/server/app.py @@ -1,14 +1,15 @@ from __future__ import annotations import concurrent.futures +import hashlib +import logging import os -from pathlib import Path import time from collections import OrderedDict -import logging -from server.road_router import get_router +from pathlib import Path + from ev_grid_oracle.traffic import TrafficModel -import hashlib +from server.road_router import get_router try: from openenv.core.env_server.http_server import create_app @@ -17,43 +18,43 @@ from typing import Any, Literal, cast from uuid import uuid4 -from pydantic import BaseModel, Field +import networkx as nx from fastapi import Body, HTTPException, Query, Request from fastapi.responses import HTMLResponse from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel, Field -from ev_grid_oracle.city_graph import build_city_graph, _BY_ID, _BY_SLUG -import networkx as nx +from ev_grid_oracle.city_graph import _BY_ID, _BY_SLUG, build_city_graph from ev_grid_oracle.env import EVGridCore, _build_prompt from ev_grid_oracle.models import ( ActionType, - EVRequest, EVGridAction, EVGridObservation, + EVRequest, GridDirective, MultiAgentStepRequest, NegotiationMessage, ) +from ev_grid_oracle.multi_agent import MultiAgentSession from ev_grid_oracle.oracle_agent import OracleAgent -from ev_grid_oracle.policies import baseline_policy from ev_grid_oracle.parsing import parse_simulation +from ev_grid_oracle.policies import baseline_policy from ev_grid_oracle.reward import split_role_rewards +from ev_grid_oracle.road_models import RoadAction, RoadObservation from ev_grid_oracle.scenarios import ScenarioName from ev_grid_oracle.world_model_verifier import ( rollout_deterministic_5ticks, score_prediction, ) -from ev_grid_oracle.multi_agent import MultiAgentSession from server.ev_grid_environment import EVGridEnvironment from server.ev_grid_road_environment import EVGridRoadEnvironment -from ev_grid_oracle.road_models import RoadAction, RoadObservation +from server.road_router import haversine_m from server.role_metrics import ( compute_role_kpis, compute_role_reward_breakdown, summarize_action, ) -from server.road_router import haversine_m log = logging.getLogger("ev-grid-oracle") if not log.handlers: @@ -249,7 +250,7 @@ def healthz(req: Request) -> dict[str, Any]: _DEMO_MAX_SESSIONS = int(os.getenv("DEMO_MAX_SESSIONS", "64")) # Ordered for deterministic eviction of oldest sessions. -_demo_sessions: "OrderedDict[str, tuple[float, EVGridCore]]" = OrderedDict() +_demo_sessions: OrderedDict[str, tuple[float, EVGridCore]] = OrderedDict() _demo_graph = build_city_graph() _SIM_VERSION = "2026-04-26.1" @@ -383,7 +384,7 @@ class DemoNewRequest(BaseModel): _MA_SESSION_TTL_SEC = int(os.getenv("MA_SESSION_TTL_SEC", "3600")) _MA_MAX_SESSIONS = int(os.getenv("MA_MAX_SESSIONS", "64")) -_ma_sessions: "OrderedDict[str, tuple[float, MultiAgentSession]]" = OrderedDict() +_ma_sessions: OrderedDict[str, tuple[float, MultiAgentSession]] = OrderedDict() def _ma_gc(now: float | None = None) -> None: @@ -1159,7 +1160,7 @@ def demo_step( seed_i = int(core._seed_for_bescom) scen = str(core.scenario) h = hashlib.sha1( - f"{seed_i}|{scen}|{mode}|ambient|{tick_i}".encode("utf-8"), + f"{seed_i}|{scen}|{mode}|ambient|{tick_i}".encode(), usedforsecurity=False, ).digest() a_i = int.from_bytes(h[:2], "big") % len(st.stations) diff --git a/server/road_router.py b/server/road_router.py index ee12289..8c6fb0a 100644 --- a/server/road_router.py +++ b/server/road_router.py @@ -1,11 +1,11 @@ from __future__ import annotations -import json import gzip +import json +from collections.abc import Callable from dataclasses import dataclass from math import asin, cos, radians, sin, sqrt from pathlib import Path -from typing import Callable, Optional import networkx as nx @@ -61,7 +61,7 @@ class RoadRouter: edge_geom: dict[tuple[int, int], list[list[float]]] @classmethod - def load(cls, path: Path) -> "RoadRouter": + def load(cls, path: Path) -> RoadRouter: if str(path).endswith(".gz"): with gzip.open(path, "rb") as f: obj = json.loads(f.read().decode("utf-8")) @@ -121,7 +121,7 @@ def route_polyline( dst_lng: float, traffic: TrafficModel | None = None, tick: int | None = None, - ) -> Optional[tuple[list[list[float]], list[int]]]: + ) -> tuple[list[list[float]], list[int]] | None: a = self.nearest_node(lat=src_lat, lng=src_lng) b = self.nearest_node(lat=dst_lat, lng=dst_lng) try: diff --git a/server/role_metrics.py b/server/role_metrics.py index 3ec1081..f82f356 100644 --- a/server/role_metrics.py +++ b/server/role_metrics.py @@ -95,7 +95,7 @@ def part(key: str) -> float: } for k in keys: wmap = weights.get(k, {}) - for r in out.keys(): + for r in out: out[r][k] = float(comps[k]) * float(wmap.get(r, 0.0)) totals: dict[Role, float] = {} diff --git a/tools/build_road_graph.py b/tools/build_road_graph.py index a68236f..e9b42b4 100644 --- a/tools/build_road_graph.py +++ b/tools/build_road_graph.py @@ -1,9 +1,9 @@ from __future__ import annotations import argparse -import json -import hashlib import gzip +import hashlib +import json from dataclasses import dataclass from math import asin, cos, radians, sin, sqrt from pathlib import Path @@ -11,7 +11,6 @@ import networkx as nx - ROOT = Path(__file__).resolve().parents[1] diff --git a/tools/build_roads_render.py b/tools/build_roads_render.py index f40f3de..a271d8a 100644 --- a/tools/build_roads_render.py +++ b/tools/build_roads_render.py @@ -5,7 +5,6 @@ from pathlib import Path from typing import Any - ROOT = Path(__file__).resolve().parents[1] diff --git a/tools/docs_sync.py b/tools/docs_sync.py new file mode 100644 index 0000000..19e9dba --- /dev/null +++ b/tools/docs_sync.py @@ -0,0 +1,59 @@ +import ast +import os + + +def extract_docstrings(filepath: str) -> str: + with open(filepath, "r", encoding="utf-8") as f: + try: + content = f.read() + tree = ast.parse(content) + except SyntaxError: + return "" + + docs = [] + + module_doc = ast.get_docstring(tree) + if module_doc: + docs.append(f"## Module: {os.path.basename(filepath)}") + docs.append(module_doc) + docs.append("") + + for node in ast.walk(tree): + if isinstance(node, ast.ClassDef): + docstring = ast.get_docstring(node) + if docstring: + docs.append(f"### Class: {node.name}") + docs.append(docstring) + docs.append("") + elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + docstring = ast.get_docstring(node) + if docstring: + docs.append(f"#### Function: {node.name}") + docs.append(docstring) + docs.append("") + + return "\n".join(docs) + + +def sync_docs(root_dir: str = "."): + output_dir = os.path.join(root_dir, "docs", "api") + os.makedirs(output_dir, exist_ok=True) + + for dirpath, dirnames, filenames in os.walk(root_dir): + dirnames[:] = [d for d in dirnames if not d.startswith(".")] + + for filename in filenames: + if filename.endswith(".py"): + filepath = os.path.join(dirpath, filename) + rel_path = os.path.relpath(filepath, root_dir) + docs = extract_docstrings(filepath) + if docs.strip(): + safe_filename = rel_path.replace(os.sep, "_").replace(".py", ".md") + out_path = os.path.join(output_dir, safe_filename) + with open(out_path, "w", encoding="utf-8") as f: + f.write(docs) + + +if __name__ == "__main__": + sync_docs() + print("Documentation sync complete.") diff --git a/tools/fetch_bangalore_roads_overpass.py b/tools/fetch_bangalore_roads_overpass.py index 4fdaa96..120d815 100644 --- a/tools/fetch_bangalore_roads_overpass.py +++ b/tools/fetch_bangalore_roads_overpass.py @@ -8,7 +8,6 @@ from pathlib import Path from typing import Any - ROOT = Path(__file__).resolve().parents[1] @@ -75,7 +74,7 @@ def _http_post(url: str, data: dict[str, str], *, retries: int = 3) -> bytes: try: with urllib.request.urlopen(req, timeout=240) as r: # nosec B310 return r.read() - except Exception as e: # noqa: BLE001 + except Exception as e: last_err = e if attempt >= retries: raise diff --git a/tools/fetch_osm_roads.py b/tools/fetch_osm_roads.py index e8fcd1d..b2fb3bb 100644 --- a/tools/fetch_osm_roads.py +++ b/tools/fetch_osm_roads.py @@ -8,7 +8,6 @@ from dataclasses import dataclass from pathlib import Path - OVERPASS_URL = "https://overpass-api.de/api/interpreter" diff --git a/tools/generate_health_dashboard.py b/tools/generate_health_dashboard.py index cf32189..39fa772 100644 --- a/tools/generate_health_dashboard.py +++ b/tools/generate_health_dashboard.py @@ -1,7 +1,8 @@ import json -import subprocess # nosec B404 import os +import subprocess # nosec B404 from datetime import datetime, timezone + from jinja2 import Environment, FileSystemLoader # Extract sensitive variables immediately to prevent child processes @@ -269,7 +270,6 @@ def generate_ai_insights(scores, complexity, vulns, lint_errors): except Exception as e: print(f"Failed to generate AI insights via OpenAI: {e}") # Fallback to static insights on error - pass # Static Fallback insights = [] diff --git a/tools/generate_knowledge_graph.py b/tools/generate_knowledge_graph.py new file mode 100644 index 0000000..6b412e6 --- /dev/null +++ b/tools/generate_knowledge_graph.py @@ -0,0 +1,66 @@ +import ast +import json +import os + + +def extract_info(filepath: str) -> dict[str, list[dict[str, str | None]]]: + with open(filepath, "r", encoding="utf-8") as f: + try: + content = f.read() + tree = ast.parse(content) + except SyntaxError: + return {"classes": [], "functions": []} + + classes: list[dict[str, str | None]] = [] + functions: list[dict[str, str | None]] = [] + + for node in ast.walk(tree): + if isinstance(node, ast.ClassDef): + docstring = ast.get_docstring(node) + classes.append( + { + "name": node.name, + "docstring": docstring, + } + ) + elif isinstance(node, ast.FunctionDef) or isinstance( + node, ast.AsyncFunctionDef + ): + # Only record top-level or method-level functions if needed, + # here we just grab all function defs + docstring = ast.get_docstring(node) + functions.append( + { + "name": node.name, + "docstring": docstring, + } + ) + + return {"classes": classes, "functions": functions} + + +def generate_knowledge_graph( + root_dir: str = ".", +) -> dict[str, dict[str, list[dict[str, str | None]]]]: + graph: dict[str, dict[str, list[dict[str, str | None]]]] = {} + for dirpath, dirnames, filenames in os.walk(root_dir): + # Ignore hidden system directories + dirnames[:] = [d for d in dirnames if not d.startswith(".")] + + for filename in filenames: + if filename.endswith(".py"): + filepath = os.path.join(dirpath, filename) + rel_path = os.path.relpath(filepath, root_dir) + info = extract_info(filepath) + if info["classes"] or info["functions"]: + graph[rel_path] = info + return graph + + +if __name__ == "__main__": + graph = generate_knowledge_graph() + output_path = os.path.join("docs", "knowledge_graph.json") + os.makedirs(os.path.dirname(output_path), exist_ok=True) + with open(output_path, "w", encoding="utf-8") as f: + json.dump(graph, f, indent=2) + print(f"Knowledge graph generated at {output_path}") diff --git a/tools/road_reward_smoke.py b/tools/road_reward_smoke.py index 41932b7..e8795b1 100644 --- a/tools/road_reward_smoke.py +++ b/tools/road_reward_smoke.py @@ -15,7 +15,7 @@ def main() -> int: ok = f"CURRENT_NODE: {st.node}\nNEXT_NODE: {int(nb)}\nREASON: go\nCONFIDENCE: 0.7\n" bad = f"CURRENT_NODE: {st.node + 999}\nNEXT_NODE: {int(nb)}\nREASON: hack\nCONFIDENCE: 0.7\n" - r = re.compile(r"CURRENT_NODE:\s*(\d+)\s*\nNEXT_NODE:\s*(\d+)\s*\n", re.I) + r = re.compile(r"CURRENT_NODE:\s*(\d+)\s*\nNEXT_NODE:\s*(\d+)\s*\n", re.IGNORECASE) def parse(t: str) -> RoadAction | None: m = r.search(t.strip()) diff --git a/training/train_grpo.ipynb b/training/train_grpo.ipynb index ef88aea..f6ac6ae 100644 --- a/training/train_grpo.ipynb +++ b/training/train_grpo.ipynb @@ -109,14 +109,12 @@ "outputs": [], "source": [ "import re\n", - "from typing import Optional\n", "\n", "from datasets import Dataset\n", "\n", "from ev_grid_oracle.road_env import RoadCore\n", "from ev_grid_oracle.road_models import RoadAction, RoadState\n", "\n", - "\n", "core = RoadCore(g=None, nodes=[]) # graph is loaded inside reset()" ] }, @@ -134,7 +132,7 @@ ")\n", "\n", "\n", - "def parse_action(text: str) -> Optional[RoadAction]:\n", + "def parse_action(text: str) -> RoadAction | None:\n", " m = ACTION_RE.search(text.strip())\n", " if not m:\n", " return None\n", diff --git a/viz/city_map.py b/viz/city_map.py index ac0b990..3e75a03 100644 --- a/viz/city_map.py +++ b/viz/city_map.py @@ -2,7 +2,6 @@ import math from dataclasses import dataclass -from typing import Optional import pygame @@ -28,7 +27,7 @@ def _norm(v: float, lo: float, hi: float) -> float: if hi <= lo: return 0.0 x = (v - lo) / (hi - lo) - return 0.0 if x < 0.0 else 1.0 if x > 1.0 else x + return 0.0 if x < 0.0 else min(x, 1.0) @dataclass @@ -91,7 +90,7 @@ def render( self, surf: pygame.Surface, *, - last_action: Optional[EVGridAction] = None, + last_action: EVGridAction | None = None, mode_label: str = "", ): cfg = self.cfg @@ -255,7 +254,7 @@ def run_live(seed: int = 123, *, mode: str = "baseline"): env.reset(seed=seed) renderer = CityMapRenderer(env, cfg) - last_action: Optional[EVGridAction] = None + last_action: EVGridAction | None = None running = True while running: for event in pygame.event.get(): diff --git a/viz/gradio_demo.py b/viz/gradio_demo.py index 26dbfa2..3b9d3fb 100644 --- a/viz/gradio_demo.py +++ b/viz/gradio_demo.py @@ -13,7 +13,6 @@ from ev_grid_oracle.policies import baseline_policy from training.evaluate import run_episode, summarize - Mode = Literal["Untrained Baseline", "Oracle Agent"] @@ -21,7 +20,7 @@ def _norm(v: float, lo: float, hi: float) -> float: if hi <= lo: return 0.0 x = (v - lo) / (hi - lo) - return 0.0 if x < 0.0 else 1.0 if x > 1.0 else x + return 0.0 if x < 0.0 else min(x, 1.0) def _station_color(load_pct: float) -> tuple[int, int, int]: diff --git a/viz/record.py b/viz/record.py index 2f0fbc0..d05caf7 100644 --- a/viz/record.py +++ b/viz/record.py @@ -2,7 +2,6 @@ import argparse from pathlib import Path -from typing import Optional import pygame @@ -37,7 +36,7 @@ def record( env.reset(seed=seed) renderer = CityMapRenderer(env, cfg) - last_action: Optional[EVGridAction] = None + last_action: EVGridAction | None = None frame = 0 for step in range(steps): st = env._grid_state diff --git a/viz/record_two_phase.py b/viz/record_two_phase.py index ac800ce..99402b7 100644 --- a/viz/record_two_phase.py +++ b/viz/record_two_phase.py @@ -1,8 +1,8 @@ from __future__ import annotations import argparse +from collections.abc import Callable from pathlib import Path -from typing import Callable, Optional import pygame @@ -13,7 +13,6 @@ from ev_grid_oracle.policies import baseline_policy from viz.city_map import CityMapRenderer, RenderConfig - PolicyFn = Callable[[GridState, object], EVGridAction] @@ -38,7 +37,7 @@ def record_phase( frame_start: int, policy: PolicyFn, ) -> int: - last_action: Optional[EVGridAction] = None + last_action: EVGridAction | None = None frame = frame_start for _ in range(steps): last_action = _step_action(env, policy)