From 6890d8bda9bd98cbdd3efdb7633e1a1f323b9ed6 Mon Sep 17 00:00:00 2001 From: khushalkottaru Date: Sun, 15 Mar 2026 11:03:06 -0700 Subject: [PATCH 1/9] chore: added personal toolkit files --- .coderabbit | 43 +++++ .github/workflows/sync-upstream.yml | 46 +++++ AGENTS.md | 0 CLAUDE.md | 71 ++++++++ jules_handoff.sh | 273 ++++++++++++++++++++++++++++ prep_pr.sh | 64 +++++++ 6 files changed, 497 insertions(+) create mode 100644 .coderabbit create mode 100644 .github/workflows/sync-upstream.yml create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 jules_handoff.sh create mode 100644 prep_pr.sh diff --git a/.coderabbit b/.coderabbit new file mode 100644 index 000000000..717968302 --- /dev/null +++ b/.coderabbit @@ -0,0 +1,43 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json + +language: "en-US" + +reviews: + profile: "assertive" + poem: false + in_progress_fortune: false + + auto_review: + enabled: true + drafts: false + ignore_title_keywords: + - "wip" + - "do not review" + + path_instructions: + - path: "**/*.py" + instructions: | + This is RocketPy, a scientific Python rocket trajectory simulator. + - Docstrings must use NumPy format with parameter types (e.g. `x : float`) but no units in the type field + - Do not add type hints to function signatures — the codebase does not use them + - No magic numbers without a comment explaining what they represent + - Flag any new public function or class missing a NumPy docstring + - Flag any logic that duplicates existing RocketPy utilities + + - path: "tests/**" + instructions: | + Tests use pytest with pytest.approx for float comparisons, not plain == or np.isclose. + Flag any float assertion that does not use pytest.approx. + Test names should describe the scenario being tested, not just the function name. + + finishing_touches: + docstrings: + enabled: true + unit_tests: + enabled: true + +knowledge_base: + web_search: + enabled: true + learnings: + scope: auto \ No newline at end of file diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml new file mode 100644 index 000000000..8a51c3194 --- /dev/null +++ b/.github/workflows/sync-upstream.yml @@ -0,0 +1,46 @@ +# .github/workflows/sync-upstream.yml +# Syncs your fork's default branch with the upstream repo daily. +# Also runs on manual trigger via workflow_dispatch. +# +# Setup: +# 1. Copy this file to .github/workflows/sync-upstream.yml in your fork +# 2. Set the UPSTREAM_REPO variable below to the upstream owner/repo + +name: Sync fork with upstream + +on: + schedule: + - cron: '0 12 * * *' # 5:00 AM PT daily (runs before Jules scout at 6) + workflow_dispatch: # manual trigger from Actions tab + +env: + UPSTREAM_REPO: RocketPy-Team/RocketPy # change per fork + BRANCH: main # change if upstream uses 'main' + +jobs: + sync: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout fork + uses: actions/checkout@v4 + with: + ref: ${{ env.BRANCH }} + fetch-depth: 0 + + - name: Add upstream remote + run: git remote add upstream https://github.com/${{ env.UPSTREAM_REPO }}.git + + - name: Fetch upstream + run: git fetch upstream ${{ env.BRANCH }} + + - name: Rebase on upstream + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git rebase upstream/${{ env.BRANCH }} + + - name: Push to fork + run: git push origin ${{ env.BRANCH }} --force-with-lease \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..e69de29bb diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..1b2d9a39a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,71 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +```bash +# Install dependencies +make install + +# Run tests +make pytest # standard test suite +make pytest-slow # slow/marked tests with verbose output +make coverage # tests with coverage + +# Lint and format +make format # ruff format + isort +make lint # ruff + pylint +make ruff-lint # ruff only +make pylint # pylint only + +# Docs +make build-docs +``` + +**Run a single test file:** +```bash +pytest tests/unit/test_environment.py -v +``` + +**Run a single test by name:** +```bash +pytest tests/unit/test_environment.py::test_methodname_expectedbehaviour -v +``` + +## Architecture + +RocketPy simulates 6-DOF rocket trajectories. The core workflow is linear: + +``` +Environment → Motor → Rocket → Flight +``` + +**`rocketpy/environment/`** — Atmospheric models, weather data fetching (NOAA, Wyoming soundings, GFS forecasts). The `Environment` class (~116KB) is the entry point for atmospheric conditions. + +**`rocketpy/motors/`** — `Motor` is the base class. `SolidMotor`, `HybridMotor`, and `LiquidMotor` extend it. `Tank`, `TankGeometry`, and `Fluid` support liquid/hybrid propellant modeling. + +**`rocketpy/rocket/`** — `Rocket` aggregates a motor and aerodynamic surfaces. `aero_surface/` contains fins, nose cone, and tail implementations. `Parachute` uses trigger functions for deployment. + +**`rocketpy/simulation/`** — `Flight` (~162KB) is the simulation engine, integrating equations of motion with scipy's LSODA solver. `MonteCarlo` orchestrates many `Flight` runs for dispersion analysis. + +**`rocketpy/stochastic/`** — Wraps any component (Environment, Rocket, Motor, Flight) with uncertainty distributions for Monte Carlo input generation. + +**`rocketpy/mathutils/`** — `Function` class wraps callable data (arrays, lambdas, files) with interpolation and mathematical operations. Heavily used throughout for aerodynamic curves, thrust profiles, etc. + +**`rocketpy/plots/` and `rocketpy/prints/`** — Visualization and text output, each mirroring the module structure of the core classes. + +**`rocketpy/sensors/`** — Simulated sensors (accelerometer, gyroscope, barometer, GNSS) that can be attached to a `Rocket`. + +**`rocketpy/sensitivity/`** — Global sensitivity analysis via `SensitivityModel`. + +## Coding Standards + +- **Docstrings:** NumPy style with `Parameters`, `Returns`, and `Examples` sections. Always include units for physical quantities (e.g., "in meters", "in radians"). +- **No type hints in function signatures** — put types in the docstring `Parameters` section instead. +- **SI units by default** throughout the codebase (meters, kilograms, seconds, radians). +- **No magic numbers** — name constants with `UPPER_SNAKE_CASE` and comment their physical meaning. +- **Performance:** Use vectorized numpy operations. Cache expensive computations with `cached_property`. +- **Test names:** `test_methodname_expectedbehaviour` pattern. Use `pytest.approx` for float comparisons. +- **Tests follow AAA** (Arrange, Act, Assert) with fixtures from `tests/fixtures/`. +- **Backward compatibility:** Use deprecation warnings before removing public API features; document changes in CHANGELOG. diff --git a/jules_handoff.sh b/jules_handoff.sh new file mode 100644 index 000000000..0ff7644e2 --- /dev/null +++ b/jules_handoff.sh @@ -0,0 +1,273 @@ +#!/bin/bash +# ───────────────────────────────────────────────────────── +# jules-handoff.sh +# Pulls a Jules branch and launches Claude Code with full context +# +# Usage: +# ./jules-handoff.sh # interactive branch picker +# ./jules-handoff.sh # direct branch name +# ./jules-handoff.sh # paste a Jules PR URL +# ───────────────────────────────────────────────────────── + +set -e + +REPO_DIR=$(git rev-parse --show-toplevel 2>/dev/null) +if [ -z "$REPO_DIR" ]; then + echo "❌ Not inside a git repo. Run this from your astropy fork directory." + exit 1 +fi + +cd "$REPO_DIR" + +# ── Detect upstream repo slug ───────────────────────────── + +UPSTREAM_URL=$(git remote get-url upstream 2>/dev/null || echo "") +if [ -z "$UPSTREAM_URL" ]; then + echo "❌ No upstream remote found. Run: git remote add upstream " + exit 1 +fi + +# Parse owner/repo from URL (handles both https and ssh) +UPSTREAM_REPO=$(echo "$UPSTREAM_URL" | sed 's|https://github.com/||' | sed 's|git@github.com:||' | sed 's|[.]git$||') + +echo "📡 Upstream: $UPSTREAM_REPO" + +# ── Step 1: Determine the Jules branch ─────────────────── + +if [ -n "$1" ]; then + INPUT="$1" + + # If it's a GitHub PR URL, extract the branch from the API + if [[ "$INPUT" == https://github.com/* ]]; then + echo "🔍 Fetching branch from PR URL..." + PR_NUMBER=$(echo "$INPUT" | grep -oE '/pull/[0-9]+' | grep -oE '[0-9]+') + REPO_SLUG=$(echo "$INPUT" | sed 's|https://github.com/||' | cut -d'/' -f1-2) + BRANCH=$(curl -s "https://api.github.com/repos/$REPO_SLUG/pulls/$PR_NUMBER" \ + | python3 -c "import sys,json; print(json.load(sys.stdin)['head']['ref'])") + echo "📌 Branch: $BRANCH" + else + BRANCH="$INPUT" + fi + +else + # Interactive: list recent Jules branches + echo "" + echo "🔍 Fetching recent branches from origin..." + git fetch origin --quiet + + echo "" + echo "Recent branches (newest first):" + echo "──────────────────────────────" + + BRANCHES=$(git branch -r --sort=-committerdate \ + | grep 'origin/' \ + | grep -v 'origin/main' \ + | sed 's|origin/||' \ + | head -20) + + if [ -z "$BRANCHES" ]; then + echo "No branches found other than main." + echo "Jules may not have created a branch yet — check jules.google.com" + exit 1 + fi + + i=1 + while IFS= read -r branch; do + echo " $i) $branch" + i=$((i+1)) + done <<< "$BRANCHES" + + echo "" + printf "Enter number of the Jules branch to work on: " + read -r SELECTION + + BRANCH=$(echo "$BRANCHES" | sed -n "${SELECTION}p" | xargs) + + if [ -z "$BRANCH" ]; then + echo "❌ Invalid selection" + exit 1 + fi +fi + +# ── Step 2: Pull the branch locally ────────────────────── + +echo "" +echo "📥 Checking out branch: $BRANCH" + +git fetch origin "$BRANCH" --quiet + +if git show-ref --verify --quiet "refs/heads/$BRANCH"; then + git checkout "$BRANCH" + git pull origin "$BRANCH" --quiet +else + git checkout -b "$BRANCH" "origin/$BRANCH" +fi + +echo "✅ On branch: $BRANCH" + +# ── Step 3: Read and extract selected issue from scout_report.md ── + +echo "" +echo "📋 Reading Jules scout report..." + +SCOUT_REPORT="" +if [ -f "scout_report.md" ]; then + # Read the selected issue number from the SELECTED ISSUE line + SELECTED=$(grep "SELECTED ISSUE:" scout_report.md | grep -oE '[0-9]+' | head -1) + + if [ -z "$SELECTED" ]; then + echo "" + echo "👉 Open scout_report.md and fill in the SELECTED ISSUE number (1, 2, or 3)" + echo " Then re-run this script." + exit 0 + fi + + echo "✅ Selected issue: #$SELECTED" + + # Extract only the selected issue section + SCOUT_REPORT=$(python3 - "$SELECTED" << 'PYEOF' +import sys, re + +selected = sys.argv[1] +with open("scout_report.md") as f: + text = f.read() + +# Find the section matching "## Issue N" +pattern = rf"(## Issue {selected} —.*?)(?= +## Issue \d|$)" +match = re.search(pattern, text, re.DOTALL) +if match: + print(match.group(1).strip()) +else: + print(f"Could not find Issue {selected} in scout_report.md") +PYEOF +) + echo "✅ Extracted Issue $SELECTED from scout_report.md" +else + echo "⚠️ No scout_report.md found — will use PR description only" +fi + +# ── Step 4: Get PR info and extract issue number ────────── + +PR_BODY="" +PR_TITLE="" +ISSUE_NUMBER="" +ISSUE_STATUS="" +ISSUE_BODY="" + +if command -v gh &> /dev/null; then + PR_JSON=$(gh pr list --head "$BRANCH" --json body,title,url 2>/dev/null || echo "[]") + PR_TITLE=$(echo "$PR_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d[0]['title'] if d else '')" 2>/dev/null || echo "") + PR_BODY=$(echo "$PR_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d[0]['body'] if d else '')" 2>/dev/null || echo "") + + # Try to extract issue number from PR body or branch name + ISSUE_NUMBER=$(echo "$PR_BODY $BRANCH" | grep -oE '#[0-9]+|issues/[0-9]+' | grep -oE '[0-9]+' | head -1) + + if [ -n "$ISSUE_NUMBER" ]; then + echo "🔍 Checking upstream issue #$ISSUE_NUMBER..." + ISSUE_JSON=$(gh issue view "$ISSUE_NUMBER" --repo "$UPSTREAM_REPO" --json state,title,body 2>/dev/null || echo "") + + if [ -n "$ISSUE_JSON" ]; then + ISSUE_STATUS=$(echo "$ISSUE_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('state','unknown'))") + ISSUE_TITLE=$(echo "$ISSUE_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('title',''))") + ISSUE_BODY=$(echo "$ISSUE_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('body','')[:800])") + echo "📌 Issue #$ISSUE_NUMBER is: $ISSUE_STATUS — $ISSUE_TITLE" + + if [ "$ISSUE_STATUS" = "closed" ]; then + echo "" + echo "⛔ Issue #$ISSUE_NUMBER is already CLOSED — someone already fixed it." + echo " Aborted. Pick a different Jules branch." + exit 0 + fi + fi + + # Check for competing open PRs + echo "🔍 Checking for competing PRs on issue #$ISSUE_NUMBER..." + COMPETING=$(gh pr list --repo "$UPSTREAM_REPO" --search "fixes #$ISSUE_NUMBER" --json number,title,state,isDraft,url 2>/dev/null || echo "[]") + + PR_COUNT=$(echo "$COMPETING" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))") + + if [ "$PR_COUNT" -gt "0" ]; then + echo "" + echo "⛔ STOP — There is already an open PR for issue #$ISSUE_NUMBER:" + echo "$COMPETING" | python3 -c " +import sys, json +prs = json.load(sys.stdin) +for pr in prs: + draft = ' [DRAFT]' if pr.get('isDraft') else '' + print(' #' + str(pr['number']) + draft + ': ' + pr['title']) + print(' ' + pr['url']) +" + echo "" + echo " Don't waste your time — pick a different Jules branch." + exit 0 + else + echo "✅ No competing PRs found — you're clear to proceed." + fi + fi +fi + +# ── Step 5: Write Claude Code context file ──────────────── + +CONTEXT_FILE="/tmp/jules-context-$(date +%s).md" + +cat > "$CONTEXT_FILE" << CONTEXT +# Jules Handoff Context + +## Branch +$BRANCH + +## Jules PR Title +${PR_TITLE:-"(not found)"} + +--- + +## Jules Scout Report (scout.md) +${SCOUT_REPORT:-"(No scout_report.md on this branch — refer to PR description below)"} + +--- + +## Jules PR Description +${PR_BODY:-"(No PR found — check jules.google.com)"} + +--- + +## Upstream Issue #${ISSUE_NUMBER:-"unknown"} +Status: ${ISSUE_STATUS:-"unknown"} +Title: ${ISSUE_TITLE:-"unknown"} + +${ISSUE_BODY:-"(Could not fetch issue body — check https://github.com/astropy/astropy/issues)"} + +--- + +## Your job (Claude Code) + +The scout report and issue above are your source of truth — ignore any diff noise +from main being ahead of this branch due to the daily sync automation. + +1. Read scout.md carefully — Jules has already identified the files and approach. + +2. Check if the issue is still open and unassigned upstream before starting: + https://github.com/astropy/astropy/issues/${ISSUE_NUMBER:-""} + +3. Run the existing tests for the relevant subpackage first to establish a baseline: + \`python -m pytest astropy//tests/ -x -v\` + +4. Implement the fix described in scout.md. Follow astropy standards: + - Type hints on all new functions + - Numpy-style docstrings + - Tests in the corresponding tests/ directory + - No public API changes unless the issue explicitly requires it + +5. Once tests pass, give me a 3-sentence summary of what changed for the PR description. +CONTEXT + +echo "✅ Context ready" + +# ── Step 6: Print context and launch Claude Code ────────── + +echo "" +echo "🚀 Launching Claude Code..." +echo "" + +claude < "$CONTEXT_FILE" \ No newline at end of file diff --git a/prep_pr.sh b/prep_pr.sh new file mode 100644 index 000000000..bac87e26b --- /dev/null +++ b/prep_pr.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# prep_pr.sh — strips toolkit files from the current branch before opening a PR +# Usage: ./prep_pr.sh +# Run after cleaning up commit history. Modifies the current branch in place. + +set -euo pipefail + +TOOLKIT_FILES=( + "AGENTS.md" + "CLAUDE.md" + "jules_handoff.sh" + ".coderabbit.yaml" + "scout_report.md" + ".github/workflows/sync-upstream.yml" + ".github/workflows/scout.yml" +) + +RED='\033[0;31m'; YELLOW='\033[1;33m'; GREEN='\033[0;32m'; NC='\033[0m' +die() { echo -e "${RED}error: $1${NC}" >&2; exit 1; } +warn() { echo -e "${YELLOW}warn: $1${NC}"; } +ok() { echo -e "${GREEN}ok: $1${NC}"; } + +command -v git &>/dev/null || die "git not found" +git rev-parse --git-dir &>/dev/null || die "not inside a git repo" + +BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null) \ + || die "HEAD is detached — check out your working branch first" + +[[ "$BRANCH" == "master" || "$BRANCH" == "main" ]] \ + && die "you're on $BRANCH — check out your working branch first" + +git diff --quiet && git diff --cached --quiet \ + || die "you have uncommitted changes — commit or stash them first" + +echo "" +echo " Branch : $BRANCH" +echo " Will strip toolkit files and add a cleanup commit." +echo "" +read -rp "Proceed? [y/N] " confirm +[[ "${confirm,,}" == "y" ]] || { echo "Aborted."; exit 0; } + +STRIPPED=() +for f in "${TOOLKIT_FILES[@]}"; do + if git ls-files --error-unmatch "$f" &>/dev/null 2>&1; then + git rm -f "$f" + STRIPPED+=("$f") + fi +done + +if [[ ${#STRIPPED[@]} -eq 0 ]]; then + warn "no toolkit files found — nothing to strip" + exit 0 +fi + +git commit -m "chore: strip toolkit files before PR + +Local workflow helpers not intended for upstream: +$(printf ' - %s\n' "${STRIPPED[@]}")" + +ok "stripped: ${STRIPPED[*]}" +echo "" +echo " Next:" +echo " git push origin $BRANCH" +echo " open PR from $BRANCH → upstream master" \ No newline at end of file From ccd52aa11ff4cdb6bc059f9d09f4d34e786097c1 Mon Sep 17 00:00:00 2001 From: khushalkottaru Date: Sun, 15 Mar 2026 11:08:08 -0700 Subject: [PATCH 2/9] update branch name in workflow --- .github/workflows/sync-upstream.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml index 8a51c3194..061f1a812 100644 --- a/.github/workflows/sync-upstream.yml +++ b/.github/workflows/sync-upstream.yml @@ -10,12 +10,12 @@ name: Sync fork with upstream on: schedule: - - cron: '0 12 * * *' # 5:00 AM PT daily (runs before Jules scout at 6) + - cron: '0 12 * * *' # 5:00 AM PT daily workflow_dispatch: # manual trigger from Actions tab env: UPSTREAM_REPO: RocketPy-Team/RocketPy # change per fork - BRANCH: main # change if upstream uses 'main' + BRANCH: master # change if upstream uses 'main' jobs: sync: From 4d5f1876f48450eec2057664e2630452075764b8 Mon Sep 17 00:00:00 2001 From: khushalkottaru Date: Mon, 16 Mar 2026 22:13:13 -0700 Subject: [PATCH 3/9] chore: update toolkit files --- CLAUDE.md | 41 +++++++++ jules_handoff.sh | 211 +++++++++++++++++++---------------------------- 2 files changed, 127 insertions(+), 125 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1b2d9a39a..d70b2e15c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -69,3 +69,44 @@ Environment → Motor → Rocket → Flight - **Test names:** `test_methodname_expectedbehaviour` pattern. Use `pytest.approx` for float comparisons. - **Tests follow AAA** (Arrange, Act, Assert) with fixtures from `tests/fixtures/`. - **Backward compatibility:** Use deprecation warnings before removing public API features; document changes in CHANGELOG. + +# RocketPy fork instructions for Claude Code + +## What this repo is +RocketPy is a 6-DOF rocket trajectory simulator in pure Python. The main source +code lives in `rocketpy/`. Tests live in `tests/`. The default branch is `master`. + +## My workflow +- I am a solo contributor. Jules scouts issues and creates a branch. I hand off + to you (Claude Code) to build the implementation. +- Branches follow the pattern `scout/YYYY-MM-DD-brief-description`. +- Read `scout_report.md` in the repo root at the start of every session — it + contains the selected issue and any context Jules gathered. + +## Scope rules — read these carefully +- Only touch files directly relevant to the issue. Do not refactor unrelated code. +- Do not modify existing function signatures unless the issue explicitly requires it. +- Do not add type hints — the codebase does not use them. +- Do not rename variables or reformat code outside the files you are changing. +- If you think something adjacent should be fixed, mention it in a comment to me + instead of changing it. + +## Code conventions +- Docstrings: NumPy format only. Every new public function and class needs one. + Parameter types go in the docstring (e.g. `x : float`), not as type hints. + Do not document units in the type field — just the type name. +- No magic numbers. Any numeric constant needs a comment explaining what it is. +- Follow the style of the surrounding code exactly — indentation, spacing, naming. + +## Before you consider something done +- All new public functions have NumPy docstrings +- Tests written and passing +- No files modified outside the scope of the issue +- No type hints added anywhere +- Diff is clean — no stray whitespace changes, no reformatting of untouched lines + +## What to ask me before doing +- Any change to an existing public API +- Adding a new dependency +- Creating a new file that isn't a test or a direct implementation of the issue +- Anything that feels like it goes beyond the issue scopeß \ No newline at end of file diff --git a/jules_handoff.sh b/jules_handoff.sh index 0ff7644e2..1b2ac5463 100644 --- a/jules_handoff.sh +++ b/jules_handoff.sh @@ -1,19 +1,23 @@ #!/bin/bash # ───────────────────────────────────────────────────────── -# jules-handoff.sh -# Pulls a Jules branch and launches Claude Code with full context +# jules_handoff.sh +# Checks out a Jules scout branch and launches Claude Code with context # # Usage: -# ./jules-handoff.sh # interactive branch picker -# ./jules-handoff.sh # direct branch name -# ./jules-handoff.sh # paste a Jules PR URL +# ./jules_handoff.sh # interactive branch picker +# ./jules_handoff.sh # direct branch name +# ───────────────────────────────────────────────────────── + +# ── Config — only thing to change when reusing across repos +DEFAULT_BRANCH="master" +UPSTREAM_FALLBACK="https://github.com/RocketPy-Team/RocketPy.git" # ───────────────────────────────────────────────────────── set -e REPO_DIR=$(git rev-parse --show-toplevel 2>/dev/null) if [ -z "$REPO_DIR" ]; then - echo "❌ Not inside a git repo. Run this from your astropy fork directory." + echo "❌ Not inside a git repo." exit 1 fi @@ -23,34 +27,22 @@ cd "$REPO_DIR" UPSTREAM_URL=$(git remote get-url upstream 2>/dev/null || echo "") if [ -z "$UPSTREAM_URL" ]; then - echo "❌ No upstream remote found. Run: git remote add upstream " + echo "❌ No upstream remote found. Run: git remote add upstream $UPSTREAM_FALLBACK" exit 1 fi -# Parse owner/repo from URL (handles both https and ssh) -UPSTREAM_REPO=$(echo "$UPSTREAM_URL" | sed 's|https://github.com/||' | sed 's|git@github.com:||' | sed 's|[.]git$||') +UPSTREAM_REPO=$(echo "$UPSTREAM_URL" \ + | sed 's|https://github.com/||' \ + | sed 's|git@github.com:||' \ + | sed 's|[.]git$||') echo "📡 Upstream: $UPSTREAM_REPO" -# ── Step 1: Determine the Jules branch ─────────────────── +# ── Step 1: Determine the branch ───────────────────────── if [ -n "$1" ]; then - INPUT="$1" - - # If it's a GitHub PR URL, extract the branch from the API - if [[ "$INPUT" == https://github.com/* ]]; then - echo "🔍 Fetching branch from PR URL..." - PR_NUMBER=$(echo "$INPUT" | grep -oE '/pull/[0-9]+' | grep -oE '[0-9]+') - REPO_SLUG=$(echo "$INPUT" | sed 's|https://github.com/||' | cut -d'/' -f1-2) - BRANCH=$(curl -s "https://api.github.com/repos/$REPO_SLUG/pulls/$PR_NUMBER" \ - | python3 -c "import sys,json; print(json.load(sys.stdin)['head']['ref'])") - echo "📌 Branch: $BRANCH" - else - BRANCH="$INPUT" - fi - + BRANCH="$1" else - # Interactive: list recent Jules branches echo "" echo "🔍 Fetching recent branches from origin..." git fetch origin --quiet @@ -61,13 +53,13 @@ else BRANCHES=$(git branch -r --sort=-committerdate \ | grep 'origin/' \ - | grep -v 'origin/main' \ + | grep -v "origin/$DEFAULT_BRANCH" \ | sed 's|origin/||' \ | head -20) if [ -z "$BRANCHES" ]; then - echo "No branches found other than main." - echo "Jules may not have created a branch yet — check jules.google.com" + echo "No branches found other than $DEFAULT_BRANCH." + echo "Jules may not have created a branch yet." exit 1 fi @@ -89,7 +81,7 @@ else fi fi -# ── Step 2: Pull the branch locally ────────────────────── +# ── Step 2: Check out the branch ───────────────────────── echo "" echo "📥 Checking out branch: $BRANCH" @@ -105,167 +97,136 @@ fi echo "✅ On branch: $BRANCH" -# ── Step 3: Read and extract selected issue from scout_report.md ── +# ── Step 3: Read scout_report.md ───────────────────────── echo "" -echo "📋 Reading Jules scout report..." +echo "📋 Reading scout report..." SCOUT_REPORT="" +SELECTED="" + if [ -f "scout_report.md" ]; then - # Read the selected issue number from the SELECTED ISSUE line SELECTED=$(grep "SELECTED ISSUE:" scout_report.md | grep -oE '[0-9]+' | head -1) if [ -z "$SELECTED" ]; then echo "" - echo "👉 Open scout_report.md and fill in the SELECTED ISSUE number (1, 2, or 3)" - echo " Then re-run this script." + echo "👉 Open scout_report.md and fill in the SELECTED ISSUE number, then re-run." exit 0 fi echo "✅ Selected issue: #$SELECTED" - # Extract only the selected issue section - SCOUT_REPORT=$(python3 - "$SELECTED" << 'PYEOF' + SCOUT_REPORT=$(python3 << PYEOF import sys, re -selected = sys.argv[1] +selected = "$SELECTED" with open("scout_report.md") as f: text = f.read() -# Find the section matching "## Issue N" -pattern = rf"(## Issue {selected} —.*?)(?= -## Issue \d|$)" +pattern = r"(## Issue " + selected + r" \u2014.*?)(?=## Issue \d|\Z)" match = re.search(pattern, text, re.DOTALL) if match: print(match.group(1).strip()) else: - print(f"Could not find Issue {selected} in scout_report.md") + print(text) PYEOF ) - echo "✅ Extracted Issue $SELECTED from scout_report.md" + echo "✅ Extracted issue $SELECTED from scout_report.md" else - echo "⚠️ No scout_report.md found — will use PR description only" + echo "⚠️ No scout_report.md found on this branch" fi -# ── Step 4: Get PR info and extract issue number ────────── +# ── Step 4: Check for competing PRs ────────────────────── -PR_BODY="" -PR_TITLE="" -ISSUE_NUMBER="" +ISSUE_NUMBER="$SELECTED" ISSUE_STATUS="" +ISSUE_TITLE="" ISSUE_BODY="" -if command -v gh &> /dev/null; then - PR_JSON=$(gh pr list --head "$BRANCH" --json body,title,url 2>/dev/null || echo "[]") - PR_TITLE=$(echo "$PR_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d[0]['title'] if d else '')" 2>/dev/null || echo "") - PR_BODY=$(echo "$PR_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d[0]['body'] if d else '')" 2>/dev/null || echo "") - - # Try to extract issue number from PR body or branch name - ISSUE_NUMBER=$(echo "$PR_BODY $BRANCH" | grep -oE '#[0-9]+|issues/[0-9]+' | grep -oE '[0-9]+' | head -1) - - if [ -n "$ISSUE_NUMBER" ]; then - echo "🔍 Checking upstream issue #$ISSUE_NUMBER..." - ISSUE_JSON=$(gh issue view "$ISSUE_NUMBER" --repo "$UPSTREAM_REPO" --json state,title,body 2>/dev/null || echo "") - - if [ -n "$ISSUE_JSON" ]; then - ISSUE_STATUS=$(echo "$ISSUE_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('state','unknown'))") - ISSUE_TITLE=$(echo "$ISSUE_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('title',''))") - ISSUE_BODY=$(echo "$ISSUE_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('body','')[:800])") - echo "📌 Issue #$ISSUE_NUMBER is: $ISSUE_STATUS — $ISSUE_TITLE" - - if [ "$ISSUE_STATUS" = "closed" ]; then - echo "" - echo "⛔ Issue #$ISSUE_NUMBER is already CLOSED — someone already fixed it." - echo " Aborted. Pick a different Jules branch." - exit 0 - fi +if command -v gh &> /dev/null && [ -n "$ISSUE_NUMBER" ]; then + echo "🔍 Checking upstream issue #$ISSUE_NUMBER..." + ISSUE_JSON=$(gh issue view "$ISSUE_NUMBER" --repo "$UPSTREAM_REPO" --json state,title,body 2>/dev/null || echo "") + + if [ -n "$ISSUE_JSON" ]; then + ISSUE_STATUS=$(echo "$ISSUE_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('state','unknown'))") + ISSUE_TITLE=$(echo "$ISSUE_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('title',''))") + ISSUE_BODY=$(echo "$ISSUE_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('body','')[:800])") + echo "📌 Issue #$ISSUE_NUMBER: $ISSUE_STATUS — $ISSUE_TITLE" + + if [ "$ISSUE_STATUS" = "closed" ]; then + echo "" + echo "⛔ Issue #$ISSUE_NUMBER is already closed. Pick a different issue." + exit 0 fi + fi - # Check for competing open PRs - echo "🔍 Checking for competing PRs on issue #$ISSUE_NUMBER..." - COMPETING=$(gh pr list --repo "$UPSTREAM_REPO" --search "fixes #$ISSUE_NUMBER" --json number,title,state,isDraft,url 2>/dev/null || echo "[]") + echo "🔍 Checking for competing PRs..." + COMPETING=$(gh pr list \ + --repo "$UPSTREAM_REPO" \ + --search "fixes #$ISSUE_NUMBER" \ + --json number,title,isDraft,url \ + 2>/dev/null || echo "[]") - PR_COUNT=$(echo "$COMPETING" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))") + PR_COUNT=$(echo "$COMPETING" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))") - if [ "$PR_COUNT" -gt "0" ]; then - echo "" - echo "⛔ STOP — There is already an open PR for issue #$ISSUE_NUMBER:" - echo "$COMPETING" | python3 -c " + if [ "$PR_COUNT" -gt "0" ]; then + echo "" + echo "⛔ STOP — There is already an open PR for issue #$ISSUE_NUMBER:" + echo "$COMPETING" | python3 -c " import sys, json -prs = json.load(sys.stdin) -for pr in prs: +for pr in json.load(sys.stdin): draft = ' [DRAFT]' if pr.get('isDraft') else '' print(' #' + str(pr['number']) + draft + ': ' + pr['title']) print(' ' + pr['url']) " - echo "" - echo " Don't waste your time — pick a different Jules branch." - exit 0 - else - echo "✅ No competing PRs found — you're clear to proceed." - fi + echo "" + echo " Pick a different issue." + exit 0 + else + echo "✅ No competing PRs — you're clear to proceed." fi fi -# ── Step 5: Write Claude Code context file ──────────────── +# ── Step 5: Build context file and launch Claude Code ──── CONTEXT_FILE="/tmp/jules-context-$(date +%s).md" cat > "$CONTEXT_FILE" << CONTEXT -# Jules Handoff Context +# Jules Handoff — $UPSTREAM_REPO ## Branch $BRANCH -## Jules PR Title -${PR_TITLE:-"(not found)"} +## Selected Issue +#${ISSUE_NUMBER:-"unknown"} — ${ISSUE_TITLE:-"unknown"} +Status: ${ISSUE_STATUS:-"unknown"} --- -## Jules Scout Report (scout.md) -${SCOUT_REPORT:-"(No scout_report.md on this branch — refer to PR description below)"} +## Scout Report +${SCOUT_REPORT:-"(no scout_report.md found)"} --- -## Jules PR Description -${PR_BODY:-"(No PR found — check jules.google.com)"} +## Issue Body +${ISSUE_BODY:-"(could not fetch)"} --- -## Upstream Issue #${ISSUE_NUMBER:-"unknown"} -Status: ${ISSUE_STATUS:-"unknown"} -Title: ${ISSUE_TITLE:-"unknown"} - -${ISSUE_BODY:-"(Could not fetch issue body — check https://github.com/astropy/astropy/issues)"} - ---- +## Instructions for Claude Code -## Your job (Claude Code) +Read CLAUDE.md before doing anything else — it contains scope rules and +conventions you must follow. -The scout report and issue above are your source of truth — ignore any diff noise -from main being ahead of this branch due to the daily sync automation. - -1. Read scout.md carefully — Jules has already identified the files and approach. - -2. Check if the issue is still open and unassigned upstream before starting: - https://github.com/astropy/astropy/issues/${ISSUE_NUMBER:-""} - -3. Run the existing tests for the relevant subpackage first to establish a baseline: - \`python -m pytest astropy//tests/ -x -v\` - -4. Implement the fix described in scout.md. Follow astropy standards: - - Type hints on all new functions - - Numpy-style docstrings - - Tests in the corresponding tests/ directory - - No public API changes unless the issue explicitly requires it - -5. Once tests pass, give me a 3-sentence summary of what changed for the PR description. +1. Read the scout report and issue above carefully. +2. Run existing tests for the relevant module first to establish a baseline: + \`python -m pytest tests/ -x -q -k \` +3. Implement the fix. Stay strictly within the scope of the issue. +4. Write tests using pytest.approx for any float assertions. +5. Ensure all new public functions have NumPy docstrings. +6. When done, give me a 3-sentence summary of what changed for the PR description. CONTEXT -echo "✅ Context ready" - -# ── Step 6: Print context and launch Claude Code ────────── - echo "" echo "🚀 Launching Claude Code..." echo "" From def437cfb9380d9df7f622d7b5202490984ffbcb Mon Sep 17 00:00:00 2001 From: khushalkottaru Date: Mon, 16 Mar 2026 22:09:37 -0700 Subject: [PATCH 4/9] Fix: add wraparound logic for wind direction and related tests --- .coderabbit | 43 ---- .github/workflows/sync-upstream.yml | 46 ---- AGENTS.md | 0 CLAUDE.md | 112 --------- jules_handoff.sh | 234 ------------------ prep_pr.sh | 64 ----- rocketpy/plots/environment_plots.py | 27 +- .../environment/test_environment.py | 38 +++ 8 files changed, 61 insertions(+), 503 deletions(-) delete mode 100644 .coderabbit delete mode 100644 .github/workflows/sync-upstream.yml delete mode 100644 AGENTS.md delete mode 100644 CLAUDE.md delete mode 100644 jules_handoff.sh delete mode 100644 prep_pr.sh diff --git a/.coderabbit b/.coderabbit deleted file mode 100644 index 717968302..000000000 --- a/.coderabbit +++ /dev/null @@ -1,43 +0,0 @@ -# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json - -language: "en-US" - -reviews: - profile: "assertive" - poem: false - in_progress_fortune: false - - auto_review: - enabled: true - drafts: false - ignore_title_keywords: - - "wip" - - "do not review" - - path_instructions: - - path: "**/*.py" - instructions: | - This is RocketPy, a scientific Python rocket trajectory simulator. - - Docstrings must use NumPy format with parameter types (e.g. `x : float`) but no units in the type field - - Do not add type hints to function signatures — the codebase does not use them - - No magic numbers without a comment explaining what they represent - - Flag any new public function or class missing a NumPy docstring - - Flag any logic that duplicates existing RocketPy utilities - - - path: "tests/**" - instructions: | - Tests use pytest with pytest.approx for float comparisons, not plain == or np.isclose. - Flag any float assertion that does not use pytest.approx. - Test names should describe the scenario being tested, not just the function name. - - finishing_touches: - docstrings: - enabled: true - unit_tests: - enabled: true - -knowledge_base: - web_search: - enabled: true - learnings: - scope: auto \ No newline at end of file diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml deleted file mode 100644 index 061f1a812..000000000 --- a/.github/workflows/sync-upstream.yml +++ /dev/null @@ -1,46 +0,0 @@ -# .github/workflows/sync-upstream.yml -# Syncs your fork's default branch with the upstream repo daily. -# Also runs on manual trigger via workflow_dispatch. -# -# Setup: -# 1. Copy this file to .github/workflows/sync-upstream.yml in your fork -# 2. Set the UPSTREAM_REPO variable below to the upstream owner/repo - -name: Sync fork with upstream - -on: - schedule: - - cron: '0 12 * * *' # 5:00 AM PT daily - workflow_dispatch: # manual trigger from Actions tab - -env: - UPSTREAM_REPO: RocketPy-Team/RocketPy # change per fork - BRANCH: master # change if upstream uses 'main' - -jobs: - sync: - runs-on: ubuntu-latest - permissions: - contents: write - - steps: - - name: Checkout fork - uses: actions/checkout@v4 - with: - ref: ${{ env.BRANCH }} - fetch-depth: 0 - - - name: Add upstream remote - run: git remote add upstream https://github.com/${{ env.UPSTREAM_REPO }}.git - - - name: Fetch upstream - run: git fetch upstream ${{ env.BRANCH }} - - - name: Rebase on upstream - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git rebase upstream/${{ env.BRANCH }} - - - name: Push to fork - run: git push origin ${{ env.BRANCH }} --force-with-lease \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index d70b2e15c..000000000 --- a/CLAUDE.md +++ /dev/null @@ -1,112 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Commands - -```bash -# Install dependencies -make install - -# Run tests -make pytest # standard test suite -make pytest-slow # slow/marked tests with verbose output -make coverage # tests with coverage - -# Lint and format -make format # ruff format + isort -make lint # ruff + pylint -make ruff-lint # ruff only -make pylint # pylint only - -# Docs -make build-docs -``` - -**Run a single test file:** -```bash -pytest tests/unit/test_environment.py -v -``` - -**Run a single test by name:** -```bash -pytest tests/unit/test_environment.py::test_methodname_expectedbehaviour -v -``` - -## Architecture - -RocketPy simulates 6-DOF rocket trajectories. The core workflow is linear: - -``` -Environment → Motor → Rocket → Flight -``` - -**`rocketpy/environment/`** — Atmospheric models, weather data fetching (NOAA, Wyoming soundings, GFS forecasts). The `Environment` class (~116KB) is the entry point for atmospheric conditions. - -**`rocketpy/motors/`** — `Motor` is the base class. `SolidMotor`, `HybridMotor`, and `LiquidMotor` extend it. `Tank`, `TankGeometry`, and `Fluid` support liquid/hybrid propellant modeling. - -**`rocketpy/rocket/`** — `Rocket` aggregates a motor and aerodynamic surfaces. `aero_surface/` contains fins, nose cone, and tail implementations. `Parachute` uses trigger functions for deployment. - -**`rocketpy/simulation/`** — `Flight` (~162KB) is the simulation engine, integrating equations of motion with scipy's LSODA solver. `MonteCarlo` orchestrates many `Flight` runs for dispersion analysis. - -**`rocketpy/stochastic/`** — Wraps any component (Environment, Rocket, Motor, Flight) with uncertainty distributions for Monte Carlo input generation. - -**`rocketpy/mathutils/`** — `Function` class wraps callable data (arrays, lambdas, files) with interpolation and mathematical operations. Heavily used throughout for aerodynamic curves, thrust profiles, etc. - -**`rocketpy/plots/` and `rocketpy/prints/`** — Visualization and text output, each mirroring the module structure of the core classes. - -**`rocketpy/sensors/`** — Simulated sensors (accelerometer, gyroscope, barometer, GNSS) that can be attached to a `Rocket`. - -**`rocketpy/sensitivity/`** — Global sensitivity analysis via `SensitivityModel`. - -## Coding Standards - -- **Docstrings:** NumPy style with `Parameters`, `Returns`, and `Examples` sections. Always include units for physical quantities (e.g., "in meters", "in radians"). -- **No type hints in function signatures** — put types in the docstring `Parameters` section instead. -- **SI units by default** throughout the codebase (meters, kilograms, seconds, radians). -- **No magic numbers** — name constants with `UPPER_SNAKE_CASE` and comment their physical meaning. -- **Performance:** Use vectorized numpy operations. Cache expensive computations with `cached_property`. -- **Test names:** `test_methodname_expectedbehaviour` pattern. Use `pytest.approx` for float comparisons. -- **Tests follow AAA** (Arrange, Act, Assert) with fixtures from `tests/fixtures/`. -- **Backward compatibility:** Use deprecation warnings before removing public API features; document changes in CHANGELOG. - -# RocketPy fork instructions for Claude Code - -## What this repo is -RocketPy is a 6-DOF rocket trajectory simulator in pure Python. The main source -code lives in `rocketpy/`. Tests live in `tests/`. The default branch is `master`. - -## My workflow -- I am a solo contributor. Jules scouts issues and creates a branch. I hand off - to you (Claude Code) to build the implementation. -- Branches follow the pattern `scout/YYYY-MM-DD-brief-description`. -- Read `scout_report.md` in the repo root at the start of every session — it - contains the selected issue and any context Jules gathered. - -## Scope rules — read these carefully -- Only touch files directly relevant to the issue. Do not refactor unrelated code. -- Do not modify existing function signatures unless the issue explicitly requires it. -- Do not add type hints — the codebase does not use them. -- Do not rename variables or reformat code outside the files you are changing. -- If you think something adjacent should be fixed, mention it in a comment to me - instead of changing it. - -## Code conventions -- Docstrings: NumPy format only. Every new public function and class needs one. - Parameter types go in the docstring (e.g. `x : float`), not as type hints. - Do not document units in the type field — just the type name. -- No magic numbers. Any numeric constant needs a comment explaining what it is. -- Follow the style of the surrounding code exactly — indentation, spacing, naming. - -## Before you consider something done -- All new public functions have NumPy docstrings -- Tests written and passing -- No files modified outside the scope of the issue -- No type hints added anywhere -- Diff is clean — no stray whitespace changes, no reformatting of untouched lines - -## What to ask me before doing -- Any change to an existing public API -- Adding a new dependency -- Creating a new file that isn't a test or a direct implementation of the issue -- Anything that feels like it goes beyond the issue scopeß \ No newline at end of file diff --git a/jules_handoff.sh b/jules_handoff.sh deleted file mode 100644 index 1b2ac5463..000000000 --- a/jules_handoff.sh +++ /dev/null @@ -1,234 +0,0 @@ -#!/bin/bash -# ───────────────────────────────────────────────────────── -# jules_handoff.sh -# Checks out a Jules scout branch and launches Claude Code with context -# -# Usage: -# ./jules_handoff.sh # interactive branch picker -# ./jules_handoff.sh # direct branch name -# ───────────────────────────────────────────────────────── - -# ── Config — only thing to change when reusing across repos -DEFAULT_BRANCH="master" -UPSTREAM_FALLBACK="https://github.com/RocketPy-Team/RocketPy.git" -# ───────────────────────────────────────────────────────── - -set -e - -REPO_DIR=$(git rev-parse --show-toplevel 2>/dev/null) -if [ -z "$REPO_DIR" ]; then - echo "❌ Not inside a git repo." - exit 1 -fi - -cd "$REPO_DIR" - -# ── Detect upstream repo slug ───────────────────────────── - -UPSTREAM_URL=$(git remote get-url upstream 2>/dev/null || echo "") -if [ -z "$UPSTREAM_URL" ]; then - echo "❌ No upstream remote found. Run: git remote add upstream $UPSTREAM_FALLBACK" - exit 1 -fi - -UPSTREAM_REPO=$(echo "$UPSTREAM_URL" \ - | sed 's|https://github.com/||' \ - | sed 's|git@github.com:||' \ - | sed 's|[.]git$||') - -echo "📡 Upstream: $UPSTREAM_REPO" - -# ── Step 1: Determine the branch ───────────────────────── - -if [ -n "$1" ]; then - BRANCH="$1" -else - echo "" - echo "🔍 Fetching recent branches from origin..." - git fetch origin --quiet - - echo "" - echo "Recent branches (newest first):" - echo "──────────────────────────────" - - BRANCHES=$(git branch -r --sort=-committerdate \ - | grep 'origin/' \ - | grep -v "origin/$DEFAULT_BRANCH" \ - | sed 's|origin/||' \ - | head -20) - - if [ -z "$BRANCHES" ]; then - echo "No branches found other than $DEFAULT_BRANCH." - echo "Jules may not have created a branch yet." - exit 1 - fi - - i=1 - while IFS= read -r branch; do - echo " $i) $branch" - i=$((i+1)) - done <<< "$BRANCHES" - - echo "" - printf "Enter number of the Jules branch to work on: " - read -r SELECTION - - BRANCH=$(echo "$BRANCHES" | sed -n "${SELECTION}p" | xargs) - - if [ -z "$BRANCH" ]; then - echo "❌ Invalid selection" - exit 1 - fi -fi - -# ── Step 2: Check out the branch ───────────────────────── - -echo "" -echo "📥 Checking out branch: $BRANCH" - -git fetch origin "$BRANCH" --quiet - -if git show-ref --verify --quiet "refs/heads/$BRANCH"; then - git checkout "$BRANCH" - git pull origin "$BRANCH" --quiet -else - git checkout -b "$BRANCH" "origin/$BRANCH" -fi - -echo "✅ On branch: $BRANCH" - -# ── Step 3: Read scout_report.md ───────────────────────── - -echo "" -echo "📋 Reading scout report..." - -SCOUT_REPORT="" -SELECTED="" - -if [ -f "scout_report.md" ]; then - SELECTED=$(grep "SELECTED ISSUE:" scout_report.md | grep -oE '[0-9]+' | head -1) - - if [ -z "$SELECTED" ]; then - echo "" - echo "👉 Open scout_report.md and fill in the SELECTED ISSUE number, then re-run." - exit 0 - fi - - echo "✅ Selected issue: #$SELECTED" - - SCOUT_REPORT=$(python3 << PYEOF -import sys, re - -selected = "$SELECTED" -with open("scout_report.md") as f: - text = f.read() - -pattern = r"(## Issue " + selected + r" \u2014.*?)(?=## Issue \d|\Z)" -match = re.search(pattern, text, re.DOTALL) -if match: - print(match.group(1).strip()) -else: - print(text) -PYEOF -) - echo "✅ Extracted issue $SELECTED from scout_report.md" -else - echo "⚠️ No scout_report.md found on this branch" -fi - -# ── Step 4: Check for competing PRs ────────────────────── - -ISSUE_NUMBER="$SELECTED" -ISSUE_STATUS="" -ISSUE_TITLE="" -ISSUE_BODY="" - -if command -v gh &> /dev/null && [ -n "$ISSUE_NUMBER" ]; then - echo "🔍 Checking upstream issue #$ISSUE_NUMBER..." - ISSUE_JSON=$(gh issue view "$ISSUE_NUMBER" --repo "$UPSTREAM_REPO" --json state,title,body 2>/dev/null || echo "") - - if [ -n "$ISSUE_JSON" ]; then - ISSUE_STATUS=$(echo "$ISSUE_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('state','unknown'))") - ISSUE_TITLE=$(echo "$ISSUE_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('title',''))") - ISSUE_BODY=$(echo "$ISSUE_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('body','')[:800])") - echo "📌 Issue #$ISSUE_NUMBER: $ISSUE_STATUS — $ISSUE_TITLE" - - if [ "$ISSUE_STATUS" = "closed" ]; then - echo "" - echo "⛔ Issue #$ISSUE_NUMBER is already closed. Pick a different issue." - exit 0 - fi - fi - - echo "🔍 Checking for competing PRs..." - COMPETING=$(gh pr list \ - --repo "$UPSTREAM_REPO" \ - --search "fixes #$ISSUE_NUMBER" \ - --json number,title,isDraft,url \ - 2>/dev/null || echo "[]") - - PR_COUNT=$(echo "$COMPETING" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))") - - if [ "$PR_COUNT" -gt "0" ]; then - echo "" - echo "⛔ STOP — There is already an open PR for issue #$ISSUE_NUMBER:" - echo "$COMPETING" | python3 -c " -import sys, json -for pr in json.load(sys.stdin): - draft = ' [DRAFT]' if pr.get('isDraft') else '' - print(' #' + str(pr['number']) + draft + ': ' + pr['title']) - print(' ' + pr['url']) -" - echo "" - echo " Pick a different issue." - exit 0 - else - echo "✅ No competing PRs — you're clear to proceed." - fi -fi - -# ── Step 5: Build context file and launch Claude Code ──── - -CONTEXT_FILE="/tmp/jules-context-$(date +%s).md" - -cat > "$CONTEXT_FILE" << CONTEXT -# Jules Handoff — $UPSTREAM_REPO - -## Branch -$BRANCH - -## Selected Issue -#${ISSUE_NUMBER:-"unknown"} — ${ISSUE_TITLE:-"unknown"} -Status: ${ISSUE_STATUS:-"unknown"} - ---- - -## Scout Report -${SCOUT_REPORT:-"(no scout_report.md found)"} - ---- - -## Issue Body -${ISSUE_BODY:-"(could not fetch)"} - ---- - -## Instructions for Claude Code - -Read CLAUDE.md before doing anything else — it contains scope rules and -conventions you must follow. - -1. Read the scout report and issue above carefully. -2. Run existing tests for the relevant module first to establish a baseline: - \`python -m pytest tests/ -x -q -k \` -3. Implement the fix. Stay strictly within the scope of the issue. -4. Write tests using pytest.approx for any float assertions. -5. Ensure all new public functions have NumPy docstrings. -6. When done, give me a 3-sentence summary of what changed for the PR description. -CONTEXT - -echo "" -echo "🚀 Launching Claude Code..." -echo "" - -claude < "$CONTEXT_FILE" \ No newline at end of file diff --git a/prep_pr.sh b/prep_pr.sh deleted file mode 100644 index bac87e26b..000000000 --- a/prep_pr.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env bash -# prep_pr.sh — strips toolkit files from the current branch before opening a PR -# Usage: ./prep_pr.sh -# Run after cleaning up commit history. Modifies the current branch in place. - -set -euo pipefail - -TOOLKIT_FILES=( - "AGENTS.md" - "CLAUDE.md" - "jules_handoff.sh" - ".coderabbit.yaml" - "scout_report.md" - ".github/workflows/sync-upstream.yml" - ".github/workflows/scout.yml" -) - -RED='\033[0;31m'; YELLOW='\033[1;33m'; GREEN='\033[0;32m'; NC='\033[0m' -die() { echo -e "${RED}error: $1${NC}" >&2; exit 1; } -warn() { echo -e "${YELLOW}warn: $1${NC}"; } -ok() { echo -e "${GREEN}ok: $1${NC}"; } - -command -v git &>/dev/null || die "git not found" -git rev-parse --git-dir &>/dev/null || die "not inside a git repo" - -BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null) \ - || die "HEAD is detached — check out your working branch first" - -[[ "$BRANCH" == "master" || "$BRANCH" == "main" ]] \ - && die "you're on $BRANCH — check out your working branch first" - -git diff --quiet && git diff --cached --quiet \ - || die "you have uncommitted changes — commit or stash them first" - -echo "" -echo " Branch : $BRANCH" -echo " Will strip toolkit files and add a cleanup commit." -echo "" -read -rp "Proceed? [y/N] " confirm -[[ "${confirm,,}" == "y" ]] || { echo "Aborted."; exit 0; } - -STRIPPED=() -for f in "${TOOLKIT_FILES[@]}"; do - if git ls-files --error-unmatch "$f" &>/dev/null 2>&1; then - git rm -f "$f" - STRIPPED+=("$f") - fi -done - -if [[ ${#STRIPPED[@]} -eq 0 ]]; then - warn "no toolkit files found — nothing to strip" - exit 0 -fi - -git commit -m "chore: strip toolkit files before PR - -Local workflow helpers not intended for upstream: -$(printf ' - %s\n' "${STRIPPED[@]}")" - -ok "stripped: ${STRIPPED[*]}" -echo "" -echo " Next:" -echo " git push origin $BRANCH" -echo " open PR from $BRANCH → upstream master" \ No newline at end of file diff --git a/rocketpy/plots/environment_plots.py b/rocketpy/plots/environment_plots.py index 4b8a91e15..7fcee0731 100644 --- a/rocketpy/plots/environment_plots.py +++ b/rocketpy/plots/environment_plots.py @@ -55,9 +55,19 @@ def __wind(self, ax): ax.set_xlabel("Wind Speed (m/s)", color="#ff7f0e") ax.tick_params("x", colors="#ff7f0e") axup = ax.twiny() + directions = np.array( + [self.environment.wind_direction(i) for i in self.grid], dtype=float + ) + altitudes = np.array(self.grid, dtype=float) + # Insert NaN where direction jumps by more than 180° (360°→0° wraparound) + # so matplotlib does not draw a horizontal line across the plot. + WRAP_THRESHOLD = 180 # degrees; half the full circle + wrap_indices = np.where(np.abs(np.diff(directions)) > WRAP_THRESHOLD)[0] + 1 + directions = np.insert(directions, wrap_indices, np.nan) + altitudes = np.insert(altitudes, wrap_indices, np.nan) axup.plot( - [self.environment.wind_direction(i) for i in self.grid], - self.grid, + directions, + altitudes, color="#1f77b4", label="Wind Direction", ) @@ -309,11 +319,20 @@ def ensemble_member_comparison(self, *, filename=None): # Create wind direction subplot ax8 = plt.subplot(324) + WRAP_THRESHOLD = 180 # degrees; half the full circle for i in range(self.environment.num_ensemble_members): self.environment.select_ensemble_member(i) + dirs = np.array( + [self.environment.wind_direction(j) for j in self.grid], dtype=float + ) + alts = np.array(self.grid, dtype=float) + # Insert NaN at wraparound points to avoid crossing lines + wrap_idx = np.where(np.abs(np.diff(dirs)) > WRAP_THRESHOLD)[0] + 1 + dirs = np.insert(dirs, wrap_idx, np.nan) + alts = np.insert(alts, wrap_idx, np.nan) ax8.plot( - [self.environment.wind_direction(i) for i in self.grid], - self.grid, + dirs, + alts, label=i, ) ax8.set_ylabel("Height Above Sea Level (m)") diff --git a/tests/integration/environment/test_environment.py b/tests/integration/environment/test_environment.py index 3bdd5209a..0f353c33a 100644 --- a/tests/integration/environment/test_environment.py +++ b/tests/integration/environment/test_environment.py @@ -2,6 +2,7 @@ from datetime import date, datetime, timezone from unittest.mock import patch +import numpy as np import pytest @@ -92,6 +93,43 @@ def test_standard_atmosphere(mock_show, example_plain_env): # pylint: disable=u assert example_plain_env.prints.print_earth_details() is None +@patch("matplotlib.pyplot.show") +def test_wind_plots_wrapping_direction(mock_show, example_plain_env): # pylint: disable=unused-argument + """Tests that wind direction plots handle 360°→0° wraparound without + drawing a horizontal line across the graph. + + Parameters + ---------- + mock_show : mock + Mock object to replace matplotlib.pyplot.show() method. + example_plain_env : rocketpy.Environment + Example environment object to be tested. + """ + # Set a custom atmosphere where wind direction wraps from ~350° to ~10° + # across the altitude range by choosing wind_u and wind_v to create a + # direction near 350° at low altitude and ~10° at higher altitude. + # wind_direction = (180 + atan2(wind_u, wind_v)) % 360 + # For direction ~350°: need atan2(wind_u, wind_v) ≈ 170° → wind_u>0, wind_v<0 + # For direction ~10°: need atan2(wind_u, wind_v) ≈ -170° → wind_u<0, wind_v<0 + example_plain_env.set_atmospheric_model( + type="custom_atmosphere", + pressure=None, + temperature=300, + wind_u=[(0, 1), (5000, -1)], # changes sign across altitude + wind_v=[(0, -6), (5000, -6)], # stays negative → heading near 350°/10° + ) + # Verify that the wind direction actually wraps through 0°/360° in this + # atmosphere so the test exercises the wraparound code path. + low_dir = example_plain_env.wind_direction(0) + high_dir = example_plain_env.wind_direction(5000) + assert abs(low_dir - high_dir) > 180, ( + "Test setup error: wind direction should cross 0°/360° boundary" + ) + # Verify info() and atmospheric_model() plots complete without error + assert example_plain_env.info() is None + assert example_plain_env.plots.atmospheric_model() is None + + @pytest.mark.parametrize( "model_name", [ From bb7b2e32215e75d9e7ee1e7f64d63486fefcacf8 Mon Sep 17 00:00:00 2001 From: khushalkottaru Date: Tue, 17 Mar 2026 11:08:18 -0700 Subject: [PATCH 5/9] style: fix ruff formatting --- tests/integration/environment/test_environment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/environment/test_environment.py b/tests/integration/environment/test_environment.py index 0f353c33a..06de5bcf1 100644 --- a/tests/integration/environment/test_environment.py +++ b/tests/integration/environment/test_environment.py @@ -115,7 +115,7 @@ def test_wind_plots_wrapping_direction(mock_show, example_plain_env): # pylint: type="custom_atmosphere", pressure=None, temperature=300, - wind_u=[(0, 1), (5000, -1)], # changes sign across altitude + wind_u=[(0, 1), (5000, -1)], # changes sign across altitude wind_v=[(0, -6), (5000, -6)], # stays negative → heading near 350°/10° ) # Verify that the wind direction actually wraps through 0°/360° in this From fbb276467cbda18d3164f0c3ee3abf507cb63fdf Mon Sep 17 00:00:00 2001 From: Khushal Kottaru Date: Tue, 17 Mar 2026 18:53:07 -0700 Subject: [PATCH 6/9] Remove unused import Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- tests/integration/environment/test_environment.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration/environment/test_environment.py b/tests/integration/environment/test_environment.py index 06de5bcf1..bb8d0ef08 100644 --- a/tests/integration/environment/test_environment.py +++ b/tests/integration/environment/test_environment.py @@ -2,7 +2,6 @@ from datetime import date, datetime, timezone from unittest.mock import patch -import numpy as np import pytest From 4590c60247f6bc3ffd39ee3e8a88d7a14ab6e46f Mon Sep 17 00:00:00 2001 From: khushalkottaru Date: Tue, 17 Mar 2026 23:01:24 -0700 Subject: [PATCH 7/9] refactor: move repetitive logic into helper method --- rocketpy/plots/environment_plots.py | 37 ++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/rocketpy/plots/environment_plots.py b/rocketpy/plots/environment_plots.py index 7fcee0731..f53cecc1b 100644 --- a/rocketpy/plots/environment_plots.py +++ b/rocketpy/plots/environment_plots.py @@ -33,6 +33,30 @@ def __init__(self, environment): self.grid = np.linspace(environment.elevation, environment.max_expected_height) self.environment = environment + def _break_direction_wraparound(self, directions, altitudes): + """Inserts NaN into direction and altitude arrays at 0°/360° wraparound + points so matplotlib does not draw a horizontal line across the plot. + + Parameters + ---------- + directions : numpy.ndarray + Wind direction values in degrees, dtype float. + altitudes : numpy.ndarray + Altitude values corresponding to each direction, dtype float. + + Returns + ------- + directions : numpy.ndarray + Direction array with NaN inserted at wraparound points. + altitudes : numpy.ndarray + Altitude array with NaN inserted at wraparound points. + """ + WRAP_THRESHOLD = 180 # degrees; half the full circle + wrap_indices = np.where(np.abs(np.diff(directions)) > WRAP_THRESHOLD)[0] + 1 + directions = np.insert(directions, wrap_indices, np.nan) + altitudes = np.insert(altitudes, wrap_indices, np.nan) + return directions, altitudes + def __wind(self, ax): """Adds wind speed and wind direction graphs to the same axis. @@ -59,12 +83,7 @@ def __wind(self, ax): [self.environment.wind_direction(i) for i in self.grid], dtype=float ) altitudes = np.array(self.grid, dtype=float) - # Insert NaN where direction jumps by more than 180° (360°→0° wraparound) - # so matplotlib does not draw a horizontal line across the plot. - WRAP_THRESHOLD = 180 # degrees; half the full circle - wrap_indices = np.where(np.abs(np.diff(directions)) > WRAP_THRESHOLD)[0] + 1 - directions = np.insert(directions, wrap_indices, np.nan) - altitudes = np.insert(altitudes, wrap_indices, np.nan) + directions, altitudes = self._break_direction_wraparound(directions, altitudes) axup.plot( directions, altitudes, @@ -319,17 +338,13 @@ def ensemble_member_comparison(self, *, filename=None): # Create wind direction subplot ax8 = plt.subplot(324) - WRAP_THRESHOLD = 180 # degrees; half the full circle for i in range(self.environment.num_ensemble_members): self.environment.select_ensemble_member(i) dirs = np.array( [self.environment.wind_direction(j) for j in self.grid], dtype=float ) alts = np.array(self.grid, dtype=float) - # Insert NaN at wraparound points to avoid crossing lines - wrap_idx = np.where(np.abs(np.diff(dirs)) > WRAP_THRESHOLD)[0] + 1 - dirs = np.insert(dirs, wrap_idx, np.nan) - alts = np.insert(alts, wrap_idx, np.nan) + dirs, alts = self._break_direction_wraparound(dirs, alts) ax8.plot( dirs, alts, From 891a8648041babe9ac59cb2d8c4a1c58a70709db Mon Sep 17 00:00:00 2001 From: khushalkottaru Date: Tue, 17 Mar 2026 23:02:11 -0700 Subject: [PATCH 8/9] fix: update test logic in test_environment --- .../integration/environment/test_environment.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/integration/environment/test_environment.py b/tests/integration/environment/test_environment.py index bb8d0ef08..d919c535d 100644 --- a/tests/integration/environment/test_environment.py +++ b/tests/integration/environment/test_environment.py @@ -124,6 +124,22 @@ def test_wind_plots_wrapping_direction(mock_show, example_plain_env): # pylint: assert abs(low_dir - high_dir) > 180, ( "Test setup error: wind direction should cross 0°/360° boundary" ) + # Verify that the helper inserts NaN breaks into the direction and altitude + # arrays at the wraparound point, which is the core of the fix. + directions = np.array( + [example_plain_env.wind_direction(i) for i in example_plain_env.plots.grid], + dtype=float, + ) + altitudes = np.array(example_plain_env.plots.grid, dtype=float) + directions_broken, altitudes_broken = ( + example_plain_env.plots._break_direction_wraparound(directions, altitudes) + ) + assert np.any(np.isnan(directions_broken)), ( + "Expected NaN breaks in direction array at 0°/360° wraparound" + ) + assert np.any(np.isnan(altitudes_broken)), ( + "Expected NaN breaks in altitude array at 0°/360° wraparound" + ) # Verify info() and atmospheric_model() plots complete without error assert example_plain_env.info() is None assert example_plain_env.plots.atmospheric_model() is None From 88c931658e9fe0ed062fb2f5df9bed7f9eaa5a19 Mon Sep 17 00:00:00 2001 From: khushalkottaru Date: Sat, 21 Mar 2026 23:22:41 -0700 Subject: [PATCH 9/9] add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e46ee3faa..3129c25b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,7 @@ Attention: The newest changes should be on top --> ### Fixed +- BUG: Add wraparound logic for wind direction in environment plots [#939](https://github.com/RocketPy-Team/RocketPy/pull/939) - BUG: Fix hard-coded radius value for parachute added mass calculation [#889](https://github.com/RocketPy-Team/RocketPy/pull/889) - DOC: Fix documentation build [#908](https://github.com/RocketPy-Team/RocketPy/pull/908) - BUG: energy_data plot not working for 3 dof sims [[#906](https://github.com/RocketPy-Team/RocketPy/issues/906)]